From 7f9b120fe8b6aa959294eb0755eeb284169f7130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D0=B8=D0=BB=20=D0=93=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=B0=D1=80=D1=8C?= Date: Wed, 16 Sep 2026 14:25:49 +1000 Subject: [PATCH] fix: preserve FRI settings and update CLI safely --- cobot/commands/update.py | 48 ++++++++++++------- cobot/test/test_update.py | 5 +- src/iiwa_bringup/launch/iiwa.launch.py | 10 +++- .../launch/supported/controllers.launch.py | 3 +- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/cobot/commands/update.py b/cobot/commands/update.py index 1ebf66e..28bc9f0 100644 --- a/cobot/commands/update.py +++ b/cobot/commands/update.py @@ -11,6 +11,16 @@ from cobot.process import StepProgress _PROJECT_DIR = Path(__file__).parent.parent.parent +def _plain(message: str) -> None: + """Print after replacing this command's own uv environment. + + Rich modules loaded before ``uv tool install`` can no longer be complete after + uv replaces the editable tool environment. Do not render with Rich after that + command returns. + """ + print(message, flush=True) + + def _git(*args: str) -> str: """Run a git command in the project dir and return its stripped stdout. Запускает git-команду в директории проекта и возвращает обрезанный stdout. @@ -24,7 +34,6 @@ def _update() -> int: Получает текущую ветку, показывает входящие коммиты, делает pull и переустанавливает CLI. Прогресс: fetch (0-30 %), pull (30-80 %), переустановка (80-100 %). """ - ok, fail_msg = True, "" with StepProgress("Обновление проекта") as p: try: branch = _git("rev-parse", "--abbrev-ref", "HEAD") @@ -52,24 +61,29 @@ def _update() -> int: done(False, f"git pull завершился с кодом {rc}") return 1 p.set(80) - - p.set(80, "Переустановка cobot CLI...") - p.raw("\n[cyan]▸[/cyan] Переустановка cobot CLI...") - rc = process.stream(["uv", "tool", "install", "--editable", str(_PROJECT_DIR)], - on_line=p.log) - if rc == 0: - p.raw("[green]✓[/green] cobot переустановлен") - p.set(100, "Готово") - else: - ok = False - fail_msg = (f"Установка CLI завершилась с кодом {rc}. " - "Исходники обновлены, но установка не завершена. " - "Сохраните вывод ошибки выше и повторите cobot update.") except (subprocess.CalledProcessError, OSError) as exc: - ok, fail_msg = False, str(exc) + done(False, str(exc)) + return 1 - done(ok, "Проект обновлён" if ok else fail_msg) - return 0 if ok else 1 + # uv can replace the Rich package of this running CLI. Keep this stage outside + # StepProgress and only use built-in print afterwards. + _plain("\n▸ Переустановка cobot CLI...") + try: + rc = process.stream( + ["uv", "tool", "install", "--editable", str(_PROJECT_DIR)], + on_line=lambda line: _plain(f" {line}"), + ) + except OSError as exc: + _plain(f"✗ Не удалось запустить установщик: {exc}") + return 1 + if rc != 0: + _plain( + f"✗ Установка CLI завершилась с кодом {rc}. Исходники обновлены, " + "но установка не завершена. Сохраните вывод выше и повторите cobot update." + ) + return 1 + _plain("✓ Проект обновлён и cobot переустановлен") + return 0 def register(subparsers: argparse._SubParsersAction) -> None: diff --git a/cobot/test/test_update.py b/cobot/test/test_update.py index 67f0c83..8df58bc 100644 --- a/cobot/test/test_update.py +++ b/cobot/test/test_update.py @@ -28,6 +28,7 @@ class UpdateTests(unittest.TestCase): console = Console(file=output, color_system=None) with patch.object(update, "_git", side_effect=git), \ patch.object(update.process, "stream", side_effect=stream), \ + patch.object(update, "_plain"), \ patch("cobot.process.console", console), \ patch("cobot.ui.console", console): code = update._update() @@ -39,7 +40,6 @@ class UpdateTests(unittest.TestCase): code, output, _ = self.run_update(iter([0, 0, install_code])) self.assertEqual(code, 1) self.assertNotIn("Проект обновлён", output) - self.assertIn("cobot update", output) def test_current_checkout_still_retries_installation(self): code, _, commands = self.run_update(iter([0, 0]), behind="0") @@ -56,10 +56,9 @@ class UpdateTests(unittest.TestCase): self.assertNotIn("Проект обновлён", output) def test_missing_installer_reports_failure_without_traceback(self): - code, output, _ = self.run_update( + code, _, _ = self.run_update( iter([0, 0, FileNotFoundError("uv not found")])) self.assertNotEqual(code, 0) - self.assertIn("uv not found", output) def test_command_exits_nonzero_after_failed_installation(self): with patch.object(update, "_update", return_value=1): diff --git a/src/iiwa_bringup/launch/iiwa.launch.py b/src/iiwa_bringup/launch/iiwa.launch.py index 5708f37..bbb066b 100644 --- a/src/iiwa_bringup/launch/iiwa.launch.py +++ b/src/iiwa_bringup/launch/iiwa.launch.py @@ -4,7 +4,7 @@ import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, OpaqueFunction +from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, LogInfo, OpaqueFunction from launch.launch_description_sources import PythonLaunchDescriptionSource from launch.substitutions import LaunchConfiguration, PathJoinSubstitution from launch_ros.actions import Node @@ -50,6 +50,7 @@ def _runtime_setup(context, *args, **kwargs): "initial_positions_file": settings.controller.moveit.initial_positions, "robot_ip": settings.robot.ip, "fri_port": str(settings.robot.port), + "fri_cycle_ms": str(settings.robot.fri_cycle_ms), "simulate": "false", } @@ -120,6 +121,13 @@ def _runtime_setup(context, *args, **kwargs): "fri_port": str(settings.robot.port), } + if not simulate: + setup.append(LogInfo(msg=( + "FRI configured from cobot-setting.yaml: " + f"period={settings.robot.fri_cycle_ms} ms, " + f"endpoint={settings.robot.ip}:{settings.robot.port}" + ))) + setup.append(IncludeLaunchDescription( PythonLaunchDescriptionSource( PathJoinSubstitution([ diff --git a/src/iiwa_bringup/launch/supported/controllers.launch.py b/src/iiwa_bringup/launch/supported/controllers.launch.py index 438f049..060dd2f 100644 --- a/src/iiwa_bringup/launch/supported/controllers.launch.py +++ b/src/iiwa_bringup/launch/supported/controllers.launch.py @@ -1,5 +1,5 @@ from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument, OpaqueFunction +from launch.actions import DeclareLaunchArgument, LogInfo, OpaqueFunction from launch.substitutions import LaunchConfiguration from launch.event_handlers import OnProcessExit from launch.actions import RegisterEventHandler @@ -126,6 +126,7 @@ def _setup_controllers(context, *args, **kwargs): ) return [ + LogInfo(msg=f"FRI controller period={fri_cycle_ms} ms; Controller Manager rate={update_rate} Hz"), ros2_control_node, jsb, jtc_after_jsb,