Documentation
¶
Index ¶
- func IsWSL() bool
- func KillPID(pid int, gracefulTimeout int) error
- func KillSessionJobObject(handle uintptr) error
- func KillSessionPGID(sessionPGID int, killSelfPID int, gracefulTimeout time.Duration, ...) error
- func KillWindowsPID(pid int) error
- func MembersOfPGID(pgid int) []int
- func ReadProcCmdline(pid int) string
- func ReadProcCwd(pid int) string
- func ShouldUseWindowsShell(path string) bool
- func SplitTasklistCSVLine(line string) []string
- func TailscaleDNSName(ctx context.Context) string
- type AncestorInfo
- type OrphanPGID
- type ProcInfo
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func KillPID ¶
KillPID sends SIGTERM to the process group for pid, waits up to gracefulTimeout seconds, then escalates to SIGKILL. On Windows it uses TerminateProcess.
func KillSessionJobObject ¶ added in v0.12.41
KillSessionJobObject is a no-op on non-Windows platforms. The session-wide process containment on Unix is implemented via POSIX process groups (see sessionpgid_unix.go's KillSessionPGID) so this stub exists only so cross-platform callers in internal/daemon can invoke the API unconditionally without build tags. The handle parameter is always zero on non-Windows (the client never reports a job handle when SessionJobHandle is unset) and this function returns nil immediately.
func KillSessionPGID ¶ added in v0.12.41
func KillSessionPGID(sessionPGID int, killSelfPID int, gracefulTimeout time.Duration, aggressive bool) error
KillSessionPGID sends SIGTERM to the POSIX process group identified by sessionPGID, waits up to gracefulTimeout for the group to drain, then escalates to SIGKILL. This is the session-shutdown equivalent of signalProcessGroup: it reaps every process that inherited the pgid from the PTY child session leader, including `npm run dev &` style jobs the coding agent spawned via non-interactive bash (which does not enable job control and therefore does not give backgrounded pipelines their own pgid).
aggressive selects SIGKILL immediately (used when the daemon is in aggressive-shutdown mode with a tight deadline, matching the existing ProcessManager.StopAll behavior).
killSelfPID is the caller's own PID. It is EXCLUDED from the kill so the daemon does not signal itself when it happens to share the pgid (it shouldn't, but defensive). Pass 0 to disable self-exclusion.
Returns an error only for the final verification step; best-effort EPERM and ESRCH during signal delivery are not reported since they are normal (the group may drain mid-loop).
func KillWindowsPID ¶ added in v0.13.5
KillWindowsPID forcibly terminates a Windows-side process identified by pid from a WSL host by shelling to taskkill.exe /F /PID <n>. Returns nil on success, an error wrapping taskkill's stderr otherwise.
taskkill.exe is the only mechanism that can reach a Windows-side PID from WSL; syscall.Kill returns ESRCH because the PIDs live in distinct kernel namespaces. Use this helper exclusively for PIDs you know are Windows-side (e.g. surfaced by FindPIDsByPort's netstat.exe fallback). Calling it on a Linux-side PID will silently no-op or fail because that PID does not exist in the Windows tasklist.
Errors are intentionally not nil-returned silently: callers must surface them as visible warning events per the Silent Failure Prohibition rule (.claude/rules/daemon-architecture.md). The most common failure modes:
- taskkill.exe missing — WSL interop disabled or not on PATH
- exit 128 — invalid argument (PID <= 0)
- exit 1 with "ERROR: The process \"<n>\" not found" — PID already gone
- exit 1 with "ERROR: Access is denied" — Windows ACL blocks termination
The 5s context deadline guards against a hung taskkill.exe holding up shutdown / preflight; the call is best-effort and a deadline-exceeded error is treated the same as any other failure by callers.
func MembersOfPGID ¶ added in v0.12.41
MembersOfPGID returns the PIDs of every process currently in the given POSIX process group. Callers: session cleanup (to verify the pgid is empty after SIGKILL), orphan scanning (to find sessions whose leaders are gone but children still live), and tests.
On WSL2 the same /proc layout applies, so no special-casing is needed. On platforms without /proc this falls back to best-effort: Getpgid(pid) on every pid in Scan() output. Returns nil on error.
func ReadProcCmdline ¶ added in v0.12.42
ReadProcCmdline returns the /proc/<pid>/cmdline contents with NUL separators rewritten to spaces, trimmed of any trailing whitespace. An empty string is returned on any read error or when the entry is empty (kernel threads, raced-away processes).
The returned string is suitable for substring matching against well-known invocations like "agnt run" or a daemon binary path. Callers must not attempt to round-trip it back into argv — the space-join is lossy for arguments that themselves contain spaces.
func ReadProcCwd ¶ added in v0.12.42
ReadProcCwd returns the resolved /proc/<pid>/cwd symlink target. An empty string is returned on any error (permission denied, process gone, non-Linux /proc without cwd entries). Callers must not assume the path is still meaningful if the target directory has been deleted — the kernel reports "(deleted)" suffixes in that case, which this function preserves verbatim so callers can detect it if they care.
func ShouldUseWindowsShell ¶ added in v0.13.5
ShouldUseWindowsShell reports whether path should be executed via the Windows shell (cmd.exe) rather than the Unix shell (sh). Returns true only on WSL when path looks Windows-shaped: a /mnt/<drive>/... DrvFs mount or a string containing a backslash (e.g. C:\foo or relative scripts\build.cmd that the user copied in from a Windows context).
On native Windows this returns true unconditionally — see the stub in process_windows.go. On non-WSL Unix it always returns false: there is no cmd.exe to dispatch to.
The empty-path case returns false: callers (config.ScriptConfig.ResolveShell) pass either Cwd or a run-command path, and an empty Cwd must fall through to the platform default rather than forcing cmd.exe.
func SplitTasklistCSVLine ¶ added in v0.13.10
SplitTasklistCSVLine splits one tasklist.exe CSV row, respecting quoted fields (the memory column embeds commas like "45,000 K"). Surrounding double quotes are stripped from each field.
func TailscaleDNSName ¶ added in v0.13.10
TailscaleDNSName returns this node's MagicDNS name (no trailing dot), e.g. "machine.tailnet.ts.net". Returns "" if tailscale is unavailable, not logged in, or the lookup times out.
If ctx has no deadline, a 2s timeout is applied. Safe to call without tailscale installed — exec errors collapse to "".
Types ¶
type AncestorInfo ¶ added in v0.12.42
AncestorInfo captures the per-process fields consulted by daemon-startup ownership gates when deciding whether an orphan pgid is plausibly owned by this daemon. Populated from /proc/<pid>/{cmdline,cwd,stat} on linux, from sysctl KERN_PROC_PID + lsof on darwin, and left empty on other platforms.
PID is the ancestor's PID. Cmdline is the argv joined by spaces (lossy for arguments that contain spaces). Cwd is the resolved working directory. Any field may be the empty string if the source data was unreadable (races where the process disappeared mid-walk are tolerated).
func WalkParents ¶ added in v0.12.42
func WalkParents(pid int) []AncestorInfo
WalkParents walks the parent chain of pid upward, returning each visited ancestor as an AncestorInfo with Cmdline and Cwd prepopulated. The walk includes PID 1 (init) in the chain — in production that's /sbin/init or systemd, whose cmdline and cwd will not match any agnt pattern and therefore contribute no evidence to ownership gates. In a PID namespace (unshare), PID 1 may be a meaningful ancestor (e.g. the namespace's designated init), which is exactly why we visit it: the procisolation daemon tests need to resolve reparented grandchildren back to a PID-namespaced init that impersonates a live agnt run.
The walk stops when:
- The next ppid is <= 0 (we've gone past init or the PID is a kernel thread with no parent recorded).
- The current pid itself is 0 (/proc/0 does not exist).
- /proc/<pid>/stat cannot be read for the current pid (race: the ancestor vanished between visits).
- A cycle is detected (PID re-seen; defensive, should not happen).
- A safety limit of 64 ancestors is reached (defensive).
The returned slice does NOT include pid itself — only strict ancestors. Callers that want the starting pid in the chain should prepend it.
This function is used by the daemon-startup orphan-pgid ownership gate to find an ancestor whose cmdline/cwd identifies the candidate pgid as owned by this daemon. Errors reading individual ancestors are silently skipped: the walk proceeds with whatever it can read.
type OrphanPGID ¶ added in v0.12.41
type OrphanPGID struct {
PGID int // process group ID (= original session leader PID)
Members []int // live member PIDs currently in this pgid
}
OrphanPGID describes a POSIX process group whose session leader PID is no longer alive but whose members are still running, owned by the caller's euid, and not members of any of the exclude sets the caller provided.
Found by ScanOrphanedPGIDs (linux: /proc walk; darwin: sysctl KERN_PROC_ALL; other unix: stub returns nil). Consumed by daemon startup cleanup to reap sessions that leaked across a daemon restart.
func ScanOrphanedPGIDs ¶ added in v0.12.41
func ScanOrphanedPGIDs(callerUID int, excludePGIDs map[int]bool) []OrphanPGID
ScanOrphanedPGIDs enumerates /proc and returns every pgid that qualifies as orphaned for the purpose of daemon-startup cleanup. A pgid is orphaned when ALL of the following are true:
- The pgid itself is > 1 (pid 0/1 are never treated as pgids).
- The pgid is NOT in excludePGIDs (typically: the caller's own pgid plus the pgid of the daemon process and any ancestor we don't want to kill).
- The leader PID (the process whose PID equals the pgid, i.e. the original session leader) does NOT currently exist. This is the "leader died, members reparented to init" case Slice B targets.
- At least one live process is still a member of the pgid.
- EVERY live member is owned by the caller's real uid. This is the safety barrier: if any member belongs to a different uid, we skip the entire pgid to avoid ever touching another user's processes. (root daemon, for example, must not reap a non-root user's pgids.)
callerUID is the uid that members must match. Pass syscall.Getuid() in production; tests pass an arbitrary value to exercise filtering paths.
Returns nil on /proc read error or when /proc is unavailable (non-Linux Unix). Callers that need absolute correctness on those platforms must layer their own fallback. On Linux and WSL2 /proc is always present.
type ProcInfo ¶
type ProcInfo struct {
PID int
PPID int // parent PID (0 if unavailable)
Command string // basename of the executable
Cmdline string // full command line
Cwd string // working directory (may be empty on some platforms)
}
ProcInfo describes a running process discovered via OS-level scanning.
func ScanWindows ¶
ScanWindows returns Windows-side processes when running under WSL. On non-WSL Unix this returns nil.