Skip to main content

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/proc

Quick Start

package main

import (
"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 ID
pid := 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 directory
workdir := proc.WorkDir()
fmt.Printf("Working directory: %s\n", workdir)

// Build path relative to working directory
configPath := proc.Path("config", "app.yaml")
// Equivalent to: filepath.Join(workdir, "config", "app.yaml")

// Build path using formatting
logPath := 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 handler
id := proc.On(syscall.SIGUSR1, func() {
fmt.Println("Received SIGUSR1 signal")
reloadConfig()
})

// Can register multiple handlers
proc.On(syscall.SIGUSR1, func() {
fmt.Println("Another handler")
})

// Returned ID can be used to cancel registration
proc.Cancel(id)

Supported Signals (Unix):

  • SIGHUP (1): Hangup signal, typically used to reload configuration
  • SIGINT (2): Interrupt signal (Ctrl+C)
  • SIGQUIT (3): Quit signal (Ctrl+)
  • SIGTERM (15): Terminate signal (default kill signal)
  • SIGUSR1 (10): User-defined signal 1
  • SIGUSR2 (12): User-defined signal 2

Once - One-time Signal Handler

Register a handler that executes only once:

// Register one-time handler
proc.Once(syscall.SIGUSR1, func() {
fmt.Println("This will execute only once")
performOneTimeAction()
})

// Send signal multiple times, only first triggers handler
syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // Executes
syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // Doesn't execute

Wait - 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 tests
func 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 handlers
id1 := proc.On(syscall.SIGUSR1, handler1)
id2 := proc.On(syscall.SIGUSR1, handler2)
id3 := proc.On(syscall.SIGUSR2, handler3)

// Cancel single handler
proc.Cancel(id1)

// Cancel multiple handlers
proc.Cancel(id2, id3)

// Canceling non-existent ID is safe
proc.Cancel(999) // Won't error

// ID 0 is ignored
proc.Cancel(0, id1, 0) // Only cancels id1

Notify - Manually Trigger Signal

Manually send signal to all registered handlers:

import (
"syscall"
"go-slim.dev/proc"
)

// Register handlers
proc.On(syscall.SIGUSR1, func() {
fmt.Println("Handler 1")
})

proc.On(syscall.SIGUSR1, func() {
fmt.Println("Handler 2")
})

// Manually trigger signal
ok := proc.Notify(syscall.SIGUSR1)
// Output: Handler 1
// Handler 2
// ok = true (handlers were triggered)

// Trigger signal with no registered handlers
ok = proc.Notify(syscall.SIGUSR2)
// ok = false (no handlers)

Features:

  • All matching handlers execute concurrently
  • Automatic panic recovery
  • Once handlers 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 quit
proc.SetTimeToForceQuit(10 * time.Second)

// Set to 0 for immediate force quit
proc.SetTimeToForceQuit(0)

Shutdown Process:

If SetTimeToForceQuit(duration) is set with duration > 0:

  1. Asynchronously send SIGTERM to all handlers
  2. Wait for duration time
  3. If process still running, forcefully kill process

If SetTimeToForceQuit(0) or not set:

  1. Synchronously send SIGTERM to all handlers
  2. 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 execution
err := proc.Exec(context.Background(), proc.ExecOptions{
Command: "echo",
Args: []string{"Hello, World!"},
})

// Execution with timeout
ctx, 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 directory
err := proc.Exec(context.Background(), proc.ExecOptions{
Command: "make",
Args: []string{"build"},
WorkDir: "/path/to/project",
})

// Set environment variables
err := proc.Exec(context.Background(), proc.ExecOptions{
Command: "node",
Args: []string{"app.js"},
Env: []string{
"NODE_ENV=production",
"PORT=8080",
},
})

// Capture output
var stdout, stderr bytes.Buffer
err := proc.Exec(context.Background(), proc.ExecOptions{
Command: "git",
Args: []string{"status"},
Stdout: &stdout,
Stderr: &stderr,
})
fmt.Println(stdout.String())

// Provide input
stdin := strings.NewReader("input data\n")
err := proc.Exec(context.Background(), proc.ExecOptions{
Command: "cat",
Stdin: stdin,
})

// Configure kill delay
err := proc.Exec(context.Background(), proc.ExecOptions{
Command: "server",
TTK: 2 * time.Second, // Wait 2 seconds after interrupt before kill
})

// Callback after start
err := 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 context
ctx := proc.Context()

// Use elsewhere
select {
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 file
logFile, _ := os.OpenFile("proc.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
proc.Logger = logFile

// Disable debug logging
proc.Logger = io.Discard

// Custom Writer
proc.Logger = myCustomWriter

Use Cases

1. Web Service Graceful Shutdown

package main

import (
"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 main

import (
"fmt"
"syscall"
"go-slim.dev/proc"
)

var config *Config

func 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 myapp

3. Database Connection Pool Management

import (
"database/sql"
"syscall"
"go-slim.dev/proc"
)

var db *sql.DB

func 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 main

import (
"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 supported
syscall.SIGHUP // Hangup
syscall.SIGINT // Interrupt (Ctrl+C)
syscall.SIGQUIT // Quit (Ctrl+\)
syscall.SIGTERM // Terminate
syscall.SIGUSR1 // User signal 1
syscall.SIGUSR2 // User signal 2
// ... etc.

Windows

Windows only supports limited signals:

syscall.SIGINT    // Ctrl+C
syscall.SIGTERM // Terminate (simulated)
syscall.SIGKILL // Force terminate

Note: On Windows, some Unix signals are mapped or ignored.

API Reference

Process Information

func Pid() int                               // Get process ID
func Name() string // Get process name
func WorkDir() string // Get working directory
func Path(components ...string) string // Build path
func Pathf(format string, args ...any) string // Format path
func Context() context.Context // Get process context

Signal Handling

func On(sig os.Signal, fn func()) uint32    // Register signal handler
func Once(sig os.Signal, fn func()) uint32 // Register one-time handler
func Cancel(ids ...uint32) // Cancel handler
func Wait(sig os.Signal) // Wait for signal
func Notify(sig os.Signal) bool // Manually trigger signal

Graceful Shutdown

func SetTimeToForceQuit(duration time.Duration)  // Set force quit time
func Shutdown(sig syscall.Signal) error // Graceful shutdown

Command Execution

func Exec(ctx context.Context, opts ExecOptions) error

type 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 output

Notes

  1. Signal Handling is Global:

    • Signal handlers affect the entire process once registered
    • Multiple goroutines can register for the same signal
    • All handlers execute concurrently
  2. Auto-Handled Signals:

    • SIGHUP, SIGINT, SIGQUIT, SIGTERM automatically trigger graceful shutdown
    • These signals first notify all SIGTERM handlers, then exit process
    • To customize behavior for these signals, register before proc initialization
  3. Handler Execution Order:

    • Multiple handlers for same signal execute concurrently
    • Execution order is not guaranteed
    • Once handlers are automatically removed after execution
  4. Panic Recovery:

    • Panics in signal handlers are automatically recovered
    • Panic information is output to debug log
    • Won't crash entire process
  5. Wait Function Implementation:

    • Internally uses Once and channel
    • Uses close(chan) instead of channel send to avoid goroutine leaks
    • Multiple goroutines can safely call Wait
  6. Command Execution Timeout:

    • Both Timeout and context timeout take effect
    • Timeout first sends interrupt signal, waits TTK then sends kill signal
    • Default TTK is 0, kills immediately
  7. Process Group Management:

    • Unix systems set process group to prevent zombie processes
    • Windows uses different process management mechanism
    • See SetSysProcAttribute platform-specific implementations
  8. Working Directory:

    • Automatically gets current working directory on initialization
    • Will panic if retrieval fails
    • Use Path family 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)
})
}