Help us shape the future of Go! Take the 2026 Go Developer Survey

attach

package
v0.38.0 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package attach is the local terminal's side of a session: an SSH client of the session's attach socket. upterm host, upterm attach and the functional tests all use it, so the local terminal is just another client in the code rather than in a design document.

Index

Constants

This section is empty.

Variables

View Source
var ErrHostKeyMismatch = errors.New("host key is not the daemon's")

ErrHostKeyMismatch is what Run's error wraps when the door presented a key that is not the daemon's. It is exported so a caller can tell this apart from every other reason an attachment did not happen: those are worth retrying, and this one is worth stopping for — whatever answered the socket is not the session, and the terminal must not be handed to it. x/crypto returns the callback's error as it is and wraps it with %w, so errors.Is finds it through the handshake's own message.

Functions

This section is empty.

Types

type Client

type Client struct {
	Socket  string
	Stdin   io.Reader
	Stdout  io.Writer
	Pty     *Pty
	Resizes <-chan termsize.Size
	Escape  byte
	Logger  *slog.Logger

	// HostKeys is the daemon's public keys, from the session record: a record
	// from a current daemon carries exactly one entry, the session host key
	// it generated for that run; a record written by an older daemon may
	// carry several, which is why this is a slice. Run refuses to attach
	// without at least one: a client that accepted any key would hand its
	// terminal to whatever bound the socket.
	HostKeys []ssh.PublicKey

	// Suspend is the Terminal's: see Terminal.Suspend.
	Suspend func() (size termsize.Size, owned bool)
	// contains filtered or unexported fields
}

Client attaches a pair of streams to a running session.

func (*Client) Run

func (c *Client) Run(ctx context.Context) (Result, error)

Run attaches and returns when the attachment ends. An error means the client never attached; once attached, every ending is a Result.

type EscapeAction

type EscapeAction int

EscapeAction is what an escape sequence asked for.

const (
	// EscapeNone: nothing completed in this read.
	EscapeNone EscapeAction = iota
	// EscapeDetach: the caller should leave. Feed returns at once, so bytes
	// after the sequence are dropped — they were typed at a terminal that is
	// no longer attached to anything.
	EscapeDetach
	// EscapeSuspend: the caller should stop this process and resume the
	// attachment afterwards. Feed keeps going, so the rest of the read is
	// still the session's.
	EscapeSuspend
)

type EscapeFilter

type EscapeFilter struct {
	// contains filtered or unexported fields
}

EscapeFilter recognises ssh's detach and suspend conventions in a stream of keystrokes: an escape byte at the start of a line followed by '.' detaches; followed by '^Z' (0x1a) suspends, when suspend is enabled; followed by itself sends one literal escape byte; followed by anything else sends both. "Start of a line" is the beginning of input or the byte after a CR or LF — CR because a raw-mode terminal sends Enter as CR.

Bytes after a detach are dropped: they were typed at a terminal that is no longer attached to anything. Bytes after a suspend are not: they are still the session's, queued for delivery once the process resumes.

func NewEscapeFilter

func NewEscapeFilter(esc byte, suspend bool) *EscapeFilter

NewEscapeFilter returns a filter for esc. Zero disables it: Feed passes everything through. suspend says whether esc followed by ^Z is offered; when it is not, both bytes reach the session like any other unrecognised pair.

func (*EscapeFilter) Feed

func (f *EscapeFilter) Feed(p []byte) (out []byte, action EscapeAction)

Feed filters p, returning what should reach the session and what escape sequence, if any, completed. out may alias an internal buffer; copy it before the next call if it must be kept.

func (*EscapeFilter) Flush

func (f *EscapeFilter) Flush() []byte

Flush returns a held escape byte, for the end of input.

type Pty

type Pty struct {
	Term string
	Size termsize.Size
}

Pty is a pty request: the terminal type and geometry the client's own terminal has. A client that requests one is eligible to be the session's primary client; one that does not is a viewer.

type Reason

type Reason int

Reason says how an attachment ended.

const (
	// Detached: the client ended it — the escape key, EOF on Stdin, or ctx.
	Detached Reason = iota
	// Exited: the session sent the command's exit status and closed.
	Exited
	// Disconnected: the session closed the connection without an exit status.
	// The daemon's log says why: a stalled or overflowed client, or a daemon
	// that went away.
	Disconnected
)

func (Reason) String

func (r Reason) String() string

type Result

type Result struct {
	Reason Reason
	Status int

	// Released closes once no write from this session to Stdout is in
	// flight: the output copy has returned, and so has every mode write. It
	// is already closed when Terminal.Run returns, except after a drain or a
	// mode write that timed out, whose write is still parked.
	Released <-chan struct{}
	// Unrestored is the mode restore left unwritten because the output was
	// abandoned with the terminal in the session's modes: the drain timed
	// out, a suspend could not take the output from a write parked on the
	// terminal, or the write that put the modes back after a suspend did
	// not finish. Empty otherwise: the restore was written or attempted, the
	// process came back without the terminal, or there is no pty and so no
	// modes.
	Unrestored []byte
}

Result is how an attachment ended. Status is meaningful only for Exited. Released and Unrestored are Terminal.Run's; Client.Run leaves them zero.

type Terminal added in v0.37.0

type Terminal struct {
	Stdin   io.Reader
	Stdout  io.Writer
	Pty     *Pty
	Resizes <-chan termsize.Size
	Escape  byte

	// Suspend stops the process this client runs in and returns once it is
	// running again, with the terminal's size and whether the terminal is
	// this client's again — for a local terminal: restore the terminal,
	// SIGTSTP, re-enter raw mode if still the foreground, measure. owned is
	// false after bg: the terminal is the foreground job's, and the
	// attachment ends there, writing nothing more to it. Nil disables the ~^Z
	// sequence, and the bytes reach the session instead.
	//
	// It is the embedder's because what "suspend" means is the terminal's
	// business, not the protocol's: this package never touches termios and
	// never signals its own process.
	Suspend func() (size termsize.Size, owned bool)

	// DeclareInteractive sends upterm.AttachInteractiveEnvVar, which says
	// this client displays on a terminal and forwards its keystrokes, so it
	// can answer the terminal queries a primary is sent. Declared, because
	// the daemon cannot tell a terminal from a pipe on the far side of an SSH
	// channel — and a pty viewer that does not read its terminal would leave
	// the terminal's replies in the foreground shell as junk. Only the attach
	// socket reads it.
	DeclareInteractive bool

	// OnReady runs once, after the shell request is confirmed and before the
	// first byte of output is written to Stdout. Nil does nothing.
	OnReady func()

	Logger *slog.Logger
}

Terminal runs a terminal session on an established connection: the pty request, the shell, the I/O between the session and a pair of streams, the escapes, a suspend, and on the way out the drain and the mode restore. Client is a Terminal on the session's attach socket.

func (*Terminal) Run added in v0.37.0

func (t *Terminal) Run(ctx context.Context, client *ssh.Client, setupBy time.Time) (_ Result, err error)

Run opens a session on client and runs it until it ends. setupBy bounds every request before the shell runs: a peer that answered the handshake and then stopped answering would otherwise hold NewSession, RequestPty or Shell forever, so past it the connection is closed. A zero setupBy has long passed, so the connection is closed at once. An error means shell startup was not confirmed, and Run has closed client before returning it, whatever the cause; once startup was confirmed, every ending is a Result, and Run closes the connection as the session ends.

An error comes with the zero Result, whose Released is nil: there is no write to wait for, and a receive from it would never return.

Jump to

Keyboard shortcuts

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