Skip to content

feat: add optional, Windows-gated zombie-killer-tray integration - #2

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

lukisch merged 4 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), mirroring the pattern already used by dev-bricks/CareCenter-for-Codex for this same tool — git-pinned pip dependency, launched as its own subprocess (python -m zombie_killer_tray watch ...), coordinated via the shared zombie_events.jsonl audit log.
  • Safe Start for Codex is explicitly cross-platform with zero required runtime deps; zombie-killer-tray is Win32-only, so this is doubly gated:
    • New [project.optional-dependencies].zombie-killer extra (never installed implicitly), itself carrying a sys_platform == 'win32' PEP 508 marker.
    • launch_zombie_killer_watch()/build_zombie_killer_status() check a small _is_windows() seam and refuse/report cleanly on any other platform.
  • New zombie_killer_integration.py + 3 CLI subcommands (zombie-killer-report / -install / -watch) mirroring the existing subparser style in cli.py.
  • Updated THIRD_PARTY_LICENSES.txt/.md and tests/test_third_party_licenses.py's exact-dependency-set assertion.
  • Pinned to dev-bricks/zombie-killer-tray@6c8eb2c (PR #4, src/-packaging, not yet merged — move this pin to the merge commit once it lands, same note as the CareCenter-for-Codex integration PR).

A note on the platform gate

Found this the hard way while writing tests: monkeypatching os.name directly on an actual Windows test runner breaks pathlib's own class dispatch for any Path() constructed afterwards (NotImplementedError: cannot instantiate 'PosixPath'). Introduced a small _is_windows() seam instead of patching os.name, and verified both the real-Windows and simulated-non-Windows code paths.

Test plan

  • 11 new tests in tests/test_zombie_killer_integration.py (target resolution, install ok/fail, launch args/cwd/failure, platform gate on both branches, status readback)
  • Full suite: 139/139 green (128 baseline + 11 new)
  • ruff check . clean

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), mirroring the pattern already used by
dev-bricks/CareCenter-for-Codex for this same tool -- 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, never an in-process import.

Safe Start for Codex is explicitly cross-platform (Windows/Linux/macOS,
zero required runtime dependencies); zombie-killer-tray is Win32-only.
This integration is therefore doubly gated to keep that intact:
- New `[project.optional-dependencies].zombie-killer` extra (never
  installed implicitly), itself carrying a PEP 508
  `sys_platform == 'win32'` marker so `pip install .[zombie-killer]`
  can't even attempt to resolve zombie-killer-tray's own Win32
  dependencies on Linux/macOS.
- `launch_zombie_killer_watch()`/`build_zombie_killer_status()` check a
  small `_is_windows()` seam and refuse/report cleanly on any other
  platform rather than attempting a doomed subprocess spawn. (Deliberately
  NOT monkeypatching `os.name` directly in tests for this -- doing so on
  an actual Windows test runner breaks pathlib's own class dispatch for
  any Path() constructed afterwards; verified this the hard way before
  introducing the `_is_windows()` seam.)

New `zombie_killer_integration.py`: target resolution (local sibling
checkout override via `SAFE_START_ZOMBIE_KILLER_SOURCE`, else the pinned
GitHub spec), install/launch/status functions. Three new CLI subcommands
in cli.py mirroring the rest of its subparser style: `zombie-killer-report`
/ `-install` / `-watch`. Updated THIRD_PARTY_LICENSES.txt/.md and
tests/test_third_party_licenses.py's exact-dependency-set assertion to
cover the new optional dependency.

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, same as the CareCenter-for-Codex integration PR).

139/139 tests green (128 baseline + 11 new), ruff clean. 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!

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

Verified: `import zombie_killer_tray.killer` resolves against this pin in
a clean venv. 139/139 tests green, 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's zombie-killer extra, zombie_killer_integration.py, and both THIRD_PARTY_LICENSES files (f39a504). Re-verified import zombie_killer_tray.killer resolves against the new pin. 139/139 tests green, 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 f39a504: nicht gemergt. Lokal 139 passed, das Windows-Gating in launch_zombie_killer_watch ist sauber (Nicht-Windows: unsupported-platform, kein Start), der Kill-Pfad bleibt der von zombie-killer-tray (nur Parent tot).

Blockierend:

  1. Paket nicht mehr installierbar: Die direkte Git-Referenz im Extra zombie-killer braucht bei hatchling [tool.hatch.metadata] allow-direct-references = true. Ohne das scheitert schon pip install -e . (CI: test 3.13 und alle smoke-Jobs rot; lokal reproduziert: „cannot be a direct reference unless field tool.hatch.metadata.allow-direct-references is set to true“). CareCenter hat die Einstellung.
  2. Watcher stirbt sofort (gleicher Fehler wie CareCenter#2): zombie-killer-watch übergibt --parent-pid os.getpid() und das CLI beendet sich danach. zombie-killer-tray beendet sich mit dem Parent. Empirisch mit Stand 039b4f2 nachgestellt: der Watcher überlebt keine Sekunde. Bitte Lebenszyklus neu lösen und mit einem echten, nicht gemockten 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. 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) 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) and a
new `zombie-killer-stop` CLI subcommand.

Added a REAL integration test (no mocked Popen, skipped on non-Windows)
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; added the same pip-install step to ci.yml
(windows-latest only, matching zombie-killer-tray's own platform
requirement -- the cross-platform source-platform-smoke.yml workflow only
runs tests/source_platform_smoke.py and never collects this file).

(3) [tool.hatch.metadata] allow-direct-references (blocking): the new
`zombie-killer` optional extra declares a direct `git+https://...`
reference, which hatchling refuses to build without this explicit opt-in
-- `pip install -e .` failed with a ValueError during metadata
generation. Reproduced the exact failure in a clean venv before adding
the fix, then reproduced the fix working the same way.

(2) is CareCenter-for-Codex-specific (taskkill /T, broad MCP markers) --
addressed in that repo's own PR, not this one; safe-start-for-codex has
no equivalent code path.

143/143 tests green (128 pre-existing/unrelated-to-this-PR + this PR's
own 15), 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 both applicable review findings (3ca6652):

(1) Lifecycle bug — same root cause as CareCenter's: --parent-pid <own PID> made the watcher die within ~1s of the launching CLI process returning. Dropped --parent-pid; watcher lifecycle now tracked via its own PID file (<state_dir>/watch.pid), independent of the caller. Added stop_zombie_killer_watch() (psutil-verified before signalling) and a zombie-killer-stop CLI subcommand. Added a real, non-mocked integration test (skipped on non-Windows, matching the platform gate already in this module) that spawns the actual package as a subprocess and proves it survives past the point where the old bug would have killed it, then proves stop actually terminates it — added the zombie-killer-tray pip install step to ci.yml (Windows-only job) so it genuinely runs instead of skipping.

(3) [tool.hatch.metadata] allow-direct-references — reproduced the exact failure first (pip install -e . → ValueError: ... cannot be a direct reference unless ... allow-direct-references ... true), then added the fix and re-verified pip install -e . succeeds in a clean venv.

(2) N/A — that finding is CareCenter-for-Codex-specific (taskkill /T, broad MCP markers); this repo has no equivalent code path.

143/143 tests green (128 pre-existing + this PR's own 15), 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 3ca6652: 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.
Zusätzlich blockierend (safe-start): CI rot auf Python 3.11. ERROR: Package zombie-killer-tray requires a different Python: 3.11.9 not in >=3.12. safe-start unterstützt 3.11, zombie-killer-tray verlangt >=3.12. Das Extra braucht zusätzlich den Marker python_version >= "3.12", und der CI-Schritt darf es unter 3.11 nicht installieren. pip install -e . geht jetzt. Lokal 142 passed (ohne den realen Kill-Test).

…rker

Re-review findings, all 4 addressed (mirrors CareCenter-for-Codex#2):

(1) stop was fail-open on unverifiable PIDs -- replaced _is_our_watch_process
with _verify_watch_process(pid, expected_create_time); stop now refuses
(status="verification-unavailable", process untouched) whenever
verification returns None instead of falling through to os.kill(). The PID
file now stores {"pid", "create_time"} (from the real watcher's own
psutil.Process(pid).create_time() right after spawn) for a create_time
cross-check on top of the cmdline check -- catches PID reuse the cmdline
check alone might miss. psutil declared explicitly in the `zombie-killer`
extra (was only relying on it arriving transitively).

(2) A second `zombie-killer-watch` silently overwrote watch.pid, orphaning
the first watcher. launch_zombie_killer_watch() now verifies any existing
PID file before spawning: confirmed-alive refuses a second start
(status="already-running"); unverifiable 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) N/A for this repo in the strict sense (safe-start's launch function
never took a MaintenanceConfig, so there was no separate "test vs
production" config split) -- added the same apply: bool = True parameter
anyway for parity with CareCenter and switched the integration test to
apply=False, since the real subprocess it spawns could otherwise reap
real, qualifying orphans on whatever machine runs it (including CI).

(4) In a frozen build, sys.executable is the packaged host app and `py -3`
spawns py.exe as a WRAPPER that itself spawns the real interpreter as ITS
OWN child (Windows has no exec()) -- the PID Popen returned was the
launcher's, not the worker's, so stop never reached the actual watcher.
Replaced the multi-candidate launch loop with
_resolve_watch_python_executable(): not frozen -> sys.executable directly;
frozen -> resolve the real path once via each launcher candidate's own
`-c "import sys; print(sys.executable)"` stdout, then spawn directly
against that resolved path. Added a unit test simulating the frozen case.

Safe-start-specific: the `zombie-killer` extra's git dependency now also
carries a `python_version >= '3.12'` marker (zombie-killer-tray's own
floor; this project's is >=3.11) alongside the existing `sys_platform ==
'win32'` marker, so `pip install .[zombie-killer]` is a correct no-op on
3.11 instead of failing. ci.yml's separate zombie-killer-tray install
step (needed for the real integration test) is now gated with
`if: matrix.python-version != '3.11'` for the same reason -- verified the
marker syntax by actually installing `.[zombie-killer]` in a clean venv
(psutil + zombie-killer-tray both resolved correctly on 3.12/win32).

150/150 tests green (128 pre-existing + this PR's own 22), 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 (99fc3e7), mirroring CareCenter-for-Codex#2:

(1)/(2)/(4) — same fixes: fail-closed stop via _verify_watch_process (create_time + cmdline check, psutil declared explicitly), double-start guard on launch_zombie_killer_watch, and _resolve_watch_python_executable() replacing the launcher-loop so the PID file always tracks the real interpreter's PID, never py.exe's.

(3) — added apply: bool = True for parity and switched the integration test to apply=False so it can't reap real orphans on the test machine.

Safe-start-specific: the zombie-killer extra's git dependency now also carries python_version >= '3.12' (zombie-killer-tray's own floor; this project's is >=3.11) alongside the existing sys_platform == 'win32' marker — verified by actually installing .[zombie-killer] in a clean venv (both psutil and zombie-killer-tray resolved correctly). ci.yml's separate zombie-killer-tray install step (needed for the real integration test) is now if: matrix.python-version != '3.11' for the same reason.

150/150 tests green (22 new/updated), 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. Das Extra hat den Marker python_version>='3.12', die CI installiert es nur ab 3.12. Lokal 150 passed, CI grün.

@lukisch
lukisch merged commit aae0b42 into main Sep 26, 2026
21 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