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
- Variables
- func SessionMembers(shellPID int) []int
- type Attachment
- type Config
- type End
- type EndKind
- type Info
- type Manager
- type Options
- type Session
- func (s *Session) Attach() (*Attachment, error)
- func (s *Session) Close() error
- func (s *Session) Done() <-chan struct{}
- func (s *Session) ID() string
- func (s *Session) Info() Info
- func (s *Session) Members() []int
- func (s *Session) PID() int
- func (s *Session) Resize(cols, rows uint16) error
- func (s *Session) TokenID() string
- func (s *Session) Write(p []byte) (int, error)
Constants ¶
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).
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 ¶
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.
var ErrSessionClosed = errors.New("term: session closed")
ErrSessionClosed is a write or resize on a session whose shell is gone.
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
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) 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.
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 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 ¶
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 ¶
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.
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 ¶
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) Members ¶ added in v0.18.1
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 ¶
PID is the shell's process id — also its process-group id, because the child is started with Setsid.