proc

package module
v0.0.0-...-054c7ea Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2025 License: MIT Imports: 15 Imported by: 0

README

proc — Process utilities for Go

CI

English | 简体中文

Small, focused helpers for working with the current process, signals, and running child processes.

Features

  • Process info: Get process metadata with Pid(), Name(), WorkDir(), Path(...), Pathf(...), Context()
  • Signals: Register listeners with On()/Once(), remove via Cancel(), trigger via Notify()
  • Shutdown: Graceful shutdown with Shutdown(syscall.Signal) and configurable force-kill delay (test-friendly via stub)
  • Exec: Run external commands with timeout, environment variables, working directory, and lifecycle callbacks
  • Logging: Control debug output via the Logger variable

Module path: go-slim.dev/proc

Install

go get go-slim.dev/proc

Go version: 1.24 (per proc/go.mod).

Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "syscall"
    "time"

    proc "go-slim.dev/proc"
)

func main() {
    // Get process information
    fmt.Println("pid:", proc.Pid(), "name:", proc.Name(), "wd:", proc.WorkDir())

    // Build paths relative to working directory
    configPath := proc.Path("config", "app.yaml")
    fmt.Println("config path:", configPath)

    // Listen for SIGTERM once
    proc.Once(syscall.SIGTERM, func() {
        fmt.Println("terminating...")
    })

    // Run a command with timeout and error handling
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    err := proc.Exec(ctx, proc.ExecOptions{
        Command: "sh",
        Args:    []string{"-c", "echo ok"},
    })
    if err != nil {
        log.Fatalf("command failed: %v", err)
    }
}

Signals

The signal API allows you to register custom handlers for OS signals:

  • On(sig, fn) uint32 - Registers a listener that fires every time the signal is received. Returns a listener ID.
  • Once(sig, fn) uint32 - Registers a one-shot listener that automatically removes itself after execution. Returns a listener ID.
  • Cancel(id...) - Removes listeners by their IDs. Safe to call with invalid or already-removed IDs.
  • Notify(sig) bool - Manually triggers callbacks for the given signal. Returns false if no listeners were found.

Automatic shutdown: The package installs a signal listener on init for common signals (SIGHUP, SIGINT, SIGQUIT, SIGTERM) that triggers graceful shutdown.

Example: Custom signal handling
import (
    "fmt"
    "syscall"
    proc "go-slim.dev/proc"
)

// Register a repeating handler
id := proc.On(syscall.SIGUSR1, func() {
    fmt.Println("Received SIGUSR1")
})

// Register a one-time handler
proc.Once(syscall.SIGUSR2, func() {
    fmt.Println("This will only run once")
})

// Later: cancel the repeating handler
proc.Cancel(id)

Shutdown

The shutdown system provides graceful termination with configurable force-kill behavior.

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

// Optional: set a delay before force-kill
proc.SetTimeToForceQuit(2 * time.Second)

// Trigger graceful shutdown sequence
err := proc.Shutdown(syscall.SIGTERM)
if err != nil {
    // handle error
}

Behavior:

  • If SetTimeToForceQuit() is called with a duration > 0:
    1. Calls Notify(SIGTERM) in a goroutine to trigger registered listeners
    2. Waits for the specified duration
    3. Force-kills the process if still alive
  • If delay is 0 or not set:
    1. Calls Notify(SIGTERM) synchronously
    2. Immediately kills the process

Testing: The Shutdown function uses an internal killFn variable (defaults to OS kill) which can be stubbed for testing graceful shutdown behavior without actually killing the process.

Exec

Execute external commands with fine-grained control over timeout, environment, and lifecycle.

import (
    "context"
    "os/exec"
    "time"
    proc "go-slim.dev/proc"
)

ctx := context.Background()
err := proc.Exec(ctx, proc.ExecOptions{
    WorkDir: "/tmp",                           // Working directory (defaults to current)
    Timeout: 3 * time.Second,                  // Maximum execution time
    Env:     []string{"FOO=BAR"},              // Additional environment variables
    Command: "sh",                             // Command to execute
    Args:    []string{"-c", "echo ok"},        // Command arguments
    TTK:     500 * time.Millisecond,           // Time To Kill: grace period between interrupt and force kill
    OnStart: func(cmd *exec.Cmd) {
        fmt.Println("Process started:", cmd.Process.Pid)
    },
})
if err != nil {
    // Handle timeout, cancellation, or execution errors
}
ExecOptions Fields
  • WorkDir: Working directory for the command (defaults to current process working directory)
  • Timeout: If > 0, creates a timeout context automatically
  • Env: Additional environment variables (appended to current process environment)
  • Stdin, Stdout, Stderr: Custom I/O streams (defaults to os.Stdout/os.Stderr)
  • Command: The executable to run
  • Args: Command-line arguments
  • TTK (Time To Kill): Delay between sending interrupt signal and kill signal during cancellation
  • OnStart: Callback invoked after the command starts successfully
Platform-specific behavior
  • Unix/Linux: Sets Setpgid=true to create a new process group, preventing zombie processes when child processes spawn their own children
  • Windows: No special process attributes are set

Logging

Control debug output by setting the Logger variable:

import (
    "io"
    "os"
    proc "go-slim.dev/proc"
)

// Disable logging
proc.Logger = io.Discard

// Log to a file
logFile, _ := os.Create("proc.log")
proc.Logger = logFile

// Default: logs to os.Stdout

Use Cases

Graceful server shutdown
proc.SetTimeToForceQuit(10 * time.Second)
proc.Once(syscall.SIGTERM, func() {
    // Close database connections
    db.Close()
    // Stop accepting new requests
    server.Shutdown(context.Background())
})
Running builds with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

err := proc.Exec(ctx, proc.ExecOptions{
    Command: "go",
    Args:    []string{"build", "./..."},
    WorkDir: proc.WorkDir(),
    TTK:     10 * time.Second,
})
Hot reload on signal
proc.On(syscall.SIGHUP, func() {
    // Reload configuration
    config.Reload()
})

Development

Run local CI-like checks:

make ci

Run benchmarks:

make bench
# or save to artifacts/bench.txt
make bench-save

License

MIT

Documentation

Overview

Package proc provides process management utilities including process information, signal handling, graceful shutdown, and command execution.

Index

Constants

This section is empty.

Variables

View Source
var Logger io.Writer

Logger is the output destination for debug messages. By default, it's set to os.Stdout in the init function. Set to io.Discard to disable debug logging.

Functions

func Cancel

func Cancel(ids ...uint32)

Cancel removes the signal listeners with the specified IDs. It's safe to pass IDs that don't exist or have already been removed. Zero IDs are ignored.

func Context

func Context() context.Context

Context return the process context.

func Exec

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

Exec executes a command with the given context and options. It supports timeout, graceful shutdown with configurable kill delay, and proper process group management to prevent zombie processes.

References: - https://github.com/gouravkrosx/golang-cmd-exit-demo?ref=hackernoon.com - https://keploy.io/blog/technology/managing-go-processes

func Name

func Name() string

Name returns the process name, same as the command name.

func Notify

func Notify(sig os.Signal) bool

Notify dispatches a signal to all registered listeners for that signal. It executes all matching listeners concurrently in separate goroutines, with panic recovery. Listeners registered with Once are automatically removed after execution.

Returns true if at least one listener was notified, false if no listeners were registered for the signal or if the signal is invalid.

func On

func On(sig os.Signal, fn func()) uint32

On registers a signal handler that will be called every time the specified signal is received. Returns a unique ID that can be used with Cancel to remove the listener.

func Once

func Once(sig os.Signal, fn func()) uint32

Once registers a signal handler that will be called at most once when the specified signal is received. After execution, the listener is automatically removed. Returns a unique ID that can be used with Cancel to remove the listener before it executes.

func Path

func Path(components ...string) string

Path returns a path with components of the working directory

func Pathf

func Pathf(format string, args ...any) string

Pathf returns a path with format of the working directory.

func Pid

func Pid() int

Pid returns pid of the current process.

func SetSysProcAttribute

func SetSysProcAttribute(cmd *exec.Cmd)

SetSysProcAttribute sets the system-specific process attributes for Unix-like systems. It configures the command to use Setpgid to create a new process group, which ensures that child processes can be properly reaped when the parent is killed, preventing defunct (zombie) processes.

This is particularly important when SubProcessA spawns SubProcessB and SubProcessA gets killed by context timeout - Setpgid ensures that SubProcessB can still be properly cleaned up.

func SetTimeToForceQuit

func SetTimeToForceQuit(duration time.Duration)

SetTimeToForceQuit sets the duration to wait before forcefully killing the process during shutdown. If set to 0, the process will be killed immediately without attempting graceful shutdown.

func Shutdown

func Shutdown(sig syscall.Signal) error

Shutdown performs a graceful shutdown by notifying all registered signal listeners and optionally waiting for a configured delay before force killing.

If delayTimeBeforeForceQuit > 0, it will:

  1. Send SIGTERM to all registered listeners in a goroutine
  2. Wait for delayTimeBeforeForceQuit duration
  3. Force kill the process if still alive

If delayTimeBeforeForceQuit == 0, it will:

  1. Send SIGTERM to all registered listeners synchronously
  2. Immediately kill the process

func Wait

func Wait(sig os.Signal)

Wait blocks until the specified signal is received. It registers a one-time signal handler and blocks the current goroutine until the signal arrives. This is useful for waiting for specific signals in a synchronous manner.

Example:

// Wait for SIGUSR1 signal
proc.Wait(syscall.SIGUSR1)
fmt.Println("Received SIGUSR1")

func WorkDir

func WorkDir() string

WorkDir returns working directory of the current process.

Types

type ExecOptions

type ExecOptions struct {
	// WorkDir specifies the working directory for the command.
	// If empty, defaults to the current process's working directory.
	WorkDir string
	// Timeout specifies the maximum duration for command execution.
	// If > 0, a timeout context will be created.
	Timeout time.Duration
	// Env specifies additional environment variables to pass to the command.
	// These are appended to the current process's environment.
	Env []string
	// Stdin specifies the standard input for the command.
	Stdin io.Reader
	// Stdout specifies the standard output for the command.
	Stdout io.Writer
	// Stderr specifies the standard error output for the command.
	Stderr io.Writer
	// Command specifies the command to execute.
	Command string
	// Args specifies the command arguments.
	Args []string
	// TTK (Time To Kill) specifies the delay between sending interrupt signal
	// and kill signal during command cancellation.
	TTK time.Duration
	// OnStart is a callback function invoked after the command starts.
	OnStart func(cmd *exec.Cmd)
}

ExecOptions configures command execution parameters.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL