diff --git a/packages/playwright-core/src/cli/driver.ts b/packages/playwright-core/src/cli/driver.ts index 8252acca07..3a64be7cca 100644 --- a/packages/playwright-core/src/cli/driver.ts +++ b/packages/playwright-core/src/cli/driver.ts @@ -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 () => { diff --git a/packages/playwright-core/src/outofprocess.ts b/packages/playwright-core/src/outofprocess.ts index 89cfdcbd52..9bfae2895b 100644 --- a/packages/playwright-core/src/outofprocess.ts +++ b/packages/playwright-core/src/outofprocess.ts @@ -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 }> { const client = new PlaywrightClient(env); @@ -30,16 +31,13 @@ export async function start(env: any = {}): Promise<{ playwright: Playwright, st class PlaywrightClient { _playwright: Promise; _driverProcess: childProcess.ChildProcess; - private _closePromise: Promise; - private _onExit: (exitCode: number | null, signal: string | null) => {}; + private _closePromise = new ManualPromise(); + 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}`); + } + } diff --git a/packages/playwright-core/src/protocol/transport.ts b/packages/playwright-core/src/protocol/transport.ts index 9b4be17edf..e23ccb683d 100644 --- a/packages/playwright-core/src/protocol/transport.ts +++ b/packages/playwright-core/src/protocol/transport.ts @@ -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 === '') + this.onclose?.(); + else + this.onmessage?.(message); + }); + } + + send(message: string) { + this._process.send!(message); + } + + close() { + this._process.send!(''); + } +} diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 40d823c59b..1f5899cc98 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -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 | undefined; + private _driverPromise: Promise | undefined; private _lastId = 0; private _callbacks = new Map 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 { + private async _driver(): Promise { if (!this._driverPromise) this._driverPromise = this._installDriver(); return this._driverPromise; } - private async _installDriver(): Promise { + private async _installDriver(): Promise { 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; diff --git a/utils/check_deps.js b/utils/check_deps.js index 1fda1853f2..d471921c31 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -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/**'];