Documentation
¶
Overview ¶
Package daemon provides a reusable, cross-platform daemon lifecycle library.
Two modes of operation:
- Client mode: Start/Stop/Restart/EnsureRunning/Status -- manage a background daemon process.
- Server mode: Serve() -- run as the foreground daemon process with HTTP, PID management, signal handling.
All lifecycle mutations (Start, Stop, Restart, EnsureRunning) serialize on a single startup lock file. Public methods are thin wrappers: acquire lock, check state, delegate to internal *Locked methods, release lock.
Consumer wires CLI commands; this library provides the operations.
import "github.com/grpmsoft/daemon"
d := daemon.New(daemon.Config{Name: "myapp", DataDir: ".myapp", Args: []string{"serve"}})
info, err := d.Start(ctx) // spawn detached background process
info, err = d.EnsureRunning(ctx) // idempotent start-or-attach
d.Stop(ctx) // graceful shutdown
d.Status() // check if running, get info
Index ¶
- Variables
- func EnsureRunning(ctx context.Context, cfg Config) (int, error)
- func Proxy(ctx context.Context, cfg Config, opts ProxyOptions) error
- func Serve(ctx context.Context, cfg Config, handler http.Handler) error
- type Config
- type ConnTracker
- type ControlClient
- type Daemon
- func (d *Daemon) ControlClient() (*ControlClient, error)
- func (d *Daemon) EnsureRunning(ctx context.Context) (*Info, error)
- func (d *Daemon) Hold(ctx context.Context) error
- func (d *Daemon) IsRunning() bool
- func (d *Daemon) Release()
- func (d *Daemon) Restart(ctx context.Context) (*Info, error)
- func (d *Daemon) Start(ctx context.Context) (*Info, error)
- func (d *Daemon) Status() (*Info, error)
- func (d *Daemon) Stop(ctx context.Context) error
- type Group
- type HealthChecker
- type Info
- type PIDInfo
- type PIDStore
- type ProcessManager
- type ProxyOptions
- type StartSpec
- type Status
Constants ¶
This section is empty.
Variables ¶
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") // ErrStopIntent indicates the daemon was explicitly held via Hold() and // EnsureRunning refuses to auto-start it. Use Release(), Start(), or // Restart() to clear the hold and allow restarts. ErrStopIntent = errors.New("daemon: held; use Release or Start to allow restarts") // ErrSpawnCooldown indicates a recent spawn failure triggered a cooldown // period. EnsureRunning returns this error instead of retrying immediately. // The cooldown prevents rapid respawn loops when multiple agents call // EnsureRunning after a failure. ErrSpawnCooldown = errors.New("daemon: spawn cooldown active after recent failure") )
Functions ¶
func EnsureRunning ¶
EnsureRunning is a convenience package-level wrapper that creates a Daemon from cfg and calls its EnsureRunning method. Returns the port the daemon is listening on.
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: the proxy holds a lease (GET /daemon/attach, the TCP connection is the lease) for as long as it runs, so the daemon's idle timer counts it as an agent.
The daemon may be replaced while the proxy runs (restart, crash, idle shutdown); the proxy follows it and the client on stdin never notices:
- When its lease drops, the proxy looks for a daemon in the PID file for up to cfg.Timeout and re-attaches. It never starts one there: a stop is a decision (Stop, Hold, the idle timer), and an idle client must not undo it.
- A request that provably never reached a daemon (the dial failed), or that a daemon rejected with 401 (another daemon on the old port), is sent once more after reconnecting: EnsureRunning when cfg can start the daemon (Binary set), else the PID file is read again.
- A request that failed after it was sent may have run (a tools/call may have edited files): it is answered with a JSON-RPC error for its id and never sent again. A notification gets no answer.
Proxy returns only when ctx is done, stdin ends or fails, or stdout fails. At start it requires a running daemon (ErrNotRunning otherwise).
Respects ctx for cancellation. Uses context-based timeouts instead of hardcoded http.Client.Timeout.
func Serve ¶
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 `json:"name"`
// DataDir is the directory for runtime files (PID, logs), relative to
// the workspace root. Example: ".gode".
DataDir string `json:"dataDir"`
// Binary is the executable path for the daemon process.
// Empty means os.Executable() (current binary).
Binary string `json:"binary"`
// Args are the arguments passed to the child process in Start/Restart.
Args []string `json:"args"`
// Timeout is how long Start() waits for the health check to pass.
// Zero means 30 seconds.
Timeout time.Duration `json:"timeout"`
// HealthPath is the HTTP path for the health endpoint.
// Zero value means "/health".
HealthPath string `json:"healthPath"`
// IdleTimeout is how long the daemon waits with zero active connections
// before initiating a graceful shutdown. Zero means never auto-shutdown.
IdleTimeout time.Duration `json:"idleTimeout"`
// Dir is the working directory for the spawned daemon process.
// Empty means DataDir. Prevents the daemon from holding the parent's
// CWD (which may be deleted or unmounted after start).
Dir string `json:"dir"`
// DisableTokenAuth, when true, skips bearer token authentication for
// the application handler (everything outside /health and /daemon/*).
// Default false = token required (secure by default). Set to true only
// for local curl debugging.
//
// Breaking change v0.4.0: replaces RequireToken (inverted semantics).
// Migration: RequireToken: true -> remove field (default secure).
// RequireToken: false -> DisableTokenAuth: true.
DisableTokenAuth bool `json:"disableTokenAuth"`
// SpawnCooldown is the duration to wait after a spawn failure before
// retrying. During cooldown, EnsureRunning returns ErrSpawnCooldown
// instead of attempting another spawn. Prevents rapid respawn loops
// when multiple MCP agents call EnsureRunning after a spawn failure.
// Default: 5s. Set to -1 to disable.
SpawnCooldown time.Duration `json:"spawnCooldown"`
// ShutdownTimeout is the total time budget for graceful shutdown.
// stopLocked divides this budget across its phases:
// - HTTP shutdown request: ShutdownTimeout / 2
// - Wait for lock release: ShutdownTimeout
// - Kill grace period: ShutdownTimeout / 2
// Also used for kill grace in startWithLock failure cleanup.
// Default: 10s.
ShutdownTimeout time.Duration `json:"shutdownTimeout,omitempty"`
}
Config controls daemon lifecycle behavior. Consumers provide this when creating a Daemon instance.
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 ControlClient ¶ added in v0.4.2
type ControlClient struct {
// contains filtered or unexported fields
}
ControlClient talks to the loopback control plane of a running daemon.
It owns the two things every caller would otherwise repeat: the address from the PID file and the bearer token written there. The token is the daemon's control-plane secret, so it stays inside the client — no accessor hands it out, and no caller has to know how a request is authenticated.
The endpoints are the daemon's own (shutdown, health) and those its owner registers on the mux it passes to Serve (GODE's /debug/metrics).
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 ¶
New creates a Daemon with the given configuration and default implementations. Config is normalized (defaults applied) at construction time and is immutable after New returns. Concurrent method calls on the same *Daemon are safe.
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) ControlClient ¶ added in v0.4.2
func (d *Daemon) ControlClient() (*ControlClient, error)
ControlClient returns a client for the running daemon's control plane. An error means the PID file cannot be read or names no port — the daemon did not reach the point of listening — never that a request failed.
func (*Daemon) EnsureRunning ¶ added in v0.3.0
EnsureRunning is idempotent: if a daemon is already running, returns its info. If not, starts a new one. Safe for concurrent callers — all serialize on the startup lock.
func (*Daemon) Hold ¶ added in v0.4.0
Hold stops the daemon and writes a stop-intent marker that prevents EnsureRunning from restarting it. Use Release() or Start() to clear the marker and allow restarts again.
func (*Daemon) IsRunning ¶
IsRunning returns true if the daemon process is alive according to the PID file.
func (*Daemon) Release ¶ added in v0.4.0
func (d *Daemon) Release()
Release clears the stop-intent marker, allowing EnsureRunning to restart the daemon. It is a no-op if no marker exists.
func (*Daemon) Restart ¶
Restart stops a running daemon (if any) and starts a new one, under a single startup lock acquisition. This prevents the deadlock that would occur if Restart called the public Stop() then Start() (each acquires the startup lock independently).
func (*Daemon) Start ¶
Start spawns a detached background process using Config.Binary and Config.Args. Returns ErrAlreadyRunning if a daemon is already running. The spawned process must eventually call Serve().
Lock protocol:
- Acquire startup lock (serializes concurrent Start/EnsureRunning/Stop/Restart)
- Under startup lock, check if daemon already running (PID file held)
- Delegate to startLocked (acquire PID lock, spawn, health check)
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.
type HealthChecker ¶
type HealthChecker interface {
Check(port int, healthPath string) error
WaitUntilReady(ctx context.Context, 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"`
Token string `json:"token,omitempty"`
}
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 {
Start(ctx context.Context, spec StartSpec) (pid int, err error)
KillProcess(ctx context.Context, pid int, grace time.Duration) 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 StartSpec ¶ added in v0.4.0
type StartSpec struct {
Binary string // executable path
Args []string // command-line arguments
Dir string // working directory
Env []string // environment variables
LogFile string // stderr/stdout log file path
ExtraFiles []*os.File // additional file descriptors (e.g., PID lock fd)
}
StartSpec describes how to spawn a daemon child process.
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.
Source Files
¶
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. |