Skip to content

Commit 55d47fd

Browse files
authored
chore: unify launching server between browser types (#2338)
1 parent 3aca21c commit 55d47fd

File tree

5 files changed

+167
-256
lines changed

5 files changed

+167
-256
lines changed

src/server/browserType.ts

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,22 @@
1414
* limitations under the License.
1515
*/
1616

17+
import * as fs from 'fs';
18+
import * as os from 'os';
19+
import * as path from 'path';
20+
import * as util from 'util';
1721
import { BrowserContext, PersistentContextOptions, validatePersistentContextOptions } from '../browserContext';
18-
import { BrowserServer } from './browserServer';
22+
import { BrowserServer, WebSocketWrapper } from './browserServer';
1923
import * as browserPaths from '../install/browserPaths';
20-
import { Logger, RootLogger } from '../logger';
24+
import { Logger, RootLogger, InnerLogger } from '../logger';
2125
import { ConnectionTransport, WebSocketTransport } from '../transport';
2226
import { BrowserBase, BrowserOptions, Browser } from '../browser';
2327
import { assert, helper } from '../helper';
2428
import { TimeoutSettings } from '../timeoutSettings';
29+
import { launchProcess, Env, waitForLine } from './processLauncher';
30+
import { Events } from '../events';
31+
import { TimeoutError } from '../errors';
32+
import { PipeTransport } from './pipeTransport';
2533

2634
export type BrowserArgOptions = {
2735
headless?: boolean,
@@ -37,23 +45,23 @@ type LaunchOptionsBase = BrowserArgOptions & {
3745
handleSIGHUP?: boolean,
3846
timeout?: number,
3947
logger?: Logger,
40-
env?: {[key: string]: string|number|boolean}
48+
env?: Env,
4149
};
4250

4351
export function processBrowserArgOptions(options: LaunchOptionsBase): { devtools: boolean, headless: boolean } {
4452
const { devtools = false, headless = !devtools } = options;
4553
return { devtools, headless };
4654
}
4755

48-
export type ConnectOptions = {
56+
type ConnectOptions = {
4957
wsEndpoint: string,
5058
slowMo?: number,
5159
logger?: Logger,
5260
timeout?: number,
5361
};
5462
export type LaunchType = 'local' | 'server' | 'persistent';
5563
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
56-
export type LaunchServerOptions = LaunchOptionsBase & { port?: number };
64+
type LaunchServerOptions = LaunchOptionsBase & { port?: number };
5765

5866
export interface BrowserType {
5967
executablePath(): string;
@@ -64,16 +72,20 @@ export interface BrowserType {
6472
connect(options: ConnectOptions): Promise<Browser>;
6573
}
6674

75+
const mkdtempAsync = util.promisify(fs.mkdtemp);
76+
6777
export abstract class BrowserTypeBase implements BrowserType {
6878
private _name: string;
6979
private _executablePath: string | undefined;
80+
private _webSocketRegexNotPipe: RegExp | null;
7081
readonly _browserPath: string;
7182

72-
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor) {
83+
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, webSocketRegexNotPipe: RegExp | null) {
7384
this._name = browser.name;
7485
const browsersPath = browserPaths.browsersPath(packagePath);
7586
this._browserPath = browserPaths.browserDirectory(browsersPath, browser);
7687
this._executablePath = browserPaths.executablePath(this._browserPath, browser);
88+
this._webSocketRegexNotPipe = webSocketRegexNotPipe;
7789
}
7890

7991
executablePath(): string {
@@ -183,6 +195,88 @@ export abstract class BrowserTypeBase implements BrowserType {
183195
return this._connectToTransport(transport, { slowMo: options.slowMo, logger });
184196
}
185197

186-
abstract _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer>;
198+
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer> {
199+
const {
200+
ignoreDefaultArgs = false,
201+
args = [],
202+
executablePath = null,
203+
env = process.env,
204+
handleSIGINT = true,
205+
handleSIGTERM = true,
206+
handleSIGHUP = true,
207+
port = 0,
208+
} = options;
209+
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
210+
211+
let temporaryUserDataDir: string | null = null;
212+
if (!userDataDir) {
213+
userDataDir = await mkdtempAsync(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
214+
temporaryUserDataDir = userDataDir;
215+
}
216+
217+
const browserArguments = [];
218+
if (!ignoreDefaultArgs)
219+
browserArguments.push(...this._defaultArgs(options, launchType, userDataDir));
220+
else if (Array.isArray(ignoreDefaultArgs))
221+
browserArguments.push(...this._defaultArgs(options, launchType, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
222+
else
223+
browserArguments.push(...args);
224+
225+
const executable = executablePath || this.executablePath();
226+
if (!executable)
227+
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
228+
229+
// Note: it is important to define these variables before launchProcess, so that we don't get
230+
// "Cannot access 'browserServer' before initialization" if something went wrong.
231+
let transport: ConnectionTransport | undefined = undefined;
232+
let browserServer: BrowserServer | undefined = undefined;
233+
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
234+
executablePath: executable,
235+
args: browserArguments,
236+
env: this._amendEnvironment(env, userDataDir, executable, browserArguments),
237+
handleSIGINT,
238+
handleSIGTERM,
239+
handleSIGHUP,
240+
logger,
241+
pipe: !this._webSocketRegexNotPipe,
242+
tempDir: temporaryUserDataDir || undefined,
243+
attemptToGracefullyClose: async () => {
244+
if ((options as any).__testHookGracefullyClose)
245+
await (options as any).__testHookGracefullyClose();
246+
// We try to gracefully close to prevent crash reporting and core dumps.
247+
// Note that it's fine to reuse the pipe transport, since
248+
// our connection ignores kBrowserCloseMessageId.
249+
this._attemptToGracefullyCloseBrowser(transport!);
250+
},
251+
onkill: (exitCode, signal) => {
252+
if (browserServer)
253+
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
254+
},
255+
});
256+
257+
try {
258+
if (this._webSocketRegexNotPipe) {
259+
const timeoutError = new TimeoutError(`Timed out while trying to connect to the browser!`);
260+
const match = await waitForLine(launchedProcess, launchedProcess.stdout, this._webSocketRegexNotPipe, helper.timeUntilDeadline(deadline), timeoutError);
261+
const innerEndpoint = match[1];
262+
transport = await WebSocketTransport.connect(innerEndpoint, logger, deadline);
263+
} else {
264+
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
265+
transport = new PipeTransport(stdio[3], stdio[4], logger);
266+
}
267+
} catch (e) {
268+
// If we can't establish a connection, kill the process and exit.
269+
helper.killProcess(launchedProcess);
270+
throw e;
271+
}
272+
browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, transport, downloadsPath,
273+
launchType === 'server' ? this._wrapTransportWithWebSocket(transport, logger, port) : null);
274+
return browserServer;
275+
}
276+
277+
abstract _defaultArgs(options: BrowserArgOptions, launchType: LaunchType, userDataDir: string): string[];
187278
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
188-
}
279+
abstract _wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper;
280+
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
281+
abstract _attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void;
282+
}

src/server/chromium.ts

Lines changed: 16 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,16 @@
1515
* limitations under the License.
1616
*/
1717

18-
import * as fs from 'fs';
19-
import * as os from 'os';
2018
import * as path from 'path';
21-
import * as util from 'util';
2219
import { helper, assert, isDebugMode } from '../helper';
2320
import { CRBrowser } from '../chromium/crBrowser';
2421
import * as ws from 'ws';
25-
import { launchProcess } from './processLauncher';
22+
import { Env } from './processLauncher';
2623
import { kBrowserCloseMessageId } from '../chromium/crConnection';
27-
import { PipeTransport } from './pipeTransport';
28-
import { BrowserArgOptions, LaunchServerOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
29-
import { BrowserServer, WebSocketWrapper } from './browserServer';
30-
import { Events } from '../events';
24+
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
25+
import { WebSocketWrapper } from './browserServer';
3126
import { ConnectionTransport, ProtocolRequest } from '../transport';
32-
import { InnerLogger, logError, RootLogger } from '../logger';
27+
import { InnerLogger, logError } from '../logger';
3328
import { BrowserDescriptor } from '../install/browserPaths';
3429
import { CRDevTools } from '../chromium/crDevTools';
3530
import { BrowserOptions } from '../browser';
@@ -38,7 +33,7 @@ export class Chromium extends BrowserTypeBase {
3833
private _devtools: CRDevTools | undefined;
3934

4035
constructor(packagePath: string, browser: BrowserDescriptor) {
41-
super(packagePath, browser);
36+
super(packagePath, browser, null /* use pipe not websocket */);
4237
if (isDebugMode())
4338
this._devtools = this._createDevTools();
4439
}
@@ -56,77 +51,22 @@ export class Chromium extends BrowserTypeBase {
5651
return CRBrowser.connect(transport, options, devtools);
5752
}
5853

59-
async _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer> {
60-
const {
61-
ignoreDefaultArgs = false,
62-
args = [],
63-
executablePath = null,
64-
env = process.env,
65-
handleSIGINT = true,
66-
handleSIGTERM = true,
67-
handleSIGHUP = true,
68-
port = 0,
69-
} = options;
70-
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
71-
72-
let temporaryUserDataDir: string | null = null;
73-
if (!userDataDir) {
74-
userDataDir = await mkdtempAsync(CHROMIUM_PROFILE_PATH);
75-
temporaryUserDataDir = userDataDir;
76-
}
77-
54+
_amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
7855
const runningAsRoot = process.geteuid && process.geteuid() === 0;
79-
assert(!runningAsRoot || args.includes('--no-sandbox'), 'Cannot launch Chromium as root without --no-sandbox. See https://crbug.com/638180.');
80-
81-
const chromeArguments = [];
82-
if (!ignoreDefaultArgs)
83-
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir));
84-
else if (Array.isArray(ignoreDefaultArgs))
85-
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
86-
else
87-
chromeArguments.push(...args);
88-
89-
const chromeExecutable = executablePath || this.executablePath();
90-
if (!chromeExecutable)
91-
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
56+
assert(!runningAsRoot || browserArguments.includes('--no-sandbox'), 'Cannot launch Chromium as root without --no-sandbox. See https://crbug.com/638180.');
57+
return env;
58+
}
9259

93-
// Note: it is important to define these variables before launchProcess, so that we don't get
94-
// "Cannot access 'browserServer' before initialization" if something went wrong.
95-
let transport: PipeTransport | undefined = undefined;
96-
let browserServer: BrowserServer | undefined = undefined;
97-
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
98-
executablePath: chromeExecutable,
99-
args: chromeArguments,
100-
env,
101-
handleSIGINT,
102-
handleSIGTERM,
103-
handleSIGHUP,
104-
logger,
105-
pipe: true,
106-
tempDir: temporaryUserDataDir || undefined,
107-
attemptToGracefullyClose: async () => {
108-
if ((options as any).__testHookGracefullyClose)
109-
await (options as any).__testHookGracefullyClose();
110-
// We try to gracefully close to prevent crash reporting and core dumps.
111-
// Note that it's fine to reuse the pipe transport, since
112-
// our connection ignores kBrowserCloseMessageId.
113-
const t = transport!;
114-
const message: ProtocolRequest = { method: 'Browser.close', id: kBrowserCloseMessageId, params: {} };
115-
t.send(message);
116-
},
117-
onkill: (exitCode, signal) => {
118-
if (browserServer)
119-
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
120-
},
121-
});
60+
_attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
61+
const message: ProtocolRequest = { method: 'Browser.close', id: kBrowserCloseMessageId, params: {} };
62+
transport.send(message);
63+
}
12264

123-
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
124-
transport = new PipeTransport(stdio[3], stdio[4], logger);
125-
browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, transport, downloadsPath, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port) : null);
126-
return browserServer;
65+
_wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper {
66+
return wrapTransportWithWebSocket(transport, logger, port);
12767
}
12868

129-
private _defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string): string[] {
69+
_defaultArgs(options: BrowserArgOptions, launchType: LaunchType, userDataDir: string): string[] {
13070
const { devtools, headless } = processBrowserArgOptions(options);
13171
const { args = [] } = options;
13272
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
@@ -301,10 +241,6 @@ function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: Inne
301241
}
302242

303243

304-
const mkdtempAsync = util.promisify(fs.mkdtemp);
305-
306-
const CHROMIUM_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-');
307-
308244
const DEFAULT_ARGS = [
309245
'--disable-background-networking',
310246
'--enable-features=NetworkService,NetworkServiceInProcess',

0 commit comments

Comments
 (0)