fix: add process tracking and cancellation support to command execution
This commit is contained in:
@@ -178,13 +178,15 @@ def _task_delete(screen: LogScreen, remove_ros: bool, remove_webots: bool) -> No
|
|||||||
screen.set_progress(88, "Removing project directory...")
|
screen.set_progress(88, "Removing project directory...")
|
||||||
_remove_project_dir(screen.write)
|
_remove_project_dir(screen.write)
|
||||||
|
|
||||||
screen.set_progress(100, "Done")
|
if not screen.is_stopped():
|
||||||
screen.write("\n[green]Project fully removed.[/green]")
|
screen.set_progress(100, "Done")
|
||||||
screen.finish(True)
|
screen.write("\n[green]Project fully removed.[/green]")
|
||||||
|
screen.finish(True)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
screen.write(f"\n[red]Error:[/red] {exc}")
|
if not screen.is_stopped():
|
||||||
screen.finish(False)
|
screen.write(f"\n[red]Error:[/red] {exc}")
|
||||||
|
screen.finish(False)
|
||||||
|
|
||||||
|
|
||||||
# Multi-step confirmation wizard before anything is deleted.
|
# Multi-step confirmation wizard before anything is deleted.
|
||||||
|
|||||||
+49
-15
@@ -53,6 +53,7 @@ def _image_exists() -> bool:
|
|||||||
def _build_docs_image(
|
def _build_docs_image(
|
||||||
write: Write,
|
write: Write,
|
||||||
on_progress: Optional[Callable[[float], None]] = None,
|
on_progress: Optional[Callable[[float], None]] = None,
|
||||||
|
register_proc: Optional[Callable] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
write("[cyan][*][/cyan] Building documentation image (runs once)...")
|
write("[cyan][*][/cyan] Building documentation image (runs once)...")
|
||||||
# DOCKER_BUILDKIT=0 gives us "Step X/Y" lines that we can parse for progress.
|
# DOCKER_BUILDKIT=0 gives us "Step X/Y" lines that we can parse for progress.
|
||||||
@@ -62,6 +63,8 @@ def _build_docs_image(
|
|||||||
["docker", "build", "-t", _IMAGE_NAME, str(_DOC_DIR)],
|
["docker", "build", "-t", _IMAGE_NAME, str(_DOC_DIR)],
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env,
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env,
|
||||||
)
|
)
|
||||||
|
if register_proc:
|
||||||
|
register_proc(proc)
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
s = line.rstrip()
|
s = line.rstrip()
|
||||||
if s:
|
if s:
|
||||||
@@ -72,6 +75,8 @@ def _build_docs_image(
|
|||||||
step, total = int(m.group(1)), int(m.group(2))
|
step, total = int(m.group(1)), int(m.group(2))
|
||||||
on_progress(step / total * 100)
|
on_progress(step / total * 100)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
|
if proc.returncode in (-9, -15):
|
||||||
|
return False
|
||||||
if proc.returncode == 0:
|
if proc.returncode == 0:
|
||||||
write("[green][ok][/green] Documentation image ready")
|
write("[green][ok][/green] Documentation image ready")
|
||||||
return True
|
return True
|
||||||
@@ -86,25 +91,34 @@ def _task_up(screen: LogScreen, port: str) -> None:
|
|||||||
if _is_running():
|
if _is_running():
|
||||||
screen.write(f"[green]Docs already running at:[/green] http://localhost:{port}")
|
screen.write(f"[green]Docs already running at:[/green] http://localhost:{port}")
|
||||||
screen.write(" Stop with: [bold]cobot doc-setup down[/bold]")
|
screen.write(" Stop with: [bold]cobot doc-setup down[/bold]")
|
||||||
screen.finish(True)
|
if not screen.is_stopped():
|
||||||
|
screen.finish(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not _DOC_DIR.exists():
|
if not _DOC_DIR.exists():
|
||||||
screen.write(f"[red]Doc directory not found:[/red] {_DOC_DIR}")
|
screen.write(f"[red]Doc directory not found:[/red] {_DOC_DIR}")
|
||||||
screen.finish(False)
|
if not screen.is_stopped():
|
||||||
|
screen.finish(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not _image_exists():
|
if not _image_exists():
|
||||||
screen.set_progress(0, "Building documentation image...")
|
screen.set_progress(0, "Building documentation image...")
|
||||||
if not _build_docs_image(
|
ok = _build_docs_image(
|
||||||
screen.write,
|
screen.write,
|
||||||
on_progress=lambda p: screen.set_progress(p * 0.85, "Building documentation image..."),
|
on_progress=lambda p: screen.set_progress(p * 0.85, "Building documentation image..."),
|
||||||
):
|
register_proc=screen.set_proc,
|
||||||
|
)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
if not ok:
|
||||||
screen.finish(False)
|
screen.finish(False)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
screen.write("[dim]Documentation image already built, skipping.[/dim]")
|
screen.write("[dim]Documentation image already built, skipping.[/dim]")
|
||||||
|
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
|
||||||
screen.set_progress(88, "Starting MkDocs server...")
|
screen.set_progress(88, "Starting MkDocs server...")
|
||||||
screen.write("\n[cyan][*][/cyan] Starting MkDocs server...")
|
screen.write("\n[cyan][*][/cyan] Starting MkDocs server...")
|
||||||
result = _docker(
|
result = _docker(
|
||||||
@@ -116,6 +130,8 @@ def _task_up(screen: LogScreen, port: str) -> None:
|
|||||||
_IMAGE_NAME, "serve", "--dev-addr=0.0.0.0:8000",
|
_IMAGE_NAME, "serve", "--dev-addr=0.0.0.0:8000",
|
||||||
capture=True,
|
capture=True,
|
||||||
)
|
)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
screen.write(f"[red]Failed to start container.[/red]\n{result.stderr}")
|
screen.write(f"[red]Failed to start container.[/red]\n{result.stderr}")
|
||||||
screen.finish(False)
|
screen.finish(False)
|
||||||
@@ -125,11 +141,13 @@ def _task_up(screen: LogScreen, port: str) -> None:
|
|||||||
screen.write(f"\n[green]Docs running at:[/green] http://localhost:{port}")
|
screen.write(f"\n[green]Docs running at:[/green] http://localhost:{port}")
|
||||||
screen.write(" Edit files in [bold]doc/lwc-doc/docs/[/bold] — reloads automatically.")
|
screen.write(" Edit files in [bold]doc/lwc-doc/docs/[/bold] — reloads automatically.")
|
||||||
screen.write(" Stop with: [bold]cobot doc-setup down[/bold]")
|
screen.write(" Stop with: [bold]cobot doc-setup down[/bold]")
|
||||||
screen.finish(True)
|
if not screen.is_stopped():
|
||||||
|
screen.finish(True)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
screen.write(f"[red]Error:[/red] {exc}")
|
if not screen.is_stopped():
|
||||||
screen.finish(False)
|
screen.write(f"[red]Error:[/red] {exc}")
|
||||||
|
screen.finish(False)
|
||||||
|
|
||||||
|
|
||||||
# Stop the running docs container.
|
# Stop the running docs container.
|
||||||
@@ -138,17 +156,21 @@ def _task_down(screen: LogScreen) -> None:
|
|||||||
try:
|
try:
|
||||||
if not _is_running():
|
if not _is_running():
|
||||||
screen.write("[yellow]Docs container is not running.[/yellow]")
|
screen.write("[yellow]Docs container is not running.[/yellow]")
|
||||||
screen.finish(True)
|
if not screen.is_stopped():
|
||||||
|
screen.finish(True)
|
||||||
return
|
return
|
||||||
screen.set_progress(30, "Stopping container...")
|
screen.set_progress(30, "Stopping container...")
|
||||||
screen.write("[cyan][*][/cyan] Stopping documentation server...")
|
screen.write("[cyan][*][/cyan] Stopping documentation server...")
|
||||||
_docker("stop", _CONTAINER_NAME)
|
_docker("stop", _CONTAINER_NAME)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
screen.set_progress(100, "Done")
|
screen.set_progress(100, "Done")
|
||||||
screen.write("[green][ok][/green] Container stopped.")
|
screen.write("[green][ok][/green] Container stopped.")
|
||||||
screen.finish(True)
|
screen.finish(True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
screen.write(f"[red]Error:[/red] {exc}")
|
if not screen.is_stopped():
|
||||||
screen.finish(False)
|
screen.write(f"[red]Error:[/red] {exc}")
|
||||||
|
screen.finish(False)
|
||||||
|
|
||||||
|
|
||||||
# Stop the container, remove the old image, rebuild it, and start a new container.
|
# Stop the container, remove the old image, rebuild it, and start a new container.
|
||||||
@@ -159,19 +181,27 @@ def _task_rebuild(screen: LogScreen, port: str) -> None:
|
|||||||
screen.set_progress(5, "Stopping container...")
|
screen.set_progress(5, "Stopping container...")
|
||||||
screen.write("[cyan][*][/cyan] Stopping existing container...")
|
screen.write("[cyan][*][/cyan] Stopping existing container...")
|
||||||
_docker("stop", _CONTAINER_NAME)
|
_docker("stop", _CONTAINER_NAME)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
screen.write("[green][ok][/green] Stopped.")
|
screen.write("[green][ok][/green] Stopped.")
|
||||||
|
|
||||||
if _image_exists():
|
if _image_exists():
|
||||||
screen.set_progress(15, "Removing old image...")
|
screen.set_progress(15, "Removing old image...")
|
||||||
screen.write("[cyan][*][/cyan] Removing old image...")
|
screen.write("[cyan][*][/cyan] Removing old image...")
|
||||||
_docker("rmi", "-f", _IMAGE_NAME)
|
_docker("rmi", "-f", _IMAGE_NAME)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
screen.write("[green][ok][/green] Image removed.")
|
screen.write("[green][ok][/green] Image removed.")
|
||||||
|
|
||||||
screen.set_progress(20, "Building documentation image...")
|
screen.set_progress(20, "Building documentation image...")
|
||||||
if not _build_docs_image(
|
ok = _build_docs_image(
|
||||||
screen.write,
|
screen.write,
|
||||||
on_progress=lambda p: screen.set_progress(20 + p * 0.68, "Building documentation image..."),
|
on_progress=lambda p: screen.set_progress(20 + p * 0.68, "Building documentation image..."),
|
||||||
):
|
register_proc=screen.set_proc,
|
||||||
|
)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
if not ok:
|
||||||
screen.finish(False)
|
screen.finish(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -184,6 +214,8 @@ def _task_rebuild(screen: LogScreen, port: str) -> None:
|
|||||||
_IMAGE_NAME, "serve", "--dev-addr=0.0.0.0:8000",
|
_IMAGE_NAME, "serve", "--dev-addr=0.0.0.0:8000",
|
||||||
capture=True,
|
capture=True,
|
||||||
)
|
)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
screen.write(f"[red]Failed to start container.[/red]\n{result.stderr}")
|
screen.write(f"[red]Failed to start container.[/red]\n{result.stderr}")
|
||||||
screen.finish(False)
|
screen.finish(False)
|
||||||
@@ -192,11 +224,13 @@ def _task_rebuild(screen: LogScreen, port: str) -> None:
|
|||||||
screen.set_progress(100, "Server running")
|
screen.set_progress(100, "Server running")
|
||||||
screen.write(f"\n[green]Docs running at:[/green] http://localhost:{port}")
|
screen.write(f"\n[green]Docs running at:[/green] http://localhost:{port}")
|
||||||
screen.write(" Stop with: [bold]cobot doc-setup down[/bold]")
|
screen.write(" Stop with: [bold]cobot doc-setup down[/bold]")
|
||||||
screen.finish(True)
|
if not screen.is_stopped():
|
||||||
|
screen.finish(True)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
screen.write(f"[red]Error:[/red] {exc}")
|
if not screen.is_stopped():
|
||||||
screen.finish(False)
|
screen.write(f"[red]Error:[/red] {exc}")
|
||||||
|
screen.finish(False)
|
||||||
|
|
||||||
|
|
||||||
# One app handles all three actions (up/down/rebuild) by branching in on_mount.
|
# One app handles all three actions (up/down/rebuild) by branching in on_mount.
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ def _build_image(
|
|||||||
on_progress: Optional[Callable[[float], None]] = None,
|
on_progress: Optional[Callable[[float], None]] = None,
|
||||||
parent_tag: Optional[str] = None,
|
parent_tag: Optional[str] = None,
|
||||||
build_type: str = "release",
|
build_type: str = "release",
|
||||||
|
register_proc: Optional[Callable] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
write(f"[cyan][*][/cyan] Building [bold]{name}[/bold]...")
|
write(f"[cyan][*][/cyan] Building [bold]{name}[/bold]...")
|
||||||
# DOCKER_BUILDKIT=0 gives us "Step X/Y" lines in the output which we parse for progress.
|
# DOCKER_BUILDKIT=0 gives us "Step X/Y" lines in the output which we parse for progress.
|
||||||
@@ -84,6 +85,8 @@ def _build_image(
|
|||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env,
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env,
|
||||||
)
|
)
|
||||||
|
if register_proc:
|
||||||
|
register_proc(proc)
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
s = line.rstrip()
|
s = line.rstrip()
|
||||||
if s:
|
if s:
|
||||||
@@ -94,6 +97,8 @@ def _build_image(
|
|||||||
step, total = int(m.group(1)), int(m.group(2))
|
step, total = int(m.group(1)), int(m.group(2))
|
||||||
on_progress(step / total * 100)
|
on_progress(step / total * 100)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
|
if proc.returncode in (-9, -15):
|
||||||
|
return False
|
||||||
if proc.returncode == 0:
|
if proc.returncode == 0:
|
||||||
write(f"[green][ok][/green] {name}")
|
write(f"[green][ok][/green] {name}")
|
||||||
return True
|
return True
|
||||||
@@ -102,12 +107,13 @@ def _build_image(
|
|||||||
|
|
||||||
|
|
||||||
# Pull a Docker image from Hub and track progress by counting downloaded layers.
|
# Pull a Docker image from Hub and track progress by counting downloaded layers.
|
||||||
# Скачиваем Docker-образ с Hub и отслеживаем прогресс по количеству скачанных слоёв.
|
# Скачиваем Docker-образ с Hub и отслеживаем прогресс по количеством скачанных слоёв.
|
||||||
def _pull_image(
|
def _pull_image(
|
||||||
name: str,
|
name: str,
|
||||||
tag: str,
|
tag: str,
|
||||||
write: Write,
|
write: Write,
|
||||||
on_progress: Optional[Callable[[float], None]] = None,
|
on_progress: Optional[Callable[[float], None]] = None,
|
||||||
|
register_proc: Optional[Callable] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
write(f"[cyan][*][/cyan] Pulling [bold]{name}[/bold] ({tag})...")
|
write(f"[cyan][*][/cyan] Pulling [bold]{name}[/bold] ({tag})...")
|
||||||
if on_progress:
|
if on_progress:
|
||||||
@@ -117,6 +123,8 @@ def _pull_image(
|
|||||||
["docker", "pull", tag],
|
["docker", "pull", tag],
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||||
)
|
)
|
||||||
|
if register_proc:
|
||||||
|
register_proc(proc)
|
||||||
layers_total = 0
|
layers_total = 0
|
||||||
layers_done = 0
|
layers_done = 0
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
@@ -132,6 +140,8 @@ def _pull_image(
|
|||||||
if on_progress and layers_total > 0:
|
if on_progress and layers_total > 0:
|
||||||
on_progress(5 + layers_done / layers_total * 90)
|
on_progress(5 + layers_done / layers_total * 90)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
|
if proc.returncode in (-9, -15):
|
||||||
|
return False
|
||||||
if proc.returncode == 0:
|
if proc.returncode == 0:
|
||||||
write(f"[green][ok][/green] {name}")
|
write(f"[green][ok][/green] {name}")
|
||||||
if on_progress:
|
if on_progress:
|
||||||
@@ -156,6 +166,8 @@ def _task_execute(screen: LogScreen, cfg: _Config) -> None:
|
|||||||
f"ROS {cfg.ros_version} — {cfg.build_type}[/bold]\n"
|
f"ROS {cfg.ros_version} — {cfg.build_type}[/bold]\n"
|
||||||
)
|
)
|
||||||
for i, name in enumerate(chain):
|
for i, name in enumerate(chain):
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
lo = i / n * 100
|
lo = i / n * 100
|
||||||
hi = (i + 1) / n * 100
|
hi = (i + 1) / n * 100
|
||||||
screen.set_progress(lo, f"Image {i + 1}/{n}: building {name}...")
|
screen.set_progress(lo, f"Image {i + 1}/{n}: building {name}...")
|
||||||
@@ -164,7 +176,8 @@ def _task_execute(screen: LogScreen, cfg: _Config) -> None:
|
|||||||
dockerfile = _DOCKER_DIR / cfg.ros_version / name / "Dockerfile"
|
dockerfile = _DOCKER_DIR / cfg.ros_version / name / "Dockerfile"
|
||||||
if not dockerfile.exists():
|
if not dockerfile.exists():
|
||||||
screen.write(f"[red]Dockerfile not found:[/red] {dockerfile}")
|
screen.write(f"[red]Dockerfile not found:[/red] {dockerfile}")
|
||||||
screen.finish(False)
|
if not screen.is_stopped():
|
||||||
|
screen.finish(False)
|
||||||
return
|
return
|
||||||
ctx = _PROJECT_DIR if name in _NEEDS_PROJECT_CTX else dockerfile.parent
|
ctx = _PROJECT_DIR if name in _NEEDS_PROJECT_CTX else dockerfile.parent
|
||||||
parent_name = _IMAGE_PARENT.get(name)
|
parent_name = _IMAGE_PARENT.get(name)
|
||||||
@@ -172,23 +185,28 @@ def _task_execute(screen: LogScreen, cfg: _Config) -> None:
|
|||||||
f"{cfg.image_prefix}:{parent_name}-{cfg.ros_version}"
|
f"{cfg.image_prefix}:{parent_name}-{cfg.ros_version}"
|
||||||
if parent_name else None
|
if parent_name else None
|
||||||
)
|
)
|
||||||
if not _build_image(
|
ok = _build_image(
|
||||||
name, tag, dockerfile, ctx, screen.write,
|
name, tag, dockerfile, ctx, screen.write,
|
||||||
on_progress=lambda p, lo=lo, hi=hi: screen.set_progress(
|
on_progress=lambda p, lo=lo, hi=hi: screen.set_progress(
|
||||||
lo + p * (hi - lo) / 100, f"Image {i + 1}/{n}: building {name}..."
|
lo + p * (hi - lo) / 100, f"Image {i + 1}/{n}: building {name}..."
|
||||||
),
|
),
|
||||||
parent_tag=parent_tag,
|
parent_tag=parent_tag,
|
||||||
build_type=cfg.build_type,
|
build_type=cfg.build_type,
|
||||||
):
|
register_proc=screen.set_proc,
|
||||||
|
)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
if not ok:
|
||||||
screen.finish(False)
|
screen.finish(False)
|
||||||
return
|
return
|
||||||
screen.set_progress(hi)
|
screen.set_progress(hi)
|
||||||
|
|
||||||
screen.set_progress(100, "All images built")
|
if not screen.is_stopped():
|
||||||
screen.write(
|
screen.set_progress(100, "All images built")
|
||||||
f"\n[green]Done.[/green] "
|
screen.write(
|
||||||
f"Images tagged [bold]{cfg.image_prefix}:<name>-{cfg.ros_version}[/bold]."
|
f"\n[green]Done.[/green] "
|
||||||
)
|
f"Images tagged [bold]{cfg.image_prefix}:<name>-{cfg.ros_version}[/bold]."
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
short = "webots" if cfg.variant == "webots" else "iiwa"
|
short = "webots" if cfg.variant == "webots" else "iiwa"
|
||||||
@@ -199,20 +217,26 @@ def _task_execute(screen: LogScreen, cfg: _Config) -> None:
|
|||||||
f"ROS {cfg.ros_version} — {cfg.build_type}[/bold]\n"
|
f"ROS {cfg.ros_version} — {cfg.build_type}[/bold]\n"
|
||||||
)
|
)
|
||||||
screen.set_progress(0, f"Pulling {full_ref}...")
|
screen.set_progress(0, f"Pulling {full_ref}...")
|
||||||
if not _pull_image(
|
ok = _pull_image(
|
||||||
short, full_ref, screen.write,
|
short, full_ref, screen.write,
|
||||||
on_progress=lambda p: screen.set_progress(p, f"Pulling {full_ref}..."),
|
on_progress=lambda p: screen.set_progress(p, f"Pulling {full_ref}..."),
|
||||||
):
|
register_proc=screen.set_proc,
|
||||||
|
)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
if not ok:
|
||||||
screen.finish(False)
|
screen.finish(False)
|
||||||
return
|
return
|
||||||
screen.set_progress(100, "Pull complete")
|
screen.set_progress(100, "Pull complete")
|
||||||
screen.write(f"\n[green]Done.[/green] Image ready: [bold]{full_ref}[/bold].")
|
screen.write(f"\n[green]Done.[/green] Image ready: [bold]{full_ref}[/bold].")
|
||||||
|
|
||||||
screen.finish(True)
|
if not screen.is_stopped():
|
||||||
|
screen.finish(True)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
screen.write(f"\n[red]Error:[/red] {exc}")
|
if not screen.is_stopped():
|
||||||
screen.finish(False)
|
screen.write(f"\n[red]Error:[/red] {exc}")
|
||||||
|
screen.finish(False)
|
||||||
|
|
||||||
|
|
||||||
# Scan the docker/ directory for subdirectories named after ROS versions (e.g. jazzy).
|
# Scan the docker/ directory for subdirectories named after ROS versions (e.g. jazzy).
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ def _run_quiet(cmd: List[str], write: Write | None = None, env: dict | None = No
|
|||||||
|
|
||||||
# Run a command and stream every output line to the log in real time.
|
# Run a command and stream every output line to the log in real time.
|
||||||
# Запускаем команду и транслируем каждую строку вывода в лог в реальном времени.
|
# Запускаем команду и транслируем каждую строку вывода в лог в реальном времени.
|
||||||
def _run_logged(cmd: List[str], write: Write, env: dict | None = None, cwd=None) -> None:
|
def _run_logged(
|
||||||
|
cmd: List[str],
|
||||||
|
write: Write,
|
||||||
|
env: dict | None = None,
|
||||||
|
cwd=None,
|
||||||
|
register_proc: Callable | None = None,
|
||||||
|
) -> None:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
@@ -87,12 +93,14 @@ def _run_logged(cmd: List[str], write: Write, env: dict | None = None, cwd=None)
|
|||||||
env=env or os.environ,
|
env=env or os.environ,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
)
|
)
|
||||||
|
if register_proc:
|
||||||
|
register_proc(proc)
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
s = line.rstrip()
|
s = line.rstrip()
|
||||||
if s:
|
if s:
|
||||||
write(s)
|
write(s)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
if proc.returncode != 0:
|
if proc.returncode not in (0, -9):
|
||||||
raise RuntimeError(f"Command failed: {cmd[0]}")
|
raise RuntimeError(f"Command failed: {cmd[0]}")
|
||||||
|
|
||||||
|
|
||||||
@@ -101,6 +109,7 @@ def _run_apt_with_progress(
|
|||||||
write: Write,
|
write: Write,
|
||||||
on_progress: Callable[[float], None],
|
on_progress: Callable[[float], None],
|
||||||
env: dict | None = None,
|
env: dict | None = None,
|
||||||
|
register_proc: Callable | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run an apt command and feed real percentage from APT::Status-Fd to on_progress(0-100)."""
|
"""Run an apt command and feed real percentage from APT::Status-Fd to on_progress(0-100)."""
|
||||||
# APT::Status-Fd makes apt write progress lines to a pipe descriptor instead of stdout.
|
# APT::Status-Fd makes apt write progress lines to a pipe descriptor instead of stdout.
|
||||||
@@ -110,7 +119,7 @@ def _run_apt_with_progress(
|
|||||||
r_fd, w_fd = os.pipe()
|
r_fd, w_fd = os.pipe()
|
||||||
try:
|
try:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
cmd + [f"-o", f"APT::Status-Fd={w_fd}"],
|
cmd + ["-o", f"APT::Status-Fd={w_fd}"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
text=True,
|
text=True,
|
||||||
@@ -122,6 +131,11 @@ def _run_apt_with_progress(
|
|||||||
# Закрываем пишущий конец в родительском процессе, чтобы читающий поток получил EOF при выходе apt.
|
# Закрываем пишущий конец в родительском процессе, чтобы читающий поток получил EOF при выходе apt.
|
||||||
os.close(w_fd)
|
os.close(w_fd)
|
||||||
|
|
||||||
|
# Tell the caller about this process so it can be killed if the user cancels.
|
||||||
|
# Сообщаем вызывающему о процессе, чтобы его можно было завершить при отмене пользователем.
|
||||||
|
if register_proc:
|
||||||
|
register_proc(proc)
|
||||||
|
|
||||||
def _read_status() -> None:
|
def _read_status() -> None:
|
||||||
with os.fdopen(r_fd, "r") as f:
|
with os.fdopen(r_fd, "r") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
@@ -141,7 +155,7 @@ def _run_apt_with_progress(
|
|||||||
write(s)
|
write(s)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
t.join()
|
t.join()
|
||||||
if proc.returncode != 0:
|
if proc.returncode not in (0, -9):
|
||||||
raise RuntimeError(f"Command failed: {cmd[0]}")
|
raise RuntimeError(f"Command failed: {cmd[0]}")
|
||||||
|
|
||||||
|
|
||||||
@@ -162,7 +176,11 @@ def _setup_locale(write: Write) -> None:
|
|||||||
|
|
||||||
# Add the official ROS2 apt repository and its signing key so we can install ROS2 packages.
|
# Add the official ROS2 apt repository and its signing key so we can install ROS2 packages.
|
||||||
# Добавляем официальный apt-репозиторий ROS2 и его ключ подписи, чтобы можно было установить пакеты ROS2.
|
# Добавляем официальный apt-репозиторий ROS2 и его ключ подписи, чтобы можно было установить пакеты ROS2.
|
||||||
def _add_ros2_repo(write: Write, on_progress: Optional[Callable[[float], None]] = None) -> None:
|
def _add_ros2_repo(
|
||||||
|
write: Write,
|
||||||
|
on_progress: Optional[Callable[[float], None]] = None,
|
||||||
|
register_proc: Callable | None = None,
|
||||||
|
) -> None:
|
||||||
def _prog(p: float) -> None:
|
def _prog(p: float) -> None:
|
||||||
if on_progress:
|
if on_progress:
|
||||||
on_progress(p)
|
on_progress(p)
|
||||||
@@ -171,7 +189,6 @@ def _add_ros2_repo(write: Write, on_progress: Optional[Callable[[float], None]]
|
|||||||
|
|
||||||
# Best-effort update - 60 second timeout so a bad mirror doesn't hang forever.
|
# Best-effort update - 60 second timeout so a bad mirror doesn't hang forever.
|
||||||
# Фоновое обновление с таймаутом 60 секунд, чтобы зависший зеркальный сервер не блокировал процесс.
|
# Фоновое обновление с таймаутом 60 секунд, чтобы зависший зеркальный сервер не блокировал процесс.
|
||||||
# subprocess.run(["sudo", "apt-get", "update", "-qq"] + _APT_TIMEOUTS, capture_output=True, timeout=120)
|
|
||||||
subprocess.run(["sudo", "apt-get", "update"] + _APT_TIMEOUTS, capture_output=True, timeout=120)
|
subprocess.run(["sudo", "apt-get", "update"] + _APT_TIMEOUTS, capture_output=True, timeout=120)
|
||||||
_prog(15)
|
_prog(15)
|
||||||
|
|
||||||
@@ -224,6 +241,7 @@ def _add_ros2_repo(write: Write, on_progress: Optional[Callable[[float], None]]
|
|||||||
write,
|
write,
|
||||||
lambda p: _prog(70 + p * 0.30),
|
lambda p: _prog(70 + p * 0.30),
|
||||||
_APT_ENV,
|
_APT_ENV,
|
||||||
|
register_proc=register_proc,
|
||||||
)
|
)
|
||||||
write("[green][ok][/green] ROS2 repository ready")
|
write("[green][ok][/green] ROS2 repository ready")
|
||||||
_prog(100)
|
_prog(100)
|
||||||
@@ -231,13 +249,18 @@ def _add_ros2_repo(write: Write, on_progress: Optional[Callable[[float], None]]
|
|||||||
|
|
||||||
# Install the full ROS2 Jazzy Desktop and the developer tools (colcon, rosdep, etc.).
|
# Install the full ROS2 Jazzy Desktop and the developer tools (colcon, rosdep, etc.).
|
||||||
# Устанавливаем полный ROS2 Jazzy Desktop и инструменты разработчика (colcon, rosdep и т.д.).
|
# Устанавливаем полный ROS2 Jazzy Desktop и инструменты разработчика (colcon, rosdep и т.д.).
|
||||||
def _install_ros2_jazzy(write: Write, on_progress: Optional[Callable[[float], None]] = None) -> None:
|
def _install_ros2_jazzy(
|
||||||
|
write: Write,
|
||||||
|
on_progress: Optional[Callable[[float], None]] = None,
|
||||||
|
register_proc: Callable | None = None,
|
||||||
|
) -> None:
|
||||||
write("[cyan][*][/cyan] Installing ros-jazzy-desktop and ros-dev-tools...")
|
write("[cyan][*][/cyan] Installing ros-jazzy-desktop and ros-dev-tools...")
|
||||||
_run_apt_with_progress(
|
_run_apt_with_progress(
|
||||||
["sudo", "apt-get", "install", "-y", "ros-jazzy-desktop", "ros-dev-tools"] + _APT_TIMEOUTS,
|
["sudo", "apt-get", "install", "-y", "ros-jazzy-desktop", "ros-dev-tools"] + _APT_TIMEOUTS,
|
||||||
write,
|
write,
|
||||||
on_progress or (lambda _: None),
|
on_progress or (lambda _: None),
|
||||||
_APT_ENV,
|
_APT_ENV,
|
||||||
|
register_proc=register_proc,
|
||||||
)
|
)
|
||||||
write("[green][ok][/green] ROS2 Jazzy Desktop installed")
|
write("[green][ok][/green] ROS2 Jazzy Desktop installed")
|
||||||
|
|
||||||
@@ -288,16 +311,24 @@ def _task_install_jazzy(screen: LogScreen) -> None:
|
|||||||
_add_ros2_repo(
|
_add_ros2_repo(
|
||||||
screen.write,
|
screen.write,
|
||||||
on_progress=lambda p: screen.set_progress(5 + p * 0.15),
|
on_progress=lambda p: screen.set_progress(5 + p * 0.15),
|
||||||
|
register_proc=screen.set_proc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
|
||||||
# Step 3 — ROS2 Jazzy (20 → 85 %)
|
# Step 3 — ROS2 Jazzy (20 → 85 %)
|
||||||
screen.set_progress(20, "Installing ROS2 Jazzy Desktop...")
|
screen.set_progress(20, "Installing ROS2 Jazzy Desktop...")
|
||||||
screen.write("\n[bold]Step 3 / 5 — ROS2 Jazzy Desktop[/bold]")
|
screen.write("\n[bold]Step 3 / 5 — ROS2 Jazzy Desktop[/bold]")
|
||||||
_install_ros2_jazzy(
|
_install_ros2_jazzy(
|
||||||
screen.write,
|
screen.write,
|
||||||
on_progress=lambda p: screen.set_progress(20 + p * 0.65),
|
on_progress=lambda p: screen.set_progress(20 + p * 0.65),
|
||||||
|
register_proc=screen.set_proc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
|
||||||
# Step 4 — colcon (85 → 92 %)
|
# Step 4 — colcon (85 → 92 %)
|
||||||
screen.set_progress(85, "Installing colcon...")
|
screen.set_progress(85, "Installing colcon...")
|
||||||
screen.write("\n[bold]Step 4 / 5 — colcon[/bold]")
|
screen.write("\n[bold]Step 4 / 5 — colcon[/bold]")
|
||||||
@@ -309,13 +340,15 @@ def _task_install_jazzy(screen: LogScreen) -> None:
|
|||||||
_setup_shell_rc(screen.write)
|
_setup_shell_rc(screen.write)
|
||||||
screen.set_progress(100, "Done")
|
screen.set_progress(100, "Done")
|
||||||
|
|
||||||
screen.write(
|
if not screen.is_stopped():
|
||||||
"\nRestart the terminal, then run [bold]cobot local-setup[/bold] again to build."
|
screen.write(
|
||||||
)
|
"\nRestart the terminal, then run [bold]cobot local-setup[/bold] again to build."
|
||||||
screen.finish(True)
|
)
|
||||||
|
screen.finish(True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
screen.write(f"\n[red]Error:[/red] {exc}")
|
if not screen.is_stopped():
|
||||||
screen.finish(False)
|
screen.write(f"\n[red]Error:[/red] {exc}")
|
||||||
|
screen.finish(False)
|
||||||
|
|
||||||
|
|
||||||
# Build all project packages with colcon and track progress by counting finished packages.
|
# Build all project packages with colcon and track progress by counting finished packages.
|
||||||
@@ -348,14 +381,16 @@ def _task_build(screen: LogScreen) -> None:
|
|||||||
built += 1
|
built += 1
|
||||||
screen.set_progress(built / total * 100, f"{built} / {total} packages done")
|
screen.set_progress(built / total * 100, f"{built} / {total} packages done")
|
||||||
|
|
||||||
_run_logged(["colcon", "build", "--symlink-install"], _track, cwd=_PROJECT_DIR)
|
_run_logged(["colcon", "build", "--symlink-install"], _track, cwd=_PROJECT_DIR, register_proc=screen.set_proc)
|
||||||
|
|
||||||
screen.set_progress(100, "Build complete")
|
if not screen.is_stopped():
|
||||||
screen.write("\nActivate workspace: [bold]source install/setup.bash[/bold]")
|
screen.set_progress(100, "Build complete")
|
||||||
screen.finish(True)
|
screen.write("\nActivate workspace: [bold]source install/setup.bash[/bold]")
|
||||||
|
screen.finish(True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
screen.write(f"\n[red]Error:[/red] {exc}")
|
if not screen.is_stopped():
|
||||||
screen.finish(False)
|
screen.write(f"\n[red]Error:[/red] {exc}")
|
||||||
|
screen.finish(False)
|
||||||
|
|
||||||
|
|
||||||
# Webots version that matches the Docker images used in this project.
|
# Webots version that matches the Docker images used in this project.
|
||||||
@@ -386,16 +421,31 @@ def _task_install_webots(screen: LogScreen) -> None:
|
|||||||
screen.write(f"[dim]{_WEBOTS_DEB_URL}[/dim]\n")
|
screen.write(f"[dim]{_WEBOTS_DEB_URL}[/dim]\n")
|
||||||
screen.set_progress(0, "Downloading Webots...")
|
screen.set_progress(0, "Downloading Webots...")
|
||||||
|
|
||||||
# urllib calls this hook periodically with how many bytes have been downloaded.
|
# Download in 64 KB chunks so we can update the progress bar and bail out if the user
|
||||||
# urllib вызывает этот обратный вызов периодически с количеством скачанных байт.
|
# cancels midway through instead of blocking in urlretrieve until the full file arrives.
|
||||||
def _hook(blocks: int, block_size: int, total: int) -> None:
|
# Скачиваем по 64 КБ, чтобы обновлять прогресс-бар и прерваться при отмене пользователем,
|
||||||
if total > 0:
|
# а не блокироваться в urlretrieve до получения всего файла.
|
||||||
pct = min(blocks * block_size / total * 65, 65)
|
with urllib.request.urlopen(_WEBOTS_DEB_URL, timeout=30) as resp:
|
||||||
mb = blocks * block_size / 1_048_576
|
total = int(resp.headers.get("Content-Length", 0))
|
||||||
total_mb = total / 1_048_576
|
downloaded = 0
|
||||||
screen.set_progress(pct, f"Downloading... {mb:.0f} / {total_mb:.0f} MB")
|
with open(deb_path, "wb") as f:
|
||||||
|
while True:
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
chunk = resp.read(65536)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
f.write(chunk)
|
||||||
|
downloaded += len(chunk)
|
||||||
|
if total > 0:
|
||||||
|
pct = min(downloaded / total * 65, 65)
|
||||||
|
mb = downloaded / 1_048_576
|
||||||
|
total_mb = total / 1_048_576
|
||||||
|
screen.set_progress(pct, f"Downloading... {mb:.0f} / {total_mb:.0f} MB")
|
||||||
|
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
|
||||||
urllib.request.urlretrieve(_WEBOTS_DEB_URL, deb_path, _hook)
|
|
||||||
screen.write("[green]Download complete.[/green]")
|
screen.write("[green]Download complete.[/green]")
|
||||||
screen.set_progress(65, "Installing package...")
|
screen.set_progress(65, "Installing package...")
|
||||||
|
|
||||||
@@ -404,23 +454,28 @@ def _task_install_webots(screen: LogScreen) -> None:
|
|||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
|
screen.set_proc(proc)
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
s = line.rstrip()
|
s = line.rstrip()
|
||||||
if s:
|
if s:
|
||||||
screen.write(s)
|
screen.write(s)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
|
|
||||||
if proc.returncode != 0:
|
if proc.returncode not in (0, -9):
|
||||||
screen.write("\n[red]Installation failed.[/red]")
|
screen.write("\n[red]Installation failed.[/red]")
|
||||||
screen.finish(False)
|
screen.finish(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
|
|
||||||
screen.set_progress(100, "Done")
|
screen.set_progress(100, "Done")
|
||||||
screen.write("\n[green]Webots installed successfully.[/green]")
|
screen.write("\n[green]Webots installed successfully.[/green]")
|
||||||
screen.finish(True)
|
screen.finish(True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
screen.write(f"\n[red]Error:[/red] {exc}")
|
if not screen.is_stopped():
|
||||||
screen.finish(False)
|
screen.write(f"\n[red]Error:[/red] {exc}")
|
||||||
|
screen.finish(False)
|
||||||
|
|
||||||
|
|
||||||
# Minimal single-question app used between steps where a full wizard is not needed.
|
# Minimal single-question app used between steps where a full wizard is not needed.
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ def _task_update(screen: LogScreen) -> None:
|
|||||||
["git", "fetch", "origin"],
|
["git", "fetch", "origin"],
|
||||||
cwd=_PROJECT_DIR, capture_output=True, text=True,
|
cwd=_PROJECT_DIR, capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
if fetch.returncode != 0:
|
if fetch.returncode != 0:
|
||||||
screen.write(f"[red]Fetch failed:[/red] {fetch.stderr.strip()}")
|
screen.write(f"[red]Fetch failed:[/red] {fetch.stderr.strip()}")
|
||||||
screen.finish(False)
|
screen.finish(False)
|
||||||
@@ -46,9 +48,10 @@ def _task_update(screen: LogScreen) -> None:
|
|||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
if behind == "0":
|
if behind == "0":
|
||||||
screen.set_progress(100, "Already up to date")
|
if not screen.is_stopped():
|
||||||
screen.write("[green][ok][/green] Already up to date.")
|
screen.set_progress(100, "Already up to date")
|
||||||
screen.finish(True)
|
screen.write("[green][ok][/green] Already up to date.")
|
||||||
|
screen.finish(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show which commits are coming in so the user knows what changed.
|
# Show which commits are coming in so the user knows what changed.
|
||||||
@@ -68,6 +71,8 @@ def _task_update(screen: LogScreen) -> None:
|
|||||||
["git", "pull", "origin", branch],
|
["git", "pull", "origin", branch],
|
||||||
capture_output=True, text=True, cwd=_PROJECT_DIR,
|
capture_output=True, text=True, cwd=_PROJECT_DIR,
|
||||||
)
|
)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
if pull.returncode != 0:
|
if pull.returncode != 0:
|
||||||
for line in (pull.stdout + pull.stderr).splitlines():
|
for line in (pull.stdout + pull.stderr).splitlines():
|
||||||
if line.strip():
|
if line.strip():
|
||||||
@@ -86,18 +91,22 @@ def _task_update(screen: LogScreen) -> None:
|
|||||||
["uv", "tool", "install", "--editable", str(_PROJECT_DIR)],
|
["uv", "tool", "install", "--editable", str(_PROJECT_DIR)],
|
||||||
capture_output=True, text=True,
|
capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
|
if screen.is_stopped():
|
||||||
|
return
|
||||||
if reinstall.returncode == 0:
|
if reinstall.returncode == 0:
|
||||||
screen.write("[green][ok][/green] cobot reinstalled")
|
screen.write("[green][ok][/green] cobot reinstalled")
|
||||||
else:
|
else:
|
||||||
screen.write(f"[yellow]Warning:[/yellow] reinstall failed — {reinstall.stderr.strip()}")
|
screen.write(f"[yellow]Warning:[/yellow] reinstall failed — {reinstall.stderr.strip()}")
|
||||||
|
|
||||||
screen.set_progress(100, "Done")
|
if not screen.is_stopped():
|
||||||
screen.write("\n[green]Project updated successfully.[/green]")
|
screen.set_progress(100, "Done")
|
||||||
screen.finish(True)
|
screen.write("\n[green]Project updated successfully.[/green]")
|
||||||
|
screen.finish(True)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
screen.write(f"\n[red]Error:[/red] {exc}")
|
if not screen.is_stopped():
|
||||||
screen.finish(False)
|
screen.write(f"\n[red]Error:[/red] {exc}")
|
||||||
|
screen.finish(False)
|
||||||
|
|
||||||
|
|
||||||
class _UpdateApp(App[None]):
|
class _UpdateApp(App[None]):
|
||||||
|
|||||||
@@ -184,6 +184,10 @@ class LogScreen(Screen[bool]):
|
|||||||
self._finished = False
|
self._finished = False
|
||||||
self._success = False
|
self._success = False
|
||||||
self._show_progress = show_progress
|
self._show_progress = show_progress
|
||||||
|
# Tracks the subprocess that is currently running so on_unmount can kill it.
|
||||||
|
# Отслеживает текущий subprocess, чтобы on_unmount мог его завершить.
|
||||||
|
self._active_proc = None
|
||||||
|
self._stopped = False
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Static(self._title, id="step")
|
yield Static(self._title, id="step")
|
||||||
@@ -201,6 +205,29 @@ class LogScreen(Screen[bool]):
|
|||||||
# Запускаем задачу в отдельном потоке, чтобы интерфейс не зависал.
|
# Запускаем задачу в отдельном потоке, чтобы интерфейс не зависал.
|
||||||
self.app.run_worker(lambda: self._run_fn(self), thread=True)
|
self.app.run_worker(lambda: self._run_fn(self), thread=True)
|
||||||
|
|
||||||
|
def set_proc(self, proc) -> None:
|
||||||
|
# Register the subprocess that is currently running.
|
||||||
|
# Called from the worker thread - GIL makes simple assignment safe here.
|
||||||
|
# Регистрируем текущий subprocess.
|
||||||
|
# Вызывается из рабочего потока - простое присваивание безопасно благодаря GIL.
|
||||||
|
self._active_proc = proc
|
||||||
|
|
||||||
|
def is_stopped(self) -> bool:
|
||||||
|
# Return True if the user has closed the screen before the task finished.
|
||||||
|
# Возвращает True если пользователь закрыл экран до завершения задачи.
|
||||||
|
return self._stopped
|
||||||
|
|
||||||
|
def on_unmount(self) -> None:
|
||||||
|
# Kill the active subprocess when the screen closes so it does not keep running in the background.
|
||||||
|
# Убиваем активный subprocess при закрытии экрана, чтобы он не продолжал работать в фоне.
|
||||||
|
self._stopped = True
|
||||||
|
proc = self._active_proc
|
||||||
|
if proc is not None:
|
||||||
|
try:
|
||||||
|
proc.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def set_progress(self, pct: float, label: str = "") -> None:
|
def set_progress(self, pct: float, label: str = "") -> None:
|
||||||
# Thread-safe - this is called from the worker thread, not the UI thread.
|
# Thread-safe - this is called from the worker thread, not the UI thread.
|
||||||
# Потокобезопасно - вызывается из рабочего потока, а не из потока интерфейса.
|
# Потокобезопасно - вызывается из рабочего потока, а не из потока интерфейса.
|
||||||
|
|||||||
Reference in New Issue
Block a user