proc

package
v0.19.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package proc is a long-running thing that produces output.

A shell command and a child agent are the same shape: something started that keeps going, emits a stream while it does, can be asked about while it runs, and eventually settles with a result. dun has both and wrote them twice — `bgjob.go` for commands and `subagent.go` for children, each with its own monitor tool taking the same (id, tail) and each buffering output its own way. Nothing here is specific to either.

What this owns is the part that is genuinely hard and was solved once already: where the output goes, how much of it a model is allowed to hear, and when.

  • Output goes to a FILE and the path goes to the caller. A 50k-line build log is grep-able; the same log spent on a context window is a session ended. The inline tail is bounded and the file has the rest.
  • Reporting is LINE-COMPLETE. Half a line tells a model nothing and a pattern cannot judge one, so bytes since the last newline are held back.
  • Silence is the DEFAULT. A process nobody asked about does not speak. Streaming without a bound is worse than silence: an unfiltered build narrates itself into the window one line at a time.

Signals, exit status and the shape of a result belong to whoever runs the thing, so they arrive through Settle rather than being modelled here.

Index

Constants

View Source
const EarlyMax = 1 << 20

EarlyMax bounds what is kept in memory before a log file exists.

A process may start before anyone has decided to keep it, and its output is usually also accumulating somewhere else for the caller that started it. Past the cap the log simply begins later rather than growing without limit.

View Source
const TailBytes = 4000

TailBytes is how much of the output an inline read carries — enough to see a failure, with the file holding the rest.

Variables

View Source
var ErrNoSQLite = fmt.Errorf(
	"sqlite3 is not on PATH, so these rows cannot be queried here — the rows " +
		"themselves are still on disk as tab-separated text, which grep and awk read")

ErrNoSQLite says the feature was reached on a machine without the tool.

Functions

func Compose

func Compose(all []*Proc, id int, tail int, now time.Time) string

Compose renders a monitor answer as the text a model reads.

ONE renderer for every kind of process, which is the whole reason this package exists: dun grew `agent_monitor` and a background-job monitor separately, taking the same (id, tail) and answering in two different shapes. A model that has learned to read one should not have to learn the other.

With no id: every process, one line each, newest last — "what is running". With an id: that one's status, then its tail, which is the question actually being asked ("how is it going") answered in the order a person reads it.

Types

type Extract

type Extract func(line []byte) (Row, bool)

Extract turns one complete line into a row. Returning false drops the line, which is the common case: most of a build log is not data.

type Monitor

type Monitor struct {
	// BufferBytes reports once this many matched-but-unreported bytes have
	// piled up. Zero never reports progress.
	BufferBytes int
	// Grep limits reports to lines matching it. Empty keeps every line.
	Grep string
	// Ignore mutes the process entirely, including its completion — for one
	// whose answer stopped mattering.
	Ignore bool
}

Monitor is what a process is allowed to say while it runs.

The zero value is silence until it settles, which is the right default: a process nobody asked about should not spend context.

type Proc

type Proc struct {
	// ID addresses this process in a monitor tool.
	ID int
	// Kind is what sort of thing it is — "job", "agent" — carried for display
	// and never interpreted here.
	Kind string
	// Label is how a person recognises it: a command line, a child's prompt.
	Label string
	// LogPath is where the full output is, and is what the caller hands the
	// model instead of the output itself.
	LogPath string
	// contains filtered or unexported fields
}

Proc is one running thing: its log, what it may say, and what it has produced.

Safe for concurrent use: Write is called from whatever is producing output while a monitor read may arrive from anywhere else.

func New

func New(id int, kind, label, logPath string, now time.Time) *Proc

New starts tracking a process. logPath may be empty, in which case output is held in memory until OpenLog is called.

func (*Proc) Collect

func (p *Proc) Collect(rows *Rows, e Extract)

Collect turns this process's output into rows as well as text.

The extract runs on the same complete lines the monitor sees, so a process is never parsed twice and a half-line is never handed to a parser. Passing a nil Rows stops collection without disturbing the text side.

func (*Proc) Done

func (p *Proc) Done() bool

Done reports whether the process has finished.

func (*Proc) OpenLog

func (p *Proc) OpenLog(path string) error

OpenLog begins writing to path, flushing whatever was held in memory.

func (*Proc) Poll

func (p *Proc) Poll(now time.Time) []string

Poll runs the query monitors that are due and returns a note for each whose answer CHANGED.

Called by the host from wherever it already pumps — a heartbeat, a turn boundary — rather than from Write. A query costs tens of milliseconds and shells out; doing that on the producer's write path would make a process's own output wait on its monitoring, and holding the lock across it would make every other write wait too.

The first run of a query PRIMES it and says nothing. There is no change yet, and reporting the starting value as news would fire every monitor once for nothing.

func (*Proc) Rows

func (p *Proc) Rows() *Rows

Rows is the structured side, or nil when nothing is collecting.

func (*Proc) Settle

func (p *Proc) Settle(result string, now time.Time) bool

Settle records the outcome. The second caller gets false: a result is claimed once, by whoever is waiting for it.

func (*Proc) Status

func (p *Proc) Status(now time.Time) string

Status is one line: what this is, how long it has run, and how it ended.

func (*Proc) Tail

func (p *Proc) Tail(n int) string

Tail returns the last n bytes of what a process has produced, cut to a line boundary so it never opens mid-line.

From the LOG, not from memory: the point of writing output to a file is that asking about a long-running thing costs the same whether it has produced ten lines or fifty thousand.

func (*Proc) Watch

func (p *Proc) Watch(m Monitor) error

Watch replaces the monitor policy and returns an error only when the pattern will not compile — a bad pattern is the caller's mistake and must not silently become "match everything".

func (*Proc) WatchQuery

func (p *Proc) WatchQuery(q QueryMonitor) error

WatchQuery registers a query to be re-run as rows arrive.

Refused when nothing is collecting rows: a query monitor on a process with no Extract would be silently permanent, and a monitor that can never fire is worse than none — it reads as coverage.

func (*Proc) Write

func (p *Proc) Write(b []byte) string

Write takes output as it is produced. It returns the progress note that is now due, or "" — the caller sends it, OUTSIDE any lock of its own, because notifying usually reaches a queue or a UI and nothing good comes of holding a process's lock across either.

type QueryMonitor

type QueryMonitor struct {
	// Name is what the note calls this, e.g. "failures".
	Name string
	// SQL runs against the `rows` table. Aggregates are the point — a query
	// returning a thousand rows will report a thousand rows.
	SQL string
	// EveryRows is the minimum number of NEW rows before re-running. Zero means
	// one: any new row may change the answer.
	EveryRows int
	// MinInterval is the minimum wall time between runs of this query, whatever
	// the row count. Both bounds must be satisfied, which is what makes a burst
	// and a dribble behave the same: 50,000 rows arriving at once re-run this
	// query once, and a row a second cannot re-run it a second.
	MinInterval time.Duration
	// contains filtered or unexported fields
}

A query as a monitor: signal on the ANSWER changing, not on the output.

`Monitor` reports lines — every line matching a pattern, once enough of them pile up. That is volume dressed as a signal: a fifteen-minute suite matching ^FAIL four hundred times narrates itself into the window, and the thing actually worth interrupting for is that the failure COUNT moved from 0 to 3.

So a query is registered, re-run as rows arrive, and reported only when its result differs from last time. Bytes are a proxy for interestingness; a changed answer is the thing itself.

type Row

type Row []string

Row is one extracted record. Fields are written in order and must match the column order of Schema.

type Rows

type Rows struct {
	// Path is the TSV the rows are appended to.
	Path string
	// Schema is the CREATE TABLE column list, e.g. "level text, pkg text".
	// Column order must match what Extract emits.
	Schema string
	// contains filtered or unexported fields
}

Rows is the structured side of a process's output.

func OpenRows

func OpenRows(path, schema string) (*Rows, error)

OpenRows starts collecting rows into path. The caller supplies the schema because only it knows what its Extract produces.

func (*Rows) Append

func (r *Rows) Append(row Row) error

Append writes one row. Tabs and newlines inside a field would break the TSV, so they are replaced rather than escaped: this is a query surface over log output, and a field containing a literal tab is a parsing bug upstream, not something to preserve faithfully.

func (*Rows) Close

func (r *Rows) Close() error

Close stops collection. The file stays: it is the queryable artifact.

func (*Rows) Count

func (r *Rows) Count() int64

Count is how many rows have been written.

func (*Rows) Query

func (r *Rows) Query(sql string) (string, error)

Query runs one SQL statement against the rows, which are imported into an in-memory database as the table `rows`.

A fresh import per query rather than a long-lived database: the rows file is append-only and the import is measured in tens of milliseconds, so there is no state to keep consistent, nothing to migrate, and a query always sees exactly what has been written by the time it ran.

Jump to

Keyboard shortcuts

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