Skip to content

Windows: daemon stop must taskkill the whole process tree (/T)#53

Open
danielhertz1999-bit wants to merge 2 commits into
CodeAbra:mainfrom
danielhertz1999-bit:fix/windows-stop-taskkill-tree
Open

Windows: daemon stop must taskkill the whole process tree (/T)#53
danielhertz1999-bit wants to merge 2 commits into
CodeAbra:mainfrom
danielhertz1999-bit:fix/windows-stop-taskkill-tree

Conversation

@danielhertz1999-bit

Copy link
Copy Markdown
Contributor

Problem

iai_mcp daemon stop on Windows can leave a child process alive and still holding the hippo DB lock, so a subsequent restart wedges.

The Windows stop path did:

os.kill(pid, _signal.SIGINT)          # (1)
... poll until pid dies or timeout ...
if _is_pid_alive(pid):
    subprocess.run(["taskkill", "/F", "/PID", str(pid)])   # (2) no /T

Two problems compound:

  1. On Windows os.kill(pid, SIGINT) is not a signal — Python maps any non-CTRL_* value to TerminateProcess, which hard-kills the parent process only. Any child the daemon spawned (a worker interpreter, a maturin build, etc.) is orphaned and keeps running — and keeps its handle on ~/.iai-mcp/hippo/.lock.
  2. Because TerminateProcess kills the parent immediately, the poll loop sees the pid gone and returns before the taskkill escalation runs. And even when it did run, taskkill /F /PID (no /T) only kills that one pid — by then childless.

Net effect: a "stopped" daemon can leave an orphan holding the lock, which then blocks the next daemon from acquiring it.

Fix

Replace the SIGINT-then-escalate sequence with a single taskkill /F /T /PID <pid>. /T terminates the daemon together with its child process tree, while that tree is still intact. schtasks /End still runs first. This is a hard kill, but the old path was already a hard kill on Windows (TerminateProcess) — no graceful-shutdown behavior is lost, and the tree is now handled correctly.

POSIX (macOS/Linux) stop paths are untouched.

Test

The Windows stop path previously had zero coverage (existing stop tests are macOS/Linux, or @skipif no-SIGKILL). The new test drives cmd_daemon_stop with platform.system() == "Windows" and asserts it issues taskkill /F /T /PID after schtasks /End, and never calls os.kill (which would orphan the tree). Verified both ways: passes on the fix, fails against the pre-fix source (which shows the os.kill(SIGINT) + taskkill /F /PID sequence).

danielhertz1999-bit and others added 2 commits July 5, 2026 14:19
`iai_mcp daemon stop` on Windows could leave a child interpreter alive and
still holding the hippo lock. The stop path sent os.kill(pid, SIGINT) --
which on Windows is not a signal but a TerminateProcess against the parent
process only. That orphans any children the daemon spawned, and because the
parent then exits immediately the subsequent poll loop returned before the
`taskkill /F /PID` escalation (itself childless) ever ran.

Replace the SIGINT-then-escalate dance with a single `taskkill /F /T /PID`,
which terminates the daemon together with its child processes as one tree
while that tree is still intact. schtasks /End still runs first. POSIX
(macOS/Linux) stop paths are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Windows stop path had no coverage (existing stop tests are macOS/Linux
or @skipif-SIGKILL). The new test drives cmd_daemon_stop with platform=Windows
and asserts it issues taskkill /F /T /PID (tree-kill) after schtasks /End and
never falls back to os.kill, which orphans the child tree. Verified it fails
against the pre-fix source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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