Own the workload, not just the process.
Cross-platform process tree lifecycle manager, supervisor, and live-reload engine for Go.
Graceful shutdown, two-phase force-kill escalation, interactive I/O streaming, telemetry, and zero orphan processes on Linux, macOS, and Windows.
- Why trexec? (The Problem Space)
- Key Value Proposition & Architecture
- Installation & Compatibility
- Complete API Reference Manual
- Operating System Kernel Primitives
- Performance Benchmarks
- Production Recipes & Patterns
- License
Go's standard os/exec only manages a single root process PID. In modern software, commands routinely spawn deep process trees:
npm run dev→sh→vite→esbuild→ worker threadsdocker-compose up→ multiple container daemonspython main.py→ multiprocessing pools & background workers
When a context.Context is cancelled with os/exec.CommandContext:
os/execsendsSIGKILLonly to the root process (npm).- The child and grandchild processes (
vite,esbuild, workers) are orphaned. - Ports remain bound, file locks remain held, and the next run fails with
EADDRINUSE: address already in use. - If grandchildren hold open
stdout/stderrfile descriptors,cmd.Wait()hangs indefinitely waiting for EOF.
os/exec.CommandContext (Standard Library)
your-app ──► npm (KILLED)
└── vite (ORPHANED ⚠️)
└── esbuild (ORPHANED ⚠️ — Port 3000 stays locked!)
trexec.CommandContext (Cross-Platform Solution)
your-app ──► [ Process Group / Windows Job Object ]
├── npm (Gracefully stopped -> Cleaned ✅)
├── vite (Gracefully stopped -> Cleaned ✅)
└── esbuild (Gracefully stopped -> Cleaned ✅ — Port released!)
trexec solves this natively across Linux, macOS, and Windows with zero third-party runtime dependencies.
- 🌲 Complete Tree Ownership: Treats the entire descendant process tree as an ownable, cleanable workload.
- ⏱️ Two-Phase Graceful Shutdown: Polite termination signal (
SIGTERM,SIGINT,Ctrl+Break) → configurable grace period → kernel force-kill (SIGKILL,TerminateJobObject). - 🛡️ Pipe Deadlock Timeout Guard: Built-in I/O timeouts unblock stuck
io.Copygoroutines if orphaned writers keep pipes open. - 🔀 Synchronized Stream Buffering:
Output()andCombinedOutput()return captured byte streams alongside rich outcome metadata. - 💬 Interactive Stdin Streaming:
StdinPipe()provides dynamicio.WriteCloserstreaming with clean EOF signaling. - 🔍 Process Tree Introspection: Returns
Result.DescendantPIDs []intandResult.ProcessesCleaned intusing Windows Job Object query APIs and Linux/proc. - 🔄 Pure Go Live-Reload Engine:
trexec/watcherdebounces filesystem events, drains old workloads completely, and guarantees socket release on restart. - 👮 Embedded Multi-Worker Supervisor:
trexec/supervisorprovides in-process worker pool management, restart policies (RestartAlways,RestartOnFailure), and exponential backoff with jitter. - 📊 Real-Time Telemetry & Metrics:
trexec/telemetry&WithMetricsPollIntervalstream live process counts, memory usage, CPU time, and state transitions to OpenTelemetry/Prometheus. - 🐍 Cobra CLI Integration:
trexec/cobraexecwrapsspf13/cobracommands with zero-leak process tree lifecycle supervision. - 🌐 Unified Cross-Platform Signals: Write
trexec.WithGracefulSignal(trexec.SIGINT)once; compiles and executes identically on Linux, macOS, and Windows with zero OS-specific imports. - 🚦 6-State Formal Lifecycle: Track execution state (
Created→Starting→Running→Stopping→Killing→Done) viaWithOnStateChange. - 📦 Zero Third-Party Runtime Dependencies: Pure Go standard library on Unix; official
golang.org/x/syson Windows.
go get github.com/Chokqu/trexecCompatible with Go 1.21, 1.22, 1.23, 1.24, 1.25, and 1.26+ on:
- Linux (x86_64, ARM64, ARM)
- macOS (Apple Silicon ARM64, Intel x86_64)
- Windows (x86_64, ARM64)
func CommandContext(ctx context.Context, name string, args ...any) *RunnerCreates a new Runner bound to the provided context.
- Parameters:
ctx: Parent context governing command lifecycle.name: Binary name or executable path.args: Variadic slice accepting command-line arguments (strings or[]string) andOptionfunctional configurations in any order.
- Returns:
*Runnerready forStart(),Wait(),Run(),Output(),CombinedOutput(), orStdinPipe().
func Run(ctx context.Context, name string, args ...any) errorConvenience helper that initializes a Runner, executes the command, blocks until natural completion or cancellation, and returns an error if setup fails or exit code is non-zero.
func RunWithResult(ctx context.Context, name string, args ...any) (*Result, error)Executes a command and returns the full structured *Result.
- Returns:
(*Result, error). The returnederroris non-nil only for startup/fork failures (binary not found, permission denied). Command crashes and cancellations are stored insideResult.ExitCodeandResult.Error.
func Output(ctx context.Context, name string, args []string, opts ...Option) ([]byte, *Result, error)Direct package-level helper that runs the command and captures its standard output.
func CombinedOutput(ctx context.Context, name string, args []string, opts ...Option) ([]byte, *Result, error)Direct package-level helper that runs the command and captures thread-safe interleaved standard output and standard error.
Starts the command process tree. Creates process group / Job Object, launches I/O goroutines, starts context monitor, and returns immediately without blocking.
Blocks until command completion or cancellation cleanup finishes. Executes two-phase graceful shutdown if context was cancelled, cleans up pipes with WithIOTimeout, closes OS handles, and returns *Result.
Calls Start() followed by Wait().
Runs the command, captures stdout into an internal buffer, and returns captured bytes along with *Result. Returns error if WithStdout was already configured.
Runs the command, captures stdout and stderr through a mutex-synchronized writer, and returns combined bytes along with *Result. Returns error if WithStdout or WithStderr was already configured.
Returns a pipe write end connected to child stdin. Must be called before Start(). Closing the writer sends EOF to child. Automatically closed on Wait() if left open.
Returns the process ID of the direct child process, or 0 if not running.
| Option | Signature | Default | Description |
|---|---|---|---|
WithGracePeriod |
func(d time.Duration) Option |
5s |
Duration to wait after graceful signal before force-killing. Set to 0 for immediate force-kill. |
WithGracefulSignal |
func(sig Signal) Option |
SIGTERM |
Polite signal sent on cancellation (SIGINT, SIGTERM, SIGHUP). |
WithIOTimeout |
func(d time.Duration) Option |
2s |
Timeout to wait for I/O goroutines after child exits before force-closing pipes. |
WithStdout |
func(w io.Writer) Option |
nil (discard) |
Destination writer for command standard output. |
WithStderr |
func(w io.Writer) Option |
nil (discard) |
Destination writer for command standard error. |
WithStdin |
func(r io.Reader) Option |
nil |
Source reader for command standard input. |
WithDir |
func(dir string) Option |
"" (inherit) |
Working directory for the process tree. |
WithEnv |
func(env []string) Option |
nil (inherit) |
Environment variables formatted as ["KEY=VALUE"]. |
WithExtraFiles |
func(files []*os.File) Option |
nil |
Additional open file descriptors passed to child (fd 3, 4, ...). |
WithSysProcAttr |
func(attr *syscall.SysProcAttr) Option |
nil |
Merges user platform attributes with required process group attributes. |
WithOnStateChange |
func(fn func(State)) Option |
nil |
Asynchronous callback invoked on every lifecycle state transition. |
WithResourceLimits |
func(limits ResourceLimits) Option |
nil |
Kernel-enforced memory and process count boundaries. |
WithMetricsPollInterval |
func(interval time.Duration, cb func(TreeMetrics)) Option |
nil |
Periodic telemetry poller streaming real-time RAM, CPU, PID metrics. |
type Result struct {
ExitCode int // Process exit code (-1 if killed)
Cancelled bool // True if stopped due to context cancellation
GracefullyTerminated bool // True if stopped within grace period
ForceKilled bool // True if killed after grace period expired
Duration time.Duration // Total wall-clock execution duration
ProcessesCleaned int // Count of descendant processes terminated
DescendantPIDs []int // Exact slice of tracked descendant PIDs
Error error // Underlying ExitError or execution error
}
func (r *Result) Success() bool
func (r *Result) String() stringtype TreeMetrics struct {
Timestamp time.Time // Snapshot timestamp
ActiveProcesses int // Count of live descendant processes
TotalMemoryBytes int64 // Cumulative committed RAM (if available)
TotalCPUTime time.Duration // Cumulative CPU execution time (if available)
State State // Current lifecycle state
}type ResourceLimits struct {
MaxMemoryBytes int64 // Maximum committed memory across process tree
MaxProcesses int // Maximum simultaneously active processes allowed
}type Signal int
const (
SIGTERM Signal = iota // Unix: kill(-pgid, SIGTERM), Windows: CTRL_BREAK_EVENT
SIGINT // Unix: kill(-pgid, SIGINT), Windows: CTRL_C_EVENT
SIGHUP // Unix: kill(-pgid, SIGHUP), Windows: CTRL_BREAK_EVENT
SIGKILL // Unix: kill(-pgid, SIGKILL), Windows: TerminateJobObject
)type State int
const (
StateCreated State = iota // Created, not started
StateStarting // Configuring group, calling fork/exec
StateRunning // Process alive, I/O active, context monitored
StateStopping // Graceful termination signal active
StateKilling // Force kill in progress
StateDone // Cleaned up, handles closed, result ready
)type ExitError struct {
ExitCode int
Signal string
Stderr []byte
Cancelled bool
}
func (e *ExitError) Error() string
func (e *ExitError) Unwrap() error // Returns context.Canceled if Cancelled == trueThe supervisor package provides in-process multi-worker supervision with exponential backoff and jitter.
import "github.com/Chokqu/trexec/supervisor"type Supervisor struct {}
func New() *Supervisor
func (s *Supervisor) Add(spec Spec) error
func (s *Supervisor) Start(ctx context.Context) error
func (s *Supervisor) Wait() map[string]*trexec.Result
func (s *Supervisor) Stop() error
func (s *Supervisor) Status() map[string]WorkerStatustype Spec struct {
Name string
Command string
Args []string
RestartPolicy RestartPolicy
MaxRestarts int
Backoff *Backoff
GracePeriod time.Duration
GracefulSignal trexec.Signal
Stdout io.Writer
Stderr io.Writer
Dir string
Env []string
}type RestartPolicy int
const (
RestartNever RestartPolicy = iota
RestartAlways
RestartOnFailure
)type Backoff struct {
Min time.Duration // Default: 100ms
Max time.Duration // Default: 10s
Factor float64 // Default: 2.0
Jitter float64 // Default: 0.1 (±10%)
}
func DefaultBackoff() *Backoff
func NewBackoff(min, max time.Duration, factor, jitter float64) *Backoff
func (b *Backoff) Duration(attempt int) time.DurationThe watcher package provides a pure Go debounced filesystem change detector and hot-reloading devserver.
import "github.com/Chokqu/trexec/watcher"type Watcher struct {}
func New(cfg Config) *Watcher
func (w *Watcher) Start(ctx context.Context) (<-chan []string, error)type Config struct {
Paths []string // Root directories/files to watch
Extensions []string // Extensions to match (e.g. [".go", ".html"])
IgnoredNames []string // Directories/files to ignore ([".git", "vendor"])
Debounce time.Duration // Coalesce window (default: 150ms)
PollInterval time.Duration // Filesystem scan period (default: 200ms)
}
func DefaultConfig(paths ...string) Configtype Reloader struct {}
func NewReloader(cfg ReloaderConfig) *Reloader
func (r *Reloader) Run(ctx context.Context) errortype ReloaderConfig struct {
Watcher Config
Command string
Args []string
Dir string
Env []string
Stdout io.Writer
Stderr io.Writer
GracePeriod time.Duration
GracefulSignal trexec.Signal
OnRestart func(attempt int, changedFiles []string)
}The telemetry package routes structured lifecycle events and periodic metrics to observability stacks.
import "github.com/Chokqu/trexec/telemetry"type Event struct {
Timestamp time.Time
State trexec.State
PID int
ActiveProcesses int
ExitCode int
Duration time.Duration
Error error
Message string
}type Sink interface {
EmitEvent(event Event)
EmitMetrics(metrics trexec.TreeMetrics)
}MemorySink: Thread-safe slice buffer for inspection and tests (NewMemorySink(),Events(),Metrics(),Reset()).LoggerSink: Formats events and metrics to text output (NewLoggerSink(w io.Writer, prefix string)).CallbackSink: Direct closure adapter (OnEvent,OnMetrics).HookOptions: Convenience bridge helper:func HookOptions(sink Sink, metricsInterval time.Duration) []trexec.Option
The cobraexec package provides middleware for github.com/spf13/cobra without forcing an external dependency.
import "github.com/Chokqu/trexec/cobraexec"type CobraLikeCommand interface {
Context() context.Context
OutOrStdout() io.Writer
ErrOrStderr() io.Writer
InOrStdin() io.Reader
}func Run(cmd CobraLikeCommand, name string, args []string, opts ...trexec.Option) (*trexec.Result, error)
func Output(cmd CobraLikeCommand, name string, args []string, opts ...trexec.Option) ([]byte, *trexec.Result, error)
func CombinedOutput(cmd CobraLikeCommand, name string, args []string, opts ...trexec.Option) ([]byte, *trexec.Result, error)
func WrapRunE(name string, args []string, opts ...trexec.Option) func(cmd CobraLikeCommand, cliArgs []string) error- Process Groups (
setpgid): Child process becomes the leader of its own process group (setpgid(0, 0)). All descendants inheritPGID = child.PID. - Negative PID Broadcasting: Signals are delivered to
-pgid(syscall.Kill(-pgid, sig)), delivering signals simultaneously to all child and grandchild processes. - Linux
PR_SET_PDEATHSIG: Configures kernel death signals so child trees terminate automatically if the parent process crashes. /procIntrospection: Scans/proc/*/stat(field 5pgrp == targetPGID) to enumerate live descendant PIDs.
- Win32 Job Objects: Anonymous Job Object configured with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK. - Suspended Process Assignment: Spawns the root process with
CREATE_SUSPENDED, assigns handle viaAssignProcessToJobObject, and unwinds thread suspension counts usingCreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0). - Graceful Console Events: Sends
CTRL_BREAK_EVENTorCTRL_C_EVENTviaGenerateConsoleCtrlEvent. - Fallback Tree Termination: If running in restricted sandboxes where Job Object creation is blocked, transparently degrades to recursive process tree termination (
taskkill /F /T /PID).
Measured on Apple Silicon (M-series / ARM64, 8 threads, Go 1.26):
| Benchmark Target | Latency | Memory Allocations | Description |
|---|---|---|---|
BenchmarkStandardExec (Baseline) |
2.82 ms/op |
10,368 B/op (30 allocs) |
Raw os/exec.Command().Run() |
BenchmarkTrexecRun |
3.17 ms/op |
11,584 B/op (47 allocs) |
trexec.Run() with tree ownership |
BenchmarkTrexecSpawnLatency |
2.93 ms/op |
11,558 B/op (47 allocs) |
Group creation, fork/exec, initialization |
BenchmarkTrexecTreeKill |
699 µs/op |
12,160 B/op (55 allocs) |
Immediate force-kill of multi-level tree |
BenchmarkTrexecOutputBuffering |
3.09 ms/op |
13,512 B/op (52 allocs) |
Capturing stdout with rich Result struct |
BenchmarkTrexecCombinedOutput |
2.91 ms/op |
77,839 B/op (56 allocs) |
Synchronized multi-stream capture |
BenchmarkTrexecSupervisorRestart |
4.38 ms/op |
12,453 B/op (57 allocs) |
Worker termination & supervised respawn |
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
result, err := trexec.RunWithResult(ctx, "npm", []string{"run", "dev"},
trexec.WithGracePeriod(5*time.Second),
trexec.WithGracefulSignal(trexec.SIGINT),
trexec.WithStdout(os.Stdout),
trexec.WithStderr(os.Stderr),
)
if err != nil {
log.Fatal(err)
}
if result.Cancelled && result.GracefullyTerminated {
log.Println("Dev server cleanly shut down.")
}reloader := watcher.NewReloader(watcher.ReloaderConfig{
Watcher: watcher.DefaultConfig("./src"),
Command: "go",
Args: []string{"run", "./src"},
GracePeriod: 2 * time.Second,
Stdout: os.Stdout,
Stderr: os.Stderr,
})
_ = reloader.Run(ctx)var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start backend service",
RunE: cobraexec.WrapRunE("python", []string{"app.py"}, trexec.WithGracePeriod(3*time.Second)),
}This project is licensed under the MIT License — see the LICENSE file for details.