Skip to content

Make watch stop work safely on Windows (#74)#75

Merged
JordanCoin merged 2 commits into
mainfrom
fix/windows-watch-stop
Jul 2, 2026
Merged

Make watch stop work safely on Windows (#74)#75
JordanCoin merged 2 commits into
mainfrom
fix/windows-watch-stop

Conversation

@JordanCoin

Copy link
Copy Markdown
Owner

Closes #74. Follow-up to #73 (which fixed watch status on Windows).

What

Makes codemap watch stop actually terminate the daemon on Windows, while guarding against the PID-reuse risk Codex flagged on #73.

  • processCommandLine on Windows (was a stub): now queries the full command line via Get-CimInstance Win32_Process — the Windows analog of the Unix ps call — so IsOwnedDaemon can confirm a PID is this repo's watch daemon <root>.
  • Platform-specific terminateDaemon:
    • Unix: unchanged — ungated SIGTERM (preserves long-standing behavior + existing tests).
    • Windows: verifies IsOwnedDaemon first, then Kill; returns ErrForeignDaemonPID when ownership can't be confirmed (refuse-when-uncertain).
  • Stop: on a stale/foreign PID it clears the bogus watch.pid (so status stops reporting it) but never kills the unrelated process. The stop command reports Watch 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-PID SIGTERM is far less catastrophic, and gating it would risk refusing to stop a real daemon in environments where process introspection is restricted).

⚠️ Testing limitation

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 vet clean.
  • Host (Unix) tests green, including a new command-layer test for the foreign-PID branch.

@ScottRosenberg2 — if you're able, verification on real Windows would be valuable:

codemap watch start
codemap watch stop     # should stop the daemon
codemap watch status   # should report not running

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings July 2, 2026 02:38
@codecov-commenter

codecov-commenter commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
watch/state.go 57.89% 6 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 update watch.Stop to clear a stale PID file without killing when ownership can’t be confirmed.
  • Implement Windows processCommandLine via PowerShell/CIM and add platform-specific terminateDaemon implementations (gated kill on Windows; SIGTERM on Unix).
  • Update the CLI watch stop output 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.

Comment thread watch/state.go Outdated
// 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)")
Comment thread watch/process_windows.go
Comment on lines +55 to +60
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>
@JordanCoin

Copy link
Copy Markdown
Owner Author

Thanks @copilot — both addressed in the latest commit:

  1. TOCTOU: terminateDaemon now classifies ownership against proc.Pid (the exact process it's about to kill) instead of re-reading watch.pid, so a concurrent start/stop can't cause it to validate one PID and kill another.
  2. Wording / refuse-when-uncertain: ownership is now three-state (owned / foreign / unknown). ErrForeignDaemonPID is only returned when the command line was read and genuinely didn't match (and Stop clears the stale pid file then). When the command line can't be read, it returns the new ErrDaemonOwnershipUnknown and keeps the pid file — so a real daemon is never orphaned and no unrelated process is killed.

Copilot AI commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Thanks @copilot — both addressed in the latest commit:

  1. TOCTOU: terminateDaemon now classifies ownership against proc.Pid (the exact process it's about to kill) instead of re-reading `wat...

Confirmed — the implementation in cc89963 correctly addresses both issues:

  1. TOCTOU: terminateDaemon calls daemonOwnershipForPID(root, proc.Pid) directly, so it validates the exact PID it's about to kill regardless of any concurrent watch.pid rewrite.
  2. Three-state ownership: ownershipOwned → Kill; ownershipForeign (command line read, didn't match) → ErrForeignDaemonPID + pid file cleared; ownershipUnknown (command line unavailable) → ErrDaemonOwnershipUnknown + pid file kept so a real daemon is never orphaned.

All Unix tests pass; GOOS=windows go build is clean.

@JordanCoin JordanCoin merged commit 38b420a into main Jul 2, 2026
13 checks passed
@JordanCoin JordanCoin deleted the fix/windows-watch-stop branch July 2, 2026 14:04
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.

Make watch stop work safely on Windows (PID-ownership verification)

4 participants