Skip to main content
โšก Calmops

Process Management and Subprocess Control in Go

Process Management and Subprocess Control in Go

Introduction

Managing subprocesses is essential for CLI tools that need to execute external programs. Go provides powerful packages for process management, I/O handling, and signal management. This guide covers comprehensive subprocess control.

Proper subprocess management ensures your applications can reliably execute external programs and handle their output and errors.

Basic Process Execution

Simple Command Execution

package main

import (
	"fmt"
	"log"
	"os/exec"
)

// ExecuteCommand executes a simple command
func ExecuteCommand(name string, args ...string) error {
	cmd := exec.Command(name, args...)
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("command failed: %w", err)
	}
	return nil
}

// ExecuteCommandWithOutput executes command and captures output
func ExecuteCommandWithOutput(name string, args ...string) (string, error) {
	cmd := exec.Command(name, args...)
	output, err := cmd.Output()
	if err != nil {
		return "", fmt.Errorf("command failed: %w", err)
	}
	return string(output), nil
}

// Example usage
func SimpleExample() {
	// Execute ls command
	if err := ExecuteCommand("ls", "-la"); err != nil {
		log.Fatal(err)
	}

	// Execute command with output
	output, err := ExecuteCommandWithOutput("echo", "Hello, World!")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(output)
}

Good: Proper Process Management Implementation

package main

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"os/signal"
	"syscall"
	"time"
)

// ProcessResult holds process execution result
type ProcessResult struct {
	ExitCode int
	Stdout   string
	Stderr   string
	Duration time.Duration
	Error    error
}

// ProcessManager manages subprocess execution
type ProcessManager struct {
	timeout time.Duration
}

// NewProcessManager creates a new process manager
func NewProcessManager(timeout time.Duration) *ProcessManager {
	return &ProcessManager{
		timeout: timeout,
	}
}

// Execute executes a command with timeout
func (pm *ProcessManager) Execute(ctx context.Context, name string, args ...string) *ProcessResult {
	result := &ProcessResult{}
	start := time.Now()

	// Create context with timeout
	if pm.timeout > 0 {
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, pm.timeout)
		defer cancel()
	}

	cmd := exec.CommandContext(ctx, name, args...)

	// Capture output
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	// Execute command
	err := cmd.Run()
	result.Duration = time.Since(start)
	result.Stdout = stdout.String()
	result.Stderr = stderr.String()

	if err != nil {
		result.Error = err
		if exitErr, ok := err.(*exec.ExitError); ok {
			result.ExitCode = exitErr.ExitCode()
		}
	}

	return result
}

// ExecuteWithStreaming executes command with streaming output
func (pm *ProcessManager) ExecuteWithStreaming(ctx context.Context, name string, args ...string) error {
	cmd := exec.CommandContext(ctx, name, args...)

	// Stream stdout and stderr
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Stdin = os.Stdin

	return cmd.Run()
}

// ExecuteWithCallback executes command and calls callback for each line
func (pm *ProcessManager) ExecuteWithCallback(ctx context.Context, name string, callback func(string), args ...string) error {
	cmd := exec.CommandContext(ctx, name, args...)

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return err
	}

	if err := cmd.Start(); err != nil {
		return err
	}

	// Read output line by line
	scanner := bufio.NewScanner(stdout)
	for scanner.Scan() {
		callback(scanner.Text())
	}

	return cmd.Wait()
}

// ExecuteParallel executes multiple commands in parallel
func (pm *ProcessManager) ExecuteParallel(ctx context.Context, commands []struct {
	Name string
	Args []string
}) []error {
	results := make([]error, len(commands))

	for i, cmd := range commands {
		go func(index int, name string, args []string) {
			result := pm.Execute(ctx, name, args...)
			results[index] = result.Error
		}(i, cmd.Name, cmd.Args)
	}

	// Wait for all to complete
	time.Sleep(time.Second)
	return results
}

// ExecuteWithSignalHandling executes command with signal handling
func (pm *ProcessManager) ExecuteWithSignalHandling(name string, args ...string) error {
	cmd := exec.Command(name, args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	// Handle signals
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	if err := cmd.Start(); err != nil {
		return err
	}

	// Wait for signal or command completion
	done := make(chan error, 1)
	go func() {
		done <- cmd.Wait()
	}()

	select {
	case sig := <-sigChan:
		// Forward signal to subprocess
		cmd.Process.Signal(sig)
		return <-done
	case err := <-done:
		return err
	}
}

Bad: Improper Process Management

package main

import (
	"os/exec"
)

// BAD: No error handling
func BadExecute(name string, args ...string) {
	cmd := exec.Command(name, args...)
	cmd.Run() // Ignoring error
}

// BAD: No timeout
func BadExecuteWithoutTimeout(name string, args ...string) {
	cmd := exec.Command(name, args...)
	cmd.Run() // Could hang indefinitely
}

// BAD: No output capture
func BadExecuteNoOutput(name string, args ...string) {
	cmd := exec.Command(name, args...)
	cmd.Run() // Output lost
}

Problems:

  • No error handling
  • No timeout management
  • No output capture
  • No signal handling

Advanced Process Control

Process Pooling

package main

import (
	"context"
	"fmt"
	"os/exec"
	"sync"
)

// ProcessPool manages a pool of processes
type ProcessPool struct {
	maxConcurrent int
	semaphore     chan struct{}
	wg            sync.WaitGroup
}

// NewProcessPool creates a new process pool
func NewProcessPool(maxConcurrent int) *ProcessPool {
	return &ProcessPool{
		maxConcurrent: maxConcurrent,
		semaphore:     make(chan struct{}, maxConcurrent),
	}
}

// Execute executes command with pool limits
func (pp *ProcessPool) Execute(ctx context.Context, name string, args ...string) error {
	pp.wg.Add(1)
	defer pp.wg.Done()

	// Acquire semaphore
	pp.semaphore <- struct{}{}
	defer func() { <-pp.semaphore }()

	cmd := exec.CommandContext(ctx, name, args...)
	return cmd.Run()
}

// Wait waits for all processes to complete
func (pp *ProcessPool) Wait() {
	pp.wg.Wait()
}

Process Monitoring

package main

import (
	"fmt"
	"os/exec"
	"time"
)

// ProcessMonitor monitors process execution
type ProcessMonitor struct {
	name      string
	args      []string
	maxRetries int
	retryDelay time.Duration
}

// NewProcessMonitor creates a new process monitor
func NewProcessMonitor(name string, args []string) *ProcessMonitor {
	return &ProcessMonitor{
		name:       name,
		args:       args,
		maxRetries: 3,
		retryDelay: time.Second,
	}
}

// ExecuteWithRetry executes command with retry logic
func (pm *ProcessMonitor) ExecuteWithRetry() error {
	var lastErr error

	for attempt := 0; attempt < pm.maxRetries; attempt++ {
		cmd := exec.Command(pm.name, pm.args...)
		if err := cmd.Run(); err == nil {
			return nil
		} else {
			lastErr = err
			if attempt < pm.maxRetries-1 {
				time.Sleep(pm.retryDelay)
			}
		}
	}

	return fmt.Errorf("command failed after %d attempts: %w", pm.maxRetries, lastErr)
}

Environment Variables

package main

import (
	"fmt"
	"os"
	"os/exec"
)

// ExecuteWithEnv executes command with custom environment
func ExecuteWithEnv(name string, env map[string]string, args ...string) error {
	cmd := exec.Command(name, args...)

	// Set environment variables
	cmd.Env = os.Environ()
	for key, value := range env {
		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
	}

	return cmd.Run()
}

// Example usage
func EnvExample() {
	env := map[string]string{
		"DEBUG": "true",
		"LOG_LEVEL": "info",
	}

	ExecuteWithEnv("myapp", env, "--config", "config.yaml")
}

Best Practices

1. Always Set Timeout

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, name, args...)

2. Capture Output

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

3. Handle Signals

signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

4. Check Exit Code

if exitErr, ok := err.(*exec.ExitError); ok {
	exitCode := exitErr.ExitCode()
}

Common Pitfalls

1. No Timeout

Always set timeouts to prevent hanging.

2. Ignoring Errors

Always check and handle errors.

3. No Output Capture

Capture output for debugging.

4. No Signal Handling

Handle signals for graceful shutdown.

Resources

Summary

Proper process management is essential for CLI tools. Key takeaways:

  • Always set timeouts for subprocess execution
  • Capture stdout and stderr
  • Handle errors properly
  • Implement signal handling
  • Use context for cancellation
  • Implement retry logic when appropriate
  • Monitor process execution

By mastering process management, you can build reliable CLI tools.

Comments