mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
fix(gateway): allow Windows launcher PID handoff
This commit is contained in:
@@ -234,6 +234,7 @@ class GatewayRuntime(ManagedProcessRuntime[ProcessStartOptions]):
|
|||||||
state = self._read_state()
|
state = self._read_state()
|
||||||
if state and result.status.pid == state.get("pid"):
|
if state and result.status.pid == state.get("pid"):
|
||||||
state["launch_mode"] = "background"
|
state["launch_mode"] = "background"
|
||||||
|
state["pending_pid_handoff"] = True
|
||||||
self._write_state(state)
|
self._write_state(state)
|
||||||
return RuntimeResult(True, result.message, self.status())
|
return RuntimeResult(True, result.message, self.status())
|
||||||
|
|
||||||
@@ -288,14 +289,24 @@ class GatewayRuntime(ManagedProcessRuntime[ProcessStartOptions]):
|
|||||||
lease.wait_for_shutdown()
|
lease.wait_for_shutdown()
|
||||||
with self._transition_lock(), self._lifecycle_lock():
|
with self._transition_lock(), self._lifecycle_lock():
|
||||||
current = self.status()
|
current = self.status()
|
||||||
if current.running and current.pid != pid:
|
state = self._read_state() or {}
|
||||||
|
pid_handoff = (
|
||||||
|
self.platform_name == "Windows"
|
||||||
|
and current.running
|
||||||
|
and current.pid != pid
|
||||||
|
and current.pid == os.getppid()
|
||||||
|
and state.get("pid") == current.pid
|
||||||
|
and state.get("launch_mode") == "background"
|
||||||
|
and state.get("pending_pid_handoff") is True
|
||||||
|
)
|
||||||
|
if current.running and current.pid != pid and not pid_handoff:
|
||||||
raise GatewayAlreadyRunningError(current)
|
raise GatewayAlreadyRunningError(current)
|
||||||
if lease._shutdown_pending_locked():
|
if lease._shutdown_pending_locked():
|
||||||
continue
|
continue
|
||||||
state = self._read_state() or {}
|
|
||||||
launch_mode: GatewayLaunchMode = (
|
launch_mode: GatewayLaunchMode = (
|
||||||
"background"
|
"background"
|
||||||
if state.get("pid") == pid and state.get("launch_mode") == "background"
|
if state.get("launch_mode") == "background"
|
||||||
|
and (state.get("pid") == pid or pid_handoff)
|
||||||
else "foreground"
|
else "foreground"
|
||||||
)
|
)
|
||||||
state.update(
|
state.update(
|
||||||
@@ -311,6 +322,7 @@ class GatewayRuntime(ManagedProcessRuntime[ProcessStartOptions]):
|
|||||||
"launch_mode": launch_mode,
|
"launch_mode": launch_mode,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
state.pop("pending_pid_handoff", None)
|
||||||
state.pop("stable_identity", None)
|
state.pop("stable_identity", None)
|
||||||
state.update(self.process_identity_record(pid))
|
state.update(self.process_identity_record(pid))
|
||||||
self._write_state(state)
|
self._write_state(state)
|
||||||
|
|||||||
@@ -120,6 +120,29 @@ def _wait_for_claim(
|
|||||||
pytest.fail(f"gateway claim failed: returncode={process.poll()}, {detail}")
|
pytest.fail(f"gateway claim failed: returncode={process.poll()}, {detail}")
|
||||||
|
|
||||||
|
|
||||||
|
class _ManagedForegroundChildRuntime(GatewayRuntime):
|
||||||
|
"""Launch the foreground claim helper through the managed-process path."""
|
||||||
|
|
||||||
|
def __init__(self, *, data_dir: Path, marker: Path) -> None:
|
||||||
|
super().__init__(
|
||||||
|
paths=_paths(data_dir),
|
||||||
|
platform_name="Windows",
|
||||||
|
python_executable=sys.executable,
|
||||||
|
)
|
||||||
|
self._data_dir = data_dir
|
||||||
|
self._marker = marker
|
||||||
|
|
||||||
|
def _build_child_command(self, options: GatewayStartOptions) -> list[str]:
|
||||||
|
return [
|
||||||
|
self.python_executable,
|
||||||
|
"-c",
|
||||||
|
_FOREGROUND_CHILD,
|
||||||
|
str(self._data_dir),
|
||||||
|
"30",
|
||||||
|
str(self._marker),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_paths_use_stable_instance_suffix_for_custom_selectors(tmp_path):
|
def test_paths_use_stable_instance_suffix_for_custom_selectors(tmp_path):
|
||||||
default_paths = GatewayRuntimePaths.for_instance(data_dir=tmp_path)
|
default_paths = GatewayRuntimePaths.for_instance(data_dir=tmp_path)
|
||||||
first_paths = GatewayRuntimePaths.for_instance(
|
first_paths = GatewayRuntimePaths.for_instance(
|
||||||
@@ -775,6 +798,32 @@ 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"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
os.name != "nt" or sys.prefix == sys.base_prefix,
|
||||||
|
reason="requires a Windows virtualenv launcher",
|
||||||
|
)
|
||||||
|
def test_managed_background_hands_off_virtualenv_launcher_pid(tmp_path: Path) -> None:
|
||||||
|
marker = tmp_path / "managed-foreground.marker"
|
||||||
|
runtime = _ManagedForegroundChildRuntime(data_dir=tmp_path, marker=marker)
|
||||||
|
|
||||||
|
result = runtime.start_on_demand(GatewayStartOptions(port=18790))
|
||||||
|
launcher = runtime._owned_process
|
||||||
|
assert isinstance(launcher, subprocess.Popen)
|
||||||
|
try:
|
||||||
|
claimed_pid = _wait_for_claim(launcher, marker)
|
||||||
|
|
||||||
|
status = runtime.status()
|
||||||
|
assert result.ok is True
|
||||||
|
assert claimed_pid != launcher.pid
|
||||||
|
assert status.pid == claimed_pid
|
||||||
|
assert status.launch_mode == "background"
|
||||||
|
assert status.lifetime == "on_demand"
|
||||||
|
finally:
|
||||||
|
if runtime.status().running:
|
||||||
|
runtime.stop(timeout_s=3)
|
||||||
|
launcher.wait(timeout=3)
|
||||||
|
|
||||||
|
|
||||||
def test_windows_process_probe_never_sends_ctrl_c(monkeypatch):
|
def test_windows_process_probe_never_sends_ctrl_c(monkeypatch):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"nanobot.process_runtime._windows_process_identity",
|
"nanobot.process_runtime._windows_process_identity",
|
||||||
|
|||||||
Reference in New Issue
Block a user