Documentation
¶
Overview ¶
Package terminal bridges a client connection to a remote PTY (§24.4, ADR-024). The wire protocol is deliberately tiny:
- binary frames carry raw bytes, both ways (keystrokes in, output out);
- text frames carry JSON control messages: the client sends {"type":"resize","cols":N,"rows":N}; the server sends {"type":"end","reason":"..."} just before closing.
A frame kind is what the WebSocket rung has; the HTTP rungs of ADR-064 §3 carry the same two kinds on two wires — the session request and one data stream — and HTTPConn presents that pair as the same Conn. The bridge below never learns which one carried it.
Keystrokes are never recorded (§24.4): the bridge moves bytes, it does not retain them.
Index ¶
Constants ¶
const ( DefaultIdleTimeout = 15 * time.Minute DefaultMaxDuration = 4 * time.Hour )
Defaults for Options; the instance configuration overrides them (AKERDOCK_TERMINAL_IDLE_TIMEOUT / AKERDOCK_TERMINAL_MAX_DURATION).
Variables ¶
var ErrClientClosed = errors.New("terminal: client closed the connection")
ErrClientClosed is what a Conn's Read must return when the client closed the connection cleanly — the bridge tells a wanted close (user_close) from a vanished peer (disconnect) by this error alone.
Functions ¶
func SendEnd ¶
SendEnd writes the session's final text frame, best effort and on a fresh context: whoever calls it is tearing down, and ctx may already be dead.
It is exported because a session can end BEFORE it has a bridge. Since ADR-066 the remote half runs behind the response head, so a resolution that fails has an open session request and no PTY — and this frame is the only channel that failure has, the terminal's data stream carrying no dial and so having nothing to answer 502 about.
Types ¶
type Conn ¶
type Conn interface {
Read(ctx context.Context) (MessageType, []byte, error)
Write(ctx context.Context, typ MessageType, data []byte) error
// Ping is the §24.4 heartbeat: it fails when the peer is gone even if no
// data is flowing.
Ping(ctx context.Context) error
}
Conn is the subset of a WebSocket connection the bridge needs; the real implementation adapts coder/websocket, tests use an in-memory fake.
type EndReason ¶
type EndReason string
EndReason mirrors the terminal_end_reason enum (data-dictionary §10.6).
const ( EndUserClose EndReason = "user_close" EndIdleTimeout EndReason = "idle_timeout" EndMaxDuration EndReason = "max_duration" EndDisconnect EndReason = "disconnect" EndRevoked EndReason = "revoked" // EndTargetUnreachable is the shell that never opened (ADR-066): the // attach answers before it dials, so a server that refuses SSH or a // container that is gone is no longer a 409 at redeem — it is reported on // the session that is already open. The two values that used to carry it // were both lies: disconnect blames the developer's own network, and // revoked claims an administrator acted when nobody did. EndTargetUnreachable EndReason = "target_unreachable" )
Why a session ended (§24.4).
type HTTPConn ¶
type HTTPConn struct {
// contains filtered or unexported fields
}
HTTPConn presents an HTTP attach pair — the session request's control wire and the one data stream carrying the PTY's bytes — as the Conn the bridge reads and writes (ADR-064 §3). It is the exact counterpart of the WebSocket adapter: the transport changes, the bridge does not.
An HTTP stream has no frames to be typed by, so the message type IS the wire it travels on: a text message is one control frame on the session request, a binary message is bytes on the data stream. Reading merges the two sources back into one ordered-per-source flow of typed messages — the same merge MultiLaneConn performs for WebSocket lanes.
The adapter is direction-agnostic: the control plane bridges a PTY with it, and the CLI pumps a local TTY through the very same code.
func NewHTTPConn ¶
func NewHTTPConn(control *tunnel.LineControl, data io.ReadWriteCloser) *HTTPConn
NewHTTPConn merges an already-open control wire and data stream. Both are owned by the returned Conn from here on: Close tears down the pair.
func (*HTTPConn) Ping ¶
Ping is the §24.4 heartbeat, on the control wire: the data stream carries keystrokes and output only, and a silent shell must still reveal a peer that vanished.
type MessageType ¶
type MessageType int
MessageType is the frame kind of the underlying WebSocket.
const ( MessageBinary MessageType = iota MessageText )
The two frame kinds the protocol uses.
type Options ¶
type Options struct {
// IdleTimeout ends the session after that long without a keystroke.
// Output does not count as activity: a spinner left running must not
// keep a forgotten root shell alive.
IdleTimeout time.Duration
// MaxDuration ends the session regardless of activity.
MaxDuration time.Duration
// Heartbeat is the ping interval detecting a silently vanished peer.
Heartbeat time.Duration
// OnHeartbeat rides that same beat to persist what only the control plane
// knows: that this session is still attached — and therefore, since
// ADR-067 §1, that its target is not idle. A beat is the only moment an
// attached session speaks to the control plane while a developer sits and
// reads, which is why the durable liveness stamp and the activity signal
// share one hook rather than growing a timer each.
//
// The EMPTY reason means "still attached", and it is also what a storage
// error must answer: the socket is the source of truth while this process
// lives, so the caller logs the failure and keeps the session. Any other
// value is reserved for a session that is durably over — another replica or
// the sweep finalized the row, or a re-claim superseded this attach — and it
// cuts the socket, which must not outlive its own authorization.
//
// It answers a REASON rather than a bool because the beat is where a close
// decided on another replica arrives, and the bridge is the one party that
// cannot know what that close was: the row was finalized elsewhere, this
// beat's update matched nothing, and the word it ended with is on the row
// the caller just failed to update. A bool could only say "it is over", and
// the bridge would then have to invent a word for it — `disconnect`, which
// blames the developer's own network for a container somebody stopped, a
// grant that expired or a wake that never came up. Only the caller can read
// that row, so only the caller may name it; the bridge reports what it is
// handed. That is tunnel.Options.OnHeartbeat's contract, word for word and
// on purpose: two bridges, one rule about what a beat means.
OnHeartbeat func(context.Context) EndReason
// Cancel ends the session from outside, naming the reason to report. A nil
// channel simply never fires, which is why the zero value is inert.
//
// It carries a reason rather than being a bare signal for the same reason
// the tunnel's does: cancelling the session's context also ends it, but it
// ends it as EndRevoked — which tells the developer an administrator acted
// when nobody did. A target that stopped under a live shell (ADR-067 §2)
// and an attach displaced by a re-claim (ADR-065 §5) are both cuts from
// outside, and neither is a revocation.
Cancel <-chan EndReason
}
Options bounds a session (§24.4). Zero values fall back to defaults.