Documentation
¶
Overview ¶
Package watchd implements the chunk watch background daemon and its client.
Index ¶
- Constants
- Variables
- func BuildID() string
- func EnsureDir() (string, error)
- func EnsureLaunched(subArgs []string) error
- func EnsureRunning(subArgs []string) error
- func IsDaemonCompatible() bool
- func IsDaemonRunning() bool
- func IsRunning(path string) (bool, int, error)
- func LogPath() (string, error)
- func PIDPath() (string, error)
- func RegisterCommand(reg CommandReg)
- func RunDaemon(ctx context.Context, client *circleci.Client, authMessage string, ...) error
- func SocketPath() (string, error)
- func StopForCredentialChange()
- type CommandReg
- type CommandState
- type OutputChunk
- type ProjectSnapshot
- type Resources
- type SidecarState
- type Snapshot
- type ValidateRequest
- type ValidateResponse
- type ValidateRunner
Constants ¶
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 )
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 )
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 ¶
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 EnsureLaunched ¶ added in v0.7.164
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 ¶
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 IsDaemonCompatible ¶ added in v0.7.175
func IsDaemonCompatible() bool
IsDaemonCompatible reports whether the watch daemon is reachable and running the same build as the current process. A daemon from a different build may not support all API endpoints (e.g. /validate), so delegation should be skipped and the operation run inline instead.
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 ¶
IsRunning reports whether the process whose PID is stored in path is alive. Returns (false, 0, nil) when the file doesn't exist.
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 ¶
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 ¶
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.