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

daemon

package module
v0.4.2 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 25 Imported by: 0

README

daemon

CI Go Reference codecov Go Version

Pure-Go on-demand process lifecycle and local IPC layer for shared background services used by CLI tools and agents.

Zero CGO. Zero dependencies. Cross-platform.

Your CLI starts a background process when needed, shares it between multiple clients, and shuts it down when idle. The owner of the process lifetime is demand, not an init system.

CLI #1 ──┐
         │    EnsureRunning
CLI #2 ──┼──────────────────► daemon process ──► idle ──► shutdown
         │                        │
CLI #3 ──┘                   lease tracking
                            (zero connections
                              + IdleTimeout)

Why

MCP servers, language servers, and dev tools need a persistent background process shared by multiple agents. Without lifecycle management, each agent spawns its own instance -- 6 agents x ~1-2 GB gopls = 6-12 GB RAM.

daemon solves this with one abstraction: EnsureRunning -- start if needed, return the port if already running. Race-safe: two agents calling simultaneously never produce two daemons. The loser detects the winner via a kernel-level file lock and returns the same port.

Installation

go get github.com/grpmsoft/daemon

Quick Start

cfg := daemon.Config{
    Name:    "myapp",
    DataDir: ".myapp",
    Args:    []string{"serve"},
}

// Start-if-needed, get port (package-level convenience):
port, err := daemon.EnsureRunning(ctx, cfg)

// Or use the method form for full control:
d := daemon.New(cfg)
info, err := d.Start(ctx)           // error if already running
info, err = d.EnsureRunning(ctx)    // idempotent
info, err = d.Restart(ctx)          // stop + start, one lock
err = d.Stop(ctx)                   // graceful shutdown
info, err = d.Status()              // read-only, no lock

// Explicit autostart prevention:
err = d.Hold(ctx)                   // stop + write .stop-intent marker
d.Release()                         // clear marker, allow restarts

All mutating methods serialize on a startup lock and an in-process mutex. Public methods never call other public methods. Config is immutable after New() -- concurrent calls are safe.

Three Modes

Client Mode

Your CLI manages a background daemon:

d := daemon.New(daemon.Config{
    Name:    "myapp",
    DataDir: ".myapp",
    Args:    []string{"serve"},
})

info, err := d.Start(ctx)    // spawn, wait for health
info, _ := d.Status()        // PID, port, uptime
d.Stop(ctx)                  // graceful shutdown
Server Mode

The spawned process calls Serve() to become the daemon. It picks a free port, writes a PID file with a bearer token, registers /health, and blocks until shutdown.

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

daemon.Serve(ctx, daemon.Config{
    Name:        "myapp",
    DataDir:     ".myapp",
    IdleTimeout: 5 * time.Minute,
}, mux)

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

Proxy Mode

Proxy() bridges stdin/stdout to the daemon's HTTP endpoint for MCP/JSON-RPC protocols:

port, _ := daemon.EnsureRunning(ctx, cfg)
daemon.Proxy(ctx, cfg, daemon.ProxyOptions{MCPPath: "/mcp"})

ProxyOptions fields: MCPPath (HTTP path, default "/mcp"), Stdin/Stdout (io.Reader/io.Writer -- replaceable for testing, default os.Stdin/os.Stdout), LogPayloads (opt-in, default false -- when enabled, logs request/response bodies to proxy.log; disabled by default because payloads may contain sensitive data).

The proxy automatically establishes a lease-based connection to the daemon (see below) and forwards newline-delimited JSON between stdin and the daemon's HTTP endpoint.

The daemon may be replaced while a proxy runs (restart, crash, idle shutdown). The proxy follows it, and the client on stdin never has to reconnect:

What happened What the proxy does
Its lease dropped (daemon restarted or exited) Looks for a daemon in the PID file for up to Config.Timeout and re-attaches. It never starts one here: a stop (Stop, Hold, the idle timer) is a decision an idle client must not undo
A request's dial failed, or a daemon answered 401 (another daemon took the old port) The request never ran: the proxy reconnects (EnsureRunning when Config.Binary is set, otherwise the PID file) and sends it once more
A request failed after it was sent It may have run (a tool call may have changed files): the client gets a JSON-RPC error for that id (-32603, "daemon unavailable: …") and the request is never sent again. Notifications get no answer
The daemon is held (Hold) Requests get the JSON-RPC error; the proxy keeps running and works again after Release

Proxy returns only when its context ends, stdin ends or fails, or stdout fails.

Lease-Based Connection Tracking

The lifetime of a TCP connection is the lease. When a proxy process dies -- SIGKILL, OOM, closed terminal -- the kernel closes the socket, the daemon sees the disconnect, and the connection count drops. No heartbeats. No timers. No stray counts from crashed clients.

Proxy process          Daemon
     │                    │
     ├── GET /daemon/attach ──► ct.Connect()
     │         │          │
     │    TCP alive       │     connection count > 0
     │    = lease held    │     idle timer blocked
     │         │          │
     ╳ SIGKILL            │
     │                    │
  kernel closes socket ──► ct.Disconnect()
                          │
                     count drops to 0
                          │
                     IdleTimeout fires
                          │
                     graceful shutdown
Proxy Limitations
  • One request in flight (sequential dispatch, not pipelined)
  • No server-to-client notifications (unidirectional)
  • Not Streamable-HTTP/SSE compatible (plain JSON-RPC POST only)
  • Use direct HTTP connection for concurrent tool calls

Hold / Release

Stop() shuts down the daemon but does not prevent EnsureRunning from starting it again. If you need the daemon to stay down -- for example, during a binary upgrade or manual debugging -- use Hold():

d := daemon.New(cfg)

// Stop and block automatic restarts:
err := d.Hold(ctx)  // writes .stop-intent marker

// Any subsequent EnsureRunning returns ErrStopIntent:
_, err = d.EnsureRunning(ctx) // err == daemon.ErrStopIntent

// Allow restarts again:
d.Release()                   // clears the marker
// -- or --
d.Start(ctx)                  // Start and Restart also clear the marker

The stop-intent marker is a file in DataDir. It survives process crashes. Start() and Restart() clear it implicitly -- an explicit start decision overrides a previous hold.

Spawn Cooldown

When a spawn fails (binary not found, port conflict, health check timeout), EnsureRunning writes a cooldown marker. During the cooldown window, subsequent EnsureRunning calls return ErrSpawnCooldown immediately instead of retrying. This prevents rapid respawn loops when multiple MCP agents call EnsureRunning after a failure.

cfg := daemon.Config{
    Name:          "myapp",
    DataDir:       ".myapp",
    SpawnCooldown: 10 * time.Second, // default: 5s, -1 to disable
}

d := daemon.New(cfg)
_, err := d.EnsureRunning(ctx) // spawn fails
_, err = d.EnsureRunning(ctx)  // err == daemon.ErrSpawnCooldown (within 10s)

The cooldown marker is time-based: it expires after SpawnCooldown elapses. A successful Start() or Restart() clears it. Context cancellation skips the cooldown check.

Shutdown Timeout

Config.ShutdownTimeout (default 10s) is the daemon's graceful drain budget. Serve() passes it to http.Server.Shutdown. On the client side, Stop() sends a shutdown request (fixed 2s timeout), waits ShutdownTimeout + 2s for the daemon to release the PID lock, then escalates to kill with a fixed 3s grace. Worst case: ShutdownTimeout + 7s.

cfg := daemon.Config{
    Name:            "myapp",
    DataDir:         ".myapp",
    ShutdownTimeout: 15 * time.Second, // default: 10s
}

Security

Serve() generates a bearer token (crypto/rand) at startup and stores it in the PID file (0600 permissions). All /daemon/* control-plane endpoints require Authorization: Bearer <token>. The /health endpoint stays open for external probes. The application handler (everything outside /health and /daemon/*) also requires the token by default (secure by default since v0.4.0).

Method Path Auth Purpose
GET /health None Readiness probe
GET /daemon/attach Bearer Lease connection
POST /daemon/shutdown Bearer Graceful stop

DNS rebinding protection: loopbackGuard middleware rejects requests with non-loopback Host or Origin headers. Combined with the bearer token, a page with a rebinding domain that passes the Host check still cannot authenticate.

Daemon.ControlClient() reads the address and the token from the PID file and sends the header for you (since v0.4.2), so an application that registers its own endpoints on the daemon's mux reaches them without handling the token:

client, err := d.ControlClient()
if err != nil {
    return err
}
body, err := client.Get(ctx, "/debug/metrics")

Set Config.DisableTokenAuth = true to skip token auth for the application handler (e.g., for local curl debugging).

Configuration

Field Type Default Description
Name string (required) Application name, used for PID file naming
DataDir string (required) Directory for runtime files (PID, logs). Resolved to absolute path
Binary string os.Executable() Executable path for the daemon process
Args []string nil Arguments passed to the child process
Dir string DataDir Working directory for the spawned daemon
Timeout time.Duration 30s How long Start() waits for health check
HealthPath string "/health" HTTP path for the health endpoint
IdleTimeout time.Duration 0 (disabled) Auto-shutdown after this duration with zero connections
DisableTokenAuth bool false Skip bearer token auth for the application handler
SpawnCooldown time.Duration 5s Cooldown after spawn failure; -1 to disable
ShutdownTimeout time.Duration 10s Daemon graceful drain budget; Stop() worst case is ShutdownTimeout + 7s

Config is normalized at construction time (New()). Set Binary and Args once; all lifecycle methods use them automatically.

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:

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

Integration tests use the helper-process pattern -- no external binaries needed. CI runs with -race on Linux, macOS, and Windows.

Not a Service Manager

This library is not a replacement for systemd, launchd, or Windows SCM. If you need a long-lived system service, use kardianos/service -- it is the industry standard for that job.

The two libraries are complementary: if your tool must be present from boot, run myapp serve under kardianos/service and skip EnsureRunning. Do not combine Restart=always with idle auto-shutdown -- two supervisors will fight.

Contributing

Contributions welcome. See CONTRIBUTING.md for guidelines and CHANGELOG.md for release history.

Star History

Star History Chart

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/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

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")

	// 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

func EnsureRunning(ctx context.Context, cfg Config) (int, error)

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

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 `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.

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 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).

func (*ControlClient) Get added in v0.4.2

func (c *ControlClient) Get(ctx context.Context, path string) ([]byte, error)

Get requests path from the control plane and returns the response body.

func (*ControlClient) Post added in v0.4.2

func (c *ControlClient) Post(ctx context.Context, path string) ([]byte, error)

Post requests path from the control plane and returns the response body.

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. 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

func (d *Daemon) EnsureRunning(ctx context.Context) (*Info, error)

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

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

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

func (d *Daemon) IsRunning() bool

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

func (d *Daemon) Restart(ctx context.Context) (*Info, error)

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

func (d *Daemon) Start(ctx context.Context) (*Info, error)

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)

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 gracefully shuts down the daemon. Serialized on the startup lock. Idempotent: returns nil if the daemon is already stopped.

Stop does NOT prevent EnsureRunning from restarting the daemon. Use Hold() to stop the daemon AND block automatic restarts.

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(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.

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