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