Documentation
¶
Overview ¶
Package attachio is the raw-mode terminal attach loop, extracted from cmd/rattach: raw mode (tty stdin only), SIGWINCH-driven resize, Ctrl-] detach, and sequence-number tracking for --since resume. It is the one place that contract lives, so cmd/rattach and cmd/rainier's `attach` (and `new`'s auto-attach) share the same resize-first handshake, detach key, and final detach/exit messages. A reconnectable disconnect returns its cursor silently: only the caller knows whether the terminal is being handed back to a shell or must remain untouched for cursor-relative replay.
Restructuring note: rattach's original loop called os.Exit(0) from its reader goroutine the instant the connection dropped or the session exited — fine for a binary whose entire job is that one attach, wrong for a library. Run below never exits the process: it returns nil on a clean detach/disconnect/session-exit (the same three cases rattach used to os.Exit(0) on) and a non-nil error otherwise (dial failure, raw-mode setup failure). Final detach/exit messages retain their original text; disconnect diagnostics belong to the one-shot caller, not the live stream.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrSessionNotReady = errors.New("attachio: session not ready")
ErrSessionNotReady is Run's answer, via errors.Is, when the dial's HTTP response carried a 503 — controld's session_not_ready (the session hasn't reached "running" yet, see internal/controld/attach.go's waitRunning). It is never returned directly: it's what a *DialError wrapping a 503 response matches against, so callers write `errors.Is(err, attachio.ErrSessionNotReady)` rather than parsing error text (the old — and fragile — strings.Contains(err.Error(), "503")).
Functions ¶
func AttachURL ¶
AttachURL builds the /attach URL from a base (sessiond directly, or runnerd's/controld's relay — same contract either way). session is appended as an extra query param only when non-empty: direct sessiond has no use for it and ignores unknown params, while a relay's /attach handler reads it to find the right hub. Factored out so the URL contract (base is just "scheme://host[:port]", never anything with a path already on it) has one place to get right and one place to unit test.
The cursor is deliberately NOT part of it: Run puts `since` on whatever URL it is handed (see withSince), so a caller that builds its own attach URL — cmd/rainier's, which is a controld route, not a `<base>/attach` — cannot end up dialing without one. That is exactly the bug this split fixes; see TestRunDialsWithTheCursor.
func Cursor ¶
Cursor maps a `--since` flag to the cursor an attach carries on the wire. given reports whether the user actually typed the flag, which is the whole point: a uint64 flag defaulting to 0 cannot tell "no cursor" from "--since 0" on its own, and those are opposite requests.
- not given → 0: no cursor. Snapshot of the current screen, then live — what a plain `attach` has always done and should keep doing (replaying a day's raw log to paint a screen is the thing spec §5 forbids).
- `--since 0` → terminal.SinceAll: the whole log, first entry onward. "I have seen nothing" is a coherent thing for a user to say, and it is what the runbook's read-the-failed-setup-log flow means by it.
- `--since N` → N: resume, replaying the entries after N — the value the disconnect line prints ("rattach --since %d to resume").
Shared by cmd/rainier and cmd/rattach so both spell it identically, which is this package's whole reason for existing.
func RestoreTerminal ¶ added in v0.0.7
RestoreTerminal relinquishes remote emulator input modes and discards queued TTY input before returning to the local shell. Call only after the entire attach lifetime (all reconnect attempts) and after Run has joined its pumps: cursor-only replay need not repeat the remote application's mode enables. It does not clear screen/scrollback or read/discard piped input. Output resets are emitted only to a TTY; write/flush failures are returned together.
func ScanDetach ¶
ScanDetach reports the index of the first Ctrl-] byte in buf, or -1 if buf contains none. Pure and side-effect-free so it's unit-testable on its own — Run uses it to split a stdin chunk into "forward this, then detach".
Types ¶
type DialError ¶
type DialError struct {
Status int
// contains filtered or unexported fields
}
DialError wraps a websocket dial failure that received an HTTP response — as opposed to a pure transport failure (DNS, connection refused), which Run returns unwrapped so callers can still match on the underlying net/url error type. Status is that response's status code.
type Outcome ¶
Outcome is the non-error result of one established attach. LastSeq is the last frame successfully rendered to local stdout and is therefore the only safe reconnect cursor. ExitCode is meaningful only when Reason is Exited.
func Run ¶
Run dials wsURL with header (nil for no extra headers — e.g. rattach's direct-to-sessiond/runnerd use, non-nil to carry an Authorization bearer against controld), performs the resize-first contract, and pipes the local terminal (os.Stdin/os.Stdout) until detach, session exit, or disconnect.
since is the attach cursor (see Cursor), and Run is what puts it on the URL it dials — wsURL is a plain attach URL, with or without a query of its own, and never carries `since` itself. It used to be the caller's job to spell it into the URL AND pass it here, which is how `rainier attach --since N` came to dial with no cursor at all for two plans: the argument was accepted and never used. One place decides now, and it is the place that dials.
It prints final terminal-handoff status lines:
"\r\n[detached at seq %d; session still running]\r\n" "\r\n[session process exited: %d]\r\n"
Raw mode is entered only when os.Stdin is a real tty; a non-tty stdin (piped input, a test) skips raw mode entirely and announces a fixed 80x24 size so the server's resize-first contract is still satisfied.
Run returns an explicit Outcome for detach, disconnect, and process exit, and a non-nil error for anything that keeps the loop from starting (the dial or putting the terminal in raw mode). It stops every stdin/stdout goroutine before returning, so a caller may safely start another Run after a Disconnected outcome without creating two readers for the local terminal. A dial failure that received an HTTP response (rather than a pure transport failure) comes back as a *DialError; errors.Is(err, ErrSessionNotReady) matches specifically controld's 503 session_not_ready — callers (cmd/rainier's `new`, retrying an attach immediately after create) should match on that sentinel rather than inspecting error text.