Fix terminal PTY teardown races

This commit is contained in:
Mohamed Boudra
2026-05-08 20:02:52 +07:00
parent 04b04fe5ee
commit 5b6e974248
3 changed files with 35 additions and 12 deletions
@@ -32,13 +32,9 @@ afterEach(async () => {
const terminalsByCwd = await Promise.all( const terminalsByCwd = await Promise.all(
manager.listDirectories().map((cwd) => manager.getTerminals(cwd)), manager.listDirectories().map((cwd) => manager.getTerminals(cwd)),
); );
await Promise.all( for (const terminal of terminalsByCwd.flat()) {
terminalsByCwd await manager.killTerminalAndWait(terminal.id);
.flat() }
.map((terminal) =>
manager.killTerminalAndWait(terminal.id, { gracefulTimeoutMs: 50, forceTimeoutMs: 50 }),
),
);
manager.killAll(); manager.killAll();
} }
await new Promise((resolve) => setTimeout(resolve, 50)); 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 () => { it("emits cwd snapshots when terminals are created", async () => {
manager = createTerminalManager(); manager = createTerminalManager();
const cwd = realpathSync(tmpdir()); 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) => { const unsubscribe = manager.subscribeTerminalsChanged((input) => {
snapshots.push({ snapshots.push({
cwd: input.cwd, cwd: input.cwd,
terminals: input.terminals.map((terminal) => ({ terminals: input.terminals.map((terminal) => ({
name: terminal.name, name: terminal.name,
...(terminal.title ? { title: terminal.title } : {}),
})), })),
}); });
}); });
@@ -114,7 +114,7 @@ const temporaryDirs: string[] = [];
afterEach(async () => { afterEach(async () => {
for (const session of sessions) { for (const session of sessions) {
session.kill(); await session.killAndWait();
} }
sessions.length = 0; sessions.length = 0;
while (temporaryDirs.length > 0) { while (temporaryDirs.length > 0) {
+30 -2
View File
@@ -102,6 +102,10 @@ interface EnsureNodePtySpawnHelperExecutableOptions {
force?: boolean; force?: boolean;
} }
interface WindowsPtyProcessReadiness {
_agent?: { innerPid?: number };
}
function resolveNodePtyPackageRoot(): string | null { function resolveNodePtyPackageRoot(): string | null {
try { try {
const packageJsonPath = require.resolve("node-pty/package.json"); const packageJsonPath = require.resolve("node-pty/package.json");
@@ -754,6 +758,22 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
disposeResources(); disposeResources();
}); });
async function waitForPtyProcessStart(): Promise<void> {
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 { function getState(): TerminalState {
return { return {
rows: terminal.rows, rows: terminal.rows,
@@ -917,10 +937,16 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
function kill(): void { function kill(): void {
if (!killed) { if (!killed) {
killed = true; killed = true;
killPtyProcess(); if (!processExited) {
killPtyProcess();
}
emitExit(buildExitInfo()); emitExit(buildExitInfo());
} }
disposeResources(); if (processExited) {
disposeResources();
return;
}
void waitForProcessExit(1000).finally(disposeResources);
} }
function killPtyProcess(signal?: NodeJS.Signals): void { function killPtyProcess(signal?: NodeJS.Signals): void {
@@ -987,6 +1013,8 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
kill(); kill();
} }
await waitForPtyProcessStart();
// Small delay to let shell initialize // Small delay to let shell initialize
await new Promise((resolve) => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));