feat(v0.6): tri-state PID liveness probe (ProcessAlive/Dead/Unknown)
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package session
|
||||
|
||||
// ProcessState is the tri-state result of a PID liveness check. The call
|
||||
// site decides how to treat ProcessUnknown; IsStale treats it
|
||||
// conservatively by falling back to wall-clock freshness.
|
||||
type ProcessState int
|
||||
|
||||
const (
|
||||
// ProcessAlive: the OS confirms a process with this PID exists.
|
||||
ProcessAlive ProcessState = iota
|
||||
// ProcessDead: the OS confirms no process with this PID exists.
|
||||
ProcessDead
|
||||
// ProcessUnknown: the liveness check was inconclusive (permission
|
||||
// error, unexpected OS error). Callers must NOT treat this as dead.
|
||||
ProcessUnknown
|
||||
)
|
||||
|
||||
// checkProcess is the test seam for PID liveness. Production code uses the
|
||||
// platform-specific defaultCheckProcess. Tests override this package-level
|
||||
// variable; such tests must restore it via t.Cleanup and must NOT run with
|
||||
// t.Parallel().
|
||||
var checkProcess = defaultCheckProcess
|
||||
@@ -0,0 +1,38 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The compiled-in defaultCheckProcess (platform-specific) must report the
|
||||
// running test process as alive.
|
||||
func TestCheckProcessSelfAlive(t *testing.T) {
|
||||
if got := defaultCheckProcess(os.Getpid()); got != ProcessAlive {
|
||||
t.Errorf("defaultCheckProcess(self pid=%d) = %v, want ProcessAlive", os.Getpid(), got)
|
||||
}
|
||||
}
|
||||
|
||||
// A child process that has exited and been reaped must report as dead.
|
||||
// Best-effort: a PID-reuse race is theoretically possible but negligible
|
||||
// within a test run.
|
||||
func TestCheckProcessDeadAfterExit(t *testing.T) {
|
||||
var cmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/c", "exit", "0")
|
||||
} else {
|
||||
cmd = exec.Command("true")
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("start child: %v", err)
|
||||
}
|
||||
pid := cmd.Process.Pid
|
||||
if err := cmd.Wait(); err != nil {
|
||||
t.Fatalf("wait child: %v", err)
|
||||
}
|
||||
if got := defaultCheckProcess(pid); got != ProcessDead {
|
||||
t.Errorf("defaultCheckProcess(exited child pid=%d) = %v, want ProcessDead", pid, got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//go:build !windows
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// defaultCheckProcess reports whether process pid exists, using signal 0
|
||||
// (the standard POSIX existence probe). pid is assumed > 0; IsStale guards
|
||||
// the pid <= 0 case before calling.
|
||||
func defaultCheckProcess(pid int) ProcessState {
|
||||
err := syscall.Kill(pid, 0)
|
||||
switch {
|
||||
case err == nil:
|
||||
return ProcessAlive
|
||||
case errors.Is(err, syscall.ESRCH):
|
||||
return ProcessDead
|
||||
case errors.Is(err, syscall.EPERM):
|
||||
// Process exists but is owned by another user. It exists — that
|
||||
// is all IsStale needs. Conservative: treat as alive.
|
||||
return ProcessAlive
|
||||
default:
|
||||
return ProcessUnknown
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//go:build windows
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// processQueryLimitedInformation is the minimal access right needed to
|
||||
// probe a process's existence. Defined locally to avoid a dependency on
|
||||
// golang.org/x/sys/windows.
|
||||
const processQueryLimitedInformation = 0x1000
|
||||
|
||||
// errorInvalidParameter is the Win32 error OpenProcess returns when no
|
||||
// process with the given PID exists.
|
||||
const errorInvalidParameter = syscall.Errno(87)
|
||||
|
||||
// defaultCheckProcess reports whether process pid exists, by trying to
|
||||
// open a query handle to it. pid is assumed > 0; IsStale guards the
|
||||
// pid <= 0 case before calling.
|
||||
//
|
||||
// Note: a handle that opens successfully is treated as ProcessAlive even
|
||||
// in the rare zombie-handle case — this is conservative (it preserves
|
||||
// wall-clock fallback rather than declaring a live owner dead).
|
||||
func defaultCheckProcess(pid int) ProcessState {
|
||||
h, err := syscall.OpenProcess(processQueryLimitedInformation, false, uint32(pid))
|
||||
if err != nil {
|
||||
if errors.Is(err, errorInvalidParameter) {
|
||||
return ProcessDead
|
||||
}
|
||||
// Access-denied or any other error: the process may exist; do
|
||||
// not claim it is dead.
|
||||
return ProcessUnknown
|
||||
}
|
||||
syscall.CloseHandle(h)
|
||||
return ProcessAlive
|
||||
}
|
||||
Reference in New Issue
Block a user