agent-orchestrator/packages/web/server/direct-terminal-ws.ts

121 lines
3.3 KiB
TypeScript

/**
* Direct WebSocket terminal server.
* Hosts the multiplexed /mux WebSocket endpoint for all terminal connections.
*/
import { createServer, type Server } from "node:http";
import type { WebSocketServer } from "ws";
import { findTmux } from "./tmux-utils.js";
import { createMuxWebSocket } from "./mux-websocket.js";
export interface DirectTerminalServer {
server: Server;
shutdown: () => void;
}
/**
* Create the direct terminal WebSocket server.
* Separated from listen() so tests can control lifecycle.
*/
export function createDirectTerminalServer(tmuxPath?: string): DirectTerminalServer {
const TMUX = tmuxPath ?? findTmux();
let muxWss: WebSocketServer | null = null;
const metrics = {
totalConnections: 0,
totalDisconnects: 0,
totalErrors: 0,
};
const server = createServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: "ok",
clients: muxWss?.clients.size ?? 0,
metrics,
}),
);
return;
}
res.writeHead(404);
res.end("Not found");
});
muxWss = createMuxWebSocket(TMUX);
if (muxWss) {
muxWss.on("connection", (ws) => {
metrics.totalConnections++;
ws.on("close", () => {
metrics.totalDisconnects++;
});
ws.on("error", () => {
metrics.totalErrors++;
});
});
}
// Manual upgrade routing — ws library doesn't support multiple WebSocketServer
// instances with different `path` options on the same HTTP server.
server.on("upgrade", (request, socket, head) => {
const pathname = new URL(request.url ?? "/", "ws://localhost").pathname;
if (pathname === "/mux" && muxWss) {
muxWss.handleUpgrade(request, socket, head, (ws) => {
muxWss!.emit("connection", ws, request);
});
} else {
socket.destroy();
}
});
function shutdown() {
// Terminate all connected mux clients — this triggers their 'close' events
// which unsubscribe terminal callbacks and kill PTY processes.
if (muxWss) {
for (const client of muxWss.clients) {
client.terminate();
}
muxWss.close();
}
server.close();
}
return { server, shutdown };
}
// --- Run as standalone script ---
// Only start the server when executed directly (not imported by tests)
const isMainModule =
process.argv[1]?.endsWith("direct-terminal-ws.ts") ||
process.argv[1]?.endsWith("direct-terminal-ws.js");
if (isMainModule) {
const TMUX = findTmux();
console.log(`[DirectTerminal] Using tmux: ${TMUX}`);
const { server, shutdown } = createDirectTerminalServer(TMUX);
const PORT = parseInt(process.env.DIRECT_TERMINAL_PORT ?? "14801", 10);
server.listen(PORT, () => {
console.log(`[DirectTerminal] WebSocket server listening on port ${PORT}`);
});
function handleShutdown(signal: string) {
console.log(`[DirectTerminal] Received ${signal}, shutting down...`);
shutdown();
const forceExitTimer = setTimeout(() => {
console.error("[DirectTerminal] Forced shutdown after timeout");
process.exit(1);
}, 5000);
forceExitTimer.unref();
}
process.on("SIGINT", () => handleShutdown("SIGINT"));
process.on("SIGTERM", () => handleShutdown("SIGTERM"));
}