diff --git a/watch/process_unix.go b/watch/process_unix.go index dba26d7..dec4ff1 100644 --- a/watch/process_unix.go +++ b/watch/process_unix.go @@ -3,9 +3,11 @@ package watch import ( + "os" "os/exec" "strconv" "strings" + "syscall" ) func processCommandLine(pid int) (string, error) { @@ -15,3 +17,17 @@ func processCommandLine(pid int) (string, error) { } return strings.TrimSpace(string(out)), nil } + +// processAlive reports whether a process with the given PID is currently +// running. FindProcess always succeeds on Unix, so liveness is probed with +// signal 0. +func processAlive(pid int) bool { + if pid <= 0 { + return false + } + proc, err := os.FindProcess(pid) + if err != nil { + return false + } + return proc.Signal(syscall.Signal(0)) == nil +} diff --git a/watch/process_windows.go b/watch/process_windows.go index bdc19e3..d04c4dc 100644 --- a/watch/process_windows.go +++ b/watch/process_windows.go @@ -2,9 +2,34 @@ package watch -import "errors" +import ( + "errors" + "syscall" +) func processCommandLine(pid int) (string, error) { _ = pid return "", errors.New("process command line lookup not supported on windows") } + +// processAlive reports whether a process with the given PID is currently +// running. os.Process.Signal(0) is unsupported on Windows — Signal returns an +// error for any signal other than Kill — so IsRunning cannot use it there. +// Instead we open the process and check its exit code: a live process reports +// STILL_ACTIVE. +func processAlive(pid int) bool { + if pid <= 0 { + return false + } + const stillActive = 259 // STILL_ACTIVE + h, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, uint32(pid)) + if err != nil { + return false + } + defer syscall.CloseHandle(h) + var code uint32 + if err := syscall.GetExitCodeProcess(h, &code); err != nil { + return false + } + return code == stillActive +} diff --git a/watch/state.go b/watch/state.go index 907ae85..1fcdec7 100644 --- a/watch/state.go +++ b/watch/state.go @@ -93,14 +93,9 @@ func IsRunning(root string) bool { if err != nil { return false } - // Check if process exists by sending signal 0 - proc, err := os.FindProcess(pid) - if err != nil { - return false - } - // On Unix, FindProcess always succeeds, so send signal 0 to check - err = proc.Signal(syscall.Signal(0)) - return err == nil + // Liveness is checked in a platform-specific way: Signal(0) on Unix is + // unsupported on Windows, so processAlive queries the OS directly there. + return processAlive(pid) } // Stop sends SIGTERM to the daemon process @@ -113,6 +108,11 @@ func Stop(root string) error { if err != nil { return err } + // NOTE: Windows has no SIGTERM, so this returns an error there (as it always + // has). `watch stop` on Windows is intentionally left non-destructive: safely + // killing would require verifying the PID still belongs to this repo's daemon + // (guarding against PID reuse), which needs a Windows command-line lookup that + // processCommandLine does not yet implement. Tracked as follow-up. if err := proc.Signal(syscall.SIGTERM); err != nil { return err } diff --git a/watch/state_test.go b/watch/state_test.go index 695c45f..5aaee8e 100644 --- a/watch/state_test.go +++ b/watch/state_test.go @@ -3,6 +3,7 @@ package watch import ( "encoding/json" "os" + "os/exec" "path/filepath" "testing" "time" @@ -144,3 +145,24 @@ func TestWriteInitialStateWritesReadableState(t *testing.T) { t.Fatalf("unexpected recent events in state: %#v", got.RecentEvents) } } + +func TestProcessAliveDetectsLiveAndDeadPIDs(t *testing.T) { + if !processAlive(os.Getpid()) { + t.Fatal("current process should be reported alive") + } + if processAlive(0) { + t.Fatal("pid 0 should not be reported alive") + } + // Spawn a short-lived process, wait for it to exit, then confirm it is dead. + // Re-exec this test binary with a run filter that matches nothing so it + // exits immediately — self-contained, no dependency on an external tool. + cmd := exec.Command(os.Args[0], "-test.run=^$") + if err := cmd.Start(); err != nil { + t.Skipf("cannot spawn helper process: %v", err) + } + pid := cmd.Process.Pid + _ = cmd.Wait() + if processAlive(pid) { + t.Fatalf("exited process %d should not be reported alive", pid) + } +}