term

package
v0.18.0 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: 11 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 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

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

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) 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"`
}

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