Slim

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 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 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 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 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 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 id1

Notify - 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
  • 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 quitproc.SetTimeToForceQuit(10 * time.Second)// Set to 0 for immediate force quitproc.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 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 = myCustomWriter

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

3. 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 terminate

Note: 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 context

Signal 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 signal

Graceful Shutdown

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

Command 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 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)    })}
Navigate Select esc Close search