chore: unify launching server between browser types (#2338)
This commit is contained in:
parent
3aca21c13b
commit
55d47fd48f
|
|
@ -14,14 +14,22 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as util from 'util';
|
||||||
import { BrowserContext, PersistentContextOptions, validatePersistentContextOptions } from '../browserContext';
|
import { BrowserContext, PersistentContextOptions, validatePersistentContextOptions } from '../browserContext';
|
||||||
import { BrowserServer } from './browserServer';
|
import { BrowserServer, WebSocketWrapper } from './browserServer';
|
||||||
import * as browserPaths from '../install/browserPaths';
|
import * as browserPaths from '../install/browserPaths';
|
||||||
import { Logger, RootLogger } from '../logger';
|
import { Logger, RootLogger, InnerLogger } from '../logger';
|
||||||
import { ConnectionTransport, WebSocketTransport } from '../transport';
|
import { ConnectionTransport, WebSocketTransport } from '../transport';
|
||||||
import { BrowserBase, BrowserOptions, Browser } from '../browser';
|
import { BrowserBase, BrowserOptions, Browser } from '../browser';
|
||||||
import { assert, helper } from '../helper';
|
import { assert, helper } from '../helper';
|
||||||
import { TimeoutSettings } from '../timeoutSettings';
|
import { TimeoutSettings } from '../timeoutSettings';
|
||||||
|
import { launchProcess, Env, waitForLine } from './processLauncher';
|
||||||
|
import { Events } from '../events';
|
||||||
|
import { TimeoutError } from '../errors';
|
||||||
|
import { PipeTransport } from './pipeTransport';
|
||||||
|
|
||||||
export type BrowserArgOptions = {
|
export type BrowserArgOptions = {
|
||||||
headless?: boolean,
|
headless?: boolean,
|
||||||
|
|
@ -37,7 +45,7 @@ type LaunchOptionsBase = BrowserArgOptions & {
|
||||||
handleSIGHUP?: boolean,
|
handleSIGHUP?: boolean,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
logger?: Logger,
|
logger?: Logger,
|
||||||
env?: {[key: string]: string|number|boolean}
|
env?: Env,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function processBrowserArgOptions(options: LaunchOptionsBase): { devtools: boolean, headless: boolean } {
|
export function processBrowserArgOptions(options: LaunchOptionsBase): { devtools: boolean, headless: boolean } {
|
||||||
|
|
@ -45,7 +53,7 @@ export function processBrowserArgOptions(options: LaunchOptionsBase): { devtools
|
||||||
return { devtools, headless };
|
return { devtools, headless };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectOptions = {
|
type ConnectOptions = {
|
||||||
wsEndpoint: string,
|
wsEndpoint: string,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
logger?: Logger,
|
logger?: Logger,
|
||||||
|
|
@ -53,7 +61,7 @@ export type ConnectOptions = {
|
||||||
};
|
};
|
||||||
export type LaunchType = 'local' | 'server' | 'persistent';
|
export type LaunchType = 'local' | 'server' | 'persistent';
|
||||||
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
|
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
|
||||||
export type LaunchServerOptions = LaunchOptionsBase & { port?: number };
|
type LaunchServerOptions = LaunchOptionsBase & { port?: number };
|
||||||
|
|
||||||
export interface BrowserType {
|
export interface BrowserType {
|
||||||
executablePath(): string;
|
executablePath(): string;
|
||||||
|
|
@ -64,16 +72,20 @@ export interface BrowserType {
|
||||||
connect(options: ConnectOptions): Promise<Browser>;
|
connect(options: ConnectOptions): Promise<Browser>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
||||||
|
|
||||||
export abstract class BrowserTypeBase implements BrowserType {
|
export abstract class BrowserTypeBase implements BrowserType {
|
||||||
private _name: string;
|
private _name: string;
|
||||||
private _executablePath: string | undefined;
|
private _executablePath: string | undefined;
|
||||||
|
private _webSocketRegexNotPipe: RegExp | null;
|
||||||
readonly _browserPath: string;
|
readonly _browserPath: string;
|
||||||
|
|
||||||
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor) {
|
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, webSocketRegexNotPipe: RegExp | null) {
|
||||||
this._name = browser.name;
|
this._name = browser.name;
|
||||||
const browsersPath = browserPaths.browsersPath(packagePath);
|
const browsersPath = browserPaths.browsersPath(packagePath);
|
||||||
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._webSocketRegexNotPipe = webSocketRegexNotPipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
executablePath(): string {
|
executablePath(): string {
|
||||||
|
|
@ -183,6 +195,88 @@ export abstract class BrowserTypeBase implements BrowserType {
|
||||||
return this._connectToTransport(transport, { slowMo: options.slowMo, logger });
|
return this._connectToTransport(transport, { slowMo: options.slowMo, logger });
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer>;
|
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer> {
|
||||||
|
const {
|
||||||
|
ignoreDefaultArgs = false,
|
||||||
|
args = [],
|
||||||
|
executablePath = null,
|
||||||
|
env = process.env,
|
||||||
|
handleSIGINT = true,
|
||||||
|
handleSIGTERM = true,
|
||||||
|
handleSIGHUP = true,
|
||||||
|
port = 0,
|
||||||
|
} = options;
|
||||||
|
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
||||||
|
|
||||||
|
let temporaryUserDataDir: string | null = null;
|
||||||
|
if (!userDataDir) {
|
||||||
|
userDataDir = await mkdtempAsync(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
|
||||||
|
temporaryUserDataDir = userDataDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserArguments = [];
|
||||||
|
if (!ignoreDefaultArgs)
|
||||||
|
browserArguments.push(...this._defaultArgs(options, launchType, userDataDir));
|
||||||
|
else if (Array.isArray(ignoreDefaultArgs))
|
||||||
|
browserArguments.push(...this._defaultArgs(options, launchType, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
|
||||||
|
else
|
||||||
|
browserArguments.push(...args);
|
||||||
|
|
||||||
|
const executable = executablePath || this.executablePath();
|
||||||
|
if (!executable)
|
||||||
|
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
|
||||||
|
|
||||||
|
// Note: it is important to define these variables before launchProcess, so that we don't get
|
||||||
|
// "Cannot access 'browserServer' before initialization" if something went wrong.
|
||||||
|
let transport: ConnectionTransport | undefined = undefined;
|
||||||
|
let browserServer: BrowserServer | undefined = undefined;
|
||||||
|
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
|
||||||
|
executablePath: executable,
|
||||||
|
args: browserArguments,
|
||||||
|
env: this._amendEnvironment(env, userDataDir, executable, browserArguments),
|
||||||
|
handleSIGINT,
|
||||||
|
handleSIGTERM,
|
||||||
|
handleSIGHUP,
|
||||||
|
logger,
|
||||||
|
pipe: !this._webSocketRegexNotPipe,
|
||||||
|
tempDir: temporaryUserDataDir || undefined,
|
||||||
|
attemptToGracefullyClose: async () => {
|
||||||
|
if ((options as any).__testHookGracefullyClose)
|
||||||
|
await (options as any).__testHookGracefullyClose();
|
||||||
|
// We try to gracefully close to prevent crash reporting and core dumps.
|
||||||
|
// Note that it's fine to reuse the pipe transport, since
|
||||||
|
// our connection ignores kBrowserCloseMessageId.
|
||||||
|
this._attemptToGracefullyCloseBrowser(transport!);
|
||||||
|
},
|
||||||
|
onkill: (exitCode, signal) => {
|
||||||
|
if (browserServer)
|
||||||
|
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this._webSocketRegexNotPipe) {
|
||||||
|
const timeoutError = new TimeoutError(`Timed out while trying to connect to the browser!`);
|
||||||
|
const match = await waitForLine(launchedProcess, launchedProcess.stdout, this._webSocketRegexNotPipe, helper.timeUntilDeadline(deadline), timeoutError);
|
||||||
|
const innerEndpoint = match[1];
|
||||||
|
transport = await WebSocketTransport.connect(innerEndpoint, logger, deadline);
|
||||||
|
} 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], logger);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If we can't establish a connection, kill the process and exit.
|
||||||
|
helper.killProcess(launchedProcess);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, transport, downloadsPath,
|
||||||
|
launchType === 'server' ? this._wrapTransportWithWebSocket(transport, logger, port) : null);
|
||||||
|
return browserServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract _defaultArgs(options: BrowserArgOptions, launchType: LaunchType, userDataDir: string): string[];
|
||||||
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
|
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
|
||||||
|
abstract _wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper;
|
||||||
|
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
|
||||||
|
abstract _attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void;
|
||||||
}
|
}
|
||||||
|
|
@ -15,21 +15,16 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as os from 'os';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as util from 'util';
|
|
||||||
import { helper, assert, isDebugMode } from '../helper';
|
import { helper, assert, isDebugMode } from '../helper';
|
||||||
import { CRBrowser } from '../chromium/crBrowser';
|
import { CRBrowser } from '../chromium/crBrowser';
|
||||||
import * as ws from 'ws';
|
import * as ws from 'ws';
|
||||||
import { launchProcess } from './processLauncher';
|
import { Env } from './processLauncher';
|
||||||
import { kBrowserCloseMessageId } from '../chromium/crConnection';
|
import { kBrowserCloseMessageId } from '../chromium/crConnection';
|
||||||
import { PipeTransport } from './pipeTransport';
|
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
|
||||||
import { BrowserArgOptions, LaunchServerOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
|
import { WebSocketWrapper } from './browserServer';
|
||||||
import { BrowserServer, WebSocketWrapper } from './browserServer';
|
|
||||||
import { Events } from '../events';
|
|
||||||
import { ConnectionTransport, ProtocolRequest } from '../transport';
|
import { ConnectionTransport, ProtocolRequest } from '../transport';
|
||||||
import { InnerLogger, logError, RootLogger } from '../logger';
|
import { InnerLogger, logError } from '../logger';
|
||||||
import { BrowserDescriptor } from '../install/browserPaths';
|
import { BrowserDescriptor } from '../install/browserPaths';
|
||||||
import { CRDevTools } from '../chromium/crDevTools';
|
import { CRDevTools } from '../chromium/crDevTools';
|
||||||
import { BrowserOptions } from '../browser';
|
import { BrowserOptions } from '../browser';
|
||||||
|
|
@ -38,7 +33,7 @@ export class Chromium extends BrowserTypeBase {
|
||||||
private _devtools: CRDevTools | undefined;
|
private _devtools: CRDevTools | undefined;
|
||||||
|
|
||||||
constructor(packagePath: string, browser: BrowserDescriptor) {
|
constructor(packagePath: string, browser: BrowserDescriptor) {
|
||||||
super(packagePath, browser);
|
super(packagePath, browser, null /* use pipe not websocket */);
|
||||||
if (isDebugMode())
|
if (isDebugMode())
|
||||||
this._devtools = this._createDevTools();
|
this._devtools = this._createDevTools();
|
||||||
}
|
}
|
||||||
|
|
@ -56,77 +51,22 @@ export class Chromium extends BrowserTypeBase {
|
||||||
return CRBrowser.connect(transport, options, devtools);
|
return CRBrowser.connect(transport, options, devtools);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer> {
|
_amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||||
const {
|
|
||||||
ignoreDefaultArgs = false,
|
|
||||||
args = [],
|
|
||||||
executablePath = null,
|
|
||||||
env = process.env,
|
|
||||||
handleSIGINT = true,
|
|
||||||
handleSIGTERM = true,
|
|
||||||
handleSIGHUP = true,
|
|
||||||
port = 0,
|
|
||||||
} = options;
|
|
||||||
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
|
||||||
|
|
||||||
let temporaryUserDataDir: string | null = null;
|
|
||||||
if (!userDataDir) {
|
|
||||||
userDataDir = await mkdtempAsync(CHROMIUM_PROFILE_PATH);
|
|
||||||
temporaryUserDataDir = userDataDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
const runningAsRoot = process.geteuid && process.geteuid() === 0;
|
const runningAsRoot = process.geteuid && process.geteuid() === 0;
|
||||||
assert(!runningAsRoot || args.includes('--no-sandbox'), 'Cannot launch Chromium as root without --no-sandbox. See https://crbug.com/638180.');
|
assert(!runningAsRoot || browserArguments.includes('--no-sandbox'), 'Cannot launch Chromium as root without --no-sandbox. See https://crbug.com/638180.');
|
||||||
|
return env;
|
||||||
const chromeArguments = [];
|
|
||||||
if (!ignoreDefaultArgs)
|
|
||||||
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir));
|
|
||||||
else if (Array.isArray(ignoreDefaultArgs))
|
|
||||||
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
|
|
||||||
else
|
|
||||||
chromeArguments.push(...args);
|
|
||||||
|
|
||||||
const chromeExecutable = executablePath || this.executablePath();
|
|
||||||
if (!chromeExecutable)
|
|
||||||
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
|
|
||||||
|
|
||||||
// Note: it is important to define these variables before launchProcess, so that we don't get
|
|
||||||
// "Cannot access 'browserServer' before initialization" if something went wrong.
|
|
||||||
let transport: PipeTransport | undefined = undefined;
|
|
||||||
let browserServer: BrowserServer | undefined = undefined;
|
|
||||||
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
|
|
||||||
executablePath: chromeExecutable,
|
|
||||||
args: chromeArguments,
|
|
||||||
env,
|
|
||||||
handleSIGINT,
|
|
||||||
handleSIGTERM,
|
|
||||||
handleSIGHUP,
|
|
||||||
logger,
|
|
||||||
pipe: true,
|
|
||||||
tempDir: temporaryUserDataDir || undefined,
|
|
||||||
attemptToGracefullyClose: async () => {
|
|
||||||
if ((options as any).__testHookGracefullyClose)
|
|
||||||
await (options as any).__testHookGracefullyClose();
|
|
||||||
// We try to gracefully close to prevent crash reporting and core dumps.
|
|
||||||
// Note that it's fine to reuse the pipe transport, since
|
|
||||||
// our connection ignores kBrowserCloseMessageId.
|
|
||||||
const t = transport!;
|
|
||||||
const message: ProtocolRequest = { method: 'Browser.close', id: kBrowserCloseMessageId, params: {} };
|
|
||||||
t.send(message);
|
|
||||||
},
|
|
||||||
onkill: (exitCode, signal) => {
|
|
||||||
if (browserServer)
|
|
||||||
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
|
||||||
transport = new PipeTransport(stdio[3], stdio[4], logger);
|
|
||||||
browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, transport, downloadsPath, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port) : null);
|
|
||||||
return browserServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string): string[] {
|
_attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||||
|
const message: ProtocolRequest = { method: 'Browser.close', id: kBrowserCloseMessageId, params: {} };
|
||||||
|
transport.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper {
|
||||||
|
return wrapTransportWithWebSocket(transport, logger, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
_defaultArgs(options: BrowserArgOptions, launchType: LaunchType, userDataDir: string): string[] {
|
||||||
const { devtools, headless } = processBrowserArgOptions(options);
|
const { devtools, headless } = processBrowserArgOptions(options);
|
||||||
const { args = [] } = options;
|
const { args = [] } = options;
|
||||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||||
|
|
@ -301,10 +241,6 @@ function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: Inne
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
|
||||||
|
|
||||||
const CHROMIUM_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-');
|
|
||||||
|
|
||||||
const DEFAULT_ARGS = [
|
const DEFAULT_ARGS = [
|
||||||
'--disable-background-networking',
|
'--disable-background-networking',
|
||||||
'--enable-features=NetworkService,NetworkServiceInProcess',
|
'--enable-features=NetworkService,NetworkServiceInProcess',
|
||||||
|
|
|
||||||
|
|
@ -15,112 +15,48 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as util from 'util';
|
|
||||||
import * as ws from 'ws';
|
import * as ws from 'ws';
|
||||||
import { TimeoutError } from '../errors';
|
|
||||||
import { Events } from '../events';
|
|
||||||
import { FFBrowser } from '../firefox/ffBrowser';
|
import { FFBrowser } from '../firefox/ffBrowser';
|
||||||
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
|
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
|
||||||
import { helper, assert } from '../helper';
|
import { helper } from '../helper';
|
||||||
import { BrowserServer, WebSocketWrapper } from './browserServer';
|
import { WebSocketWrapper } from './browserServer';
|
||||||
import { BrowserArgOptions, LaunchServerOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
|
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
|
||||||
import { launchProcess, waitForLine } from './processLauncher';
|
import { Env } from './processLauncher';
|
||||||
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
|
import { ConnectionTransport, SequenceNumberMixer } from '../transport';
|
||||||
import { InnerLogger, logError, RootLogger } from '../logger';
|
import { InnerLogger, logError } from '../logger';
|
||||||
import { BrowserOptions } from '../browser';
|
import { BrowserOptions } from '../browser';
|
||||||
|
import { BrowserDescriptor } from '../install/browserPaths';
|
||||||
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
|
||||||
|
|
||||||
export class Firefox extends BrowserTypeBase {
|
export class Firefox extends BrowserTypeBase {
|
||||||
|
constructor(packagePath: string, browser: BrowserDescriptor) {
|
||||||
|
const websocketRegex = /^Juggler listening on (ws:\/\/.*)$/;
|
||||||
|
super(packagePath, browser, websocketRegex /* use websocket not pipe */);
|
||||||
|
}
|
||||||
|
|
||||||
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
|
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
|
||||||
return FFBrowser.connect(transport, options);
|
return FFBrowser.connect(transport, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer> {
|
_amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||||
const {
|
return os.platform() === 'linux' ? {
|
||||||
ignoreDefaultArgs = false,
|
...env,
|
||||||
args = [],
|
// On linux Juggler ships the libstdc++ it was linked against.
|
||||||
executablePath = null,
|
LD_LIBRARY_PATH: `${path.dirname(executable)}:${process.env.LD_LIBRARY_PATH}`,
|
||||||
env = process.env,
|
} : env;
|
||||||
handleSIGHUP = true,
|
|
||||||
handleSIGINT = true,
|
|
||||||
handleSIGTERM = true,
|
|
||||||
timeout = 30000,
|
|
||||||
port = 0,
|
|
||||||
} = options;
|
|
||||||
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
|
||||||
|
|
||||||
let temporaryProfileDir = null;
|
|
||||||
if (!userDataDir) {
|
|
||||||
userDataDir = await mkdtempAsync(path.join(os.tmpdir(), 'playwright_dev_firefox_profile-'));
|
|
||||||
temporaryProfileDir = userDataDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firefoxArguments = [];
|
|
||||||
if (!ignoreDefaultArgs)
|
|
||||||
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir, 0));
|
|
||||||
else if (Array.isArray(ignoreDefaultArgs))
|
|
||||||
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir, 0).filter(arg => !ignoreDefaultArgs.includes(arg)));
|
|
||||||
else
|
|
||||||
firefoxArguments.push(...args);
|
|
||||||
|
|
||||||
const firefoxExecutable = executablePath || this.executablePath();
|
|
||||||
if (!firefoxExecutable)
|
|
||||||
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
|
|
||||||
|
|
||||||
// Note: it is important to define these variables before launchProcess, so that we don't get
|
|
||||||
// "Cannot access 'transport' before initialization" if something went wrong.
|
|
||||||
let browserServer: BrowserServer | undefined = undefined;
|
|
||||||
let transport: ConnectionTransport | undefined = undefined;
|
|
||||||
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
|
|
||||||
executablePath: firefoxExecutable,
|
|
||||||
args: firefoxArguments,
|
|
||||||
env: os.platform() === 'linux' ? {
|
|
||||||
...env,
|
|
||||||
// On linux Juggler ships the libstdc++ it was linked against.
|
|
||||||
LD_LIBRARY_PATH: `${path.dirname(firefoxExecutable)}:${process.env.LD_LIBRARY_PATH}`,
|
|
||||||
} : env,
|
|
||||||
handleSIGINT,
|
|
||||||
handleSIGTERM,
|
|
||||||
handleSIGHUP,
|
|
||||||
logger,
|
|
||||||
pipe: false,
|
|
||||||
tempDir: temporaryProfileDir || undefined,
|
|
||||||
attemptToGracefullyClose: async () => {
|
|
||||||
if ((options as any).__testHookGracefullyClose)
|
|
||||||
await (options as any).__testHookGracefullyClose();
|
|
||||||
|
|
||||||
// We try to gracefully close to prevent crash reporting and core dumps.
|
|
||||||
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
|
|
||||||
transport!.send(message);
|
|
||||||
},
|
|
||||||
onkill: (exitCode, signal) => {
|
|
||||||
if (browserServer)
|
|
||||||
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`);
|
|
||||||
const match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError);
|
|
||||||
const innerEndpoint = match[1];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// If we can't communicate with Firefox on start, kill the process and exit.
|
|
||||||
transport = await WebSocketTransport.connect(innerEndpoint, logger, deadline);
|
|
||||||
} catch (e) {
|
|
||||||
helper.killProcess(launchedProcess);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
const webSocketWrapper = launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port) : null;
|
|
||||||
browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, transport, downloadsPath, webSocketWrapper);
|
|
||||||
return browserServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string, port: number): string[] {
|
_attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||||
|
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
|
||||||
|
transport.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper {
|
||||||
|
return wrapTransportWithWebSocket(transport, logger, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
_defaultArgs(options: BrowserArgOptions, launchType: LaunchType, userDataDir: string): string[] {
|
||||||
const { devtools, headless } = processBrowserArgOptions(options);
|
const { devtools, headless } = processBrowserArgOptions(options);
|
||||||
const { args = [] } = options;
|
const { args = [] } = options;
|
||||||
if (devtools)
|
if (devtools)
|
||||||
|
|
@ -139,7 +75,7 @@ export class Firefox extends BrowserTypeBase {
|
||||||
firefoxArguments.push('-foreground');
|
firefoxArguments.push('-foreground');
|
||||||
}
|
}
|
||||||
firefoxArguments.push(`-profile`, userDataDir);
|
firefoxArguments.push(`-profile`, userDataDir);
|
||||||
firefoxArguments.push('-juggler', String(port));
|
firefoxArguments.push('-juggler', '0');
|
||||||
firefoxArguments.push(...args);
|
firefoxArguments.push(...args);
|
||||||
if (launchType === 'persistent')
|
if (launchType === 'persistent')
|
||||||
firefoxArguments.push('about:blank');
|
firefoxArguments.push('about:blank');
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,12 @@ const browserStdErrLog: Log = {
|
||||||
severity: 'warning'
|
severity: 'warning'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Env = {[key: string]: string | number | boolean | undefined};
|
||||||
|
|
||||||
export type LaunchProcessOptions = {
|
export type LaunchProcessOptions = {
|
||||||
executablePath: string,
|
executablePath: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
env?: {[key: string]: string | number | boolean | undefined},
|
env?: Env,
|
||||||
|
|
||||||
handleSIGINT?: boolean,
|
handleSIGINT?: boolean,
|
||||||
handleSIGTERM?: boolean,
|
handleSIGTERM?: boolean,
|
||||||
|
|
|
||||||
|
|
@ -16,93 +16,40 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { WKBrowser } from '../webkit/wkBrowser';
|
import { WKBrowser } from '../webkit/wkBrowser';
|
||||||
import { PipeTransport } from './pipeTransport';
|
import { Env } from './processLauncher';
|
||||||
import { launchProcess } from './processLauncher';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import { helper } from '../helper';
|
||||||
import * as util from 'util';
|
|
||||||
import { helper, assert } from '../helper';
|
|
||||||
import { kBrowserCloseMessageId } from '../webkit/wkConnection';
|
import { kBrowserCloseMessageId } from '../webkit/wkConnection';
|
||||||
import { BrowserArgOptions, LaunchServerOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
|
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
|
||||||
import { ConnectionTransport, SequenceNumberMixer } from '../transport';
|
import { ConnectionTransport, SequenceNumberMixer } from '../transport';
|
||||||
import * as ws from 'ws';
|
import * as ws from 'ws';
|
||||||
import { BrowserServer, WebSocketWrapper } from './browserServer';
|
import { WebSocketWrapper } from './browserServer';
|
||||||
import { Events } from '../events';
|
import { InnerLogger, logError } from '../logger';
|
||||||
import { InnerLogger, logError, RootLogger } from '../logger';
|
|
||||||
import { BrowserOptions } from '../browser';
|
import { BrowserOptions } from '../browser';
|
||||||
|
import { BrowserDescriptor } from '../install/browserPaths';
|
||||||
|
|
||||||
export class WebKit extends BrowserTypeBase {
|
export class WebKit extends BrowserTypeBase {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer> {
|
_amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||||
const {
|
return { ...env, CURL_COOKIE_JAR_PATH: path.join(userDataDir, 'cookiejar.db') };
|
||||||
ignoreDefaultArgs = false,
|
|
||||||
args = [],
|
|
||||||
executablePath = null,
|
|
||||||
env = process.env,
|
|
||||||
handleSIGINT = true,
|
|
||||||
handleSIGTERM = true,
|
|
||||||
handleSIGHUP = true,
|
|
||||||
port = 0,
|
|
||||||
} = options;
|
|
||||||
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
|
||||||
|
|
||||||
let temporaryUserDataDir: string | null = null;
|
|
||||||
if (!userDataDir) {
|
|
||||||
userDataDir = await mkdtempAsync(WEBKIT_PROFILE_PATH);
|
|
||||||
temporaryUserDataDir = userDataDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
const webkitArguments = [];
|
|
||||||
if (!ignoreDefaultArgs)
|
|
||||||
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir, port));
|
|
||||||
else if (Array.isArray(ignoreDefaultArgs))
|
|
||||||
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir, port).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
|
|
||||||
else
|
|
||||||
webkitArguments.push(...args);
|
|
||||||
|
|
||||||
const webkitExecutable = executablePath || this.executablePath();
|
|
||||||
if (!webkitExecutable)
|
|
||||||
throw new Error(`No executable path is specified.`);
|
|
||||||
|
|
||||||
// Note: it is important to define these variables before launchProcess, so that we don't get
|
|
||||||
// "Cannot access 'browserServer' before initialization" if something went wrong.
|
|
||||||
let transport: ConnectionTransport | undefined = undefined;
|
|
||||||
let browserServer: BrowserServer | undefined = undefined;
|
|
||||||
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
|
|
||||||
executablePath: webkitExecutable,
|
|
||||||
args: webkitArguments,
|
|
||||||
env: { ...env, CURL_COOKIE_JAR_PATH: path.join(userDataDir, 'cookiejar.db') },
|
|
||||||
handleSIGINT,
|
|
||||||
handleSIGTERM,
|
|
||||||
handleSIGHUP,
|
|
||||||
logger,
|
|
||||||
pipe: true,
|
|
||||||
tempDir: temporaryUserDataDir || undefined,
|
|
||||||
attemptToGracefullyClose: async () => {
|
|
||||||
if ((options as any).__testHookGracefullyClose)
|
|
||||||
await (options as any).__testHookGracefullyClose();
|
|
||||||
// We try to gracefully close to prevent crash reporting and core dumps.
|
|
||||||
// Note that it's fine to reuse the pipe transport, since
|
|
||||||
// our connection ignores kBrowserCloseMessageId.
|
|
||||||
transport!.send({method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId});
|
|
||||||
},
|
|
||||||
onkill: (exitCode, signal) => {
|
|
||||||
if (browserServer)
|
|
||||||
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
|
||||||
transport = new PipeTransport(stdio[3], stdio[4], logger);
|
|
||||||
browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, transport, downloadsPath, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port || 0) : null);
|
|
||||||
return browserServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string, port: number): string[] {
|
_attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||||
|
transport.send({method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId});
|
||||||
|
}
|
||||||
|
|
||||||
|
_wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper {
|
||||||
|
return wrapTransportWithWebSocket(transport, logger, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
_defaultArgs(options: BrowserArgOptions, launchType: LaunchType, userDataDir: string): string[] {
|
||||||
const { devtools, headless } = processBrowserArgOptions(options);
|
const { devtools, headless } = processBrowserArgOptions(options);
|
||||||
const { args = [] } = options;
|
const { args = [] } = options;
|
||||||
if (devtools)
|
if (devtools)
|
||||||
|
|
@ -126,10 +73,6 @@ export class WebKit extends BrowserTypeBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
|
||||||
|
|
||||||
const WEBKIT_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-');
|
|
||||||
|
|
||||||
function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper {
|
function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper {
|
||||||
const server = new ws.Server({ port });
|
const server = new ws.Server({ port });
|
||||||
const guid = helper.guid();
|
const guid = helper.guid();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue