[eric] apps: the ghost reaper reads working directories too, so an orphaned app backend whose argv never names the workspace stops being invisible

This commit is contained in:
ciregenz
2026-08-07 21:36:51 -07:00
parent 01e295ac5c
commit b537a4ed63
2 changed files with 65 additions and 7 deletions
+37 -7
View File
@@ -56,13 +56,41 @@ def p_ppid_map() -> dict:
return m
@typechecked
def p_cwd_map(needle: str) -> dict:
"""pid -> cwd, for processes whose working directory sits under the workspace.
An app's backend is spawned as `python -u backend.py` with `cwd=<workspace>`, so the workspace
path appears NOWHERE in its argv: an argv-only scan is structurally blind to exactly the ghost
we most want dead. lsof is the only way to read another process's cwd on macOS. Best-effort by
design, since a machine that restricts lsof must still boot.
"""
try:
out = subprocess.run(
["lsof", "-a", "-d", "cwd", "-Fn"], capture_output=True, text=True, timeout=15
)
except Exception:
return {}
m = {}
pid = None
for raw in (out.stdout or "").splitlines():
if not raw:
continue
tag, val = raw[0], raw[1:]
if tag == "p" and val.isdigit():
pid = int(val)
elif tag == "n" and pid is not None and val.casefold().startswith(needle):
m[pid] = val
return m
@typechecked
def find_ghost_runtime_pids() -> List[int]:
"""PIDs of workspace processes that NO live backend owns.
Matched on the absolute workspace path, so an unrelated `npm run dev` elsewhere is never touched,
then filtered by walking each candidate's ancestry: if a live backend is anywhere above it, it is
someone's working app and is left alone.
Matched on the absolute workspace path (in argv OR as the process's working directory), so an
unrelated `npm run dev` elsewhere is never touched, then filtered by walking each candidate's
ancestry: if a live backend is anywhere above it, it is someone's working app and is left alone.
"""
# Case-FOLDED needle. macOS's default filesystem is case-insensitive, so a process may report
# `.../openswarm/...` while our resolved path is `.../OpenSwarm/...`: the same folder, but a
@@ -80,15 +108,17 @@ def find_ghost_runtime_pids() -> List[int]:
# Boot relied on running before anything spawned; the 10-minute sweep gets no such alibi.
if not owners or not parents:
return []
ghosts: List[int] = []
by_cwd = p_cwd_map(needle)
candidates = dict.fromkeys(by_cwd)
for line in (out.stdout or "").splitlines():
line = line.strip()
if needle not in line.casefold():
continue
head = line.split(None, 1)
if not head or not head[0].isdigit():
continue
pid = int(head[0])
if head and head[0].isdigit():
candidates[int(head[0])] = None
ghosts: List[int] = []
for pid in candidates:
if pid == mine:
continue
cur, owned, broken = pid, False, False
+28
View File
@@ -133,3 +133,31 @@ def test_ghost_matched_despite_path_case_difference(monkeypatch):
stdout = f"50 python -m uvicorn backend.main:app\n700 bash {lower_ws}/ws-x/backend/run.sh\n"
monkeypatch.setattr(rg.subprocess, "run", lambda *a, **k: P_Out())
assert rg.find_ghost_runtime_pids() == [700], "a case-different path must still match the ghost"
def test_an_orphan_is_found_by_its_CWD_when_argv_hides_the_path(monkeypatch):
"""An app's backend runs as `python -u backend.py` with cwd=<workspace>, so the workspace path is
nowhere in its argv. An argv-only scan was structurally blind to exactly the ghost we most want
dead; found live on a packaged build where orphaned app backends survived every reap."""
from backend.apps.outputs import reap_ghost_runtimes as rg
ws = rg.os.path.abspath(rg.WORKSPACE_DIR)
monkeypatch.setattr(rg, "p_live_backend_pids", lambda: {50})
monkeypatch.setattr(rg, "p_ppid_map", lambda: {900: 1})
monkeypatch.setattr(rg, "p_cwd_map", lambda needle: {900: ws + "/app-7"})
class P_Out:
stdout = "50 python -m uvicorn backend.main:app\n900 python3 -u backend.py\n"
monkeypatch.setattr(rg.subprocess, "run", lambda *a, **k: P_Out())
assert rg.find_ghost_runtime_pids() == [900], "a cwd-only orphan must still be reaped"
def test_a_cwd_orphan_owned_by_a_live_backend_is_spared(monkeypatch):
"""The cwd path must obey the same ancestry rule: a working app is not a ghost."""
from backend.apps.outputs import reap_ghost_runtimes as rg
ws = rg.os.path.abspath(rg.WORKSPACE_DIR)
monkeypatch.setattr(rg, "p_live_backend_pids", lambda: {50})
monkeypatch.setattr(rg, "p_ppid_map", lambda: {900: 50, 50: 1})
monkeypatch.setattr(rg, "p_cwd_map", lambda needle: {900: ws + "/app-7"})
class P_Out:
stdout = "50 python -m uvicorn backend.main:app\n900 python3 -u backend.py\n"
monkeypatch.setattr(rg.subprocess, "run", lambda *a, **k: P_Out())
assert rg.find_ghost_runtime_pids() == [], "a live backend's own app runtime must never be killed"