Add ARCHITECTURE.md documentation

Comprehensive architecture document covering:
- System overview with ASCII diagram
- Core components and their responsibilities
- Data flow for terminal I/O, screenshots, and SSE
- Session lifecycle
- WebSocket protocol specification
- API endpoints reference
- Key design decisions and rationale
- File structure overview
This commit is contained in:
GitHub Copilot
2026-01-24 20:28:56 +00:00
parent ba23994c68
commit 45645ab724
+232
View File
@@ -0,0 +1,232 @@
# Architecture
This document describes the internal architecture of textual-webterm.
## Overview
textual-webterm is a web-based terminal server that exposes terminal sessions (or Textual apps) over HTTP and WebSocket. It's designed to run behind a reverse proxy with authentication.
```
┌─────────────┐ ┌─────────────────────────────────────────────────┐
│ Browser │────▶│ local_server.py │
│ │◀────│ (aiohttp web server) │
└─────────────┘ │ │
│ │ ┌─────────────┐ ┌──────────────────────────┐ │
│ WebSocket │ │ session_ │ │ terminal_session.py │ │
└────────────▶│ │ manager.py │──│ (PTY + pyte emulator) │ │
│ └─────────────┘ └──────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────────────────┐ │
│ │ poller.py │ │ docker_stats.py │ │
│ │ (I/O thread)│ │ (CPU metrics via socket) │ │
│ └─────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
## Core Components
### local_server.py
The main HTTP/WebSocket server built on aiohttp. Handles:
- **HTTP routes**: Dashboard, screenshots, sparklines, SSE events, health checks
- **WebSocket connections**: Terminal I/O multiplexing with JSON protocol
- **Screenshot caching**: Time-based and change-based cache invalidation
- **SSE broadcasting**: Real-time activity notifications to dashboard
Key classes:
- `Server`: Main server class managing routes and session lifecycle
### session_manager.py
Manages the mapping between route keys and sessions:
- **TwoWayDict**: Bidirectional mapping of RouteKey ↔ SessionID
- **Session creation**: Creates TerminalSession or AppSession on demand
- **App registry**: Stores app configurations from manifest files
### terminal_session.py
Manages a single terminal session:
- **PTY management**: Fork/exec with pseudo-terminal
- **pyte emulator**: Interprets ANSI escape sequences for screen state
- **Replay buffer**: 64KB ring buffer for reconnection support
- **Resize handling**: Propagates window size changes to PTY
The pyte screen buffer provides character-level access for screenshots.
### poller.py
Background thread for non-blocking PTY I/O:
- **selector-based**: Uses `selectors.DefaultSelector` for efficient I/O
- **Async queues**: Bridges sync I/O thread to async main loop
- **Write queuing**: Handles backpressure for terminal input
### svg_exporter.py
Custom SVG renderer for terminal screenshots:
- **Per-character positioning**: Each character has explicit x coordinate
- **Box-drawing scaling**: Vertical 1.2x scale for line-height alignment
- **Color handling**: ANSI 16-color palette + 256-color + truecolor
- **Wide character support**: Proper column tracking for CJK characters
### docker_stats.py
Collects CPU metrics from Docker containers:
- **Unix socket client**: Direct HTTP-over-Unix-socket to Docker API
- **Compose awareness**: Filters containers by compose project label
- **History buffer**: 180 samples (30 min at 10s intervals)
- **Sparkline SVG**: Renders mini CPU graphs
## Data Flow
### Terminal I/O
```
Browser Server PTY
│ │ │
│──["stdin", "ls\n"]──────▶│ │
│ │────write(b"ls\n")───────▶│
│ │ │
│ │◀───read(output)──────────│
│◀─["stdout", "..."]───────│ │
```
### Screenshot Generation
```
Dashboard ──GET /screenshot.svg──▶ Server
TerminalSession
get_screen_state()
pyte.Screen
.buffer
svg_exporter.py
render_terminal_svg()
<svg>...</svg>
```
### SSE Activity Updates
```
Dashboard ──GET /events──▶ Server (SSE connection held open)
Terminal activity ───────────▶│
Broadcast to all SSE clients:
data: {"route_key": "...", "type": "activity"}
```
## Session Lifecycle
1. **Browser connects** to `/ws/{route_key}`
2. **SessionManager** looks up or creates session for route_key
3. **TerminalSession** forks PTY process, initializes pyte emulator
4. **Poller** registers PTY fd for I/O events
5. **WebSocket handler** bridges browser ↔ PTY via JSON messages
6. **On disconnect**: Session stays alive; browser can reconnect
7. **On reconnect**: Replay buffer restores recent output
## Configuration
### Manifest-based (--landing-manifest)
```yaml
- name: Display Name
slug: route-key
command: /path/to/command
```
### Compose-based (--compose-manifest)
Reads docker-compose.yaml, creates tiles for services with `webterm-command` label:
```yaml
services:
myservice:
labels:
webterm-command: docker exec -it myservice bash
```
## WebSocket Protocol
JSON-encoded messages over WebSocket:
| Direction | Message | Description |
|-----------|---------|-------------|
| Client→Server | `["stdin", "data"]` | Terminal input |
| Client→Server | `["resize", {"width": N, "height": M}]` | Window resize |
| Client→Server | `["ping", data]` | Keep-alive |
| Server→Client | `["stdout", "data"]` | Terminal output |
| Server→Client | `["pong", data]` | Keep-alive response |
## API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/` | GET | Dashboard HTML or terminal redirect |
| `/ws/{route_key}` | WS | WebSocket terminal connection |
| `/screenshot.svg` | GET | SVG screenshot (query: `route_key`) |
| `/cpu-sparkline.svg` | GET | CPU sparkline (query: `container`) |
| `/events` | GET | SSE stream for activity updates |
| `/health` | GET | Health check |
## Key Design Decisions
### Why custom SVG exporter?
Rich's `export_svg()` had alignment issues with box-drawing characters and varied font rendering across browsers. The custom exporter:
- Positions each character individually for pixel-perfect alignment
- Scales box-drawing characters vertically to fill line height
- Uses explicit x coordinates instead of relying on font metrics
### Why pyte?
pyte provides a pure-Python terminal emulator that tracks screen state character-by-character, enabling:
- Screenshot generation without screen scraping
- Dirty tracking for efficient cache invalidation
- Full ANSI/VT100 escape sequence support
### Why session persistence?
Unlike traditional web terminals, sessions survive page refreshes:
- Replay buffer allows catching up on missed output
- SessionManager keeps sessions alive until explicit close
- Enables dashboard with multiple live terminal thumbnails
## File Structure
```
src/textual_webterm/
├── cli.py # Typer CLI entry point
├── config.py # Configuration parsing (YAML manifests)
├── local_server.py # Main HTTP/WebSocket server
├── session_manager.py # Session registry and routing
├── session.py # Abstract session interface
├── terminal_session.py # PTY-based terminal session
├── app_session.py # Textual app session
├── poller.py # Async I/O polling thread
├── svg_exporter.py # Terminal→SVG renderer
├── docker_stats.py # Docker CPU metrics collector
├── exit_poller.py # Graceful shutdown handling
├── identity.py # Session ID generation
├── slugify.py # URL-safe slug generation
├── types.py # Type aliases
└── constants.py # Platform constants
```