From 5b6e97424888a210edea3df228f51f883122f5ed Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 8 May 2026 20:02:52 +0700 Subject: [PATCH] Fix terminal PTY teardown races --- .../src/terminal/terminal-manager.test.ts | 13 +++----- .../src/terminal/terminal.posix.test.ts | 2 +- packages/server/src/terminal/terminal.ts | 32 +++++++++++++++++-- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/packages/server/src/terminal/terminal-manager.test.ts b/packages/server/src/terminal/terminal-manager.test.ts index 304b4650d..689340bd3 100644 --- a/packages/server/src/terminal/terminal-manager.test.ts +++ b/packages/server/src/terminal/terminal-manager.test.ts @@ -32,13 +32,9 @@ afterEach(async () => { const terminalsByCwd = await Promise.all( manager.listDirectories().map((cwd) => manager.getTerminals(cwd)), ); - await Promise.all( - terminalsByCwd - .flat() - .map((terminal) => - manager.killTerminalAndWait(terminal.id, { gracefulTimeoutMs: 50, forceTimeoutMs: 50 }), - ), - ); + for (const terminal of terminalsByCwd.flat()) { + await manager.killTerminalAndWait(terminal.id); + } manager.killAll(); } await new Promise((resolve) => setTimeout(resolve, 50)); @@ -306,13 +302,12 @@ it("kills all terminals and clears state", async () => { it("emits cwd snapshots when terminals are created", async () => { manager = createTerminalManager(); const cwd = realpathSync(tmpdir()); - const snapshots: Array<{ cwd: string; terminals: Array<{ name: string; title?: string }> }> = []; + const snapshots: Array<{ cwd: string; terminals: Array<{ name: string }> }> = []; const unsubscribe = manager.subscribeTerminalsChanged((input) => { snapshots.push({ cwd: input.cwd, terminals: input.terminals.map((terminal) => ({ name: terminal.name, - ...(terminal.title ? { title: terminal.title } : {}), })), }); }); diff --git a/packages/server/src/terminal/terminal.posix.test.ts b/packages/server/src/terminal/terminal.posix.test.ts index 616b179a3..4666d9f94 100644 --- a/packages/server/src/terminal/terminal.posix.test.ts +++ b/packages/server/src/terminal/terminal.posix.test.ts @@ -114,7 +114,7 @@ const temporaryDirs: string[] = []; afterEach(async () => { for (const session of sessions) { - session.kill(); + await session.killAndWait(); } sessions.length = 0; while (temporaryDirs.length > 0) { diff --git a/packages/server/src/terminal/terminal.ts b/packages/server/src/terminal/terminal.ts index ca5436c3f..c83c1f279 100644 --- a/packages/server/src/terminal/terminal.ts +++ b/packages/server/src/terminal/terminal.ts @@ -102,6 +102,10 @@ interface EnsureNodePtySpawnHelperExecutableOptions { force?: boolean; } +interface WindowsPtyProcessReadiness { + _agent?: { innerPid?: number }; +} + function resolveNodePtyPackageRoot(): string | null { try { const packageJsonPath = require.resolve("node-pty/package.json"); @@ -754,6 +758,22 @@ export async function createTerminal(options: CreateTerminalOptions): Promise { + if (process.platform !== "win32") { + return; + } + + const started = (): boolean => { + const windowsPtyProcess = ptyProcess as unknown as WindowsPtyProcessReadiness; + return ptyProcess.pid > 0 || (windowsPtyProcess._agent?.innerPid ?? 0) > 0 || processExited; + }; + + const deadline = Date.now() + 5000; + while (!started() && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + function getState(): TerminalState { return { rows: terminal.rows, @@ -917,10 +937,16 @@ export async function createTerminal(options: CreateTerminalOptions): Promise setTimeout(resolve, 50));