feat(webserver): send graceful SIGINT before killing
This commit is contained in:
parent
676f014b5f
commit
18f35f820a
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
|
import timers from 'timers/promises';
|
||||||
|
|
||||||
import { colors, debug } from 'playwright-core/lib/utilsBundle';
|
import { colors, debug } from 'playwright-core/lib/utilsBundle';
|
||||||
import { raceAgainstDeadline, launchProcess, monotonicTime, isURLAvailable } from 'playwright-core/lib/utils';
|
import { raceAgainstDeadline, launchProcess, monotonicTime, isURLAvailable } from 'playwright-core/lib/utils';
|
||||||
|
|
@ -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,24 @@ 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.
|
const success = launchedProcess.kill('SIGINT');
|
||||||
attemptToGracefullyClose: () => Promise.reject(),
|
if (!success)
|
||||||
|
throw new Error(`SIGINT didn't succeed, fall back to non-graceful shutdown`);
|
||||||
|
await Promise.race([
|
||||||
|
timers.setTimeout(1000).then(() => {
|
||||||
|
// @ts-expect-error. SIGINT didn't kill the process, but `processLauncher` will only attempt killing it if this is false
|
||||||
|
launchedProcess.killed = false;
|
||||||
|
return Promise.reject(new Error(`server didn't close gracefully within a second, falling back to non-graceful shutdown`))
|
||||||
|
}),
|
||||||
|
new Promise(f => launchedProcess.once('exit', f)),
|
||||||
|
]);
|
||||||
|
},
|
||||||
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`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,9 +151,17 @@ export class TestChildProcess {
|
||||||
this.exitCode = this.exited.then(r => r.exitCode);
|
this.exitCode = this.exited.then(r => r.exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
outputLines(): string[] {
|
outputLines(options: { prefix?: string } = {}): string[] {
|
||||||
const strippedOutput = stripAnsi(this.output);
|
const strippedOutput = stripAnsi(this.output);
|
||||||
return strippedOutput.split('\n').filter(line => line.startsWith('%%')).map(line => line.substring(2).trim());
|
return strippedOutput
|
||||||
|
.split('\n')
|
||||||
|
.map(line => {
|
||||||
|
if (options.prefix && line.startsWith(options.prefix))
|
||||||
|
return line.substring(options.prefix.length);
|
||||||
|
return line;
|
||||||
|
})
|
||||||
|
.filter(line => line.startsWith('%%'))
|
||||||
|
.map(line => line.substring(2).trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
async kill(signal: 'SIGINT' | 'SIGKILL' = 'SIGKILL') {
|
async kill(signal: 'SIGINT' | 'SIGKILL' = 'SIGKILL') {
|
||||||
|
|
|
||||||
|
|
@ -744,3 +744,37 @@ 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('should gracefully kill server', async ({ interactWithTestRunner }, { workerIndex }) => {
|
||||||
|
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
|
||||||
|
|
||||||
|
const port = workerIndex * 2 + 10510;
|
||||||
|
|
||||||
|
const testProcess = await interactWithTestRunner({
|
||||||
|
'web-server.js': `
|
||||||
|
process.on('SIGINT', () => { console.log('%%webserver received SIGINT but stubbornly refuses to wind down') })
|
||||||
|
const server = require('http').createServer((req, res) => { res.end("ok"); })
|
||||||
|
server.listen(process.argv[2], () => { console.log('webserver started'); });
|
||||||
|
`,
|
||||||
|
'test.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('pass', async ({}) => {});
|
||||||
|
`,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
webServer: {
|
||||||
|
command: 'node web-server.js ${port}',
|
||||||
|
port: ${port},
|
||||||
|
stdout: 'pipe',
|
||||||
|
timeout: 3000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
|
||||||
|
await testProcess.waitForOutput('webserver started');
|
||||||
|
process.kill(-testProcess.process.pid!, 'SIGINT');
|
||||||
|
await testProcess.exited;
|
||||||
|
|
||||||
|
expect(testProcess.outputLines({ prefix: '[WebServer] ' })).toEqual(['webserver received SIGINT but stubbornly refuses to wind down']);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue