capture

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package capture is instigator's opt-in install recorder. Enabled with serve --capture-dir, it writes a per-run bundle - run.json (provenance and media manifest), events.jsonl (the source of truth: one JSON object per BOOTP/TFTP/rsh/inst lifecycle event), and a derived summary.json - that becomes debugging data, performance numbers, and the command corpus for later CI replay.

The package is a standard-library-only leaf: it never imports config, vfs, or any protocol package, so bootp/tftp/instcmd/serve can all import it without a cycle. Provenance and event fields arrive as plain primitives assembled by the caller.

A *Recorder is nil-safe: every method is a no-op on a nil receiver, exactly like logging.Logger. That is the whole "disabled" path - when --capture-dir is unset the recorder is nil, no files are opened, and callers install none of the counting wrappers, so capture costs nothing.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Binary

type Binary struct {
	Version     string `json:"version,omitempty"`
	VCSRevision string `json:"vcs_revision,omitempty"`
	VCSDirty    bool   `json:"vcs_dirty"`
	GoVersion   string `json:"go_version"`
	SHA256      string `json:"sha256,omitempty"`
}

Binary identifies the running instigator build, so a capture can be tied back to exact source. Fields are best-effort: a build without VCS stamping or an unreadable executable leaves them empty rather than failing capture.

func BuildInfo

func BuildInfo() Binary

BuildInfo reports the running binary's identity from the Go build stamp plus a SHA-256 of the executable file. Everything is best-effort so it never blocks capture.

type Client

type Client struct {
	Alias string `json:"alias"`
	MAC   string `json:"mac"`
	IP    string `json:"ip"`
}

Client is one configured install client, by alias. The bundle also carries its MAC and IP: LAN identifiers, not secrets, but the reason the capture directory is private (0700).

type Command

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

Command is one wire line's recording context: its counters and the files it served, snapshotted into an inst_command_end event by End.

func (*Command) End

func (c *Command) End(exitStatus int, refused bool)

End emits inst_command_end with the command's counters and served files. refused marks a command this shell would not run (not whitelisted, a write attempt); a nonzero exitStatus without refusal is a command that ran and failed.

type ConfigInfo

type ConfigInfo struct {
	ServerIP string         `json:"server_ip"`
	Netmask  string         `json:"netmask"`
	Ports    map[string]int `json:"ports,omitempty"`
}

ConfigInfo is the redacted effective config: enough to reproduce the serving layout, none of it secret (the config carries no credentials).

type Media

type Media struct {
	Media string `json:"media"`
	Disc  string `json:"disc"`
	Image string `json:"image"`
	Size  int64  `json:"size"`
	Mtime string `json:"mtime"`
}

Media is one backing image in the served tree. Image is a basename, and there is deliberately no digest: hashing multi-GB ISOs at startup is out of scope (a separate manifest step can add hashes later). Size and Mtime are cheap identity that a later run can compare.

type Option

type Option func(*Recorder)

Option configures a Recorder at construction.

func WithSummaryWriter

func WithSummaryWriter(w io.Writer) Option

WithSummaryWriter directs the human summary Finish renders at shutdown to w instead of os.Stdout, so a test can capture it.

type PathReuse

type PathReuse struct {
	Path  string `json:"path"`
	Opens int    `json:"opens"`
	Bytes int64  `json:"bytes"`
}

PathReuse is how often one backing image:path was served and how many backing bytes its commands read - whether a bounded cache might pay off.

type Provenance

type Provenance struct {
	Schema   int        `json:"schema"`
	RunID    string     `json:"run_id"`
	Start    string     `json:"start"`
	End      string     `json:"end,omitempty"`
	Binary   Binary     `json:"binary"`
	Services Services   `json:"services"`
	Config   ConfigInfo `json:"config"`
	Clients  []Client   `json:"clients"`
	Media    []Media    `json:"media"`
}

Provenance is the run.json payload. serve assembles it (it alone knows config and the media tree) and hands it here as plain primitives; the recorder never imports config or vfs. Schema and RunID are filled by WriteRun.

type Recorder

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

Recorder writes a run's events to events.jsonl and owns the run identity and provenance. It is safe for concurrent use: TFTP serves each transfer in its own goroutine and multiple rsh sessions run at once, so every event write is serialized by mu. A nil *Recorder is a no-op.

func New

func New(dir string, opts ...Option) (*Recorder, error)

New creates the capture directory (0700) and opens events.jsonl (0600) for appending. The run id is generated once here and is stable for the life of the Recorder.

func (*Recorder) BeginSession

func (r *Recorder) BeginSession(alias, remoteAddr, remoteUser, localUser string) *Session

BeginSession emits rsh_session_start and returns the session's recording context. A nil Recorder yields a nil Session.

func (*Recorder) BootpReply

func (r *Recorder) BootpReply(alias, mac, file, offeredIP, result string)

BootpReply records the outcome of a BOOTP request: answered (a configured client was sent its reply), or ignored (a well-formed request from a MAC that is not configured). alias is the client's configured name, empty when unknown.

func (*Recorder) Close

func (r *Recorder) Close() error

Close flushes and closes events.jsonl. It is safe on a nil Recorder and idempotent enough for a deferred call.

func (*Recorder) Finish

func (r *Recorder) Finish(reason string) error

Finish closes the run at a clean shutdown: it emits server_stop, closes events.jsonl, rewrites run.json with the end time, then reads the events back to write summary.json and render the human summary. It must run after every session and transfer has ended; a late event from an in-flight goroutine is safely dropped once events.jsonl is closed.

An event dropped mid-run means the summary was computed from an incomplete file, so that error outranks anything that goes wrong here.

func (*Recorder) ListenerExit

func (r *Recorder) ListenerExit(name string)

ListenerExit records a protocol listener returning while the server was not shutting down, so a listener that died mid-run is visible in both the log and the trace.

func (*Recorder) RSHRejected

func (r *Recorder) RSHRejected(alias, remoteAddr, reason string)

RSHRejected records a connection rcmd refused before any session began - a non-reserved source port, or a client outside the allow list. It is a start/end pair with result "refused" so it shows in the session list as a connection that never became a session. reason is a short class.

func (*Recorder) ServerStart

func (r *Recorder) ServerStart()

ServerStart records that the server bound its listeners and began serving.

func (*Recorder) ServerStop

func (r *Recorder) ServerStop(reason string)

ServerStop records a clean shutdown. reason is the stop cause (today always "clean"); it rides in Result so the summary can tell a graceful stop from a future abnormal one.

func (*Recorder) TFTPTransferEnd

func (r *Recorder) TFTPTransferEnd(rec TransferRecord)

TFTPTransferEnd records a completed (or abandoned) TFTP transfer. There is no start event: a boot transfer is short, and a missing end for an interrupted one is not worth a second line here.

func (*Recorder) WriteRun

func (r *Recorder) WriteRun(p Provenance) error

WriteRun marshals the provenance to run.json (0600). It stamps the schema version and run id so the caller need not. The stored copy lets a clean shutdown rewrite run.json with the end time.

type Services

type Services struct {
	BOOTP         bool   `json:"bootp"`
	TFTP          bool   `json:"tftp"`
	RSH           bool   `json:"rsh"`
	TFTPPortRange [2]int `json:"tftp_port_range"`
}

Services records which listeners were enabled for the run.

type Session

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

Session is one rsh connection's recording context. It is created by BeginSession, hands out per-command counting wrappers, and is closed by End. A nil *Session is a no-op, so instcmd can hold one unconditionally.

The rsh shell runs one command at a time, but a single command's pipeline stages (cat a | fgrep b) execute in concurrent goroutines that both write the current command's counters - so the counters are atomic. cur is an atomic pointer because the session-level stdout wrapper reads it on every write while BeginCommand replaces it between commands.

func (*Session) BeginCommand

func (s *Session) BeginCommand(line string) *Command

BeginCommand emits inst_command_start and makes this command current, so the session's counting wrappers attribute to it. Every non-empty wire line is a command, including the marker wrapper.

func (*Session) End

func (s *Session) End(err error)

End emits rsh_session_end, classifying the outcome: a nil error is a clean close, a deadline is the idle timeout firing, anything else is an interrupted session (a lost connection mid-stream).

func (*Session) RecordServed

func (s *Session) RecordServed(treePath, image, imagePath string)

RecordServed notes a file a leaf command actually read, with the backing image and in-image path when the tree could resolve it. Safe to call from concurrent pipeline stages.

func (*Session) WrapReaderAt

func (s *Session) WrapReaderAt(r io.ReaderAt) io.ReaderAt

WrapReaderAt wraps a backing EFS file so its ReadAt calls and bytes are counted against the command current when the file was opened - which is the command that reads it, since these leaf commands open and consume a file within one line. Returns r unchanged on a nil session.

func (*Session) WrapWriter

func (s *Session) WrapWriter(w io.Writer, stderr bool) io.Writer

WrapWriter wraps a session output stream, both of which are bound to the rsh socket, so every write is counted against the session total and the current command. stderr selects which per-command counters it feeds, so dd's data (stdout) and its records summary (stderr) are told apart. Returns w unchanged on a nil session.

type SessionSummary

type SessionSummary struct {
	ID        string `json:"id"`
	Client    string `json:"client,omitempty"`
	Result    string `json:"result"`
	WallMS    int64  `json:"wall_ms"`
	ActiveMS  int64  `json:"active_ms"`
	IdleMS    int64  `json:"idle_ms"`
	Commands  int    `json:"commands"`
	BytesOut  int64  `json:"bytes_out"`
	ActiveBps int64  `json:"active_bps"` // bytes_out over active command time: the server-path rate
	WallBps   int64  `json:"wall_bps"`   // bytes_out over wall time: end-to-end, including client think time
}

SessionSummary is one rsh session's timing. Active is the sum of its command durations, idle is the rest of its wall time - the client thinking and the round trips, not server work.

type SlowCommand

type SlowCommand struct {
	Session    string `json:"session"`
	Seq        int    `json:"seq"`
	Verb       string `json:"verb"`
	DurationMS int64  `json:"duration_ms"`
	Path       string `json:"path,omitempty"`
}

SlowCommand names one slow command so it can be found in the serial transcript by session and sequence.

type Summary

type Summary struct {
	Sessions []SessionSummary `json:"sessions"`

	BytesServed      int64 `json:"bytes_served"`
	Commands         int   `json:"commands"`
	RefusedCommands  int   `json:"refused_commands"`
	NonzeroCommands  int   `json:"nonzero_commands"`
	Transfers        int   `json:"transfers"`
	TFTPRetransmits  int   `json:"tftp_retransmits"`
	AbortedTransfers int   `json:"aborted_transfers"`
	GaveupTransfers  int   `json:"gaveup_transfers"`
	ListenerExits    int   `json:"listener_exits"`
	BootpAnswered    int   `json:"bootp_answered"`

	EFSReadCalls int64 `json:"efs_read_calls"`
	EFSReadBytes int64 `json:"efs_read_bytes"`
	StdoutCalls  int64 `json:"stdout_calls"`
	StdoutBytes  int64 `json:"stdout_bytes"`
	StderrCalls  int64 `json:"stderr_calls"`
	StderrBytes  int64 `json:"stderr_bytes"`

	Verbs   []VerbLatency `json:"verbs"`
	Paths   []PathReuse   `json:"path_reuse"`
	Slowest []SlowCommand `json:"slowest_commands"`
}

Summary is the derived view of one run's events.jsonl: per-session timing, run-wide totals, per-verb latency, and path reuse. It is what summary.json holds and what the shutdown/`trace summary` human report is rendered from. Idle is reported per session and never summed run-wide - two sessions idle in the same wall-second are not two seconds of idle.

func Summarize

func Summarize(r io.Reader) (Summary, error)

Summarize reads an events.jsonl stream and aggregates it. It tolerates a truncated final line (a crash mid-write): a line that won't parse is skipped, not fatal.

func (Summary) WriteText

func (s Summary) WriteText(w io.Writer)

WriteText renders a short operator report: per-session timing, run-wide totals, and the slowest commands. It is the human half of the summary the server prints at shutdown and `trace summary` prints on demand.

type TransferRecord

type TransferRecord struct {
	Client      string
	Name        string
	TreePath    string
	Image       string
	ImagePath   string
	Size        int64
	BlockSize   int
	Blocks      int
	BytesSent   int64
	BytesAcked  int64
	Retransmits int
	DurationMS  int64
	Result      string
}

TransferRecord is one completed TFTP transfer, assembled by the tftp server and handed here for recording. Result is the outcome: ok, unacked_final (all blocks sent but the client never acked the last one, the normal SGI PROM case), aborted (the client sent ERROR or a send failed), gaveup (a mid-transfer block went unacked within the retry budget), notfound, or error. BytesSent is what went on the wire; BytesAcked is what the client confirmed - they differ exactly when the final block is unacked.

type VerbLatency

type VerbLatency struct {
	Verb     string `json:"verb"`
	Count    int    `json:"count"`
	MedianMS int64  `json:"median_ms"`
	P95MS    int64  `json:"p95_ms"`
	WorstMS  int64  `json:"worst_ms"`
}

VerbLatency is one command verb's duration distribution.

Jump to

Keyboard shortcuts

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