fix(gateway): handle Windows stop fallback
This commit is contained in:
@@ -293,14 +293,26 @@ class GatewayRuntime:
|
|||||||
def _terminate_windows(self, pid: int, *, timeout_s: int) -> bool:
|
def _terminate_windows(self, pid: int, *, timeout_s: int) -> bool:
|
||||||
ctrl_break = getattr(signal, "CTRL_BREAK_EVENT", None)
|
ctrl_break = getattr(signal, "CTRL_BREAK_EVENT", None)
|
||||||
if ctrl_break is not None:
|
if ctrl_break is not None:
|
||||||
with suppress(ProcessLookupError):
|
# Detached Windows children can reject CTRL_BREAK_EVENT with WinError 87;
|
||||||
|
# keep the existing taskkill fallback for that process shape.
|
||||||
|
with suppress(ProcessLookupError, OSError):
|
||||||
os.kill(pid, ctrl_break)
|
os.kill(pid, ctrl_break)
|
||||||
if self._wait_for_exit(pid, timeout_s):
|
if self._wait_for_exit(pid, timeout_s):
|
||||||
return True
|
return True
|
||||||
self._subprocess_run(["taskkill", "/PID", str(pid), "/T"], check=False)
|
self._subprocess_run(
|
||||||
|
["taskkill", "/PID", str(pid), "/T"],
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
if self._wait_for_exit(pid, 2):
|
if self._wait_for_exit(pid, 2):
|
||||||
return True
|
return True
|
||||||
self._subprocess_run(["taskkill", "/PID", str(pid), "/T", "/F"], check=False)
|
self._subprocess_run(
|
||||||
|
["taskkill", "/PID", str(pid), "/T", "/F"],
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
return self._wait_for_exit(pid, 2)
|
return self._wait_for_exit(pid, 2)
|
||||||
|
|
||||||
def _wait_for_exit(self, pid: int, timeout_s: int | float) -> bool:
|
def _wait_for_exit(self, pid: int, timeout_s: int | float) -> bool:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from nanobot.gateway import GatewayRuntime, GatewayRuntimePaths, GatewayStartOptions
|
from nanobot.gateway import GatewayRuntime, GatewayRuntimePaths, GatewayStartOptions
|
||||||
@@ -168,3 +170,42 @@ def test_stop_keeps_state_when_process_survives_timeout(tmp_path, monkeypatch):
|
|||||||
assert result.status.running is True
|
assert result.status.running is True
|
||||||
assert result.status.reason == "stop_timeout"
|
assert result.status.reason == "stop_timeout"
|
||||||
assert runtime.paths.state_path.exists()
|
assert runtime.paths.state_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_terminate_windows_falls_back_when_ctrl_break_is_rejected(tmp_path, monkeypatch):
|
||||||
|
taskkill_calls: list[dict] = []
|
||||||
|
|
||||||
|
def fake_run(command, **kwargs):
|
||||||
|
taskkill_calls.append({"command": command, "kwargs": kwargs})
|
||||||
|
|
||||||
|
runtime = GatewayRuntime(
|
||||||
|
paths=_paths(tmp_path),
|
||||||
|
platform_name="Windows",
|
||||||
|
subprocess_run=fake_run,
|
||||||
|
sleep=lambda _seconds: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(signal, "CTRL_BREAK_EVENT", 1, raising=False)
|
||||||
|
|
||||||
|
def fake_kill(_pid, _signal):
|
||||||
|
raise OSError(87, "The parameter is incorrect")
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.gateway.runtime.os.kill", fake_kill)
|
||||||
|
|
||||||
|
def fake_wait_for_exit(_pid, _timeout_s):
|
||||||
|
# Simulate a process that only exits after the taskkill fallback runs.
|
||||||
|
return bool(taskkill_calls)
|
||||||
|
|
||||||
|
monkeypatch.setattr(runtime, "_wait_for_exit", fake_wait_for_exit)
|
||||||
|
|
||||||
|
assert runtime._terminate_windows(12345, timeout_s=20) is True
|
||||||
|
assert taskkill_calls == [
|
||||||
|
{
|
||||||
|
"command": ["taskkill", "/PID", "12345", "/T"],
|
||||||
|
"kwargs": {
|
||||||
|
"check": False,
|
||||||
|
"stdout": subprocess.DEVNULL,
|
||||||
|
"stderr": subprocess.DEVNULL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user