feat(debug): persist devtools preferences in Chromium (#2266)

We store devtools-preferences.json file in the downloaded browser directory.
This commit is contained in:
Dmitry Gozman 2020-05-19 14:55:11 -07:00 committed by GitHub
parent fbccd328cc
commit 481643409e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 146 additions and 17 deletions

View file

@ -30,6 +30,7 @@ import { Events } from './events';
import { Protocol } from './protocol';
import { CRExecutionContext } from './crExecutionContext';
import { logError } from '../logger';
import { CRDevTools } from './crDevTools';
export class CRBrowser extends BrowserBase {
readonly _connection: CRConnection;
@ -40,14 +41,16 @@ export class CRBrowser extends BrowserBase {
_crPages = new Map<string, CRPage>();
_backgroundPages = new Map<string, CRPage>();
_serviceWorkers = new Map<string, CRServiceWorker>();
_devtools?: CRDevTools;
private _tracingRecording = false;
private _tracingPath: string | null = '';
private _tracingClient: CRSession | undefined;
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<CRBrowser> {
static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> {
const connection = new CRConnection(SlowMoTransport.wrap(transport, options.slowMo), options.logger);
const browser = new CRBrowser(connection, options);
browser._devtools = devtools;
const session = connection.rootSession;
if (!options.persistent) {
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
@ -123,6 +126,11 @@ export class CRBrowser extends BrowserBase {
context = this._defaultContext;
}
if (targetInfo.type === 'other' && targetInfo.url.startsWith('devtools://devtools') && this._devtools) {
this._devtools.install(session);
return;
}
if (targetInfo.type === 'other' || !context) {
if (waitingForDebugger) {
// Ideally, detaching should resume any target, but there is a bug in the backend.

106
src/chromium/crDevTools.ts Normal file
View file

@ -0,0 +1,106 @@
/**
* 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 fs from 'fs';
import * as util from 'util';
import { CRSession } from './crConnection';
const kBindingName = '__pw_devtools__';
// This method intercepts preferences-related DevTools embedder methods
// and stores preferences as a json file in the browser installation directory.
export class CRDevTools {
private _preferencesPath: string;
private _prefs: any;
private _savePromise: Promise<any>;
__testHookOnBinding?: (parsed: any) => any;
constructor(preferencesPath: string) {
this._preferencesPath = preferencesPath;
this._savePromise = Promise.resolve();
}
async install(session: CRSession) {
session.on('Runtime.bindingCalled', async event => {
if (event.name !== kBindingName)
return;
const parsed = JSON.parse(event.payload);
let result = undefined;
if (this.__testHookOnBinding)
this.__testHookOnBinding(parsed);
if (parsed.method === 'getPreferences') {
if (this._prefs === undefined) {
try {
const json = await util.promisify(fs.readFile)(this._preferencesPath, 'utf8');
this._prefs = JSON.parse(json);
} catch (e) {
this._prefs = {};
}
}
result = this._prefs;
} else if (parsed.method === 'setPreference') {
this._prefs[parsed.params[0]] = parsed.params[1];
this._save();
} else if (parsed.method === 'removePreference') {
delete this._prefs[parsed.params[0]];
this._save();
} else if (parsed.method === 'clearPreferences') {
this._prefs = {};
this._save();
}
session.send('Runtime.evaluate', {
expression: `window.DevToolsAPI.embedderMessageAck(${parsed.id}, ${JSON.stringify(result)})`,
contextId: event.executionContextId
}).catch(e => null);
});
await Promise.all([
session.send('Runtime.enable'),
session.send('Runtime.addBinding', { name: kBindingName }),
session.send('Page.enable'),
session.send('Page.addScriptToEvaluateOnNewDocument', { source: `
(() => {
const init = () => {
// Lazy init happens when InspectorFrontendHost is initialized.
// At this point DevToolsHost is ready to be used.
const host = window.DevToolsHost;
const old = host.sendMessageToEmbedder.bind(host);
host.sendMessageToEmbedder = message => {
if (['getPreferences', 'setPreference', 'removePreference', 'clearPreferences'].includes(JSON.parse(message).method))
window.${kBindingName}(message);
else
old(message);
};
};
let value;
Object.defineProperty(window, 'InspectorFrontendHost', {
configurable: true,
enumerable: true,
get() { return value; },
set(v) { value = v; init(); },
});
})()
` }),
session.send('Runtime.runIfWaitingForDebugger'),
]).catch(e => null);
}
_save() {
// Serialize saves to avoid corruption.
this._savePromise = this._savePromise.then(async () => {
await util.promisify(fs.writeFile)(this._preferencesPath, JSON.stringify(this._prefs)).catch(e => null);
});
}
}

View file

@ -60,12 +60,13 @@ export interface BrowserType<Browser> {
export abstract class AbstractBrowserType<Browser> implements BrowserType<Browser> {
private _name: string;
private _executablePath: string | undefined;
readonly _browserPath: string;
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor) {
this._name = browser.name;
const browsersPath = browserPaths.browsersPath(packagePath);
const browserPath = browserPaths.browserDirectory(browsersPath, browser);
this._executablePath = browserPaths.executablePath(browserPath, browser);
this._browserPath = browserPaths.browserDirectory(browsersPath, browser);
this._executablePath = browserPaths.executablePath(this._browserPath, browser);
}
executablePath(): string {

View file

@ -19,7 +19,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as util from 'util';
import { helper, assert } from '../helper';
import { helper, assert, isDebugMode } from '../helper';
import { CRBrowser } from '../chromium/crBrowser';
import * as ws from 'ws';
import { launchProcess } from './processLauncher';
@ -34,10 +34,19 @@ import { BrowserContext } from '../browserContext';
import { InnerLogger, logError, RootLogger } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths';
import { TimeoutSettings } from '../timeoutSettings';
import { CRDevTools } from '../chromium/crDevTools';
export class Chromium extends AbstractBrowserType<CRBrowser> {
private _devtools: CRDevTools | undefined;
constructor(packagePath: string, browser: BrowserDescriptor) {
super(packagePath, browser);
if (isDebugMode())
this._devtools = this._createDevTools();
}
private _createDevTools() {
return new CRDevTools(path.join(this._browserPath, 'devtools-preferences.json'));
}
async launch(options: LaunchOptions = {}): Promise<CRBrowser> {
@ -48,13 +57,18 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
let devtools = this._devtools;
if ((options as any).__testHookForDevTools) {
devtools = this._createDevTools();
await (options as any).__testHookForDevTools(devtools);
}
return await CRBrowser.connect(transport!, {
slowMo: options.slowMo,
headful: !processBrowserArgOptions(options).headless,
logger,
downloadsPath,
ownedServer: browserServer
});
ownedServer: browserServer,
}, devtools);
});
}
@ -76,7 +90,7 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
downloadsPath,
headful: !processBrowserArgOptions(options).headless,
ownedServer: browserServer
});
}, this._devtools);
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await context._loadDefaultContext();

View file

@ -33,16 +33,16 @@ describe('launcher', function() {
await browser.close();
});
it('should open devtools when "devtools: true" option is given', async({browserType, defaultBrowserOptions}) => {
const browser = await browserType.launch(Object.assign({devtools: true}, {...defaultBrowserOptions, headless: false}));
let devtoolsCallback;
const devtoolsPromise = new Promise(f => devtoolsCallback = f);
const __testHookForDevTools = devtools => devtools.__testHookOnBinding = parsed => {
if (parsed.method === 'getPreferences')
devtoolsCallback();
};
const browser = await browserType.launch({...defaultBrowserOptions, headless: false, devtools: true, __testHookForDevTools});
const context = await browser.newContext();
const browserSession = await browser.newBrowserCDPSession();
await browserSession.send('Target.setDiscoverTargets', { discover: true });
const devtoolsPagePromise = new Promise(fulfill => browserSession.on('Target.targetCreated', async ({targetInfo}) => {
if (targetInfo.type === 'other' && targetInfo.url.includes('devtools://'))
fulfill();
}));
await Promise.all([
devtoolsPagePromise,
devtoolsPromise,
context.newPage()
]);
await browser.close();
@ -74,8 +74,8 @@ describe('extensions', () => {
});
describe('BrowserContext', function() {
it('should not create pages automatically', async ({browserType}) => {
const browser = await browserType.launch();
it('should not create pages automatically', async ({browserType, defaultBrowserOptions}) => {
const browser = await browserType.launch(defaultBrowserOptions);
const browserSession = await browser.newBrowserCDPSession();
const targets = [];
browserSession.on('Target.targetCreated', async ({targetInfo}) => {