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 process-group leader and a signal to -pgid reaches everything it spawned. 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 to the process group, then wait; a SIGKILL to the same group after CloseGrace (2s) if the pump has not finished. No zombies and no orphaned grandchildren — TestCloseKillsProcessGroup pins the grandchild.
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, size, attachment count, createdAt, lastOutputAt, bytesOut, droppedAttachments. It carries no output bytes and no token id, so it is safe to serve.
Index ¶
- Constants
- Variables
- 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) 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 ¶
This section is empty.
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"`
}
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 to the process group, then SIGKILL 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) PID ¶
PID is the shell's process id — also its process-group id, because the child is started with Setsid.