Documentation
¶
Overview ¶
Package session manages persistent PTY shell sessions that survive SSH disconnects.
Two diagrams — one per direction — because they are almost independent paths.
Input: user keystrokes → shell stdin
┌──────┐ SSH ┌────────┐ WriteInput() ┌────────┐ ┌────────┐ │ user │ channel │ Client │ ──(writer only)─► │ PTY │ ──► │ shell │ │ term │ ─────────► │ │ → s.ptmx.Write │ master │ │ stdin │ └──────┘ └────────┘ └────────┘ └────────┘
Only the Client currently marked as writer gets to push bytes into the PTY. Non-writer clients are read-only while they remain non-writers. If the writer detaches, the most recently attached remaining client is promoted.
Output: shell stdout → user terminal (+ VTE side channel)
┌────────┐ ┌────────┐ ┌──────────────┐ deliver() ┌─────────┐ SSH ┌──────┐
│ shell │ ─► │ PTY │ ──► │ pump() │ ─── fan-out ───► │ Client₁ │ ─────► │ user │
│ stdout │ │ master │ │ ptmx.Read() │ (N clients) │ Client₂ │ chan │ term │
└────────┘ └────────┘ └──────┬───────┘ │ ... │ └──────┘
│ └─────────┘
│ under s.mu
▼
┌─────────────┐ response pipe ┌──────────┐
│ vte.Write() │ ────────────────► │ drainVTE │ (discarded,
│ updates: │ (DSR/DA/CPR etc) └──────────┘ safety valve)
│ • screen │
│ • cursor │
│ • scrollbk │
│ • alt-scr │
└─────▲───────┘
│
│ snapshot on Attach() / re-attach
│ • renderVTEScrollback()
│ • vte.Render() (visible screen)
│ • vte.CursorPosition()
▼
┌─────────────┐
│ replay blob │ → prepended to the new client's
│ ESC c + │ output stream in normal-screen
│ scrollback +│ mode, so its terminal is
│ screen + │ restored across reconnects
│ cursor pos │
└─────────────┘
Three things worth calling out:
The VTE is on a side branch of the output pump, not in series. Live clients get the raw PTY bytes unmodified. The VTE is fed on every pump read and is continuously drained by drainVTE, but its rendered screen, cursor position, and scrollback are only consulted on Attach() to synthesize replay state. If nobody ever re-attaches, the VTE is mostly just absorbing output to preserve future replay state.
The normal-screen replay path uses the blob shown above: terminal reset, rendered scrollback, visible screen, and cursor position. Alternate-screen sessions take a different path. When vte.IsAltScreen() is true, Attach() sends enter-alt-screen + clear to the new client, writes Ctrl-L directly to the PTY, and relies on the running application to redraw.
Terminal queries get answered by the writer client's real terminal, not by the VTE. When an app writes ESC[6n, those bytes flow to the VTE and to every attached client's SSH channel. A real terminal emulator can respond on its input stream, but only the writer client's response can pass WriteInput() and reach the PTY; non-writer responses are rejected as read-only. The VTE's own response goes into drainVTE and is thrown away. With no client attached, TUI queries go unanswered. The drain exists to keep vte.Write from blocking on a full response pipe.
Index ¶
- func MergeEnv(sessionEnv []string) []string
- type Client
- type Manager
- type Session
- func (s *Session) Attach(cols, rows uint16) *Client
- func (s *Session) Detach(c *Client)
- func (s *Session) Exited() bool
- func (s *Session) ID() string
- func (s *Session) Info() SessionInfo
- func (s *Session) Resize(c *Client, cols, rows uint16)
- func (s *Session) TakeOver(c *Client, cols, rows uint16)
- func (s *Session) WriteInput(c *Client, p []byte) (int, error)
- type SessionInfo
- type Status
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client represents a connection to a session. It implements io.ReadWriteCloser. Reads return PTY output; writes send PTY input (only if this client is the session's writer).
func (*Client) Read ¶
Read returns the next chunk of PTY output. It blocks until data is available or the client is closed. Supports partial reads — if the caller's buffer is smaller than the available data, the remainder is preserved for the next Read call.
Read is not safe for concurrent use. It assumes a single reader goroutine (typically io.Copy in the SSH handler).
type Manager ¶
type Manager struct {
// Command is the shell command and arguments used for new sessions.
// Defaults to ["/bin/bash", "--login"] when empty.
Command []string
// contains filtered or unexported fields
}
Manager owns all sessions and enforces the concurrency limit.
func NewManager ¶
NewManager creates a session manager with the given maximum number of concurrent active sessions.
func (*Manager) Create ¶
Create starts a new shell session with the given terminal dimensions. The env parameter provides additional environment variables (e.g., from the SSH session) that are merged with the container's environment.
func (*Manager) List ¶
func (m *Manager) List() []SessionInfo
List returns info snapshots for all current sessions.
func (*Manager) SetStateFilePath ¶
SetStateFilePath sets the path where session state is written on changes.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session represents a persistent PTY shell session that survives client disconnects. It manages the shell process, PTY, attached clients, and output fan-out.
func (*Session) Attach ¶
Attach creates a new client and attaches it to this session. The first client to attach becomes the writer. If cols/rows are provided and this client becomes the writer, the PTY is resized.
On attach, the client receives a replay of scrollback history plus the current VTE screen state so the terminal appears restored.
func (*Session) Detach ¶
Detach removes a client from the session. If the detached client was the writer, the most recently attached remaining client is promoted.
func (*Session) Info ¶
func (s *Session) Info() SessionInfo
Info returns a snapshot of the session's current state.
func (*Session) Resize ¶
Resize changes the PTY dimensions. Only the writer client may resize. Resizes that don't change the dimensions are dropped to avoid redundant ioctls and VTE allocations from a chatty client.