From 909b27d6c6c005aea1935557cf99d9b924537298 Mon Sep 17 00:00:00 2001 From: lvkunjie Date: Thu, 23 Dec 2021 20:19:24 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=96=AD=E7=82=B9=E8=B0=83=E8=AF=95=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/devtools.js | 25 + chobitsu | 1 + frontend/bindings/BreakpointManager.js | 873 +++++++++++++ frontend/devtools_app.json | 3 + frontend/sdk/DebuggerModel.js | 1570 +++++++++++++++++++++++ frontend/sdk/NetworkManager.js | 1584 ++++++++++++++++++++++++ frontend/sdk/NetworkRequest.js | 26 + frontend/sdk/ResourceTreeModel.js | 1111 +++++++++++++++++ frontend/sdk/Script.js | 403 ++++++ server/Server.js | 2 +- server/WebSocketServer.js | 4 +- server/middlewares/debugger.js | 201 +++ server/middlewares/parseSourceMap.js | 84 ++ server/utils/config.json | 1 + server/utils/modifyDebuggerInfo.js | 23 + server/websocket/Channel.js | 77 +- src/backend.js | 4 + src/debugger.js | 369 ++++++ src/home.js | 6 +- src/utils/createFrontendUrl.js | 4 +- 20 files changed, 6363 insertions(+), 8 deletions(-) create mode 160000 chobitsu create mode 100644 frontend/bindings/BreakpointManager.js create mode 100644 frontend/sdk/DebuggerModel.js create mode 100644 frontend/sdk/NetworkManager.js create mode 100644 frontend/sdk/ResourceTreeModel.js create mode 100644 frontend/sdk/Script.js create mode 100644 server/middlewares/debugger.js create mode 100644 server/middlewares/parseSourceMap.js create mode 100644 server/utils/config.json create mode 100644 server/utils/modifyDebuggerInfo.js create mode 100644 src/debugger.js diff --git a/bin/devtools.js b/bin/devtools.js index f514306..5b7aa14 100755 --- a/bin/devtools.js +++ b/bin/devtools.js @@ -133,9 +133,11 @@ require('yargs') throw err; } port = p; + startStaticServer(); startServer(); }); } else { + startStaticServer(); startServer(); } function startServer() { @@ -189,6 +191,29 @@ require('yargs') argv.open && require('opener')(home); }); } + function startStaticServer() { + // 单启动静态资源服务器 + let staticPort = 8890; + const options = { + ...config.options, + https: https ? {} : null, + plugins, + port, + hostname + }; + let staticOptions = Object.assign({}, options, {port: staticPort}) + const staticServer = new Server(staticOptions); + + staticServer.listen(staticPort, hostname, err => { + if (err) { + console.log('----静态资源服务器启动err', err); + throw err; + } + console.log('----静态资源服务器启动', staticOptions, staticPort); + }); + return staticServer; + + } } ) .help('h') diff --git a/chobitsu b/chobitsu new file mode 160000 index 0000000..500824f --- /dev/null +++ b/chobitsu @@ -0,0 +1 @@ +Subproject commit 500824f2199abc85f496c8b5c86aa416fc0877e1 diff --git a/frontend/bindings/BreakpointManager.js b/frontend/bindings/BreakpointManager.js new file mode 100644 index 0000000..91fd2dc --- /dev/null +++ b/frontend/bindings/BreakpointManager.js @@ -0,0 +1,873 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js'; // eslint-disable-line no-unused-vars +import {LiveLocation, LiveLocationPool} from './LiveLocation.js'; // eslint-disable-line no-unused-vars + +/** + * @unrestricted + */ +export class BreakpointManager extends Common.Object { + /** + * @param {!Workspace.Workspace} workspace + * @param {!SDK.TargetManager} targetManager + * @param {!DebuggerWorkspaceBinding} debuggerWorkspaceBinding + */ + constructor(workspace, targetManager, debuggerWorkspaceBinding) { + super(); + this._storage = new Storage(); + this._workspace = workspace; + this._targetManager = targetManager; + this._debuggerWorkspaceBinding = debuggerWorkspaceBinding; + + /** @type {!Map>} */ + this._breakpointsForUISourceCode = new Map(); + /** @type {!Map} */ + this._breakpointByStorageId = new Map(); + + this._workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this); + } + + /** + * @param {string} url + * @param {number} lineNumber + * @param {number} columnNumber + * @return {string} + */ + static _breakpointStorageId(url, lineNumber, columnNumber) { + if (!url) { + return ''; + } + return url + ':' + lineNumber + ':' + columnNumber; + } + + /** + * @param {string} fromURL + * @param {!Workspace.UISourceCode} toSourceCode + */ + copyBreakpoints(fromURL, toSourceCode) { + const breakpointItems = this._storage.breakpointItems(fromURL); + for (const item of breakpointItems) { + this.setBreakpoint(toSourceCode, item.lineNumber, item.columnNumber, item.condition, item.enabled); + } + } + + /** + * @param {!Workspace.UISourceCode} uiSourceCode + */ + _restoreBreakpoints(uiSourceCode) { + const url = uiSourceCode.url(); + if (!url) { + return; + } + + this._storage.mute(); + const breakpointItems = this._storage.breakpointItems(url); + for (const item of breakpointItems) { + this._innerSetBreakpoint(uiSourceCode, item.lineNumber, item.columnNumber, item.condition, item.enabled); + } + this._storage.unmute(); + } + + /** + * @param {!Common.Event} event + */ + _uiSourceCodeAdded(event) { + const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); + this._restoreBreakpoints(uiSourceCode); + } + + /** + * @param {!Workspace.UISourceCode} uiSourceCode + * @param {number} lineNumber + * @param {number} columnNumber + * @param {string} condition + * @param {boolean} enabled + * @return {!Breakpoint} + */ + setBreakpoint(uiSourceCode, lineNumber, columnNumber, condition, enabled) { + let uiLocation = new Workspace.UILocation(uiSourceCode, lineNumber, columnNumber); + const normalizedLocation = this._debuggerWorkspaceBinding.normalizeUILocation(uiLocation); + if (normalizedLocation.id() !== uiLocation.id()) { + Common.Revealer.reveal(normalizedLocation); + uiLocation = normalizedLocation; + } + return this._innerSetBreakpoint( + uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber, condition, enabled); + } + + /** + * @param {!Workspace.UISourceCode} uiSourceCode + * @param {number} lineNumber + * @param {number} columnNumber + * @param {string} condition + * @param {boolean} enabled + * @return {!Breakpoint} + */ + _innerSetBreakpoint(uiSourceCode, lineNumber, columnNumber, condition, enabled) { + const itemId = BreakpointManager._breakpointStorageId(uiSourceCode.url(), lineNumber, columnNumber); + let breakpoint = this._breakpointByStorageId.get(itemId); + if (breakpoint) { + breakpoint._updateState(condition, enabled); + breakpoint.setPrimaryUISourceCode(uiSourceCode); + breakpoint._updateBreakpoint(); + return breakpoint; + } + breakpoint = new Breakpoint(this, uiSourceCode, uiSourceCode.url(), lineNumber, columnNumber, condition, enabled); + this._breakpointByStorageId.set(itemId, breakpoint); + return breakpoint; + } + + /** + * @param {!Workspace.UILocation} uiLocation + * @return {?Bindings.BreakpointManager.BreakpointLocation} + */ + findBreakpoint(uiLocation) { + const breakpoints = this._breakpointsForUISourceCode.get(uiLocation.uiSourceCode); + return breakpoints ? (breakpoints.get(uiLocation.id())) || null : null; + } + + /** + * @param {!Workspace.UISourceCode} uiSourceCode + * @param {!TextUtils.TextRange} textRange + * @return {!Promise>} + */ + possibleBreakpoints(uiSourceCode, textRange) { + const startLocations = Bindings.debuggerWorkspaceBinding.uiLocationToRawLocations( + uiSourceCode, textRange.startLine, textRange.startColumn); + const endLocations = Bindings.debuggerWorkspaceBinding.uiLocationToRawLocations( + uiSourceCode, textRange.endLine, textRange.endColumn); + const endLocationByModel = new Map(); + for (const location of endLocations) { + endLocationByModel.set(location.debuggerModel, location); + } + let startLocation = null; + let endLocation = null; + for (const location of startLocations) { + const endLocationCandidate = endLocationByModel.get(location.debuggerModel); + if (endLocationCandidate) { + startLocation = location; + endLocation = endLocationCandidate; + break; + } + } + if (!startLocation || !endLocation) { + return Promise.resolve([]); + } + + return startLocation.debuggerModel + .getPossibleBreakpoints(startLocation, endLocation, /* restrictToFunction */ false) + .then(toUILocations.bind(this)); + + /** + * @this {!BreakpointManager} + * @param {!Array} locations + * @return {!Array} + */ + function toUILocations(locations) { + let sortedLocations = locations.map(location => this._debuggerWorkspaceBinding.rawLocationToUILocation(location)); + sortedLocations = sortedLocations.filter(location => location && location.uiSourceCode === uiSourceCode); + sortedLocations.sort(Workspace.UILocation.comparator); + if (!sortedLocations.length) { + return []; + } + const result = [sortedLocations[0]]; + let lastLocation = sortedLocations[0]; + for (let i = 1; i < sortedLocations.length; ++i) { + if (sortedLocations[i].id() === lastLocation.id()) { + continue; + } + result.push(sortedLocations[i]); + lastLocation = sortedLocations[i]; + } + return result; + } + } + + /** + * @param {!Workspace.UISourceCode} uiSourceCode + * @return {!Array} + */ + breakpointLocationsForUISourceCode(uiSourceCode) { + const breakpoints = this._breakpointsForUISourceCode.get(uiSourceCode); + return breakpoints ? Array.from(breakpoints.values()) : []; + } + + /** + * @return {!Array} + */ + allBreakpointLocations() { + let result = []; + for (const breakpoints of this._breakpointsForUISourceCode.values()) { + result = result.concat(Array.from(breakpoints.values())); + } + return result; + } + + /** + * @param {!Breakpoint} breakpoint + * @param {boolean} removeFromStorage + */ + _removeBreakpoint(breakpoint, removeFromStorage) { + if (removeFromStorage) { + this._storage._removeBreakpoint(breakpoint); + } + this._breakpointByStorageId.delete(breakpoint._breakpointStorageId()); + } + + /** + * @param {!Breakpoint} breakpoint + * @param {!Workspace.UILocation} uiLocation + */ + _uiLocationAdded(breakpoint, uiLocation) { + let breakpoints = this._breakpointsForUISourceCode.get(uiLocation.uiSourceCode); + if (!breakpoints) { + breakpoints = new Map(); + this._breakpointsForUISourceCode.set(uiLocation.uiSourceCode, breakpoints); + } + const breakpointLocation = {breakpoint: breakpoint, uiLocation: uiLocation}; + breakpoints.set(uiLocation.id(), breakpointLocation); + this.dispatchEventToListeners(Events.BreakpointAdded, breakpointLocation); + } + + /** + * @param {!Breakpoint} breakpoint + * @param {!Workspace.UILocation} uiLocation + */ + _uiLocationRemoved(breakpoint, uiLocation) { + const breakpoints = this._breakpointsForUISourceCode.get(uiLocation.uiSourceCode); + if (!breakpoints) { + return; + } + const breakpointLocation = breakpoints.get(uiLocation.id()) || null; + if (!breakpointLocation) { + return; + } + breakpoints.delete(uiLocation.id()); + if (breakpoints.size === 0) { + this._breakpointsForUISourceCode.delete(uiLocation.uiSourceCode); + } + this.dispatchEventToListeners(Events.BreakpointRemoved, {breakpoint: breakpoint, uiLocation: uiLocation}); + } +} + +/** @enum {symbol} */ +export const Events = { + BreakpointAdded: Symbol('breakpoint-added'), + BreakpointRemoved: Symbol('breakpoint-removed') +}; + +/** + * @unrestricted + * @implements {SDK.SDKModelObserver} + */ +export class Breakpoint { + /** + * @param {!BreakpointManager} breakpointManager + * @param {!Workspace.UISourceCode} primaryUISourceCode + * @param {string} url + * @param {number} lineNumber + * @param {number} columnNumber + * @param {string} condition + * @param {boolean} enabled + */ + constructor(breakpointManager, primaryUISourceCode, url, lineNumber, columnNumber, condition, enabled) { + this._breakpointManager = breakpointManager; + this._url = url; + this._lineNumber = lineNumber; + this._columnNumber = columnNumber; + + /** @type {?Workspace.UILocation} */ + this._defaultUILocation = null; + /** @type {!Set} */ + this._uiLocations = new Set(); + + /** @type {string} */ this._condition; + /** @type {boolean} */ this._enabled; + /** @type {boolean} */ this._isRemoved; + + this._currentState = null; + /** @type {!Map.}*/ + this._modelBreakpoints = new Map(); + this._updateState(condition, enabled); + this.setPrimaryUISourceCode(primaryUISourceCode); + this._breakpointManager._targetManager.observeModels(SDK.DebuggerModel, this); + } + + async refreshInDebugger() { + if (this._isRemoved) { + return; + } + const breakpoints = Array.from(this._modelBreakpoints.values()); + return Promise.all(breakpoints.map(breakpoint => breakpoint._refreshBreakpoint())); + } + + /** + * @override + * @param {!SDK.DebuggerModel} debuggerModel + */ + modelAdded(debuggerModel) { + const debuggerWorkspaceBinding = this._breakpointManager._debuggerWorkspaceBinding; + this._modelBreakpoints.set(debuggerModel, new ModelBreakpoint(debuggerModel, this, debuggerWorkspaceBinding)); + } + + /** + * @override + * @param {!SDK.DebuggerModel} debuggerModel + */ + modelRemoved(debuggerModel) { + const modelBreakpoint = this._modelBreakpoints.remove(debuggerModel); + modelBreakpoint._cleanUpAfterDebuggerIsGone(); + modelBreakpoint._removeEventListeners(); + } + + /** + * @param {?Workspace.UISourceCode} primaryUISourceCode + */ + setPrimaryUISourceCode(primaryUISourceCode) { + if (this._uiLocations.size === 0 && this._defaultUILocation) { + this._breakpointManager._uiLocationRemoved(this, this._defaultUILocation); + } + if (primaryUISourceCode) { + this._defaultUILocation = primaryUISourceCode.uiLocation(this._lineNumber, this._columnNumber); + } else { + this._defaultUILocation = null; + } + if (this._uiLocations.size === 0 && this._defaultUILocation && !this._isRemoved) { + this._breakpointManager._uiLocationAdded(this, this._defaultUILocation); + } + } + + /** + * @return {string} + */ + url() { + return this._url; + } + + /** + * @return {number} + */ + lineNumber() { + return this._lineNumber; + } + + /** + * @return {number} + */ + columnNumber() { + return this._columnNumber; + } + + /** + * @param {!Workspace.UILocation} uiLocation + */ + _uiLocationAdded(uiLocation) { + if (this._isRemoved) { + return; + } + if (this._uiLocations.size === 0 && this._defaultUILocation) { + this._breakpointManager._uiLocationRemoved(this, this._defaultUILocation); + } + this._uiLocations.add(uiLocation); + this._breakpointManager._uiLocationAdded(this, uiLocation); + } + + /** + * @param {!Workspace.UILocation} uiLocation + */ + _uiLocationRemoved(uiLocation) { + this._uiLocations.delete(uiLocation); + this._breakpointManager._uiLocationRemoved(this, uiLocation); + if (this._uiLocations.size === 0 && this._defaultUILocation && !this._isRemoved) { + this._breakpointManager._uiLocationAdded(this, this._defaultUILocation); + } + } + + /** + * @return {boolean} + */ + enabled() { + return this._enabled; + } + + /** + * @param {boolean} enabled + */ + setEnabled(enabled) { + this._updateState(this._condition, enabled); + } + + /** + * @return {string} + */ + condition() { + return this._condition; + } + + /** + * @param {string} condition + */ + setCondition(condition) { + this._updateState(condition, this._enabled); + } + + /** + * @param {string} condition + * @param {boolean} enabled + */ + _updateState(condition, enabled) { + if (this._enabled === enabled && this._condition === condition) { + return; + } + this._enabled = enabled; + this._condition = condition; + this._breakpointManager._storage._updateBreakpoint(this); + this._updateBreakpoint(); + } + + _updateBreakpoint() { + if (this._uiLocations.size === 0 && this._defaultUILocation) { + this._breakpointManager._uiLocationRemoved(this, this._defaultUILocation); + } + if (this._uiLocations.size === 0 && this._defaultUILocation && !this._isRemoved) { + this._breakpointManager._uiLocationAdded(this, this._defaultUILocation); + } + const modelBreakpoints = this._modelBreakpoints.valuesArray(); + for (let i = 0; i < modelBreakpoints.length; ++i) { + modelBreakpoints[i]._scheduleUpdateInDebugger(); + } + } + + /** + * @param {boolean} keepInStorage + */ + remove(keepInStorage) { + this._isRemoved = true; + const removeFromStorage = !keepInStorage; + const modelBreakpoints = this._modelBreakpoints.valuesArray(); + for (let i = 0; i < modelBreakpoints.length; ++i) { + modelBreakpoints[i]._scheduleUpdateInDebugger(); + modelBreakpoints[i]._removeEventListeners(); + } + + this._breakpointManager._removeBreakpoint(this, removeFromStorage); + this._breakpointManager._targetManager.unobserveModels(SDK.DebuggerModel, this); + this.setPrimaryUISourceCode(null); + } + + /** + * @return {string} + */ + _breakpointStorageId() { + return BreakpointManager._breakpointStorageId(this._url, this._lineNumber, this._columnNumber); + } + + _resetLocations() { + this.setPrimaryUISourceCode(null); + const modelBreakpoints = this._modelBreakpoints.valuesArray(); + for (let i = 0; i < modelBreakpoints.length; ++i) { + modelBreakpoints[i]._resetLocations(); + } + } +} + +/** + * @unrestricted + */ +export class ModelBreakpoint { + /** + * @param {!SDK.DebuggerModel} debuggerModel + * @param {!Breakpoint} breakpoint + * @param {!DebuggerWorkspaceBinding} debuggerWorkspaceBinding + */ + constructor(debuggerModel, breakpoint, debuggerWorkspaceBinding) { + this._debuggerModel = debuggerModel; + this._breakpoint = breakpoint; + this._debuggerWorkspaceBinding = debuggerWorkspaceBinding; + + this._liveLocations = new LiveLocationPool(); + + /** @type {!Map} */ + this._uiLocations = new Map(); + this._debuggerModel.addEventListener( + SDK.DebuggerModel.Events.DebuggerWasDisabled, this._cleanUpAfterDebuggerIsGone, this); + this._debuggerModel.addEventListener( + SDK.DebuggerModel.Events.DebuggerWasEnabled, this._scheduleUpdateInDebugger, this); + this._hasPendingUpdate = false; + this._isUpdating = false; + this._cancelCallback = false; + this._currentState = null; + if (this._debuggerModel.debuggerEnabled()) { + this._scheduleUpdateInDebugger(); + } + } + + _resetLocations() { + for (const uiLocation of this._uiLocations.values()) { + this._breakpoint._uiLocationRemoved(uiLocation); + } + + this._uiLocations.clear(); + this._liveLocations.disposeAll(); + } + + _scheduleUpdateInDebugger() { + if (this._isUpdating) { + this._hasPendingUpdate = true; + return; + } + + this._isUpdating = true; + this._updateInDebugger(this._didUpdateInDebugger.bind(this)); + } + + _didUpdateInDebugger() { + this._isUpdating = false; + if (this._hasPendingUpdate) { + this._hasPendingUpdate = false; + this._scheduleUpdateInDebugger(); + } + } + + /** + * @return {boolean} + */ + _scriptDiverged() { + const uiLocation = this._breakpoint._defaultUILocation; + const uiSourceCode = uiLocation ? uiLocation.uiSourceCode : null; + if (!uiSourceCode) { + return false; + } + const scriptFile = this._debuggerWorkspaceBinding.scriptFile(uiSourceCode, this._debuggerModel); + return !!scriptFile && scriptFile.hasDivergedFromVM(); + } + + /** + * @param {function()} callback + * @return {!Promise} + */ + async _updateInDebugger(callback) { + if (this._debuggerModel.target().isDisposed()) { + this._cleanUpAfterDebuggerIsGone(); + callback(); + return; + } + + const uiLocation = this._breakpoint._defaultUILocation; + const uiSourceCode = uiLocation ? uiLocation.uiSourceCode : null; + const lineNumber = this._breakpoint._lineNumber; + const columnNumber = this._breakpoint._columnNumber; + const condition = this._breakpoint.condition(); + + let debuggerLocation = null; + if (uiSourceCode) { + const locations = + Bindings.debuggerWorkspaceBinding.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); + debuggerLocation = locations.find(location => location.debuggerModel === this._debuggerModel); + } + let newState; + if (this._breakpoint._isRemoved || !this._breakpoint.enabled() || this._scriptDiverged()) { + newState = null; + } else if (debuggerLocation && debuggerLocation.script()) { + const script = debuggerLocation.script(); + if (script.sourceURL) { + newState = new Breakpoint.State( + script.sourceURL, null, null, debuggerLocation.lineNumber, debuggerLocation.columnNumber, condition); + } else { + newState = new Breakpoint.State( + null, script.scriptId, script.hash, debuggerLocation.lineNumber, debuggerLocation.columnNumber, condition); + } + } else if (this._breakpoint._currentState && this._breakpoint._currentState.url) { + const position = this._breakpoint._currentState; + newState = new Breakpoint.State(position.url, null, null, position.lineNumber, position.columnNumber, condition); + } else if (uiSourceCode) { + newState = new Breakpoint.State(uiSourceCode.url(), null, null, lineNumber, columnNumber, condition); + } + if (this._debuggerId && Breakpoint.State.equals(newState, this._currentState)) { + callback(); + return; + } + + this._breakpoint._currentState = newState; + + if (this._debuggerId) { + await this._refreshBreakpoint(); + callback(); + return; + } + + if (!newState) { + callback(); + return; + } + + let result; + this._currentState = newState; + if (newState.url) { + result = await this._debuggerModel.setBreakpointByURL( + newState.url, newState.lineNumber, newState.columnNumber, newState.condition); + } else if (newState.scriptId && newState.scriptHash) { + result = await this._debuggerModel.setBreakpointInAnonymousScript( + newState.scriptId, newState.scriptHash, newState.lineNumber, newState.columnNumber, newState.condition); + } + if (result && result.breakpointId) { + this._didSetBreakpointInDebugger(callback, result.breakpointId, result.locations); + } else { + this._didSetBreakpointInDebugger(callback, null, []); + } + } + + async _refreshBreakpoint() { + if (!this._debuggerId) { + return; + } + this._resetLocations(); + await this._debuggerModel.removeBreakpoint(this._debuggerId, this._breakpoint); + this._didRemoveFromDebugger(); + this._currentState = null; + this._scheduleUpdateInDebugger(); + } + + /** + * @param {function()} callback + * @param {?Protocol.Debugger.BreakpointId} breakpointId + * @param {!Array.} locations + */ + _didSetBreakpointInDebugger(callback, breakpointId, locations) { + if (this._cancelCallback) { + this._cancelCallback = false; + callback(); + return; + } + + if (!breakpointId) { + this._breakpoint.remove(true); + callback(); + return; + } + + this._debuggerId = breakpointId; + this._debuggerModel.addBreakpointListener(this._debuggerId, this._breakpointResolved, this); + for (let i = 0; i < locations.length; ++i) { + if (!this._addResolvedLocation(locations[i])) { + break; + } + } + callback(); + } + + _didRemoveFromDebugger() { + if (this._cancelCallback) { + this._cancelCallback = false; + return; + } + + this._resetLocations(); + this._debuggerModel.removeBreakpointListener(this._debuggerId, this._breakpointResolved, this); + delete this._debuggerId; + } + + /** + * @param {!Common.Event} event + */ + _breakpointResolved(event) { + this._addResolvedLocation(/** @type {!SDK.DebuggerModel.Location}*/ (event.data)); + } + + /** + * @param {!LiveLocation} liveLocation + */ + _locationUpdated(liveLocation) { + const oldUILocation = this._uiLocations.get(liveLocation); + if (oldUILocation) { + this._breakpoint._uiLocationRemoved(oldUILocation); + } + let uiLocation = liveLocation.uiLocation(); + + if (uiLocation) { + const breakpointLocation = this._breakpoint._breakpointManager.findBreakpoint(uiLocation); + if (breakpointLocation && breakpointLocation.uiLocation !== breakpointLocation.breakpoint._defaultUILocation) { + uiLocation = null; + } + } + + if (uiLocation) { + this._uiLocations.set(liveLocation, uiLocation); + this._breakpoint._uiLocationAdded(uiLocation); + } else { + this._uiLocations.delete(liveLocation); + } + } + + /** + * @param {!SDK.DebuggerModel.Location} location + * @return {boolean} + */ + _addResolvedLocation(location) { + const uiLocation = this._debuggerWorkspaceBinding.rawLocationToUILocation(location); + if (!uiLocation) { + return false; + } + const breakpointLocation = this._breakpoint._breakpointManager.findBreakpoint(uiLocation); + if (breakpointLocation && breakpointLocation.breakpoint !== this._breakpoint) { + // location clash + this._breakpoint.remove(false /* keepInStorage */); + return false; + } + this._debuggerWorkspaceBinding.createLiveLocation(location, this._locationUpdated.bind(this), this._liveLocations); + return true; + } + + _cleanUpAfterDebuggerIsGone() { + if (this._isUpdating) { + this._cancelCallback = true; + } + + this._resetLocations(); + this._currentState = null; + if (this._debuggerId) { + this._didRemoveFromDebugger(); + } + } + + _removeEventListeners() { + this._debuggerModel.removeEventListener( + SDK.DebuggerModel.Events.DebuggerWasDisabled, this._cleanUpAfterDebuggerIsGone, this); + this._debuggerModel.removeEventListener( + SDK.DebuggerModel.Events.DebuggerWasEnabled, this._scheduleUpdateInDebugger, this); + } +} + +Breakpoint.State = class { + /** + * @param {?string} url + * @param {?string} scriptId + * @param {?string} scriptHash + * @param {number} lineNumber + * @param {number} columnNumber + * @param {string} condition + */ + constructor(url, scriptId, scriptHash, lineNumber, columnNumber, condition) { + this.url = url; + this.scriptId = scriptId; + this.scriptHash = scriptHash; + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + this.condition = condition; + } + + /** + * @param {?Breakpoint.State|undefined} stateA + * @param {?Breakpoint.State|undefined} stateB + * @return {boolean} + */ + static equals(stateA, stateB) { + if (!stateA || !stateB) { + return false; + } + return stateA.url === stateB.url && stateA.scriptId === stateB.scriptId && + stateA.scriptHash === stateB.scriptHash && stateA.lineNumber === stateB.lineNumber && + stateA.columnNumber === stateB.columnNumber && stateA.condition === stateB.condition; + } +}; + + +class Storage { + constructor() { + this._setting = Common.settings.createLocalSetting('breakpoints', []); + /** @type {!Map} */ + this._breakpoints = new Map(); + const items = /** @type {!Array} */ (this._setting.get()); + for (const item of items) { + item.columnNumber = item.columnNumber || 0; + this._breakpoints.set(BreakpointManager._breakpointStorageId(item.url, item.lineNumber, item.columnNumber), item); + } + /** @type {boolean|undefined} */ this._muted; + } + + mute() { + this._muted = true; + } + + unmute() { + delete this._muted; + } + + /** + * @param {string} url + * @return {!Array} + */ + breakpointItems(url) { + return Array.from(this._breakpoints.values()).filter(item => item.url === url); + } + + /** + * @param {!Breakpoint} breakpoint + */ + _updateBreakpoint(breakpoint) { + if (this._muted || !breakpoint._breakpointStorageId()) { + return; + } + this._breakpoints.set(breakpoint._breakpointStorageId(), new Storage.Item(breakpoint)); + this._save(); + } + + /** + * @param {!Breakpoint} breakpoint + */ + _removeBreakpoint(breakpoint) { + if (this._muted) { + return; + } + this._breakpoints.delete(breakpoint._breakpointStorageId()); + this._save(); + } + + _save() { + this._setting.set(Array.from(this._breakpoints.values())); + } +} + +/** + * @unrestricted + */ +Storage.Item = class { + /** + * @param {!Breakpoint} breakpoint + */ + constructor(breakpoint) { + this.url = breakpoint._url; + this.lineNumber = breakpoint.lineNumber(); + this.columnNumber = breakpoint.columnNumber(); + this.condition = breakpoint.condition(); + this.enabled = breakpoint.enabled(); + } +}; diff --git a/frontend/devtools_app.json b/frontend/devtools_app.json index 88ca290..4e161ea 100644 --- a/frontend/devtools_app.json +++ b/frontend/devtools_app.json @@ -4,6 +4,9 @@ {"name": "emulation", "type": "autostart"}, {"name": "inspector_main", "type": "autostart"}, {"name": "mobile_throttling", "type": "autostart"}, + { "name": "browser_debugger" }, + { "name": "timeline" }, + { "name": "timeline_model" }, {"name": "accessibility"}, {"name": "animation"}, {"name": "css_overview"}, diff --git a/frontend/sdk/DebuggerModel.js b/frontend/sdk/DebuggerModel.js new file mode 100644 index 0000000..9590132 --- /dev/null +++ b/frontend/sdk/DebuggerModel.js @@ -0,0 +1,1570 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {RemoteObject, ScopeRef} from './RemoteObject.js'; // eslint-disable-line no-unused-vars +import {ExecutionContext, RuntimeModel} from './RuntimeModel.js'; // eslint-disable-line no-unused-vars +import {Script} from './Script.js'; +import {Capability, SDKModel, Target, Type} from './SDKModel.js'; // eslint-disable-line no-unused-vars +import {SourceMapManager} from './SourceMapManager.js'; + +/** + * @unrestricted + */ +export class DebuggerModel extends SDKModel { + /** + * @param {!Target} target + */ + constructor(target) { + super(target); + + target.registerDebuggerDispatcher(new DebuggerDispatcher(this)); + this._agent = target.debuggerAgent(); + this._runtimeModel = /** @type {!RuntimeModel} */ (target.model(RuntimeModel)); + + /** @type {!SourceMapManager} */ + this._sourceMapManager = new SourceMapManager(target); + /** @type {!Map} */ + this._sourceMapIdToScript = new Map(); + + /** @type {?DebuggerPausedDetails} */ + this._debuggerPausedDetails = null; + /** @type {!Map} */ + this._scripts = new Map(); + /** @type {!Map.>} */ + this._scriptsBySourceURL = new Map(); + /** @type {!Array.} */ + this._discardableScripts = []; + + /** @type {!Common.Object} */ + this._breakpointResolvedEventTarget = new Common.Object(); + + /** @type {boolean} */ + this._autoStepOver = false; + + this._isPausing = false; + Common.moduleSetting('pauseOnExceptionEnabled').addChangeListener(this._pauseOnExceptionStateChanged, this); + Common.moduleSetting('pauseOnCaughtException').addChangeListener(this._pauseOnExceptionStateChanged, this); + Common.moduleSetting('disableAsyncStackTraces').addChangeListener(this._asyncStackTracesStateChanged, this); + Common.moduleSetting('breakpointsActive').addChangeListener(this._breakpointsActiveChanged, this); + + if (!target.suspended()) { + this._enableDebugger(); + } + + /** @type {!Map} */ + this._stringMap = new Map(); + this._sourceMapManager.setEnabled(Common.moduleSetting('jsSourceMapsEnabled').get()); + Common.moduleSetting('jsSourceMapsEnabled') + .addChangeListener(event => this._sourceMapManager.setEnabled(/** @type {boolean} */ (event.data))); + } + + /** + * @param {!Protocol.Runtime.ExecutionContextId} executionContextId + * @param {string} sourceURL + * @param {string|undefined} sourceMapURL + * @return {?string} + */ + static _sourceMapId(executionContextId, sourceURL, sourceMapURL) { + if (!sourceMapURL) { + return null; + } + return executionContextId + ':' + sourceURL + ':' + sourceMapURL; + } + + /** + * @return {!SourceMapManager} + */ + sourceMapManager() { + return this._sourceMapManager; + } + + /** + * @return {!RuntimeModel} + */ + runtimeModel() { + return this._runtimeModel; + } + + /** + * @return {boolean} + */ + debuggerEnabled() { + return !!this._debuggerEnabled; + } + + /** + * @return {!Promise} + */ + _enableDebugger() { + if (this._debuggerEnabled) { + return Promise.resolve(); + } + this._debuggerEnabled = true; + + // Set a limit for the total size of collected script sources retained by debugger. + // 10MB for remote frontends, 100MB for others. + const isRemoteFrontend = Root.Runtime.queryParam('remoteFrontend') || Root.Runtime.queryParam('ws'); + const maxScriptsCacheSize = isRemoteFrontend ? 10e6 : 100e6; + const enablePromise = this._agent.enable(maxScriptsCacheSize); + enablePromise.then(this._registerDebugger.bind(this)); + this._pauseOnExceptionStateChanged(); + this._asyncStackTracesStateChanged(); + if (!Common.moduleSetting('breakpointsActive').get()) { + this._breakpointsActiveChanged(); + } + if (DebuggerModel._scheduledPauseOnAsyncCall) { + this._pauseOnAsyncCall(DebuggerModel._scheduledPauseOnAsyncCall); + } + this.dispatchEventToListeners(Events.DebuggerWasEnabled, this); + return enablePromise; + } + + /** + * @param {string|null} debuggerId + */ + _registerDebugger(debuggerId) { + if (!debuggerId) { + return; + } + _debuggerIdToModel.set(debuggerId, this); + this._debuggerId = debuggerId; + this.dispatchEventToListeners(Events.DebuggerIsReadyToPause, this); + } + + /** + * @return {boolean} + */ + isReadyToPause() { + return !!this._debuggerId; + } + + /** + * @param {string} debuggerId + * @return {?DebuggerModel} + */ + static modelForDebuggerId(debuggerId) { + return _debuggerIdToModel.get(debuggerId) || null; + } + + /** + * @return {!Promise} + */ + _disableDebugger() { + if (!this._debuggerEnabled) { + return Promise.resolve(); + } + this._debuggerEnabled = false; + + const disablePromise = this._agent.disable(); + this._isPausing = false; + this._asyncStackTracesStateChanged(); + this.globalObjectCleared(); + this.dispatchEventToListeners(Events.DebuggerWasDisabled); + _debuggerIdToModel.delete(this._debuggerId); + return disablePromise; + } + + /** + * @param {boolean} skip + */ + _skipAllPauses(skip) { + if (this._skipAllPausesTimeout) { + clearTimeout(this._skipAllPausesTimeout); + delete this._skipAllPausesTimeout; + } + this._agent.setSkipAllPauses(skip); + } + + /** + * @param {number} timeout + */ + skipAllPausesUntilReloadOrTimeout(timeout) { + if (this._skipAllPausesTimeout) { + clearTimeout(this._skipAllPausesTimeout); + } + this._agent.setSkipAllPauses(true); + // If reload happens before the timeout, the flag will be already unset and the timeout callback won't change anything. + this._skipAllPausesTimeout = setTimeout(this._skipAllPauses.bind(this, false), timeout); + } + + _pauseOnExceptionStateChanged() { + let state; + if (!Common.moduleSetting('pauseOnExceptionEnabled').get()) { + state = PauseOnExceptionsState.DontPauseOnExceptions; + } else if (Common.moduleSetting('pauseOnCaughtException').get()) { + state = PauseOnExceptionsState.PauseOnAllExceptions; + } else { + state = PauseOnExceptionsState.PauseOnUncaughtExceptions; + } + + this._agent.setPauseOnExceptions(state); + } + + _asyncStackTracesStateChanged() { + const maxAsyncStackChainDepth = 32; + const enabled = !Common.moduleSetting('disableAsyncStackTraces').get() && this._debuggerEnabled; + this._agent.setAsyncCallStackDepth(enabled ? maxAsyncStackChainDepth : 0); + } + + _breakpointsActiveChanged() { + this._agent.setBreakpointsActive(Common.moduleSetting('breakpointsActive').get()); + } + + stepInto() { + this._agent.stepInto(); + } + + stepOver() { + // Mark that in case of auto-stepping, we should be doing + // step-over instead of step-in. + this._autoStepOver = true; + this._agent.stepOver(); + } + + stepOut() { + this._agent.stepOut(); + } + + scheduleStepIntoAsync() { + this._agent.invoke_stepInto({breakOnAsyncCall: true}); + } + + resume() { + this._agent.resume(); + this._isPausing = false; + } + + pause() { + this._isPausing = true; + this._skipAllPauses(false); + this._agent.pause(); + } + + /** + * @param {!Protocol.Runtime.StackTraceId} parentStackTraceId + * @return {!Promise} + */ + _pauseOnAsyncCall(parentStackTraceId) { + return this._agent.invoke_pauseOnAsyncCall({parentStackTraceId: parentStackTraceId}); + } + + /** + * @param {string} url + * @param {number} lineNumber + * @param {number=} columnNumber + * @param {string=} condition + * @return {!Promise} + */ + async setBreakpointByURL(url, lineNumber, columnNumber, condition) { + // Convert file url to node-js path. + let urlRegex; + if (this.target().type() === Type.Node) { + const platformPath = Common.ParsedURL.urlToPlatformPath(url, Host.isWin()); + urlRegex = `${platformPath.escapeForRegExp()}|${url.escapeForRegExp()}`; + } + // Adjust column if needed. + let minColumnNumber = 0; + const scripts = this._scriptsBySourceURL.get(url) || []; + for (let i = 0, l = scripts.length; i < l; ++i) { + const script = scripts[i]; + if (lineNumber === script.lineOffset) { + minColumnNumber = minColumnNumber ? Math.min(minColumnNumber, script.columnOffset) : script.columnOffset; + } + } + columnNumber = Math.max(columnNumber, minColumnNumber); + const response = await this._agent.invoke_setBreakpointByUrl({ + lineNumber: lineNumber, + url: urlRegex ? undefined : url, + urlRegex: urlRegex, + columnNumber: columnNumber, + condition: condition + }); + if (response[Protocol.Error]) { + return {locations: [], breakpointId: null}; + } + let locations = []; + if (response.locations) { + locations = response.locations.map(payload => Location.fromPayload(this, payload)); + } + return {locations: locations, breakpointId: response.breakpointId}; + } + + /** + * @param {string} scriptId + * @param {string} scriptHash + * @param {number} lineNumber + * @param {number=} columnNumber + * @param {string=} condition + * @return {!Promise} + */ + async setBreakpointInAnonymousScript(scriptId, scriptHash, lineNumber, columnNumber, condition) { + const response = await this._agent.invoke_setBreakpointByUrl( + {lineNumber: lineNumber, scriptHash: scriptHash, columnNumber: columnNumber, condition: condition}); + const error = response[Protocol.Error]; + if (error) { + // Old V8 backend doesn't support scriptHash argument. + if (error !== 'Either url or urlRegex must be specified.') { + return {locations: [], breakpointId: null}; + } + return this._setBreakpointBySourceId(scriptId, lineNumber, columnNumber, condition); + } + let locations = []; + if (response.locations) { + locations = response.locations.map(payload => Location.fromPayload(this, payload)); + } + return {locations: locations, breakpointId: response.breakpointId}; + } + + /** + * @param {string} scriptId + * @param {number} lineNumber + * @param {number=} columnNumber + * @param {string=} condition + * @return {!Promise} + */ + async _setBreakpointBySourceId(scriptId, lineNumber, columnNumber, condition) { + // This method is required for backward compatibility with V8 before 6.3.275. + const response = await this._agent.invoke_setBreakpoint( + {location: {scriptId: scriptId, lineNumber: lineNumber, columnNumber: columnNumber}, condition: condition}); + if (response[Protocol.Error]) { + return {breakpointId: null, locations: []}; + } + let actualLocation = []; + if (response.actualLocation) { + actualLocation = [Location.fromPayload(this, response.actualLocation)]; + } + return {locations: actualLocation, breakpointId: response.breakpointId}; + } + + /** + * @param {!Protocol.Debugger.BreakpointId} breakpointId + * @return {!Promise} + */ + async removeBreakpoint(breakpointId, params) { + const response = await this._agent.invoke_removeBreakpoint({ + breakpointId, + url: params._url, + lineNumber: params._lineNumber, + columnNumber: params._columnNumber, + condition: params._condition + }); + if (response[Protocol.Error]) { + console.error('Failed to remove breakpoint: ' + response[Protocol.Error]); + } + } + + /** + * @param {!Location} startLocation + * @param {?Location} endLocation + * @param {boolean} restrictToFunction + * @return {!Promise>} + */ + async getPossibleBreakpoints(startLocation, endLocation, restrictToFunction) { + const response = await this._agent.invoke_getPossibleBreakpoints({ + start: startLocation.payload(), + end: endLocation ? endLocation.payload() : undefined, + restrictToFunction: restrictToFunction + }); + if (response[Protocol.Error] || !response.locations) { + return []; + } + return response.locations.map(location => BreakLocation.fromPayload(this, location)); + } + + /** + * @param {!Protocol.Runtime.StackTraceId} stackId + * @return {!Promise} + */ + async fetchAsyncStackTrace(stackId) { + const response = await this._agent.invoke_getStackTrace({stackTraceId: stackId}); + return response[Protocol.Error] ? null : response.stackTrace; + } + + /** + * @param {!Protocol.Debugger.BreakpointId} breakpointId + * @param {!Protocol.Debugger.Location} location + */ + _breakpointResolved(breakpointId, location) { + this._breakpointResolvedEventTarget.dispatchEventToListeners(breakpointId, Location.fromPayload(this, location)); + } + + globalObjectCleared() { + this._setDebuggerPausedDetails(null); + this._reset(); + // TODO(dgozman): move clients to ExecutionContextDestroyed/ScriptCollected events. + this.dispatchEventToListeners(Events.GlobalObjectCleared, this); + } + + _reset() { + for (const scriptWithSourceMap of this._sourceMapIdToScript.values()) { + this._sourceMapManager.detachSourceMap(scriptWithSourceMap); + } + this._sourceMapIdToScript.clear(); + + this._scripts.clear(); + this._scriptsBySourceURL.clear(); + this._stringMap.clear(); + this._discardableScripts = []; + this._autoStepOver = false; + } + + /** + * @return {!Array} + */ + scripts() { + return Array.from(this._scripts.values()); + } + + /** + * @param {!Protocol.Runtime.ScriptId} scriptId + * @return {?Script} + */ + scriptForId(scriptId) { + return this._scripts.get(scriptId) || null; + } + + /** + * @return {!Array.} + */ + scriptsForSourceURL(sourceURL) { + if (!sourceURL) { + return []; + } + return this._scriptsBySourceURL.get(sourceURL) || []; + } + + /** + * @param {!ExecutionContext} executionContext + * @return {!Array} + */ + scriptsForExecutionContext(executionContext) { + const result = []; + for (const script of this._scripts.values()) { + if (script.executionContextId === executionContext.id) { + result.push(script); + } + } + return result; + } + + /** + * @param {!Protocol.Runtime.ScriptId} scriptId + * @param {string} newSource + * @param {function(?Protocol.Error, !Protocol.Runtime.ExceptionDetails=)} callback + */ + setScriptSource(scriptId, newSource, callback) { + this._scripts.get(scriptId).editSource( + newSource, this._didEditScriptSource.bind(this, scriptId, newSource, callback)); + } + + /** + * @param {!Protocol.Runtime.ScriptId} scriptId + * @param {string} newSource + * @param {function(?Protocol.Error, !Protocol.Runtime.ExceptionDetails=)} callback + * @param {?Protocol.Error} error + * @param {!Protocol.Runtime.ExceptionDetails=} exceptionDetails + * @param {!Array.=} callFrames + * @param {!Protocol.Runtime.StackTrace=} asyncStackTrace + * @param {!Protocol.Runtime.StackTraceId=} asyncStackTraceId + * @param {boolean=} needsStepIn + */ + _didEditScriptSource( + scriptId, newSource, callback, error, exceptionDetails, callFrames, asyncStackTrace, asyncStackTraceId, + needsStepIn) { + callback(error, exceptionDetails); + if (needsStepIn) { + this.stepInto(); + return; + } + + if (!error && callFrames && callFrames.length) { + this._pausedScript( + callFrames, this._debuggerPausedDetails.reason, this._debuggerPausedDetails.auxData, + this._debuggerPausedDetails.breakpointIds, asyncStackTrace, asyncStackTraceId); + } + } + + /** + * @return {?Array.} + */ + get callFrames() { + return this._debuggerPausedDetails ? this._debuggerPausedDetails.callFrames : null; + } + + /** + * @return {?DebuggerPausedDetails} + */ + debuggerPausedDetails() { + return this._debuggerPausedDetails; + } + + /** + * @param {?DebuggerPausedDetails} debuggerPausedDetails + * @return {boolean} + */ + _setDebuggerPausedDetails(debuggerPausedDetails) { + this._isPausing = false; + this._debuggerPausedDetails = debuggerPausedDetails; + if (this._debuggerPausedDetails) { + if (this._beforePausedCallback) { + if (!this._beforePausedCallback.call(null, this._debuggerPausedDetails)) { + return false; + } + } + // If we resolved a location in auto-stepping callback, reset the + // step-over marker. + this._autoStepOver = false; + this.dispatchEventToListeners(Events.DebuggerPaused, this); + } + if (debuggerPausedDetails) { + this.setSelectedCallFrame(debuggerPausedDetails.callFrames[0]); + } else { + this.setSelectedCallFrame(null); + } + return true; + } + + /** + * @param {?function(!DebuggerPausedDetails):boolean} callback + */ + setBeforePausedCallback(callback) { + this._beforePausedCallback = callback; + } + + /** + * @param {!Array.} callFrames + * @param {string} reason + * @param {!Object|undefined} auxData + * @param {!Array.} breakpointIds + * @param {!Protocol.Runtime.StackTrace=} asyncStackTrace + * @param {!Protocol.Runtime.StackTraceId=} asyncStackTraceId + * @param {!Protocol.Runtime.StackTraceId=} asyncCallStackTraceId + */ + async _pausedScript( + callFrames, reason, auxData, breakpointIds, asyncStackTrace, asyncStackTraceId, asyncCallStackTraceId) { + if (asyncCallStackTraceId) { + // Note: this is only to support old backends. Newer ones do not send asyncCallStackTraceId. + DebuggerModel._scheduledPauseOnAsyncCall = asyncCallStackTraceId; + const promises = []; + for (const model of _debuggerIdToModel.values()) { + promises.push(model._pauseOnAsyncCall(asyncCallStackTraceId)); + } + await Promise.all(promises); + this.resume(); + return; + } + + const pausedDetails = + new DebuggerPausedDetails(this, callFrames, reason, auxData, breakpointIds, asyncStackTrace, asyncStackTraceId); + + if (pausedDetails && this._continueToLocationCallback) { + const callback = this._continueToLocationCallback; + delete this._continueToLocationCallback; + if (callback(pausedDetails)) { + return; + } + } + + if (!this._setDebuggerPausedDetails(pausedDetails)) { + if (this._autoStepOver) { + this._agent.stepOver(); + } else { + this._agent.stepInto(); + } + } + + DebuggerModel._scheduledPauseOnAsyncCall = null; + } + + _resumedScript() { + this._setDebuggerPausedDetails(null); + this.dispatchEventToListeners(Events.DebuggerResumed, this); + } + + /** + * @param {!Protocol.Runtime.ScriptId} scriptId + * @param {string} sourceURL + * @param {number} startLine + * @param {number} startColumn + * @param {number} endLine + * @param {number} endColumn + * @param {!Protocol.Runtime.ExecutionContextId} executionContextId + * @param {string} hash + * @param {*|undefined} executionContextAuxData + * @param {boolean} isLiveEdit + * @param {string|undefined} sourceMapURL + * @param {boolean} hasSourceURLComment + * @param {boolean} hasSyntaxError + * @param {number} length + * @param {?Protocol.Runtime.StackTrace} originStackTrace + * @return {!Script} + */ + _parsedScriptSource( + scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, + executionContextAuxData, isLiveEdit, sourceMapURL, hasSourceURLComment, hasSyntaxError, length, + originStackTrace) { + if (this._scripts.has(scriptId)) { + return this._scripts.get(scriptId); + } + let isContentScript = false; + if (executionContextAuxData && ('isDefault' in executionContextAuxData)) { + isContentScript = !executionContextAuxData['isDefault']; + } + sourceURL = this._internString(sourceURL); + const script = new Script( + this, scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, + this._internString(hash), isContentScript, isLiveEdit, sourceMapURL, hasSourceURLComment, length, + originStackTrace); + this._registerScript(script); + this.dispatchEventToListeners(Events.ParsedScriptSource, script); + + const sourceMapId = DebuggerModel._sourceMapId(script.executionContextId, script.sourceURL, script.sourceMapURL); + if (sourceMapId && !hasSyntaxError) { + // Consecutive script evaluations in the same execution context with the same sourceURL + // and sourceMappingURL should result in source map reloading. + const previousScript = this._sourceMapIdToScript.get(sourceMapId); + if (previousScript) { + this._sourceMapManager.detachSourceMap(previousScript); + } + this._sourceMapIdToScript.set(sourceMapId, script); + this._sourceMapManager.attachSourceMap(script, script.sourceURL, script.sourceMapURL); + } + + const isDiscardable = hasSyntaxError && script.isAnonymousScript(); + if (isDiscardable) { + this._discardableScripts.push(script); + this._collectDiscardedScripts(); + } + return script; + } + + /** + * @param {!Script} script + * @param {string} newSourceMapURL + */ + setSourceMapURL(script, newSourceMapURL) { + let sourceMapId = DebuggerModel._sourceMapId(script.executionContextId, script.sourceURL, script.sourceMapURL); + if (sourceMapId && this._sourceMapIdToScript.get(sourceMapId) === script) { + this._sourceMapIdToScript.delete(sourceMapId); + } + this._sourceMapManager.detachSourceMap(script); + + script.sourceMapURL = newSourceMapURL; + sourceMapId = DebuggerModel._sourceMapId(script.executionContextId, script.sourceURL, script.sourceMapURL); + if (!sourceMapId) { + return; + } + this._sourceMapIdToScript.set(sourceMapId, script); + this._sourceMapManager.attachSourceMap(script, script.sourceURL, script.sourceMapURL); + } + + /** + * @param {!ExecutionContext} executionContext + */ + executionContextDestroyed(executionContext) { + const sourceMapIds = Array.from(this._sourceMapIdToScript.keys()); + for (const sourceMapId of sourceMapIds) { + const script = this._sourceMapIdToScript.get(sourceMapId); + if (script.executionContextId === executionContext.id) { + this._sourceMapIdToScript.delete(sourceMapId); + this._sourceMapManager.detachSourceMap(script); + } + } + } + + /** + * @param {!Script} script + */ + _registerScript(script) { + this._scripts.set(script.scriptId, script); + if (script.isAnonymousScript()) { + return; + } + + let scripts = this._scriptsBySourceURL.get(script.sourceURL); + if (!scripts) { + scripts = []; + this._scriptsBySourceURL.set(script.sourceURL, scripts); + } + scripts.push(script); + } + + /** + * @param {!Script} script + */ + _unregisterScript(script) { + console.assert(script.isAnonymousScript()); + this._scripts.delete(script.scriptId); + } + + _collectDiscardedScripts() { + if (this._discardableScripts.length < 1000) { + return; + } + const scriptsToDiscard = this._discardableScripts.splice(0, 100); + for (const script of scriptsToDiscard) { + this._unregisterScript(script); + this.dispatchEventToListeners(Events.DiscardedAnonymousScriptSource, script); + } + } + + /** + * @param {!Script} script + * @param {number} lineNumber + * @param {number} columnNumber + * @return {?Location} + */ + createRawLocation(script, lineNumber, columnNumber) { + return new Location(this, script.scriptId, lineNumber, columnNumber); + } + + /** + * @param {string} sourceURL + * @param {number} lineNumber + * @param {number} columnNumber + * @return {?Location} + */ + createRawLocationByURL(sourceURL, lineNumber, columnNumber) { + let closestScript = null; + const scripts = this._scriptsBySourceURL.get(sourceURL) || []; + for (let i = 0, l = scripts.length; i < l; ++i) { + const script = scripts[i]; + if (!closestScript) { + closestScript = script; + } + if (script.lineOffset > lineNumber || (script.lineOffset === lineNumber && script.columnOffset > columnNumber)) { + continue; + } + if (script.endLine < lineNumber || (script.endLine === lineNumber && script.endColumn <= columnNumber)) { + continue; + } + closestScript = script; + break; + } + return closestScript ? new Location(this, closestScript.scriptId, lineNumber, columnNumber) : null; + } + + /** + * @param {!Protocol.Runtime.ScriptId} scriptId + * @param {number} lineNumber + * @param {number} columnNumber + * @return {?Location} + */ + createRawLocationByScriptId(scriptId, lineNumber, columnNumber) { + const script = this.scriptForId(scriptId); + return script ? this.createRawLocation(script, lineNumber, columnNumber) : null; + } + + /** + * @param {!Protocol.Runtime.StackTrace} stackTrace + * @return {!Array} + */ + createRawLocationsByStackTrace(stackTrace) { + const frames = []; + while (stackTrace) { + for (const frame of stackTrace.callFrames) { + frames.push(frame); + } + stackTrace = stackTrace.parent; + } + + const rawLocations = []; + for (const frame of frames) { + const rawLocation = this.createRawLocationByScriptId(frame.scriptId, frame.lineNumber, frame.columnNumber); + if (rawLocation) { + rawLocations.push(rawLocation); + } + } + return rawLocations; + } + + /** + * @return {boolean} + */ + isPaused() { + return !!this.debuggerPausedDetails(); + } + + /** + * @return {boolean} + */ + isPausing() { + return this._isPausing; + } + + /** + * @param {?CallFrame} callFrame + */ + setSelectedCallFrame(callFrame) { + if (this._selectedCallFrame === callFrame) { + return; + } + this._selectedCallFrame = callFrame; + this.dispatchEventToListeners(Events.CallFrameSelected, this); + } + + /** + * @return {?CallFrame} + */ + selectedCallFrame() { + return this._selectedCallFrame; + } + + /** + * @param {!SDK.RuntimeModel.EvaluationOptions} options + * @return {!Promise} + */ + evaluateOnSelectedCallFrame(options) { + return this.selectedCallFrame().evaluate(options); + } + + /** + * @param {!RemoteObject} remoteObject + * @return {!Promise} + */ + functionDetailsPromise(remoteObject) { + return remoteObject.getAllProperties(false /* accessorPropertiesOnly */, false /* generatePreview */) + .then(buildDetails.bind(this)); + + /** + * @param {!SDK.GetPropertiesResult} response + * @return {?SDK.DebuggerModel.FunctionDetails} + * @this {!DebuggerModel} + */ + function buildDetails(response) { + if (!response) { + return null; + } + let location = null; + if (response.internalProperties) { + for (const prop of response.internalProperties) { + if (prop.name === '[[FunctionLocation]]') { + location = prop.value; + } + } + } + let functionName = null; + if (response.properties) { + for (const prop of response.properties) { + if (prop.name === 'name' && prop.value && prop.value.type === 'string') { + functionName = prop.value; + } + if (prop.name === 'displayName' && prop.value && prop.value.type === 'string') { + functionName = prop.value; + break; + } + } + } + let debuggerLocation = null; + if (location) { + debuggerLocation = this.createRawLocationByScriptId( + location.value.scriptId, location.value.lineNumber, location.value.columnNumber); + } + return {location: debuggerLocation, functionName: functionName ? /** @type {string} */ (functionName.value) : ''}; + } + } + + /** + * @param {number} scopeNumber + * @param {string} variableName + * @param {!Protocol.Runtime.CallArgument} newValue + * @param {string} callFrameId + * @return {!Promise} + */ + async setVariableValue(scopeNumber, variableName, newValue, callFrameId) { + const response = await this._agent.invoke_setVariableValue({scopeNumber, variableName, newValue, callFrameId}); + const error = response[Protocol.Error]; + if (error) { + console.error(error); + } + return error; + } + + /** + * @param {!Protocol.Debugger.BreakpointId} breakpointId + * @param {function(!Common.Event)} listener + * @param {!Object=} thisObject + */ + addBreakpointListener(breakpointId, listener, thisObject) { + this._breakpointResolvedEventTarget.addEventListener(breakpointId, listener, thisObject); + } + + /** + * @param {!Protocol.Debugger.BreakpointId} breakpointId + * @param {function(!Common.Event)} listener + * @param {!Object=} thisObject + */ + removeBreakpointListener(breakpointId, listener, thisObject) { + this._breakpointResolvedEventTarget.removeEventListener(breakpointId, listener, thisObject); + } + + /** + * @param {!Array} patterns + * @return {!Promise} + */ + async setBlackboxPatterns(patterns) { + const response = await this._agent.invoke_setBlackboxPatterns({patterns}); + const error = response[Protocol.Error]; + if (error) { + console.error(error); + } + return !error; + } + + /** + * @override + */ + dispose() { + this._sourceMapManager.dispose(); + _debuggerIdToModel.delete(this._debuggerId); + Common.moduleSetting('pauseOnExceptionEnabled').removeChangeListener(this._pauseOnExceptionStateChanged, this); + Common.moduleSetting('pauseOnCaughtException').removeChangeListener(this._pauseOnExceptionStateChanged, this); + Common.moduleSetting('disableAsyncStackTraces').removeChangeListener(this._asyncStackTracesStateChanged, this); + } + + /** + * @override + * @return {!Promise} + */ + async suspendModel() { + await this._disableDebugger(); + } + + /** + * @override + * @return {!Promise} + */ + async resumeModel() { + await this._enableDebugger(); + } + + /** + * @param {string} string + * @return {string} string + */ + _internString(string) { + if (!this._stringMap.has(string)) { + this._stringMap.set(string, string); + } + return this._stringMap.get(string); + } +} + +/** @type {!Map} */ +export const _debuggerIdToModel = new Map(); + +/** @type {?Protocol.Runtime.StackTraceId} */ +export const _scheduledPauseOnAsyncCall = null; + +/** + * Keep these in sync with WebCore::V8Debugger + * + * @enum {string} + */ +export const PauseOnExceptionsState = { + DontPauseOnExceptions: 'none', + PauseOnAllExceptions: 'all', + PauseOnUncaughtExceptions: 'uncaught' +}; + +/** @enum {symbol} */ +export const Events = { + DebuggerWasEnabled: Symbol('DebuggerWasEnabled'), + DebuggerWasDisabled: Symbol('DebuggerWasDisabled'), + DebuggerPaused: Symbol('DebuggerPaused'), + DebuggerResumed: Symbol('DebuggerResumed'), + ParsedScriptSource: Symbol('ParsedScriptSource'), + FailedToParseScriptSource: Symbol('FailedToParseScriptSource'), + DiscardedAnonymousScriptSource: Symbol('DiscardedAnonymousScriptSource'), + GlobalObjectCleared: Symbol('GlobalObjectCleared'), + CallFrameSelected: Symbol('CallFrameSelected'), + ConsoleCommandEvaluatedInSelectedCallFrame: Symbol('ConsoleCommandEvaluatedInSelectedCallFrame'), + DebuggerIsReadyToPause: Symbol('DebuggerIsReadyToPause'), +}; + +/** @enum {string} */ +export const BreakReason = { + DOM: 'DOM', + EventListener: 'EventListener', + XHR: 'XHR', + Exception: 'exception', + PromiseRejection: 'promiseRejection', + Assert: 'assert', + DebugCommand: 'debugCommand', + OOM: 'OOM', + Other: 'other' +}; + +const ContinueToLocationTargetCallFrames = { + Any: 'any', + Current: 'current' +}; + +/** + * @extends {Protocol.DebuggerDispatcher} + * @unrestricted + */ +class DebuggerDispatcher { + /** + * @param {!DebuggerModel} debuggerModel + */ + constructor(debuggerModel) { + this._debuggerModel = debuggerModel; + } + + /** + * @override + * @param {!Array.} callFrames + * @param {string} reason + * @param {!Object=} auxData + * @param {!Array.=} breakpointIds + * @param {!Protocol.Runtime.StackTrace=} asyncStackTrace + * @param {!Protocol.Runtime.StackTraceId=} asyncStackTraceId + * @param {!Protocol.Runtime.StackTraceId=} asyncCallStackTraceId + */ + paused(callFrames, reason, auxData, breakpointIds, asyncStackTrace, asyncStackTraceId, asyncCallStackTraceId) { + this._debuggerModel._pausedScript( + callFrames, reason, auxData, breakpointIds || [], asyncStackTrace, asyncStackTraceId, asyncCallStackTraceId); + } + + /** + * @override + */ + resumed() { + this._debuggerModel._resumedScript(); + } + + /** + * @override + * @param {!Protocol.Runtime.ScriptId} scriptId + * @param {string} sourceURL + * @param {number} startLine + * @param {number} startColumn + * @param {number} endLine + * @param {number} endColumn + * @param {!Protocol.Runtime.ExecutionContextId} executionContextId + * @param {string} hash + * @param {*=} executionContextAuxData + * @param {boolean=} isLiveEdit + * @param {string=} sourceMapURL + * @param {boolean=} hasSourceURL + * @param {boolean=} isModule + * @param {number=} length + * @param {!Protocol.Runtime.StackTrace=} stackTrace + */ + scriptParsed( + scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, + executionContextAuxData, isLiveEdit, sourceMapURL, hasSourceURL, isModule, length, stackTrace) { + this._debuggerModel._parsedScriptSource( + scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, + executionContextAuxData, !!isLiveEdit, sourceMapURL, !!hasSourceURL, false, length || 0, stackTrace || null); + } + + /** + * @override + * @param {!Protocol.Runtime.ScriptId} scriptId + * @param {string} sourceURL + * @param {number} startLine + * @param {number} startColumn + * @param {number} endLine + * @param {number} endColumn + * @param {!Protocol.Runtime.ExecutionContextId} executionContextId + * @param {string} hash + * @param {*=} executionContextAuxData + * @param {string=} sourceMapURL + * @param {boolean=} hasSourceURL + * @param {boolean=} isModule + * @param {number=} length + * @param {!Protocol.Runtime.StackTrace=} stackTrace + */ + scriptFailedToParse( + scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, + executionContextAuxData, sourceMapURL, hasSourceURL, isModule, length, stackTrace) { + this._debuggerModel._parsedScriptSource( + scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, + executionContextAuxData, false, sourceMapURL, !!hasSourceURL, true, length || 0, stackTrace || null); + } + + /** + * @override + * @param {!Protocol.Debugger.BreakpointId} breakpointId + * @param {!Protocol.Debugger.Location} location + */ + breakpointResolved(breakpointId, location) { + this._debuggerModel._breakpointResolved(breakpointId, location); + } +} + +/** + * @unrestricted + */ +export class Location { + /** + * @param {!DebuggerModel} debuggerModel + * @param {string} scriptId + * @param {number} lineNumber + * @param {number=} columnNumber + */ + constructor(debuggerModel, scriptId, lineNumber, columnNumber) { + this.debuggerModel = debuggerModel; + this.scriptId = scriptId; + this.lineNumber = lineNumber; + this.columnNumber = columnNumber || 0; + } + + /** + * @param {!DebuggerModel} debuggerModel + * @param {!Protocol.Debugger.Location} payload + * @return {!Location} + */ + static fromPayload(debuggerModel, payload) { + return new Location(debuggerModel, payload.scriptId, payload.lineNumber, payload.columnNumber); + } + + /** + * @return {!Protocol.Debugger.Location} + */ + payload() { + return {scriptId: this.scriptId, lineNumber: this.lineNumber, columnNumber: this.columnNumber}; + } + + /** + * @return {?Script} + */ + script() { + return this.debuggerModel.scriptForId(this.scriptId); + } + + /** + * @param {function()=} pausedCallback + */ + continueToLocation(pausedCallback) { + if (pausedCallback) { + this.debuggerModel._continueToLocationCallback = this._paused.bind(this, pausedCallback); + } + this.debuggerModel._agent.continueToLocation(this.payload(), ContinueToLocationTargetCallFrames.Current); + } + + /** + * @param {function()|undefined} pausedCallback + * @param {!DebuggerPausedDetails} debuggerPausedDetails + * @return {boolean} + */ + _paused(pausedCallback, debuggerPausedDetails) { + const location = debuggerPausedDetails.callFrames[0].location(); + if (location.scriptId === this.scriptId && location.lineNumber === this.lineNumber && + location.columnNumber === this.columnNumber) { + pausedCallback(); + return true; + } + return false; + } + + /** + * @return {string} + */ + id() { + return this.debuggerModel.target().id() + ':' + this.scriptId + ':' + this.lineNumber + ':' + this.columnNumber; + } +} + +/** + * @unrestricted + */ +export class BreakLocation extends Location { + /** + * @param {!DebuggerModel} debuggerModel + * @param {string} scriptId + * @param {number} lineNumber + * @param {number=} columnNumber + * @param {!Protocol.Debugger.BreakLocationType=} type + */ + constructor(debuggerModel, scriptId, lineNumber, columnNumber, type) { + super(debuggerModel, scriptId, lineNumber, columnNumber); + if (type) { + this.type = type; + } + } + + /** + * @override + * @param {!DebuggerModel} debuggerModel + * @param {!Protocol.Debugger.BreakLocation} payload + * @return {!BreakLocation} + */ + static fromPayload(debuggerModel, payload) { + return new BreakLocation(debuggerModel, payload.scriptId, payload.lineNumber, payload.columnNumber, payload.type); + } +} + +/** + * @unrestricted + */ +export class CallFrame { + /** + * @param {!DebuggerModel} debuggerModel + * @param {!Script} script + * @param {!Protocol.Debugger.CallFrame} payload + */ + constructor(debuggerModel, script, payload) { + this.debuggerModel = debuggerModel; + this._script = script; + this._payload = payload; + this._location = Location.fromPayload(debuggerModel, payload.location); + this._scopeChain = []; + this._localScope = null; + for (let i = 0; i < payload.scopeChain.length; ++i) { + const scope = new Scope(this, i); + this._scopeChain.push(scope); + if (scope.type() === Protocol.Debugger.ScopeType.Local) { + this._localScope = scope; + } + } + if (payload.functionLocation) { + this._functionLocation = Location.fromPayload(debuggerModel, payload.functionLocation); + } + this._returnValue = + payload.returnValue ? this.debuggerModel._runtimeModel.createRemoteObject(payload.returnValue) : null; + } + + /** + * @param {!DebuggerModel} debuggerModel + * @param {!Array.} callFrames + * @return {!Array.} + */ + static fromPayloadArray(debuggerModel, callFrames) { + const result = []; + for (let i = 0; i < callFrames.length; ++i) { + const callFrame = callFrames[i]; + const script = debuggerModel.scriptForId(callFrame.location.scriptId); + if (script) { + result.push(new CallFrame(debuggerModel, script, callFrame)); + } + } + return result; + } + + /** + * @return {!Script} + */ + get script() { + return this._script; + } + + /** + * @return {string} + */ + get id() { + return this._payload.callFrameId; + } + + /** + * @return {!Array.} + */ + scopeChain() { + return this._scopeChain; + } + + /** + * @return {?Scope} + */ + localScope() { + return this._localScope; + } + + /** + * @return {?RemoteObject} + */ + thisObject() { + return this._payload.this ? this.debuggerModel._runtimeModel.createRemoteObject(this._payload.this) : null; + } + + /** + * @return {?RemoteObject} + */ + returnValue() { + return this._returnValue; + } + + /** + * @param {string} expression + * @return {!Promise} + */ + async setReturnValue(expression) { + if (!this._returnValue) { + return null; + } + + const evaluateResponse = await this.debuggerModel._agent.invoke_evaluateOnCallFrame( + {callFrameId: this.id, expression: expression, silent: true, objectGroup: 'backtrace'}); + if (evaluateResponse[Protocol.Error] || evaluateResponse.exceptionDetails) { + return null; + } + const response = await this.debuggerModel._agent.invoke_setReturnValue({newValue: evaluateResponse.result}); + if (response[Protocol.Error]) { + return null; + } + this._returnValue = this.debuggerModel._runtimeModel.createRemoteObject(evaluateResponse.result); + return this._returnValue; + } + + /** + * @return {string} + */ + get functionName() { + return this._payload.functionName; + } + + /** + * @return {!Location} + */ + location() { + return this._location; + } + + /** + * @return {?Location} + */ + functionLocation() { + return this._functionLocation || null; + } + + /** + * @param {!SDK.RuntimeModel.EvaluationOptions} options + * @return {!Promise} + */ + async evaluate(options) { + const runtimeModel = this.debuggerModel.runtimeModel(); + // Assume backends either support both throwOnSideEffect and timeout options or neither. + const needsTerminationOptions = !!options.throwOnSideEffect || options.timeout !== undefined; + if (needsTerminationOptions && + (runtimeModel.hasSideEffectSupport() === false || + (runtimeModel.hasSideEffectSupport() === null && !await runtimeModel.checkSideEffectSupport()))) { + return {error: 'Side-effect checks not supported by backend.'}; + } + + const response = await this.debuggerModel._agent.invoke_evaluateOnCallFrame({ + callFrameId: this.id, + expression: options.expression, + objectGroup: options.objectGroup, + includeCommandLineAPI: options.includeCommandLineAPI, + silent: options.silent, + returnByValue: options.returnByValue, + generatePreview: options.generatePreview, + throwOnSideEffect: options.throwOnSideEffect, + timeout: options.timeout + }); + const error = response[Protocol.Error]; + if (error) { + console.error(error); + return {error: error}; + } + return {object: runtimeModel.createRemoteObject(response.result), exceptionDetails: response.exceptionDetails}; + } + + async restart() { + const response = await this.debuggerModel._agent.invoke_restartFrame({callFrameId: this._payload.callFrameId}); + if (!response[Protocol.Error]) { + this.debuggerModel.stepInto(); + } + } +} + +/** + * @unrestricted + */ +export class Scope { + /** + * @param {!CallFrame} callFrame + * @param {number} ordinal + */ + constructor(callFrame, ordinal) { + this._callFrame = callFrame; + this._payload = callFrame._payload.scopeChain[ordinal]; + this._type = this._payload.type; + this._name = this._payload.name; + this._ordinal = ordinal; + this._startLocation = + this._payload.startLocation ? Location.fromPayload(callFrame.debuggerModel, this._payload.startLocation) : null; + this._endLocation = + this._payload.endLocation ? Location.fromPayload(callFrame.debuggerModel, this._payload.endLocation) : null; + } + + /** + * @return {!CallFrame} + */ + callFrame() { + return this._callFrame; + } + + /** + * @return {string} + */ + type() { + return this._type; + } + + /** + * @return {string} + */ + typeName() { + switch (this._type) { + case Protocol.Debugger.ScopeType.Local: + return Common.UIString('Local'); + case Protocol.Debugger.ScopeType.Closure: + return Common.UIString('Closure'); + case Protocol.Debugger.ScopeType.Catch: + return Common.UIString('Catch'); + case Protocol.Debugger.ScopeType.Block: + return Common.UIString('Block'); + case Protocol.Debugger.ScopeType.Script: + return Common.UIString('Script'); + case Protocol.Debugger.ScopeType.With: + return Common.UIString('With Block'); + case Protocol.Debugger.ScopeType.Global: + return Common.UIString('Global'); + case Protocol.Debugger.ScopeType.Module: + return Common.UIString('Module'); + } + return ''; + } + + + /** + * @return {string|undefined} + */ + name() { + return this._name; + } + + /** + * @return {?Location} + */ + startLocation() { + return this._startLocation; + } + + /** + * @return {?Location} + */ + endLocation() { + return this._endLocation; + } + + /** + * @return {!RemoteObject} + */ + object() { + if (this._object) { + return this._object; + } + const runtimeModel = this._callFrame.debuggerModel._runtimeModel; + + const declarativeScope = + this._type !== Protocol.Debugger.ScopeType.With && this._type !== Protocol.Debugger.ScopeType.Global; + if (declarativeScope) { + this._object = + runtimeModel.createScopeRemoteObject(this._payload.object, new ScopeRef(this._ordinal, this._callFrame.id)); + } else { + this._object = runtimeModel.createRemoteObject(this._payload.object); + } + + return this._object; + } + + /** + * @return {string} + */ + description() { + const declarativeScope = + this._type !== Protocol.Debugger.ScopeType.With && this._type !== Protocol.Debugger.ScopeType.Global; + return declarativeScope ? '' : (this._payload.object.description || ''); + } +} + +/** + * @unrestricted + */ +export class DebuggerPausedDetails { + /** + * @param {!DebuggerModel} debuggerModel + * @param {!Array.} callFrames + * @param {string} reason + * @param {!Object|undefined} auxData + * @param {!Array.} breakpointIds + * @param {!Protocol.Runtime.StackTrace=} asyncStackTrace + * @param {!Protocol.Runtime.StackTraceId=} asyncStackTraceId + */ + constructor(debuggerModel, callFrames, reason, auxData, breakpointIds, asyncStackTrace, asyncStackTraceId) { + this.debuggerModel = debuggerModel; + this.callFrames = CallFrame.fromPayloadArray(debuggerModel, callFrames); + this.reason = reason; + this.auxData = auxData; + this.breakpointIds = breakpointIds; + if (asyncStackTrace) { + this.asyncStackTrace = this._cleanRedundantFrames(asyncStackTrace); + } + this.asyncStackTraceId = asyncStackTraceId; + } + + /** + * @return {?RemoteObject} + */ + exception() { + if (this.reason !== BreakReason.Exception && this.reason !== BreakReason.PromiseRejection) { + return null; + } + return this.debuggerModel._runtimeModel.createRemoteObject( + /** @type {!Protocol.Runtime.RemoteObject} */ (this.auxData)); + } + + /** + * @param {!Protocol.Runtime.StackTrace} asyncStackTrace + * @return {!Protocol.Runtime.StackTrace} + */ + _cleanRedundantFrames(asyncStackTrace) { + let stack = asyncStackTrace; + let previous = null; + while (stack) { + if (stack.description === 'async function' && stack.callFrames.length) { + stack.callFrames.shift(); + } + if (previous && !stack.callFrames.length) { + previous.parent = stack.parent; + } else { + previous = stack; + } + stack = stack.parent; + } + return asyncStackTrace; + } +} + +SDKModel.register(DebuggerModel, Capability.JS, true); diff --git a/frontend/sdk/NetworkManager.js b/frontend/sdk/NetworkManager.js new file mode 100644 index 0000000..77a678a --- /dev/null +++ b/frontend/sdk/NetworkManager.js @@ -0,0 +1,1584 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {ResourceLoader} from '../host/ResourceLoader.js'; // eslint-disable-line no-unused-vars + +import {Cookie} from './Cookie.js'; +import {Events as NetworkRequestEvents, NetworkRequest} from './NetworkRequest.js'; +import {Capability, SDKModel, SDKModelObserver, Target} from './SDKModel.js'; // eslint-disable-line no-unused-vars + +/** + * @unrestricted + */ +export class NetworkManager extends SDKModel { + /** + * @param {!Target} target + */ + constructor(target) { + super(target); + this._dispatcher = new NetworkDispatcher(this); + this._networkAgent = target.networkAgent(); + target.registerNetworkDispatcher(this._dispatcher); + if (Common.moduleSetting('cacheDisabled').get()) { + this._networkAgent.setCacheDisabled(true); + } + + this._networkAgent.enable(undefined, undefined, MAX_EAGER_POST_REQUEST_BODY_LENGTH); + + this._bypassServiceWorkerSetting = Common.settings.createSetting('bypassServiceWorker', false); + if (this._bypassServiceWorkerSetting.get()) { + this._bypassServiceWorkerChanged(); + } + this._bypassServiceWorkerSetting.addChangeListener(this._bypassServiceWorkerChanged, this); + + Common.moduleSetting('cacheDisabled').addChangeListener(this._cacheDisabledSettingChanged, this); + } + + /** + * @param {!NetworkRequest} request + * @return {?NetworkManager} + */ + static forRequest(request) { + return request[_networkManagerForRequestSymbol]; + } + + /** + * @param {!NetworkRequest} request + * @return {boolean} + */ + static canReplayRequest(request) { + return !!request[_networkManagerForRequestSymbol] && request.resourceType() === Common.resourceTypes.XHR; + } + + /** + * @param {!NetworkRequest} request + */ + static replayRequest(request) { + const manager = request[_networkManagerForRequestSymbol]; + if (!manager) { + return; + } + manager._networkAgent.replayXHR(request.requestId()); + } + + /** + * @param {!NetworkRequest} request + * @param {string} query + * @param {boolean} caseSensitive + * @param {boolean} isRegex + * @return {!Promise>} + */ + static async searchInRequest(request, query, caseSensitive, isRegex) { + const manager = NetworkManager.forRequest(request); + if (!manager) { + return []; + } + const response = await manager._networkAgent.invoke_searchInResponseBody( + {requestId: request.requestId(), query: query, caseSensitive: caseSensitive, isRegex: isRegex}); + return response.result || []; + } + + /** + * @param {!NetworkRequest} request + * @return {!Promise} + */ + static async requestContentData(request) { + if (request.resourceType() === Common.resourceTypes.WebSocket) { + return {error: 'Content for WebSockets is currently not supported', content: null, encoded: false}; + } + if (!request.finished) { + await request.once(NetworkRequestEvents.FinishedLoading); + } + const manager = NetworkManager.forRequest(request); + if (!manager) { + return {error: 'No network manager for request', content: null, encoded: false}; + } + // ======= ME INSERT ======= + let info = {requestId: request.requestId()}; + + info.requestTrueContentUrl = request._url; + info.requestTrueContentFlag = true; + info.loadedRequestTrueContent = true; + let response = request.originalCode; + if (!request.trueReceived) { + response = await manager._networkAgent.invoke_getResponseBody(info); + } + const error = response[Protocol.Error] || null; + return {error: error, content: error ? null : response, encoded: false}; + } + + /** + * @param {!NetworkRequest} request + * @return {!Promise} + */ + static requestPostData(request) { + const manager = NetworkManager.forRequest(request); + if (manager) { + return manager._networkAgent.getRequestPostData(request.backendRequestId()); + } + console.error('No network manager for request'); + return /** @type {!Promise} */ (Promise.resolve(null)); + } + + /** + * @param {!SDK.NetworkManager.Conditions} conditions + * @return {!Protocol.Network.ConnectionType} + * TODO(allada): this belongs to NetworkConditionsSelector, which should hardcode/guess it. + */ + static _connectionType(conditions) { + if (!conditions.download && !conditions.upload) { + return Protocol.Network.ConnectionType.None; + } + let types = NetworkManager._connectionTypes; + if (!types) { + NetworkManager._connectionTypes = []; + types = NetworkManager._connectionTypes; + types.push(['2g', Protocol.Network.ConnectionType.Cellular2g]); + types.push(['3g', Protocol.Network.ConnectionType.Cellular3g]); + types.push(['4g', Protocol.Network.ConnectionType.Cellular4g]); + types.push(['bluetooth', Protocol.Network.ConnectionType.Bluetooth]); + types.push(['wifi', Protocol.Network.ConnectionType.Wifi]); + types.push(['wimax', Protocol.Network.ConnectionType.Wimax]); + } + for (const type of types) { + if (conditions.title.toLowerCase().indexOf(type[0]) !== -1) { + return type[1]; + } + } + return Protocol.Network.ConnectionType.Other; + } + + /** + * @param {!Object} headers + * @return {!Object} + */ + static lowercaseHeaders(headers) { + const newHeaders = {}; + for (const headerName in headers) { + newHeaders[headerName.toLowerCase()] = headers[headerName]; + } + return newHeaders; + } + + /** + * @param {string} url + * @return {!NetworkRequest} + */ + inflightRequestForURL(url) { + return this._dispatcher._inflightRequestsByURL[url]; + } + + /** + * @param {!Common.Event} event + */ + _cacheDisabledSettingChanged(event) { + const enabled = /** @type {boolean} */ (event.data); + this._networkAgent.setCacheDisabled(enabled); + } + + /** + * @override + */ + dispose() { + Common.moduleSetting('cacheDisabled').removeChangeListener(this._cacheDisabledSettingChanged, this); + } + + _bypassServiceWorkerChanged() { + this._networkAgent.setBypassServiceWorker(this._bypassServiceWorkerSetting.get()); + } +} + +/** @enum {symbol} */ +export const Events = { + RequestStarted: Symbol('RequestStarted'), + RequestUpdated: Symbol('RequestUpdated'), + RequestFinished: Symbol('RequestFinished'), + RequestUpdateDropped: Symbol('RequestUpdateDropped'), + ResponseReceived: Symbol('ResponseReceived'), + MessageGenerated: Symbol('MessageGenerated'), + RequestRedirected: Symbol('RequestRedirected'), + LoadingFinished: Symbol('LoadingFinished'), +}; + +const _MIMETypes = { + 'text/html': {'document': true}, + 'text/xml': {'document': true}, + 'text/plain': {'document': true}, + 'application/xhtml+xml': {'document': true}, + 'image/svg+xml': {'document': true}, + 'text/css': {'stylesheet': true}, + 'text/xsl': {'stylesheet': true}, + 'text/vtt': {'texttrack': true}, + 'application/pdf': {'document': true}, +}; + +/** @type {!SDK.NetworkManager.Conditions} */ +export const NoThrottlingConditions = { + title: ls`Online`, + download: -1, + upload: -1, + latency: 0 +}; + +/** @type {!SDK.NetworkManager.Conditions} */ +export const OfflineConditions = { + title: Common.UIString('Offline'), + download: 0, + upload: 0, + latency: 0, +}; + +/** @type {!SDK.NetworkManager.Conditions} */ +export const Slow3GConditions = { + title: Common.UIString('Slow 3G'), + download: 500 * 1024 / 8 * .8, + upload: 500 * 1024 / 8 * .8, + latency: 400 * 5, +}; + +/** @type {!SDK.NetworkManager.Conditions} */ +export const Fast3GConditions = { + title: Common.UIString('Fast 3G'), + download: 1.6 * 1024 * 1024 / 8 * .9, + upload: 750 * 1024 / 8 * .9, + latency: 150 * 3.75, +}; + +const _networkManagerForRequestSymbol = Symbol('NetworkManager'); +const MAX_EAGER_POST_REQUEST_BODY_LENGTH = 64 * 1024; // bytes + +/** + * @implements {Protocol.NetworkDispatcher} + * @unrestricted + */ +export class NetworkDispatcher { + /** + * @param {!NetworkManager} manager + */ + constructor(manager) { + this._manager = manager; + /** @type {!Object} */ + this._inflightRequestsById = {}; + /** @type {!Object} */ + this._inflightRequestsByURL = {}; + /** @type {!Map} */ + this._requestIdToRedirectExtraInfoBuilder = new Map(); + } + + /** + * @param {!Protocol.Network.Headers} headersMap + * @return {!Array.} + */ + _headersMapToHeadersArray(headersMap) { + const result = []; + for (const name in headersMap) { + const values = headersMap[name].split('\n'); + for (let i = 0; i < values.length; ++i) { + result.push({name: name, value: values[i]}); + } + } + return result; + } + + /** + * @param {!NetworkRequest} networkRequest + * @param {!Protocol.Network.Request} request + */ + _updateNetworkRequestWithRequest(networkRequest, request) { + networkRequest.requestMethod = request.method; + networkRequest.setRequestHeaders(this._headersMapToHeadersArray(request.headers)); + networkRequest.setRequestFormData(!!request.hasPostData, request.postData || null); + networkRequest.setInitialPriority(request.initialPriority); + networkRequest.mixedContentType = request.mixedContentType || Protocol.Security.MixedContentType.None; + networkRequest.setReferrerPolicy(request.referrerPolicy); + } + + /** + * @param {!NetworkRequest} networkRequest + * @param {!Protocol.Network.Response=} response + */ + _updateNetworkRequestWithResponse(networkRequest, response) { + if (response.url && networkRequest.url() !== response.url) { + networkRequest.setUrl(response.url); + } + networkRequest.mimeType = response.mimeType; + networkRequest.statusCode = response.status; + networkRequest.statusText = response.statusText; + if (!networkRequest.hasExtraResponseInfo()) { + networkRequest.responseHeaders = this._headersMapToHeadersArray(response.headers); + } + + if (response.encodedDataLength >= 0) { + networkRequest.setTransferSize(response.encodedDataLength); + } + + if (response.requestHeaders && !networkRequest.hasExtraRequestInfo()) { + // TODO(http://crbug.com/1004979): Stop using response.requestHeaders and + // response.requestHeadersText once shared workers + // emit Network.*ExtraInfo events for their network requests. + networkRequest.setRequestHeaders(this._headersMapToHeadersArray(response.requestHeaders)); + networkRequest.setRequestHeadersText(response.requestHeadersText || ''); + } + + networkRequest.connectionReused = response.connectionReused; + networkRequest.connectionId = String(response.connectionId); + if (response.remoteIPAddress) { + networkRequest.setRemoteAddress(response.remoteIPAddress, response.remotePort || -1); + } + + if (response.fromServiceWorker) { + networkRequest.fetchedViaServiceWorker = true; + } + + if (response.fromDiskCache) { + networkRequest.setFromDiskCache(); + } + + if (response.fromPrefetchCache) { + networkRequest.setFromPrefetchCache(); + } + + networkRequest.timing = response.timing; + + networkRequest.protocol = response.protocol || ''; + + networkRequest.setSecurityState(response.securityState); + + if (!this._mimeTypeIsConsistentWithType(networkRequest)) { + const message = Common.UIString( + 'Resource interpreted as %s but transferred with MIME type %s: "%s".', networkRequest.resourceType().title(), + networkRequest.mimeType, networkRequest.url()); + this._manager.dispatchEventToListeners( + Events.MessageGenerated, {message: message, requestId: networkRequest.requestId(), warning: true}); + } + + if (response.securityDetails) { + networkRequest.setSecurityDetails(response.securityDetails); + } + } + + /** + * @param {!NetworkRequest} networkRequest + * @return {boolean} + */ + _mimeTypeIsConsistentWithType(networkRequest) { + // If status is an error, content is likely to be of an inconsistent type, + // as it's going to be an error message. We do not want to emit a warning + // for this, though, as this will already be reported as resource loading failure. + // Also, if a URL like http://localhost/wiki/load.php?debug=true&lang=en produces text/css and gets reloaded, + // it is 304 Not Modified and its guessed mime-type is text/php, which is wrong. + // Don't check for mime-types in 304-resources. + if (networkRequest.hasErrorStatusCode() || networkRequest.statusCode === 304 || networkRequest.statusCode === 204) { + return true; + } + + const resourceType = networkRequest.resourceType(); + if (resourceType !== Common.resourceTypes.Stylesheet && resourceType !== Common.resourceTypes.Document && + resourceType !== Common.resourceTypes.TextTrack) { + return true; + } + + + if (!networkRequest.mimeType) { + return true; + } // Might be not known for cached resources with null responses. + + if (networkRequest.mimeType in _MIMETypes) { + return resourceType.name() in _MIMETypes[networkRequest.mimeType]; + } + + return false; + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.ResourcePriority} newPriority + * @param {!Protocol.Network.MonotonicTime} timestamp + */ + resourceChangedPriority(requestId, newPriority, timestamp) { + const networkRequest = this._inflightRequestsById[requestId]; + if (networkRequest) { + networkRequest.setPriority(newPriority); + } + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.SignedExchangeInfo} info + */ + signedExchangeReceived(requestId, info) { + // While loading a signed exchange, a signedExchangeReceived event is sent + // between two requestWillBeSent events. + // 1. The first requestWillBeSent is sent while starting the navigation (or + // prefetching). + // 2. This signedExchangeReceived event is sent when the browser detects the + // signed exchange. + // 3. The second requestWillBeSent is sent with the generated redirect + // response and a new redirected request which URL is the inner request + // URL of the signed exchange. + let networkRequest = this._inflightRequestsById[requestId]; + // |requestId| is available only for navigation requests. If the request was + // sent from a renderer process for prefetching, it is not available. In the + // case, need to fallback to look for the URL. + // TODO(crbug/841076): Sends the request ID of prefetching to the browser + // process and DevTools to find the matching request. + if (!networkRequest) { + networkRequest = this._inflightRequestsByURL[info.outerResponse.url]; + if (!networkRequest) { + return; + } + } + networkRequest.setSignedExchangeInfo(info); + networkRequest.setResourceType(Common.resourceTypes.SignedExchange); + + this._updateNetworkRequestWithResponse(networkRequest, info.outerResponse); + this._updateNetworkRequest(networkRequest); + this._manager.dispatchEventToListeners(Events.ResponseReceived, networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.LoaderId} loaderId + * @param {string} documentURL + * @param {!Protocol.Network.Request} request + * @param {!Protocol.Network.MonotonicTime} time + * @param {!Protocol.Network.TimeSinceEpoch} wallTime + * @param {!Protocol.Network.Initiator} initiator + * @param {!Protocol.Network.Response=} redirectResponse + * @param {!Protocol.Network.ResourceType=} resourceType + * @param {!Protocol.Page.FrameId=} frameId + */ + requestWillBeSent( + requestId, loaderId, documentURL, request, time, wallTime, initiator, redirectResponse, resourceType, frameId) { + let networkRequest = this._inflightRequestsById[requestId]; + if (networkRequest) { + // FIXME: move this check to the backend. + if (!redirectResponse) { + return; + } + // If signedExchangeReceived event has already been sent for the request, + // ignores the internally generated |redirectResponse|. The + // |outerResponse| of SignedExchangeInfo was set to |networkRequest| in + // signedExchangeReceived(). + if (!networkRequest.signedExchangeInfo()) { + this.responseReceived( + requestId, loaderId, time, Protocol.Network.ResourceType.Other, redirectResponse, frameId); + } + networkRequest = this._appendRedirect(requestId, time, request.url); + this._manager.dispatchEventToListeners(Events.RequestRedirected, networkRequest); + } else { + networkRequest = + this._createNetworkRequest(requestId, frameId || '', loaderId, request.url, documentURL, initiator); + } + networkRequest.setTrueReceived(); + window.__devtools_controller__ || (window.__devtools_controller__ = {}); + window.__devtools_controller__.documentUrl = window.__devtools_controller__.documentUrl || networkRequest._documentURL; + networkRequest.hasNetworkData = true; + this._updateNetworkRequestWithRequest(networkRequest, request); + networkRequest.setIssueTime(time, wallTime); + networkRequest.setResourceType( + resourceType ? Common.resourceTypes[resourceType] : Protocol.Network.ResourceType.Other); + + this._getExtraInfoBuilder(requestId).addRequest(networkRequest); + + this._startNetworkRequest(networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + */ + requestServedFromCache(requestId) { + const networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + return; + } + + networkRequest.setFromMemoryCache(); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.LoaderId} loaderId + * @param {!Protocol.Network.MonotonicTime} time + * @param {!Protocol.Network.ResourceType} resourceType + * @param {!Protocol.Network.Response} response + * @param {!Protocol.Page.FrameId=} frameId + */ + responseReceived(requestId, loaderId, time, resourceType, response, frameId) { + const networkRequest = this._inflightRequestsById[requestId]; + const lowercaseHeaders = NetworkManager.lowercaseHeaders(response.headers); + if (!networkRequest) { + // We missed the requestWillBeSent. + const eventData = {}; + eventData.url = response.url; + eventData.frameId = frameId || ''; + eventData.loaderId = loaderId; + eventData.resourceType = resourceType; + eventData.mimeType = response.mimeType; + const lastModifiedHeader = lowercaseHeaders['last-modified']; + eventData.lastModified = lastModifiedHeader ? new Date(lastModifiedHeader) : null; + this._manager.dispatchEventToListeners(Events.RequestUpdateDropped, eventData); + return; + } + + networkRequest.responseReceivedTime = time; + networkRequest.setResourceType(Common.resourceTypes[resourceType]); + // ==== me insert === + // 仅一个frame + window.__devtools_controller__ || (window.__devtools_controller__ = {}); + window.__devtools_controller__.responseMap = window.__devtools_controller__.responseMap || {}; + window.__devtools_controller__.responseMap[networkRequest._url] = { + scriptSource: response.originalCode + }; + + networkRequest.setOriginalCode(response.originalCode); + // net::ParsedCookie::kMaxCookieSize = 4096 (net/cookies/parsed_cookie.h) + if ('set-cookie' in lowercaseHeaders && lowercaseHeaders['set-cookie'].length > 4096) { + const values = lowercaseHeaders['set-cookie'].split('\n'); + for (let i = 0; i < values.length; ++i) { + if (values[i].length <= 4096) { + continue; + } + const message = Common.UIString( + 'Set-Cookie header is ignored in response from url: %s. Cookie length should be less than or equal to 4096 characters.', + response.url); + this._manager.dispatchEventToListeners( + Events.MessageGenerated, {message: message, requestId: requestId, warning: true}); + } + } + + this._updateNetworkRequestWithResponse(networkRequest, response); + + this._updateNetworkRequest(networkRequest); + this._manager.dispatchEventToListeners(Events.ResponseReceived, networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + * @param {number} dataLength + * @param {number} encodedDataLength + */ + dataReceived(requestId, time, dataLength, encodedDataLength) { + let networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + networkRequest = this._maybeAdoptMainResourceRequest(requestId); + } + if (!networkRequest) { + return; + } + + networkRequest.resourceSize += dataLength; + if (encodedDataLength !== -1) { + networkRequest.increaseTransferSize(encodedDataLength); + } + networkRequest.endTime = time; + + this._updateNetworkRequest(networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} finishTime + * @param {number} encodedDataLength + * @param {boolean=} shouldReportCorbBlocking + */ + loadingFinished(requestId, finishTime, encodedDataLength, shouldReportCorbBlocking) { + let networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + networkRequest = this._maybeAdoptMainResourceRequest(requestId); + } + if (!networkRequest) { + return; + } + this._getExtraInfoBuilder(requestId).finished(); + this._finishNetworkRequest(networkRequest, finishTime, encodedDataLength, shouldReportCorbBlocking); + this._manager.dispatchEventToListeners(Events.LoadingFinished, networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + * @param {!Protocol.Network.ResourceType} resourceType + * @param {string} localizedDescription + * @param {boolean=} canceled + * @param {!Protocol.Network.BlockedReason=} blockedReason + */ + loadingFailed(requestId, time, resourceType, localizedDescription, canceled, blockedReason) { + const networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + return; + } + + networkRequest.failed = true; + networkRequest.setResourceType(Common.resourceTypes[resourceType]); + networkRequest.canceled = !!canceled; + if (blockedReason) { + networkRequest.setBlockedReason(blockedReason); + if (blockedReason === Protocol.Network.BlockedReason.Inspector) { + const message = Common.UIString('Request was blocked by DevTools: "%s".', networkRequest.url()); + this._manager.dispatchEventToListeners( + Events.MessageGenerated, {message: message, requestId: requestId, warning: true}); + } + } + networkRequest.localizedFailDescription = localizedDescription; + this._getExtraInfoBuilder(requestId).finished(); + this._finishNetworkRequest(networkRequest, time, -1); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {string} requestURL + * @param {!Protocol.Network.Initiator=} initiator + */ + webSocketCreated(requestId, requestURL, initiator) { + const networkRequest = new NetworkRequest(requestId, requestURL, '', '', '', initiator || null); + networkRequest[_networkManagerForRequestSymbol] = this._manager; + networkRequest.setResourceType(Common.resourceTypes.WebSocket); + this._startNetworkRequest(networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + * @param {!Protocol.Network.TimeSinceEpoch} wallTime + * @param {!Protocol.Network.WebSocketRequest} request + */ + webSocketWillSendHandshakeRequest(requestId, time, wallTime, request) { + const networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + return; + } + + networkRequest.requestMethod = 'GET'; + networkRequest.setRequestHeaders(this._headersMapToHeadersArray(request.headers)); + networkRequest.setIssueTime(time, wallTime); + + this._updateNetworkRequest(networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + * @param {!Protocol.Network.WebSocketResponse} response + */ + webSocketHandshakeResponseReceived(requestId, time, response) { + const networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + return; + } + + networkRequest.statusCode = response.status; + networkRequest.statusText = response.statusText; + networkRequest.responseHeaders = this._headersMapToHeadersArray(response.headers); + networkRequest.responseHeadersText = response.headersText || ''; + if (response.requestHeaders) { + networkRequest.setRequestHeaders(this._headersMapToHeadersArray(response.requestHeaders)); + } + if (response.requestHeadersText) { + networkRequest.setRequestHeadersText(response.requestHeadersText); + } + networkRequest.responseReceivedTime = time; + networkRequest.protocol = 'websocket'; + + this._updateNetworkRequest(networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + * @param {!Protocol.Network.WebSocketFrame} response + */ + webSocketFrameReceived(requestId, time, response) { + const networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + return; + } + + networkRequest.addProtocolFrame(response, time, false); + networkRequest.responseReceivedTime = time; + + this._updateNetworkRequest(networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + * @param {!Protocol.Network.WebSocketFrame} response + */ + webSocketFrameSent(requestId, time, response) { + const networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + return; + } + + networkRequest.addProtocolFrame(response, time, true); + networkRequest.responseReceivedTime = time; + + this._updateNetworkRequest(networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + * @param {string} errorMessage + */ + webSocketFrameError(requestId, time, errorMessage) { + const networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + return; + } + + networkRequest.addProtocolFrameError(errorMessage, time); + networkRequest.responseReceivedTime = time; + + this._updateNetworkRequest(networkRequest); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + */ + webSocketClosed(requestId, time) { + const networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + return; + } + this._finishNetworkRequest(networkRequest, time, -1); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + * @param {string} eventName + * @param {string} eventId + * @param {string} data + */ + eventSourceMessageReceived(requestId, time, eventName, eventId, data) { + const networkRequest = this._inflightRequestsById[requestId]; + if (!networkRequest) { + return; + } + networkRequest.addEventSourceMessage(time, eventName, eventId, data); + } + + /** + * @override + * @param {!Protocol.Network.InterceptionId} interceptionId + * @param {!Protocol.Network.Request} request + * @param {!Protocol.Page.FrameId} frameId + * @param {!Protocol.Network.ResourceType} resourceType + * @param {boolean} isNavigationRequest + * @param {boolean=} isDownload + * @param {string=} redirectUrl + * @param {!Protocol.Network.AuthChallenge=} authChallenge + * @param {!Protocol.Network.ErrorReason=} responseErrorReason + * @param {number=} responseStatusCode + * @param {!Protocol.Network.Headers=} responseHeaders + * @param {!Protocol.Network.RequestId=} requestId + */ + requestIntercepted( + interceptionId, request, frameId, resourceType, isNavigationRequest, isDownload, redirectUrl, authChallenge, + responseErrorReason, responseStatusCode, responseHeaders, requestId) { + SDK.multitargetNetworkManager._requestIntercepted(new InterceptedRequest( + this._manager.target().networkAgent(), interceptionId, request, frameId, resourceType, isNavigationRequest, + isDownload, redirectUrl, authChallenge, responseErrorReason, responseStatusCode, responseHeaders, requestId)); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Array} blockedCookies + * @param {!Protocol.Network.Headers} headers + */ + requestWillBeSentExtraInfo(requestId, blockedCookies, headers) { + /** @type {!SDK.NetworkRequest.ExtraRequestInfo} */ + const extraRequestInfo = { + blockedRequestCookies: blockedCookies.map(blockedCookie => { + return {blockedReasons: blockedCookie.blockedReasons, cookie: Cookie.fromProtocolCookie(blockedCookie.cookie)}; + }), + requestHeaders: this._headersMapToHeadersArray(headers) + }; + this._getExtraInfoBuilder(requestId).addRequestExtraInfo(extraRequestInfo); + } + + /** + * @override + * @param {!Protocol.Network.RequestId} requestId + * @param {!Array} blockedCookies + * @param {!Protocol.Network.Headers} headers + * @param {string=} headersText + */ + responseReceivedExtraInfo(requestId, blockedCookies, headers, headersText) { + /** @type {!SDK.NetworkRequest.ExtraResponseInfo} */ + const extraResponseInfo = { + blockedResponseCookies: blockedCookies.map(blockedCookie => { + return { + blockedReasons: blockedCookie.blockedReasons, + cookieLine: blockedCookie.cookieLine, + cookie: blockedCookie.cookie ? Cookie.fromProtocolCookie(blockedCookie.cookie) : null + }; + }), + responseHeaders: this._headersMapToHeadersArray(headers), + responseHeadersText: headersText + }; + this._getExtraInfoBuilder(requestId).addResponseExtraInfo(extraResponseInfo); + } + + /** + * @suppress {missingOverride} + * @param {string} url + * @param {string} firstPartyUrl + * @param {!Array} blockedCookies + */ + cookiesChanged(url, firstPartyUrl, blockedCookies) { + // TODO(chromium:1032063): Implement this protocol message handler. + } + + /** + * @param {string} requestId + * @return {!RedirectExtraInfoBuilder} + */ + _getExtraInfoBuilder(requestId) { + if (!this._requestIdToRedirectExtraInfoBuilder.get(requestId)) { + const deleteCallback = () => { + this._requestIdToRedirectExtraInfoBuilder.delete(requestId); + }; + this._requestIdToRedirectExtraInfoBuilder.set(requestId, new RedirectExtraInfoBuilder(deleteCallback)); + } + return this._requestIdToRedirectExtraInfoBuilder.get(requestId); + } + + /** + * @param {!Protocol.Network.RequestId} requestId + * @param {!Protocol.Network.MonotonicTime} time + * @param {string} redirectURL + * @return {!NetworkRequest} + */ + _appendRedirect(requestId, time, redirectURL) { + const originalNetworkRequest = this._inflightRequestsById[requestId]; + let redirectCount = 0; + for (let redirect = originalNetworkRequest.redirectSource(); redirect; redirect = redirect.redirectSource()) { + redirectCount++; + } + + originalNetworkRequest.markAsRedirect(redirectCount); + this._finishNetworkRequest(originalNetworkRequest, time, -1); + const newNetworkRequest = this._createNetworkRequest( + requestId, originalNetworkRequest.frameId, originalNetworkRequest.loaderId, redirectURL, + originalNetworkRequest.documentURL, originalNetworkRequest.initiator()); + newNetworkRequest.setRedirectSource(originalNetworkRequest); + originalNetworkRequest.setRedirectDestination(newNetworkRequest); + return newNetworkRequest; + } + + /** + * @param {string} requestId + * @return {?NetworkRequest} + */ + _maybeAdoptMainResourceRequest(requestId) { + const request = SDK.multitargetNetworkManager._inflightMainResourceRequests.get(requestId); + if (!request) { + return null; + } + const oldDispatcher = NetworkManager.forRequest(request)._dispatcher; + delete oldDispatcher._inflightRequestsById[requestId]; + delete oldDispatcher._inflightRequestsByURL[request.url()]; + this._inflightRequestsById[requestId] = request; + this._inflightRequestsByURL[request.url()] = request; + request[_networkManagerForRequestSymbol] = this._manager; + return request; + } + + /** + * @param {!NetworkRequest} networkRequest + */ + _startNetworkRequest(networkRequest) { + this._inflightRequestsById[networkRequest.requestId()] = networkRequest; + this._inflightRequestsByURL[networkRequest.url()] = networkRequest; + // The following relies on the fact that loaderIds and requestIds are + // globally unique and that the main request has them equal. + if (networkRequest.loaderId === networkRequest.requestId()) { + SDK.multitargetNetworkManager._inflightMainResourceRequests.set(networkRequest.requestId(), networkRequest); + } + + this._manager.dispatchEventToListeners(Events.RequestStarted, networkRequest); + } + + /** + * @param {!NetworkRequest} networkRequest + */ + _updateNetworkRequest(networkRequest) { + this._manager.dispatchEventToListeners(Events.RequestUpdated, networkRequest); + } + + /** + * @param {!NetworkRequest} networkRequest + * @param {!Protocol.Network.MonotonicTime} finishTime + * @param {number} encodedDataLength + * @param {boolean=} shouldReportCorbBlocking + */ + _finishNetworkRequest(networkRequest, finishTime, encodedDataLength, shouldReportCorbBlocking) { + networkRequest.endTime = finishTime; + networkRequest.finished = true; + if (encodedDataLength >= 0) { + const redirectSource = networkRequest.redirectSource(); + if (redirectSource && redirectSource.signedExchangeInfo()) { + networkRequest.setTransferSize(0); + redirectSource.setTransferSize(encodedDataLength); + this._updateNetworkRequest(redirectSource); + } else { + networkRequest.setTransferSize(encodedDataLength); + } + } + this._manager.dispatchEventToListeners(Events.RequestFinished, networkRequest); + delete this._inflightRequestsById[networkRequest.requestId()]; + delete this._inflightRequestsByURL[networkRequest.url()]; + SDK.multitargetNetworkManager._inflightMainResourceRequests.delete(networkRequest.requestId()); + + if (shouldReportCorbBlocking) { + const message = Common.UIString( + `Cross-Origin Read Blocking (CORB) blocked cross-origin response %s with MIME type %s. See https://www.chromestatus.com/feature/5629709824032768 for more details.`, + networkRequest.url(), networkRequest.mimeType); + this._manager.dispatchEventToListeners( + Events.MessageGenerated, {message: message, requestId: networkRequest.requestId(), warning: true}); + } + + if (Common.moduleSetting('monitoringXHREnabled').get() && + networkRequest.resourceType().category() === Common.resourceCategories.XHR) { + let message; + const failedToLoad = networkRequest.failed || networkRequest.hasErrorStatusCode(); + if (failedToLoad) { + message = Common.UIString( + '%s failed loading: %s "%s".', networkRequest.resourceType().title(), networkRequest.requestMethod, + networkRequest.url()); + } else { + message = Common.UIString( + '%s finished loading: %s "%s".', networkRequest.resourceType().title(), networkRequest.requestMethod, + networkRequest.url()); + } + + this._manager.dispatchEventToListeners( + Events.MessageGenerated, {message: message, requestId: networkRequest.requestId(), warning: false}); + } + } + + /** + * @param {!Protocol.Network.RequestId} requestId + * @param {string} frameId + * @param {!Protocol.Network.LoaderId} loaderId + * @param {string} url + * @param {string} documentURL + * @param {?Protocol.Network.Initiator} initiator + */ + _createNetworkRequest(requestId, frameId, loaderId, url, documentURL, initiator) { + const request = new NetworkRequest(requestId, url, documentURL, frameId, loaderId, initiator); + request[_networkManagerForRequestSymbol] = this._manager; + return request; + } +} + +/** + * @implements {SDKModelObserver} + * @unrestricted + */ +export class MultitargetNetworkManager extends Common.Object { + constructor() { + super(); + this._userAgentOverride = ''; + /** @type {!Set} */ + this._agents = new Set(); + /** @type {!Map} */ + this._inflightMainResourceRequests = new Map(); + /** @type {!SDK.NetworkManager.Conditions} */ + this._networkConditions = NoThrottlingConditions; + /** @type {?Promise} */ + this._updatingInterceptionPatternsPromise = null; + + // TODO(allada) Remove these and merge it with request interception. + this._blockingEnabledSetting = Common.moduleSetting('requestBlockingEnabled'); + this._blockedPatternsSetting = Common.settings.createSetting('networkBlockedPatterns', []); + this._effectiveBlockedURLs = []; + this._updateBlockedPatterns(); + + /** @type {!Platform.Multimap} */ + this._urlsForRequestInterceptor = new Platform.Multimap(); + + SDK.targetManager.observeModels(NetworkManager, this); + } + + /** + * @param {string} uaString + * @return {string} + */ + static patchUserAgentWithChromeVersion(uaString) { + // Patches Chrome/CriOS version from user agent ("1.2.3.4" when user agent is: "Chrome/1.2.3.4"). + // Edge also contains an appVersion which should be patched to match the Chrome major version. + // Otherwise, ignore it. This assumes additional appVersions appear after the Chrome version. + const chromeRegex = new RegExp('(?:^|\\W)Chrome/(\\S+)'); + const chromeMatch = navigator.userAgent.match(chromeRegex); + if (chromeMatch && chromeMatch.length > 1) { + // "1.2.3.4" becomes "1.0.100.0" + const additionalAppVersion = chromeMatch[1].split('.', 1)[0] + '.0.100.0'; + return String.sprintf(uaString, chromeMatch[1], additionalAppVersion); + } + return uaString; + } + + /** + * @override + * @param {!NetworkManager} networkManager + */ + modelAdded(networkManager) { + const networkAgent = networkManager.target().networkAgent(); + if (this._extraHeaders) { + networkAgent.setExtraHTTPHeaders(this._extraHeaders); + } + if (this._currentUserAgent()) { + networkAgent.setUserAgentOverride(this._currentUserAgent()); + } + if (this._effectiveBlockedURLs.length) { + networkAgent.setBlockedURLs(this._effectiveBlockedURLs); + } + if (this.isIntercepting()) { + networkAgent.setRequestInterception(this._urlsForRequestInterceptor.valuesArray()); + } + this._agents.add(networkAgent); + if (this.isThrottling()) { + this._updateNetworkConditions(networkAgent); + } + } + + /** + * @override + * @param {!NetworkManager} networkManager + */ + modelRemoved(networkManager) { + for (const entry of this._inflightMainResourceRequests) { + const manager = NetworkManager.forRequest(/** @type {!NetworkRequest} */ (entry[1])); + if (manager !== networkManager) { + continue; + } + this._inflightMainResourceRequests.delete(/** @type {string} */ (entry[0])); + } + this._agents.delete(networkManager.target().networkAgent()); + } + + /** + * @return {boolean} + */ + isThrottling() { + return this._networkConditions.download >= 0 || this._networkConditions.upload >= 0 || + this._networkConditions.latency > 0; + } + + /** + * @return {boolean} + */ + isOffline() { + return !this._networkConditions.download && !this._networkConditions.upload; + } + + /** + * @param {!SDK.NetworkManager.Conditions} conditions + */ + setNetworkConditions(conditions) { + this._networkConditions = conditions; + for (const agent of this._agents) { + this._updateNetworkConditions(agent); + } + this.dispatchEventToListeners(MultitargetNetworkManager.Events.ConditionsChanged); + } + + /** + * @return {!SDK.NetworkManager.Conditions} + */ + networkConditions() { + return this._networkConditions; + } + + /** + * @param {!Protocol.NetworkAgent} networkAgent + */ + _updateNetworkConditions(networkAgent) { + const conditions = this._networkConditions; + if (!this.isThrottling()) { + networkAgent.emulateNetworkConditions(false, 0, 0, 0); + } else { + networkAgent.emulateNetworkConditions( + this.isOffline(), conditions.latency, conditions.download < 0 ? 0 : conditions.download, + conditions.upload < 0 ? 0 : conditions.upload, NetworkManager._connectionType(conditions)); + } + } + + /** + * @param {!Protocol.Network.Headers} headers + */ + setExtraHTTPHeaders(headers) { + this._extraHeaders = headers; + for (const agent of this._agents) { + agent.setExtraHTTPHeaders(this._extraHeaders); + } + } + + /** + * @return {string} + */ + _currentUserAgent() { + return this._customUserAgent ? this._customUserAgent : this._userAgentOverride; + } + + _updateUserAgentOverride() { + const userAgent = this._currentUserAgent(); + for (const agent of this._agents) { + agent.setUserAgentOverride(userAgent); + } + } + + /** + * @param {string} userAgent + */ + setUserAgentOverride(userAgent) { + if (this._userAgentOverride === userAgent) { + return; + } + this._userAgentOverride = userAgent; + if (!this._customUserAgent) { + this._updateUserAgentOverride(); + } + this.dispatchEventToListeners(MultitargetNetworkManager.Events.UserAgentChanged); + } + + /** + * @return {string} + */ + userAgentOverride() { + return this._userAgentOverride; + } + + /** + * @param {string} userAgent + */ + setCustomUserAgentOverride(userAgent) { + this._customUserAgent = userAgent; + this._updateUserAgentOverride(); + } + + // TODO(allada) Move all request blocking into interception and let view manage blocking. + /** + * @return {!Array} + */ + blockedPatterns() { + return this._blockedPatternsSetting.get().slice(); + } + + /** + * @return {boolean} + */ + blockingEnabled() { + return this._blockingEnabledSetting.get(); + } + + /** + * @return {boolean} + */ + isBlocking() { + return !!this._effectiveBlockedURLs.length; + } + + /** + * @param {!Array} patterns + */ + setBlockedPatterns(patterns) { + this._blockedPatternsSetting.set(patterns); + this._updateBlockedPatterns(); + this.dispatchEventToListeners(MultitargetNetworkManager.Events.BlockedPatternsChanged); + } + + /** + * @param {boolean} enabled + */ + setBlockingEnabled(enabled) { + if (this._blockingEnabledSetting.get() === enabled) { + return; + } + this._blockingEnabledSetting.set(enabled); + this._updateBlockedPatterns(); + this.dispatchEventToListeners(MultitargetNetworkManager.Events.BlockedPatternsChanged); + } + + _updateBlockedPatterns() { + const urls = []; + if (this._blockingEnabledSetting.get()) { + for (const pattern of this._blockedPatternsSetting.get()) { + if (pattern.enabled) { + urls.push(pattern.url); + } + } + } + + if (!urls.length && !this._effectiveBlockedURLs.length) { + return; + } + this._effectiveBlockedURLs = urls; + for (const agent of this._agents) { + agent.setBlockedURLs(this._effectiveBlockedURLs); + } + } + + /** + * @return {boolean} + */ + isIntercepting() { + return !!this._urlsForRequestInterceptor.size; + } + + /** + * @param {!Array} patterns + * @param {!SDK.MultitargetNetworkManager.RequestInterceptor} requestInterceptor + * @return {!Promise} + */ + setInterceptionHandlerForPatterns(patterns, requestInterceptor) { + // Note: requestInterceptors may recieve interception requests for patterns they did not subscribe to. + this._urlsForRequestInterceptor.deleteAll(requestInterceptor); + for (const newPattern of patterns) { + this._urlsForRequestInterceptor.set(requestInterceptor, newPattern); + } + return this._updateInterceptionPatternsOnNextTick(); + } + + /** + * @return {!Promise} + */ + _updateInterceptionPatternsOnNextTick() { + // This is used so we can register and unregister patterns in loops without sending lots of protocol messages. + if (!this._updatingInterceptionPatternsPromise) { + this._updatingInterceptionPatternsPromise = Promise.resolve().then(this._updateInterceptionPatterns.bind(this)); + } + return this._updatingInterceptionPatternsPromise; + } + + /** + * @return {!Promise} + */ + _updateInterceptionPatterns() { + if (!Common.moduleSetting('cacheDisabled').get()) { + Common.moduleSetting('cacheDisabled').set(true); + } + this._updatingInterceptionPatternsPromise = null; + const promises = /** @type {!Array} */ ([]); + for (const agent of this._agents) { + promises.push(agent.setRequestInterception(this._urlsForRequestInterceptor.valuesArray())); + } + this.dispatchEventToListeners(MultitargetNetworkManager.Events.InterceptorsChanged); + return Promise.all(promises); + } + + /** + * @param {!InterceptedRequest} interceptedRequest + */ + async _requestIntercepted(interceptedRequest) { + for (const requestInterceptor of this._urlsForRequestInterceptor.keysArray()) { + await requestInterceptor(interceptedRequest); + if (interceptedRequest.hasResponded()) { + return; + } + } + if (!interceptedRequest.hasResponded()) { + interceptedRequest.continueRequestWithoutChange(); + } + } + + clearBrowserCache() { + for (const agent of this._agents) { + agent.clearBrowserCache(); + } + } + + clearBrowserCookies() { + for (const agent of this._agents) { + agent.clearBrowserCookies(); + } + } + + /** + * @param {string} origin + * @return {!Promise>} + */ + getCertificate(origin) { + const target = SDK.targetManager.mainTarget(); + return target.networkAgent().getCertificate(origin).then(certificate => certificate || []); + } + + /** + * @param {string} url + * @param {function(boolean, !Object., string, !ResourceLoader.LoadErrorDescription)} callback + */ + loadResource(url, callback) { + const headers = {}; + + const currentUserAgent = this._currentUserAgent(); + if (currentUserAgent) { + headers['User-Agent'] = currentUserAgent; + } + + if (Common.moduleSetting('cacheDisabled').get()) { + headers['Cache-Control'] = 'no-cache'; + } + + Host.ResourceLoader.load(url, headers, callback); + } +} + +/** @enum {symbol} */ +MultitargetNetworkManager.Events = { + BlockedPatternsChanged: Symbol('BlockedPatternsChanged'), + ConditionsChanged: Symbol('ConditionsChanged'), + UserAgentChanged: Symbol('UserAgentChanged'), + InterceptorsChanged: Symbol('InterceptorsChanged') +}; + +export class InterceptedRequest { + /** + * @param {!Protocol.NetworkAgent} networkAgent + * @param {!Protocol.Network.InterceptionId} interceptionId + * @param {!Protocol.Network.Request} request + * @param {!Protocol.Page.FrameId} frameId + * @param {!Protocol.Network.ResourceType} resourceType + * @param {boolean} isNavigationRequest + * @param {boolean=} isDownload + * @param {string=} redirectUrl + * @param {!Protocol.Network.AuthChallenge=} authChallenge + * @param {!Protocol.Network.ErrorReason=} responseErrorReason + * @param {number=} responseStatusCode + * @param {!Protocol.Network.Headers=} responseHeaders + * @param {!Protocol.Network.RequestId=} requestId + */ + constructor( + networkAgent, interceptionId, request, frameId, resourceType, isNavigationRequest, isDownload, redirectUrl, + authChallenge, responseErrorReason, responseStatusCode, responseHeaders, requestId) { + this._networkAgent = networkAgent; + this._interceptionId = interceptionId; + this._hasResponded = false; + this.request = request; + this.frameId = frameId; + this.resourceType = resourceType; + this.isNavigationRequest = isNavigationRequest; + this.isDownload = !!isDownload; + this.redirectUrl = redirectUrl; + this.authChallenge = authChallenge; + this.responseErrorReason = responseErrorReason; + this.responseStatusCode = responseStatusCode; + this.responseHeaders = responseHeaders; + this.requestId = requestId; + } + + /** + * @return {boolean} + */ + hasResponded() { + return this._hasResponded; + } + + /** + * @param {!Blob} contentBlob + */ + async continueRequestWithContent(contentBlob) { + this._hasResponded = true; + const headers = [ + 'HTTP/1.1 200 OK', + 'Date: ' + (new Date()).toUTCString(), + 'Server: Chrome Devtools Request Interceptor', + 'Connection: closed', + 'Content-Length: ' + contentBlob.size, + 'Content-Type: ' + contentBlob.type || 'text/x-unknown', + ]; + const encodedResponse = await blobToBase64(new Blob([headers.join('\r\n'), '\r\n\r\n', contentBlob])); + this._networkAgent.continueInterceptedRequest(this._interceptionId, undefined, encodedResponse); + + /** + * @param {!Blob} blob + * @return {!Promise} + */ + async function blobToBase64(blob) { + const reader = new FileReader(); + const fileContentsLoadedPromise = new Promise(resolve => reader.onloadend = resolve); + reader.readAsDataURL(blob); + await fileContentsLoadedPromise; + if (reader.error) { + console.error('Could not convert blob to base64.', reader.error); + return ''; + } + const result = reader.result; + if (result === undefined) { + console.error('Could not convert blob to base64.'); + return ''; + } + return result.substring(result.indexOf(',') + 1); + } + } + + continueRequestWithoutChange() { + console.assert(!this._hasResponded); + this._hasResponded = true; + this._networkAgent.continueInterceptedRequest(this._interceptionId); + } + + /** + * @param {!Protocol.Network.ErrorReason} errorReason + */ + continueRequestWithError(errorReason) { + console.assert(!this._hasResponded); + this._hasResponded = true; + this._networkAgent.continueInterceptedRequest(this._interceptionId, errorReason); + } + + /** + * @return {!Promise} + */ + async responseBody() { + const response = + await this._networkAgent.invoke_getResponseBodyForInterception({interceptionId: this._interceptionId}); + const error = response[Protocol.Error] || null; + return {error: error, content: error ? null : response.body, encoded: response.base64Encoded}; + } +} + +/** + * Helper class to match requests created from requestWillBeSent with + * requestWillBeSentExtraInfo and responseReceivedExtraInfo when they have the + * same requestId due to redirects. + */ +class RedirectExtraInfoBuilder { + /** + * @param {function()} deleteCallback + */ + constructor(deleteCallback) { + /** @type {!Array} */ + this._requests = []; + /** @type {!Array} */ + this._requestExtraInfos = []; + /** @type {!Array} */ + this._responseExtraInfos = []; + /** @type {boolean} */ + this._finished = false; + /** @type {boolean} */ + this._hasExtraInfo = false; + /** @type {function()} */ + this._deleteCallback = deleteCallback; + } + + /** + * @param {!NetworkRequest} req + */ + addRequest(req) { + this._requests.push(req); + this._sync(this._requests.length - 1); + } + + /** + * @param {!SDK.NetworkRequest.ExtraRequestInfo} info + */ + addRequestExtraInfo(info) { + this._hasExtraInfo = true; + this._requestExtraInfos.push(info); + this._sync(this._requestExtraInfos.length - 1); + } + + /** + * @param {!SDK.NetworkRequest.ExtraResponseInfo} info + */ + addResponseExtraInfo(info) { + this._responseExtraInfos.push(info); + this._sync(this._responseExtraInfos.length - 1); + } + + finished() { + this._finished = true; + this._deleteIfComplete(); + } + + /** + * @param {number} index + */ + _sync(index) { + const req = this._requests[index]; + if (!req) { + return; + } + + const requestExtraInfo = this._requestExtraInfos[index]; + if (requestExtraInfo) { + req.addExtraRequestInfo(requestExtraInfo); + this._requestExtraInfos[index] = null; + } + + const responseExtraInfo = this._responseExtraInfos[index]; + if (responseExtraInfo) { + req.addExtraResponseInfo(responseExtraInfo); + this._responseExtraInfos[index] = null; + } + + this._deleteIfComplete(); + } + + _deleteIfComplete() { + if (!this._finished) { + return; + } + + if (this._hasExtraInfo) { + // if we haven't gotten the last responseExtraInfo event, we have to wait for it. + if (!this._requests.peekLast().hasExtraResponseInfo()) { + return; + } + } + + this._deleteCallback(); + } +} + +SDKModel.register(NetworkManager, Capability.Network, true); diff --git a/frontend/sdk/NetworkRequest.js b/frontend/sdk/NetworkRequest.js index 97858e6..99251ae 100644 --- a/frontend/sdk/NetworkRequest.js +++ b/frontend/sdk/NetworkRequest.js @@ -141,6 +141,32 @@ export class NetworkRequest extends Common.Object { this._blockedResponseCookies = []; } + /** + * @return {string} + */ + get originalCode() { + return this._originalCode || ''; + } + + /** + * @param {string} x + */ + setOriginalCode(source) { + this._originalCode = source; + } + /** + * @param {number} x + */ + setTrueReceived() { + this._hasReceived = true; + } + /** + * @param {number} x + */ + get trueReceived() { + return this._hasReceived || false; + } + /** * @param {!NetworkRequest} other * @return {number} diff --git a/frontend/sdk/ResourceTreeModel.js b/frontend/sdk/ResourceTreeModel.js new file mode 100644 index 0000000..7173e74 --- /dev/null +++ b/frontend/sdk/ResourceTreeModel.js @@ -0,0 +1,1111 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import {DOMModel} from './DOMModel.js'; +import {Events as NetworkManagerEvents, NetworkManager} from './NetworkManager.js'; +import {NetworkRequest} from './NetworkRequest.js'; // eslint-disable-line no-unused-vars +import {Resource} from './Resource.js'; +import {ExecutionContext, RuntimeModel} from './RuntimeModel.js'; +import {Capability, SDKModel, Target} from './SDKModel.js'; // eslint-disable-line no-unused-vars +import {SecurityOriginManager} from './SecurityOriginManager.js'; + +export class ResourceTreeModel extends SDKModel { + /** + * @param {!Target} target + */ + constructor(target) { + super(target); + + const networkManager = target.model(NetworkManager); + if (networkManager) { + networkManager.addEventListener(NetworkManagerEvents.RequestFinished, this._onRequestFinished, this); + networkManager.addEventListener(NetworkManagerEvents.RequestUpdateDropped, this._onRequestUpdateDropped, this); + } + this._agent = target.pageAgent(); + this._agent.enable(); + this._securityOriginManager = target.model(SecurityOriginManager); + + target.registerPageDispatcher(new PageDispatcher(this)); + + /** @type {!Map} */ + this._frames = new Map(); + this._cachedResourcesProcessed = false; + this._pendingReloadOptions = null; + this._reloadSuspensionCount = 0; + this._isInterstitialShowing = false; + /** @type {?ResourceTreeFrame} */ + this.mainFrame = null; + this._processCachedResources(); + } + + /** + * @param {!NetworkRequest} request + * @return {?ResourceTreeFrame} + */ + static frameForRequest(request) { + const networkManager = NetworkManager.forRequest(request); + const resourceTreeModel = networkManager ? networkManager.target().model(ResourceTreeModel) : null; + if (!resourceTreeModel) { + return null; + } + return resourceTreeModel.frameForId(request.frameId); + } + + /** + * @return {!Array.} + */ + static frames() { + let result = []; + for (const resourceTreeModel of SDK.targetManager.models(ResourceTreeModel)) { + result = result.concat(resourceTreeModel._frames.valuesArray()); + } + return result; + } + + /** + * @param {string} url + * @return {?Resource} + */ + static resourceForURL(url) { + for (const resourceTreeModel of SDK.targetManager.models(ResourceTreeModel)) { + const mainFrame = resourceTreeModel.mainFrame; + const result = mainFrame ? mainFrame.resourceForURL(url) : null; + if (result) { + return result; + } + } + return null; + } + + /** + * @param {boolean=} bypassCache + * @param {string=} scriptToEvaluateOnLoad + */ + static reloadAllPages(bypassCache, scriptToEvaluateOnLoad) { + for (const resourceTreeModel of SDK.targetManager.models(ResourceTreeModel)) { + if (!resourceTreeModel.target().parentTarget()) { + resourceTreeModel.reloadPage(bypassCache, scriptToEvaluateOnLoad); + } + } + } + + /** + * @return {!DOMModel} + */ + domModel() { + return /** @type {!DOMModel} */ (this.target().model(DOMModel)); + } + + /** + * @param {?Protocol.Page.FrameResourceTree} mainFramePayload + */ + _processCachedResources(mainFramePayload) { + // todo + const getUrlDomain = str => { + let reg = /http:\/\/([^\/]+)/i; + let domain = str.match(reg); + return ((domain != null && domain.length > 0) ? domain[0] : ''); + }; + const parseUrl = key => { + let query = location.search.match(new RegExp('[\?\&]' + key + '=([^\&]*)(\&?)', 'i')); + return query ? query[1] : query; + }; + let documenturl = parseUrl('documenturl'); + if (!mainFramePayload) { + mainFramePayload = { + frame: { + id: '', + mimeType: 'text/html', + securityOrigin: getUrlDomain(documenturl), + url: documenturl, + }, + resources: [{ + url: documenturl, + type: 'Document', + mimeType: 'text/html', + lastModified: '2021', + contentSize: '20' + }] + } + } + if (mainFramePayload) { + this.dispatchEventToListeners(Events.WillLoadCachedResources); + this._addFramesRecursively(null, mainFramePayload); + this.target().setInspectedURL(mainFramePayload.frame.url); + } + this._cachedResourcesProcessed = true; + const runtimeModel = this.target().model(RuntimeModel); + if (runtimeModel) { + runtimeModel.setExecutionContextComparator(this._executionContextComparator.bind(this)); + runtimeModel.fireExecutionContextOrderChanged(); + } + this.dispatchEventToListeners(Events.CachedResourcesLoaded, this); + } + + /** + * @return {boolean} + */ + cachedResourcesLoaded() { + return this._cachedResourcesProcessed; + } + + /** + * @return {boolean} + */ + isInterstitialShowing() { + return this._isInterstitialShowing; + } + + /** + * @param {!ResourceTreeFrame} frame + * @param {boolean=} aboutToNavigate + */ + _addFrame(frame, aboutToNavigate) { + this._frames.set(frame.id, frame); + if (frame.isMainFrame()) { + this.mainFrame = frame; + } + this.dispatchEventToListeners(Events.FrameAdded, frame); + this._updateSecurityOrigins(); + } + + /** + * @param {!Protocol.Page.FrameId} frameId + * @param {?Protocol.Page.FrameId} parentFrameId + * @param {!Protocol.Runtime.StackTrace=} stackTrace + * @return {?ResourceTreeFrame} + */ + _frameAttached(frameId, parentFrameId, stackTrace) { + const parentFrame = parentFrameId ? (this._frames.get(parentFrameId) || null) : null; + // Do nothing unless cached resource tree is processed - it will overwrite everything. + if (!this._cachedResourcesProcessed && parentFrame) { + return null; + } + if (this._frames.has(frameId)) { + return null; + } + + const frame = new ResourceTreeFrame(this, parentFrame, frameId, null, stackTrace || null); + if (parentFrameId && !parentFrame) { + frame._crossTargetParentFrameId = parentFrameId; + } + if (frame.isMainFrame() && this.mainFrame) { + // Navigation to the new backend process. + this._frameDetached(this.mainFrame.id); + } + this._addFrame(frame, true); + return frame; + } + + /** + * @param {!Protocol.Page.Frame} framePayload + */ + _frameNavigated(framePayload) { + const parentFrame = framePayload.parentId ? (this._frames.get(framePayload.parentId) || null) : null; + // Do nothing unless cached resource tree is processed - it will overwrite everything. + if (!this._cachedResourcesProcessed && parentFrame) { + return; + } + let frame = this._frames.get(framePayload.id); + if (!frame) { + // Simulate missed "frameAttached" for a main frame navigation to the new backend process. + frame = this._frameAttached(framePayload.id, framePayload.parentId || ''); + console.assert(frame); + } + + this.dispatchEventToListeners(Events.FrameWillNavigate, frame); + frame._navigate(framePayload); + this.dispatchEventToListeners(Events.FrameNavigated, frame); + + if (frame.isMainFrame()) { + this.dispatchEventToListeners(Events.MainFrameNavigated, frame); + } + + // Fill frame with retained resources (the ones loaded using new loader). + const resources = frame.resources(); + for (let i = 0; i < resources.length; ++i) { + this.dispatchEventToListeners(Events.ResourceAdded, resources[i]); + } + + if (frame.isMainFrame()) { + this.target().setInspectedURL(frame.url); + } + this._updateSecurityOrigins(); + } + + /** + * @param {!Protocol.Page.FrameId} frameId + */ + _frameDetached(frameId) { + // Do nothing unless cached resource tree is processed - it will overwrite everything. + if (!this._cachedResourcesProcessed) { + return; + } + + const frame = this._frames.get(frameId); + if (!frame) { + return; + } + + if (frame.parentFrame) { + frame.parentFrame._removeChildFrame(frame); + } else { + frame._remove(); + } + this._updateSecurityOrigins(); + } + + /** + * @param {!Common.Event} event + */ + _onRequestFinished(event) { + if (!this._cachedResourcesProcessed) { + return; + } + + const request = /** @type {!NetworkRequest} */ (event.data); + if (request.failed || request.resourceType() === Common.resourceTypes.XHR) { + return; + } + + const frame = this._frames.get(request.frameId); + if (frame) { + frame._addRequest(request); + } + } + + /** + * @param {!Common.Event} event + */ + _onRequestUpdateDropped(event) { + if (!this._cachedResourcesProcessed) { + return; + } + + const frameId = event.data.frameId; + const frame = this._frames.get(frameId); + if (!frame) { + return; + } + + const url = event.data.url; + if (frame._resourcesMap[url]) { + return; + } + + const resource = new Resource( + this, null, url, frame.url, frameId, event.data.loaderId, Common.resourceTypes[event.data.resourceType], + event.data.mimeType, event.data.lastModified, null); + frame.addResource(resource); + } + + /** + * @param {!Protocol.Page.FrameId} frameId + * @return {!ResourceTreeFrame} + */ + frameForId(frameId) { + return this._frames.get(frameId); + } + + /** + * @param {function(!Resource)} callback + * @return {boolean} + */ + forAllResources(callback) { + if (this.mainFrame) { + return this.mainFrame._callForFrameResources(callback); + } + return false; + } + + /** + * @return {!Array} + */ + frames() { + return this._frames.valuesArray(); + } + + /** + * @param {string} url + * @return {?Resource} + */ + resourceForURL(url) { + // Workers call into this with no frames available. + return this.mainFrame ? this.mainFrame.resourceForURL(url) : null; + } + + /** + * @param {?ResourceTreeFrame} parentFrame + * @param {!Protocol.Page.FrameResourceTree} frameTreePayload + */ + _addFramesRecursively(parentFrame, frameTreePayload) { + const framePayload = frameTreePayload.frame; + const frame = new ResourceTreeFrame(this, parentFrame, framePayload.id, framePayload, null); + if (!parentFrame && framePayload.parentId) { + frame._crossTargetParentFrameId = framePayload.parentId; + } + this._addFrame(frame); + + for (let i = 0; frameTreePayload.childFrames && i < frameTreePayload.childFrames.length; ++i) { + this._addFramesRecursively(frame, frameTreePayload.childFrames[i]); + } + + for (let i = 0; i < frameTreePayload.resources.length; ++i) { + const subresource = frameTreePayload.resources[i]; + const resource = this._createResourceFromFramePayload( + framePayload, subresource.url, Common.resourceTypes[subresource.type], subresource.mimeType, + subresource.lastModified || null, subresource.contentSize || null); + frame.addResource(resource); + } + + if (!frame._resourcesMap[framePayload.url]) { + const frameResource = this._createResourceFromFramePayload( + framePayload, framePayload.url, Common.resourceTypes.Document, framePayload.mimeType, null, null); + frame.addResource(frameResource); + } + } + + /** + * @param {!Protocol.Page.Frame} frame + * @param {string} url + * @param {!Common.ResourceType} type + * @param {string} mimeType + * @param {?number} lastModifiedTime + * @param {?number} contentSize + * @return {!Resource} + */ + _createResourceFromFramePayload(frame, url, type, mimeType, lastModifiedTime, contentSize) { + const lastModified = typeof lastModifiedTime === 'number' ? new Date(lastModifiedTime * 1000) : null; + return new Resource( + this, null, url, frame.url, frame.id, frame.loaderId, type, mimeType, lastModified, contentSize); + } + + suspendReload() { + this._reloadSuspensionCount++; + } + + resumeReload() { + this._reloadSuspensionCount--; + console.assert(this._reloadSuspensionCount >= 0, 'Unbalanced call to ResourceTreeModel.resumeReload()'); + if (!this._reloadSuspensionCount && this._pendingReloadOptions) { + this.reloadPage.apply(this, this._pendingReloadOptions); + } + } + + /** + * @param {boolean=} bypassCache + * @param {string=} scriptToEvaluateOnLoad + */ + reloadPage(bypassCache, scriptToEvaluateOnLoad) { + // Only dispatch PageReloadRequested upon first reload request to simplify client logic. + if (!this._pendingReloadOptions) { + this.dispatchEventToListeners(Events.PageReloadRequested, this); + } + if (this._reloadSuspensionCount) { + this._pendingReloadOptions = [bypassCache, scriptToEvaluateOnLoad]; + return; + } + this._pendingReloadOptions = null; + this.dispatchEventToListeners(Events.WillReloadPage); + this._agent.reload(bypassCache, scriptToEvaluateOnLoad); + } + + /** + * @param {string} url + * @return {!Promise} + */ + navigate(url) { + return this._agent.navigate(url); + } + + /** + * @return {!Promise}>} + */ + async navigationHistory() { + const response = await this._agent.invoke_getNavigationHistory({}); + if (response[Protocol.Error]) { + return null; + } + return {currentIndex: response.currentIndex, entries: response.entries}; + } + + /** + * @param {!Protocol.Page.NavigationEntry} entry + */ + navigateToHistoryEntry(entry) { + this._agent.navigateToHistoryEntry(entry.id); + } + + /** + * @return {!Promise<{url: string, data: ?string, errors: !Array}>} + */ + async fetchAppManifest() { + const response = await this._agent.invoke_getAppManifest({}); + if (response[Protocol.Error]) { + return {url: response.url, data: null, errors: []}; + } + return {url: response.url, data: response.data || null, errors: response.errors}; + } + + /** + * @return {!Promise>} + */ + async getInstallabilityErrors() { + const response = await this._agent.invoke_getInstallabilityErrors({}); + return response.errors || []; + } + + /** + * @return {!Promise<{primaryIcon: ?string}>} + */ + async getManifestIcons() { + const response = await this._agent.invoke_getManifestIcons({}); + return {primaryIcon: response.primaryIcon || null}; + } + + /** + * @param {!ExecutionContext} a + * @param {!SDK.ExecutionContext} b + * @return {number} + */ + _executionContextComparator(a, b) { + /** + * @param {!ResourceTreeFrame} frame + * @return {!Array} + */ + function framePath(frame) { + let currentFrame = frame; + const parents = []; + while (currentFrame) { + parents.push(currentFrame); + currentFrame = currentFrame.parentFrame; + } + return parents.reverse(); + } + + if (a.target() !== b.target()) { + return ExecutionContext.comparator(a, b); + } + + const framesA = a.frameId ? framePath(this.frameForId(a.frameId)) : []; + const framesB = b.frameId ? framePath(this.frameForId(b.frameId)) : []; + let frameA; + let frameB; + for (let i = 0;; i++) { + if (!framesA[i] || !framesB[i] || (framesA[i] !== framesB[i])) { + frameA = framesA[i]; + frameB = framesB[i]; + break; + } + } + if (!frameA && frameB) { + return -1; + } + + if (!frameB && frameA) { + return 1; + } + + if (frameA && frameB) { + return frameA.id.localeCompare(frameB.id); + } + + return ExecutionContext.comparator(a, b); + } + + /** + * @return {!SDK.ResourceTreeModel.SecurityOriginData} + */ + _getSecurityOriginData() { + /** @type {!Set} */ + const securityOrigins = new Set(); + + let mainSecurityOrigin = null; + let unreachableMainSecurityOrigin = null; + for (const frame of this._frames.values()) { + const origin = frame.securityOrigin; + if (!origin) { + continue; + } + + securityOrigins.add(origin); + if (frame.isMainFrame()) { + mainSecurityOrigin = origin; + if (frame.unreachableUrl()) { + const unreachableParsed = new Common.ParsedURL(frame.unreachableUrl()); + unreachableMainSecurityOrigin = unreachableParsed.securityOrigin(); + } + } + } + return { + securityOrigins: securityOrigins, + mainSecurityOrigin: mainSecurityOrigin, + unreachableMainSecurityOrigin: unreachableMainSecurityOrigin + }; + } + + _updateSecurityOrigins() { + const data = this._getSecurityOriginData(); + this._securityOriginManager.setMainSecurityOrigin( + data.mainSecurityOrigin || '', data.unreachableMainSecurityOrigin || ''); + this._securityOriginManager.updateSecurityOrigins(data.securityOrigins); + } + + /** + * @return {?string} + */ + getMainSecurityOrigin() { + const data = this._getSecurityOriginData(); + return data.mainSecurityOrigin || data.unreachableMainSecurityOrigin; + } +} + +/** @enum {symbol} */ +export const Events = { + FrameAdded: Symbol('FrameAdded'), + FrameNavigated: Symbol('FrameNavigated'), + FrameDetached: Symbol('FrameDetached'), + FrameResized: Symbol('FrameResized'), + FrameWillNavigate: Symbol('FrameWillNavigate'), + MainFrameNavigated: Symbol('MainFrameNavigated'), + ResourceAdded: Symbol('ResourceAdded'), + WillLoadCachedResources: Symbol('WillLoadCachedResources'), + CachedResourcesLoaded: Symbol('CachedResourcesLoaded'), + DOMContentLoaded: Symbol('DOMContentLoaded'), + LifecycleEvent: Symbol('LifecycleEvent'), + Load: Symbol('Load'), + PageReloadRequested: Symbol('PageReloadRequested'), + WillReloadPage: Symbol('WillReloadPage'), + InterstitialShown: Symbol('InterstitialShown'), + InterstitialHidden: Symbol('InterstitialHidden') +}; + +/** + * @unrestricted + */ +export class ResourceTreeFrame { + /** + * @param {!ResourceTreeModel} model + * @param {?ResourceTreeFrame} parentFrame + * @param {!Protocol.Page.FrameId} frameId + * @param {?Protocol.Page.Frame} payload + * @param {?Protocol.Runtime.StackTrace} creationStackTrace + */ + constructor(model, parentFrame, frameId, payload, creationStackTrace) { + this._model = model; + this._parentFrame = parentFrame; + this._id = frameId; + this._url = ''; + this._crossTargetParentFrameId = null; + + if (payload) { + this._loaderId = payload.loaderId; + this._name = payload.name; + this._url = payload.url; + this._securityOrigin = payload.securityOrigin; + this._mimeType = payload.mimeType; + this._unreachableUrl = payload.unreachableUrl || ''; + } + + this._creationStackTrace = creationStackTrace; + + /** + * @type {!Array.} + */ + this._childFrames = []; + + /** + * @type {!Object.} + */ + this._resourcesMap = {}; + + if (this._parentFrame) { + this._parentFrame._childFrames.push(this); + } + } + + + /** + * @param {!Protocol.Page.Frame} framePayload + */ + _navigate(framePayload) { + this._loaderId = framePayload.loaderId; + this._name = framePayload.name; + this._url = framePayload.url; + this._securityOrigin = framePayload.securityOrigin; + this._mimeType = framePayload.mimeType; + this._unreachableUrl = framePayload.unreachableUrl || ''; + const mainResource = this._resourcesMap[this._url]; + this._resourcesMap = {}; + this._removeChildFrames(); + if (mainResource && mainResource.loaderId === this._loaderId) { + this.addResource(mainResource); + } + } + + /** + * @return {!ResourceTreeModel} + */ + resourceTreeModel() { + return this._model; + } + + /** + * @return {string} + */ + get id() { + return this._id; + } + + /** + * @return {string} + */ + get name() { + return this._name || ''; + } + + /** + * @return {string} + */ + get url() { + return this._url; + } + + /** + * @return {string} + */ + get securityOrigin() { + return this._securityOrigin; + } + + /** + * @return {string} + */ + unreachableUrl() { + return this._unreachableUrl; + } + + /** + * @return {string} + */ + get loaderId() { + return this._loaderId; + } + + /** + * @return {?ResourceTreeFrame} + */ + get parentFrame() { + return this._parentFrame; + } + + /** + * @return {!Array.} + */ + get childFrames() { + return this._childFrames; + } + + /** + * @return {?ResourceTreeFrame} + */ + crossTargetParentFrame() { + if (!this._crossTargetParentFrameId) { + return null; + } + if (!this._model.target().parentTarget()) { + return null; + } + const parentModel = this._model.target().parentTarget().model(ResourceTreeModel); + if (!parentModel) { + return null; + } + // Note that parent model has already processed cached resources: + // - when parent target was created, we issued getResourceTree call; + // - strictly after we issued setAutoAttach call; + // - both of them were handled in renderer in the same order; + // - cached resource tree got processed on parent model; + // - child target was created as a result of setAutoAttach call. + return parentModel._frames.get(this._crossTargetParentFrameId) || null; + } + + /** + * @param {function(!Protocol.Runtime.CallFrame):boolean} searchFn + * @return {?Protocol.Runtime.CallFrame} + */ + findCreationCallFrame(searchFn) { + let stackTrace = this._creationStackTrace; + while (stackTrace) { + const foundEntry = stackTrace.callFrames.find(searchFn); + if (foundEntry) { + return foundEntry; + } + stackTrace = this.parent; + } + return null; + } + + /** + * @return {boolean} + */ + isMainFrame() { + return !this._parentFrame; + } + + isTopFrame() { + return !this._parentFrame && !this._crossTargetParentFrameId; + } + + /** + * @return {!Resource} + */ + get mainResource() { + return this._resourcesMap[this._url]; + } + + /** + * @param {!ResourceTreeFrame} frame + */ + _removeChildFrame(frame) { + this._childFrames.remove(frame); + frame._remove(); + } + + _removeChildFrames() { + const frames = this._childFrames; + this._childFrames = []; + for (let i = 0; i < frames.length; ++i) { + frames[i]._remove(); + } + } + + _remove() { + this._removeChildFrames(); + this._model._frames.delete(this.id); + this._model.dispatchEventToListeners(Events.FrameDetached, this); + } + + /** + * @param {!Resource} resource + */ + addResource(resource) { + if (this._resourcesMap[resource.url] === resource) { + // Already in the tree, we just got an extra update. + return; + } + this._resourcesMap[resource.url] = resource; + this._model.dispatchEventToListeners(Events.ResourceAdded, resource); + } + + /** + * @param {!NetworkRequest} request + */ + _addRequest(request) { + let resource = this._resourcesMap[request.url()]; + if (resource && resource.request === request) { + // Already in the tree, we just got an extra update. + return; + } + resource = new Resource( + this._model, request, request.url(), request.documentURL, request.frameId, request.loaderId, + request.resourceType(), request.mimeType, null, null); + this._resourcesMap[resource.url] = resource; + this._model.dispatchEventToListeners(Events.ResourceAdded, resource); + } + + /** + * @return {!Array.} + */ + resources() { + const result = []; + for (const url in this._resourcesMap) { + result.push(this._resourcesMap[url]); + } + return result; + } + + /** + * @param {string} url + * @return {?Resource} + */ + resourceForURL(url) { + let resource = this._resourcesMap[url] || null; + if (resource) { + return resource; + } + for (let i = 0; !resource && i < this._childFrames.length; ++i) { + resource = this._childFrames[i].resourceForURL(url); + } + return resource; + } + + /** + * @param {function(!Resource)} callback + * @return {boolean} + */ + _callForFrameResources(callback) { + for (const url in this._resourcesMap) { + if (callback(this._resourcesMap[url])) { + return true; + } + } + + for (let i = 0; i < this._childFrames.length; ++i) { + if (this._childFrames[i]._callForFrameResources(callback)) { + return true; + } + } + return false; + } + + /** + * @return {string} + */ + displayName() { + if (this.isTopFrame()) { + return Common.UIString('top'); + } + const subtitle = new Common.ParsedURL(this._url).displayName; + if (subtitle) { + if (!this._name) { + return subtitle; + } + return this._name + ' (' + subtitle + ')'; + } + return Common.UIString('