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(
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 } : {}),
})),
});
});
@@ -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) {
+30 -2
View File
@@ -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<Te
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 {
return {
rows: terminal.rows,
@@ -917,10 +937,16 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
function kill(): void {
if (!killed) {
killed = true;
killPtyProcess();
if (!processExited) {
killPtyProcess();
}
emitExit(buildExitInfo());
}
disposeResources();
if (processExited) {
disposeResources();
return;
}
void waitForProcessExit(1000).finally(disposeResources);
}
function killPtyProcess(signal?: NodeJS.Signals): void {
@@ -987,6 +1013,8 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
kill();
}
await waitForPtyProcessStart();
// Small delay to let shell initialize
await new Promise((resolve) => setTimeout(resolve, 50));