feat(debug): persist devtools preferences in Chromium (#2266)
We store devtools-preferences.json file in the downloaded browser directory.
This commit is contained in:
parent
fbccd328cc
commit
481643409e
|
|
@ -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
106
src/chromium/crDevTools.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue