daemon

package
v0.158.0 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package daemon owns the durable local lifecycle record for the WB daemon.

The record deliberately contains scheduler ownership, rather than command output, so a later ConnectRPC/gRPC transport and the MCP adapter can use the same queue handoff contract without reading a dashboard-only file.

Package daemon owns WB's durable local operation queue.

Index

Constants

View Source
const (
	// StateSchemaVersion is the version this build writes. Load accepts every
	// version in SupportedStateSchemaVersions: a running daemon's record must
	// stay readable across an upgrade, and refusing an older record would make
	// the daemon that wrote it look absent.
	StateSchemaVersion      = 2
	MinSupportedStateSchema = 1
	QueueSchemaVersion      = 1
)
View Source
const (
	ProtocolVersion = 1
	QueueSchema     = 1
)
View Source
const RawExecutionPolicyVersion = 1
View Source
const RuntimeDirName = "runtime"

RuntimeDirName is the directory inside WB's home that holds daemon runtime state: the local socket, the lifecycle record, and the daemon's log.

View Source
const SocketFileName = "daemon.sock"

SocketFileName is the local endpoint's name inside the runtime directory.

View Source
const StateFileName = "daemon-state.json"

StateFileName is the durable lifecycle record inside the runtime directory.

Variables

This section is empty.

Functions

func LegacyRuntimeDir added in v0.134.0

func LegacyRuntimeDir(projectsRoot string) string

LegacyRuntimeDirName reproduces the runtime directory WB used before the daemon consulted the home resolver: a literal ".wb" beneath the projects root. It exists so a daemon left there can be *detected* and reported rather than silently doubled. Nothing in this package writes to it.

It is spelled out here, rather than reusing wbhome, precisely because it is the shape that must not be produced again: wbhome resolves a home, and this is a fixed historical path.

func LegacyStatePath added in v0.134.0

func LegacyStatePath(projectsRoot string) string

LegacyStatePath is the lifecycle record a daemon wrote before the runtime directory followed WB's home. It exists for detection only; nothing here reads it as this build's own state.

func LoadRawExecutionPolicy added in v0.105.0

func LoadRawExecutionPolicy(path, projectsRoot string) (bool, error)

LoadRawExecutionPolicy enables raw subprocesses only through a protected, explicit administrator opt-in outside the agent-writable projects tree.

func ObservedCgroupUnit added in v0.150.0

func ObservedCgroupUnit(pid int) (string, bool)

ObservedCgroupUnit independently observes the raw systemd unit name (if any) pid is currently running inside, regardless of what unit a caller expects. It is what lets a daemon record its OWN actual unit at `daemon serve` startup (sneat-dev/wb#622 review round 3, item M3), rather than a later, separate `wb daemon status` invocation guessing from its own environment.

func OperationsDir added in v0.134.0

func OperationsDir(projectsRoot string) (string, error)

OperationsDir is the daemon's durable operation store.

It is derived from the same home as every other runtime artefact, and it is passed to NewService explicitly rather than resolved inside it: a constructor that reads the environment makes every caller share one store, which is both untestable in isolation and wrong for a library.

func ParseCgroupUnit added in v0.150.0

func ParseCgroupUnit(contents string) (unit string, known bool)

ParseCgroupUnit is ParseCgroupSupervisor's raw half: it returns the unit-shaped last path component of the unified (cgroup v2 "0::") hierarchy line, WITHOUT comparing it against any expected name — so a caller that wants to know "what unit, if any, is this process actually in" (a daemon recording its OWN unit at serve startup, sneat-dev/wb#622 review round 3 item M3) does not have to already know the name it is looking for, the way ParseCgroupSupervisor's caller must.

known=false only when there is no readable cgroup evidence at all (empty input, or no "0::" line). known=true with unit="" means the process IS in a cgroup, just not one shaped like a specific service unit: a component that is not a service at all (an app.slice, a *.scope — the shape a session scope, or a process merely reparented to PID 1, runs inside), or the per-user manager's own unit (user@<uid>.service, which wraps every process in the login session and confirms nothing about any specific unit).

func ParseProcStatBootTime added in v0.134.0

func ParseProcStatBootTime(contents string) (time.Time, bool)

ParseProcStatBootTime extracts the btime (boot time, in Unix seconds) from the contents of /proc/stat.

func ParseProcStatStartTicks added in v0.134.0

func ParseProcStatStartTicks(contents string) (uint64, bool)

ParseProcStatStartTicks extracts the process start time from the contents of a Linux /proc/<pid>/stat file, in clock ticks since boot.

The comm field is wrapped in parentheses and may itself contain spaces and parentheses, so the fields are counted from the last ')' rather than by splitting the whole line: a process named "my (odd) name" must not shift every field after it. Start time is field 22 of the record, which is the twentieth field after the state field that follows comm.

func ProcessStartFromProcStat added in v0.134.0

func ProcessStartFromProcStat(ticks uint64, bootTime time.Time) time.Time

ProcessStartFromProcStat converts a /proc start-tick count and the system boot time into an absolute start time.

func ProcessStartTime added in v0.134.0

func ProcessStartTime(pid int) (time.Time, bool)

ProcessStartTime observes when pid started, so a recorded PID can be checked against the process now holding it. It reports false when the platform cannot answer, which callers must surface as unknown rather than as a match.

func RawExecutionPolicyPath added in v0.105.0

func RawExecutionPolicyPath() (string, error)

func RequireRawExecutionPolicy added in v0.105.0

func RequireRawExecutionPolicy(path, projectsRoot string) error

RequireRawExecutionPolicy re-reads and validates the protected host policy. Callers must invoke it at every execution boundary so revocation is immediate.

func RuntimeDir added in v0.134.0

func RuntimeDir(projectsRoot string) (string, error)

RuntimeDir resolves the daemon's runtime directory through WB's one home resolver, so a WB_HOME move moves the daemon with every other subsystem.

This is the whole point of the function: the daemon previously built its runtime path by joining the projects root with a literal ".wb", which meant that when WB_HOME moved — including the symlink-then-revert migration of 2026-09-15 — every subsystem followed and the daemon stayed behind, still serving a socket inside a directory the rest of WB had abandoned.

func SocketPath added in v0.134.0

func SocketPath(projectsRoot string) (string, error)

SocketPath is the home-derived local endpoint of the daemon.

The path is also an identity: unlike the shared loopback port it names the one home whose daemon may own it, which is why status reports it rather than inferring ownership from whoever answers on the port.

func StatePath added in v0.134.0

func StatePath(projectsRoot string) (string, error)

StatePath is the runtime directory's lifecycle record.

func SupportedStateSchemaVersions added in v0.134.0

func SupportedStateSchemaVersions() []int

SupportedStateSchemaVersions lists the record versions this build can read.

func SystemdUnitLooksOrphaned added in v0.150.0

func SystemdUnitLooksOrphaned(state SystemdUnitState) bool

SystemdUnitLooksOrphaned reports whether a queried unit's state is evidence that it still exists and is fighting an unsupervised process for ownership of the daemon it is meant to run: failed outright, or stuck restarting after at least one failure. A unit that is simply "inactive" (never started, or cleanly stopped) or "active" (running normally — the unit itself, not necessarily the process this build is inspecting) is not evidence of anything wrong.

Types

type Provenance

type Provenance struct {
	Executable string `json:"executable"`
	SHA256     string `json:"sha256"`
	Version    string `json:"version"`
	Revision   string `json:"revision,omitempty"`
	Built      string `json:"built,omitempty"`
}

Provenance identifies the exact executable trusted to own a daemon generation. SHA256 is intentionally included even when the released version is known: a development binary can otherwise look identical to a release.

func ProvenanceForExecutable

func ProvenanceForExecutable(executable, version, revision, built string) (Provenance, error)

ProvenanceForExecutable produces exact local evidence for a running binary.

func (Provenance) SameBinary

func (p Provenance) SameBinary(other Provenance) bool

type Queue

type Queue struct {
	SchemaVersion int         `json:"schema_version"`
	Generation    uint64      `json:"generation"`
	Owner         Provenance  `json:"owner"`
	OwnerToken    string      `json:"owner_token"`
	HandoffFrom   *Provenance `json:"handoff_from,omitempty"`
	HandoffAt     *time.Time  `json:"handoff_at,omitempty"`
}

Queue describes durable queue ownership. Operations are added by the async scheduler lane; lifecycle code preserves this object byte-for-byte apart from a fenced owner/generation transition.

type Service added in v0.105.0

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

Service implements the generated ConnectRPC service and persists every state transition before it is returned to the caller.

func NewService added in v0.105.0

func NewService(projectsRoot, operationsDirectory, build, generation string, authorizeRaw func() error) (*Service, error)

NewService opens the durable operation store at operationsDirectory for the projects root the daemon serves.

The store's location is an argument rather than something NewService resolves from the environment: the daemon's runtime directory follows WB's home (see RuntimeDir and OperationsDir), and a constructor that looked that up itself would silently put every caller — including every test in this package — on one shared store.

func (*Service) CancelOperation added in v0.105.0

func (*Service) CompleteOperation added in v0.106.0

func (*Service) DisconnectWorker added in v0.106.0

func (*Service) GetDaemonInfo added in v0.105.0

func (*Service) GetOperation added in v0.105.0

func (*Service) HeartbeatOperation added in v0.106.0

func (*Service) LeaseOperation added in v0.106.0

func (*Service) RegisterWorker added in v0.106.0

func (*Service) StartLeaseRecovery added in v0.106.0

func (service *Service) StartLeaseRecovery(ctx context.Context)

StartLeaseRecovery watches running worker leases even when no client is polling an operation. The daemon owns only this recovery clock; execution remains in the connected worker process.

func (*Service) SubmitOperation added in v0.105.0

func (*Service) WaitOperation added in v0.105.0

type State

type State struct {
	SchemaVersion int        `json:"schema_version"`
	Status        Status     `json:"status"`
	PID           int        `json:"pid,omitempty"`
	OwnerToken    string     `json:"owner_token,omitempty"`
	Listen        string     `json:"listen"`
	Provenance    Provenance `json:"provenance"`
	Queue         Queue      `json:"queue"`
	StartedAt     time.Time  `json:"started_at,omitempty"`
	UpdatedAt     time.Time  `json:"updated_at"`

	// WBHome and StatePath record *where* this daemon lives, so a reader can
	// tell "the daemon belonging to my home" from "a daemon answering on my
	// endpoint". They are empty in records written before schema 2, which is
	// reported as unknown home identity rather than assumed to match.
	WBHome    string `json:"wb_home,omitempty"`
	StatePath string `json:"state_path,omitempty"`

	// ProcessStartedAt is the recorded process's start time. PID alone is a
	// liveness coordinate that a recycled number can satisfy; the start time is
	// what makes "still running" a claim about *this* process. Zero means the
	// platform could not observe it, which is reported as unknown rather than
	// treated as a match.
	ProcessStartedAt time.Time `json:"process_started_at,omitempty"`

	// StoppedReason explains a stop the daemon did not choose — a runtime
	// directory removed underneath it, or an endpoint it could not bind. It is
	// recorded so the condition survives the process that hit it: a supervisor
	// restarts the daemon, and without this the reason would exist only in a
	// log the same removal may have unlinked.
	StoppedReason string `json:"stopped_reason,omitempty"`

	// Supervisor and SupervisorExecPID are what the process itself observed
	// about its own start, through DetectSupervisor. They are empty in records
	// written before this field existed and in a record whose process was
	// never observed to start under a supervisor at all — both read as
	// SupervisorNone by a reporter, since neither can name an owner to hand a
	// restart to (sneat-dev/wb#617, sneat-dev/wb#546).
	Supervisor        Supervisor `json:"supervisor,omitempty"`
	SupervisorExecPID string     `json:"supervisor_exec_pid,omitempty"`

	// SupervisorLabel is the supervisor's own identity for the job it started,
	// when DetectSupervisor could read one — currently only launchd's job
	// label (from XPC_SERVICE_NAME). It is what lets a reader tell wb's own
	// self-managed launchd job (cmd/wb's daemonLaunchdLabel, which wb already
	// knows how to re-bootstrap with a new binary) from a foreign one wb must
	// hand off to instead of touching directly (sneat-dev/wb#622 review item 1).
	SupervisorLabel string `json:"supervisor_label,omitempty"`

	// SystemdUnit is the systemd unit name this process observed ITSELF
	// running inside, from its own /proc/self/cgroup, at `daemon serve`
	// startup -- independent of any config a LATER, separate `wb daemon
	// status` invocation's own environment happens to carry. A later reader
	// checking this daemon's unit health MUST prefer this field over its own
	// configured/default unit name: unit identity read from the status
	// invoker's own environment gives a false negative for a daemon
	// correctly supervised under a unit name the status invocation was never
	// told about (sneat-dev/wb#622 review round 3, item M3). Empty when this
	// process is not in a systemd service unit's own cgroup at all.
	SystemdUnit string `json:"systemd_unit,omitempty"`
}

State is private local state. It is never served by the dashboard API.

func NewStarting

func NewStarting(previous *State, listen string, provenance Provenance, ownerToken string, now time.Time) State

NewStarting creates the next fenced queue generation. Existing queue jobs stay in the durable queue file owned by the scheduler; this record tells the replacement scheduler exactly which binary owned the preceding generation.

func NewStartingAt added in v0.134.0

func NewStartingAt(previous *State, listen string, provenance Provenance, ownerToken, wbHome, statePath string, now time.Time) State

NewStartingAt is NewStarting with the daemon's own location recorded, so the generation it opens names the home it belongs to.

func (State) IsForeignHome added in v0.134.0

func (s State) IsForeignHome(currentHome string) (foreign bool, known bool)

IsForeignHome reports whether a record was written for a different WB home than the one this invocation resolves.

An empty recorded home means the record predates home identity. That is reported as unknown rather than as a match: claiming a record is ours because it failed to say otherwise is exactly how a daemon from an abandoned home gets presented as this machine's daemon.

func (*State) MarkDraining

func (s *State) MarkDraining(now time.Time)

func (*State) MarkReady

func (s *State) MarkReady(pid int, now time.Time)

func (*State) MarkReadyWithProcess added in v0.134.0

func (s *State) MarkReadyWithProcess(pid int, processStartedAt time.Time, now time.Time)

MarkReadyWithProcess records the process generation that is now serving. A zero processStartedAt records that the platform could not observe one.

func (*State) MarkStartingPID added in v0.127.1

func (s *State) MarkStartingPID(pid int, now time.Time)

func (*State) MarkStopped

func (s *State) MarkStopped(now time.Time)

func (*State) MarkStoppedWithReason added in v0.134.0

func (s *State) MarkStoppedWithReason(reason string, now time.Time)

MarkStoppedWithReason records a stop the daemon did not choose, keeping the reason readable after the process is gone.

func (State) ProcessGenerationMatches added in v0.134.0

func (s State) ProcessGenerationMatches(observedStart time.Time, observed bool) (match bool, known bool)

ProcessGenerationMatches reports whether the process now holding the recorded PID is the generation this record was written for.

The second result is whether the comparison was possible at all. A record with no recorded start time, or a platform that cannot observe one, yields known=false and leaves the caller to fall back to PID liveness and to say that it did. A recycled PID that *is* observable and differs is reported as a mismatch, because a confident "still running" about someone else's process is worse than an admitted unknown.

func (State) ReportedSupervisor added in v0.150.0

func (s State) ReportedSupervisor() Supervisor

ReportedSupervisor is Supervisor normalized for a reader: an empty or otherwise unrecognized recorded value — a record written before this field existed, most notably — is reported as SupervisorNone rather than as whatever raw string happens to be on disk, because "no supervisor known" is exactly the state that must never launch a detached replacement believing something else owns the process.

func (State) Valid

func (s State) Valid() error

type Status

type Status string
const (
	StatusStarting Status = "starting"
	StatusReady    Status = "ready"
	StatusDraining Status = "draining"
	StatusStopped  Status = "stopped"
)

type Store

type Store struct{ Path string }

Store reads and atomically replaces the local state record. Its directory and file permissions keep operation/queue metadata per-user on Unix; the Windows service adapter will use the current-user application-data ACL.

func (Store) Load

func (s Store) Load() (State, bool, error)

func (Store) Save

func (s Store) Save(state State) error

type Supervisor added in v0.150.0

type Supervisor string

Supervisor names what owns a running daemon process's lifecycle: whichever process manager exec'd it and will restart it if it exits.

Recording this at `daemon serve` startup, rather than inferring it later, is what lets a restart hand the process back to its owner instead of starting a detached replacement behind its back (sneat-dev/wb#617): the process that knows it is systemd's or launchd's child is the process being asked to restart, and no later reader can observe that as reliably as it can.

const (
	// SupervisorNone: nothing is recorded as having supervised this process's
	// start. A restart of a daemon in this state has nothing to hand off to,
	// so it must start its own replacement.
	SupervisorNone Supervisor = "none"
	// SupervisorSystemd: a systemd (user or system) unit started this process.
	SupervisorSystemd Supervisor = "systemd"
	// SupervisorLaunchd: a launchd agent or daemon started this process.
	SupervisorLaunchd Supervisor = "launchd"
)

func DetectSupervisor added in v0.150.0

func DetectSupervisor(getenv func(string) string, pid, ppid int) (kind Supervisor, execPID string, label string)

DetectSupervisor observes the environment variables systemd and launchd are documented to set on a process they exec directly, through an injectable seam so no test needs a real systemd or launchd. pid and ppid are this process's own — normally os.Getpid() and os.Getppid(), overridden by tests — because a bare environment variable is not enough evidence on its own: both variables are observed to survive into children that inherit their parent's environment without being started by the supervisor at all.

  • systemd sets INVOCATION_ID (systemd.exec(5)) on every unit it starts, and since systemd 248 also sets SYSTEMD_EXEC_PID to the exact PID it exec'd. Confirmed on a live host: a shell or an agent process started *inside* a systemd-supervised session inherits INVOCATION_ID from its parent without SYSTEMD_EXEC_PID ever being set for it. Detection therefore requires SYSTEMD_EXEC_PID to be present *and* equal to this process's own pid; INVOCATION_ID alone, or a non-matching SYSTEMD_EXEC_PID, reports None.
  • launchd sets XPC_SERVICE_NAME to the job label for a job it manages, and to the literal string "0" for a process it did not launch directly (for example a login shell) — "0" is therefore treated as absent, not launchd. launchd is additionally required to be this process's *direct* parent (ppid == 1, which is launchd on every macOS version this targets) as the equivalent inheritance guard: a child of a launchd-managed process can otherwise inherit XPC_SERVICE_NAME from its parent's environment without having been launched by launchd itself. A label prefixed "application." is also treated as absent regardless of parentage: that is macOS's own label for an ordinary foreground GUI application (an IDE, a terminal app), not a launch agent — a `daemon serve` run from an IDE's integrated terminal must never be mistaken for one wb should try to kickstart or refuse under (sneat-dev/wb#622 review item 6).

label is the launchd job label from XPC_SERVICE_NAME (empty for systemd and for None): callers use it to tell wb's own self-managed launchd job from a foreign one, which needs different handoff handling (see cmd/wb's daemonLaunchdLabel).

func ObservedCgroupSupervisor added in v0.150.0

func ObservedCgroupSupervisor(pid int, expectedUnit string) (Supervisor, bool)

ObservedCgroupSupervisor independently observes whether pid is currently running inside expectedUnit's own cgroup, so `wb daemon status` can compare it against what that process recorded about its own start (State.Supervisor). See ParseCgroupSupervisor for why cgroup membership, not parentage, is what is checked, and why expectedUnit — not a bare ".service" substring match — is required.

func ParseCgroupSupervisor added in v0.150.0

func ParseCgroupSupervisor(contents string, expectedUnit string) (Supervisor, bool)

ParseCgroupSupervisor is the pure half of ObservedCgroupSupervisor: it classifies the contents of a Linux `/proc/<pid>/cgroup` file against a specific expected systemd unit name (see cmd/wb's daemonSystemdUnitName).

Only the unified (cgroup v2) "0::" hierarchy line is considered — a v1/hybrid host's legacy per-controller lines are not authoritative for which service actually owns the process. The unit is identified by the LAST path component of that line: a system unit's cgroup path is `.../system.slice/wb-daemon.service`, and a user unit's is `.../user@1000.service/app.slice/wb-daemon.service`.

A plain substring match on ".service" anywhere in the path — this function's first version — flagged ANY process running under the user's systemd session, including one under a completely unrelated foreign unit, as "supervised" (sneat-dev/wb#622 review item 4). This version instead:

  • excludes the per-user manager's own unit (user@<uid>.service), which wraps every process in the session and confirms nothing about any specific unit;
  • reports SupervisorNone for a component that is not a service at all (an app.slice, a *.scope — for example a session scope, which is what a process merely reparented to PID 1 after its original parent exited runs inside, never a `.service`: this is exactly the false positive a parent-PID check could not avoid, sneat-dev/wb#622 review item 7 from the previous round);
  • reports SupervisorNone for a service unit that IS a service but is not the expected one (a foreign unit such as openclaw-gateway.service) — it is not evidence that OUR unit is managing this process, so it is treated the same as no service membership at all.

func (Supervisor) Valid added in v0.150.0

func (kind Supervisor) Valid() bool

Valid reports whether kind is one of the three reportable values. It exists so a record read back from disk — including one written by a future build with a supervisor kind this build does not know — is never silently treated as SupervisorNone.

type SystemdUnitState added in v0.150.0

type SystemdUnitState struct {
	ActiveState string
	Result      string
	NRestarts   int
}

SystemdUnitState is the subset of `systemctl show` fields relevant to telling a genuinely-owned, healthy systemd unit from one that exists but is failing to keep the daemon up. This is the sneat-dev/wb#617 detector's central evidence: a live host was confirmed running an orphaned, unsupervised `daemon serve` (PPID 1, a session scope — not the unit's own cgroup, so ObservedCgroupSupervisor cannot see it) answering the port, while its own systemd unit sat ActiveState=failed with NRestarts=4468 — systemd itself was crash-looping trying and failing to keep the real daemon up, and the orphan was serving the port instead (sneat-dev/wb#622 review item 2).

func ParseSystemctlShow added in v0.150.0

func ParseSystemctlShow(output string) (SystemdUnitState, bool)

ParseSystemctlShow parses `systemctl show -p ActiveState,Result,NRestarts <unit>` output (newline-separated Key=Value pairs) into a SystemdUnitState. found=false for empty input — systemctl being entirely absent, or its user manager unreachable, both look like this from the caller's side, and neither is evidence that the unit is (or is not) healthy.

Jump to

Keyboard shortcuts

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