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 { Protocol } from './protocol';
|
||||||
import { CRExecutionContext } from './crExecutionContext';
|
import { CRExecutionContext } from './crExecutionContext';
|
||||||
import { logError } from '../logger';
|
import { logError } from '../logger';
|
||||||
|
import { CRDevTools } from './crDevTools';
|
||||||
|
|
||||||
export class CRBrowser extends BrowserBase {
|
export class CRBrowser extends BrowserBase {
|
||||||
readonly _connection: CRConnection;
|
readonly _connection: CRConnection;
|
||||||
|
|
@ -40,14 +41,16 @@ export class CRBrowser extends BrowserBase {
|
||||||
_crPages = new Map<string, CRPage>();
|
_crPages = new Map<string, CRPage>();
|
||||||
_backgroundPages = new Map<string, CRPage>();
|
_backgroundPages = new Map<string, CRPage>();
|
||||||
_serviceWorkers = new Map<string, CRServiceWorker>();
|
_serviceWorkers = new Map<string, CRServiceWorker>();
|
||||||
|
_devtools?: CRDevTools;
|
||||||
|
|
||||||
private _tracingRecording = false;
|
private _tracingRecording = false;
|
||||||
private _tracingPath: string | null = '';
|
private _tracingPath: string | null = '';
|
||||||
private _tracingClient: CRSession | undefined;
|
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 connection = new CRConnection(SlowMoTransport.wrap(transport, options.slowMo), options.logger);
|
||||||
const browser = new CRBrowser(connection, options);
|
const browser = new CRBrowser(connection, options);
|
||||||
|
browser._devtools = devtools;
|
||||||
const session = connection.rootSession;
|
const session = connection.rootSession;
|
||||||
if (!options.persistent) {
|
if (!options.persistent) {
|
||||||
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
|
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
|
||||||
|
|
@ -123,6 +126,11 @@ export class CRBrowser extends BrowserBase {
|
||||||
context = this._defaultContext;
|
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 (targetInfo.type === 'other' || !context) {
|
||||||
if (waitingForDebugger) {
|
if (waitingForDebugger) {
|
||||||
// Ideally, detaching should resume any target, but there is a bug in the backend.
|
// 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> {
|
export abstract class AbstractBrowserType<Browser> implements BrowserType<Browser> {
|
||||||
private _name: string;
|
private _name: string;
|
||||||
private _executablePath: string | undefined;
|
private _executablePath: string | undefined;
|
||||||
|
readonly _browserPath: string;
|
||||||
|
|
||||||
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor) {
|
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor) {
|
||||||
this._name = browser.name;
|
this._name = browser.name;
|
||||||
const browsersPath = browserPaths.browsersPath(packagePath);
|
const browsersPath = browserPaths.browsersPath(packagePath);
|
||||||
const browserPath = browserPaths.browserDirectory(browsersPath, browser);
|
this._browserPath = browserPaths.browserDirectory(browsersPath, browser);
|
||||||
this._executablePath = browserPaths.executablePath(browserPath, browser);
|
this._executablePath = browserPaths.executablePath(this._browserPath, browser);
|
||||||
}
|
}
|
||||||
|
|
||||||
executablePath(): string {
|
executablePath(): string {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ 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 util from 'util';
|
||||||
import { helper, assert } 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 { launchProcess } from './processLauncher';
|
||||||
|
|
@ -34,10 +34,19 @@ import { BrowserContext } from '../browserContext';
|
||||||
import { InnerLogger, logError, RootLogger } from '../logger';
|
import { InnerLogger, logError, RootLogger } from '../logger';
|
||||||
import { BrowserDescriptor } from '../install/browserPaths';
|
import { BrowserDescriptor } from '../install/browserPaths';
|
||||||
import { TimeoutSettings } from '../timeoutSettings';
|
import { TimeoutSettings } from '../timeoutSettings';
|
||||||
|
import { CRDevTools } from '../chromium/crDevTools';
|
||||||
|
|
||||||
export class Chromium extends AbstractBrowserType<CRBrowser> {
|
export class Chromium extends AbstractBrowserType<CRBrowser> {
|
||||||
|
private _devtools: CRDevTools | undefined;
|
||||||
|
|
||||||
constructor(packagePath: string, browser: BrowserDescriptor) {
|
constructor(packagePath: string, browser: BrowserDescriptor) {
|
||||||
super(packagePath, browser);
|
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> {
|
async launch(options: LaunchOptions = {}): Promise<CRBrowser> {
|
||||||
|
|
@ -48,13 +57,18 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
|
||||||
return await browserServer._initializeOrClose(deadline, async () => {
|
return await browserServer._initializeOrClose(deadline, async () => {
|
||||||
if ((options as any).__testHookBeforeCreateBrowser)
|
if ((options as any).__testHookBeforeCreateBrowser)
|
||||||
await (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!, {
|
return await CRBrowser.connect(transport!, {
|
||||||
slowMo: options.slowMo,
|
slowMo: options.slowMo,
|
||||||
headful: !processBrowserArgOptions(options).headless,
|
headful: !processBrowserArgOptions(options).headless,
|
||||||
logger,
|
logger,
|
||||||
downloadsPath,
|
downloadsPath,
|
||||||
ownedServer: browserServer
|
ownedServer: browserServer,
|
||||||
});
|
}, devtools);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +90,7 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
|
||||||
downloadsPath,
|
downloadsPath,
|
||||||
headful: !processBrowserArgOptions(options).headless,
|
headful: !processBrowserArgOptions(options).headless,
|
||||||
ownedServer: browserServer
|
ownedServer: browserServer
|
||||||
});
|
}, this._devtools);
|
||||||
const context = browser._defaultContext!;
|
const context = browser._defaultContext!;
|
||||||
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
|
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
|
||||||
await context._loadDefaultContext();
|
await context._loadDefaultContext();
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,16 @@ describe('launcher', function() {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
});
|
});
|
||||||
it('should open devtools when "devtools: true" option is given', async({browserType, defaultBrowserOptions}) => {
|
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 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([
|
await Promise.all([
|
||||||
devtoolsPagePromise,
|
devtoolsPromise,
|
||||||
context.newPage()
|
context.newPage()
|
||||||
]);
|
]);
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|
@ -74,8 +74,8 @@ describe('extensions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('BrowserContext', function() {
|
describe('BrowserContext', function() {
|
||||||
it('should not create pages automatically', async ({browserType}) => {
|
it('should not create pages automatically', async ({browserType, defaultBrowserOptions}) => {
|
||||||
const browser = await browserType.launch();
|
const browser = await browserType.launch(defaultBrowserOptions);
|
||||||
const browserSession = await browser.newBrowserCDPSession();
|
const browserSession = await browser.newBrowserCDPSession();
|
||||||
const targets = [];
|
const targets = [];
|
||||||
browserSession.on('Target.targetCreated', async ({targetInfo}) => {
|
browserSession.on('Target.targetCreated', async ({targetInfo}) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue