#1221·watchdog

watchmedo auto-restart: stop_signal is never delivered on Windows, and grandchildren leak

Author: MohammedAlkindiCreated Aug 17, 2026Updated Aug 19, 2026

tests/test_0_watchmedo.py currently xfails test_auto_restart_subprocess_termination on Windows and macOS with reason="known to be problematic, see #972". This is what I found underneath the Windows half of that.

On Windows, AutoRestartTrick never delivers the signal it documents. tricks/__init__.py:321-322 is os.kill(pid, stop_signal) inside a platform.is_windows() branch, and CPython maps that to TerminateProcess(handle, sig) for every value except CTRL_C_EVENT and CTRL_BREAK_EVENT (Modules/posixmodule.c, os_kill_impl). The child is hard-killed, with the signal number used as its exit code.

The giveaway is that arbitrary integers pass straight through. os.kill(pid, N) gives the child returncode N for N in 2, 15, 9, 42 and 137. A 42 exit code is not signal dispatch.

Three places document the opposite. watchmedo.py:686, docs/source/watchmedo_cli.rst:149 and tricks/__init__.py:180 all say "Stop the subprocess with this signal (default SIGINT)".

There is a second half to it. _start_process at line 263 passes preexec_fn=getattr(os, "setsid", None), so POSIX puts the child in its own process group and Windows gets no creationflags and so no group at all. POSIX pairs setsid with killpg, and the Windows branch implements neither, so only the direct child dies and grandchildren leak on every restart. That is the mirror of #609, which reports the POSIX side of the same missing design: there the child survives because it is in its own group, here grandchildren survive because nothing ever is.

Measured against master eb233b8, three identical runs, driving the real AutoRestartTrick:

signal_delivered  = False
clean_exit        = False
atexit_ran        = False
grandchild_leaked = True

Anyone following the documented watchmedo auto-restart --patterns="*.py" --command="python app.py" . on Windows gets their dev server hard-killed on every file save, with no atexit or finally cleanup, and anything it spawned left orphaned.

The other reason the suite stays quiet is at tests/test_0_watchmedo.py:74-75:

python
# in windows we seem to lose the subprocess stderr
# assert 'KeyboardInterrupt' in cap.err

I think that comment is a misdiagnosis rather than a flake. TerminateProcess means no KeyboardInterrupt is ever raised in the child, so there is no stderr to lose. With that assertion commented out and the termination test xfailed, every auto-restart test passes here with the defect fully present.

None of this is specific to my machine, so windows-latest should be able to verify it.

On a fix, CREATE_NEW_PROCESS_GROUP plus CTRL_BREAK_EVENT gave me a delivered signal, a clean exit, atexit handlers running, and the grandchild reaped. That direction has a consequence I could not measure and would rather leave to your judgement: a new process group changes how a console Ctrl+C reaches the child. My attempt to test interactive Ctrl+C was inconclusive for both the fixed and the unfixed case, so I am not claiming anything about it either way. Happy to open a PR if the direction looks right to you.