mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
fix(gateway): probe Windows clients safely
This commit is contained in:
@@ -26,6 +26,7 @@ from nanobot.process_runtime import (
|
|||||||
ProcessRuntimePaths,
|
ProcessRuntimePaths,
|
||||||
ProcessStartOptions,
|
ProcessStartOptions,
|
||||||
ProcessStatus,
|
ProcessStatus,
|
||||||
|
process_is_running,
|
||||||
)
|
)
|
||||||
|
|
||||||
GatewayStartOptions = ProcessStartOptions
|
GatewayStartOptions = ProcessStartOptions
|
||||||
@@ -374,7 +375,7 @@ class GatewayClientLease:
|
|||||||
|
|
||||||
def _process_is_running(self, pid: int) -> bool:
|
def _process_is_running(self, pid: int) -> bool:
|
||||||
checker = getattr(self.runtime, "process_is_running", None)
|
checker = getattr(self.runtime, "process_is_running", None)
|
||||||
return bool(checker(pid)) if callable(checker) else _pid_is_running(pid)
|
return bool(checker(pid)) if callable(checker) else process_is_running(pid)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _clients(state: dict[str, object]) -> dict[str, object]:
|
def _clients(state: dict[str, object]) -> dict[str, object]:
|
||||||
@@ -442,17 +443,3 @@ def _instance_suffix(*, workspace: str | None, config_path: str | None) -> str |
|
|||||||
if not raw:
|
if not raw:
|
||||||
return None
|
return None
|
||||||
return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16]
|
return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16]
|
||||||
|
|
||||||
|
|
||||||
def _pid_is_running(pid: int) -> bool:
|
|
||||||
if pid <= 0:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
os.kill(pid, 0)
|
|
||||||
except ProcessLookupError:
|
|
||||||
return False
|
|
||||||
except PermissionError:
|
|
||||||
return True
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|||||||
+20
-11
@@ -356,17 +356,7 @@ class ManagedProcessRuntime(Generic[_StartOptionsT]):
|
|||||||
return poll() is None
|
return poll() is None
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
if self.platform_name == "Windows":
|
return process_is_running(pid, platform_name=self.platform_name)
|
||||||
return _windows_process_identity(pid) is not None
|
|
||||||
try:
|
|
||||||
os.kill(pid, 0)
|
|
||||||
except ProcessLookupError:
|
|
||||||
return False
|
|
||||||
except PermissionError:
|
|
||||||
return True
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _process_identity(self, pid: int) -> str | int | None:
|
def _process_identity(self, pid: int) -> str | int | None:
|
||||||
if self.platform_name == "Windows":
|
if self.platform_name == "Windows":
|
||||||
@@ -455,6 +445,25 @@ def _platform_name() -> str:
|
|||||||
return "Linux"
|
return "Linux"
|
||||||
|
|
||||||
|
|
||||||
|
def process_is_running(pid: int, *, platform_name: str | None = None) -> bool:
|
||||||
|
"""Probe a PID without delivering a control event on Windows."""
|
||||||
|
if pid <= 0:
|
||||||
|
return False
|
||||||
|
if (platform_name or _platform_name()) == "Windows":
|
||||||
|
# On Windows ``os.kill(pid, 0)`` sends CTRL_C_EVENT (whose value is 0)
|
||||||
|
# instead of performing the harmless POSIX existence probe.
|
||||||
|
return _windows_process_identity(pid) is not None
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
except ProcessLookupError:
|
||||||
|
return False
|
||||||
|
except PermissionError:
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _utc_now() -> str:
|
def _utc_now() -> str:
|
||||||
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from nanobot.gateway import (
|
|||||||
GatewayStatus,
|
GatewayStatus,
|
||||||
)
|
)
|
||||||
from nanobot.gateway.runtime import monitor_gateway_clients
|
from nanobot.gateway.runtime import monitor_gateway_clients
|
||||||
|
from nanobot.process_runtime import process_is_running
|
||||||
|
|
||||||
|
|
||||||
class FakeProcess:
|
class FakeProcess:
|
||||||
@@ -436,6 +437,20 @@ def test_start_background_uses_windows_process_group_flags(tmp_path, monkeypatch
|
|||||||
assert "start_new_session" not in calls[0]["kwargs"]
|
assert "start_new_session" not in calls[0]["kwargs"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_windows_process_probe_never_sends_ctrl_c(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.process_runtime._windows_process_identity",
|
||||||
|
lambda pid: "created-at" if pid == 12345 else None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.process_runtime.os.kill",
|
||||||
|
lambda *_args: pytest.fail("Windows process probes must not call os.kill(pid, 0)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert process_is_running(12345, platform_name="Windows") is True
|
||||||
|
assert process_is_running(54321, platform_name="Windows") is False
|
||||||
|
|
||||||
|
|
||||||
def test_status_clears_stale_state(tmp_path, monkeypatch):
|
def test_status_clears_stale_state(tmp_path, monkeypatch):
|
||||||
runtime = GatewayRuntime(paths=_paths(tmp_path), platform_name="Linux")
|
runtime = GatewayRuntime(paths=_paths(tmp_path), platform_name="Linux")
|
||||||
runtime.paths.run_dir.mkdir(parents=True)
|
runtime.paths.run_dir.mkdir(parents=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user