Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type HostResult ¶
type HostResult struct {
Changed bool `json:"changed"`
Skipped bool `json:"skipped"`
Failed bool `json:"failed"`
Unreachable bool `json:"unreachable"`
Msg string `json:"msg"`
}
HostResult is the handful of classification fields Apply needs to decide a host's outcome for one task. It's deliberately not the full result shape - different modules return wildly different fields (command: stdout/stderr/cmd/rc; most modules: msg on failure; others: arbitrary module-specific fields) and hand-modeling all of them isn't worth it. The full original bytes are kept separately (TaskNode.Raw) for on-demand display.
func DecodeHostResult ¶
func DecodeHostResult(raw json.RawMessage) HostResult
DecodeHostResult extracts HostResult's classification fields from one host's raw JSON payload. A decode error is treated as "no flags set" rather than surfaced - a malformed/unexpected shape for these fields shouldn't prevent the host's full raw payload from still being recorded.
type PlaybookState ¶
type PlaybookState struct {
Plays []*PlayNode
// AllHosts is the run-wide, alphabetically-sorted set of hosts that have
// reported anything, for any task, so far this run. It only ever grows.
// tui.go's taskLabel uses it to show every known host on every task's
// collapsed row (grey if this specific task hasn't recorded a result
// for it yet) instead of only the hosts each task has itself heard
// from — see TUI.md's "New ideas for the task lines". There's no
// upstream event that reveals a task's target hosts before its first
// result, so a task's very first appearance in a run necessarily starts
// this list from empty; every later task inherits whatever's already
// been discovered.
AllHosts []string
// HadUnreachable is true once any host has been recorded as
// OutcomeUnreachable, at any point in this run - run-wide, once true,
// never cleared, like AllHosts. Exists so main.go can disambiguate
// ansible-playbook's own overloaded exit code 4 (ansible-core's own
// ExitCode enum assigns 4 to both HOST_UNREACHABLE and PARSER_ERROR,
// with its own "FIXME: conflicts" comment) using evidence Tangsible
// independently observed via the real v2_runner_on_unreachable event
// stream, rather than trusting the exit code alone.
HadUnreachable bool
// Optional hooks a UI layer wires up before streaming begins, so a
// tree can grow incrementally instead of being rebuilt from scratch on
// every event. nil-checked before every call. Deliberately typed using
// only this file's own types, so this file stays free of any UI
// dependency.
OnPlayAdded func(play *PlayNode)
OnTaskAdded func(play *PlayNode, task *TaskNode)
OnHostRecorded func(task *TaskNode, host string)
// OnPlayStarted fires on every real v2_playbook_on_play_start event,
// unconditionally - unlike OnPlayAdded, which only ever fires once a
// play gets its own first task (see the tree's own "plays with no
// executed tasks never appear" rule). Confirmed empirically: a play
// whose hosts: pattern matches zero hosts in this run's inventory
// still gets a real v2_playbook_on_play_start event, even though none
// of its tasks ever fire a single event afterward - so this is the
// only reliable signal that such a play (and, transitively, every
// task nested inside it) has been passed over entirely. progress.go's
// tracker uses this as a resync point precisely because of that -
// see NewLiveTUI's own wiring for why per-task matching alone can't
// recover from an entirely-skipped play on its own.
OnPlayStarted func(name string)
// contains filtered or unexported fields
}
PlaybookState is the play -> task -> host tree built up from the live event stream. Plays and tasks are appended lazily, on their first task's start, so plays with no executed tasks never show up (see TUI.md).
Once its On*Added/OnHostRecorded hooks are wired up (by tui.go), this is only safe to mutate from whichever single goroutine calls Apply — no mutex, by construction, since that's always tview's event-loop goroutine (Apply runs inside an app.QueueUpdateDraw closure). Not a general-purpose concurrent data structure.
func (*PlaybookState) Apply ¶
func (s *PlaybookState) Apply(ev RawEvent)
func (*PlaybookState) EarliestFailingPlay ¶ added in v0.2.0
func (s *PlaybookState) EarliestFailingPlay() string
EarliestFailingPlay returns the name of the first play, in run order, containing at least one host recorded as OutcomeFailed on any of its tasks - design-docs/Rerun.md's "Resume where failed" own "Start with play" default. Returns "" if there were no failures at all this run.
func (*PlaybookState) FailedHosts ¶ added in v0.2.0
func (s *PlaybookState) FailedHosts() []string
FailedHosts returns the sorted, deduplicated set of hosts that recorded OutcomeFailed on any task, anywhere in this run - design-docs/Rerun.md's "Only failed hosts" checkbox and the host-half of "Resume where failed." Deliberately reuses OutcomeFailed as-is, same as everywhere else in this package: a host whose only failure was an ignore_errors: true task is still counted here, matching this app's existing simplification (Ansible itself would count that toward "ok" + a separate "ignored" tally, not "failed" - see TaskNode.Counts' own doc comment history).
func (*PlaybookState) IncompleteTasks ¶ added in v0.2.0
func (s *PlaybookState) IncompleteTasks() []*TaskNode
IncompleteTasks returns every task, across all plays, in first-seen (run) order, that currently has at least one host dispatched but not yet reporting a terminal outcome (TaskNode.inFlight, set by a v2_runner_on_start event, cleared the moment that host's own result arrives - see Apply/TaskNode.record). Exposed read-only for tui.go, which uses it to mark every currently-active task's row with a spinner and to keep an in-progress task visible regardless of the active filter - design-docs/StrategyFree.md's exact replacement for the old single CurrentTask()/currentTask pointer, which assumed Ansible's linear strategy (never more than one task genuinely in flight) and simply broke under strategy: free, where several tasks can be simultaneously incomplete. Under linear this returns exactly the same single task CurrentTask() used to (Started is populated per host by this app's own bundled callback plugin fork - RawEvent.Host's own doc comment - for both lockstep and free strategies alike); a pre-fork run log (no v2_runner_on_start events at all, e.g. a saved run replayed via revisit/ diff) simply never populates inFlight for anything, so this always returns empty for one - harmless, since a replayed log is already frozen (processDone pre-true) by the time anything asks. A plain scan over Plays/Tasks, not a maintained index - same "simple beats clever at ~10-host scale" reasoning hostsWithOutcome below already follows.
func (*PlaybookState) Reset ¶
func (s *PlaybookState) Reset()
Reset clears every field Apply/recordHost populate during a run, back to the same zero state a fresh &PlaybookState{} starts in - used when starting a rerun (Rerun.md) so the next Apply sequence builds a brand new tree instead of appending onto the previous run's. The OnPlayAdded/ OnTaskAdded/OnHostRecorded hooks are deliberately left untouched: they're wired once by tui.go and need to keep firing for the new run's events exactly as they did for the run before it.
func (*PlaybookState) UnreachableHosts ¶ added in v0.2.0
func (s *PlaybookState) UnreachableHosts() []string
UnreachableHosts returns the sorted, deduplicated set of hosts that recorded OutcomeUnreachable on any task, anywhere in this run - design-docs/Rerun.md's "Only unreachable hosts" checkbox.
type RawEvent ¶
type RawEvent struct {
Event string `json:"_event"`
Play *PlayRef `json:"play"`
Task *TaskRef `json:"task"`
Hosts map[string]json.RawMessage `json:"hosts"`
// Host is only ever populated on a "v2_runner_on_start" event - the one
// event this app's bundled callback plugin fork emits that jsonl.py
// never did under the linear strategy (design-docs/
// OwnCallbackPlugin.md). It names which single host was just dispatched
// for the current task, disambiguating "genuinely slow" from "queued
// behind a full --forks" once paired with that same host's later
// outcome-event timestamp (aggregate.go's TaskNode.Started/Finished).
Host string `json:"host"`
// TimestampText is the event's own "_timestamp" (empirically RFC3339
// with fractional seconds, e.g. "2026-08-06T12:41:02.439015Z").
// Deliberately a plain string, not time.Time: a time.Time field's own
// UnmarshalJSON failing on a malformed value would make this whole
// struct's json.Unmarshal call return an error, which main.go's
// streamEvents treats as "not JSON" and drops the entire event - task
// and hosts included - over nothing but a bad timestamp. A string
// field can't fail that way; Timestamp() below does the fallible
// parsing on demand and swallows any error, same convention as
// DecodeHostResult.
TimestampText string `json:"_timestamp"`
}
RawEvent is the subset of the event schema this app cares about, emitted by tangsible's own bundled callback plugin (design-docs/ OwnCallbackPlugin.md) - a GPL-3.0 fork of ansible.posix.jsonl that deliberately mimics its schema byte-for-byte except where noted (Host below), so this type doubles as the schema for both. Fields we don't need are simply dropped by json.Unmarshal. Hosts keeps each host's full original bytes (rather than decoding straight into HostResult) so the complete result - not just the fields below - can be recorded and shown later; see aggregate.go's TaskNode.Raw.
type TaskNode ¶
type TaskNode struct {
Name string
// Path is the task's own source location ("<absolute file>:<line>"),
// straight from the starting event's own task.path - used by
// tui.go's output drill-down view to look up the task's raw source
// text via source.go's taskSourceIndex. Empty if the event didn't
// carry one (shouldn't happen for a real run, but not trusted
// blindly - same caveat as this file's other event-derived fields).
Path string
// IsHandler is RawEvent.Task.IsHandler at the moment this task started
// - see that field's own doc comment (design-docs/
// OwnCallbackPlugin.md). Always false for a pre-fork run log, or for
// any task-start event this app's own plugin didn't stamp. Used by
// uikit.TaskLabel to render a "[Handler]" tag and by runner.
// ProgressTracker.Advance to treat a handler's own miss against the
// progress skeleton (which never lists handlers at all - progress.go's
// own doc comment) as expected rather than evidence of drift.
IsHandler bool
// StartedAt is from the starting event's own _timestamp
// (RawEvent.Timestamp()), not our wall-clock time.Now() at the moment
// we process it - "when did Ansible itself start this task." Zero if
// that timestamp was missing/malformed. Currently unused by any
// renderer (tui.go shows a spinner rather than an elapsed readout for
// the active task) - kept because Ansible provides it for free and a
// future summary/history view (see Findings.md) would want it; remove
// if that never materializes.
StartedAt time.Time
HostOrder []string
Hosts map[string]Outcome
// Raw holds each host's full original result payload for this task,
// parallel to Hosts - populated alongside it in record, read by
// tui.go's showOutput on demand. Never formatted here; formatting is
// a UI concern (see tui.go).
Raw map[string]json.RawMessage
// Warnings mirrors Hosts, not Raw: like Outcome, "did this host report
// a non-empty warnings field for this task" is a classification
// decided once from the raw payload, not a rendering decision - the
// same category of fact Outcome already is, computed the same way
// (once, in record, from this event's own raw bytes), just a second
// independent axis alongside it (a result can be both Changed and
// carry a warning). Exists purely so callers rendering many rows
// across many hosts (tui.go's TaskLabel/HostLabel, recap.go's
// recapForHost) don't have to re-decode the same JSON on every single
// redraw just to answer a yes/no question - a real, measured cost:
// profiling a frozen 40-host/30-task recap showed ~63% of
// FlattenRecapRows' own CPU time was encoding/json decode calls doing
// exactly this, repeated on every cursor move. hasNonEmptyWarnings
// (events.go) deliberately doesn't reuse uikit's own DecodeWarnings/
// JoinedStringList - this package can't import uikit (uikit already
// imports this one) - but only needs a plain presence check, not
// JoinedStringList's own multi-shape text-joining, so duplicating that
// much is a deliberately small, self-contained trade.
Warnings map[string]bool
// HasStderr mirrors Warnings - same shape, same reasoning, same
// "computed once in record from this event's own raw bytes" timing -
// just a third independent axis (Filters.md's "Interesting" filter
// needs "changed/failed/unreachable, has stderr output, or has a
// warning" without decoding every host's raw JSON on every rebuild).
HasStderr map[string]bool
// Ignored mirrors Warnings/HasStderr - same shape, same "computed once
// in record" timing - true when this host's own result carried
// "ignore_errors": true (design-docs/OwnCallbackPlugin.md's
// hasIgnoreErrors), which only a Failed outcome ever sets. Deliberately
// doesn't change what Outcome itself is: a host whose only failure was
// ignore_errors: true still records as OutcomeFailed everywhere that
// matters operationally (tree coloring, FailedHosts, rerun's "Only
// failed") - this is purely an additional, additive signal for
// renderers that want to distinguish the two without touching that
// established, deliberate simplification (see FailedHosts' own doc
// comment).
Ignored map[string]bool
// Started is each host's own "v2_runner_on_start" timestamp - see
// RawEvent.Host's own doc comment for why this event (and so this map)
// didn't used to exist under the linear strategy. This is what
// disambiguates real per-host execution time from queue-wait
// (design-docs/PerHostTaskTiming.md's "the problem: this delta doesn't
// mean what it looks like it means"), once paired with Finished below.
// Absent entry (zero time.Time) means "no v2_runner_on_start ever
// arrived for this host" - true for every run recorded before this
// fork existed, and the same "zero means unknown" convention StartedAt
// already uses.
Started map[string]time.Time
// Finished is each host's own outcome-event timestamp (the same
// ev.Timestamp() already used to decide task.duration.end), recorded
// per host rather than just once per task - parallel to Hosts/Raw,
// populated alongside them in record. Combined with Started, this
// gives a real per-host duration; used alone (Started absent, e.g. a
// pre-fork run log) it's not meaningful on its own, same caveat
// PerHostTaskTiming.md raised for the finish-only approach.
Finished map[string]time.Time
// contains filtered or unexported fields
}
type TaskRef ¶
type TaskRef struct {
Name string `json:"name"`
// Path is the task's own source location, "<absolute file>:<line>",
// exactly as Ansible reports it on every event - used by tui.go's
// output drill-down view to look up the task's raw source text via
// source.go's taskSourceIndex.
Path string `json:"path"`
// ID is the task's own stable UUID (task._uuid, stringified) - present
// on every task-start and terminal event alike, under stock
// ansible.posix.jsonl as much as this app's own bundled fork
// (design-docs/OwnCallbackPlugin.md's own "_new_task shapes ...
// exactly as jsonl has it" - confirmed directly against jsonl.py's own
// source, not assumed). Unlike Name/Path, it's stable and unique
// across every event referencing the same task, which is what lets
// aggregate.go's findOrCreateTask resolve a task's own identity from
// any event that reports it - not just the one that happened to start
// it - see design-docs/StrategyFree.md.
ID string `json:"id"`
// IsHandler is only ever true on a "v2_playbook_on_handler_task_start"
// event - stamped by this app's own bundled callback plugin fork
// (design-docs/OwnCallbackPlugin.md), absent (so false) on every other
// event and on any pre-fork run log. jsonl.py's own emitted JSON has
// no field distinguishing a handler's task-start event from a regular
// task's, even though aggregate.go already gives each its own
// TaskNode - this is what lets that distinction actually render.
IsHandler bool `json:"is_handler"`
}