Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

go-svcmgr

Go Reference Go Report Card Coverage Status License: Apache 2.0 GitHub release

A Go-native library for controlling process supervisors including runit, s6, and other daemontools-compatible systems, with an adapter for systemd on Linux.

Features

  • Native daemontools protocol: Direct binary control via supervise/control and supervise/status
  • systemd adapter: Unified API for systemd services on Linux
  • Real-time monitoring: Status changes via fsnotify (no polling)
  • Concurrent operations: Worker pool management via Manager
  • Zero allocations: Optimized hot paths with stack-based operations
  • Cross-platform: Linux and macOS (systemd on Linux only)
  • Development mode: Unprivileged runsvdir trees for testing
  • Multi-supervisor support: runit, s6, daemontools, and systemd

Installation

go get github.com/axondata/go-svcmgr

Optional build tags:

  • fsnotify - Enable file watching (recommended)
  • devtree_cmd - Enable dev tree helpers for spawning runsvdir
  • sv_fallback - Enable text-based status fallback (testing only)

Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "sync"
    "time"

    "github.com/axondata/go-svcmgr"
)

func main() {
    // Create client for a service
    client, err := svcmgr.New("/etc/service/web")
    if err != nil {
        log.Fatal(err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    var wg sync.WaitGroup

    // Start watching in a goroutine
    wg.Add(1)
    go func() {
        defer wg.Done()

        events, stop, err := client.Watch(ctx)
        if err != nil {
            log.Printf("Watch error: %v", err)
            return
        }
        defer stop()

        for event := range events {
            if event.Err != nil {
                log.Printf("Event error: %v", event.Err)
                continue
            }
            log.Printf("State changed: %v (PID: %d)",
                event.Status.State, event.Status.PID)
        }
    }()

    // Control the service in another goroutine
    wg.Add(1)
    go func() {
        defer wg.Done()

        // Give watcher time to start
        time.Sleep(100 * time.Millisecond)

        // Stop the service
        log.Println("Stopping service...")
        if err := client.Down(ctx); err != nil {
            log.Printf("Down error: %v", err)
            return
        }

        time.Sleep(2 * time.Second)

        // Start the service
        log.Println("Starting service...")
        if err := client.Up(ctx); err != nil {
            log.Printf("Up error: %v", err)
            return
        }

        time.Sleep(2 * time.Second)

        // Get final status
        status, err := client.Status(ctx)
        if err != nil {
            log.Printf("Status error: %v", err)
            return
        }

        fmt.Printf("Final state: %v, PID: %d, Uptime: %s\n",
            status.State, status.PID, status.Uptime)

        // Cancel context to stop watcher
        cancel()
    }()

    wg.Wait()
}

API Reference

Client (Single Service)

// Create a client
client, err := svcmgr.New("/etc/service/myapp",
    svcmgr.WithDialTimeout(3*time.Second),
    svcmgr.WithMaxAttempts(5),
    svcmgr.WithBackoff(10*time.Millisecond, 1*time.Second),
)

// Control commands
client.Up(ctx)            // Start service (send 'u')
client.Down(ctx)          // Stop service (send 'd')
client.Once(ctx)          // Run once (send 'o')
client.Term(ctx)          // Send SIGTERM (send 't')
client.Kill(ctx)          // Send SIGKILL (send 'k')
client.HUP(ctx)           // Send SIGHUP (send 'h')
client.Interrupt(ctx)     // Send SIGINT (send 'i')
client.Alarm(ctx)         // Send SIGALRM (send 'a')
client.Quit(ctx)          // Send SIGQUIT (send 'q')
client.Pause(ctx)         // Send SIGSTOP (send 'p')
client.Cont(ctx)          // Send SIGCONT (send 'c')
client.ExitSupervise(ctx) // Exit supervise (send 'x')

// Get status
status, err := client.Status(ctx)

Status Structure

See Status and State types in the API documentation.

Manager (Multiple Services)

// Create manager with worker pool
mgr := svcmgr.NewManager(
    svcmgr.WithConcurrency(10),
    svcmgr.WithTimeout(5*time.Second),
)

// Bulk operations
services := []string{
    "/etc/service/web",
    "/etc/service/db",
    "/etc/service/cache",
}

// Start all services
err := mgr.Up(ctx, services...)

// Get all statuses
statuses, err := mgr.Status(ctx, services...)
for svc, status := range statuses {
    fmt.Printf("%s: %v (PID %d)\n", svc, status.State, status.PID)
}

// Stop all services
err = mgr.Down(ctx, services...)

DevTree (Development Mode)

Build with -tags devtree_cmd to enable:

// Create dev tree
tree, err := svcmgr.NewDevTree("/tmp/my-runit")
if err != nil {
    log.Fatal(err)
}

// Initialize directories
err = tree.Ensure()

// Start runsvdir
err = tree.EnsureRunsvdir()

// Enable a service
err = tree.EnableService("myapp")

// Disable a service
err = tree.DisableService("myapp")
builder := svcmgr.NewServiceBuilder("myapp", "/tmp/services")

builder.
    WithCmd([]string{"/usr/bin/myapp", "--port", "8080"}).
    WithCwd("/var/myapp").
    WithEnv("NODE_ENV", "production").
    WithChpst(func(c *svcmgr.ChpstBuilder) { // See https://pkg.go.dev/github.com/axondata/go-svcmgr#ChpstBuilder
        c.User = "myapp"
        c.LimitMem = 1024 * 1024 * 512  // 512MB
        c.LimitFiles = 1024
    }).
    WithSvlogd(func(s *svcmgr.SvlogdBuilder) { // See https://pkg.go.dev/github.com/axondata/go-svcmgr#SvlogdBuilder
        s.Size = 10000000  // 10MB per file
        s.Num = 10         // Keep 10 files
    })

err := builder.Build()

Compatibility with daemontools and s6

This library works with any daemontools-compatible supervision system, including:

  • runit - Full support for all operations
  • daemontools - Compatible except for Once() and Quit() operations
  • s6 - Full compatibility with all operations

The library provides factory functions for each system (see compatibility functions):

// For runit
config := svcmgr.ConfigRunit()
client, err := svcmgr.NewClientWithConfig("/etc/service/myapp", config)

// For daemontools
config := svcmgr.ConfigDaemontools()
client, err := svcmgr.NewClientWithConfig("/service/myapp", config)

// For s6
config := svcmgr.ConfigS6()
client, err := svcmgr.NewClientWithConfig("/run/service/myapp", config)

// Service builders for each system
runitBuilder := svcmgr.ServiceBuilderRunit("myapp", "/etc/service")        // See https://pkg.go.dev/github.com/axondata/go-svcmgr#ServiceBuilderRunit
dtBuilder := svcmgr.ServiceBuilderDaemontools("myapp", "/service")         // See https://pkg.go.dev/github.com/axondata/go-svcmgr#ServiceBuilderDaemontools
s6Builder := svcmgr.ServiceBuilderS6("myapp", "/run/service")              // See https://pkg.go.dev/github.com/axondata/go-svcmgr#ServiceBuilderS6

systemd Adapter (Linux only)

While systemd uses a different architecture than daemontools-family supervisors, this library provides an adapter that offers a consistent API:

// Create a service builder
builder := svcmgr.NewServiceBuilder("myapp", "")
builder.WithCmd([]string{"/usr/bin/myapp", "--config", "/etc/myapp.conf"})
builder.WithCwd("/var/lib/myapp")
builder.WithEnv("ENV_VAR", "value")
builder.WithChpst(func(c *svcmgr.ChpstConfig) {
    c.User = "myuser"
    c.LimitMem = 1024*1024*1024  // 1GB
})

// Generate and install systemd unit file
systemdBuilder := svcmgr.NewBuilderSystemd(builder)
if err := systemdBuilder.Build(); err != nil {
    log.Fatal(err)
}

// Control the service
client := svcmgr.NewClientSystemd("myapp")
if err := client.Start(context.Background()); err != nil {
    log.Fatal(err)
}

// Send signals
client.USR1(ctx)  // Send SIGUSR1 to main process
client.Term(ctx)  // Send SIGTERM to main process

Key features:

  • Generates native systemd unit files from ServiceBuilder configurations
  • Maps process limits and environment variables to systemd directives
  • Translates operations to appropriate systemctl commands
  • Sends signals directly to MainPID for precise control
  • Automatic sudo handling for non-root users

Differences between systems

Feature runit daemontools s6 systemd
Default path /etc/service /service /run/service /etc/systemd/system
Privilege tool chpst setuidgid s6-setuidgid Unit directives
Logger svlogd multilog s6-log journald
Scanner runsvdir svscan s6-svscan systemd (PID 1)
Control method Binary protocol Binary protocol Binary protocol D-Bus/systemctl
Once() support ✓ (via systemd-run)
Quit() support
USR1/USR2 support
Platform Unix-like Unix-like Unix-like Linux only

The daemontools family (runit, daemontools, s6) share a common binary protocol for supervise/control and supervise/status. The systemd adapter translates the same operations to systemctl commands and generates native unit files from shared service configurations.

Control Commands Reference

Method Byte Signal Description runit daemontools s6 systemd
Up() / Start() u - Start service (want up)
Once() o - Run service once
Down() / Stop() d - Stop service (want down)
Restart() - - Stop then start service
Term() t SIGTERM Graceful termination
Interrupt() i SIGINT Interrupt
HUP() h SIGHUP Reload configuration
Alarm() a SIGALRM Alarm signal
Quit() q SIGQUIT Quit with core dump
USR1() 1 SIGUSR1 User signal 1
USR2() 2 SIGUSR2 User signal 2
Kill() k SIGKILL Force kill
Pause() p SIGSTOP Pause process
Cont() c SIGCONT Continue process
ExitSupervise() x - Terminate supervise N/A

Status Binary Format

The 20-byte supervise/status record:

Bytes 0-7:   TAI64N seconds (big-endian uint64)
Bytes 8-11:  TAI64N nanoseconds (big-endian uint32)
Bytes 12-15: PID (big-endian uint32)
Byte 16:     Paused flag (non-zero = paused)
Byte 17:     Want flag ('u' = up, 'd' = down)
Byte 18:     Term flag (non-zero = TERM sent)
Byte 19:     Run flag (non-zero = normally up)

Error Handling

The library provides typed errors. See OpError and the error variables in the API documentation.

Testing

Unit Tests

Run the standard unit tests:

go test ./...

Integration Tests

The library includes integration tests for different supervision systems. Each requires the respective tools to be installed.

Runit Integration Tests

Tests for runit require runsv and runsvdir to be installed:

# Run all runit integration tests
go test -tags=integration -v ./...

# Or explicitly for runit
go test -tags=integration_runit -v ./...

# Run a specific integration test
go test -tags=integration -v -run TestIntegrationSingleService

Daemontools Integration Tests

Tests for daemontools require svscan and supervise to be installed:

# Run daemontools integration tests
go test -tags=integration_daemontools -v ./...

S6 Integration Tests

Tests for s6 require s6-svscan and s6-supervise to be installed:

# Run s6 integration tests
go test -tags=integration_s6 -v ./...

The runit integration tests cover:

  • Service lifecycle (start, stop, restart)
  • Signal handling (TERM, HUP, etc.)
  • Status monitoring and state transitions
  • Watch functionality with fsnotify
  • Services with different exit codes
  • ServiceBuilder generated services

Performance

Benchmarks on Apple M3 Pro (2025-09-08):

BenchmarkStatusDecode-12          32006547	 37.64 ns/op   0 B/op  0 allocs/op
BenchmarkStatusDecodeParallel-12  187062128	  9.825 ns/op  0 B/op  0 allocs/op
BenchmarkDecodeStatus-12          31235832	 37.89 ns/op   0 B/op  0 allocs/op
  • Status decode: ~38ns/op with zero allocations
  • Parallel decode: ~10ns/op when running concurrently
  • State/Op strings: <1ns/op with zero allocations
  • Control send: Sub-millisecond for local sockets
  • Watch events: Debounced at 25ms by default (configurable)

Examples

See the examples/ directory for complete examples:

  • examples/basic/ - Simple service control
  • examples/watch/ - Real-time status monitoring
  • examples/manager/ - Bulk service operations
  • examples/compat/ - Using with daemontools and s6
  • examples/devtree/ - Development environment setup

Requirements

License

Apache 2.0 - See LICENSE file for details.

About

Pure Go client library for runit service supervision - control, monitor, and manage runit services programmatically

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages