From 060a5311efbde2ffa6cac5e3eb5817a4fd1c38d2 Mon Sep 17 00:00:00 2001 From: Isaac Sanchez-Hawkins <266845420+isanchez404@users.noreply.github.com> Date: Tue, 12 May 2026 04:01:31 -0400 Subject: [PATCH] fix(terminal): remove upgrade listener on shutdown (#1233) Co-authored-by: Isaac Sanchez --- packages/web/server/lib/terminal/runtime.js | 8 +++- .../web/server/lib/terminal/runtime.test.js | 44 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 packages/web/server/lib/terminal/runtime.test.js diff --git a/packages/web/server/lib/terminal/runtime.js b/packages/web/server/lib/terminal/runtime.js index 90eada79..2d0ac299 100644 --- a/packages/web/server/lib/terminal/runtime.js +++ b/packages/web/server/lib/terminal/runtime.js @@ -385,7 +385,7 @@ export function createTerminalRuntime({ }); }); - server.on('upgrade', (req, socket, head) => { + const upgradeHandler = (req, socket, head) => { const pathname = parseRequestPathname(req.url); if (pathname !== TERMINAL_INPUT_WS_PATH) { return; @@ -422,7 +422,9 @@ export function createTerminalRuntime({ }; void handleUpgrade(); - }); + }; + + server.on('upgrade', upgradeHandler); const wireTerminalSession = (sessionId, session) => { session.ptyProcess.onData((data) => { @@ -791,6 +793,8 @@ export function createTerminalRuntime({ }); const shutdown = async () => { + server.off('upgrade', upgradeHandler); + if (idleSweepInterval) { clearInterval(idleSweepInterval); } diff --git a/packages/web/server/lib/terminal/runtime.test.js b/packages/web/server/lib/terminal/runtime.test.js new file mode 100644 index 00000000..326e1b4b --- /dev/null +++ b/packages/web/server/lib/terminal/runtime.test.js @@ -0,0 +1,44 @@ +import { EventEmitter } from 'node:events'; +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +import { createTerminalRuntime } from './runtime.js'; + +function createRuntime(server) { + const app = { + post() {}, + get() {}, + delete() {}, + }; + + return createTerminalRuntime({ + app, + server, + express: { text: () => (_req, _res, next) => next?.() }, + fs, + path, + uiAuthController: null, + buildAugmentedPath: () => process.env.PATH || '', + searchPathFor: () => null, + isExecutable: () => false, + isRequestOriginAllowed: async () => true, + rejectWebSocketUpgrade() {}, + TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS: 30_000, + TERMINAL_INPUT_WS_REBIND_WINDOW_MS: 1_000, + TERMINAL_INPUT_WS_MAX_REBINDS_PER_WINDOW: 3, + }); +} + +describe('terminal runtime', () => { + it('removes its websocket upgrade listener on shutdown', async () => { + const server = new EventEmitter(); + const runtime = createRuntime(server); + + expect(server.listenerCount('upgrade')).toBe(1); + + await runtime.shutdown(); + + expect(server.listenerCount('upgrade')).toBe(0); + }); +});