2020-05-11 18:00:33 -07:00
|
|
|
/**
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import * as path from 'path';
|
2020-09-02 17:30:10 -07:00
|
|
|
import * as os from 'os';
|
2020-08-23 13:46:40 -07:00
|
|
|
import { CRBrowser, CRBrowserContext } from '../chromium/crBrowser';
|
|
|
|
import { CRConnection, CRSession } from '../chromium/crConnection';
|
|
|
|
import { CRExecutionContext } from '../chromium/crExecutionContext';
|
2020-08-24 06:51:51 -07:00
|
|
|
import * as js from '../javascript';
|
|
|
|
import { Page } from '../page';
|
2020-08-23 13:46:40 -07:00
|
|
|
import { TimeoutSettings } from '../../utils/timeoutSettings';
|
2020-08-24 06:51:51 -07:00
|
|
|
import { WebSocketTransport } from '../transport';
|
|
|
|
import * as types from '../types';
|
2020-08-23 13:46:40 -07:00
|
|
|
import { launchProcess, waitForLine, envArrayToObject } from '../processLauncher';
|
2020-08-24 06:51:51 -07:00
|
|
|
import { BrowserContext } from '../browserContext';
|
2020-05-13 20:51:53 -07:00
|
|
|
import type {BrowserWindow} from 'electron';
|
2020-08-24 06:51:51 -07:00
|
|
|
import { runAbortableTask, ProgressController } from '../progress';
|
2020-06-10 15:12:50 -07:00
|
|
|
import { EventEmitter } from 'events';
|
2020-08-24 06:51:51 -07:00
|
|
|
import { helper } from '../helper';
|
|
|
|
import { BrowserProcess } from '../browser';
|
2020-05-11 18:00:33 -07:00
|
|
|
|
2020-07-20 17:38:06 -07:00
|
|
|
export type ElectronLaunchOptionsBase = {
|
2020-05-11 18:00:33 -07:00
|
|
|
args?: string[],
|
|
|
|
cwd?: string,
|
2020-08-18 09:37:40 -07:00
|
|
|
env?: types.EnvArray,
|
2020-05-11 18:00:33 -07:00
|
|
|
handleSIGINT?: boolean,
|
|
|
|
handleSIGTERM?: boolean,
|
|
|
|
handleSIGHUP?: boolean,
|
|
|
|
timeout?: number,
|
|
|
|
};
|
|
|
|
|
2020-07-15 18:48:19 -07:00
|
|
|
export interface ElectronPage extends Page {
|
2020-05-13 20:51:53 -07:00
|
|
|
browserWindow: js.JSHandle<BrowserWindow>;
|
|
|
|
_browserWindowId: number;
|
|
|
|
}
|
|
|
|
|
2020-06-10 15:12:50 -07:00
|
|
|
export class ElectronApplication extends EventEmitter {
|
2020-08-21 16:26:33 -07:00
|
|
|
static Events = {
|
|
|
|
Close: 'close',
|
|
|
|
Window: 'window',
|
|
|
|
};
|
|
|
|
|
2020-05-11 18:00:33 -07:00
|
|
|
private _browserContext: CRBrowserContext;
|
|
|
|
private _nodeConnection: CRConnection;
|
|
|
|
private _nodeSession: CRSession;
|
|
|
|
private _nodeExecutionContext: js.ExecutionContext | undefined;
|
2020-07-13 21:46:59 -07:00
|
|
|
_nodeElectronHandle: js.JSHandle<any> | undefined;
|
2020-05-13 20:51:53 -07:00
|
|
|
private _windows = new Set<ElectronPage>();
|
2020-05-11 18:00:33 -07:00
|
|
|
private _lastWindowId = 0;
|
|
|
|
readonly _timeoutSettings = new TimeoutSettings();
|
|
|
|
|
2020-08-17 14:12:31 -07:00
|
|
|
constructor(browser: CRBrowser, nodeConnection: CRConnection) {
|
2020-05-11 18:00:33 -07:00
|
|
|
super();
|
2020-05-20 16:30:04 -07:00
|
|
|
this._browserContext = browser._defaultContext as CRBrowserContext;
|
2020-08-21 16:26:33 -07:00
|
|
|
this._browserContext.on(BrowserContext.Events.Close, () => {
|
2020-07-23 11:02:43 -07:00
|
|
|
// Emit application closed after context closed.
|
2020-08-21 16:26:33 -07:00
|
|
|
Promise.resolve().then(() => this.emit(ElectronApplication.Events.Close));
|
2020-07-23 11:02:43 -07:00
|
|
|
});
|
2020-08-21 16:26:33 -07:00
|
|
|
this._browserContext.on(BrowserContext.Events.Page, event => this._onPage(event));
|
2020-05-11 18:00:33 -07:00
|
|
|
this._nodeConnection = nodeConnection;
|
|
|
|
this._nodeSession = nodeConnection.rootSession;
|
|
|
|
}
|
|
|
|
|
2020-05-13 20:51:53 -07:00
|
|
|
private async _onPage(page: ElectronPage) {
|
2020-05-11 18:00:33 -07:00
|
|
|
// Needs to be sync.
|
|
|
|
const windowId = ++this._lastWindowId;
|
|
|
|
// Can be async.
|
2020-05-12 15:28:37 -07:00
|
|
|
const handle = await this._nodeElectronHandle!.evaluateHandle(({ BrowserWindow }, windowId) => BrowserWindow.fromId(windowId), windowId).catch(e => {});
|
|
|
|
if (!handle)
|
|
|
|
return;
|
2020-05-13 20:51:53 -07:00
|
|
|
page.browserWindow = handle;
|
|
|
|
page._browserWindowId = windowId;
|
2020-08-21 16:26:33 -07:00
|
|
|
page.on(Page.Events.Close, () => {
|
2020-05-13 20:51:53 -07:00
|
|
|
page.browserWindow.dispose();
|
2020-05-11 18:00:33 -07:00
|
|
|
this._windows.delete(page);
|
|
|
|
});
|
|
|
|
this._windows.add(page);
|
2020-08-14 18:25:32 -07:00
|
|
|
await page.mainFrame().waitForLoadState('domcontentloaded').catch(e => {}); // can happen after detach
|
2020-08-21 16:26:33 -07:00
|
|
|
this.emit(ElectronApplication.Events.Window, page);
|
2020-05-11 18:00:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async newBrowserWindow(options: any): Promise<Page> {
|
2020-08-17 14:36:51 -07:00
|
|
|
const windowId = await this._nodeElectronHandle!.evaluate(async ({ BrowserWindow }, options) => {
|
2020-05-11 18:00:33 -07:00
|
|
|
const win = new BrowserWindow(options);
|
|
|
|
win.loadURL('about:blank');
|
|
|
|
return win.id;
|
|
|
|
}, options);
|
|
|
|
|
|
|
|
for (const page of this._windows) {
|
2020-05-13 20:51:53 -07:00
|
|
|
if (page._browserWindowId === windowId)
|
2020-05-11 18:00:33 -07:00
|
|
|
return page;
|
|
|
|
}
|
|
|
|
|
2020-08-21 16:26:33 -07:00
|
|
|
return await this._waitForEvent(ElectronApplication.Events.Window, (page: ElectronPage) => page._browserWindowId === windowId);
|
2020-05-11 18:00:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
context(): BrowserContext {
|
|
|
|
return this._browserContext;
|
|
|
|
}
|
|
|
|
|
|
|
|
async close() {
|
2020-08-21 16:26:33 -07:00
|
|
|
const closed = this._waitForEvent(ElectronApplication.Events.Close);
|
2020-08-17 14:36:51 -07:00
|
|
|
await this._nodeElectronHandle!.evaluate(({ app }) => app.quit());
|
2020-05-11 18:00:33 -07:00
|
|
|
this._nodeConnection.close();
|
2020-07-23 11:02:43 -07:00
|
|
|
await closed;
|
2020-05-11 18:00:33 -07:00
|
|
|
}
|
|
|
|
|
2020-08-17 16:19:21 -07:00
|
|
|
private async _waitForEvent(event: string, predicate?: Function): Promise<any> {
|
2020-08-17 14:36:51 -07:00
|
|
|
const progressController = new ProgressController(this._timeoutSettings.timeout({}));
|
2020-08-21 16:26:33 -07:00
|
|
|
if (event !== ElectronApplication.Events.Close)
|
2020-06-10 15:12:50 -07:00
|
|
|
this._browserContext._closePromise.then(error => progressController.abort(error));
|
2020-08-17 14:36:51 -07:00
|
|
|
return progressController.run(progress => helper.waitForEvent(progress, this, event, predicate).promise);
|
2020-06-10 15:12:50 -07:00
|
|
|
}
|
|
|
|
|
2020-05-11 18:00:33 -07:00
|
|
|
async _init() {
|
|
|
|
this._nodeSession.once('Runtime.executionContextCreated', event => {
|
2020-06-09 16:11:17 -07:00
|
|
|
this._nodeExecutionContext = new js.ExecutionContext(new CRExecutionContext(this._nodeSession, event.context));
|
2020-05-11 18:00:33 -07:00
|
|
|
});
|
|
|
|
await this._nodeSession.send('Runtime.enable', {}).catch(e => {});
|
2020-06-03 17:50:16 -07:00
|
|
|
this._nodeElectronHandle = await js.evaluate(this._nodeExecutionContext!, false /* returnByValue */, () => {
|
2020-05-11 18:00:33 -07:00
|
|
|
// Resolving the race between the debugger and the boot-time script.
|
|
|
|
if ((global as any)._playwrightRun)
|
|
|
|
return (global as any)._playwrightRun();
|
|
|
|
return new Promise(f => (global as any)._playwrightRunCallback = f);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Electron {
|
2020-08-17 14:12:31 -07:00
|
|
|
async launch(executablePath: string, options: ElectronLaunchOptionsBase = {}): Promise<ElectronApplication> {
|
2020-05-11 18:00:33 -07:00
|
|
|
const {
|
|
|
|
args = [],
|
|
|
|
handleSIGINT = true,
|
|
|
|
handleSIGTERM = true,
|
|
|
|
handleSIGHUP = true,
|
|
|
|
} = options;
|
2020-06-04 16:43:48 -07:00
|
|
|
return runAbortableTask(async progress => {
|
2020-05-29 14:39:34 -07:00
|
|
|
let app: ElectronApplication | undefined = undefined;
|
|
|
|
const electronArguments = ['--inspect=0', '--remote-debugging-port=0', '--require', path.join(__dirname, 'electronLoader.js'), ...args];
|
2020-09-02 17:30:10 -07:00
|
|
|
|
|
|
|
if (os.platform() === 'linux') {
|
|
|
|
const runningAsRoot = process.geteuid && process.geteuid() === 0;
|
2020-09-02 18:02:11 -07:00
|
|
|
if (runningAsRoot && electronArguments.indexOf('--no-sandbox') === -1)
|
2020-09-02 17:30:10 -07:00
|
|
|
electronArguments.push('--no-sandbox');
|
|
|
|
}
|
|
|
|
|
2020-05-29 14:39:34 -07:00
|
|
|
const { launchedProcess, gracefullyClose, kill } = await launchProcess({
|
|
|
|
executablePath,
|
|
|
|
args: electronArguments,
|
2020-08-18 09:37:40 -07:00
|
|
|
env: options.env ? envArrayToObject(options.env) : process.env,
|
2020-05-29 14:39:34 -07:00
|
|
|
handleSIGINT,
|
|
|
|
handleSIGTERM,
|
|
|
|
handleSIGHUP,
|
|
|
|
progress,
|
|
|
|
pipe: true,
|
|
|
|
cwd: options.cwd,
|
|
|
|
tempDirectories: [],
|
|
|
|
attemptToGracefullyClose: () => app!.close(),
|
2020-07-23 11:02:43 -07:00
|
|
|
onExit: (exitCode, signal) => {},
|
2020-05-29 14:39:34 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
const nodeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^Debugger listening on (ws:\/\/.*)$/);
|
|
|
|
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
|
2020-08-17 14:12:31 -07:00
|
|
|
const nodeConnection = new CRConnection(nodeTransport);
|
2020-05-29 14:39:34 -07:00
|
|
|
|
|
|
|
const chromeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/);
|
|
|
|
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
|
2020-08-14 13:19:12 -07:00
|
|
|
const browserProcess: BrowserProcess = {
|
|
|
|
onclose: undefined,
|
|
|
|
process: launchedProcess,
|
|
|
|
close: gracefullyClose,
|
|
|
|
kill
|
|
|
|
};
|
2020-09-08 15:10:36 -07:00
|
|
|
const browser = await CRBrowser.connect(chromeTransport, { name: 'electron', headful: true, persistent: { noDefaultViewport: true }, browserProcess });
|
2020-08-17 14:12:31 -07:00
|
|
|
app = new ElectronApplication(browser, nodeConnection);
|
2020-05-29 14:39:34 -07:00
|
|
|
await app._init();
|
|
|
|
return app;
|
2020-08-17 14:12:31 -07:00
|
|
|
}, TimeoutSettings.timeout(options), 'browser');
|
2020-05-11 18:00:33 -07:00
|
|
|
}
|
|
|
|
}
|