procreap

package
v1.55.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 12 Imported by: 0

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

View Source
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

func SweepAndLog(opts Options, reason string)

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 Process

type Process struct {
	PID     int
	PPID    int
	PGID    int
	Command string
	Elapsed time.Duration
}

Process is one entry of the system process table.

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

func Sweep(opts Options) ([]Victim, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL