term

package
v0.18.1 Latest Latest
Warning

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

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

Documentation

Overview

Package term owns the PTY sessions `gadak serve` runs next to the mirror — one session core for all three surfaces (the web pane, the desktop app, and a phone over the paired network). Only renderers differ; the shell, its lifetime, and its byte pump live here (GDK-862). Nothing in this package speaks HTTP: the WebSocket that carries these bytes is internal/server's, and the VT/render half is web-side.

The contracts, in one place, because every one of them is pinned by a test in this package:

  • Shell. $SHELL, else /bin/sh, started under a PTY in its own session (Setsid + Setctty) so the child is a session leader and this PTY is its controlling terminal. A job-control shell puts background jobs in a new process group, so a signal to -pgid does not reach them. cwd is the workspace directory unless Options.Dir names another. Env is the parent's plus TERM=xterm-256color and GADAK_TERMINAL=1.

  • Close. SIGHUP every process on the shell's controlling terminal, then wait; a SIGKILL to whoever is still on that terminal after CloseGrace (2s). No zombies and no orphaned grandchildren — TestCloseKillsHUPImmuneGrandchild pins a grandchild that ignores SIGHUP, which bash's own HUP-to-jobs cannot hide.

  • Ring. Each session keeps the last DefaultRingBytes (256 KiB) of output. Attach replays that buffer as the first chunk a reader sees, then live bytes: a client that reconnects inside the grace picks up its scrollback instead of a blank screen.

  • Backpressure. Every attachment has its own bounded channel (DefaultAttachBuffer chunks). When it is full the attachment is dropped — closed with a reason — and the PTY read loop keeps going. A slow client never stalls the shell and never delays another client. The one thing that is never dropped is the PTY.

  • Reconnect. A session survives its last attachment leaving for DefaultGrace (60s), then is reaped (Close). Reattaching by session id inside the grace cancels the reap and replays the ring. A session that still has an attachment never reaps, and an attached idle session is not timed out in v0.18.

  • Ids are 128 bits of crypto/rand, hex. Never sequential: a session id is the only thing a socket URL carries.

  • Revocation. Sessions record the pairing token id they were opened with (empty for a loopback client, which needs no token). Manager .CloseByToken is how `gadak pairing revoke` reaches a live shell — see internal/server's watchdog for who calls it.

  • Windows returns ErrUnsupportedPlatform from Create, naming GDK-861 (the ConPTY shape). An honest stub beats a silent one.

Snapshot() is the debug surface: per-session id, pid, pids (every process on the session's controlling terminal), size, attachment count, createdAt, lastOutputAt, bytesOut, droppedAttachments. It carries no output bytes and no token id, so it is safe to serve.

Parsers for the Linux /proc/<pid>/stat line. Deliberately without a //go:build linux tag, and named without a platform suffix while its callers (members_linux.go) carry one: these are pure functions over a string, so tagging them would only stop them being tested anywhere but Linux. `go test ./...` runs on ubuntu in CI, but a change made on a mac would then have no local gate at all.

Index

Constants

View Source
const (
	// DefaultRingBytes is the scrollback a session replays on reattach.
	DefaultRingBytes = 256 << 10
	// DefaultAttachBuffer is how many output chunks one attachment may
	// fall behind before it is dropped.
	DefaultAttachBuffer = 256
	// DefaultGrace is how long a session outlives its last attachment.
	DefaultGrace = 60 * time.Second
	// CloseGrace is how long Close waits after SIGHUP before SIGKILL.
	CloseGrace = 2 * time.Second
)

Defaults named in doc.go. They are package constants so the contract has one owner; Config overrides them for tests (a 60-second grace is not something a test may sleep through).

View Source
const (
	// ReasonSlow: the client fell further behind than its channel bound.
	// The attachment is dropped; the PTY is never stalled for it.
	ReasonSlow = "slow_client"
	// ReasonRevoked: the pairing token this session was opened with is no
	// longer active.
	ReasonRevoked = "token_revoked"
	// ReasonReaped: nothing was attached for the reconnect grace.
	ReasonReaped = "idle_timeout"
	// ReasonShutdown: the serve is going away.
	ReasonShutdown = "server_shutdown"
	// ReasonClosed: an explicit DELETE or Close.
	ReasonClosed = "closed"
)

Why an attachment ended, as the socket reports it to its client.

Variables

View Source
var ErrNotFound = errors.New("term: no such session")

ErrNotFound is Get/Close for a session id the manager does not hold — including one already reaped after its grace.

View Source
var ErrSessionClosed = errors.New("term: session closed")

ErrSessionClosed is a write or resize on a session whose shell is gone.

View Source
var ErrUnsupportedPlatform = errors.New("term: no PTY on this platform")

ErrUnsupportedPlatform is Create's answer where gadak has no PTY yet.

Functions

func SessionMembers added in v0.18.1

func SessionMembers(shellPID int) []int

SessionMembers returns every process whose controlling terminal is the same device as shellPID's, including the shell itself. The empty set means the pid is gone or has no controlling terminal: a zero (or, on darwin, -1) device is not a key, because that would sweep every daemon.

Types

type Attachment

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

Attachment is one client's view of a session's output: the ring replayed as the first chunk, then live bytes, then an End.

C() is not closed when the attachment ends — Done() is the signal, and chunks already buffered stay readable after it so a socket can flush what it has before sending its close frame.

func (*Attachment) C

func (a *Attachment) C() <-chan []byte

C yields output chunks, oldest first.

func (*Attachment) Detach

func (a *Attachment) Detach()

Detach lets this client go without touching the session. The reconnect grace starts if it was the last one.

func (*Attachment) Done

func (a *Attachment) Done() <-chan struct{}

Done closes when this attachment ends, for any of the four reasons.

func (*Attachment) End

func (a *Attachment) End() End

End is why it ended. Read it after Done.

type Config

type Config struct {
	// WorkDir is the cwd every session starts in unless Options.Dir names
	// another — the workspace directory in `gadak serve`.
	WorkDir string
	// Grace is how long a session outlives its last attachment.
	Grace time.Duration
	// RingBytes is the per-session scrollback.
	RingBytes int
	// AttachBuffer is the per-attachment channel bound.
	AttachBuffer int
	// AfterFunc is time.AfterFunc, injectable so a test can pin the
	// reconnect grace without sleeping a minute.
	AfterFunc func(time.Duration, func()) *time.Timer
	// Now is time.Now, injectable for the same reason.
	Now func() time.Time
}

Config tunes a Manager. Zero values take the package defaults, so New(Config{}) is the production shape.

type End

type End struct {
	Kind   EndKind
	Code   int
	Reason string
}

End is the terminal event of one attachment.

type EndKind

type EndKind int

EndKind is why an Attachment stopped.

const (
	// EndDetached: this client let go. The session may still be running.
	EndDetached EndKind = iota
	// EndExited: the shell exited; Code is its status.
	EndExited
	// EndDropped: backpressure. Reason says which.
	EndDropped
	// EndClosed: the session was closed out from under the client.
	// Reason says why (revoked, reaped, shutdown, explicit close).
	EndClosed
)

type Info

type Info struct {
	ID                 string    `json:"id"`
	PID                int       `json:"pid"`
	Cols               uint16    `json:"cols"`
	Rows               uint16    `json:"rows"`
	Attached           int       `json:"attached"`
	CreatedAt          time.Time `json:"created_at"`
	LastOutputAt       time.Time `json:"last_output_at"`
	BytesOut           int64     `json:"bytes_out"`
	DroppedAttachments int       `json:"dropped_attachments"`
	Exited             bool      `json:"exited"`
	ExitCode           int       `json:"exit_code"`
	// PIDs is every process currently on this session's controlling
	// terminal, including the shell. Empty when the enumerator cannot
	// see a tty (Windows, or a pid with no controlling terminal).
	PIDs []int `json:"pids,omitempty"`
}

Info is one row of Snapshot: everything a `gadak terminal list` needs and nothing a socket carries. No output bytes, no token id.

type Manager

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

Manager owns every live session in this process.

func New

func New(cfg Config) *Manager

New returns a Manager. It starts no goroutines: a Manager with no sessions costs nothing, which is what every `gadak serve` that never opens a terminal should pay.

func (*Manager) CloseAll

func (m *Manager) CloseAll()

CloseAll reaps every session. Called on server shutdown: an exiting serve must not leave shells behind.

func (*Manager) CloseByToken

func (m *Manager) CloseByToken(tokenID string) int

CloseByToken reaps every session opened with tokenID. This is what `gadak pairing revoke` reaches through: a revoked terminal token must lose the shell it opened, not just the next request. An empty tokenID matches nothing — loopback sessions are not token-bound and revoking a token must not cut the local pane.

func (*Manager) Create

func (m *Manager) Create(opts Options) (*Session, error)

Create spawns a shell under a PTY and returns its session.

func (*Manager) Get

func (m *Manager) Get(id string) (*Session, error)

Get returns a live session by id.

func (*Manager) List

func (m *Manager) List() []*Session

List returns every live session, oldest first.

func (*Manager) Snapshot

func (m *Manager) Snapshot() []Info

Snapshot is the introspection surface — see doc.go.

func (*Manager) TokenIDs

func (m *Manager) TokenIDs() []string

TokenIDs is the distinct set of non-empty token ids live sessions were opened with — what the revoke watchdog re-checks against the store.

type Options

type Options struct {
	// Dir overrides Config.WorkDir for this session.
	Dir string
	// Cols and Rows are the initial PTY size. Zero takes 80x24 — a shell
	// with a zero-sized terminal draws nothing and is a support ticket.
	Cols, Rows uint16
	// Env is appended after the inherited environment and the two
	// variables this package always sets.
	Env []string
	// Shell overrides $SHELL. Tests use it; nothing in the product does.
	Shell string
	// Args are the shell's arguments.
	Args []string
	// TokenID is the pairing token this session was opened with — empty
	// for a loopback client, which needs none. CloseByToken reads it.
	TokenID string
}

Options is one Create call.

type Session

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

Session is one shell under one PTY.

func (*Session) Attach

func (s *Session) Attach() (*Attachment, error)

Attach returns a reader that first yields the ring, then live output. Attaching cancels a pending reap.

func (*Session) Close

func (s *Session) Close() error

Close reaps the session: SIGHUP every process on the shell's controlling terminal, then SIGKILL whoever is still there after CloseGrace if the pump has not finished.

func (*Session) Done

func (s *Session) Done() <-chan struct{}

Done closes when the shell is gone and every attachment has been told.

func (*Session) ID

func (s *Session) ID() string

ID is the session id a socket URL carries.

func (*Session) Info

func (s *Session) Info() Info

Info is this session's Snapshot row.

func (*Session) Members added in v0.18.1

func (s *Session) Members() []int

Members is the process set this session currently holds: every pid whose controlling terminal is the shell's. One call, no HTTP.

func (*Session) PID

func (s *Session) PID() int

PID is the shell's process id — also its process-group id, because the child is started with Setsid.

func (*Session) Resize

func (s *Session) Resize(cols, rows uint16) error

Resize sets the PTY window size; on unix the child receives SIGWINCH.

func (*Session) TokenID

func (s *Session) TokenID() string

TokenID is the pairing token this session was opened with, empty for a loopback client. Not exposed by Snapshot.

func (*Session) Write

func (s *Session) Write(p []byte) (int, error)

Write sends bytes to the shell's stdin.

Jump to

Keyboard shortcuts

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