bootstrap

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: May 8, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package bootstrap composes the nine-step PersistentPreRunE sequence pinned by the resurrection spec. Step ordering is load-bearing:

  1. EnsureServer
  2. RegisterPortalHooks
  3. Set @portal-restoring (MUST precede step 4)
  4. EnsureSaver (best-effort; SaverDownWarning on failure)
  5. Restore
  6. Clear @portal-restoring
  7. SweepOrphanFIFOs (best-effort; observes still-set per-pane @portal-skeleton-* markers from step 5 — those outlive @portal-restoring and are cleared per-pane on hydration)
  8. CleanStale (best-effort)
  9. Return

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type FIFOSweeper

type FIFOSweeper interface {
	Sweep() error
}

FIFOSweeper removes stale hydrate-*.fifo files in the state directory whose paneKey is no longer represented by a live `@portal-skeleton-*` marker. Best-effort: the implementation MUST swallow per-file failures internally and return nil unless the underlying directory enumeration itself fails. The orchestrator treats a non-nil err as a soft warning and continues — a stuck FIFO must never block PersistentPreRunE.

Step 7 of the bootstrap sequence: runs after Clear (step 6) so the suppression window has closed, but before CleanStale (step 8) so the per-pane skeleton markers it observes via state.ListSkeletonMarkers are still set on the live tmux server.

type FatalError

type FatalError struct {
	// UserMessage is the single line emitted to stderr at the top-level
	// Execute path. It is the only text the user sees; the spec mandates
	// a single line, no banners, no colors.
	UserMessage string

	// Cause is the underlying error this FatalError wraps. Exposed via
	// Unwrap for errors.Is / errors.As traversal so callers can match on
	// sentinel values further down the stack.
	Cause error
}

FatalError is the typed sentinel for unrecoverable bootstrap conditions (tmux missing, version too old, EnsureServer failure, hook registration failure, @portal-restoring marker failure). The orchestrator's job is only to surface a single user-facing line on stderr and exit non-zero; FatalError carries that line in UserMessage and the underlying cause for errors.Is / errors.As traversal and portal.log diagnostics.

Soft failures (EnsureSaver, CleanStale, Restore content errors) are NOT wrapped in FatalError — they degrade locally and continue per spec.

func NewFatal

func NewFatal(userMsg string, cause error) *FatalError

NewFatal constructs a FatalError pairing the user-facing message with the underlying cause. Both arguments are stored verbatim; callers are responsible for formatting userMsg per the spec ("Portal failed to ...: <err>").

func (*FatalError) Error

func (e *FatalError) Error() string

Error returns the UserMessage so the default Cobra/main.go error path (fmt.Fprintln(os.Stderr, err)) emits exactly the user-facing line.

func (*FatalError) Unwrap

func (e *FatalError) Unwrap() error

Unwrap exposes the underlying cause to errors.Is and errors.As.

type HookRegistrar

type HookRegistrar interface {
	RegisterPortalHooks() error
}

HookRegistrar registers Portal's global tmux hooks idempotently.

type Logger

type Logger interface {
	Debug(component, format string, args ...any)
	Warn(component, format string, args ...any)
	Error(component, format string, args ...any)
}

Logger is the sink for failure diagnostics. It is internally nil-safe: Orchestrator.Run substitutes a no-op default when Logger is unset, so callers never need to nil-check before invoking it.

Step-entry diagnostics emit via Debug — per the spec's Observability section, bootstrap events are logged at DEBUG so PORTAL_LOG_LEVEL=debug surfaces the step boundary an operator scans when a session fails to come back; production runs (default LevelWarn) drop these lines. Soft failures (best-effort steps that degrade-and-continue) emit via Warn. Fatal failures emit via Error before the orchestrator returns the wrapped *FatalError so the same line lands in portal.log via ComponentBootstrap as well as on stderr at the top-level Execute path.

type NoOpFIFOSweeper

type NoOpFIFOSweeper struct{}

NoOpFIFOSweeper satisfies FIFOSweeper. Sweep always reports success. FIFO sweeping is best-effort and degradable per spec, so a no-op is the natural fallback when the production wiring cannot resolve the state directory and matches the NoOp policy: "only steps that may degrade get a NoOp."

func (NoOpFIFOSweeper) Sweep

func (NoOpFIFOSweeper) Sweep() error

Sweep always returns nil.

type NoOpHooks

type NoOpHooks struct{}

NoOpHooks satisfies HookRegistrar. RegisterPortalHooks always reports success. Useful for tests / production fallbacks.

func (NoOpHooks) RegisterPortalHooks

func (NoOpHooks) RegisterPortalHooks() error

RegisterPortalHooks always returns nil.

type NoOpRestorer

type NoOpRestorer struct{}

NoOpRestorer satisfies Restorer. Restore always returns (false, nil) — the happy path under the (corrupt, err) Restorer contract. Useful for tests / production fallbacks.

func (NoOpRestorer) Restore

func (NoOpRestorer) Restore() (bool, error)

Restore always returns (false, nil).

type NoOpSaver

type NoOpSaver struct{}

NoOpSaver satisfies SaverBootstrapper. EnsureSaver always reports success. Useful for tests / production fallbacks.

func (NoOpSaver) EnsureSaver

func (NoOpSaver) EnsureSaver() error

EnsureSaver always returns nil.

type NoOpStaleCleaner

type NoOpStaleCleaner struct{}

NoOpStaleCleaner satisfies StaleCleaner. CleanStale always reports success. Useful for tests / production fallbacks.

func (NoOpStaleCleaner) CleanStale

func (NoOpStaleCleaner) CleanStale() error

CleanStale always returns nil.

type Orchestrator

type Orchestrator struct {
	Server    ServerBootstrapper
	Hooks     HookRegistrar
	Restoring RestoringMarker
	Saver     SaverBootstrapper
	Restore   Restorer
	Sweeper   FIFOSweeper
	Clean     StaleCleaner
	Logger    Logger // nil tolerated; Run substitutes a no-op default
}

Orchestrator runs the nine-step bootstrap sequence. Wiring of production implementations lives in cmd/root.go (task 5-3); this package stays pure (interfaces + Run) so the ordering contract is independently testable.

func (*Orchestrator) Run

func (o *Orchestrator) Run(ctx context.Context) (bool, []Warning, error)

Run executes the nine bootstrap steps in spec order. It returns the serverStarted flag from step 1 (EnsureServer) verbatim, the slice of soft Warnings accumulated across steps 4-5 (in step order), and any fatal error. The ctx parameter is reserved for Phase 6 timeout/cancel wiring.

Soft warning paths (do NOT short-circuit Run, do NOT produce fatal err):

  • Step 4 (EnsureSaver) returns non-nil → SaverDownWarning.
  • Step 5 (Restore) returns corrupt=true → CorruptSessionsJSONWarning; restoreErr is treated as soft and the final return swallows it (per spec, corrupt sessions.json is a non-fatal no-op warning).
  • Step 5 (Restore) returns (false, err) — a contract violation under the Restorer contract — is treated defensively as soft: logged and swallowed. Step 5 NEVER escalates to a fatal abort, so a future Restorer implementation cannot silently break PersistentPreRunE.
  • Step 7 (Sweep) returns non-nil → logged via Warn and swallowed.

type Restorer

type Restorer interface {
	Restore() (corrupt bool, err error)
}

Restorer performs skeleton-only session restoration.

Contract (self-enforcing via the typed return signature):

  • Returns (false, nil) on the happy path and after isolating any per-session failures. Per the spec's degrade-locally-and-continue principle, every soft per-session error MUST be logged and swallowed inside the implementation — they MUST NOT travel up through err.
  • Returns (true, err) when sessions.json itself is unparseable; err MUST wrap state.ErrCorruptIndex so callers downstream can match via errors.Is. corrupt=true is the ONLY case in which err is non-nil.

The bool exists so Orchestrator step 5 can branch on a typed signal rather than a string-equality check on the error chain. A future implementation that violates the contract by returning (false, err) is treated defensively by Run: the err is logged and the orchestrator continues without escalating to a PersistentPreRunE abort. This guards the "degrade locally, log, continue" principle against silent drift.

type RestoringMarker

type RestoringMarker interface {
	Set() error
	Clear() error
}

RestoringMarker manages the @portal-restoring server option that suppresses the save daemon while skeleton restoration is in flight.

type Runner

type Runner interface {
	Run(ctx context.Context) (bool, []Warning, error)
}

Runner is the abstraction cmd/root.go depends on so PersistentPreRunE does not import the concrete *Orchestrator type. Orchestrator implicitly satisfies Runner; tests inject lightweight fakes (no-op runners, recording fakes, panic guards) via BootstrapDeps.Orchestrator.

The middle return value carries any soft Warnings accumulated during the run (Phase 6 task 6-9). Lightweight test fakes typically return a nil slice — only the full Orchestrator produces warnings.

type SaverBootstrapper

type SaverBootstrapper interface {
	EnsureSaver() error
}

SaverBootstrapper ensures the _portal-saver detached session exists and matches the current binary version.

type ServerBootstrapper

type ServerBootstrapper interface {
	EnsureServer() (bool, error)
}

ServerBootstrapper starts the tmux server when not already running. EnsureServer reports whether Portal itself was the one that started it.

type StaleCleaner

type StaleCleaner interface {
	CleanStale() error
}

StaleCleaner prunes stale entries from the on-disk hooks store.

type Warning

type Warning = warning.Warning

Warning is a soft bootstrap failure that must NOT terminate Portal. It is a type alias for internal/warning.Warning so the cmd/bootstrap, cmd, and tui packages all reference the same canonical shape — see the internal/warning package for the canonical godoc.

func CorruptSessionsJSONWarning

func CorruptSessionsJSONWarning() Warning

CorruptSessionsJSONWarning returns the canonical warning for the "sessions.json exists but cannot be used" path. Both unparseable content (malformed JSON, unsupported version) AND unreadable files (permission denied) flow through this warning, since internal/state/index_reader.go wraps every non-nil error with state.ErrCorruptIndex. Wording matches the spec section "Observability → Proactive Health Signals" verbatim.

func SaverDownWarning

func SaverDownWarning() Warning

SaverDownWarning returns the canonical warning for the "_portal-saver failed to start after retries" path. Wording matches the spec section "Observability → Proactive Health Signals" verbatim.

Jump to

Keyboard shortcuts

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