Cross-platform Go helpers for running shell and exec commands in an isolated process group or tree, streaming stdout/stderr, killing the full child tree on context cancellation or timeout, and inspecting process state.
From pkg.go.dev:
go get github.com/brandonkramer/proctreectx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var buf bytes.Buffer
spec := &proctree.Spec{Shell: "make test"}
opts := &proctree.Options{
OnStdout: func(line string) { fmt.Println("out:", line) },
Stdout: &buf,
}
res, err := proctree.Run(ctx, spec, opts)
if err != nil {
// context.Canceled, context.DeadlineExceeded, or non-zero exit
}
fmt.Println("exit", res.ExitCode, "pid", res.PID, "started", res.StartedAt)res, err := proctree.Run(ctx, &proctree.Spec{
Path: "/usr/bin/git",
Args: []string{"status", "--short"},
Dir: "/path/to/repo",
Env: []string{"GIT_TERMINAL_PROMPT=0"},
}, &proctree.Options{
Stdin: strings.NewReader(""),
})res := proctree.ReconcilePID(pid, &proctree.Ownership{
Spec: *spec,
StartedAt: startedAt,
})
switch res.Outcome {
case proctree.ReconcileKilled:
// verified and terminated
case proctree.ReconcileUnverified:
// alive but not ours — left running (fail closed)
case proctree.ReconcileNotRunning:
// already gone
}opts := proctree.TeeOptions(stdoutFile, stderrFile,
func(line string) { emit("stdout", line) },
func(line string) { emit("stderr", line) },
)
proctree.Run(ctx, spec, opts)own := &proctree.Ownership{
Spec: *spec,
StartedAt: res.StartedAt,
}
if proctree.VerifyOwnership(pid, own) {
_ = proctree.KillTreeByPID(pid)
}Custom matcher for processes that do not match Spec shape:
own := &proctree.Ownership{
StartedAt: res.StartedAt,
Match: func(parts []string) bool {
return len(parts) > 0 && strings.Contains(parts[0], "my-worker")
},
}
proctree.VerifyOwnershipOpts(pid, own, &proctree.VerifyOptions{
SkipProcessGroup: true,
})VerifyOwnership checks cmdline match, Unix process-group leader (when applicable), and optional create-time window. Fails closed when uncertain.
info, err := proctree.Inspect(pid)
// info.Cmdline, info.CreateTime, info.MemoryRSS, info.Status, info.OpenFiles (linux)
kids, _ := proctree.Children(pid)
tree, _ := proctree.InspectTree(pid) // pid + descendantscmd := proctree.NewCommand(&proctree.Spec{Shell: "sleep 300"})
cmd.Start()
proctree.KillTree(cmd)
proctree.KillTreeByPID(pid)
proctree.Alive(pid)
proctree.VerifyOwned(pid, spec)| Platform | Process isolation | Tree kill | Alive | Inspect sources | Ownership verify |
|---|---|---|---|---|---|
| Linux | Setpgid |
SIGKILL to -pid |
/proc stat (skip zombies) |
/proc |
cmdline + pgid + create time |
| macOS | Setpgid |
SIGKILL to -pid and pid |
sysctl stat (skip zombies) | sysctl (KinfoProc, procargs2) |
cmdline + pgid + create time |
| Windows | process group + Job Object | TerminateJobObject (fallback: descendant walk) |
OpenProcess |
Toolhelp + NT APIs | cmdline + create time |
Runassigns each child to a kill-on-close Job Object when the OS allows it.- Inspect uses
NtQueryInformationProcess,GetProcessTimes, andGetProcessMemoryInfo(nowmic).
Alive consults /proc on Linux and sysctl process state on macOS so zombies count as not running. Use VerifyOwnership or ReconcilePID before killing stale pids.
Execution
Run(ctx, spec, opts)— context-first execution with streaming, optional stdin/writer sinks, tree kill on cancel/timeoutNewCommand(spec)— build a configured*exec.Cmdwithout startingKillTree(cmd)/KillTreeByPID(pid)— terminate process treesAlive(pid)— liveness probe
Recovery
ReconcilePID(pid, own)/ReconcilePIDOpts(pid, own, opts)— verify-and-kill for daemon recoveryWaitNotAlive(pid, timeout)— poll until process exits
Streaming helpers
TeeLine(w, fn)/TeeOptions(stdoutW, stderrW, onStdout, onStderr)— mirror lines to writers and callbacks
Verification
VerifyOwned(pid, spec)— cmdline + platform checksVerifyOwnership(pid, own)— adds create-time window and optionalCmdlineMatcherVerifyOwnershipOpts(pid, own, opts)— tune timing, matcher, andSkipProcessGroup
Introspection
Inspect(pid)— point-in-timeProcessInfosnapshotInspectTree(root)— snapshots for root + descendantsChildren(pid)/Descendants(root)— process tree discoveryCmdline(pid)/CreateTime(pid)— convenience accessors
Lefthook and golangci-lint are pinned in go.mod as tools (dev-only). Install git hooks once per clone:
make install-hooksHooks and make lint use go tool binaries from go.mod. Pre-commit lints staged .go files; pre-push runs ./scripts/check.sh. Skip hooks with LEFTHOOK=0 git push.
make check
make test
make lintTagged semver releases are published to pkg.go.dev. See GitHub releases for notes.
MIT