* fix: guard dashboard update in Docker * fix(dashboard): align action response type --------- Co-authored-by: Donovan Yohan <donovan-yohan@users.noreply.github.com> Co-authored-by: Donovan Yohan <34756395+donovan-yohan@users.noreply.github.com>
This commit is contained in:
@ -49,6 +49,9 @@ from hermes_cli.config import (
|
||||
save_env_value,
|
||||
remove_env_value,
|
||||
check_config_version,
|
||||
detect_install_method,
|
||||
format_docker_update_message,
|
||||
recommended_update_command_for_method,
|
||||
redact_key,
|
||||
)
|
||||
from gateway.status import get_running_pid, read_runtime_status
|
||||
@ -772,6 +775,26 @@ _ACTION_LOG_FILES: Dict[str, str] = {
|
||||
# report liveness and exit code without shelling out to ``ps``.
|
||||
_ACTION_PROCS: Dict[str, subprocess.Popen] = {}
|
||||
|
||||
# ``name`` → completed synthetic action result for actions the server handled
|
||||
# without spawning a subprocess (for example, unsupported Docker updates).
|
||||
_ACTION_RESULTS: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
def _record_completed_action(name: str, message: str, exit_code: int = 1) -> None:
|
||||
"""Record a non-spawned action result and write it to the action log."""
|
||||
log_file_name = _ACTION_LOG_FILES[name]
|
||||
_ACTION_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
log_path = _ACTION_LOG_DIR / log_file_name
|
||||
with open(log_path, "ab", buffering=0) as log_file:
|
||||
log_file.write(
|
||||
f"\n=== {name} completed {time.strftime('%Y-%m-%d %H:%M:%S')} ===\n".encode()
|
||||
)
|
||||
log_file.write(message.encode("utf-8", errors="replace"))
|
||||
if not message.endswith("\n"):
|
||||
log_file.write(b"\n")
|
||||
_ACTION_PROCS.pop(name, None)
|
||||
_ACTION_RESULTS[name] = {"exit_code": exit_code, "pid": None}
|
||||
|
||||
|
||||
def _spawn_hermes_action(subcommand: List[str], name: str) -> subprocess.Popen:
|
||||
"""Spawn ``hermes <subcommand>`` detached and record the Popen handle.
|
||||
@ -805,6 +828,7 @@ def _spawn_hermes_action(subcommand: List[str], name: str) -> subprocess.Popen:
|
||||
popen_kwargs["start_new_session"] = True
|
||||
|
||||
proc = subprocess.Popen(cmd, **popen_kwargs)
|
||||
_ACTION_RESULTS.pop(name, None)
|
||||
_ACTION_PROCS[name] = proc
|
||||
return proc
|
||||
|
||||
@ -841,6 +865,19 @@ async def restart_gateway():
|
||||
@app.post("/api/hermes/update")
|
||||
async def update_hermes():
|
||||
"""Kick off ``hermes update`` in the background."""
|
||||
install_method = detect_install_method(PROJECT_ROOT)
|
||||
if install_method == "docker":
|
||||
message = format_docker_update_message()
|
||||
_record_completed_action("hermes-update", message, exit_code=1)
|
||||
return {
|
||||
"ok": False,
|
||||
"pid": None,
|
||||
"name": "hermes-update",
|
||||
"error": "docker_update_unsupported",
|
||||
"message": message,
|
||||
"update_command": recommended_update_command_for_method(install_method),
|
||||
}
|
||||
|
||||
try:
|
||||
proc = _spawn_hermes_action(["update"], "hermes-update")
|
||||
except Exception as exc:
|
||||
@ -1065,9 +1102,10 @@ async def get_action_status(name: str, lines: int = 200):
|
||||
|
||||
proc = _ACTION_PROCS.get(name)
|
||||
if proc is None:
|
||||
result = _ACTION_RESULTS.get(name)
|
||||
running = False
|
||||
exit_code: Optional[int] = None
|
||||
pid: Optional[int] = None
|
||||
exit_code = result.get("exit_code") if result else None
|
||||
pid = result.get("pid") if result else None
|
||||
else:
|
||||
exit_code = proc.poll()
|
||||
running = exit_code is None
|
||||
|
||||
@ -348,6 +348,66 @@ class TestWebServerEndpoints:
|
||||
resp = self.client.post("/api/audio/speak", json={"text": " "})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_update_hermes_returns_docker_guidance_without_spawning(self, monkeypatch):
|
||||
import hermes_cli.web_server as web_server
|
||||
|
||||
spawned = False
|
||||
|
||||
def fail_spawn(*_args, **_kwargs):
|
||||
nonlocal spawned
|
||||
spawned = True
|
||||
raise AssertionError("docker update guard should not spawn hermes update")
|
||||
|
||||
monkeypatch.setattr(web_server, "detect_install_method", lambda _root: "docker")
|
||||
monkeypatch.setattr(web_server, "_spawn_hermes_action", fail_spawn)
|
||||
web_server._ACTION_PROCS.pop("hermes-update", None)
|
||||
web_server._ACTION_RESULTS.pop("hermes-update", None)
|
||||
|
||||
resp = self.client.post("/api/hermes/update")
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["ok"] is False
|
||||
assert data["name"] == "hermes-update"
|
||||
assert data["pid"] is None
|
||||
assert data["error"] == "docker_update_unsupported"
|
||||
assert "docker pull nousresearch/hermes-agent:latest" in data["message"]
|
||||
assert spawned is False
|
||||
|
||||
status = self.client.get("/api/actions/hermes-update/status")
|
||||
assert status.status_code == 200
|
||||
status_data = status.json()
|
||||
assert status_data["running"] is False
|
||||
assert status_data["exit_code"] == 1
|
||||
assert status_data["pid"] is None
|
||||
assert any("docker pull nousresearch/hermes-agent:latest" in line for line in status_data["lines"])
|
||||
|
||||
def test_update_hermes_spawns_on_non_docker_install(self, monkeypatch):
|
||||
import hermes_cli.web_server as web_server
|
||||
|
||||
class Proc:
|
||||
pid = 12345
|
||||
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_spawn(subcommand, name):
|
||||
calls.append((subcommand, name))
|
||||
return Proc()
|
||||
|
||||
monkeypatch.setattr(web_server, "detect_install_method", lambda _root: "git")
|
||||
monkeypatch.setattr(web_server, "_spawn_hermes_action", fake_spawn)
|
||||
web_server._ACTION_PROCS.pop("hermes-update", None)
|
||||
web_server._ACTION_RESULTS.pop("hermes-update", None)
|
||||
|
||||
resp = self.client.post("/api/hermes/update")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"ok": True, "pid": 12345, "name": "hermes-update"}
|
||||
assert calls == [(["update"], "hermes-update")]
|
||||
|
||||
def test_get_status_filters_unconfigured_gateway_platforms(self, monkeypatch):
|
||||
import gateway.config as gateway_config
|
||||
import hermes_cli.web_server as web_server
|
||||
|
||||
@ -526,7 +526,10 @@ export interface AuthMeResponse {
|
||||
export interface ActionResponse {
|
||||
name: string;
|
||||
ok: boolean;
|
||||
pid: number;
|
||||
pid: number | null;
|
||||
error?: string;
|
||||
message?: string;
|
||||
update_command?: string;
|
||||
}
|
||||
|
||||
/** Per-call overrides for {@link fetchJSON}. */
|
||||
|
||||
Reference in New Issue
Block a user