feat(firefox): switch to use pipe instead of websocket (#3279)
This commit is contained in:
parent
aafcf932bc
commit
5faf6f9e69
|
|
@ -20,9 +20,9 @@ import * as path from 'path';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import { BrowserContext, normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
|
import { BrowserContext, normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
|
||||||
import * as browserPaths from '../utils/browserPaths';
|
import * as browserPaths from '../utils/browserPaths';
|
||||||
import { ConnectionTransport, WebSocketTransport } from './transport';
|
import { ConnectionTransport } from './transport';
|
||||||
import { BrowserOptions, Browser, BrowserProcess } from './browser';
|
import { BrowserOptions, Browser, BrowserProcess } from './browser';
|
||||||
import { launchProcess, Env, waitForLine, envArrayToObject } from './processLauncher';
|
import { launchProcess, Env, envArrayToObject } from './processLauncher';
|
||||||
import { PipeTransport } from './pipeTransport';
|
import { PipeTransport } from './pipeTransport';
|
||||||
import { Progress, ProgressController } from './progress';
|
import { Progress, ProgressController } from './progress';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
|
|
@ -35,22 +35,18 @@ const mkdtempAsync = util.promisify(fs.mkdtemp);
|
||||||
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
||||||
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
|
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
|
||||||
|
|
||||||
type WebSocketNotPipe = { webSocketRegex: RegExp, stream: 'stdout' | 'stderr' };
|
|
||||||
|
|
||||||
export abstract class BrowserType {
|
export abstract class BrowserType {
|
||||||
private _name: string;
|
private _name: string;
|
||||||
private _executablePath: string;
|
private _executablePath: string;
|
||||||
private _webSocketNotPipe: WebSocketNotPipe | null;
|
|
||||||
private _browserDescriptor: browserPaths.BrowserDescriptor;
|
private _browserDescriptor: browserPaths.BrowserDescriptor;
|
||||||
readonly _browserPath: string;
|
readonly _browserPath: string;
|
||||||
|
|
||||||
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, webSocketOrPipe: WebSocketNotPipe | null) {
|
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor) {
|
||||||
this._name = browser.name;
|
this._name = browser.name;
|
||||||
const browsersPath = browserPaths.browsersPath(packagePath);
|
const browsersPath = browserPaths.browsersPath(packagePath);
|
||||||
this._browserDescriptor = browser;
|
this._browserDescriptor = browser;
|
||||||
this._browserPath = browserPaths.browserDirectory(browsersPath, browser);
|
this._browserPath = browserPaths.browserDirectory(browsersPath, browser);
|
||||||
this._executablePath = browserPaths.executablePath(this._browserPath, browser) || '';
|
this._executablePath = browserPaths.executablePath(this._browserPath, browser) || '';
|
||||||
this._webSocketNotPipe = webSocketOrPipe;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
executablePath(): string {
|
executablePath(): string {
|
||||||
|
|
@ -175,7 +171,7 @@ export abstract class BrowserType {
|
||||||
handleSIGTERM,
|
handleSIGTERM,
|
||||||
handleSIGHUP,
|
handleSIGHUP,
|
||||||
progress,
|
progress,
|
||||||
pipe: !this._webSocketNotPipe,
|
stdio: 'pipe',
|
||||||
tempDirectories,
|
tempDirectories,
|
||||||
attemptToGracefullyClose: async () => {
|
attemptToGracefullyClose: async () => {
|
||||||
if ((options as any).__testHookGracefullyClose)
|
if ((options as any).__testHookGracefullyClose)
|
||||||
|
|
@ -198,14 +194,8 @@ export abstract class BrowserType {
|
||||||
};
|
};
|
||||||
progress.cleanupWhenAborted(() => browserProcess && closeOrKill(browserProcess, progress.timeUntilDeadline()));
|
progress.cleanupWhenAborted(() => browserProcess && closeOrKill(browserProcess, progress.timeUntilDeadline()));
|
||||||
|
|
||||||
if (this._webSocketNotPipe) {
|
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
||||||
const match = await waitForLine(progress, launchedProcess, this._webSocketNotPipe.stream === 'stdout' ? launchedProcess.stdout : launchedProcess.stderr, this._webSocketNotPipe.webSocketRegex);
|
transport = new PipeTransport(stdio[3], stdio[4]);
|
||||||
const innerEndpoint = match[1];
|
|
||||||
transport = await WebSocketTransport.connect(progress, innerEndpoint);
|
|
||||||
} else {
|
|
||||||
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
|
||||||
transport = new PipeTransport(stdio[3], stdio[4]);
|
|
||||||
}
|
|
||||||
return { browserProcess, downloadsPath, transport };
|
return { browserProcess, downloadsPath, transport };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,22 +26,13 @@ import type { BrowserDescriptor } from '../../utils/browserPaths';
|
||||||
import { CRDevTools } from './crDevTools';
|
import { CRDevTools } from './crDevTools';
|
||||||
import { BrowserOptions } from '../browser';
|
import { BrowserOptions } from '../browser';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import { isDebugMode, getFromENV } from '../../utils/utils';
|
import { isDebugMode } from '../../utils/utils';
|
||||||
|
|
||||||
export class Chromium extends BrowserType {
|
export class Chromium extends BrowserType {
|
||||||
private _devtools: CRDevTools | undefined;
|
private _devtools: CRDevTools | undefined;
|
||||||
private _debugPort: number | undefined;
|
|
||||||
|
|
||||||
constructor(packagePath: string, browser: BrowserDescriptor) {
|
constructor(packagePath: string, browser: BrowserDescriptor) {
|
||||||
const debugPortStr = getFromENV('PLAYWRIGHT_CHROMIUM_DEBUG_PORT');
|
super(packagePath, browser);
|
||||||
const debugPort: number | undefined = debugPortStr ? +debugPortStr : undefined;
|
|
||||||
if (debugPort !== undefined) {
|
|
||||||
if (Number.isNaN(debugPort))
|
|
||||||
throw new Error(`PLAYWRIGHT_CHROMIUM_DEBUG_PORT must be a number, but is set to "${debugPortStr}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
super(packagePath, browser, debugPort ? { webSocketRegex: /^DevTools listening on (ws:\/\/.*)$/, stream: 'stderr' } : null);
|
|
||||||
this._debugPort = debugPort;
|
|
||||||
if (isDebugMode())
|
if (isDebugMode())
|
||||||
this._devtools = this._createDevTools();
|
this._devtools = this._createDevTools();
|
||||||
}
|
}
|
||||||
|
|
@ -95,10 +86,7 @@ export class Chromium extends BrowserType {
|
||||||
throw new Error('Arguments can not specify page to be opened');
|
throw new Error('Arguments can not specify page to be opened');
|
||||||
const chromeArguments = [...DEFAULT_ARGS];
|
const chromeArguments = [...DEFAULT_ARGS];
|
||||||
chromeArguments.push(`--user-data-dir=${userDataDir}`);
|
chromeArguments.push(`--user-data-dir=${userDataDir}`);
|
||||||
if (this._debugPort !== undefined)
|
chromeArguments.push('--remote-debugging-pipe');
|
||||||
chromeArguments.push('--remote-debugging-port=' + this._debugPort);
|
|
||||||
else
|
|
||||||
chromeArguments.push('--remote-debugging-pipe');
|
|
||||||
if (options.devtools)
|
if (options.devtools)
|
||||||
chromeArguments.push('--auto-open-devtools-for-tabs');
|
chromeArguments.push('--auto-open-devtools-for-tabs');
|
||||||
if (options.headless) {
|
if (options.headless) {
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export class VideoRecorder {
|
||||||
const { launchedProcess, gracefullyClose } = await launchProcess({
|
const { launchedProcess, gracefullyClose } = await launchProcess({
|
||||||
executablePath,
|
executablePath,
|
||||||
args,
|
args,
|
||||||
pipeStdin: true,
|
stdio: 'stdin',
|
||||||
progress,
|
progress,
|
||||||
tempDirectories: [],
|
tempDirectories: [],
|
||||||
attemptToGracefullyClose: async () => {
|
attemptToGracefullyClose: async () => {
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,15 @@ import { Page } from '../page';
|
||||||
import { TimeoutSettings } from '../../utils/timeoutSettings';
|
import { TimeoutSettings } from '../../utils/timeoutSettings';
|
||||||
import { WebSocketTransport } from '../transport';
|
import { WebSocketTransport } from '../transport';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import { launchProcess, waitForLine, envArrayToObject } from '../processLauncher';
|
import { launchProcess, envArrayToObject } from '../processLauncher';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import type {BrowserWindow} from 'electron';
|
import type {BrowserWindow} from 'electron';
|
||||||
import { ProgressController, runAbortableTask } from '../progress';
|
import { Progress, ProgressController, runAbortableTask } from '../progress';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { helper } from '../helper';
|
import { helper } from '../helper';
|
||||||
import { BrowserProcess } from '../browser';
|
import { BrowserProcess } from '../browser';
|
||||||
|
import * as childProcess from 'child_process';
|
||||||
|
import * as readline from 'readline';
|
||||||
|
|
||||||
export type ElectronLaunchOptionsBase = {
|
export type ElectronLaunchOptionsBase = {
|
||||||
args?: string[],
|
args?: string[],
|
||||||
|
|
@ -163,18 +165,18 @@ export class Electron {
|
||||||
handleSIGTERM,
|
handleSIGTERM,
|
||||||
handleSIGHUP,
|
handleSIGHUP,
|
||||||
progress,
|
progress,
|
||||||
pipe: true,
|
stdio: 'pipe',
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
tempDirectories: [],
|
tempDirectories: [],
|
||||||
attemptToGracefullyClose: () => app!.close(),
|
attemptToGracefullyClose: () => app!.close(),
|
||||||
onExit: () => {},
|
onExit: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^Debugger listening on (ws:\/\/.*)$/);
|
const nodeMatch = await waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/);
|
||||||
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
|
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
|
||||||
const nodeConnection = new CRConnection(nodeTransport);
|
const nodeConnection = new CRConnection(nodeTransport);
|
||||||
|
|
||||||
const chromeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/);
|
const chromeMatch = await waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/);
|
||||||
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
|
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
|
||||||
const browserProcess: BrowserProcess = {
|
const browserProcess: BrowserProcess = {
|
||||||
onclose: undefined,
|
onclose: undefined,
|
||||||
|
|
@ -189,3 +191,31 @@ export class Electron {
|
||||||
}, TimeoutSettings.timeout(options));
|
}, TimeoutSettings.timeout(options));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function waitForLine(progress: Progress, process: childProcess.ChildProcess, regex: RegExp): Promise<RegExpMatchArray> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const rl = readline.createInterface({ input: process.stderr });
|
||||||
|
const failError = new Error('Process failed to launch!');
|
||||||
|
const listeners = [
|
||||||
|
helper.addEventListener(rl, 'line', onLine),
|
||||||
|
helper.addEventListener(rl, 'close', reject.bind(null, failError)),
|
||||||
|
helper.addEventListener(process, 'exit', reject.bind(null, failError)),
|
||||||
|
// It is Ok to remove error handler because we did not create process and there is another listener.
|
||||||
|
helper.addEventListener(process, 'error', reject.bind(null, failError))
|
||||||
|
];
|
||||||
|
|
||||||
|
progress.cleanupWhenAborted(cleanup);
|
||||||
|
|
||||||
|
function onLine(line: string) {
|
||||||
|
const match = line.match(regex);
|
||||||
|
if (!match)
|
||||||
|
return;
|
||||||
|
cleanup();
|
||||||
|
resolve(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
helper.removeEventListeners(listeners);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,9 @@ import { BrowserType } from '../browserType';
|
||||||
import { Env } from '../processLauncher';
|
import { Env } from '../processLauncher';
|
||||||
import { ConnectionTransport } from '../transport';
|
import { ConnectionTransport } from '../transport';
|
||||||
import { BrowserOptions } from '../browser';
|
import { BrowserOptions } from '../browser';
|
||||||
import { BrowserDescriptor } from '../../utils/browserPaths';
|
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
|
||||||
export class Firefox extends BrowserType {
|
export class Firefox extends BrowserType {
|
||||||
constructor(packagePath: string, browser: BrowserDescriptor) {
|
|
||||||
const webSocketRegex = /^Juggler listening on (ws:\/\/.*)$/;
|
|
||||||
super(packagePath, browser, { webSocketRegex, stream: 'stdout' });
|
|
||||||
}
|
|
||||||
|
|
||||||
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
|
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
|
||||||
return FFBrowser.connect(transport, options);
|
return FFBrowser.connect(transport, options);
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +72,7 @@ export class Firefox extends BrowserType {
|
||||||
firefoxArguments.push('-foreground');
|
firefoxArguments.push('-foreground');
|
||||||
}
|
}
|
||||||
firefoxArguments.push(`-profile`, userDataDir);
|
firefoxArguments.push(`-profile`, userDataDir);
|
||||||
firefoxArguments.push('-juggler', '0');
|
firefoxArguments.push('-juggler-pipe');
|
||||||
firefoxArguments.push(...args);
|
firefoxArguments.push(...args);
|
||||||
if (isPersistent)
|
if (isPersistent)
|
||||||
firefoxArguments.push('about:blank');
|
firefoxArguments.push('about:blank');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import * as removeFolder from 'rimraf';
|
import * as removeFolder from 'rimraf';
|
||||||
import * as stream from 'stream';
|
|
||||||
import { helper } from './helper';
|
import { helper } from './helper';
|
||||||
import { Progress } from './progress';
|
import { Progress } from './progress';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
|
|
@ -34,8 +33,7 @@ export type LaunchProcessOptions = {
|
||||||
handleSIGINT?: boolean,
|
handleSIGINT?: boolean,
|
||||||
handleSIGTERM?: boolean,
|
handleSIGTERM?: boolean,
|
||||||
handleSIGHUP?: boolean,
|
handleSIGHUP?: boolean,
|
||||||
pipe?: boolean,
|
stdio: 'pipe' | 'stdin',
|
||||||
pipeStdin?: boolean,
|
|
||||||
tempDirectories: string[],
|
tempDirectories: string[],
|
||||||
|
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
|
|
@ -68,9 +66,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
const cleanup = () => helper.removeFolders(options.tempDirectories);
|
const cleanup = () => helper.removeFolders(options.tempDirectories);
|
||||||
|
|
||||||
const progress = options.progress;
|
const progress = options.progress;
|
||||||
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];
|
const stdio: ('ignore' | 'pipe')[] = options.stdio === 'pipe' ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'];
|
||||||
if (options.pipeStdin)
|
|
||||||
stdio[0] = 'pipe';
|
|
||||||
progress.log(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
|
progress.log(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
|
||||||
const spawnedProcess = childProcess.spawn(
|
const spawnedProcess = childProcess.spawn(
|
||||||
options.executablePath,
|
options.executablePath,
|
||||||
|
|
@ -193,34 +189,6 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
return { launchedProcess: spawnedProcess, gracefullyClose, kill: killAndWait };
|
return { launchedProcess: spawnedProcess, gracefullyClose, kill: killAndWait };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function waitForLine(progress: Progress, process: childProcess.ChildProcess, inputStream: stream.Readable, regex: RegExp): Promise<RegExpMatchArray> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const rl = readline.createInterface({ input: inputStream });
|
|
||||||
const failError = new Error('Process failed to launch!');
|
|
||||||
const listeners = [
|
|
||||||
helper.addEventListener(rl, 'line', onLine),
|
|
||||||
helper.addEventListener(rl, 'close', reject.bind(null, failError)),
|
|
||||||
helper.addEventListener(process, 'exit', reject.bind(null, failError)),
|
|
||||||
// It is Ok to remove error handler because we did not create process and there is another listener.
|
|
||||||
helper.addEventListener(process, 'error', reject.bind(null, failError))
|
|
||||||
];
|
|
||||||
|
|
||||||
progress.cleanupWhenAborted(cleanup);
|
|
||||||
|
|
||||||
function onLine(line: string) {
|
|
||||||
const match = line.match(regex);
|
|
||||||
if (!match)
|
|
||||||
return;
|
|
||||||
cleanup();
|
|
||||||
resolve(match);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
helper.removeEventListeners(listeners);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function envArrayToObject(env: types.EnvArray): Env {
|
export function envArrayToObject(env: types.EnvArray): Env {
|
||||||
const result: Env = {};
|
const result: Env = {};
|
||||||
for (const { name, value } of env)
|
for (const { name, value } of env)
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,9 @@ import { kBrowserCloseMessageId } from './wkConnection';
|
||||||
import { BrowserType } from '../browserType';
|
import { BrowserType } from '../browserType';
|
||||||
import { ConnectionTransport } from '../transport';
|
import { ConnectionTransport } from '../transport';
|
||||||
import { BrowserOptions } from '../browser';
|
import { BrowserOptions } from '../browser';
|
||||||
import { BrowserDescriptor } from '../../utils/browserPaths';
|
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
|
||||||
export class WebKit extends BrowserType {
|
export class WebKit extends BrowserType {
|
||||||
constructor(packagePath: string, browser: BrowserDescriptor) {
|
|
||||||
super(packagePath, browser, null /* use pipe not websocket */);
|
|
||||||
}
|
|
||||||
|
|
||||||
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> {
|
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> {
|
||||||
return WKBrowser.connect(transport, options);
|
return WKBrowser.connect(transport, options);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue