mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(install): reap Windows cua installer process tree
This commit is contained in:
parent
26670bce95
commit
23d0dca832
2 changed files with 105 additions and 1 deletions
|
|
@ -1150,7 +1150,48 @@ def _run_cua_driver_installer(label: str = "Installing", verbose: bool = True) -
|
|||
if not is_windows:
|
||||
os.killpg(os.getpgid(proc.pid), _signal.SIGKILL) # windows-footgun: ok — POSIX branch only
|
||||
else:
|
||||
proc.kill()
|
||||
# PowerShell may leave download/install helpers alive after its
|
||||
# direct process is killed. Those descendants inherit stdout
|
||||
# and can keep both communicate() and install.lock wedged, so
|
||||
# collect the tree first and kill it leaf-up.
|
||||
import psutil as _psutil
|
||||
|
||||
try:
|
||||
parent = _psutil.Process(proc.pid)
|
||||
descendants = parent.children(recursive=True)
|
||||
except _psutil.NoSuchProcess:
|
||||
return
|
||||
except _psutil.Error as e:
|
||||
logger.debug(
|
||||
"could not enumerate cua-driver installer tree for pid %s: %s",
|
||||
proc.pid,
|
||||
e,
|
||||
)
|
||||
proc.kill()
|
||||
return
|
||||
|
||||
for child in reversed(descendants):
|
||||
try:
|
||||
child.kill()
|
||||
except _psutil.NoSuchProcess:
|
||||
pass
|
||||
except _psutil.Error as e:
|
||||
logger.debug(
|
||||
"could not kill cua-driver installer child pid %s: %s",
|
||||
child.pid,
|
||||
e,
|
||||
)
|
||||
try:
|
||||
parent.kill()
|
||||
except _psutil.NoSuchProcess:
|
||||
pass
|
||||
except _psutil.Error as e:
|
||||
logger.debug(
|
||||
"could not kill cua-driver installer parent pid %s: %s",
|
||||
proc.pid,
|
||||
e,
|
||||
)
|
||||
proc.kill()
|
||||
except (OSError, ProcessLookupError):
|
||||
proc.kill()
|
||||
|
||||
|
|
|
|||
|
|
@ -464,6 +464,69 @@ class TestInstallerTimeoutKillsProcessGroup:
|
|||
|
||||
assert captured.get("start_new_session") is True
|
||||
|
||||
def test_windows_timeout_kills_descendants_and_parent(self):
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock
|
||||
from hermes_cli import tools_config
|
||||
|
||||
child = MagicMock()
|
||||
parent = MagicMock()
|
||||
parent.children.return_value = [child]
|
||||
|
||||
fake_proc = MagicMock()
|
||||
fake_proc.pid = 12345
|
||||
fake_proc.communicate.side_effect = [
|
||||
subprocess.TimeoutExpired(cmd="powershell", timeout=1),
|
||||
("", None),
|
||||
]
|
||||
|
||||
with patch("platform.system", return_value="Windows"), \
|
||||
patch("subprocess.Popen", return_value=fake_proc), \
|
||||
patch("psutil.Process", return_value=parent), \
|
||||
patch.object(tools_config, "_clear_stale_cua_install_lock"), \
|
||||
patch.object(tools_config, "_print_warning"), \
|
||||
patch.object(tools_config, "_print_info"):
|
||||
ok = tools_config._run_cua_driver_installer(
|
||||
label="Refreshing", verbose=False
|
||||
)
|
||||
|
||||
assert ok is False
|
||||
parent.children.assert_called_once_with(recursive=True)
|
||||
child.kill.assert_called_once_with()
|
||||
parent.kill.assert_called_once_with()
|
||||
fake_proc.kill.assert_not_called()
|
||||
assert fake_proc.communicate.call_count == 2
|
||||
|
||||
def test_windows_tree_enumeration_failure_falls_back_to_direct_kill(self):
|
||||
import psutil
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock
|
||||
from hermes_cli import tools_config
|
||||
|
||||
parent = MagicMock()
|
||||
parent.children.side_effect = psutil.AccessDenied(pid=12345)
|
||||
|
||||
fake_proc = MagicMock()
|
||||
fake_proc.pid = 12345
|
||||
fake_proc.communicate.side_effect = [
|
||||
subprocess.TimeoutExpired(cmd="powershell", timeout=1),
|
||||
("", None),
|
||||
]
|
||||
|
||||
with patch("platform.system", return_value="Windows"), \
|
||||
patch("subprocess.Popen", return_value=fake_proc), \
|
||||
patch("psutil.Process", return_value=parent), \
|
||||
patch.object(tools_config, "_clear_stale_cua_install_lock"), \
|
||||
patch.object(tools_config, "_print_warning"), \
|
||||
patch.object(tools_config, "_print_info"):
|
||||
ok = tools_config._run_cua_driver_installer(
|
||||
label="Refreshing", verbose=False
|
||||
)
|
||||
|
||||
assert ok is False
|
||||
fake_proc.kill.assert_called_once_with()
|
||||
assert fake_proc.communicate.call_count == 2
|
||||
|
||||
|
||||
class TestInstallerNoShell:
|
||||
"""The POSIX installer path must not use shell=True or command
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue