lifecycle is a Go library for managing application shutdown signals and interactive terminal I/O robustly. It centralizes the "Dual Signal" logic and "Interruptible I/O" patterns originally extracted from Trellis and designed for any tool needing robust signal handling.
To be the standard Control Plane for Infrastructure-Aware Applications (Services, Agents, CLIs).
- v1 (Foundation): Solves "Death Management" (Signals, Blocking I/O, Zombies).
- v2 (Evolution): Solves "Life Management" (Events, Reactions, Hot Reloading).
Important
v2.x (Current): Introduces the Application Control Plane, generalizing "Signals" into "Events" (Hot Reload, Health Checks, Input Commands).
v1.x (Stable - LTS): Focuses strictly on Death Management (Graceful Shutdown, Signals, Leak Prevention).
go get github.com/aretw0/lifecycle- SignalContext: Differentiates between
SIGINT(User Interrupt) andSIGTERM(System Shutdown).- Configurable Thresholds: Support for "Industry Standard" (Cancel on 1st), "Escalation" (Cancel on Nth), and "Unsafe" (No auto-kill) modes.
- SIGTERM: Always triggers immediate graceful shutdown (context cancellation).
- TermIO:
- InterruptibleReader: Wraps
io.Readerto allowRead()calls to be abandoned when a context is cancelled (avoids goroutine leaks). - Platform Aware: Automatically uses
CONIN$on Windows.- Why? On Windows, standard
os.Stdincloses immediately upon receiving a signal (like Ctrl+C), causing a fatalEOFbefore the application can gracefully handle the signal.lifecycleswitches toCONIN$, which keeps the handle open, allowing theSignalContextto process the event.
- Why? On Windows, standard
- UpgradeTerminal: Helper to upgrade an arbitrary existing
io.Reader(if it identifies as a terminal) to the safe platform-specific reader.
- InterruptibleReader: Wraps
- Observability & Introspection:
- Unified Dashboard:
SystemDiagramsynthesizes Signal and Worker states into a single Mermaid visualization. - Rich Metrics: Built-in providers for tracking shutdown health, data loss, and shutdown latency.
- Stall Detection: Automatically detects and warns if a shutdown hook is stalled (runs > 5s).
- Unified Dashboard:
- Reliability Primitives (v1.4):
- Critical Sections:
lifecycle.Do(ctx, fn)shields atomic operations from cancellation and returns any error from the protected function. - Introspection:
SignalContext.Reason()to differentiate between "Manual Stop", "Interrupt", or "Timeout".
- Critical Sections:
- Worker & Supervisor (v1.3):
- Unified Interface: Standard
Start,Stop,Waitcontract for Processes, Goroutines, and Containers. - Supervision Tree:
Supervisormanages hierarchical worker clusters with restart policies (OneForOne,OneForAll). - Dynamic Topology: Add or remove workers at runtime.
- Functional Workers: Turn any Go function into a managed Worker.
- Process Hygiene: Automatic cleanup of child processes if the parent dies (Job Objects/PDeathSig).
- Handover Protocol: Standardized environment variables (
LIFECYCLE_RESUME_ID,LIFECYCLE_PREV_EXIT) to pass context across restarts. - Container Abstraction: Generic interface to manage containerized workloads without direct SDK dependencies.
- Unified Interface: Standard
- DX Helpers (v1.4 & v2.0):
Run: One-linemainentry point with options (WithLogger,WithMetrics).NewInteractiveRouter: Pre-configured router for CLIs (Signals + Input + Commands).Sleep: Context-aware sleep (returns immediately on cancel).OnShutdown: Type-safe hook registration without casting.
- Event Router: Generalize
SignalsintoEvents(Webhook, FileWatch, HealthCheck). - Managed Concurrency:
lifecycle.Go(ctx, fn)for non-leaking goroutines. - Reactions:
Reload,Suspend,ScalealongsideShutdown.
lifecycle now provides primitives to manage goroutines safely, ensuring they respect shutdown signals and provide visibility.
lifecycle.Run(func(ctx context.Context) error {
// Fire-and-forget but tracked and panic-safe
lifecycle.Go(ctx, func(ctx context.Context) error {
// ...
return nil
})
return nil
})For 99% of CLI applications, you just need lifecycle.Run. It handles signals, context cancellation, and cleanup automatically.
package main
import (
"context"
"fmt"
"time"
"github.com/aretw0/lifecycle"
)
func main() {
// 1. Wrap your logic in a Job
// 2. lifecycle.Run manages the boring "Death Management" stuff
lifecycle.Run(lifecycle.Job(func(ctx context.Context) error {
fmt.Println("App started. Press Ctrl+C to exit.")
// 3. Use lifecycle.Go to spawn safe, tracked background tasks
lifecycle.Go(ctx, func(ctx context.Context) error {
// This goroutine is automatically waited for on shutdown
// Panics are caught and logged, preventing crashes
select {
case <-ctx.Done():
return nil
case <-time.After(5 * time.Second):
fmt.Println("Task complete")
return nil
}
})
// 4. Wait for interrupt
<-ctx.Done()
fmt.Println("Shutting down...")
return nil
}))
}Reading from Stdin on Windows is tricky. lifecycle solves the "Ctrl+C kills the prompt" problem by automatically using CONIN$.
// Smart Open (handles Windows CONIN$)
reader, _ := lifecycle.OpenTerminal()
defer reader.Close()
// Wrap to respect context cancellation (prevents blocked Read calls)
r := lifecycle.NewInterruptibleReader(reader, ctx.Done())
buf := make([]byte, 1024)
n, err := r.Read(buf)
if lifecycle.IsInterrupted(err) {
return // Clean exit
}Register cleanup functions that run after the context is cancelled but before the process exits.
lifecycle.OnShutdown(ctx, func() {
db.Close()
fmt.Println("Cleanup done locally")
})For complex long-running services/agents that need dynamic behavior (Hot Reload, Supervisors).
lifecycle.Go(ctx, fn) is designed to work within lifecycle.Run. However, if you use it in a standalone script, it safely falls back to a Global Task Tracker.
func main() {
ctx := context.Background()
// Works safely even without lifecycle.Run
lifecycle.Go(ctx, func(ctx context.Context) error {
// ...
return nil
})
// Explicitly wait for global tasks (required without Run)
lifecycle.WaitForGlobal()
}Manage long-running processes, containers, or goroutines with restarts and hygiene.
// Create a Supervisor with a "OneForOne" restart strategy
sup := lifecycle.NewSupervisor("agent", lifecycle.SupervisorStrategyOneForOne,
lifecycle.NewProcessWorker("pinger", "ping", "1.1.1.1"),
lifecycle.NewWorkerFromFunc("metrics", metricsLoop),
)
sup.Start(ctx)
<-ctx.Done()
sup.Stop(context.Background())Generate live architecture diagrams of your running application.
// Generate Mermaid Dashboard
diagram := lifecycle.SystemDiagram(ctx.State(), supervisor.State())
fmt.Println(diagram)The library uses a consistent color palette for all generated diagrams:
- π‘ Pending: Defined but not yet active.
- π΅ Running: Active and healthy.
- π’ Stopped: Successfully terminated.
- π΄ Failed: Crashed or terminated with error.
The library implements Context-Aware I/O to balance data preservation and responsiveness:
Read()(Pipeline Safe): Uses a Shielded Return strategy. If data arrives simultaneously with a cancellation signal, it returns the data (nil error). This guarantees no data loss in pipelines or logs.ReadInteractive()(Interactive Safe): Uses a Strict Discard strategy. If the user hits Ctrl+C while typing, any partial input is discarded to prevent accidental execution of commands.