diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx index 37c4472e359f17..9dbaf6c0998e31 100644 --- a/src/content/docs/sandbox/api/index.mdx +++ b/src/content/docs/sandbox/api/index.mdx @@ -67,4 +67,12 @@ The Sandbox SDK provides a comprehensive API for executing code, managing files, Create isolated execution contexts within a sandbox. Each session maintains its own shell state, environment variables, and working directory. + + Manage pseudo-terminal (PTY) sessions for interactive command execution. Support real-time terminal I/O, collaborative editing, and advanced shell features. + + diff --git a/src/content/docs/sandbox/api/pty.mdx b/src/content/docs/sandbox/api/pty.mdx new file mode 100644 index 00000000000000..56a8aaf16f0295 --- /dev/null +++ b/src/content/docs/sandbox/api/pty.mdx @@ -0,0 +1,477 @@ +--- +title: PTY API +description: Manage pseudo-terminal (PTY) sessions for interactive command execution in Sandbox containers. +sidebar: + order: 7 +head: + - tag: meta + attrs: + property: og:image + content: https://images.ctfassets.net/in6v9lxmm5c8/4MYWFGZNnhqxRfkDNDmWNm/e7e3d8b4b4b4b4b4b4b4b4b4/sandbox-social.png +--- + +import { TypeScriptExample } from "~/components"; + +The PTY API provides pseudo-terminal functionality for interactive command execution in Sandbox containers. PTY sessions enable real-time, bidirectional communication with terminal applications, supporting features like terminal resizing, signal handling, and proper terminal emulation. + +## Core Methods + +### `sandbox.pty.create()` + +Creates a new PTY session with configurable terminal dimensions and environment. + + + +```ts +const pty = await sandbox.pty.create({ + cols: 80, // Terminal width (default: 80) + rows: 24, // Terminal height (default: 24) + command: ['bash'], // Command to run (default: ['bash']) + cwd: '/workspace', // Working directory (default: /home/user) + env: { TERM: 'xterm-256color' } // Environment variables +}); + +// Subscribe to terminal output +pty.onData((data) => { + process.stdout.write(data); +}); + +// Send commands to the terminal +await pty.write('ls -la\n'); +await pty.write('cd /workspace && npm install\n'); +``` + + + +**Parameters:** + +- `options` (optional): PTY creation options + - `cols` (number): Terminal width in columns (default: 80) + - `rows` (number): Terminal height in rows (default: 24) + - `command` (string[]): Command to execute (default: ['bash']) + - `cwd` (string): Working directory (default: /home/user) + - `env` (object): Environment variables + - `sessionId` (string): Session ID for session attachment + +**Returns:** PTY handle for terminal interaction + +### `sandbox.pty.attach()` + +Attaches a PTY session to an existing sandbox session, inheriting its working directory and environment variables. + + + +```ts +// Create a session and set up environment +await sandbox.sessions.create('dev-session'); +await sandbox.exec('cd /workspace', { sessionId: 'dev-session' }); +await sandbox.exec('export NODE_ENV=development', { sessionId: 'dev-session' }); + +// Attach PTY to the session +const pty = await sandbox.pty.attach('dev-session', { + cols: 100, + rows: 30 +}); + +// PTY now inherits /workspace cwd and NODE_ENV=development +``` + + + +**Parameters:** + +- `sessionId` (string): Existing session ID to attach to +- `options` (optional): PTY creation options (cols, rows, command) + +**Returns:** PTY handle attached to the session + +### `sandbox.pty.getById()` + +Reconnects to an existing PTY session by its unique identifier. + + + +```ts +// Store PTY ID for later reconnection +const pty = await sandbox.pty.create(); +const ptyId = pty.id; + +// Later: reconnect to the same PTY +const reconnectedPty = await sandbox.pty.getById(ptyId); + +// Both handles control the same terminal session +await reconnectedPty.write('echo "Still connected!"\n'); +``` + + + +**Parameters:** + +- `id` (string): PTY session identifier + +**Returns:** PTY handle for the existing session + +### `sandbox.pty.list()` + +Lists all active PTY sessions with their metadata. + + + +```ts +const ptys = await sandbox.pty.list(); + +console.log(`Found ${ptys.length} active PTY sessions:`); +ptys.forEach(pty => { + console.log(`- ${pty.id}: ${pty.command.join(' ')} (${pty.cols}x${pty.rows})`); + console.log(` State: ${pty.state}, Created: ${pty.createdAt}`); + if (pty.exitCode !== undefined) { + console.log(` Exit code: ${pty.exitCode}`); + } +}); +``` + + + +**Returns:** Array of PTY information objects with metadata + +## PTY Handle Interface + +PTY handles provide methods for interacting with terminal sessions: + +### `write()` + +Sends input to the terminal. Returns a Promise for error handling. + + + +```ts +// Interactive typing (fire-and-forget) +pty.write('ls -la\n'); + +// Programmatic commands (await for errors) +try { + await pty.write('npm test\n'); +} catch (error) { + console.error('Failed to send command:', error); +} +``` + + + +**Parameters:** + +- `data` (string): Input to send to the terminal + +**Returns:** Promise that resolves on success + +### `resize()` + +Changes the terminal dimensions. Applications receive SIGWINCH signals to adapt to the new size. + + + +```ts +// Resize to accommodate wide terminal output +await pty.resize(120, 40); + +// Responsive terminal sizing +window.addEventListener('resize', async () => { + const cols = Math.floor(window.innerWidth / charWidth); + const rows = Math.floor(window.innerHeight / lineHeight); + await pty.resize(cols, rows); +}); +``` + + + +**Parameters:** + +- `cols` (number): New terminal width (1-1000) +- `rows` (number): New terminal height (1-1000) + +**Returns:** Promise that resolves on success + +### `kill()` + +Terminates the PTY process with an optional signal. + + + +```ts +// Graceful termination +await pty.kill('SIGTERM'); + +// Force kill if needed +await pty.kill('SIGKILL'); + +// Default termination +await pty.kill(); +``` + + + +**Parameters:** + +- `signal` (optional string): Signal name (SIGTERM, SIGKILL, etc.) + +**Returns:** Promise that resolves when terminated + +### Event Listeners + +#### `onData()` + +Subscribes to terminal output data. + + + +```ts +const unsubscribe = pty.onData((data) => { + // Forward output to xterm.js terminal + terminal.write(data); + + // Or process the output + if (data.includes('Error:')) { + console.error('Terminal error detected'); + } +}); + +// Later: stop listening +unsubscribe(); +``` + + + +**Parameters:** + +- `callback` (function): Handler for output data + +**Returns:** Unsubscribe function + +#### `onExit()` + +Handles PTY process termination. + + + +```ts +pty.onExit((exitCode) => { + if (exitCode === 0) { + console.log('Process completed successfully'); + } else { + console.error(`Process exited with code ${exitCode}`); + + // Check for signal termination + if (exitCode > 128) { + const signal = exitCode - 128; + console.log(`Killed by signal ${signal}`); + } + } +}); +``` + + + +**Parameters:** + +- `callback` (function): Handler for process exit + +**Returns:** Unsubscribe function + +### Properties + +#### `exited` + +Promise that resolves when the PTY process exits, providing the exit code. + + + +```ts +// Wait for process completion +const { exitCode } = await pty.exited; +console.log(`PTY exited with code ${exitCode}`); + +// Use with timeout for long-running processes +const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 30000) +); + +try { + const result = await Promise.race([pty.exited, timeout]); + console.log('Completed:', result.exitCode); +} catch (error) { + console.log('Still running after 30 seconds'); +} +``` + + + +### Async Iteration + +PTY handles support async iteration for scripting scenarios: + + + +```ts +async function processOutput() { + const pty = await sandbox.pty.create(); + pty.write('find /etc -name "*.conf"\n'); + + for await (const output of pty) { + if (output.includes('/etc/nginx/nginx.conf')) { + console.log('Found Nginx config'); + break; + } + } +} + +// Or collect all output +async function captureSession() { + const pty = await sandbox.pty.create(); + pty.write('git status && git log --oneline -5\n'); + pty.write('exit\n'); + + let fullOutput = ''; + for await (const chunk of pty) { + fullOutput += chunk; + } + return fullOutput; +} +``` + + + +## Advanced Usage + +### Real-time Terminal UI + +Build interactive terminal applications with live output streaming: + + + +```ts +import { Terminal } from 'xterm'; + +// Create xterm.js terminal +const terminal = new Terminal({ + cursorBlink: true, + theme: { + background: '#1a1a1a', + foreground: '#ffffff', + cursor: '#ffffff' + } +}); + +// Connect to PTY +const pty = await sandbox.pty.create({ + cols: terminal.cols, + rows: terminal.rows +}); + +// Bidirectional communication +pty.onData(data => terminal.write(data)); +terminal.onData(data => pty.write(data)); + +// Handle terminal resize +terminal.onResize(({ cols, rows }) => { + pty.resize(cols, rows); +}); +``` + + + +### Multi-user Collaboration + +Enable multiple users to share the same terminal session: + + + +```ts +// User 1: Create and share PTY +const pty = await sandbox.pty.create(); +const ptyId = pty.id; // Share this ID + +// All users: Connect to the same PTY +const sharedPty = await sandbox.pty.getById(ptyId); + +// Everyone sees the same output +sharedPty.onData(data => { + broadcastToAllUsers(data); +}); + +// Any user can send input +sharedPty.write(userInput); +``` + + + +### Session-aware PTY + +Combine PTY with sandbox sessions for persistent state: + + + +```ts +// Set up project environment in a session +await sandbox.sessions.create('project'); +await sandbox.exec('cd /workspace/my-project', { sessionId: 'project' }); +await sandbox.exec('source .env && export $(grep -v "^#" .env | xargs)', { + sessionId: 'project' +}); + +// Attach PTY to inherit the environment +const pty = await sandbox.pty.attach('project'); + +// PTY automatically has project directory and environment +``` + + + +## Error Handling + +PTY operations can fail due to various conditions: + + + +```ts +try { + const pty = await sandbox.pty.create(); + await pty.write('risky-command\n'); +} catch (error) { + if (error.message.includes('PTY is closed')) { + console.log('PTY was terminated externally'); + } else if (error.message.includes('Container not available')) { + console.log('Sandbox container is not running'); + } else { + console.error('Unexpected PTY error:', error); + } +} + +// Handle connection issues +pty.onExit(exitCode => { + if (exitCode === 137) { // SIGKILL + console.log('PTY was force-killed'); + } else if (exitCode === 143) { // SIGTERM + console.log('PTY terminated gracefully'); + } +}); +``` + + + +## Transport Requirements + +PTY operations require WebSocket connectivity for real-time bidirectional communication: + +- **WebSocket Support**: PTY automatically establishes WebSocket connections for streaming I/O +- **Keepalive**: The client sends periodic HTTP pings to prevent container sleep during active PTY sessions +- **Connection Management**: WebSocket connections are automatically managed and reconnected as needed + +For environments without WebSocket support, consider using the standard command execution API with streaming options. + +## Limits and Considerations + +- **Terminal Dimensions**: Width and height must be between 1-1000 columns/rows +- **Concurrent PTYs**: Multiple PTY sessions can run simultaneously per sandbox +- **Memory Usage**: PTY sessions buffer output for reconnection scenarios +- **Container Lifecycle**: PTY sessions are terminated when the sandbox container stops +- **Signal Support**: Standard POSIX signals are supported (SIGTERM, SIGKILL, SIGINT, etc.) \ No newline at end of file diff --git a/src/content/docs/sandbox/tutorials/interactive-terminal.mdx b/src/content/docs/sandbox/tutorials/interactive-terminal.mdx new file mode 100644 index 00000000000000..8931c4cc3f32d8 --- /dev/null +++ b/src/content/docs/sandbox/tutorials/interactive-terminal.mdx @@ -0,0 +1,640 @@ +--- +title: Build an interactive terminal +description: Create real-time terminal applications using PTY (pseudo-terminal) functionality in Cloudflare Sandbox. +sidebar: + order: 8 +--- + +import { TypeScriptExample, PackageManagers, WranglerConfig } from "~/components"; + +This tutorial shows how to build interactive terminal applications using Cloudflare Sandbox's PTY (pseudo-terminal) functionality. You will create a web-based terminal that provides real-time command execution with proper terminal emulation. + +## What you will learn + +- How to create and manage PTY sessions +- Building real-time terminal UIs with WebSocket streaming +- Handling terminal input/output and resize events +- Implementing collaborative terminal sharing + +## Prerequisites + +- Cloudflare account with Workers and Containers enabled +- Node.js 18 or later installed locally +- Basic knowledge of TypeScript and React + +## Project setup + +Create a new project directory and initialize it: + + + +Navigate to your project and install dependencies: + + + +## Configure Wrangler + +Update your `wrangler.toml` to include the Sandbox binding: + + + +```toml +name = "my-terminal-app" +compatibility_date = "2024-01-01" + +[env.dev] +[[env.dev.durable_objects.bindings]] +name = "Sandbox" +class_name = "Container" +script_name = "containers" +``` + + + +## Create the Worker backend + +Create `src/index.ts` for your Worker: + + + +```ts +import { getSandbox } from '@cloudflare/sandbox'; + +export default { + async fetch(request, env, ctx): Promise { + const url = new URL(request.url); + + // Handle WebSocket connections for terminal streaming + if (url.pathname === '/ws' && request.headers.get('Upgrade') === 'websocket') { + return handleWebSocket(request, env); + } + + // Handle terminal creation + if (url.pathname === '/api/terminal' && request.method === 'POST') { + return handleCreateTerminal(request, env); + } + + // Serve static files or your frontend + return new Response('Terminal App', { headers: { 'Content-Type': 'text/html' } }); + }, +} satisfies ExportedHandler; + +async function handleCreateTerminal(request: Request, env: Env): Promise { + const { cols, rows } = await request.json(); + + // Get sandbox instance + const sandbox = getSandbox(env.Sandbox, 'terminal-sandbox'); + + // Create PTY session + const pty = await sandbox.pty.create({ + cols: cols || 80, + rows: rows || 24, + command: ['bash'] + }); + + return Response.json({ + success: true, + ptyId: pty.id, + message: 'Terminal created successfully' + }); +} + +async function handleWebSocket(request: Request, env: Env): Promise { + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair); + + server.accept(); + + // Set up PTY connection when client sends ptyId + server.addEventListener('message', async (event) => { + const message = JSON.parse(event.data as string); + + if (message.type === 'connect' && message.ptyId) { + await setupPtyConnection(server, message.ptyId, env); + } + }); + + return new Response(null, { + status: 101, + webSocket: client, + }); +} + +async function setupPtyConnection(websocket: WebSocket, ptyId: string, env: Env) { + const sandbox = getSandbox(env.Sandbox, 'terminal-sandbox'); + + try { + // Get existing PTY + const pty = await sandbox.pty.getById(ptyId); + + // Forward PTY output to WebSocket + pty.onData((data) => { + websocket.send(JSON.stringify({ + type: 'output', + data: data + })); + }); + + // Handle PTY exit + pty.onExit((exitCode) => { + websocket.send(JSON.stringify({ + type: 'exit', + exitCode: exitCode + })); + }); + + // Handle WebSocket messages (terminal input) + websocket.addEventListener('message', async (event) => { + const message = JSON.parse(event.data as string); + + switch (message.type) { + case 'input': + await pty.write(message.data); + break; + + case 'resize': + await pty.resize(message.cols, message.rows); + break; + } + }); + + } catch (error) { + websocket.send(JSON.stringify({ + type: 'error', + message: error.message + })); + } +} + +interface Env { + Sandbox: DurableObjectNamespace; +} +``` + + + +## Build the frontend terminal + +Create `src/terminal.html` for the terminal interface: + + + +```html + + + + + Interactive Terminal + + + + + Interactive Terminal + + + Connect + Disconnect + Clear + + + + + + + + + +``` + + + +## Enhanced features + +### Add collaboration support + +Enable multiple users to share the same terminal session: + + + +```ts +// In your Worker, modify the WebSocket handler +const sessions = new Map>(); + +async function handleCollaborativeTerminal(websocket: WebSocket, ptyId: string, env: Env) { + // Add user to session + if (!sessions.has(ptyId)) { + sessions.set(ptyId, new Set()); + } + sessions.get(ptyId)!.add(websocket); + + const sandbox = getSandbox(env.Sandbox, 'terminal-sandbox'); + const pty = await sandbox.pty.getById(ptyId); + + // Broadcast output to all connected users + pty.onData((data) => { + const sessionSockets = sessions.get(ptyId); + if (sessionSockets) { + sessionSockets.forEach(ws => { + if (ws.readyState === WebSocket.READY_STATE_OPEN) { + ws.send(JSON.stringify({ type: 'output', data })); + } + }); + } + }); + + // Handle user input (any user can type) + websocket.addEventListener('message', async (event) => { + const message = JSON.parse(event.data as string); + if (message.type === 'input') { + await pty.write(message.data); + + // Broadcast typing indicator + const sessionSockets = sessions.get(ptyId); + sessionSockets?.forEach(ws => { + if (ws !== websocket && ws.readyState === WebSocket.READY_STATE_OPEN) { + ws.send(JSON.stringify({ + type: 'user_typing', + user: message.userId + })); + } + }); + } + }); + + // Clean up on disconnect + websocket.addEventListener('close', () => { + sessions.get(ptyId)?.delete(websocket); + }); +} +``` + + + +### Add terminal recording + +Capture and replay terminal sessions: + + + +```ts +class TerminalRecorder { + private events: Array<{ timestamp: number; type: 'output' | 'input'; data: string }> = []; + private startTime: number = Date.now(); + + recordOutput(data: string) { + this.events.push({ + timestamp: Date.now() - this.startTime, + type: 'output', + data: data + }); + } + + recordInput(data: string) { + this.events.push({ + timestamp: Date.now() - this.startTime, + type: 'input', + data: data + }); + } + + async replay(terminal: Terminal, speed: number = 1) { + for (const event of this.events) { + await new Promise(resolve => setTimeout(resolve, event.timestamp / speed)); + + if (event.type === 'output') { + terminal.write(event.data); + } + } + } + + export(): string { + return JSON.stringify({ + version: 1, + startTime: this.startTime, + events: this.events + }); + } + + static import(data: string): TerminalRecorder { + const parsed = JSON.parse(data); + const recorder = new TerminalRecorder(); + recorder.events = parsed.events; + recorder.startTime = parsed.startTime; + return recorder; + } +} +``` + + + +## Testing your terminal + +Deploy your application: + + + +Open your browser and navigate to your local development URL. You should see: + +1. **Terminal Interface**: A web-based terminal with xterm.js rendering +2. **Real-time I/O**: Commands execute immediately with live output streaming +3. **Terminal Features**: Support for colors, cursor movement, and control sequences +4. **Resize Handling**: Terminal automatically adapts to window size changes + +## Advanced use cases + +### Development environments + +Create cloud-based development environments: + + + +```ts +// Set up development environment +const setupDevEnvironment = async (sandbox: Sandbox) => { + // Install tools + await sandbox.exec('apt-get update && apt-get install -y git vim tmux'); + + // Clone repository + await sandbox.exec('git clone https://github.com/user/project.git /workspace'); + + // Set up environment + await sandbox.exec('cd /workspace && npm install'); + + // Create PTY with project context + const pty = await sandbox.pty.create({ + cwd: '/workspace', + env: { + NODE_ENV: 'development', + EDITOR: 'vim' + } + }); + + return pty; +}; +``` + + + +### Educational platforms + +Build interactive coding tutorials: + + + +```ts +class CodingTutorial { + constructor(private sandbox: Sandbox) {} + + async startLesson(lessonId: string) { + // Set up lesson environment + await this.sandbox.exec(`curl -o /tmp/lesson-${lessonId}.sh https://tutorials.com/${lessonId}.sh`); + await this.sandbox.exec(`chmod +x /tmp/lesson-${lessonId}.sh`); + await this.sandbox.exec(`/tmp/lesson-${lessonId}.sh`); + + // Create guided PTY session + const pty = await this.sandbox.pty.create({ + cwd: `/lessons/${lessonId}`, + command: ['bash', '--init-file', '/lessons/tutorial.bashrc'] + }); + + // Provide hints and validation + pty.onData((output) => { + if (output.includes('ERROR')) { + this.showHint('Try checking your syntax'); + } + if (output.includes('SUCCESS')) { + this.advanceToNextStep(); + } + }); + + return pty; + } +} +``` + + + +## Next steps + +You have successfully built an interactive terminal using Cloudflare Sandbox PTY functionality. Consider these enhancements: + +- **File editor integration**: Combine PTY with file operations for a full IDE experience +- **Multi-pane terminals**: Support tabbed or split terminal layouts +- **Terminal sharing**: Generate shareable links for collaborative sessions +- **Persistent sessions**: Save and restore terminal state across page reloads +- **Custom themes**: Allow users to customize terminal appearance +- **Keyboard shortcuts**: Implement common terminal shortcuts and hotkeys + +For more advanced PTY usage patterns, see the [PTY API reference](/sandbox/api/pty/). \ No newline at end of file