A Go-native library for controlling process supervisors including runit, s6, and other daemontools-compatible systems, with an adapter for systemd on Linux.
- Native daemontools protocol: Direct binary control via
supervise/controlandsupervise/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
runsvdirtrees for testing - Multi-supervisor support:
runit,s6,daemontools, andsystemd
go get github.com/axondata/go-svcmgrOptional build tags:
fsnotify- Enable file watching (recommended)devtree_cmd- Enable dev tree helpers for spawning runsvdirsv_fallback- Enable text-based status fallback (testing only)
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()
}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)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()This library works with any daemontools-compatible supervision system, including:
- runit - Full support for all operations
- daemontools - Compatible except for
Once()andQuit()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#ServiceBuilderS6While 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 processKey features:
- Generates native systemd unit files from
ServiceBuilderconfigurations - 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
| 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.
| 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 |
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)
The library provides typed errors. See OpError and the error variables in the API documentation.
Run the standard unit tests:
go test ./...The library includes integration tests for different supervision systems. Each requires the respective tools to be installed.
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 TestIntegrationSingleServiceTests for daemontools require svscan and supervise to be installed:
# Run daemontools integration tests
go test -tags=integration_daemontools -v ./...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
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)
See the examples/ directory for complete examples:
examples/basic/- Simple service controlexamples/watch/- Real-time status monitoringexamples/manager/- Bulk service operationsexamples/compat/- Using with daemontools and s6examples/devtree/- Development environment setup
- Go 1.21+
runit,s6, or anydaemontools-compatible process supervisor.- Linux or macOS
Apache 2.0 - See LICENSE file for details.