Help us shape the future of Go! Take the 2026 Go Developer Survey

daemon

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2026 License: MIT Imports: 23 Imported by: 0

README

daemon

CI Go Reference codecov Go Version

Cross-platform daemon lifecycle library for Go applications. Pure Go, zero CGO, zero dependencies.

Manages background daemon processes with HTTP health checks, PID file locking, signal handling, connection tracking, and idle auto-shutdown. Built for tools that need a persistent background server -- MCP servers, language servers, dev tools.

Features

  • Client mode -- Start/Stop/Restart/Status to manage a background daemon from any CLI
  • Server mode -- Serve() runs as a foreground daemon with automatic port selection, PID file, signal handling, and health endpoint
  • Proxy mode -- Proxy() bridges stdin/stdout to the daemon's HTTP endpoint for MCP/JSON-RPC protocols
  • EnsureRunning -- "start if needed" with race-safe PID locking (two agents calling simultaneously is safe)
  • run.Group -- actor concurrency pattern (oklog/run reimplemented, zero deps) for composing HTTP server + signal handler + idle timer
  • Connection tracking -- atomic connect/disconnect counting with idle auto-shutdown when all clients leave
  • Rich Model -- Daemon struct depends on interfaces (PIDStore, ProcessManager, HealthChecker), not concrete implementations. Testable by design
  • Cross-platform -- Windows (CREATE_NEW_PROCESS_GROUP), Linux/macOS (setsid) with platform-specific process management in internal/
  • Pure Go -- no CGO, no assembly, stdlib only

Installation

go get github.com/grpmsoft/daemon

Quick Start

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/grpmsoft/daemon"
)

func main() {
    cfg := daemon.Config{
        Name:    "myapp",
        DataDir: ".myapp",
    }

    d := daemon.New(cfg)

    // Start a background daemon.
    binary, _ := os.Executable()
    if err := d.Start(context.Background(), binary, []string{"serve"}); err != nil {
        fmt.Fprintf(os.Stderr, "start: %v\n", err)
        os.Exit(1)
    }

    // Check status.
    info, _ := d.Status()
    fmt.Printf("Status: %s, PID: %d, Port: %d\n", info.Status, info.PID, info.Port)

    // Stop the daemon.
    if err := d.Stop(context.Background()); err != nil {
        fmt.Fprintf(os.Stderr, "stop: %v\n", err)
    }
}

Server Mode

The spawned process calls Serve() to run as the daemon. It picks a free port, writes a PID file, registers a /health endpoint, and blocks until shutdown.

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "time"

    "github.com/grpmsoft/daemon"
)

func main() {
    cfg := daemon.Config{
        Name:        "myapp",
        DataDir:     ".myapp",
        IdleTimeout: 5 * time.Minute, // auto-shutdown after 5 min idle
    }

    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/hello", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "hello from daemon")
    })

    // Blocks until SIGINT/SIGTERM, context cancel, or idle timeout.
    if err := daemon.Serve(context.Background(), cfg, mux); err != nil {
        fmt.Fprintf(os.Stderr, "serve: %v\n", err)
        os.Exit(1)
    }
}

Serve() uses a Group (actor pattern) internally: HTTP server + signal handler + idle timer run as concurrent actors. When any actor returns, all others are interrupted.

Proxy Mode

Proxy() bridges stdin/stdout to the daemon's HTTP endpoint. Reads newline-delimited JSON from stdin, POSTs each message to the daemon, writes responses to stdout. Tracks connections for idle auto-shutdown.

// In your CLI's "mcp serve" command:
port, err := daemon.EnsureRunning(ctx, cfg, binary, args)
if err != nil {
    return err
}
return daemon.Proxy(ctx, cfg, daemon.ProxyOptions{MCPPath: "/mcp"})

ProxyOptions supports custom Stdin/Stdout (for testing) and LogPayloads (opt-in, disabled by default for security).

EnsureRunning

EnsureRunning is the main entry point for consumers. It checks if a daemon is already running and returns its port. If not, it starts a new one and waits for the health check to pass.

port, err := daemon.EnsureRunning(ctx, daemon.Config{
    Name:    "myapp",
    DataDir: ".myapp",
}, binary, []string{"serve"})
if err != nil {
    return err
}
fmt.Printf("Daemon ready on port %d\n", port)

Race-safe: if two agents call EnsureRunning simultaneously and one wins the start, the other detects the running daemon via the PID file and returns its port.

Configuration

Field Type Default Description
Name string (required) Application name. Used for PID file naming and health response
DataDir string (required) Directory for runtime files (PID, logs)
Timeout time.Duration 30s How long Start() waits for health check to pass
HealthPath string "/health" HTTP path for the health endpoint
IdleTimeout time.Duration 0 (disabled) Auto-shutdown after this duration with zero connections

Note: DataDir will contain a persistent <Name>.lock file used to serialize concurrent EnsureRunning calls. After a binary upgrade (go install), EnsureRunning returns the existing daemon's port — call Restart() to pick up the new binary.

run.Group

Group is a zero-dependency reimplementation of the oklog/run actor pattern. Start multiple concurrent actors; when any one returns, all others are interrupted.

var g daemon.Group

g.Add(
    func() error { return httpServer.Serve(ln) },
    func(error) { httpServer.Shutdown(ctx) },
)

g.Add(
    func() error { <-sigCtx.Done(); return sigCtx.Err() },
    func(error) { sigStop() },
)

err := g.Run() // blocks until all actors stop

Testing

All interfaces (PIDStore, ProcessManager, HealthChecker) are designed for test injection via NewWithDeps():

d := daemon.NewWithDeps(cfg, mockPIDStore, mockProcs, mockHealth)
go test ./...

Contributing

Contributions are welcome. Please open an issue or pull request on GitHub.

See CONTRIBUTING.md for development guidelines and CHANGELOG.md for release history.

License

MIT License. See LICENSE.

Documentation

Overview

Package daemon provides a reusable, cross-platform daemon lifecycle library.

Two modes of operation:

  • Client mode: Start/Stop/Restart/Status -- manage a background daemon process.
  • Server mode: Serve() -- run as the foreground daemon process with HTTP, PID management, signal handling.

Consumer wires CLI commands; this library provides the operations.

import "github.com/grpmsoft/daemon"

d := daemon.New(daemon.Config{Name: "myapp", DataDir: ".myapp"})
d.Start(ctx, binary, args) // spawn detached background process
d.Stop(ctx)                // graceful shutdown
d.Status()                 // check if running, get info

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotRunning indicates the daemon is not currently running.
	ErrNotRunning = errors.New("daemon: not running")

	// ErrAlreadyRunning indicates a daemon instance is already running.
	ErrAlreadyRunning = errors.New("daemon: already running")

	// ErrStartTimeout indicates the daemon did not become ready within the configured timeout.
	ErrStartTimeout = errors.New("daemon: start timeout")

	// ErrUnhealthy indicates the health check failed after the daemon started.
	ErrUnhealthy = errors.New("daemon: health check failed")

	// ErrStalePIDFile indicates the PID file exists but the daemon is not running.
	ErrStalePIDFile = errors.New("daemon: stale pid file")

	// ErrInvalidConfig indicates the configuration is invalid.
	ErrInvalidConfig = errors.New("daemon: invalid config")
)

Functions

func EnsureRunning

func EnsureRunning(ctx context.Context, cfg Config, binary string, args []string) (int, error)

EnsureRunning checks if a daemon is already running for this config. If running, it returns the existing port. If not, it starts a new daemon using the provided binary and args, then waits for health check to pass.

Protocol: two locks, two purposes, one ordering.

  • Startup lock (<name>.lock): serializes starters. Held during spawn→ready. Kernel releases on holder death → next waiter becomes starter automatically (N12).
  • PID file lock (<name>.pid): liveness signal. Held by running daemon for lifetime. Under startup lock, "PID held" means exactly "daemon alive" (no race with spawning).

func Proxy

func Proxy(ctx context.Context, cfg Config, opts ProxyOptions) error

Proxy bridges an input stream to the daemon's HTTP endpoint. Reads newline-delimited JSON-RPC from opts.Stdin, POSTs each to the daemon, writes responses to opts.Stdout.

Connection tracking: signals /daemon/connect on start and /daemon/disconnect on exit (only if connect succeeded — prevents stray decrements).

Respects ctx for cancellation. Uses context-based timeouts instead of hardcoded http.Client.Timeout.

func Serve

func Serve(ctx context.Context, cfg Config, handler http.Handler) error

Serve runs the daemon in the foreground. Called by the spawned process. It picks a free port, registers /health + connection tracking endpoints, writes PID data to the locked PID file, and blocks until ctx is cancelled, SIGINT/SIGTERM is received, or idle timeout fires.

Lock inheritance: if DAEMON_PIDFD=3 is set (spawned by Start/EnsureRunning), Serve inherits the locked fd from the parent via ExtraFiles. If not set (manual foreground start), Serve acquires the lock itself.

Idle auto-shutdown: if Config.IdleTimeout > 0 and all connections disconnect, Serve waits IdleTimeout before initiating graceful shutdown. A new connection arriving during the wait resets the timer.

Types

type Config

type Config struct {
	// Name identifies the application (e.g. "gode", "goda").
	// Used for PID file naming and health response.
	Name string

	// DataDir is the directory for runtime files (PID, logs), relative to
	// the workspace root. Example: ".gode".
	DataDir string

	// Timeout is how long Start() waits for the health check to pass.
	// Zero means 30 seconds.
	Timeout time.Duration

	// HealthPath is the HTTP path for the health endpoint.
	// Zero value means "/health".
	HealthPath string

	// IdleTimeout is how long the daemon waits with zero active connections
	// before initiating a graceful shutdown. Zero means never auto-shutdown.
	IdleTimeout time.Duration
}

Config controls daemon lifecycle behavior. Consumers provide this when creating a Daemon instance.

func (*Config) Validate added in v0.2.0

func (c *Config) Validate() error

Validate checks the configuration for invalid values. Name must be a single path element (no slashes). DataDir must be non-empty. HealthPath must not conflict with reserved daemon paths.

type ConnTracker

type ConnTracker struct {
	// contains filtered or unexported fields
}

ConnTracker tracks active client connections to the daemon. When IdleTimeout is configured, dropping to zero connections starts an idle timer that triggers graceful shutdown.

func NewConnTracker

func NewConnTracker() *ConnTracker

NewConnTracker creates a connection tracker.

func (*ConnTracker) Active

func (ct *ConnTracker) Active() int64

Active returns the current number of active connections.

func (*ConnTracker) Connect

func (ct *ConnTracker) Connect()

Connect increments the connection count and signals that idle timer should be reset.

func (*ConnTracker) Disconnect

func (ct *ConnTracker) Disconnect()

Disconnect decrements the connection count. If it drops to zero, signals on idleCh so the idle timer can start. The count is clamped to zero — an unmatched Disconnect (stray request, crashed client) cannot drive it negative and cause premature shutdown.

type Daemon

type Daemon struct {
	// contains filtered or unexported fields
}

Daemon manages a background daemon process. It depends on PIDStore, ProcessManager, and HealthChecker interfaces — not on concrete implementations. New() wires the defaults.

func New

func New(cfg Config) *Daemon

New creates a Daemon with the given configuration and default implementations.

func NewWithDeps

func NewWithDeps(cfg Config, pids PIDStore, procs ProcessManager, health HealthChecker) *Daemon

NewWithDeps creates a Daemon with explicit interface implementations. Intended for testing — callers supply mock PIDStore, ProcessManager, and HealthChecker.

func (*Daemon) IsRunning

func (d *Daemon) IsRunning() bool

IsRunning returns true if the daemon process is alive according to the PID file.

func (*Daemon) Restart

func (d *Daemon) Restart(ctx context.Context, binary string, args []string) error

Restart stops a running daemon (if any) and starts a new one.

func (*Daemon) Start

func (d *Daemon) Start(ctx context.Context, binary string, args []string) error

Start spawns a detached background process running the given binary with args. It waits for the health check to pass within Config.Timeout. The spawned process must eventually call Serve().

func (*Daemon) Status

func (d *Daemon) Status() (*Info, error)

Status reads the PID file and checks the process state.

func (*Daemon) Stop

func (d *Daemon) Stop(ctx context.Context) error

Stop reads the PID file, verifies the process identity (PID + binary path), kills the daemon process, and clears the PID file. If the PID has been recycled to a different process, the stale PID file is cleared without killing.

type Group

type Group struct {
	// contains filtered or unexported fields
}

Group manages multiple concurrent actors. When any actor returns, all others are interrupted. Modeled after oklog/run.Group but implemented from scratch with zero external dependencies.

func (*Group) Add

func (g *Group) Add(execute func() error, interrupt func(error))

Add registers an actor pair. execute is started in a goroutine by Run. interrupt is called when another actor's execute returns, with that actor's error. interrupt must cause execute to return promptly.

func (*Group) Run

func (g *Group) Run() error

Run starts all actors concurrently. It blocks until all actors have stopped. The first actor whose execute returns triggers interrupt on all others (including itself). Returns the error from the first actor that returned.

type HealthChecker

type HealthChecker interface {
	Check(port int, healthPath string) error
	WaitUntilReady(port int, healthPath string, timeout time.Duration) error
}

HealthChecker abstracts daemon readiness verification.

type Info

type Info struct {
	Status    Status        `json:"status"`
	PID       int           `json:"pid"`
	Port      int           `json:"port"`
	Name      string        `json:"name"`
	StartTime time.Time     `json:"startTime"`
	Uptime    time.Duration `json:"uptime"`
}

Info is the status snapshot returned by Daemon.Status().

type PIDInfo

type PIDInfo struct {
	PID       int       `json:"pid"`
	Port      int       `json:"port"`
	Name      string    `json:"name"`
	Binary    string    `json:"binary"`
	StartTime time.Time `json:"startTime"`
}

PIDInfo holds the persisted state of a running daemon, as returned by PIDStore.Load().

type PIDStore

type PIDStore interface {
	Save(pid, port int, name, binary string, startTime time.Time) error
	Load() (PIDInfo, error)
	Clear() error
	IsAlive() bool
	Path() string
}

PIDStore abstracts PID file persistence. The Daemon model depends on this interface, not on a concrete file-based implementation. PIDInfo is returned by Load() with the stored daemon state.

type ProcessManager

type ProcessManager interface {
	StartDetached(binary string, args []string, logFile string, env []string) (pid int, err error)
	KillProcess(pid int) error
	IsProcessAlive(pid int) bool
}

ProcessManager abstracts platform-specific process lifecycle operations.

type ProxyOptions added in v0.2.0

type ProxyOptions struct {
	// MCPPath is the HTTP path on the daemon to POST JSON-RPC messages to.
	// Default: "/mcp".
	MCPPath string

	// Stdin is the input reader for JSON-RPC messages. Default: os.Stdin.
	Stdin io.Reader

	// Stdout is the output writer for JSON-RPC responses. Default: os.Stdout.
	Stdout io.Writer

	// LogPayloads enables logging of request/response bodies (first 500 bytes).
	// Default: false (only method/id/size/latency logged for security — S3).
	LogPayloads bool
}

ProxyOptions configures the Proxy behavior.

type Status

type Status int

Status represents the lifecycle state of a daemon process.

const (
	StatusStopped  Status = iota // not running, no PID file
	StatusStarting               // process spawned, health check pending
	StatusRunning                // healthy and responding
	StatusStopping               // shutdown in progress
	StatusError                  // PID file exists but process is dead or unhealthy
)

Status constants representing the lifecycle state of a daemon process.

func (Status) String

func (s Status) String() string

String returns a human-readable representation of the status.

Directories

Path Synopsis
Package internal provides platform-specific implementations for the daemon library: PID file management, process lifecycle operations, and health checking.
Package internal provides platform-specific implementations for the daemon library: PID file management, process lifecycle operations, and health checking.
pidlock
Package pidlock provides an exclusive advisory lock backed by the PID file itself.
Package pidlock provides an exclusive advisory lock backed by the PID file itself.

Jump to

Keyboard shortcuts

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