How no-mistakes Handles Process Cancellation Using Unix Process Groups
no-mistakes ensures clean subprocess termination by isolating commands in their own Unix process groups and sending SIGKILL to the entire group when cancellation is triggered.
Long-running subprocesses that spawn child processes (like test runners or Git commands) often leave orphaned grandchildren if the parent process is killed. The no-mistakes project solves this by leveraging Unix process group isolation to guarantee complete termination of entire process trees. This implementation lives primarily in the internal/shellenv package and is reused across the codebase for consistent cleanup semantics.
The Unix Process Group Strategy
Unix process groups provide a mechanism to treat related processes as a single unit for signal delivery. When a process creates a new process group and becomes its leader, all its descendants inherit that group ID by default.
no-mistakes exploits this behavior to solve the "orphan process" problem through two coordinated actions:
-
Process Group Creation – By setting
Setpgid: trueinsyscall.SysProcAttr, the spawned process becomes the leader of a new process group. This mirrors thesetpgid(2)system call, ensuring that any grandchildren automatically join the same group. -
Group-Wide Termination – Instead of killing only the parent process, the cancellation routine sends
SIGKILLto the negative process ID, which targets the entire process group.
This approach guarantees that when a context is cancelled, no stray subprocesses remain to lock worktrees or consume resources.
Implementation in shellenv.ConfigureShellCommand
The core logic resides in internal/shellenv/shell_command_unix.go within the ConfigureShellCommand function. This helper transforms a standard exec.Cmd into a properly isolated, cancellable command.
When invoked, the function performs two critical modifications to the command:
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Cancel = func() error {
err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
// Handle ESRCH (process already gone)...
return err
}
The Setpgid flag places the process in its own group, while the custom Cancel closure overrides the default behavior. The negative sign in -cmd.Process.Pid instructs the kernel to deliver the signal to every process in that group, not just the leader.
If the group no longer exists (returning ESRCH), the function returns os.ErrProcessDone, signaling that termination has already occurred.
Practical Code Examples
Spawning a Cancellable Shell Command
To execute a long-running task that can be fully aborted, wire the context through exec.CommandContext and apply the shellenv configuration:
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := exec.CommandContext(ctx, "bash", "-c", "some-long-running-task")
shellenv.ConfigureShellCommand(cmd) // Isolates in its own process group
if err := cmd.Start(); err != nil {
log.Fatalf("start failed: %v", err)
}
// Later, to abort the entire process tree:
cancel() // Triggers cmd.Cancel → SIGKILL to the whole group
Calling cancel() propagates through the context, triggering the custom Cancel function that kills the entire process group.
Integration in Pipeline Steps
Pipeline steps utilize this helper to ensure cleanup even when scripts spawn complex subprocess hierarchies:
func runShellStep(step *pipeline.Step) error {
ctx, cancel := context.WithCancel(step.Ctx)
defer cancel()
cmd := exec.CommandContext(ctx, step.Shell, step.Script)
shellenv.ConfigureShellCommand(cmd) // Ensures proper cancellation
cmd.Stdout = step.Stdout
cmd.Stderr = step.Stderr
return cmd.Run()
}
This pattern ensures that if the step times out or is interrupted, no grandchildren persist beyond the step's lifecycle.
Testing the Behavior
The test suite in internal/pipeline/steps/shell_unix_test.go validates this behavior by launching a child process that spawns a grandchild. When the test triggers context cancellation, it verifies that both processes terminate, confirming that Setpgid combined with group-wide SIGKILL successfully cleans up the entire tree.
Cross-Cutting Usage Across the Codebase
The ConfigureShellCommand helper is not limited to shell execution. The same pattern appears in internal/daemon/recover_servers_unix.go, where daemon-managed servers use process group isolation to ensure recovery routines can terminate stale server processes completely.
Similarly, cmd/recordfixture/proc_unix.go reuses this logic for fixture-recording subprocesses, ensuring that recording tools and their dependencies are fully terminated when fixtures are captured or aborted.
While the Unix implementation relies on Setpgid and syscall.Kill, the Windows counterpart in internal/shellenv/shell_command_windows.go achieves equivalent semantics using job objects, providing consistent cross-platform behavior.
Summary
- no-mistakes prevents orphaned processes by leveraging Unix process groups in
internal/shellenv/shell_command_unix.go. - The
ConfigureShellCommandfunction setsSetpgid: trueto create a new process group and overridescmd.Cancelto sendSIGKILLto the entire group. - The negative PID in
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)targets all processes in the group, not just the parent. - This pattern is reused across the codebase, including daemon recovery and fixture recording, ensuring consistent termination semantics.
- The implementation gracefully handles cases where the process group has already exited by returning
os.ErrProcessDone.
Frequently Asked Questions
How does no-mistakes ensure that grandchild processes are killed when a command is cancelled?
By setting Setpgid: true in syscall.SysProcAttr, the spawned process becomes a process group leader. All descendants inherit this group ID. When cancellation occurs, no-mistakes calls syscall.Kill with the negative process ID (e.g., -cmd.Process.Pid), which delivers SIGKILL to every process in that group, ensuring grandchildren are terminated along with their parent.
What happens if the process group no longer exists when cancellation is triggered?
The syscall.Kill call returns ESRCH (no such process) if the group has already terminated. The implementation catches this condition and returns os.ErrProcessDone, indicating that the cleanup has already occurred and no further action is needed.
Where else in the no-mistakes codebase is this process group logic used?
Beyond shell commands in internal/shellenv/shell_command_unix.go, the same process group isolation appears in internal/daemon/recover_servers_unix.go for terminating daemon-managed servers and in cmd/recordfixture/proc_unix.go for managing fixture-recording subprocesses. This ensures consistent cleanup behavior across all subprocess management in the application.
Is there a Windows equivalent to this Unix process group behavior?
Yes, the Windows implementation in internal/shellenv/shell_command_windows.go uses job objects instead of process groups. This provides equivalent functionality for grouping and terminating processes on Windows platforms, maintaining consistent cross-platform semantics for process cancellation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →