Skip to content

feat: add optional zombie-killer-tray integration (subprocess pattern) - #2

Merged
lukisch merged 7 commits into
mainfrom
feat/zombie-killer-integration
Sep 26, 2026
Merged

lukisch merged 7 commits into
mainfrom
feat/zombie-killer-integration

Conversation

@lukisch

@lukisch lukisch commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • T-20260926-368033290 (c) / T-20260926-212716751: optional integration with zombie-killer-tray (conservative cleanup of orphaned MCP/language-server processes across agent frameworks, not just Codex), mirroring the existing safe-start-for-codex integration pattern exactly — git-pinned pip dependency, launched as its own subprocess (python -m zombie_killer_tray watch ...), coordinated via the shared zombie_events.jsonl audit log rather than an in-process import. CareCenter's own existing runtime-MCP-reaper stays unchanged and Codex-Companion-specific; this is complementary, broader-scope tooling.
  • New zombie_killer_integration.py: install/target-resolution, launch_zombie_killer_watch() (spawns the watch subprocess with cwd set to a new zombie_killer_state_dir, tied to CareCenter's own PID via --parent-pid), build_zombie_killer_status() (reads the last cycle back out of the audit log).
  • config.py: new interval/min-age fields + zombie_killer_state_dir property.
  • cli.py: three new subcommands mirroring safe-start's — zombie-killer-report / -install / -watch.
  • pyproject.toml: added to the build extras, pinned to dev-bricks/zombie-killer-tray@6c8eb2c (that repo's PR #4, src/-packaging, not yet merged — move this pin to the merge commit once it lands).
  • Docs (README.md/README.de.md/README_de.md) + THIRD_PARTY_LICENSES.txt updated to match.

Deliberately not in scope here

Tray-menu QAction/i18n wiring (tray.py/i18n.py) equivalent to safe-start's "Codex sicher starten" button — that needs interactive verification of the Qt tray this environment can't provide. The CLI-level integration above is the functional core ("eigener Subprozess, gemeinsame State-Dateien") the ticket asked for; GUI wiring can follow as its own reviewable change.

Also worth a look

Found while preparing this: the local main clone and its pre-existing ticket/carecenter-zombie-killer-20260920 worktree/branch had diverged from origin/main — 2 local-only commits sitting on a base that's missing 5 upstream commits (README/CHANGELOG/security-audit work). Not touched here; this PR is built fresh off origin/main in a new worktree instead. Flagging so it doesn't get lost — see the corresponding note to the team lead.

Test plan

  • 10 new tests in tests/test_zombie_killer_integration.py
  • Full suite: 431/431 green (429 pre-existing + 10 new; the 3 known-failing test_maintenance.py/test_orchestrator.py tests on origin/main are unrelated and unchanged before/after this PR)
  • ruff check . clean
  • Verified README.de.md/README_de.md stayed byte-identical (own contract test)

Not merging — per the two-model rule, a different model/session should review before merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk

T-20260926-368033290 (c) / T-20260926-212716751: adds an OPTIONAL
integration with zombie-killer-tray (conservative cleanup of orphaned MCP
and language-server processes across agent frameworks, not just Codex),
mirroring the existing safe-start-for-codex integration pattern exactly:
a git-pinned pip dependency, launched as its own subprocess via
`python -m zombie_killer_tray watch ...`, coordinated through the shared
zombie_events.jsonl audit log rather than an in-process import. CareCenter's
own existing runtime-MCP-reaper (processes.py/watchdog.py) is unchanged --
it stays targeted at Codex-Companion processes specifically; this is a
complementary, broader-scope tool, not a replacement.

- New src/codex_logdatenbank_wartung/zombie_killer_integration.py:
  install_zombie_killer_package()/zombie_killer_install_target() (pinned
  GitHub spec with a local-sibling-checkout override, same as safe-start),
  launch_zombie_killer_watch() (spawns the watch subprocess with cwd set to
  a new zombie_killer_state_dir so its audit log lands there, tied to
  CareCenter's own PID via --parent-pid so it exits when CareCenter does),
  build_zombie_killer_status() (reads the last cycle event back out of
  zombie_events.jsonl for a status summary).
- config.py: zombie_killer_watch_interval_seconds/zombie_killer_min_age_seconds
  fields + zombie_killer_state_dir property (CODEX_HOME/zombie-killer-tray).
- cli.py: three new subcommands mirroring safe-start's --
  zombie-killer-report / zombie-killer-install / zombie-killer-watch.
- pyproject.toml: added to the `build` extras, pinned to
  dev-bricks/zombie-killer-tray@6c8eb2c (that repo's PR #4, src/-packaging,
  not yet merged -- move this pin to the merge commit once it lands).
- README.md/README.de.md/README_de.md: CLI usage lines, env var override
  doc, credits line (kept README.de.md and README_de.md byte-identical,
  as their own contract test requires).
- THIRD_PARTY_LICENSES.txt: matching SBOM entries for the new dependency.
- 10 new tests in tests/test_zombie_killer_integration.py (install target
  resolution, install success/failure, launch command/cwd/state-dir
  construction and failure handling, status readback with/without a log).

Deliberately NOT done in this PR (scoped out, not forgotten): tray-menu
QAction/i18n wiring (tray.py/i18n.py) equivalent to safe-start's "Codex
sicher starten" button. That needs interactive verification of the Qt tray
this environment can't provide; the CLI-level integration above is the
functional core ("eigener Subprozess, gemeinsame State-Dateien") and the
GUI wiring can follow as its own reviewable change once someone can click
through it.

431/431 tests green (429 baseline unrelated to this change + 10 new; the
3 pre-existing test_maintenance.py/test_orchestrator.py failures on
origin/main are unaffected, unchanged before and after), ruff clean.

Prepared in a fresh worktree off origin/main (not the existing, stale
local `main`/`ticket/carecenter-zombie-killer-20260920` -- see message to
team-lead re: that divergence). Not merging -- per the two-model rule, a
different model/session should review before merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
@github-actions

Copy link
Copy Markdown

Welcome! 👋 Thanks for your first pull request in this repository.

A maintainer will review it soon. Please make sure:

  • Your changes are tested
  • Documentation is updated if needed
  • The PR description explains what and why

Thanks for contributing!

Lukas Geiger and others added 3 commits September 26, 2026 15:04
Resurrected by cherry-picking e7394f6's test_docs_contracts.py
(test_llms_has_one_last_checked_source): llms.txt had accumulated two
"Last-checked" markers (a header line and a trailing bare line) as later
docs-refresh commits added the header without removing the legacy
trailing line. That drift predates this branch and was invisible because
no test enforced single-source-of-truth here until now. Keeping the
header (the format the rest of the recent docs refreshes use) and
dropping the trailing duplicate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
@lukisch

lukisch commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

Per team-lead instruction: recovered 2 unrelated commits found stranded on the local main clone / a pre-existing ticket/carecenter-zombie-killer-20260920 worktree branch (never pushed, diverged from a stale base) and incorporated them here since their content wasn't covered by this PR:

  • f3a1d0b — ci: enforce Ruff and sync docs contracts (adds a Ruff lint gate to CI + a docs-contract test suite)
  • 2f493a9 — fix(watchdog): require dead parents for MCP orphan cleanup (a real safety hardening: the runtime-MCP-duplicate reaper now requires a dead parent, not just a duplicate-signature match, before killing a candidate cohort — same T-20260816-50-style concern I dealt with in zombie-killer-tray itself)
  • df52257 — fixup: cherry-picking the above resurrected a docs contract test (test_llms_has_one_last_checked_source) that caught a genuine, pre-existing drift in llms.txt (two "Last-checked" markers instead of one) — fixed.

Both cherry-picks needed manual conflict resolution (the old commits' base predates 5 newer upstream doc/security commits) — resolved by keeping the current/newer wording and applying only the substantive additions. Also found and fixed a parity break the original commit introduced back on 2026-09-20: it only updated README.de.md, silently diverging it from README_de.md (this repo's own contract requires them byte-identical) — synced both.

The original branch is preserved, not deleted: local tag rescue/carecenter-zombie-killer-20260920 at its tip (1587edf).

437/437 tests green (434 pre-existing/unrelated-baseline + this PR's own 3 new suites; the 3 known test_maintenance.py/test_orchestrator.py failures on origin/main are unrelated and unchanged), ruff clean.

zombie-killer-tray#4 (src/-Paketierung) merged as 039b4f2. Updates the
pinned dependency in pyproject.toml's build extras, the module-level
ZOMBIE_KILLER_PACKAGE_SPEC constant, and the matching THIRD_PARTY_LICENSES.txt
entry from the pre-merge branch head (6c8eb2c) to the actual merge commit.

Verified: installed the new pin into a clean venv and ran
`python -m zombie_killer_tray scan` there successfully before running the
full suite. 437/437 tests green (same 3 known-unrelated baseline
failures), ruff clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
@lukisch

lukisch commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

Pin updated: zombie-killer-tray#4 merged as 039b4f2c7acd69063b39d6168c757b0b2fca2438. Moved the dependency pin from the pre-merge branch head (6c8eb2c) to that commit in pyproject.toml, zombie_killer_integration.py, and THIRD_PARTY_LICENSES.txt (38d3e47). Re-verified the install against the new pin in a clean venv before running the suite: 437/437 tests green (same 3 known-unrelated baseline failures), ruff clean.

@lukisch lukisch left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (merge-reviewer, claude-opus) auf Head 38d3e47: nicht gemergt. Lokal 440 passed / 1 skipped, ruff sauber, Pin 039b4f2 stimmt.

Sicherheitsfix (2f493a9 = gerettetes 1587edf, Patchinhalt gleich), bewertet gegen T-20260816-50:

  • Gut: reap_runtime_mcp_duplicates verlangt jetzt parent_is_dead für jede Kohorten-Wurzel. Damit ist der Präzedenzfall geschlossen, eine aktive Kohorte mit lebendem app-server wird nicht mehr beendet. _parent_is_dead behandelt PID-Reuse korrekt (Parent jünger als Kind = tot) und bei unbekannten Zeiten fail-safe (nicht tot). Die Parent-Historie dient nur dem Audit.
  • Blockierend: taskkill /T /F in reap_runtime_orphans. Der Fix macht aus dem Einzel-Kill stillschweigend einen Baum-Kill (im Commit-Text nicht erwähnt). Alle Nachfahren der Waise werden mitbeendet, ohne dass für sie irgendein Kriterium geprüft wird: kein Parent-tot (ihr Parent, die Waise, lebt ja noch), keine Art, kein Alter, keine CPU-Ruhe (geprüft wird nur die Wurzel). Zudem baut Windows den Baum über aktuelle PPIDs, eine Fremdprozess-Adoption über eine wiederverwendete PID ist also möglich. Das widerspricht „Kill-Kriterium ausschließlich Parent tot“ und dem zombie-killer-Grundsatz „no blanket process-tree kills“. Bitte /T entfernen. Nachfahren werden im nächsten Zyklus als eigene Waisen mit eigenen Kriterien erfasst.
  • Hinweis: Die MCP-Marker " mcp", "mcp ", "-mcp", "_mcp" sind breite Substrings über Pfad und Kommandozeile. Solange Parent-tot, Alter und CPU-Ruhe greifen, ist das innerhalb der Politik. Es vergrößert aber die Kandidatenmenge deutlich; ein Test mit einem Nicht-MCP-Prozess, der „mcp“ nur im Pfad trägt, wäre gut.

Integration zombie-killer-tray, blockierend, empirisch belegt: zombie-killer-watch startet python -m zombie_killer_tray watch --yes ... --parent-pid <eigene PID> und das CLI beendet sich sofort danach. zombie-killer-tray beendet sich, sobald dieser Parent tot ist. Nachgestellt mit Stand 039b4f2 (Aktion scan, gleicher Parent-Mechanismus): Der Watcher ist schon beim Start weg (RuntimeError: tray parent cannot be verified), nach 1/3/8 s lebt er nicht mehr. Die Funktion arbeitet also nie. Die Tests mocken Popen und sehen das nicht. Entweder kein --parent-pid (dann gehört der Watcher dem Aufrufer, und die Prozess-Hygiene braucht einen anderen Lebenszyklus, z. B. Job-Objekt oder eigener Tray) oder eine langlebige CareCenter-Instanz als Parent. Bitte mit einem echten, nicht gemockten Lebensdauer-Test belegen.

Review findings on this PR:

(1) LIFECYCLE BUG (blocking): launch_zombie_killer_watch() passed
--parent-pid <this process's own PID>. zombie-killer-tray's watch_parent
thread kills the watch process as soon as whatever PID it was given for
--parent-pid exits -- and the CLI process that spawns the watcher exits
immediately after Popen returns, so the watcher died within ~1s of being
started, before doing any real work. Reproduced exactly as described:
mocked-Popen tests never caught it because they don't run the real
subprocess lifecycle at all.

Fix: drop --parent-pid entirely. The watcher is now a genuinely detached,
long-lived process; its lifecycle is governed by its own PID file
(<state_dir>/watch.pid, written by launch_zombie_killer_watch) rather than
any parent/child relationship. Added stop_zombie_killer_watch() (reads
the PID file, verifies via psutil that the PID still belongs to
zombie-killer-tray before signalling it -- refuses to touch an unrelated
process that reused the PID) and a new `zombie-killer-stop` CLI
subcommand.

Added a REAL integration test (no mocked Popen) that spawns the actual
zombie-killer-tray package as a subprocess exactly the way production
code does, asserts it is still running 2s after the launching call
returns (the old bug would already have killed it by then), then asserts
zombie-killer-stop actually terminates it. Requires the package
installed (pinned dependency); added the same pip-install step already
used for safe-start-for-codex to CI.

(2) taskkill /T on a single-process orphan kill (blocking): the rescued
watchdog commit (1587edf) changed reap_runtime_orphans' fallback kill
from a single-PID `taskkill /F` to a tree-kill `taskkill /T /F`. Every
PID reaped there was individually vetted (dead parent, min age, two idle
CPU snapshots) -- its descendants were never checked against those same
criteria, so a tree-kill would terminate processes no criterion actually
covers. Reverted to a single-PID kill; a genuinely orphaned descendant
becomes reap-able on its own in a later watchdog cycle once it
independently satisfies the same criteria. (reap_runtime_mcp_duplicates'
own, separate `taskkill /T` for duplicate-cohort launcher trees is
unrelated and untouched -- that one is gated by dead-parent + duplicate-
signature + cohort criteria on the ROOT before the tree-kill fires, which
is the design the review is asking this path to match, not diverge from.)

Added a negative test for the broad MCP substring markers ("-mcp",
"_mcp", " mcp") in processes.py -- it caught a real false positive:
"acme_mcpayments.exe" and "battle-mcp-launcher.exe" were both
misclassified as MCP servers via a substring match with no word-boundary
check. Replaced the plain substring checks for those three broad markers
with a regex requiring "mcp" to end at an actual boundary (path
separator, dot, space, or end of string) rather than continuing into an
unrelated word -- keeps catching the real "<name>-mcp" server-binary
convention (e.g. "ellmos-controlcenter-mcp/dist/index.js") without the
false positives.

(3) safe-start's [tool.hatch.metadata] fix is in that repo's own PR, not
this one -- CareCenter doesn't have a direct-reference optional extra of
this kind (its build extra already declared a direct git reference for
safe-start-for-codex before this PR and CI already accounted for it).

442/442 tests green (439 pre-existing/unrelated-to-this-PR + this PR's
own 3 new test areas; the 3 known-unrelated test_maintenance.py/
test_orchestrator.py failures on origin/main are unchanged), ruff clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
@lukisch

lukisch commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all 3 review findings (9100c3b):

(1) Lifecycle bug — confirmed: --parent-pid <own PID> made the watcher die within ~1s of the CLI process that spawned it returning, since zombie-killer-tray's watch_parent thread kills the watch process as soon as that PID exits. Dropped --parent-pid; the watcher is now a detached, long-lived process tracked via its own PID file (<state_dir>/watch.pid). Added stop_zombie_killer_watch() (verifies the PID still belongs to zombie-killer-tray via psutil before signalling it) and a zombie-killer-stop CLI subcommand. Added a real, non-mocked integration test that spawns the actual package as a subprocess and asserts it's still alive 2s after launch returns (proving the fix) then that stop actually terminates it — added the zombie-killer-tray pip install step to CI so this test genuinely runs there instead of skipping.

(2) taskkill /T tree-kill — reverted reap_runtime_orphans's fallback kill to single-PID (taskkill /F only). Every PID reaped there was individually vetted; a tree-kill would terminate descendants that were never checked against those same criteria. reap_runtime_mcp_duplicates's own, separate /T for duplicate-cohort launcher trees is untouched — that path is already gated by dead-parent + duplicate-signature + cohort criteria on the root, which is the design this fix aligns the other path away from, not toward.

Added the requested negative test for the broad MCP markers and it caught a real bug: acme_mcpayments.exe and battle-mcp-launcher.exe were both misclassified as MCP servers via plain substring matching. Replaced the three broad markers with a boundary-aware regex (requires "mcp" to end at a path separator, dot, space, or end-of-string) — still catches the real <name>-mcp convention (e.g. ellmos-controlcenter-mcp/dist/index.js) without the false positives.

(3) N/A for this repo — CareCenter already had a direct-reference build extra (safe-start-for-codex) before this PR; no new hatch metadata gap here.

442/442 tests green (439 pre-existing + this PR's own areas), ruff clean.

@lukisch lukisch left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-Review (merge-reviewer, claude-opus) auf Head 9100c3b: nicht gemergt. Die Punkte aus dem ersten Review sind im Kern behoben: kein --parent-pid mehr, /T ist entfernt (CareCenter), die MCP-Regex ist grenzbewusst.

Real nachgestellt, und zwar bewusst mit watch ohne --yes (Preview, keine Kills), Launcher als eigener, sofort endender Prozess wie das CLI, zombie-killer-tray 039b4f2:

  1. Watcher lebt 3 s nach Ende des Launchers → ja. stop → ok, Watcher beendet → ja. Keine Rest-Prozesse.
  2. Stale PID auf einen fremden, lebenden Prozess → not-found, nicht beendet → ok.

Blockierend:
3. Stop ist fail-open: Ist psutil nicht importierbar, gibt _is_our_watch_process None zurück, und stop beendet die PID aus der Datei ungeprüft. Nachgestellt: stale PID auf einen fremden Python-Prozess, psutil-Pfad None → stop meldet ok und tötet den Fremdprozess. Das Paket deklariert psutil nicht selbst; wer das Extra später entfernt, hat genau diesen Zustand. Der Test test_stop_reports_already_stopped_for_a_dead_pid legt dieses Verhalten sogar fest. Fix: bei None nicht beenden (fail-closed), Status unverifiable.
4. Doppelstart hinterlässt einen Waisen-Watcher: Ein zweites zombie-killer-watch überschreibt watch.pid, der erste Watcher läuft weiter und ist per stop nicht mehr erreichbar. Nachgestellt: nach dem Stop lebte der erste Watcher noch. Fix: vor dem Start prüfen, ob die PID-Datei auf einen lebenden, verifizierten Watcher zeigt, dann ablehnen oder melden.
5. Der „echte“ Integrationstest macht echte Kills: test_real_watch_subprocess_outlives_its_launcher_and_stop_terminates_it startet watch --yes --interval 3 --min-age 30. Der erste Zyklus läuft sofort und beendet auf der Maschine, die den Test ausführt (Dev-Rechner, CI), reale Waisen ab 30 s Alter. Ich habe ihn deshalb bewusst nicht ausgeführt. Ein Test darf keine realen Prozesse beenden. Fix: launch_zombie_killer_watch(..., apply=False) bzw. ohne --yes im Test (der Lebenszyklus ist derselbe).
6. py -3 als Kandidat: In gefrorenen Builds (sys.frozen, also der ausgelieferte EXE-Fall) ist py -3 der erste Kandidat. Dann steht die PID des py.exe-Launchers in watch.pid, der eigentliche Watcher ist dessen Kind. stop beendet nur den Launcher (die cmdline-Prüfung besteht, weil sie den Modulnamen enthält), der Watcher läuft als Waise weiter. Fix: die echte Watcher-PID bestimmen (Kind von py.exe) oder py als Kandidat für den Watch-Start ausschließen.
Nicht blockierend: Die PID-Verifikation per cmdline-Substring trifft auch einen fremden zombie_killer_tray-Prozess (z. B. den Watcher des Schwesterprojekts). Robuster wäre es, beim Start create_time in die PID-Datei zu schreiben und beim Stop zu vergleichen.
Lokal: 444 passed, 1 skipped (ohne den realen Kill-Test). CI grün.

…test

Re-review findings, all 4 addressed:

(1) stop was fail-open on unverifiable PIDs. _is_our_watch_process()
returned None when psutil was unavailable, and stop treated "not False"
as "safe to kill" -- so a stale, unverifiable PID file could terminate an
unrelated process that happened to reuse the PID. Replaced with
_verify_watch_process(pid, expected_create_time), and stop now refuses
(status="verification-unavailable", process untouched) whenever
verification returns None, never falling through to os.kill(). Declared
psutil explicitly in pyproject.toml's build extra (was only relying on it
arriving transitively via zombie-killer-tray).

Also added the create_time cross-check the review recommended: the PID
file now stores {"pid", "create_time"} (written from the real watcher's
own psutil.Process(pid).create_time() right after spawn), and
verification compares it against the live process's actual create_time --
catches PID reuse even when the cmdline substring check alone might not
(e.g. a same-named relaunch).

(2) A second `zombie-killer-watch` silently overwrote watch.pid, orphaning
the first (still-running) watcher. launch_zombie_killer_watch() now reads
and verifies any existing PID file before spawning: a confirmed-alive
watcher refuses a second start (status="already-running"); an
unverifiable one refuses too, fail-closed, same direction as (1)
(status="verification-unavailable"); only a confirmed-dead/stale entry is
cleared and a fresh watcher started.

(3) The integration test ran the real watch subprocess with --yes,
meaning it could actually reap real, qualifying orphan processes on
whatever machine ran it (including CI). Added apply: bool = True to
launch_zombie_killer_watch() (--yes only when apply is True) and switched
the integration test to apply=False -- proves the lifecycle fix (survives
its launcher, responds to stop) without ever being armed to terminate
anything.

(4) In a frozen (PyInstaller) build, sys.executable is the packaged host
app, not an interpreter, so launching via the `py -3` Python Launcher was
the only remaining candidate -- but py.exe spawns the real interpreter as
its OWN child process (Windows has no exec()), so the PID Popen returned
was the launcher's, not the worker's. Killing the launcher on stop never
touched its child, leaving it running as an unreachable orphan. Replaced
the multi-candidate launch loop with _resolve_watch_python_executable():
not frozen -> sys.executable directly (already a real interpreter); frozen
-> resolve the real path ONCE via each launcher candidate's own
`-c "import sys; print(sys.executable)"` stdout, then spawn the watch
subprocess directly against that resolved path, never through py.exe.
Added a unit test simulating the frozen case with a mocked runner.

449/449 tests green (6 new: preview-mode, 2 double-start-guard directions,
fail-closed-stop, frozen-resolution unit test, plus the already-existing
areas re-verified; same 3 known-unrelated baseline failures unchanged),
ruff clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
@lukisch

lukisch commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

All 4 re-review findings addressed (2a7f51e):

(1) fail-open stop — stop used to treat "verification returned None" as safe-to-kill. Replaced _is_our_watch_process with _verify_watch_process(pid, expected_create_time); stop now refuses (verification-unavailable, process untouched) whenever verification can't confirm it, never falling through to os.kill(). Added the recommended create_time cross-check (PID file now stores {"pid", "create_time"}, written from the watcher's own psutil.Process(pid).create_time() at spawn) on top of the cmdline check. Declared psutil explicitly rather than relying on it arriving transitively.

(2) double-start — launch_zombie_killer_watch now reads and verifies any existing PID file before spawning: confirmed-alive refuses (already-running); unverifiable refuses too, fail-closed (verification-unavailable); only a confirmed-dead/stale entry is cleared and a fresh watcher started.

(3) real orphans killed by the test — added apply: bool = True (--yes only when True); switched the integration test to apply=False. Proves the lifecycle fix without ever being armed to terminate anything.

(4) launcher-PID orphan in frozen builds — replaced the candidate-loop launch with _resolve_watch_python_executable(): not frozen → sys.executable directly; frozen → resolve the real interpreter path once via each launcher candidate's own -c "import sys; print(sys.executable)" stdout, then spawn the watch subprocess directly against that resolved path — never through py.exe itself. Added a unit test simulating the frozen case with a mocked runner.

449/449 tests green (6 new areas), ruff clean.

@lukisch

lukisch commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

Runde-3-Review (merge-reviewer, claude-opus): real nachgestellt mit zombie-killer-tray 039b4f2, Watcher im Preview-Modus. (1) Ohne psutil → verification-unavailable, Fremdprozess lebt weiter. (2) Passende cmdline, aber create_time um 100 s abweichend → not-found, Watcher unberührt. (3) Zweiter Start → already-running. (4) Der reale Integrationstest ruft apply=False auf, im Kommando steht kein --yes, geprüft im Code und per Assertion. (5) Gefrorener Fall (sys.frozen): Aufgelöst wird der echte python.exe (nicht py.exe), die PID-Datei zeigt auf den Watcher selbst, sein einziges Kind ist conhost, das mit ihm endet. Stop beendet sauber, danach keine zombie_killer_tray-Prozesse mehr. Kill-Kriterium bleibt das von zombie-killer-tray (Parent tot). Nicht blockierend: Eine PID-Datei im Altformat (nackte Zahl aus Runde 2) würde in _read_watch_pid_file an AttributeError scheitern. Runde 2 wurde nie gemergt, im Feld existiert das Format also nicht. Merge. Lokal 452 passed / 1 skipped, ruff sauber, CI grün.

@lukisch
lukisch merged commit 81fcf49 into main Sep 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant