Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "fix(webserver): do not spawn webserver as a new process group (#18564)" #18968

Merged
merged 1 commit into from Nov 21, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 22 additions & 33 deletions packages/playwright-test/src/plugins/webServerPlugin.ts
Expand Up @@ -17,11 +17,10 @@ import http from 'http';
import https from 'https';
import path from 'path';
import net from 'net';
import { spawn, spawnSync } from 'child_process';
import type { ChildProcess } from 'child_process';

import { debug } from 'playwright-core/lib/utilsBundle';
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
import { launchProcess } from 'playwright-core/lib/utils/processLauncher';

import type { FullConfig, Reporter, Suite } from '../../types/testReporter';
import type { TestRunnerPlugin } from '.';
Expand All @@ -47,8 +46,8 @@ const debugWebServer = debug('pw:webserver');

export class WebServerPlugin implements TestRunnerPlugin {
private _isAvailable?: () => Promise<boolean>;
private _processExitedPromise?: Promise<{ code: number|null, signal: NodeJS.Signals|null }>;
private _childProcess?: ChildProcess;
private _killProcess?: () => Promise<void>;
private _processExitedPromise!: Promise<any>;
private _options: WebServerPluginOptions;
private _checkPortOnly: boolean;
private _reporter?: Reporter;
Expand All @@ -65,34 +64,21 @@ export class WebServerPlugin implements TestRunnerPlugin {
this._options.cwd = this._options.cwd ? path.resolve(configDir, this._options.cwd) : configDir;
try {
await this._startProcess();
await this._waitForProcess();
} catch (error) {
await this.teardown();
throw error;
}
}

public async teardown() {
if (!this._childProcess || !this._childProcess.pid || this._childProcess.killed)
return;
// Send SIGTERM and wait for it to gracefully close.
try {
if (process.platform === 'win32') {
const taskkillProcess = spawnSync(`taskkill /pid ${this._childProcess.pid} /T /F`, { shell: true });
const [stdout, stderr] = [taskkillProcess.stdout.toString(), taskkillProcess.stderr.toString()];
if (stdout)
debugWebServer(`[pid=${this._childProcess.pid}] taskkill stdout: ${stdout}`);
if (stderr)
debugWebServer(`[pid=${this._childProcess.pid}] taskkill stderr: ${stderr}`);
} else {
process.kill(this._childProcess.pid, 'SIGTERM');
}
} catch (e) {
// the process might have already stopped
}
await this._processExitedPromise;
await this._killProcess?.();
}

private async _startProcess(): Promise<void> {
let processExitedReject = (error: Error) => { };
this._processExitedPromise = new Promise((_, reject) => processExitedReject = reject);

const isAlreadyAvailable = await this._isAvailable!();
if (isAlreadyAvailable) {
debugWebServer(`WebServer is already available`);
Expand All @@ -103,28 +89,33 @@ export class WebServerPlugin implements TestRunnerPlugin {
}

debugWebServer(`Starting WebServer process ${this._options.command}...`);

this._childProcess = spawn(this._options.command, [], {
const { launchedProcess, kill } = await launchProcess({
command: this._options.command,
env: {
...DEFAULT_ENVIRONMENT_VARIABLES,
...envWithoutExperimentalLoaderOptions(),
...this._options.env,
},
cwd: this._options.cwd,
stdio: 'pipe',
stdio: 'stdin',
shell: true,
attemptToGracefullyClose: async () => {},
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.')),
tempDirectories: [],
});
this._processExitedPromise = new Promise((resolve, reject) => {
this._childProcess!.once('exit', (code, signal) => resolve({ code, signal }));
});
this._killProcess = kill;

debugWebServer(`Process started`);

this._childProcess.stderr!.on('data', line => this._reporter!.onStdErr?.('[WebServer] ' + line.toString()));
this._childProcess.stdout!.on('data', line => {
launchedProcess.stderr!.on('data', line => this._reporter!.onStdErr?.('[WebServer] ' + line.toString()));
launchedProcess.stdout!.on('data', line => {
if (debugWebServer.enabled)
this._reporter!.onStdOut?.('[WebServer] ' + line.toString());
});
}

private async _waitForProcess() {
debugWebServer(`Waiting for availability...`);
await this._waitForAvailability();
debugWebServer(`WebServer available`);
Expand All @@ -135,9 +126,7 @@ export class WebServerPlugin implements TestRunnerPlugin {
const cancellationToken = { canceled: false };
const { timedOut } = (await Promise.race([
raceAgainstTimeout(() => waitFor(this._isAvailable!, cancellationToken), launchTimeout),
this._processExitedPromise!.then(({ code, signal }) => {
throw new Error(`Process from config.webServer terminated with exit code "${code}" and signal "${signal}"`);
}),
this._processExitedPromise,
]));
cancellationToken.canceled = true;
if (timedOut)
Expand Down