mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
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.
This commit is contained in:
parent
2164e548b1
commit
88c19a4edf
2 changed files with 194 additions and 1 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue