watchd

package
v0.7.174 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: MIT Imports: 29 Imported by: 0

Documentation

Overview

Package watchd implements the chunk watch background daemon and its client.

Index

Constants

View Source
const (
	// PollInterval is how often the daemon refreshes project state from disk.
	PollInterval = 5 * time.Second
	// RecentEvents is the maximum number of events kept per project.
	RecentEvents = 300
	// RunningTimeout is how long after the last non-terminal event a sidecar is
	// considered to still be running.
	RunningTimeout = 5 * time.Minute
)
View Source
const (
	// MaxCommandBytes caps retained output per command. Output is capped in bytes
	// rather than lines because a verbose test suite emits megabytes in seconds
	// and a line count says nothing about memory. The tail is what survives: the
	// end of a failed run is the part anyone wants to read.
	MaxCommandBytes = 256 << 10

	// MaxCommands caps retained commands per project. Only finished commands are
	// evicted, so a project running more than this many at once keeps them all.
	MaxCommands = 20
)
View Source
const (
	// SampleInterval is how often the remote sampler emits a reading.
	SampleInterval = 2 * time.Second

	// StaleSamples is how many intervals a sample may age before the dashboard
	// should treat it as stale. A sampler that dies must look stalled rather than
	// look like an idle sidecar, so the last value is kept and marked, not
	// discarded.
	//
	// The budget has to cover more than SampleInterval: the reading crosses an SSH
	// connection before the daemon sees it, and the dashboard then renders that
	// same snapshot until its next 5s poll while re-evaluating the age every
	// frame. Measured against real sidecars, a healthy sampler reaches ~10s of
	// apparent age just before a poll lands, so a tighter bound flags working
	// samplers as stale for part of every cycle. Twelve seconds clears that and
	// still catches a dead sampler within two polls.
	StaleSamples = 6
)

Variables

View Source
var ErrDaemonUnavailable = errors.New("daemon unavailable")

ErrDaemonUnavailable is returned by RunValidate when the daemon socket is unreachable, so callers can distinguish a transient connectivity failure from a real validation error and fall back to inline execution.

Functions

func BuildID added in v0.7.164

func BuildID() string

BuildID identifies the binary a process was started from.

The daemon serves snapshots shaped by the code it was started from: a field added to SidecarState since then is absent rather than wrong, so a newer client renders a well-formed view of stale data with nothing to say why. A sidecar owned by a session, for instance, arrives from a pre-session daemon looking like a sidecar nobody owns. Comparing this on every ping is what makes that visible instead of silent.

The version alone will not do: every local build reports the same development version, so the executable's path, size and modification time come along to tell two of them apart. Path is included so a dev build and an installed one are never mistaken for each other.

func EnsureDir

func EnsureDir() (string, error)

EnsureDir creates ~/.chunk/watchd/ if it doesn't exist and returns the path.

func EnsureLaunched added in v0.7.164

func EnsureLaunched(subArgs []string) error

EnsureLaunched starts the daemon when nothing is answering and otherwise leaves whatever is there alone.

Unlike EnsureRunning it never replaces a daemon from another build. It is called when a poll fails mid-session, and a dashboard that has been open for a while has no business restarting a daemon another one is using: the build check is a startup decision, made once, where the cost of being wrong is one restart rather than a restart per poll for as long as two dashboards are open.

func EnsureRunning

func EnsureRunning(subArgs []string) error

EnsureRunning checks whether the watch daemon is running and serving, and launches it if not. subArgs are the CLI arguments used to invoke the daemon (e.g. ["watch", "_daemon"]).

func IsDaemonRunning added in v0.7.174

func IsDaemonRunning() bool

IsDaemonRunning reports whether the watch daemon is reachable and was built from the same binary as the caller. A daemon from an older build is treated as absent: it may not serve routes added since it was compiled.

func IsRunning

func IsRunning(path string) (bool, int, error)

IsRunning reports whether the process whose PID is stored in path is alive. Returns (false, 0, nil) when the file doesn't exist.

func LogPath

func LogPath() (string, error)

LogPath returns the path to the daemon log file.

func PIDPath

func PIDPath() (string, error)

PIDPath returns the path to the daemon PID file.

func RegisterCommand added in v0.7.173

func RegisterCommand(reg CommandReg)

RegisterCommand tells the running watch daemon to stream and buffer a command's output.

It is best-effort by design and reports no error. If the daemon is not running, the command still runs and still streams to the caller's own stdout; the only thing lost is the buffered copy. Notably this does not start the daemon: spawning a background process as a side effect of a hook firing is intrusive, and a hook that hangs waiting for a daemon launch is a far worse failure than a missing logs pane.

func RunDaemon

func RunDaemon(ctx context.Context, client *circleci.Client, authMessage string, runner ValidateRunner) error

RunDaemon is the watch daemon entry point, called by the hidden _daemon subcommand.

client and authMessage support the output-buffering feature; runner is called in-process to handle /validate requests. Both client and runner may be nil (the daemon still records commands without a client, and /validate returns an error without a runner).

func SocketPath

func SocketPath() (string, error)

SocketPath returns the path to the daemon Unix socket.

func StopForCredentialChange added in v0.7.173

func StopForCredentialChange()

StopForCredentialChange stops a running watch daemon so that the next launch picks up newly stored credentials.

The daemon resolves its CircleCI client once, at startup, so one that started before a login holds a nil client for the rest of its life and streams no output however many times the developer retries. Stopping it here is what makes `chunk auth login` take effect: a `chunk watch` already on screen relaunches it through EnsureLaunched on its next poll, and otherwise the next `chunk watch` starts a daemon that can authenticate.

Best-effort and silent, like RegisterCommand. Failing to stop the daemon must not fail a login that has otherwise succeeded, and the cost of not stopping it is the buffered output of a daemon that was not streaming anything anyway.

Types

type CommandReg added in v0.7.173

type CommandReg struct {
	CommandID   string    `json:"command_id"`
	SidecarID   string    `json:"sidecar_id"`
	ProjectRoot string    `json:"project_root"`
	Op          string    `json:"op"`
	Name        string    `json:"name"`
	SubmittedAt time.Time `json:"submitted_at"`
}

CommandReg is the registration a process sends after submitting a remote command, so the daemon can stream and buffer that command's output. The submitting process may exit immediately afterwards — that is the whole point, since most remote commands are run by a hook that exits as soon as the command finishes.

type CommandState added in v0.7.173

type CommandState struct {
	CommandID   string     `json:"command_id"`
	SidecarID   string     `json:"sidecar_id"`
	Op          string     `json:"op"`
	Name        string     `json:"name"`
	SubmittedAt time.Time  `json:"submitted_at"`
	EndedAt     *time.Time `json:"ended_at,omitempty"`
	ExitCode    *int       `json:"exit_code,omitempty"`
	Running     bool       `json:"running"`
	Bytes       int64      `json:"bytes"`
	Truncated   bool       `json:"truncated"`
}

CommandState describes one remote command the daemon is buffering output for.

type OutputChunk added in v0.7.173

type OutputChunk struct {
	// Data is raw command output, exactly as the remote command wrote it —
	// interleaved stdout and stderr, ANSI and carriage returns intact.
	Data []byte `json:"data"`
	// NextOffset is the offset to pass on the following read.
	NextOffset int64 `json:"next_offset"`
	Running    bool  `json:"running"`
	ExitCode   *int  `json:"exit_code,omitempty"`
	// Truncated reports that output before the returned data was evicted and is
	// gone. Saying so is the difference between showing a partial run and
	// showing a partial run that looks whole.
	Truncated bool `json:"truncated"`
	// Found is false when the daemon knows nothing about the command.
	Found bool `json:"found"`
	// Error explains why streaming stopped early, when it did. Without it a
	// failed stream is indistinguishable from a command that produced no output,
	// which sends the reader looking for a bug in their own command.
	Error string `json:"error,omitempty"`
}

OutputChunk is one response to an output read.

func FetchOutput added in v0.7.173

func FetchOutput(commandID string, offset int64) (OutputChunk, error)

FetchOutput reads buffered output for a command starting at offset.

type ProjectSnapshot

type ProjectSnapshot struct {
	Root     string           `json:"root"`
	Branch   string           `json:"branch"`
	HeadRef  string           `json:"head_ref"`
	RepoName string           `json:"repo_name"`
	Sidecars []SidecarState   `json:"sidecars"`
	Events   []eventlog.Event `json:"events"`
	Commands []CommandState   `json:"commands,omitempty"`
}

ProjectSnapshot is the daemon's view of one project at a point in time.

type Resources added in v0.7.174

type Resources struct {
	CPUPercent     float64   `json:"cpu_percent"`
	MemUsedBytes   int64     `json:"mem_used_bytes"`
	MemLimitBytes  int64     `json:"mem_limit_bytes"`
	DiskUsedBytes  int64     `json:"disk_used_bytes"`
	DiskTotalBytes int64     `json:"disk_total_bytes"`
	SampledAt      time.Time `json:"sampled_at"`
}

Resources is one sample of a sidecar's resource usage.

Memory is reported as used-of-limit rather than a percentage so the display can show both, and because a limit of zero (unknown) has to be distinguishable from a usage of zero.

type SidecarState

type SidecarState struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	// SessionID is the agent session that owns this sidecar, empty for state
	// written outside a session or before sessions existed. Sidecars are
	// isolated per session, so two entries for one project and branch are two
	// sessions working in the same tree — this is what tells them apart.
	SessionID    string    `json:"session_id,omitempty"`
	ProjectName  string    `json:"project_name"`
	RepoName     string    `json:"repo_name"`
	SnapshotName string    `json:"snapshot_name"`
	FileMtime    time.Time `json:"file_mtime"`
	// Workspace is the sidecar-side repo path, used to sample disk usage where
	// the work actually happens rather than wherever a shell starts.
	Workspace    string      `json:"workspace,omitempty"`
	LastActivity time.Time   `json:"last_activity"`
	LastOp       eventlog.Op `json:"last_op"`
	LastLevel    string      `json:"last_level"`
	Running      bool        `json:"running"`
	// Resources is the most recent resource sample, or nil when none has
	// arrived — sampling only runs while a dashboard is attached.
	Resources *Resources `json:"resources,omitempty"`
}

SidecarState describes one active sidecar as maintained by the daemon.

type Snapshot

type Snapshot struct {
	Projects []ProjectSnapshot `json:"projects"`
	// AuthError explains why output streaming is unavailable, when it is. An
	// empty logs pane with no explanation sends people hunting the wrong fault,
	// so the daemon reports this rather than silently serving nothing.
	AuthError string `json:"auth_error,omitempty"`
}

Snapshot is a point-in-time view of all watched projects.

func FetchSnapshot

func FetchSnapshot(roots []string) (Snapshot, error)

FetchSnapshot connects to the running watch daemon and returns the current snapshot for the given project roots. If roots is empty all known projects are returned.

type ValidateRequest added in v0.7.174

type ValidateRequest struct {
	// Args is os.Args[1:] from the caller, e.g. ["validate", "test", "--remote"].
	Args []string `json:"args"`
	// CircleCIToken is forwarded to the subprocess as CIRCLE_TOKEN.
	CircleCIToken string `json:"circleci_token,omitempty"`
	// Env is the caller's os.Environ(), forwarded verbatim to the subprocess so
	// session-identity variables (e.g. CLAUDE_CODE_SESSION_ID) reach it intact.
	Env []string `json:"env,omitempty"`
}

ValidateRequest is the payload sent to POST /validate.

type ValidateResponse added in v0.7.174

type ValidateResponse struct {
	ExitCode int    `json:"exit_code"`
	Stdout   string `json:"stdout"`
	Stderr   string `json:"stderr"`
}

ValidateResponse is the response from POST /validate.

func RunValidate added in v0.7.174

func RunValidate(args []string, circleCIToken string) (ValidateResponse, error)

RunValidate delegates a validate run to the daemon. args is os.Args[1:]; circleCIToken is forwarded to the subprocess as CIRCLE_TOKEN.

type ValidateRunner added in v0.7.174

type ValidateRunner func(ctx context.Context, args []string, env []string, stdout, stderr io.Writer) int

ValidateRunner runs a validate command in-process. args is os.Args[1:] from the caller (e.g. ["validate", "test", "--remote"]); env is the caller's os.Environ(), which may differ from the daemon's own environment. stdout and stderr capture the command output. Returns the exit code.

Jump to

Keyboard shortcuts

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