From 96744b6ac8df962178eea1de0034ee2e94c0134f Mon Sep 17 00:00:00 2001 From: AlexTemirov Date: Mon, 10 Aug 2026 16:38:59 -0700 Subject: [PATCH] Support Hardware install beside legacy Runtime --- editor-server/device_installer.py | 60 ++++++++++++++++++++++++----- editor-server/server.py | 2 +- tests/test_editor_devices.py | 64 ++++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 12 deletions(-) diff --git a/editor-server/device_installer.py b/editor-server/device_installer.py index be7425a..68c6a4d 100644 --- a/editor-server/device_installer.py +++ b/editor-server/device_installer.py @@ -33,6 +33,7 @@ class DeviceInstallError(RuntimeError): _HARDWARE_PAIRING_INSPECTION_MARKER = "__BLACKNODE_HARDWARE_PAIRINGS__=" _HARDWARE_ADOPTION_MARKER = "__BLACKNODE_HARDWARE_ADOPTION__=" _HARDWARE_CONFIGURATION_MARKER = "__BLACKNODE_HARDWARE_CONFIGURATION__=" +_HARDWARE_INSTALL_MARKER = "__BLACKNODE_HARDWARE_INSTALL__=" _UPDATE_REPORT_MARKER = "__BLACKNODE_UPDATE_REPORT__=" _OFFLINE_BUNDLE_SCHEMA_VERSION = 2 _OFFLINE_BUNDLE_LOCK = threading.Lock() @@ -2080,16 +2081,35 @@ def report(percent: int, message: str) -> None: exit 2 } stack_root="$HOME/Blacknode/devices/$instance" -runtime_dir="$stack_root/runtime" -hardware_dir="$stack_root/hardware" +organized_runtime_dir="$stack_root/runtime" +organized_hardware_dir="$stack_root/hardware" service_instance="" [[ "$instance" == "default" ]] || service_instance="$instance" +if [[ "$instance" == "default" ]]; then + legacy_runtime_dir="$HOME/blacknode-runtime" + legacy_hardware_dir="$HOME/blacknode-hardware" +else + legacy_runtime_dir="$HOME/blacknode-runtimes/$instance" + legacy_hardware_dir="$HOME/blacknode-hardware-instances/$instance" +fi +runtime_dir="$organized_runtime_dir" +hardware_dir="$organized_hardware_dir" +layout="organized" +if [[ ! -d "$runtime_dir/.git" || ! -f "$runtime_dir/pyproject.toml" ]]; then + runtime_dir="$legacy_runtime_dir" + hardware_dir="$legacy_hardware_dir" + layout="legacy" +fi case "$hardware_dir" in - "$HOME/Blacknode/devices/"*/hardware) ;; + "$HOME/Blacknode/devices/"*/hardware|\ + "$HOME/blacknode-hardware"|\ + "$HOME/blacknode-hardware-instances/"*) ;; *) echo "Unsafe Hardware directory."; exit 2 ;; esac [[ -d "$runtime_dir/.git" && -f "$runtime_dir/pyproject.toml" ]] || { - echo "The organized Runtime installation is missing: $runtime_dir" + echo "No valid Runtime installation was found. Checked:" + echo " $organized_runtime_dir" + echo " $legacy_runtime_dir" exit 3 } created=false @@ -2154,6 +2174,8 @@ def report(percent: int, message: str) -> None: ) PY created=false +printf '__BLACKNODE_HARDWARE_INSTALL__={"hardware_dir":"%s","layout":"%s","stack_mode":"isolated"}\n' \ + "$hardware_dir" "$layout" progress 100 "Robot Hardware package installed" """ remote_script_path = ( @@ -2167,7 +2189,7 @@ def report(percent: int, message: str) -> None: sftp.chmod(remote_script_path, 0o700) finally: sftp.close() - _run( + output = _run( connection, f"bash {remote_script_path} {selected_instance}", stdin_text=_sudo_input(password, attempts=8), @@ -2184,13 +2206,33 @@ def report(percent: int, message: str) -> None: else None ), ) + marker_line = next( + ( + line[len(_HARDWARE_INSTALL_MARKER):] + for line in output.splitlines() + if line.startswith(_HARDWARE_INSTALL_MARKER) + ), + "", + ) + if not marker_line: + raise DeviceInstallError( + "The device did not confirm the Robot Hardware installation path." + ) + try: + installation = json.loads(marker_line) + hardware_dir = str(installation["hardware_dir"]) + stack_mode = str(installation.get("stack_mode") or "isolated") + layout = str(installation.get("layout") or "organized") + except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc: + raise DeviceInstallError( + "The device returned invalid Robot Hardware installation information." + ) from exc return { "ok": True, "instance_id": selected_instance, - "hardware_dir": ( - f"~/Blacknode/devices/{selected_instance}/hardware" - ), - "stack_mode": "isolated", + "hardware_dir": hardware_dir, + "stack_mode": stack_mode, + "layout": layout, } finally: try: diff --git a/editor-server/server.py b/editor-server/server.py index 44e8372..6f3b30a 100644 --- a/editor-server/server.py +++ b/editor-server/server.py @@ -7400,7 +7400,7 @@ def _install_device_host_hardware_payload( updated_management = { **managed, "hardware_dir": str(installed["hardware_dir"]), - "stack_mode": "isolated", + "stack_mode": str(installed.get("stack_mode") or "isolated"), } device = _device_registry.set_host_management( host_id, diff --git a/tests/test_editor_devices.py b/tests/test_editor_devices.py index d36e4c0..fcc7cac 100644 --- a/tests/test_editor_devices.py +++ b/tests/test_editor_devices.py @@ -1128,6 +1128,12 @@ def fake_run(_connection, command, **kwargs): "__BLACKNODE_HARDWARE_INSTALL_PROGRESS__=50|" "Setting up the Robot Hardware environment" ) + if "blacknode-hardware-install-" in command and command.startswith("bash "): + return ( + '__BLACKNODE_HARDWARE_INSTALL__={"hardware_dir":' + '"/home/alex/Blacknode/devices/default/hardware",' + '"layout":"organized","stack_mode":"isolated"}\n' + ) return "" with ( @@ -1146,17 +1152,21 @@ def fake_run(_connection, command, **kwargs): self.assertEqual( result["hardware_dir"], - "~/Blacknode/devices/default/hardware", + "/home/alex/Blacknode/devices/default/hardware", ) self.assertEqual(result["stack_mode"], "isolated") + self.assertEqual(result["layout"], "organized") self.assertTrue(any(item["progress"] == 50 for item in progress)) self.assertIn("bash /tmp/blacknode-hardware-install-", commands[0]) self.assertEqual(stdin_values[0], "ssh-password\n" * 8) self.assertNotIn("ssh-password", uploaded[0]) self.assertIn( - 'hardware_dir="$stack_root/hardware"', + 'organized_hardware_dir="$stack_root/hardware"', uploaded[0], ) + self.assertIn('legacy_runtime_dir="$HOME/blacknode-runtime"', uploaded[0]) + self.assertIn('legacy_hardware_dir="$HOME/blacknode-hardware"', uploaded[0]) + self.assertIn('layout="legacy"', uploaded[0]) self.assertIn( "git clone https://github.com/temiroff/blacknode-robot.git", uploaded[0], @@ -1172,6 +1182,56 @@ def fake_run(_connection, command, **kwargs): remote_python = uploaded[0].split("<<'PY'\n", 1)[1].rsplit("\nPY", 1)[0] compile(remote_python, "", "exec") + def test_hardware_environment_reports_legacy_layout_without_moving_runtime(self): + class RemoteFile(io.StringIO): + def __enter__(self): + return self + + def __exit__(self, *_args): + self.close() + return False + + class Sftp: + def file(self, _path, _mode): + return RemoteFile() + + def chmod(self, _path, _mode): + return None + + def close(self): + return None + + connection = SimpleNamespace( + client=SimpleNamespace(open_sftp=lambda: Sftp()), + fingerprint="SHA256:trusted-device-key", + close=lambda: None, + ) + + def fake_run(_connection, command, **_kwargs): + if command.startswith("bash "): + return ( + '__BLACKNODE_HARDWARE_INSTALL__={"hardware_dir":' + '"/home/ubuntu/blacknode-hardware","layout":"legacy",' + '"stack_mode":"isolated"}\n' + ) + return "" + + with ( + patch.object(device_installer, "_connect", return_value=connection), + patch.object(device_installer, "_run", side_effect=fake_run), + ): + result = device_installer.install_hardware_environment( + host="192.168.1.87", + port=22, + username="ubuntu", + password="ssh-password", + host_fingerprint="SHA256:trusted-device-key", + instance_id="default", + ) + + self.assertEqual(result["hardware_dir"], "/home/ubuntu/blacknode-hardware") + self.assertEqual(result["layout"], "legacy") + def test_default_stack_can_adopt_recognized_legacy_hardware_services(self): uploaded = []