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).
[!IMPORTANT] v1.5+ (Current): Provides the Application Control Plane, generalizing “Signals” into “Events” (Hot Reload, Health Checks, Input Commands). Contains breaking changes from v1.4 - see MIGRATION.md.
v1.0-v1.4 (LTS): Focuses strictly on Death Management (Graceful Shutdown, Signals, Leak Prevention).
go get github.com/aretw0/lifecycle
SIGINT (User Interrupt) and SIGTERM (System Shutdown).
io.Reader to allow Read() calls to be abandoned when a context is cancelled (avoids goroutine leaks).CONIN$ on Windows.
os.Stdin closes immediately upon receiving a signal (like Ctrl+C), causing a fatal EOF before the application can gracefully handle the signal. lifecycle switches to CONIN$, which keeps the handle open, allowing the SignalContext to process the event.io.Reader (if it identifies as a terminal) to the safe platform-specific reader.SystemDiagram synthesizes Signal and Worker states into a single Mermaid visualization.lifecycle.Do(ctx, fn) shields atomic operations from cancellation and returns any error from the protected function.SignalContext.Reason() to differentiate between “Manual Stop”, “Interrupt”, or “Timeout”.Start, Stop, Wait contract for Processes, Goroutines, and Containers.Supervisor manages hierarchical worker clusters with restart policies (OneForOne, OneForAll).LIFECYCLE_RESUME_ID, LIFECYCLE_PREV_EXIT) to pass context across restarts.Run: One-line main entry 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.The Control Plane provides event-driven orchestration for modern Go applications:
Signals into Events (Webhook, FileWatch, HealthCheck).DebounceHandler and route events to idiomatic Go channels via events.Notify(ch).lifecycle.Go(ctx, fn) for non-leaking goroutines.Reload, Suspend, Scale alongside Shutdown.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
task := lifecycle.Go(ctx, func(ctx context.Context) error {
// This goroutine is automatically tracked and waited for on shutdown
return doWork(ctx)
}, lifecycle.WithErrorHandler(func(err error) {
fmt.Printf("Task failed: %v\n", err)
}))
// Optional: Wait for a specific task manually
// err := task.Wait()
// 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:
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.If you are developing lifecycle and procio simultaneously (e.g. adding features to procio that lifecycle needs), you can use the provided Makefile helpers to toggle a local go.work file:
# Enable local development (uses ../procio by default)
make work-on
# Enable local development with custom path
make work-on WORK_PATH=../../my-fork/procio
# Disable local development (return to published module)
make work-off