proc - Process Management Library
proc is a process management utility library providing process information retrieval, signal handling, graceful shutdown, and command execution. Supports cross-platform signal handling (Unix/Windows) and process lifecycle management. Minimum supported Go version: 1.21.0.
Installation
go get -u go-slim.dev/procQuick Start
package mainimport ( "syscall" "go-slim.dev/proc")func main() { // Get process information pid := proc.Pid() name := proc.Name() workdir := proc.WorkDir() // Register signal handler proc.On(syscall.SIGTERM, func() { println("Received termination signal, preparing to exit...") cleanup() }) // Wait for specific signal proc.Wait(syscall.SIGUSR1)}Core Features
1. Process Information
Get basic information about the current process:
import "go-slim.dev/proc"// Get process IDpid := proc.Pid()fmt.Printf("Current process ID: %d\n", pid)// Get process name (command name)name := proc.Name()fmt.Printf("Process name: %s\n", name)// Get working directoryworkdir := proc.WorkDir()fmt.Printf("Working directory: %s\n", workdir)// Build path relative to working directoryconfigPath := proc.Path("config", "app.yaml")// Equivalent to: filepath.Join(workdir, "config", "app.yaml")// Build path using formattinglogPath := proc.Pathf("logs/app-%d.log", pid)// Equivalent to: filepath.Join(workdir, fmt.Sprintf("logs/app-%d.log", pid))2. Signal Handling
On - Register Signal Handler
Register a signal handler that executes every time the signal is received:
import ( "syscall" "go-slim.dev/proc")// Register SIGUSR1 handlerid := proc.On(syscall.SIGUSR1, func() { fmt.Println("Received SIGUSR1 signal") reloadConfig()})// Can register multiple handlersproc.On(syscall.SIGUSR1, func() { fmt.Println("Another handler")})// Returned ID can be used to cancel registrationproc.Cancel(id)Supported Signals (Unix):
SIGHUP(1): Hangup signal, typically used to reload configurationSIGINT(2): Interrupt signal (Ctrl+C)SIGQUIT(3): Quit signal (Ctrl+)SIGTERM(15): Terminate signal (default kill signal)SIGUSR1(10): User-defined signal 1SIGUSR2(12): User-defined signal 2
Once - One-time Signal Handler
Register a handler that executes only once:
// Register one-time handlerproc.Once(syscall.SIGUSR1, func() { fmt.Println("This will execute only once") performOneTimeAction()})// Send signal multiple times, only first triggers handlersyscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // Executessyscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // Doesn't executeWait - Wait for Signal
Blocks current goroutine until specified signal is received:
import ( "syscall" "go-slim.dev/proc")func main() { fmt.Println("Waiting for SIGUSR1 signal...") proc.Wait(syscall.SIGUSR1) fmt.Println("Received SIGUSR1, continuing execution")}// Use in testsfunc TestSignalHandling(t *testing.T) { done := make(chan struct{}) go func() { proc.Wait(syscall.SIGUSR1) close(done) }() // Send signal syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // Wait for signal handling completion <-done}Cancel - Cancel Signal Handler
Remove registered signal handler:
// Register multiple handlersid1 := proc.On(syscall.SIGUSR1, handler1)id2 := proc.On(syscall.SIGUSR1, handler2)id3 := proc.On(syscall.SIGUSR2, handler3)// Cancel single handlerproc.Cancel(id1)// Cancel multiple handlersproc.Cancel(id2, id3)// Canceling non-existent ID is safeproc.Cancel(999) // Won't error// ID 0 is ignoredproc.Cancel(0, id1, 0) // Only cancels id1Notify - Manually Trigger Signal
Manually send signal to all registered handlers:
import ( "syscall" "go-slim.dev/proc")// Register handlersproc.On(syscall.SIGUSR1, func() { fmt.Println("Handler 1")})proc.On(syscall.SIGUSR1, func() { fmt.Println("Handler 2")})// Manually trigger signalok := proc.Notify(syscall.SIGUSR1)// Output: Handler 1// Handler 2// ok = true (handlers were triggered)// Trigger signal with no registered handlersok = proc.Notify(syscall.SIGUSR2)// ok = false (no handlers)Features:
- All matching handlers execute concurrently
- Automatic panic recovery
Oncehandlers are automatically removed after execution
3. Graceful Shutdown
Shutdown - Graceful Process Shutdown
Trigger graceful shutdown process, notifying all SIGTERM handlers:
import ( "syscall" "time" "go-slim.dev/proc")func main() { // Register cleanup handlers proc.On(syscall.SIGTERM, func() { fmt.Println("Starting resource cleanup...") db.Close() cache.Close() fmt.Println("Cleanup complete") }) // Set wait time before force quit proc.SetTimeToForceQuit(10 * time.Second) // Start service... // Trigger graceful shutdown proc.Shutdown(syscall.SIGTERM)}SetTimeToForceQuit - Set Force Quit Time
Configure how long to wait after sending SIGTERM before forcefully killing the process:
import ( "time" "go-slim.dev/proc")// Wait 10 seconds before force quitproc.SetTimeToForceQuit(10 * time.Second)// Set to 0 for immediate force quitproc.SetTimeToForceQuit(0)Shutdown Process:
If SetTimeToForceQuit(duration) is set with duration > 0:
- Asynchronously send SIGTERM to all handlers
- Wait for
durationtime - If process still running, forcefully kill process
If SetTimeToForceQuit(0) or not set:
- Synchronously send SIGTERM to all handlers
- Immediately kill process
4. Command Execution
Exec - Execute External Command
Execute external commands with context and rich configuration options:
import ( "context" "time" "go-slim.dev/proc")// Basic executionerr := proc.Exec(context.Background(), proc.ExecOptions{ Command: "echo", Args: []string{"Hello, World!"},})// Execution with timeoutctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()err := proc.Exec(ctx, proc.ExecOptions{ Command: "long-running-task", Args: []string{"--verbose"}, Timeout: 5 * time.Second,})// Set working directoryerr := proc.Exec(context.Background(), proc.ExecOptions{ Command: "make", Args: []string{"build"}, WorkDir: "/path/to/project",})// Set environment variableserr := proc.Exec(context.Background(), proc.ExecOptions{ Command: "node", Args: []string{"app.js"}, Env: []string{ "NODE_ENV=production", "PORT=8080", },})// Capture outputvar stdout, stderr bytes.Buffererr := proc.Exec(context.Background(), proc.ExecOptions{ Command: "git", Args: []string{"status"}, Stdout: &stdout, Stderr: &stderr,})fmt.Println(stdout.String())// Provide inputstdin := strings.NewReader("input data\n")err := proc.Exec(context.Background(), proc.ExecOptions{ Command: "cat", Stdin: stdin,})// Configure kill delayerr := proc.Exec(context.Background(), proc.ExecOptions{ Command: "server", TTK: 2 * time.Second, // Wait 2 seconds after interrupt before kill})// Callback after starterr := proc.Exec(context.Background(), proc.ExecOptions{ Command: "daemon", OnStart: func(cmd *exec.Cmd) { fmt.Printf("Process started, PID: %d\n", cmd.Process.Pid) // Can write PID to file, etc. },})ExecOptions Configuration
type ExecOptions struct { // WorkDir specifies the command's working directory // If empty, defaults to current process's working directory WorkDir string // Timeout specifies the maximum duration for command execution // If > 0, creates a timeout context Timeout time.Duration // Env specifies additional environment variables to pass to command // These are appended to current process's environment variables Env []string // Stdin specifies the command's standard input Stdin io.Reader // Stdout specifies the command's standard output Stdout io.Writer // Stderr specifies the command's standard error output Stderr io.Writer // Command specifies the command to execute Command string // Args specifies command arguments Args []string // TTK (Time To Kill) specifies the delay when canceling command, // between sending interrupt signal and sending kill signal TTK time.Duration // OnStart is a callback function called after command starts OnStart func(cmd *exec.Cmd)}5. Process Context
Get global process context:
import "go-slim.dev/proc"// Get process contextctx := proc.Context()// Use elsewhereselect {case <-ctx.Done(): fmt.Println("Process context canceled")case <-time.After(5 * time.Second): fmt.Println("Operation complete")}6. Debug Logging
Control debug log output:
import ( "io" "os" "go-slim.dev/proc")// Default outputs to os.Stdout// Can set to other output// Output to filelogFile, _ := os.OpenFile("proc.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)proc.Logger = logFile// Disable debug loggingproc.Logger = io.Discard// Custom Writerproc.Logger = myCustomWriterUse Cases
1. Web Service Graceful Shutdown
package mainimport ( "context" "net/http" "syscall" "time" "go-slim.dev/proc" "go-slim.dev/slim")func main() { s := slim.New() // Configure routes... // Set graceful shutdown wait time proc.SetTimeToForceQuit(10 * time.Second) // Register shutdown handler proc.On(syscall.SIGTERM, func() { fmt.Println("Received shutdown signal, preparing to stop server...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Shutdown(ctx); err != nil { fmt.Printf("Server shutdown error: %v\n", err) } else { fmt.Println("Server gracefully shut down") } }) // Start server fmt.Printf("Server started on port 8080, PID: %d\n", proc.Pid()) if err := s.Start(":8080"); err != nil && err != http.ErrServerClosed { fmt.Printf("Server error: %v\n", err) }}2. Configuration Hot Reload
package mainimport ( "fmt" "syscall" "go-slim.dev/proc")var config *Configfunc main() { // Load initial configuration config = loadConfig() // Register SIGHUP signal handler (reload configuration) proc.On(syscall.SIGHUP, func() { fmt.Println("Received SIGHUP, reloading configuration...") newConfig := loadConfig() if newConfig != nil { config = newConfig fmt.Println("Configuration reloaded successfully") } else { fmt.Println("Configuration reload failed, keeping old configuration") } }) // Start application... runApp()}func loadConfig() *Config { // Load configuration from file configPath := proc.Path("config", "app.yaml") // ... return cfg}// Reload configuration by:// kill -HUP <pid>// or// pkill -HUP myapp3. Database Connection Pool Management
import ( "database/sql" "syscall" "go-slim.dev/proc")var db *sql.DBfunc initDatabase() { var err error db, err = sql.Open("postgres", "connection-string") if err != nil { panic(err) } // Register cleanup handler proc.On(syscall.SIGTERM, func() { fmt.Println("Closing database connection...") if err := db.Close(); err != nil { fmt.Printf("Database close error: %v\n", err) } else { fmt.Println("Database connection closed") } })}4. Background Task Management
import ( "context" "syscall" "time" "go-slim.dev/proc")func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Register cancel handler proc.On(syscall.SIGTERM, func() { fmt.Println("Canceling all background tasks...") cancel() }) // Start background tasks go backgroundWorker(ctx, "worker-1") go backgroundWorker(ctx, "worker-2") go backgroundWorker(ctx, "worker-3") // Wait for termination signal proc.Wait(syscall.SIGTERM) fmt.Println("Main program exiting")}func backgroundWorker(ctx context.Context, name string) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): fmt.Printf("%s: Received cancel signal, exiting\n", name) return case <-ticker.C: fmt.Printf("%s: Executing task\n", name) // Perform actual work... } }}5. Executing Build Scripts
import ( "bytes" "context" "fmt" "time" "go-slim.dev/proc")func buildProject(projectPath string) error { var stdout, stderr bytes.Buffer err := proc.Exec(context.Background(), proc.ExecOptions{ Command: "make", Args: []string{"build"}, WorkDir: projectPath, Stdout: &stdout, Stderr: &stderr, Timeout: 5 * time.Minute, Env: []string{ "CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64", }, OnStart: func(cmd *exec.Cmd) { fmt.Printf("Build started, PID: %d\n", cmd.Process.Pid) }, }) if err != nil { fmt.Println("Build error:") fmt.Println(stderr.String()) return err } fmt.Println("Build output:") fmt.Println(stdout.String()) return nil}6. Testing Tools
import ( "syscall" "testing" "time" "go-slim.dev/proc")func TestSignalHandler(t *testing.T) { executed := false // Register handler id := proc.Once(syscall.SIGUSR1, func() { executed = true }) defer proc.Cancel(id) // Send signal syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // Wait for handler execution time.Sleep(100 * time.Millisecond) if !executed { t.Fatal("Signal handler not executed") }}func TestWait(t *testing.T) { done := make(chan struct{}) go func() { proc.Wait(syscall.SIGUSR1) close(done) }() // Send signal syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // Wait for completion select { case <-done: // Success case <-time.After(1 * time.Second): t.Fatal("Wait didn't complete within timeout") }}7. Daemon Process
package mainimport ( "fmt" "syscall" "time" "go-slim.dev/proc")func main() { // Create PID file pidFile := proc.Path("app.pid") writePidFile(pidFile, proc.Pid()) // Register cleanup handler proc.On(syscall.SIGTERM, func() { fmt.Println("Cleaning up PID file...") os.Remove(pidFile) }) // Set graceful shutdown time proc.SetTimeToForceQuit(10 * time.Second) // Daemon logic runDaemon()}func writePidFile(path string, pid int) { content := fmt.Sprintf("%d\n", pid) os.WriteFile(path, []byte(content), 0644)}Cross-Platform Support
The proc package supports both Unix and Windows platforms, but signal handling differs:
Unix (Linux, macOS, BSD)
Supports full POSIX signals:
// All standard signals are supportedsyscall.SIGHUP // Hangupsyscall.SIGINT // Interrupt (Ctrl+C)syscall.SIGQUIT // Quit (Ctrl+\)syscall.SIGTERM // Terminatesyscall.SIGUSR1 // User signal 1syscall.SIGUSR2 // User signal 2// ... etc.Windows
Windows only supports limited signals:
syscall.SIGINT // Ctrl+Csyscall.SIGTERM // Terminate (simulated)syscall.SIGKILL // Force terminateNote: On Windows, some Unix signals are mapped or ignored.
API Reference
Process Information
func Pid() int // Get process IDfunc Name() string // Get process namefunc WorkDir() string // Get working directoryfunc Path(components ...string) string // Build pathfunc Pathf(format string, args ...any) string // Format pathfunc Context() context.Context // Get process contextSignal Handling
func On(sig os.Signal, fn func()) uint32 // Register signal handlerfunc Once(sig os.Signal, fn func()) uint32 // Register one-time handlerfunc Cancel(ids ...uint32) // Cancel handlerfunc Wait(sig os.Signal) // Wait for signalfunc Notify(sig os.Signal) bool // Manually trigger signalGraceful Shutdown
func SetTimeToForceQuit(duration time.Duration) // Set force quit timefunc Shutdown(sig syscall.Signal) error // Graceful shutdownCommand Execution
func Exec(ctx context.Context, opts ExecOptions) errortype ExecOptions struct { WorkDir string Timeout time.Duration Env []string Stdin io.Reader Stdout io.Writer Stderr io.Writer Command string Args []string TTK time.Duration OnStart func(cmd *exec.Cmd)}Debug Logging
var Logger io.Writer // Debug log outputNotes
-
Signal Handling is Global:
- Signal handlers affect the entire process once registered
- Multiple goroutines can register for the same signal
- All handlers execute concurrently
-
Auto-Handled Signals:
SIGHUP,SIGINT,SIGQUIT,SIGTERMautomatically trigger graceful shutdown- These signals first notify all SIGTERM handlers, then exit process
- To customize behavior for these signals, register before
procinitialization
-
Handler Execution Order:
- Multiple handlers for same signal execute concurrently
- Execution order is not guaranteed
Oncehandlers are automatically removed after execution
-
Panic Recovery:
- Panics in signal handlers are automatically recovered
- Panic information is output to debug log
- Won’t crash entire process
-
Wait Function Implementation:
- Internally uses
Onceand channel - Uses
close(chan)instead of channel send to avoid goroutine leaks - Multiple goroutines can safely call Wait
- Internally uses
-
Command Execution Timeout:
- Both
Timeoutand context timeout take effect - Timeout first sends interrupt signal, waits TTK then sends kill signal
- Default TTK is 0, kills immediately
- Both
-
Process Group Management:
- Unix systems set process group to prevent zombie processes
- Windows uses different process management mechanism
- See
SetSysProcAttributeplatform-specific implementations
-
Working Directory:
- Automatically gets current working directory on initialization
- Will panic if retrieval fails
- Use
Pathfamily functions to build relative paths
Common Patterns
Multi-Resource Cleanup
func main() { // Initialize resources db := initDB() cache := initCache() queue := initQueue() // Register cleanup handlers proc.On(syscall.SIGTERM, func() { fmt.Println("Cleaning up database...") db.Close() }) proc.On(syscall.SIGTERM, func() { fmt.Println("Cleaning up cache...") cache.Close() }) proc.On(syscall.SIGTERM, func() { fmt.Println("Cleaning up queue...") queue.Close() }) // Run application...}Timeout Control
func runWithTimeout(cmd string, args []string, timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return proc.Exec(ctx, proc.ExecOptions{ Command: cmd, Args: args, Timeout: timeout, // Double insurance })}Signal Forwarding
func main() { // Capture all signals and forward to child process childPid := startChildProcess() proc.On(syscall.SIGUSR1, func() { syscall.Kill(childPid, syscall.SIGUSR1) }) proc.On(syscall.SIGUSR2, func() { syscall.Kill(childPid, syscall.SIGUSR2) })}