terminal

package
v0.0.0-...-acf2466 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

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

View Source
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

View Source
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

func SendEnd(conn Conn, reason EndReason, msg string)

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).

func Bridge

func Bridge(ctx context.Context, conn Conn, pty PTY, opts Options) EndReason

Bridge pumps bytes between conn and pty until one side ends the session, then closes the pty — the guaranteed kill of §24.4 — and reports why. The caller owns the WebSocket close handshake and the session row.

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) Close

func (c *HTTPConn) Close() error

Close tears both halves down once, and wakes any pending Read.

func (*HTTPConn) Ping

func (c *HTTPConn) Ping(ctx context.Context) error

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.

func (*HTTPConn) Read

func (c *HTTPConn) Read(ctx context.Context) (MessageType, []byte, error)

Read returns the next message from either half.

func (*HTTPConn) Write

func (c *HTTPConn) Write(ctx context.Context, typ MessageType, data []byte) error

Write routes on the type: a control message travels on the session request, bytes on the data stream. Nothing is re-framed — that is the whole point of carrying the two on two wires (ADR-064 §3).

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.

type PTY

type PTY interface {
	io.ReadWriteCloser
	Resize(cols, rows int) error
}

PTY is the remote pseudo-terminal end of the bridge (sshexec.PTY in production). Read returning io.EOF means the remote command exited.

Jump to

Keyboard shortcuts

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