Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/playwright/src/common/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export function startProcessRunner(create: (params: any) => ProcessRunner) {
}

const kForceExitTimeout = +(process.env.PWTEST_FORCE_EXIT_TIMEOUT || 30000);
const kHeartbeatInterval = 1000;

async function gracefullyCloseAndExit(forceExit: boolean) {
if (forceExit && !forceExitInitiated) {
Expand All @@ -115,8 +116,13 @@ async function gracefullyCloseAndExit(forceExit: boolean) {
}
if (!gracefullyCloseCalled) {
gracefullyCloseCalled = true;
// Heartbeats tell the parent that graceful close is still running, e.g. a fixture
// teardown with "timeout: 0", as opposed to a hung process that must be force-killed.
const heartbeat = setInterval(() => sendMessageToParent({ method: '__heartbeat__' }), kHeartbeatInterval);
heartbeat.unref();
// Meanwhile, try to gracefully shutdown.
await processRunner?.gracefullyClose().catch(() => {});
clearInterval(heartbeat);
if (processName)
await stopProfiling(processName).catch(() => {});
// eslint-disable-next-line no-restricted-properties
Expand Down
24 changes: 19 additions & 5 deletions packages/playwright/src/runner/processHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,25 @@ export class ProcessHost extends EventEmitter {
return;
const exitPromise = new Promise<void>(f => this.once('exit', () => f()));
const timeout = +(process.env.PWTEST_CHILD_PROCESS_TIMEOUT || 5 * 60 * 1000);
const result = await raceAgainstDeadline(() => exitPromise, monotonicTime() + timeout);
if (result.timedOut) {
this.emit('processError', { message: `Error: ${this._processName} process did not exit within ${timeout}ms after stop, force-killed it` });
this._forceKill();
await exitPromise;
// Child sends heartbeats while gracefully closing, e.g. running a slow fixture
// teardown with "timeout: 0". Only force-kill when heartbeats stop coming.
let lastHeartbeat = monotonicTime();
const onHeartbeat = () => lastHeartbeat = monotonicTime();
this.on('__heartbeat__', onHeartbeat);
try {
while (true) {
const result = await raceAgainstDeadline(() => exitPromise, lastHeartbeat + timeout);
if (!result.timedOut)
return;
if (monotonicTime() < lastHeartbeat + timeout)
continue;
this.emit('processError', { message: `Error: ${this._processName} process did not exit within ${timeout}ms after stop, force-killed it` });
this._forceKill();
await exitPromise;
return;
}
} finally {
this.off('__heartbeat__', onHeartbeat);
}
}

Expand Down
21 changes: 21 additions & 0 deletions tests/playwright-test/exit-code.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,24 @@ test('should force-kill a worker that does not exit on stop', async ({ runInline
// Should complete well within a minute thanks to the watchdog.
expect(monotonicTime() - now).toBeLessThan(60000);
});

test('should not force-kill a worker that is running a slow fixture teardown', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42007' });
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend<{}, { slowTeardown: void }>({
slowTeardown: [async ({}, use) => {
await use();
await new Promise(f => setTimeout(f, 4000));
console.log('slow teardown finished');
}, { scope: 'worker', timeout: 0 }],
});
test('passes', async ({ slowTeardown }) => {});
`,
}, undefined, { PWTEST_CHILD_PROCESS_TIMEOUT: '2000' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(result.output).toContain('slow teardown finished');
expect(result.output).not.toContain('force-killed');
});
Loading