mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(webserver): customize shutdown with new gracefulShutdown option (#34130)
Signed-off-by: Simon Knott <info@simonknott.de> Co-authored-by: Dmitry Gozman <dgozman@gmail.com>
This commit is contained in:
parent
04a3574f80
commit
6bdd2694ee
@ -629,6 +629,9 @@ export default defineConfig({
|
|||||||
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
|
- `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`.
|
||||||
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
|
- `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`.
|
||||||
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
||||||
|
- `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGINT` and `SIGTERM` signals, so this option is ignored.
|
||||||
|
- `signal` <["SIGINT"|"SIGTERM"]>
|
||||||
|
- `timeout` <[int]>
|
||||||
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
|
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
|
||||||
|
|
||||||
Launch a development web server (or multiple) during the tests.
|
Launch a development web server (or multiple) during the tests.
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export default defineConfig({
|
|||||||
| `stdout` | If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. |
|
| `stdout` | If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. |
|
||||||
| `stderr` | Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. |
|
| `stderr` | Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. |
|
||||||
| `timeout` | How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. |
|
| `timeout` | How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. |
|
||||||
|
| `gracefulShutdown` | How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGINT` and `SIGTERM` signals, so this option is ignored. |
|
||||||
|
|
||||||
## Adding a server timeout
|
## Adding a server timeout
|
||||||
|
|
||||||
|
|||||||
@ -180,7 +180,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
|||||||
let processClosed = false;
|
let processClosed = false;
|
||||||
let fulfillCleanup = () => {};
|
let fulfillCleanup = () => {};
|
||||||
const waitForCleanup = new Promise<void>(f => fulfillCleanup = f);
|
const waitForCleanup = new Promise<void>(f => fulfillCleanup = f);
|
||||||
spawnedProcess.once('exit', (exitCode, signal) => {
|
spawnedProcess.once('close', (exitCode, signal) => {
|
||||||
options.log(`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`);
|
options.log(`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`);
|
||||||
processClosed = true;
|
processClosed = true;
|
||||||
gracefullyCloseSet.delete(gracefullyClose);
|
gracefullyCloseSet.delete(gracefullyClose);
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export type WebServerPluginOptions = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
ignoreHTTPSErrors?: boolean;
|
ignoreHTTPSErrors?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
gracefulShutdown?: { signal: 'SIGINT' | 'SIGTERM', timeout?: number };
|
||||||
reuseExistingServer?: boolean;
|
reuseExistingServer?: boolean;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
env?: { [key: string]: string; };
|
env?: { [key: string]: string; };
|
||||||
@ -92,7 +93,7 @@ export class WebServerPlugin implements TestRunnerPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debugWebServer(`Starting WebServer process ${this._options.command}...`);
|
debugWebServer(`Starting WebServer process ${this._options.command}...`);
|
||||||
const { launchedProcess, kill } = await launchProcess({
|
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||||
command: this._options.command,
|
command: this._options.command,
|
||||||
env: {
|
env: {
|
||||||
...DEFAULT_ENVIRONMENT_VARIABLES,
|
...DEFAULT_ENVIRONMENT_VARIABLES,
|
||||||
@ -102,14 +103,33 @@ export class WebServerPlugin implements TestRunnerPlugin {
|
|||||||
cwd: this._options.cwd,
|
cwd: this._options.cwd,
|
||||||
stdio: 'stdin',
|
stdio: 'stdin',
|
||||||
shell: true,
|
shell: true,
|
||||||
// Reject to indicate that we cannot close the web server gracefully
|
attemptToGracefullyClose: async () => {
|
||||||
// and should fallback to non-graceful shutdown.
|
if (process.platform === 'win32')
|
||||||
attemptToGracefullyClose: () => Promise.reject(),
|
throw new Error('Graceful shutdown is not supported on Windows');
|
||||||
|
if (!this._options.gracefulShutdown)
|
||||||
|
throw new Error('skip graceful shutdown');
|
||||||
|
|
||||||
|
const { signal, timeout = 0 } = this._options.gracefulShutdown;
|
||||||
|
|
||||||
|
// proper usage of SIGINT is to send it to the entire process group, see https://www.cons.org/cracauer/sigint.html
|
||||||
|
// there's no such convention for SIGTERM, so we decide what we want. signaling the process group for consistency.
|
||||||
|
process.kill(-launchedProcess.pid!, signal);
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const timer = timeout !== 0
|
||||||
|
? setTimeout(() => reject(new Error(`process didn't close gracefully within timeout`)), timeout)
|
||||||
|
: undefined;
|
||||||
|
launchedProcess.once('close', (...args) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
log: () => {},
|
log: () => {},
|
||||||
onExit: code => processExitedReject(new Error(code ? `Process from config.webServer was not able to start. Exit code: ${code}` : 'Process from config.webServer exited early.')),
|
onExit: code => processExitedReject(new Error(code ? `Process from config.webServer was not able to start. Exit code: ${code}` : 'Process from config.webServer exited early.')),
|
||||||
tempDirectories: [],
|
tempDirectories: [],
|
||||||
});
|
});
|
||||||
this._killProcess = kill;
|
this._killProcess = gracefullyClose;
|
||||||
|
|
||||||
debugWebServer(`Process started`);
|
debugWebServer(`Process started`);
|
||||||
|
|
||||||
|
|||||||
12
packages/playwright/types/test.d.ts
vendored
12
packages/playwright/types/test.d.ts
vendored
@ -9657,6 +9657,18 @@ interface TestConfigWebServer {
|
|||||||
*/
|
*/
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal:
|
||||||
|
* 'SIGINT', timeout: 500 }`, the process group is sent a `SIGINT` signal, followed by `SIGKILL` if it doesn't exit
|
||||||
|
* within 500ms. You can also use `SIGTERM` instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't
|
||||||
|
* support `SIGINT` and `SIGTERM` signals, so this option is ignored.
|
||||||
|
*/
|
||||||
|
gracefulShutdown?: {
|
||||||
|
signal: "SIGINT"|"SIGTERM";
|
||||||
|
|
||||||
|
timeout: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the
|
* The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the
|
||||||
* server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is
|
* server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
import type http from 'http';
|
import type http from 'http';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { test, expect, parseTestRunnerOutput } from './playwright-test-fixtures';
|
import { test, expect, parseTestRunnerOutput } from './playwright-test-fixtures';
|
||||||
|
import type { RunResult } from './playwright-test-fixtures';
|
||||||
import { createHttpServer } from '../../packages/playwright-core/lib/utils/network';
|
import { createHttpServer } from '../../packages/playwright-core/lib/utils/network';
|
||||||
|
|
||||||
const SIMPLE_SERVER_PATH = path.join(__dirname, 'assets', 'simple-server.js');
|
const SIMPLE_SERVER_PATH = path.join(__dirname, 'assets', 'simple-server.js');
|
||||||
@ -744,3 +745,66 @@ test('should forward stdout when set to "pipe" before server is ready', async ({
|
|||||||
expect(result.output).toContain('[WebServer] output from server');
|
expect(result.output).toContain('[WebServer] output from server');
|
||||||
expect(result.output).not.toContain('Timed out waiting 3000ms');
|
expect(result.output).not.toContain('Timed out waiting 3000ms');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('gracefulShutdown option', () => {
|
||||||
|
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
|
||||||
|
|
||||||
|
const files = (additionalOptions = {}) => {
|
||||||
|
const port = test.info().workerIndex * 2 + 10510;
|
||||||
|
return {
|
||||||
|
'child.js': `
|
||||||
|
process.on('SIGINT', () => { console.log('%%childprocess received SIGINT'); setTimeout(() => process.exit(), 10) })
|
||||||
|
process.on('SIGTERM', () => { console.log('%%childprocess received SIGTERM'); setTimeout(() => process.exit(), 10) })
|
||||||
|
setTimeout(() => {}, 100000) // prevent child from exiting
|
||||||
|
`,
|
||||||
|
'web-server.js': `
|
||||||
|
require("node:child_process").fork('./child.js', { silent: false })
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('%%webserver received SIGINT but stubbornly refuses to wind down')
|
||||||
|
})
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('%%webserver received SIGTERM but stubbornly refuses to wind down')
|
||||||
|
})
|
||||||
|
|
||||||
|
const server = require("node:http").createServer((req, res) => { res.end("ok"); })
|
||||||
|
server.listen(process.argv[2]);
|
||||||
|
`,
|
||||||
|
'test.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('pass', async ({}) => {});
|
||||||
|
`,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
webServer: {
|
||||||
|
command: 'echo some-precondition && node web-server.js ${port}',
|
||||||
|
port: ${port},
|
||||||
|
stdout: 'pipe',
|
||||||
|
timeout: 3000,
|
||||||
|
...${JSON.stringify(additionalOptions)}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseOutputLines(result: RunResult): string[] {
|
||||||
|
const prefix = '[WebServer] %%';
|
||||||
|
return result.output.split('\n').filter(line => line.startsWith(prefix)).map(line => line.substring(prefix.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('sends SIGKILL by default', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest(files(), { workers: 1 });
|
||||||
|
expect(parseOutputLines(result)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be configured to send SIGTERM', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest(files({ gracefulShutdown: { signal: 'SIGTERM', timeout: 500 } }), { workers: 1 });
|
||||||
|
expect(parseOutputLines(result).sort()).toEqual(['childprocess received SIGTERM', 'webserver received SIGTERM but stubbornly refuses to wind down']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be configured to send SIGINT', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest(files({ gracefulShutdown: { signal: 'SIGINT', timeout: 500 } }), { workers: 1 });
|
||||||
|
expect(parseOutputLines(result).sort()).toEqual(['childprocess received SIGINT', 'webserver received SIGINT but stubbornly refuses to wind down']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user