feat: update version and dependencies in setup.py; add privilege management and process handling modules
- Updated version from 2026.05.31 to 2026.06.11 in setup.py - Replaced 'textual' with 'rich' in install_requires - Added privilege.py for managing sudo privileges with a keep-alive mechanism - Introduced process.py for handling subprocesses with enhanced control and output streaming - Created ui.py for unified console interactions and user prompts
This commit is contained in:
+63
-138
@@ -5,154 +5,63 @@ import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from textual.app import App
|
||||
|
||||
from cobot.tui import SCREEN_CSS, InputScreen, LogScreen, PickScreen
|
||||
from cobot import ui, process
|
||||
from cobot.commands.local_setup import _ros2_env
|
||||
|
||||
_PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
|
||||
def _task_rebuild(screen: LogScreen, packages: List[str], symlink: bool) -> None:
|
||||
"""Run colcon build for the selected packages (or all if packages is empty).
|
||||
Streams output to the log and tracks per-package progress.
|
||||
|
||||
Запускает colcon build для выбранных пакетов (или всех если packages пуст).
|
||||
Транслирует вывод в лог и отслеживает прогресс по каждому пакету.
|
||||
def _count_packages(packages: List[str], env: dict) -> int:
|
||||
"""Count how many colcon packages will be built so we can show X / total progress.
|
||||
Считает количество пакетов colcon для отображения прогресса X / всего.
|
||||
"""
|
||||
try:
|
||||
env = _ros2_env()
|
||||
|
||||
# Count packages so we can show X / total progress.
|
||||
# Считаем пакеты чтобы показывать X / всего в прогрессе.
|
||||
list_cmd = ["colcon", "list", "--base-paths", "src"]
|
||||
if packages:
|
||||
list_cmd += ["--packages-select"] + packages
|
||||
list_result = subprocess.run(
|
||||
list_cmd, capture_output=True, text=True,
|
||||
cwd=_PROJECT_DIR, env=env,
|
||||
)
|
||||
total = max(len([l for l in list_result.stdout.splitlines() if l.strip()]), 1)
|
||||
|
||||
pkg_label = " ".join(packages) if packages else "all packages"
|
||||
symlink_label = " --symlink-install" if symlink else ""
|
||||
screen.write(f"[bold]colcon build{symlink_label} — {pkg_label}[/bold]\n")
|
||||
screen.set_progress(0, f"0 / {total} packages done")
|
||||
built = 0
|
||||
|
||||
cmd = ["colcon", "build", "--base-paths", "src"]
|
||||
if symlink:
|
||||
cmd.append("--symlink-install")
|
||||
if packages:
|
||||
cmd += ["--packages-select"] + packages
|
||||
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, cwd=_PROJECT_DIR, env=env,
|
||||
)
|
||||
screen.set_proc(proc)
|
||||
|
||||
for line in proc.stdout:
|
||||
s = line.rstrip()
|
||||
if s:
|
||||
screen.write(s)
|
||||
if "Finished <<<" in line or "Failed <<<" in line:
|
||||
built += 1
|
||||
screen.set_progress(
|
||||
built / total * 100,
|
||||
f"{built} / {total} packages done",
|
||||
)
|
||||
proc.wait()
|
||||
|
||||
if screen.is_stopped():
|
||||
return
|
||||
|
||||
if proc.returncode not in (0, -9):
|
||||
screen.write("\n[red]Build failed.[/red]")
|
||||
screen.finish(False)
|
||||
return
|
||||
|
||||
screen.set_progress(100, "Done")
|
||||
screen.write("\n[green]Build complete.[/green]")
|
||||
screen.finish(True)
|
||||
|
||||
except Exception as exc:
|
||||
if not screen.is_stopped():
|
||||
screen.write(f"\n[red]Error:[/red] {exc}")
|
||||
screen.finish(False)
|
||||
list_cmd = ["colcon", "list", "--base-paths", "src"]
|
||||
if packages:
|
||||
list_cmd += ["--packages-select"] + packages
|
||||
result = subprocess.run(list_cmd, capture_output=True, text=True,
|
||||
cwd=_PROJECT_DIR, env=env)
|
||||
return max(len([l for l in result.stdout.splitlines() if l.strip()]), 1)
|
||||
|
||||
|
||||
class _RebuildApp(App[None]):
|
||||
"""Rebuild wizard: optionally asks for packages and symlink flag, then runs colcon.
|
||||
Мастер пересборки: опционально спрашивает пакеты и флаг symlink, затем запускает colcon.
|
||||
def _rebuild(packages: List[str], symlink: bool) -> None:
|
||||
"""Run colcon build for the selected packages (or all), streaming live output
|
||||
with a per-package progress bar.
|
||||
Запускает colcon build для выбранных пакетов (или всех), транслируя живой вывод
|
||||
с прогресс-баром по пакетам.
|
||||
"""
|
||||
env = _ros2_env()
|
||||
total = _count_packages(packages, env)
|
||||
|
||||
CSS = SCREEN_CSS
|
||||
pkg_label = " ".join(packages) if packages else "все пакеты"
|
||||
symlink_label = " --symlink-install" if symlink else ""
|
||||
|
||||
def __init__(self, packages: Optional[List[str]], symlink: Optional[bool]):
|
||||
super().__init__()
|
||||
# None means "ask the user interactively".
|
||||
# None означает "спросить пользователя интерактивно".
|
||||
self._packages = packages
|
||||
self._symlink = symlink
|
||||
cmd = ["colcon", "build", "--base-paths", "src"]
|
||||
if symlink:
|
||||
cmd.append("--symlink-install")
|
||||
if packages:
|
||||
cmd += ["--packages-select"] + packages
|
||||
|
||||
def on_mount(self) -> None:
|
||||
if self._packages is None:
|
||||
self._ask_packages()
|
||||
elif self._symlink is None:
|
||||
self._ask_symlink()
|
||||
else:
|
||||
self._start()
|
||||
built = 0
|
||||
|
||||
def _ask_packages(self) -> None:
|
||||
self.push_screen(
|
||||
InputScreen(
|
||||
"rebuild",
|
||||
"Packages to rebuild (space-separated, leave empty for all):",
|
||||
"",
|
||||
note="Example: iiwa_controller iiwa_bringup",
|
||||
),
|
||||
self._got_packages,
|
||||
)
|
||||
def _parse(line: str):
|
||||
nonlocal built
|
||||
if "Finished <<<" in line or "Failed <<<" in line:
|
||||
built += 1
|
||||
return (built / total * 100, f"{built} / {total} пакетов")
|
||||
return None
|
||||
|
||||
def _got_packages(self, value: Optional[str]) -> None:
|
||||
if value is None:
|
||||
self.exit()
|
||||
return
|
||||
self._packages = value.split() if value.strip() else []
|
||||
if self._symlink is None:
|
||||
self._ask_symlink()
|
||||
else:
|
||||
self._start()
|
||||
|
||||
def _ask_symlink(self) -> None:
|
||||
self.push_screen(
|
||||
PickScreen(
|
||||
"rebuild",
|
||||
"Use --symlink-install?",
|
||||
["Yes", "No"],
|
||||
"Yes",
|
||||
),
|
||||
self._got_symlink,
|
||||
)
|
||||
|
||||
def _got_symlink(self, value: Optional[str]) -> None:
|
||||
if value is None:
|
||||
self.exit()
|
||||
return
|
||||
self._symlink = value == "Yes"
|
||||
self._start()
|
||||
|
||||
def _start(self) -> None:
|
||||
self.push_screen(
|
||||
LogScreen(
|
||||
"Rebuilding packages",
|
||||
lambda s: _task_rebuild(s, self._packages, self._symlink),
|
||||
show_progress=True,
|
||||
),
|
||||
lambda _: self.exit(),
|
||||
)
|
||||
rc = process.run_step(
|
||||
f"colcon build{symlink_label} — {pkg_label}",
|
||||
cmd,
|
||||
env=env,
|
||||
cwd=str(_PROJECT_DIR),
|
||||
total=100.0,
|
||||
parse_progress=_parse,
|
||||
success_msg="Сборка завершена",
|
||||
fail_msg="Сборка завершилась с ошибкой",
|
||||
)
|
||||
if rc in (0, -9, -15):
|
||||
ui.note("Активируйте окружение: source install/setup.bash")
|
||||
|
||||
|
||||
def register(subparsers: argparse._SubParsersAction) -> None:
|
||||
@@ -186,7 +95,23 @@ def run(args: argparse.Namespace) -> None:
|
||||
"""Entry point for the rebuild command.
|
||||
Точка входа для команды rebuild.
|
||||
"""
|
||||
# Convert empty list to None so the TUI asks interactively.
|
||||
# Преобразуем пустой список в None, чтобы TUI спросил интерактивно.
|
||||
packages = args.packages if args.packages else None
|
||||
_RebuildApp(packages, args.symlink).run()
|
||||
packages: Optional[List[str]] = args.packages if args.packages else None
|
||||
|
||||
if packages is None:
|
||||
value = ui.text(
|
||||
"Какие пакеты пересобрать? (через пробел, пусто — все)",
|
||||
"",
|
||||
note="Пример: iiwa_controller iiwa_bringup",
|
||||
)
|
||||
if value is None:
|
||||
return
|
||||
packages = value.split() if value.strip() else []
|
||||
|
||||
symlink = args.symlink
|
||||
if symlink is None:
|
||||
choice = ui.select("Использовать --symlink-install?", ["Да", "Нет"], "Да")
|
||||
if choice is None:
|
||||
return
|
||||
symlink = choice == "Да"
|
||||
|
||||
_rebuild(packages, symlink)
|
||||
|
||||
Reference in New Issue
Block a user