chore: use ipc transport for out-of-process driver (#11826)

This commit is contained in:
Pavel Feldman 2022-02-02 21:26:45 -08:00 committed by GitHub
parent fdda759a9d
commit 1215057ca1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 55 additions and 26 deletions

View file

@ -22,7 +22,7 @@ import { BrowserType } from '../client/browserType';
import { LaunchServerOptions } from '../client/types';
import { DispatcherConnection, Root } from '../dispatchers/dispatcher';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { Transport } from '../protocol/transport';
import { IpcTransport, PipeTransport } from '../protocol/transport';
import { PlaywrightServer } from '../remote/playwrightServer';
import { createPlaywright } from '../server/playwright';
import { gracefullyCloseAll } from '../utils/processLauncher';
@ -38,7 +38,7 @@ export function runDriver() {
const playwright = createPlaywright(sdkLanguage);
return new PlaywrightDispatcher(rootScope, playwright);
});
const transport = new Transport(process.stdout, process.stdin);
const transport = process.send ? new IpcTransport(process) : new PipeTransport(process.stdout, process.stdin);
transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message));
dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message));
transport.onclose = async () => {

View file

@ -15,10 +15,11 @@
*/
import { Connection } from './client/connection';
import { Transport } from './protocol/transport';
import { IpcTransport } from './protocol/transport';
import { Playwright } from './client/playwright';
import * as childProcess from 'child_process';
import * as path from 'path';
import { ManualPromise } from './utils/async';
export async function start(env: any = {}): Promise<{ playwright: Playwright, stop: () => Promise<void> }> {
const client = new PlaywrightClient(env);
@ -30,16 +31,13 @@ export async function start(env: any = {}): Promise<{ playwright: Playwright, st
class PlaywrightClient {
_playwright: Promise<Playwright>;
_driverProcess: childProcess.ChildProcess;
private _closePromise: Promise<void>;
private _onExit: (exitCode: number | null, signal: string | null) => {};
private _closePromise = new ManualPromise<void>();
private _transport: IpcTransport;
private _stopped = false;
constructor(env: any) {
this._onExit = (exitCode: number | null, signal: string | null) => {
throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`);
};
this._driverProcess = childProcess.fork(path.join(__dirname, 'cli', 'cli.js'), ['run-driver'], {
stdio: 'pipe',
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
detached: true,
env: {
...process.env,
@ -47,22 +45,28 @@ class PlaywrightClient {
},
});
this._driverProcess.unref();
this._driverProcess.on('exit', this._onExit);
this._driverProcess.on('exit', this._onExit.bind(this));
const connection = new Connection();
const transport = new Transport(this._driverProcess.stdin!, this._driverProcess.stdout!);
connection.onmessage = message => transport.send(JSON.stringify(message));
transport.onmessage = message => connection.dispatch(JSON.parse(message));
this._closePromise = new Promise(f => transport.onclose = f);
this._transport = new IpcTransport(this._driverProcess);
connection.onmessage = message => this._transport.send(JSON.stringify(message));
this._transport.onmessage = message => connection.dispatch(JSON.parse(message));
this._transport.onclose = () => this._closePromise.resolve();
this._playwright = connection.initializePlaywright();
}
async stop() {
this._driverProcess.removeListener('exit', this._onExit);
this._driverProcess.stdin!.destroy();
this._driverProcess.stdout!.destroy();
this._driverProcess.stderr!.destroy();
this._stopped = true;
this._transport.close();
await this._closePromise;
}
private _onExit(exitCode: number | null, signal: string | null) {
if (this._stopped)
this._closePromise.resolve();
else
throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`);
}
}

View file

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { ChildProcess } from 'child_process';
import { makeWaitForNextTask } from '../utils/utils';
export interface WritableStream {
@ -29,7 +30,7 @@ export interface ClosableStream {
close(): void;
}
export class Transport {
export class PipeTransport {
private _pipeWrite: WritableStream;
private _data = Buffer.from([]);
private _waitForNextTask = makeWaitForNextTask();
@ -102,3 +103,27 @@ export class Transport {
}
}
}
export class IpcTransport {
private _process: NodeJS.Process | ChildProcess;
onmessage?: (message: string) => void;
onclose?: () => void;
constructor(process: NodeJS.Process | ChildProcess) {
this._process = process;
this._process.on('message', message => {
if (message === '<eof>')
this.onclose?.();
else
this.onmessage?.(message);
});
}
send(message: string) {
this._process.send!(message);
}
close() {
this._process.send!('<eof>');
}
}

View file

@ -26,7 +26,7 @@ import { BrowserContext, validateBrowserContextOptions } from '../browserContext
import { ProgressController } from '../progress';
import { CRBrowser } from '../chromium/crBrowser';
import { helper } from '../helper';
import { Transport } from '../../protocol/transport';
import { PipeTransport } from '../../protocol/transport';
import { RecentLogsCollector } from '../../utils/debugLogger';
import { TimeoutSettings } from '../../utils/timeoutSettings';
import { AndroidWebView } from '../../protocol/channels';
@ -95,7 +95,7 @@ export class AndroidDevice extends SdkObject {
readonly _backend: DeviceBackend;
readonly model: string;
readonly serial: string;
private _driverPromise: Promise<Transport> | undefined;
private _driverPromise: Promise<PipeTransport> | undefined;
private _lastId = 0;
private _callbacks = new Map<number, { fulfill: (result: any) => void, reject: (error: Error) => void }>();
private _pollingWebViews: NodeJS.Timeout | undefined;
@ -155,13 +155,13 @@ export class AndroidDevice extends SdkObject {
return await this._backend.runCommand(`shell:screencap -p`);
}
private async _driver(): Promise<Transport> {
private async _driver(): Promise<PipeTransport> {
if (!this._driverPromise)
this._driverPromise = this._installDriver();
return this._driverPromise;
}
private async _installDriver(): Promise<Transport> {
private async _installDriver(): Promise<PipeTransport> {
debug('pw:android')('Stopping the old driver');
await this.shell(`am force-stop com.microsoft.playwright.androiddriver`);
@ -176,7 +176,7 @@ export class AndroidDevice extends SdkObject {
debug('pw:android')('Starting the new driver');
this.shell('am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner').catch(e => debug('pw:android')(e));
const socket = await this._waitForLocalAbstract('playwright_android_driver_socket');
const transport = new Transport(socket, socket, socket, 'be');
const transport = new PipeTransport(socket, socket, socket, 'be');
transport.onmessage = message => {
const response = JSON.parse(message);
const { id, result, error } = response;

View file

@ -181,7 +181,7 @@ DEPS['src/protocol/'] = ['src/utils/'];
// Client depends on chromium protocol for types.
DEPS['src/client/'] = ['src/common/', 'src/utils/', 'src/protocol/', 'src/server/chromium/protocol.d.ts'];
DEPS['src/outofprocess.ts'] = ['src/client/', 'src/protocol/'];
DEPS['src/outofprocess.ts'] = ['src/client/', 'src/protocol/', 'src/utils/'];
DEPS['src/dispatchers/'] = ['src/common/', 'src/utils/', 'src/protocol/', 'src/server/**'];