mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +03:00
fix(gateway): register foreground instances
This commit is contained in:
@@ -387,19 +387,27 @@ def _run_gateway(
|
|||||||
raise typer.Exit(1) from exc
|
raise typer.Exit(1) from exc
|
||||||
session_manager = SessionManager(config.workspace_path)
|
session_manager = SessionManager(config.workspace_path)
|
||||||
|
|
||||||
# Self-heal the gateway state file with the current PID after any restart.
|
# Use the same runtime identity for foreground and managed gateway processes.
|
||||||
from nanobot.config.loader import get_config_path
|
from nanobot.config.loader import get_config_path
|
||||||
from nanobot.gateway.runtime import GatewayRuntime, GatewayRuntimePaths
|
from nanobot.gateway.runtime import GatewayRuntime, GatewayRuntimePaths, GatewayStartOptions
|
||||||
|
|
||||||
config_path = str(get_config_path().resolve(strict=False))
|
config_path = str(get_config_path().resolve(strict=False))
|
||||||
GatewayRuntime.refresh_state_pid(
|
gateway_workspace = (
|
||||||
|
str(config.workspace_path)
|
||||||
|
if not is_default_workspace(config.workspace_path)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
gateway_runtime = GatewayRuntime(
|
||||||
paths=GatewayRuntimePaths.for_instance(
|
paths=GatewayRuntimePaths.for_instance(
|
||||||
workspace=str(config.workspace_path)
|
workspace=gateway_workspace,
|
||||||
if not is_default_workspace(config.workspace_path)
|
|
||||||
else None,
|
|
||||||
config_path=config_path,
|
config_path=config_path,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
gateway_start_options = GatewayStartOptions(
|
||||||
|
port=port,
|
||||||
|
workspace=gateway_workspace,
|
||||||
|
config_path=config_path,
|
||||||
|
)
|
||||||
|
|
||||||
# Preserve existing single-workspace installs, but keep custom workspaces clean.
|
# Preserve existing single-workspace installs, but keep custom workspaces clean.
|
||||||
if is_default_workspace(config.workspace_path):
|
if is_default_workspace(config.workspace_path):
|
||||||
@@ -946,4 +954,8 @@ def _run_gateway(
|
|||||||
finally:
|
finally:
|
||||||
restore_shutdown_handlers()
|
restore_shutdown_handlers()
|
||||||
|
|
||||||
asyncio.run(run())
|
gateway_runtime.claim_current_process(gateway_start_options)
|
||||||
|
try:
|
||||||
|
asyncio.run(run())
|
||||||
|
finally:
|
||||||
|
gateway_runtime.release_current_process()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
@@ -102,6 +103,33 @@ class GatewayRuntime(ManagedProcessRuntime[ProcessStartOptions]):
|
|||||||
def _build_child_command(self, options: ProcessStartOptions) -> list[str]:
|
def _build_child_command(self, options: ProcessStartOptions) -> list[str]:
|
||||||
return build_gateway_command(self.python_executable, options)
|
return build_gateway_command(self.python_executable, options)
|
||||||
|
|
||||||
|
def claim_current_process(self, options: ProcessStartOptions) -> None:
|
||||||
|
"""Publish the running foreground gateway for local client discovery."""
|
||||||
|
with self._lifecycle_lock():
|
||||||
|
state = self._read_state() or {}
|
||||||
|
pid = os.getpid()
|
||||||
|
state.update(
|
||||||
|
{
|
||||||
|
"pid": pid,
|
||||||
|
"identity": self._process_identity(pid),
|
||||||
|
"started_at": datetime.now(UTC).isoformat(),
|
||||||
|
"platform": self.platform_name,
|
||||||
|
"port": options.port,
|
||||||
|
"workspace": options.workspace,
|
||||||
|
"config_path": options.config_path,
|
||||||
|
"command": self._build_child_command(options),
|
||||||
|
"log_path": str(self.paths.log_path),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self._write_state(state)
|
||||||
|
|
||||||
|
def release_current_process(self) -> None:
|
||||||
|
"""Remove this foreground gateway's state without touching a replacement."""
|
||||||
|
with self._lifecycle_lock():
|
||||||
|
state = self._read_state()
|
||||||
|
if state and self._record_matches_process(state, os.getpid()):
|
||||||
|
self._clear_state()
|
||||||
|
|
||||||
def restart(self, options: ProcessStartOptions, *, timeout_s: int = 20) -> ProcessResult:
|
def restart(self, options: ProcessStartOptions, *, timeout_s: int = 20) -> ProcessResult:
|
||||||
"""Restart an existing gateway without creating a new persistent instance."""
|
"""Restart an existing gateway without creating a new persistent instance."""
|
||||||
with self._lifecycle_lock():
|
with self._lifecycle_lock():
|
||||||
|
|||||||
@@ -2658,6 +2658,10 @@ def test_webui_foreground_reports_an_existing_gateway_without_leaking_secret(
|
|||||||
_patch_webui_provider_ready(monkeypatch)
|
_patch_webui_provider_ready(monkeypatch)
|
||||||
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
||||||
monkeypatch.setattr("nanobot.cli.webui._gateway_health_ready", lambda *_args, **_kwargs: True)
|
monkeypatch.setattr("nanobot.cli.webui._gateway_health_ready", lambda *_args, **_kwargs: True)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.cli.webui_support._gateway_health_ready",
|
||||||
|
lambda *_args, **_kwargs: True,
|
||||||
|
)
|
||||||
monkeypatch.setattr("nanobot.cli.webui._webui_endpoint_reachable", lambda *_args, **_kwargs: False)
|
monkeypatch.setattr("nanobot.cli.webui._webui_endpoint_reachable", lambda *_args, **_kwargs: False)
|
||||||
|
|
||||||
result = runner.invoke(app, ["webui", "--config", str(config_file), "--yes"])
|
result = runner.invoke(app, ["webui", "--config", str(config_file), "--yes"])
|
||||||
|
|||||||
@@ -104,6 +104,48 @@ def test_start_background_writes_state_and_child_command(tmp_path, monkeypatch):
|
|||||||
assert state["port"] == 18790
|
assert state["port"] == 18790
|
||||||
|
|
||||||
|
|
||||||
|
def test_foreground_gateway_claim_is_discoverable_and_released(tmp_path, monkeypatch):
|
||||||
|
runtime = GatewayRuntime(
|
||||||
|
paths=_paths(tmp_path),
|
||||||
|
platform_name="Darwin",
|
||||||
|
python_executable="/python",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(runtime, "_process_identity", lambda _pid: 54321)
|
||||||
|
monkeypatch.setattr(runtime, "_is_pid_running", lambda _pid: True)
|
||||||
|
options = GatewayStartOptions(
|
||||||
|
port=18790,
|
||||||
|
workspace="/tmp/workspace",
|
||||||
|
config_path="/tmp/config.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime.claim_current_process(options)
|
||||||
|
|
||||||
|
status = runtime.status()
|
||||||
|
assert status.running is True
|
||||||
|
assert status.pid == os.getpid()
|
||||||
|
assert status.port == 18790
|
||||||
|
assert status.command == tuple(runtime._build_child_command(options))
|
||||||
|
|
||||||
|
runtime.release_current_process()
|
||||||
|
|
||||||
|
assert runtime.status().running is False
|
||||||
|
assert not runtime.paths.state_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_foreground_gateway_release_preserves_a_replacement_state(tmp_path, monkeypatch):
|
||||||
|
runtime = GatewayRuntime(paths=_paths(tmp_path), platform_name="Darwin")
|
||||||
|
monkeypatch.setattr(runtime, "_process_identity", lambda pid: pid)
|
||||||
|
runtime.claim_current_process(GatewayStartOptions(port=18790))
|
||||||
|
replacement = json.loads(runtime.paths.state_path.read_text(encoding="utf-8"))
|
||||||
|
replacement["pid"] = os.getpid() + 1
|
||||||
|
replacement["identity"] = replacement["pid"]
|
||||||
|
runtime.paths.state_path.write_text(json.dumps(replacement), encoding="utf-8")
|
||||||
|
|
||||||
|
runtime.release_current_process()
|
||||||
|
|
||||||
|
assert runtime.paths.state_path.exists()
|
||||||
|
|
||||||
|
|
||||||
def test_stop_reaps_an_owned_child_without_consuming_the_shutdown_timeout(
|
def test_stop_reaps_an_owned_child_without_consuming_the_shutdown_timeout(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
monkeypatch,
|
monkeypatch,
|
||||||
|
|||||||
Reference in New Issue
Block a user