790e1eb6bd
On Windows installs where the gateway runs as an SCM service (WinSW, NSSM, sc.exe create), the existing pause machinery kills the gateway process directly — and the service wrapper's failure ladder resurrects it within seconds, re-taking the venv file locks mid-update. The update then dies partway through dependency sync with access-denied errors. This extends _pause_windows_gateways_for_update() to detect when a gateway's process tree is owned by a running SCM service, and to stop the SERVICE through sc.exe instead of killing the child: - gateway/status.py: expose service-ownership discovery for gateway runtimes (find_windows_gateway_services maps validated gateway PIDs through process ancestry to running SCM service PIDs, with create-time identity checks against PID reuse). - hermes_cli/update_cmd.py: stop verified services via sc.exe before venv mutation and restart them afterward. Stops wait for a stable SCM 'stopped' state AND for the original descendant processes to exit (service 'Stopped' is not proof the child released its handles). Failure to prove ownership, stop a service, or restart it fails closed; rollback restores attempted services, and rollback failures are surfaced rather than swallowed. - Fail-closed throughout: unreadable identities, ambiguous ancestry, or a service that will not reach a stable state abort the update before any file mutation. Complements #37039 (gateway-only concurrent instances no longer abort): that fix lets the update proceed past the gate; this one makes the pause actually stick when the gateway is service-supervised. Note: tests/gateway/test_status.py::TestReadProcessCmdlinePsFallback:: test_ps_fallback_when_proc_unavailable fails on Windows on current main before this change as well (POSIX ps fallback asserted on a platform without it); all other touched suites pass (155 passed, 5 skipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
53 lines
2.0 KiB
Python
53 lines
2.0 KiB
Python
"""#84185: a Windows gateway cold-started after update that dies immediately
|
|
(e.g. a job object denying breakaway) must not be reported as started.
|
|
|
|
``_cold_start_windows_gateway_after_update`` used to print the success line
|
|
straight off a successful ``Popen`` return, which only proves the process was
|
|
created, not that it survived. This asserts the observable output: the
|
|
success line is gated on the process actually being found alive afterwards,
|
|
same as every other ``_spawn_detached`` caller.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from hermes_cli import gateway as hermes_gateway
|
|
from hermes_cli import gateway_windows
|
|
from hermes_cli import main as cli_main
|
|
from hermes_cli import update_cmd
|
|
|
|
|
|
def _run_cold_start(monkeypatch, capsys, *, surviving_pids):
|
|
monkeypatch.setattr(cli_main, "_is_windows", lambda: True)
|
|
|
|
# The pre-spawn re-check (``all_profiles=True``) must find nothing
|
|
# running so the cold-start path proceeds and actually spawns.
|
|
monkeypatch.setattr(
|
|
hermes_gateway,
|
|
"find_gateway_pids",
|
|
lambda all_profiles=False: [] if all_profiles else surviving_pids,
|
|
)
|
|
monkeypatch.setattr(gateway_windows, "_spawn_detached", lambda: 4242)
|
|
# Avoid the real 6s/0.4s poll loop in _report_gateway_start.
|
|
monkeypatch.setattr(
|
|
gateway_windows, "_wait_for_gateway_ready", lambda *a, **k: surviving_pids
|
|
)
|
|
|
|
update_cmd._cold_start_windows_gateway_after_update()
|
|
|
|
return capsys.readouterr().out
|
|
|
|
|
|
def test_cold_start_raises_when_process_does_not_survive(monkeypatch, capsys):
|
|
with pytest.raises(RuntimeError, match="did not become ready"):
|
|
_run_cold_start(monkeypatch, capsys, surviving_pids=[])
|
|
|
|
assert "✓ Starting Windows gateway after update" not in capsys.readouterr().out
|
|
|
|
|
|
def test_cold_start_reports_success_when_process_survives(monkeypatch, capsys):
|
|
out = _run_cold_start(monkeypatch, capsys, surviving_pids=[4242])
|
|
|
|
assert "✓ Gateway started via cold-start after update" in out
|