fix(update): preserve SSH-owned backends during cleanup
This commit is contained in:
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user