daemon

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package daemon runs baum's one long-lived local server per user, which the desktop app and coding-agent hooks share, and the client that finds, starts and stops it.

A daemon holds an SQLite write lock in its private state directory for its whole life; the operating system releases it when the process exits, so a crash never leaves a lock behind, and the next daemon clears the socket, credential and status files the crashed one left. It offers two transports:

  • control: a versioned line-delimited JSON protocol on a Unix socket only the user can reach (Protocol), used by the baum command line and hooks;
  • app: the authenticated loopback HTTP server of internal/uiserver, whose credential the app reads from a private file in the state directory.

The daemon never contacts a baum server. What it runs in the background is whatever the application started with Options.Start does.

Index

Constants

View Source
const (
	// DefaultReadyTimeout covers a new daemon waiting for its predecessor
	// to finish stopping.
	DefaultReadyTimeout = DefaultLockTimeout + 15*time.Second
	// DefaultStopTimeout leaves a stopping daemon its shutdown timeout and
	// time to close.
	DefaultStopTimeout = DefaultShutdownTimeout + 15*time.Second
)

Client defaults.

View Source
const (
	MaxLogBytes = 1 << 20
	LogBackups  = 2
)

Log sizes: the current file rotates at MaxLogBytes, and LogBackups older files are kept, so the logs never exceed (LogBackups+1)*MaxLogBytes.

View Source
const (
	CodeProtocolMismatch = "protocol_mismatch"
	CodeUnknownMethod    = "unknown_method"
	CodeBadRequest       = "bad_request"
	CodeFailed           = "failed"
)

Error codes.

View Source
const (
	DefaultShutdownTimeout = 20 * time.Second
	// DefaultLockTimeout outlasts a predecessor's shutdown, so a successor
	// started during a restart waits for it rather than giving up.
	DefaultLockTimeout = DefaultShutdownTimeout + 15*time.Second
)

Defaults of Options.

View Source
const Protocol = 1

Protocol is the control protocol version. Increment it when a request or response changes incompatibly. "hello" and "stop" keep their meaning in every version, so any client can identify and replace any daemon.

View Source
const StartBackoff = 3 * time.Minute

StartBackoff is how long StartDetached waits after starting a daemon before it starts another, unless that daemon served meanwhile: a daemon that cannot start must not be started again by every hook.

Variables

View Source
var ErrNoCredential = errors.New("the baum daemon has not published an app credential")

ErrNoCredential means no daemon has published a credential.

View Source
var ErrNotRunning = errors.New("the baum daemon is not running")

ErrNotRunning means no daemon answers on the socket and none holds the lock.

View Source
var ErrRunning = errors.New("another baum daemon is already running")

ErrRunning is returned by Run when another daemon owns the lock.

View Source
var ErrStartBackoff = errors.New("a baum daemon was started moments ago and has not served yet")

ErrStartBackoff means StartDetached started a daemon less than StartBackoff ago that has not served yet.

View Source
var TrustedExecutables []string

TrustedExecutables are further executables a daemon may restart into at a newer client's request, beside its own executable and the baum on its login shell's PATH. Entries are absolute paths, compared after resolving symbolic links, and are set before Run. It is empty: the desktop app links its bundled baum onto the PATH instead, which makes it the baum on the PATH.

Functions

func ClearStopped

func ClearStopped(p Paths) error

ClearStopped removes the mark MarkStopped wrote.

func HasRun

func HasRun(p Paths) bool

HasRun reports whether a daemon ever started for these paths: its lock file, which only a daemon creates (clients only look for it), exists.

func MarkStopped

func MarkStopped(p Paths) error

MarkStopped records that the user stopped the daemon. Without a state directory no daemon ever ran here, and nothing is marked.

func Older

func Older(daemon, client string) bool

Older reports whether a daemon of version daemon should be replaced by a client of version client: an older release, or a development build when the client is a release. Unequal development builds are kept; stop one with "baum daemon stop".

func Run

func Run(ctx context.Context, opts Options) error

Run is the daemon. It returns ErrRunning when another daemon holds the lock, and otherwise serves until ctx ends or a client asks it to stop.

func Spawn

func Spawn(paths Paths, cmd *exec.Cmd) (<-chan struct{}, error)

Spawn starts cmd as a detached daemon process with its output in OutputFile. The returned channel is closed when the process exits.

func SpawnSelf

func SpawnSelf(paths Paths, start StartOptions) (<-chan struct{}, error)

SpawnSelf starts this executable as "baum daemon run --detached", detached from the caller: in its own session (Unix) or as a detached process group (Windows), with no standard input and its output in OutputFile. The daemon resolves its own environment, so the caller's is passed unchanged.

func StoppedByUser

func StoppedByUser(p Paths) bool

StoppedByUser reports whether the user stopped the daemon and nothing started it explicitly since.

Types

type App

type App struct {
	// API serves /api/ routes on the app transport, behind its
	// authentication, Host and Origin checks.
	API http.Handler
	// Assets serves the browser application under the session namespace.
	Assets http.Handler
	// Sample serves the API of the sample project the tour shows when there
	// is no local data yet, under /s/<namespace>/sample/.
	Sample http.Handler
	// Run does background work until its context ends.
	Run func(ctx context.Context)
	// Shutdown stops work that must be recorded when it is cut short (the
	// generation queue records running jobs as interrupted). It is given
	// Options.ShutdownTimeout.
	Shutdown func(ctx context.Context) error
	// Close releases resources once nothing is served.
	Close func()
	// Drain stops accepting paid work and reports true when none is queued
	// or running, atomically with accepting work, so nothing confirmed can
	// slip in between; when work is pending it changes nothing and reports
	// false. Without it the daemon is always idle.
	Drain func() bool
	// DrainRunning is Drain for a restart into a newer version, which
	// waits only for running work: it starts no queued work, and reports
	// true once none runs, stopping accepting paid work in the same step.
	// Queued work stays for the new daemon. While it reports false, it
	// keeps holding queued work until Resume. Without it a restart waits for
	// Drain.
	DrainRunning func() bool
	// Resume accepts paid work again after a Drain or DrainRunning that led
	// nowhere, and starts queued work again.
	Resume func()
	// Methods registers further control requests the application serves,
	// beside Options.Methods; they require the client's protocol to match.
	Methods map[string]Method
	// Waits registers further control requests that may wait for as long as
	// their client stays connected, beside Options.Waits.
	Waits map[string]Method
}

App is what the daemon serves and runs. Every field may be nil.

type Client

type Client struct {
	Paths Paths
	// Version is this baum's version, compared with the daemon's.
	Version string
	Start   StartOptions
	// Spawn starts a daemon; nil starts this executable as "baum daemon run".
	Spawn        Spawner
	ReadyTimeout time.Duration
	StopTimeout  time.Duration
	// Restarted is told when Ensure stops an older daemon to replace it.
	Restarted func(old Status)
	// Deferred is told when Ensure keeps an older daemon that is running jobs
	// and has asked it to restart as this version once they finish.
	Deferred func(old Status)
	// Executable is what an older daemon restarts into; empty is this
	// process's executable.
	Executable string
}

Client finds, starts and stops the user's daemon.

func (*Client) Call

func (c *Client) Call(ctx context.Context, method string, params any) (Response, error)

Call sends one request and reads its response. A missing socket or a refused connection is ErrNotRunning; a socket in a directory that is not private to the user is refused.

func (*Client) Ensure

func (c *Client) Ensure(ctx context.Context) (Status, error)

Ensure returns a running daemon this client can use, starting one when none runs and replacing one that is older than this client. Concurrent calls start exactly one daemon. A daemon newer than this client is kept; one speaking a newer protocol is an IncompatibleError.

func (*Client) Hello

func (c *Client) Hello(ctx context.Context) (Status, error)

Hello returns the running daemon's status.

func (*Client) StartDetached

func (c *Client) StartDetached(ctx context.Context) (bool, error)

StartDetached starts a daemon without waiting for it to serve, for callers that must not wait, such as hooks; Ensure waits. It starts none while a daemon holds the daemon lock (one is starting, stopping or not answering), another client is starting one, or it started one less than StartBackoff ago that has not served yet (ErrStartBackoff); it reports whether it started one. The start is recorded in a file the daemon removes once it serves, so callers racing it see it rather than start a second one.

func (*Client) Status

func (c *Client) Status(ctx context.Context) (Status, error)

Status is the daemon's status, ErrNotRunning, or an error naming the pid of a daemon that holds the lock but does not answer.

func (*Client) Stop

func (c *Client) Stop(ctx context.Context) (bool, error)

Stop asks the daemon to stop and waits until it has exited. A daemon that is still starting is asked once it answers. It reports false when no daemon was running.

func (*Client) Wait

func (c *Client) Wait(ctx context.Context, method string, params any) (Response, error)

Wait sends a request the daemon may hold for as long as the connection stays open (Options.Waits) and reads its response. It connects as Call does, then waits until ctx ends; ending it closes the connection, which cancels the request in the daemon.

type Credential

type Credential struct {
	URL string `json:"url"`
}

Credential is how an app client reaches the daemon's HTTP transport. URL is the launch URL, http://127.0.0.1:PORT/#token=<secret>: opening it runs the bootstrap page, which exchanges the fragment for the session cookie. The secret also works as a Bearer token on /api/ routes. It stays valid for repeated exchanges for the daemon's whole life and changes only when a new daemon starts, which rewrites the file atomically.

func ReadCredential

func ReadCredential(paths Paths) (Credential, error)

ReadCredential reads the running daemon's credential. The file must be a small private regular file holding a loopback launch URL.

func (Credential) Origin

func (c Credential) Origin() string

Origin is the credential's loopback origin, http://127.0.0.1:PORT.

func (Credential) Token

func (c Credential) Token() string

Token is the credential's secret.

type Delivery

type Delivery struct {
	Result  any
	Written func(error)
}

Delivery is a method's result when the method must know whether its answer was written to the client, such as one that hands something over once: Result is answered, and Written is told how writing the answer went. A written answer can still be lost by a client that goes away after it.

type Error

type Error struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

Error is a failed request. Code is stable; Message is for people.

func (*Error) Error

func (e *Error) Error() string

type IncompatibleError

type IncompatibleError struct {
	Daemon Status
	Client string
}

IncompatibleError is a running daemon this client cannot use or replace: it speaks a newer protocol.

func (*IncompatibleError) Error

func (e *IncompatibleError) Error() string

type Method

type Method func(ctx context.Context, params json.RawMessage) (any, error)

Method handles one control request kind. Its params are the request's Params; its result is encoded as the response's Result. An *Error it returns is answered with its code; any other error with CodeFailed.

type Options

type Options struct {
	Paths   Paths
	Version string
	// Port is the app transport's loopback port; zero picks a free one.
	Port int
	// Prepare runs once this process owns the daemon lock, before Start. The
	// command line resolves the login shell environment here.
	Prepare func(ctx context.Context) error
	// Start builds the application once this process owns the daemon lock.
	// The server is not listening yet; Publish may be called at any time.
	Start func(ctx context.Context, server *uiserver.Server) (App, error)
	// Methods registers further control requests; they require the client's
	// protocol to match.
	Methods map[string]Method
	// Waits registers control requests that may wait for as long as their
	// client stays connected, such as a hook holding its agent until the app
	// answers: neither the method timeout nor the idle timeout applies, and
	// the method's context ends when the client disconnects (or sends
	// anything more) or the daemon stops. The connection closes after the
	// answer. They require the client's protocol to match; clients send them
	// with Client.Wait.
	Waits map[string]Method
	// Respawn starts the daemon executable a newer client named, detached,
	// to take over once this daemon exits (see StopParams). The daemon gives
	// it only an executable it trusts (see executablePolicy), with symbolic
	// links resolved, checked when it is requested and again just before.
	// Without it such requests stop the daemon only when it is idle, and are
	// refused while it is not.
	Respawn func(executable string, status Status) error
	// OpenLogger opens the log once the lock is held, so a daemon that loses
	// the race never rotates the winner's log. It replaces Logger.
	OpenLogger func() (*slog.Logger, func(), error)
	// ShutdownTimeout bounds App.Shutdown; zero is DefaultShutdownTimeout.
	ShutdownTimeout time.Duration
	// LockTimeout is how long to wait for a previous daemon to release the
	// lock; zero is DefaultLockTimeout.
	LockTimeout time.Duration
	Logger      *slog.Logger
	// contains filtered or unexported fields
}

Options configure Run.

type Paths

type Paths struct {
	// State is the private directory holding the lock, credential, status
	// file and logs.
	State string
	// Socket is the control socket, in a private directory.
	Socket string
}

Paths locate one user's daemon.

func DefaultPaths

func DefaultPaths() (Paths, error)

DefaultPaths are the daemon's state directory, $XDG_STATE_HOME/baum/daemon (default ~/.local/state/baum/daemon), and its socket in $XDG_RUNTIME_DIR/baum or, without one, in the state directory. A socket path too long for the platform moves to /tmp/baum-<uid>, whatever TMPDIR is; the daemon creates that directory private to the user and refuses one another user owns.

func (Paths) CredentialFile

func (p Paths) CredentialFile() string

CredentialFile is the private file holding the app transport's address and credential while a daemon runs.

func (Paths) LockFile

func (p Paths) LockFile() string

LockFile is the SQLite file whose write lock a running daemon holds. Only a daemon creates it; clients only look for it.

func (Paths) LogFile

func (p Paths) LogFile() string

LogFile is the daemon's log; older logs are LogFile()+".1" and ".2".

func (Paths) OutputFile

func (p Paths) OutputFile() string

OutputFile receives a detached daemon's standard output and error, which hold only what is written before its log opens or by a crash.

func (Paths) StoppedFile

func (p Paths) StoppedFile() string

StoppedFile marks a daemon the user stopped (`baum daemon stop`, which the desktop app's "Quit and stop background reviews" runs). Hooks do not start a daemon while it exists; Ensure and a foreground run remove it.

type Request

type Request struct {
	Protocol int             `json:"protocol"`
	Method   string          `json:"method"`
	Client   string          `json:"client,omitempty"`
	Params   json.RawMessage `json:"params,omitempty"`
}

The control protocol: a client connects to the socket and writes one JSON request per line; the daemon answers each with one JSON response line, in order. A line is at most maxLine bytes. Every request names the protocol the client speaks.

  • "hello" (any protocol) answers with the daemon's Status: its protocol, version, pid and start time. Clients use it to decide whether to keep, replace or refuse the daemon.
  • "stop" (any protocol) asks the daemon to stop gracefully; it answers before it begins.
  • "ping" answers ok.
  • "event" records one coding-agent or Git hook event (internal/hookevents).

Every other method requires the client's protocol to equal the daemon's; otherwise the answer is the error code "protocol_mismatch" together with the daemon's Status. Methods are registered through Options.Methods and App.Methods, and those that may wait for as long as their client stays connected through Options.Waits and App.Waits.

type Response

type Response struct {
	OK     bool            `json:"ok"`
	Status *Status         `json:"status,omitempty"`
	Result json.RawMessage `json:"result,omitempty"`
	Error  *Error          `json:"error,omitempty"`
	// contains filtered or unexported fields
}

Response answers one Request. Status is set for "hello", "stop" and a protocol mismatch.

type RotatingFile

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

RotatingFile is a private, size-bounded log file.

func OpenLog

func OpenLog(path string, max int64, keep int) (*RotatingFile, error)

OpenLog opens path for appending, with mode 0600, rotating it at max bytes and keeping keep older files (path.1 newest).

func (*RotatingFile) Close

func (r *RotatingFile) Close() error

Close closes the file.

func (*RotatingFile) Write

func (r *RotatingFile) Write(p []byte) (int, error)

type Spawner

type Spawner func(paths Paths, start StartOptions) (done <-chan struct{}, err error)

Spawner starts a detached daemon process. done is closed when the process exits (it never is for a daemon that keeps running).

type StartOptions

type StartOptions struct {
	Port int
	// Env, when not nil, is the environment the daemon process starts with;
	// nil passes the caller's.
	Env []string
}

StartOptions apply only when a client starts a daemon.

type Status

type Status struct {
	Protocol  int       `json:"protocol"`
	Version   string    `json:"version"`
	PID       int       `json:"pid"`
	StartedAt time.Time `json:"started_at"`
	// App is the loopback origin of the app transport.
	App    string `json:"app,omitempty"`
	Socket string `json:"socket,omitempty"`
	State  string `json:"state,omitempty"`
	// Stopping is set once the daemon has been asked to stop.
	Stopping bool `json:"stopping,omitempty"`
	// RestartPending is set once a newer client asked the daemon to restart
	// as its version when no job is running; until then no queued job
	// starts, and the queued ones wait for the new daemon.
	RestartPending bool `json:"restart_pending,omitempty"`
}

Status describes a running daemon. It never holds the credential.

func Self

func Self() (Status, bool)

Self returns the status of the daemon running in this process. Code that runs inside the daemon (its own diagnostics) uses it rather than probing the socket or the lock.

func (Status) Uptime

func (s Status) Uptime(now time.Time) time.Duration

Uptime is how long the daemon has run, to the second.

type StopParams

type StopParams struct {
	// WhenIdle asks the daemon to restart as Executable once no job is
	// running, starting no queued job meanwhile (the new daemon runs them);
	// a daemon with no job queued or running stops at once.
	WhenIdle   bool   `json:"when_idle,omitempty"`
	Executable string `json:"executable,omitempty"`
}

StopParams are the optional parameters of "stop".

Jump to

Keyboard shortcuts

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