import { execInContainer, resizeExec } from '../services/docker.js'; /** * Handle WebSocket terminal session * Path: /ws/terminal/{containerId} * * Creates an interactive shell (docker exec) in the container * and bridges it to the WebSocket connection. * * Client messages: * - { type: 'input', data: '...' } — stdin data * - { type: 'resize', cols: N, rows: N } — terminal resize */ export async function handleTerminal(ws, containerId) { let execInstance = null; let stream = null; try { ws.send(JSON.stringify({ type: 'info', data: `Connecting to container ${containerId.slice(0, 12)}...` })); const result = await execInContainer(containerId, ['/bin/sh', '-c', 'if command -v bash > /dev/null; then exec bash; else exec sh; fi']); execInstance = result.exec; stream = result.stream; ws.send(JSON.stringify({ type: 'connected', data: 'Terminal connected' })); // Docker exec stream → WebSocket stream.on('data', (chunk) => { if (ws.readyState === 1) { ws.send(JSON.stringify({ type: 'output', data: chunk.toString('utf8') })); } }); stream.on('end', () => { if (ws.readyState === 1) { ws.send(JSON.stringify({ type: 'info', data: 'Terminal session ended' })); ws.close(); } }); stream.on('error', (err) => { if (ws.readyState === 1) { ws.send(JSON.stringify({ type: 'error', data: `Stream error: ${err.message}` })); } }); // WebSocket → Docker exec stream ws.on('message', (message) => { try { const msg = JSON.parse(message.toString()); if (msg.type === 'input' && stream && stream.writable) { stream.write(msg.data); } else if (msg.type === 'resize' && execInstance) { resizeExec(execInstance.id, msg.cols || 80, msg.rows || 24); } } catch { // If raw text, treat as stdin input if (stream && stream.writable) { stream.write(message.toString()); } } }); } catch (err) { ws.send(JSON.stringify({ type: 'error', data: `Failed to connect: ${err.message}` })); ws.close(); return; } ws.on('close', () => { try { if (stream) stream.end(); } catch {} }); ws.on('error', () => { try { if (stream) stream.end(); } catch {} }); }