Documentation
¶
Overview ¶
Package procreap finds and terminates processes that outlived the pipeline run whose worktree they were launched in.
Why a sweep is needed at all. Every subprocess no-mistakes spawns is already isolated in its own process group by internal/shellenv, and that group is killed on cancellation and on every exit path. A process group is, however, only a *lineage* container: a descendant that calls setsid(2) or setpgid(2) - which agent CLIs do for their own tool sandboxing, and which any daemonizing helper script does on purpose - leaves the group and becomes invisible to kill(-pgid). Once its parent dies it reparents to init, and from that moment no lineage-based mechanism (process group, parent chain, exec.Cmd handle) can ever name it again. Such a process keeps burning CPU and holding a deleted worktree's cwd open indefinitely.
The one identity that survives lineage loss is where the process is standing: its current working directory. Pipeline children are launched in (or below) the run's worktree, so a live process whose cwd resolves under <NM_HOME>/worktrees/<repoID>/<runID> belongs to that run, no matter who its parent is now. That is the association this package matches on.
Deliberately NOT matched: the command line. A path can appear in argv for entirely legitimate reasons (the daemon's own `git worktree remove`, a grep, an editor), so argv matching trades a precise signal for a false-positive that would kill an innocent process. cwd is held by exactly the processes that are actually running inside the run.
Safety rules, in order:
- pid 0 and 1 are never signalled, and neither is this process or any of its ancestors.
- A worktree whose run is still pending or running is never swept (Options.RunActive).
- Unscoped sweeps additionally require Options.MinAge, so a process that started moments ago is never mistaken for a leak.
- SIGTERM first; SIGKILL only for what is still alive after Options.Grace.
A process whose cwd is outside <NM_HOME>/worktrees can never match, which is what keeps long-lived unrelated daemons (a user's LaunchAgent worker, an editor, another tool's background job) out of reach by construction rather than by name-based allowlisting.
Index ¶
Constants ¶
const ( // DefaultMinAge is the age floor for an unscoped sweep. It is comfortably // longer than any single pipeline step's startup so a process that belongs // to a run the daemon is about to resume is never a candidate. DefaultMinAge = 10 * time.Minute // DefaultGrace is how long a matched process may take to exit after // SIGTERM before it is SIGKILLed. DefaultGrace = 3 * time.Second )
Variables ¶
This section is empty.
Functions ¶
func SweepAndLog ¶
SweepAndLog runs Sweep and reports the outcome on the daemon log. It is the form both call sites want: a sweep is best effort, and a failure to read the process table must never fail a run or block daemon startup.
Types ¶
type Options ¶
type Options struct {
// WorktreesRoot is <NM_HOME>/worktrees. Required.
WorktreesRoot string
// Scope, when set, restricts the sweep to exactly this worktree directory
// and to its subdirectories. The caller that sets it owns that run and has
// already observed its execution return, so RunActive and MinAge are not
// consulted for a scoped sweep.
Scope string
// MinAge skips processes younger than this. Ignored when Scope is set.
MinAge time.Duration
// RunActive reports whether the run still owns its worktree; its worktree
// is then left alone. A nil RunActive means "no run is active", which is
// only correct for a scoped sweep. Ignored when Scope is set.
RunActive func(repoID, runID string) bool
// Grace is how long a signalled process may exit before SIGKILL.
// Zero means DefaultGrace.
Grace time.Duration
}
Options configures a sweep.
type Victim ¶
type Victim struct {
PID int
PGID int
Command string
Worktree string
Killed bool // true when SIGTERM was not enough and SIGKILL was needed
}
Victim is a process the sweep terminated, reported for logging.
func Sweep ¶
Sweep terminates every process associated with a worktree that no run owns any more. It returns the processes it signalled. An empty result with a nil error is the normal outcome and also what platforms without a process-table reader (Windows, where job objects already contain the whole tree) return.