fix(update): preserve SSH-owned backends during cleanup

This commit is contained in:
fangliquan
2026-08-22 10:41:53 +08:00
committed by Teknium
parent b2bd1ac63f
commit 1676c614b3
2 changed files with 47 additions and 6 deletions
+9 -6
View File
@@ -360,24 +360,27 @@ def _kill_stale_dashboard_processes(
# When the Hermes Desktop Electron app spawns this dashboard as a
# backend child, it sets HERMES_DESKTOP_CHILD_PID so that the update
# path can skip killing the desktop-managed process. (#37532)
exclude: set[int] | None = None
exclude: set[int] = set()
raw_pid = os.environ.get("HERMES_DESKTOP_CHILD_PID")
if raw_pid:
# The desktop may manage several backends (one per active profile) and
# passes them comma-separated; a lone int still parses for back-compat.
parsed: set[int] = set()
for part in raw_pid.split(","):
part = part.strip()
if not part:
continue
try:
parsed.add(int(part))
exclude.add(int(part))
except (ValueError, TypeError):
pass
if parsed:
exclude = parsed
pids = _m()._find_stale_dashboard_pids(exclude_pids=exclude)
# An SSH-owned backend belongs to an attached Desktop client even when the
# updater runs from an unrelated remote shell with no Desktop child PID.
# Honor the same validated ownership records as the orphan reaper; killing
# one permanently strands that client's fixed SSH port-forward.
exclude |= _lock_owned_serve_pids()
pids = _m()._find_stale_dashboard_pids(exclude_pids=exclude or None)
if not pids:
return {"matched": [], "killed": [], "failed": []}
@@ -14,6 +14,7 @@ History:
from __future__ import annotations
import importlib
import json
import os
import subprocess
import sys
@@ -87,6 +88,43 @@ def _ps_runner(stdout: str):
return _side_effect
def test_update_cleanup_spares_backend_owned_by_valid_ssh_lock(tmp_path, monkeypatch):
pid = 4242
ownership_id = "a" * 32
spawn_nonce = "b" * 16
lock_dir = tmp_path / "desktop-ssh" / ownership_id
lock_dir.mkdir(parents=True)
(lock_dir / "backend.lock.json").write_text(json.dumps({
"schemaVersion": 2,
"protocolVersion": 1,
"ownershipId": ownership_id,
"spawnNonce": spawn_nonce,
"tokenFingerprint": "c" * 32,
"pid": pid,
"port": 46369,
"profile": "default",
"hermesPath": "/opt/hermes/bin/hermes",
"hermesHome": str(tmp_path),
"logPath": f"{tmp_path}/desktop-ssh/{ownership_id}/{spawn_nonce}.log",
"startedAt": "2026-08-21T15:27:39Z",
}))
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.delenv("HERMES_DESKTOP_CHILD_PID", raising=False)
def assert_owned_pid_is_excluded(*, exclude_pids=None):
assert exclude_pids is not None
assert pid in exclude_pids
return []
with patch(
"hermes_cli.main._find_stale_dashboard_pids",
side_effect=assert_owned_pid_is_excluded,
):
result = _kill_stale_dashboard_processes(restart_managed=True)
assert result == {"matched": [], "killed": [], "failed": []}
class TestFindStaleDashboardPids:
"""Unit tests for the ps/wmic-based detection step."""