mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user