Make watch stop work safely on Windows (#74)#75
Conversation
Implements the follow-up from #73: `watch stop` now terminates the daemon on Windows, guarded against PID reuse. - process_windows.go: implement processCommandLine via Get-CimInstance Win32_Process (the Windows analog of `ps`), so IsOwnedDaemon can confirm a PID is this repo's `watch daemon <root>`. - Add platform-specific terminateDaemon: Unix keeps ungated SIGTERM (unchanged behavior); Windows verifies ownership then Kills, returning ErrForeignDaemonPID when the PID can't be confirmed as ours. - Stop clears a stale/foreign pid file instead of killing an unrelated process; the stop command reports "not running (cleared stale PID file)" for that case. Adds a command-layer test for the foreign-PID path (platform-independent via the stopWatchDaemon var). The Windows runtime path (CIM lookup + ownership kill) can't be exercised on non-Windows or in CI (the Windows job only builds), so it needs verification on real hardware. Closes #74 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR updates the watch stop flow to safely terminate the watch daemon on Windows by verifying PID ownership (to avoid PID-reuse killing an unrelated process), while preserving the existing Unix SIGTERM behavior.
Changes:
- Add a new sentinel error (
watch.ErrForeignDaemonPID) and updatewatch.Stopto clear a stale PID file without killing when ownership can’t be confirmed. - Implement Windows
processCommandLinevia PowerShell/CIM and add platform-specificterminateDaemonimplementations (gated kill on Windows;SIGTERMon Unix). - Update the CLI
watch stopoutput and add a command-layer test for the “foreign/stale PID” branch.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| watch/state.go | Introduces ErrForeignDaemonPID and routes stop through terminateDaemon, clearing stale PID files safely. |
| watch/process_windows.go | Implements CIM-based command-line lookup and Windows-specific gated termination logic. |
| watch/process_unix.go | Adds Unix terminateDaemon wrapper to preserve historical SIGTERM behavior behind a shared API. |
| main.go | Prints a user-friendly message when a stale/foreign PID is detected and cleared. |
| main_more_test.go | Adds a regression test ensuring the CLI reports the stale-PID branch correctly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // cannot be confirmed to be this repo's watch daemon (e.g. a stale PID reused by | ||
| // an unrelated process). Callers should treat it as "nothing of ours to stop" | ||
| // rather than a hard failure. | ||
| var ErrForeignDaemonPID = errors.New("watch.pid does not belong to a codemap watch daemon for this repo (stale or reused PID)") |
| func terminateDaemon(root string, proc *os.Process) error { | ||
| if !IsOwnedDaemon(root) { | ||
| return ErrForeignDaemonPID | ||
| } | ||
| return proc.Kill() | ||
| } |
- Fix TOCTOU: terminateDaemon now classifies ownership against proc.Pid
(the exact process it will kill) instead of re-reading watch.pid, so a
concurrent start/stop can't make it validate one PID and kill another.
- Three-state ownership (owned / foreign / unknown) via
daemonOwnershipForPID:
- owned -> Kill
- foreign -> ErrForeignDaemonPID (command line read, didn't match);
Stop clears the stale pid file.
- unknown -> ErrDaemonOwnershipUnknown (command line unavailable);
refuse to kill AND keep the pid file so a real daemon is
never orphaned.
- Reword ErrForeignDaemonPID so it no longer implies "not ours" for the
uncertain case, and add ErrDaemonOwnershipUnknown for it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @copilot — both addressed in the latest commit:
|
Confirmed — the implementation in
All Unix tests pass; |
Closes #74. Follow-up to #73 (which fixed
watch statuson Windows).What
Makes
codemap watch stopactually terminate the daemon on Windows, while guarding against the PID-reuse risk Codex flagged on #73.processCommandLineon Windows (was a stub): now queries the full command line viaGet-CimInstance Win32_Process— the Windows analog of the Unixpscall — soIsOwnedDaemoncan confirm a PID is this repo'swatch daemon <root>.terminateDaemon:SIGTERM(preserves long-standing behavior + existing tests).IsOwnedDaemonfirst, thenKill; returnsErrForeignDaemonPIDwhen ownership can't be confirmed (refuse-when-uncertain).Stop: on a stale/foreign PID it clears the boguswatch.pid(so status stops reporting it) but never kills the unrelated process. Thestopcommand reportsWatch daemon not running (cleared stale PID file).Why Windows-only gating
Killing a reused PID is destructive; on Windows we refuse when uncertain. Unix keeps its historical ungated
SIGTERM(a reused-PIDSIGTERMis far less catastrophic, and gating it would risk refusing to stop a real daemon in environments where process introspection is restricted).The Windows runtime path (CIM command-line lookup + ownership kill) cannot be exercised in this environment or in CI — the Windows CI job only builds, it doesn't run tests. Verified here:
GOOS=windows go build+go vetclean.@ScottRosenberg2 — if you're able, verification on real Windows would be valuable:
🤖 Generated with Claude Code