From 88c19a4edf0adc5ca29037a1e857ab2c9ab81213 Mon Sep 17 00:00:00 2001 From: embwl0x Date: Wed, 29 Jul 2026 08:57:00 -0700 Subject: [PATCH] fix(cli): repair Windows cua-driver autostart registration for paths with spaces Detect a missing cua-driver-serve scheduled task after the installer runs and retry registration via Start-Process -FilePath/-ArgumentList instead of interpolating the binary path into a PowerShell command string (which splits at the first space in a username-space path). Salvaged from #60880 by @embwl0x. Related: #60808. --- hermes_cli/tools_config.py | 91 +++++++++++++++++ tests/hermes_cli/test_install_cua_driver.py | 104 +++++++++++++++++++- 2 files changed, 194 insertions(+), 1 deletion(-) diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 56b5d9d8c65..0f3c1d666e8 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -1242,6 +1242,91 @@ def _clear_stale_cua_install_lock() -> None: logger.debug("stale cua install lock check failed: %s", e) +def _ps_single_quote(value: str) -> str: + """Return a PowerShell single-quoted string literal.""" + return "'" + value.replace("'", "''") + "'" + + +def _cua_driver_autostart_registered_windows() -> bool: + """Return whether the Windows cua-driver scheduled task is registered.""" + if sys.platform != "win32": + return False + import subprocess + + try: + result = subprocess.run( + ["schtasks.exe", "/Query", "/TN", "cua-driver-serve"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=10, + ) + except Exception: + return False + return result.returncode == 0 + + +def _repair_cua_driver_autostart_windows(driver_cmd: str, *, verbose: bool) -> bool: + """Best-effort repair for Windows installer autostart quoting failures. + + Older install.ps1 builds invoked + ``& C:\\Users\\Name With Spaces\\...\\cua-driver`` from an elevated + PowerShell command string, which PowerShell split at the first space. If + the installer left the scheduled task missing, retry by + launching the resolved binary through Start-Process's structured + ``-FilePath`` / ``-ArgumentList`` parameters instead of interpolating a + path into a command string. + """ + if sys.platform != "win32": + return True + if _cua_driver_autostart_registered_windows(): + return True + + import subprocess + + binary = shutil.which(driver_cmd) + if not binary: + return False + + ps = shutil.which("powershell") or shutil.which("powershell.exe") or "powershell" + ps_cmd = ( + f"$exe = {_ps_single_quote(binary)}; " + "$proc = Start-Process -FilePath $exe " + "-ArgumentList @('autostart','enable') " + "-Verb RunAs -Wait -PassThru -ErrorAction Stop; " + "exit $proc.ExitCode" + ) + + if verbose: + _print_info(" Registering cua-driver auto-start...") + else: + _print_info(" Repairing cua-driver auto-start registration...") + + try: + result = subprocess.run( + [ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd], + capture_output=True, + text=True, + timeout=300, + env=_cua_driver_env(), + ) + except subprocess.TimeoutExpired: + _print_warning(" cua-driver autostart registration timed out.") + return False + except Exception as exc: + _print_warning(f" cua-driver autostart registration failed: {exc}") + return False + + if result.returncode == 0: + return True + + tail = (result.stderr or result.stdout or "").strip().splitlines()[-3:] + _print_warning(" cua-driver autostart registration failed.") + for line in tail: + _print_info(f" {line[:200]}") + _print_info(" From an elevated shell, run: cua-driver autostart enable") + return False + + def _run_cua_driver_installer( label: str = "Installing", verbose: bool = True, @@ -1461,6 +1546,12 @@ def _run_cua_driver_installer( if result.returncode != 0: logger.debug("cua-driver installer output:\n%s", result.stdout) if result.returncode == 0 and shutil.which(driver_cmd): + if is_windows and not _repair_cua_driver_autostart_windows( + driver_cmd, verbose=verbose + ): + _print_warning( + " cua-driver installed, but auto-start was not registered." + ) if verbose: _print_success(f" {driver_cmd} installed.") if is_windows: diff --git a/tests/hermes_cli/test_install_cua_driver.py b/tests/hermes_cli/test_install_cua_driver.py index 78cfbb25bd6..cc3f3012dd8 100644 --- a/tests/hermes_cli/test_install_cua_driver.py +++ b/tests/hermes_cli/test_install_cua_driver.py @@ -22,6 +22,7 @@ cleanly on missing-arch assets, and the upgrade path uses from __future__ import annotations import sys +from types import SimpleNamespace from unittest.mock import patch import pytest @@ -786,7 +787,6 @@ class TestRunInstallerPinEnv: into the installer child env; unpinned runs leave it untouched.""" def _run(self, pin_version): - import subprocess from unittest.mock import MagicMock from hermes_cli import tools_config @@ -825,3 +825,105 @@ class TestRunInstallerPinEnv: def test_no_pin_leaves_env_untouched(self): env = self._run(None) assert "CUA_DRIVER_RS_VERSION" not in env + + +class TestWindowsAutostartRepair: + def test_existing_task_skips_elevated_powershell_repair(self): + from hermes_cli import tools_config + + calls = [] + + def fake_run(cmd, **kwargs): + calls.append((cmd, kwargs)) + return SimpleNamespace(returncode=0) + + with patch.object(tools_config.sys, "platform", "win32"), \ + patch("subprocess.run", side_effect=fake_run), \ + patch.object(tools_config.shutil, "which") as which: + ok = tools_config._repair_cua_driver_autostart_windows( + "cua-driver", verbose=False + ) + + assert ok is True + assert [cmd for cmd, _kwargs in calls] == [ + ["schtasks.exe", "/Query", "/TN", "cua-driver-serve"] + ] + which.assert_not_called() + + def test_windows_installer_runs_autostart_repair_after_success(self): + from unittest.mock import MagicMock + from hermes_cli import tools_config + + captured = {} + fake_proc = MagicMock() + fake_proc.pid = 1 + fake_proc.returncode = 0 + fake_proc.communicate.return_value = ("", None) + + def fake_popen(cmd, **kwargs): + captured["cmd"] = cmd + captured["kwargs"] = kwargs + return fake_proc + + def fake_which(name: str): + if name == "cua-driver": + return r"C:\Users\Ha Trung\AppData\Local\Programs\Cua\cua-driver\bin\cua-driver.exe" + return None + + with patch("platform.system", return_value="Windows"), \ + patch.object(tools_config.shutil, "which", side_effect=fake_which), \ + patch("subprocess.Popen", side_effect=fake_popen), \ + patch.object(tools_config, "_clear_stale_cua_install_lock"), \ + patch.object(tools_config, "_repair_cua_driver_autostart_windows", return_value=True) as repair, \ + patch.object(tools_config, "_print_warning"), \ + patch.object(tools_config, "_print_info"), \ + patch.object(tools_config, "_print_success"): + ok = tools_config._run_cua_driver_installer(label="Refreshing", verbose=False) + + assert ok is True + assert captured["kwargs"].get("shell") is False + assert isinstance(captured["cmd"], list) + assert captured["cmd"][:4] == [ + "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", + ] + repair.assert_called_once_with("cua-driver", verbose=False) + + def test_autostart_repair_quotes_username_space_path_via_file_path(self): + from hermes_cli import tools_config + + calls = [] + driver = ( + r"C:\Users\Ha Trung\AppData\Local\Programs\Cua" + r"\cua-driver\bin\cua-driver.exe" + ) + + def fake_which(name: str): + if name == "cua-driver": + return driver + if name == "powershell": + return r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + return None + + def fake_run(cmd, **kwargs): + calls.append((cmd, kwargs)) + if cmd[0] == "schtasks.exe": + return SimpleNamespace(returncode=1) + return SimpleNamespace(returncode=0, stdout="", stderr="") + + with patch.object(tools_config.sys, "platform", "win32"), \ + patch.object(tools_config.shutil, "which", side_effect=fake_which), \ + patch("subprocess.run", side_effect=fake_run), \ + patch.object(tools_config, "_print_warning"), \ + patch.object(tools_config, "_print_info"): + ok = tools_config._repair_cua_driver_autostart_windows( + "cua-driver", verbose=False + ) + + assert ok is True + ps_calls = [cmd for cmd, _kwargs in calls if cmd[0].endswith("powershell.exe")] + assert len(ps_calls) == 1 + ps_command = ps_calls[0][-1] + assert "-FilePath $exe" in ps_command + assert "-ArgumentList @('autostart','enable')" in ps_command + assert f"$exe = '{driver}'" in ps_command + assert f"& {driver}" not in ps_command