From d9649d72626e9a17cab2241c7effb4742db2d9ad Mon Sep 17 00:00:00 2001 From: mehmet turac Date: Mon, 15 Jun 2026 14:32:33 +0300 Subject: [PATCH] fix: avoid waiting for inherited Edge stdio --- .../playwright-core/src/server/browserType.ts | 1 + packages/utils/processLauncher.ts | 9 +- .../playwright-test/process-launcher.spec.ts | 91 +++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 tests/playwright-test/process-launcher.spec.ts diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 5631142825940..e94c871fe6cfa 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -225,6 +225,7 @@ export abstract class BrowserType extends SdkObject { browserLogsCollector.log(message); }, stdio: 'pipe', + waitForStdioClose: !this.getExecutableName(options).startsWith('msedge'), tempDirectories: prepared.tempDirectories, attemptToGracefullyClose: async () => { if ((options as any).__testHookGracefullyClose) diff --git a/packages/utils/processLauncher.ts b/packages/utils/processLauncher.ts index 9c77c7a0ce3ad..fd8200b1f03f4 100644 --- a/packages/utils/processLauncher.ts +++ b/packages/utils/processLauncher.ts @@ -32,6 +32,9 @@ export type LaunchProcessOptions = { handleSIGTERM?: boolean, handleSIGHUP?: boolean, stdio: 'pipe' | 'stdin', + // Defaults to true. Set to false when a child process can spawn helpers + // that inherit stdio and outlive the child itself. + waitForStdioClose?: boolean, tempDirectories: string[], cwd?: string, @@ -179,10 +182,11 @@ export async function launchProcess(options: LaunchProcessOptions): Promise {}; const waitForCleanup = new Promise(f => fulfillCleanup = f); - spawnedProcess.once('close', (exitCode, signal) => { + const handleProcessExit = (exitCode: number | null, signal: string | null) => { options.log(`[pid=${spawnedProcess.pid}] `); processClosed = true; gracefullyCloseSet.delete(gracefullyClose); @@ -191,7 +195,8 @@ export async function launchProcess(options: LaunchProcessOptions): Promise {}, 30000)'], { stdio: 'inherit' }); + fs.writeFileSync(process.argv[1], String(child.pid)); + child.unref(); + `; + const result = await launchProcess({ + command: process.execPath, + args: ['-e', script, pidFile], + stdio: 'pipe', + waitForStdioClose, + tempDirectories: [cleanupDir], + attemptToGracefullyClose: async () => {}, + handleSIGINT: false, + handleSIGTERM: false, + handleSIGHUP: false, + log: () => {}, + onExit: () => ++onExitCalls, + }); + return { ...result, onExitCalls: () => onExitCalls }; +} + +function killGrandchild(pidFile: string) { + if (!fs.existsSync(pidFile)) + return; + const pid = +fs.readFileSync(pidFile, 'utf8'); + try { + process.kill(pid, 'SIGKILL'); + } catch (e) { + } +} + +test('process launcher can wait for the main process exit without waiting for inherited stdio', async ({}, testInfo) => { + const pidFile = testInfo.outputPath('grandchild.pid'); + const cleanupDir = testInfo.outputPath('cleanup'); + const { gracefullyClose, onExitCalls } = await launchProcessWithStdioGrandchild(pidFile, cleanupDir, false); + try { + const start = Date.now(); + await gracefullyClose(); + expect(Date.now() - start).toBeLessThan(1000); + expect(onExitCalls()).toBe(1); + expect(fs.existsSync(cleanupDir)).toBe(false); + } finally { + killGrandchild(pidFile); + } +}); + +test('process launcher waits for stdio close by default', async ({}, testInfo) => { + const pidFile = testInfo.outputPath('grandchild.pid'); + const cleanupDir = testInfo.outputPath('cleanup'); + const { gracefullyClose, onExitCalls } = await launchProcessWithStdioGrandchild(pidFile, cleanupDir, true); + const closePromise = gracefullyClose(); + try { + const closed = await Promise.race([ + closePromise.then(() => true), + new Promise(f => setTimeout(() => f(false), 1000)), + ]); + expect(closed).toBe(false); + } finally { + killGrandchild(pidFile); + await closePromise; + expect(onExitCalls()).toBe(1); + expect(fs.existsSync(cleanupDir)).toBe(false); + } +});