--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -69,16 +69,18 @@ loader.lazyRequireGetter(this, "HUDServi
loader.lazyRequireGetter(this, "viewSource",
"devtools/client/shared/view-source");
loader.lazyRequireGetter(this, "StyleSheetsFront",
"devtools/shared/fronts/stylesheets", true);
loader.lazyRequireGetter(this, "buildHarLog",
"devtools/client/netmonitor/src/har/har-builder-utils", true);
loader.lazyRequireGetter(this, "getKnownDeviceFront",
"devtools/shared/fronts/device", true);
+loader.lazyRequireGetter(this, "NetMonitorAPI",
+ "devtools/client/netmonitor/src/api", true);
loader.lazyGetter(this, "domNodeConstants", () => {
return require("devtools/shared/dom-node-constants");
});
loader.lazyGetter(this, "registerHarOverlay", () => {
return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
});
@@ -111,24 +113,22 @@ function Toolbox(target, selectedTool, h
this._toolPanels = new Map();
this._inspectorExtensionSidebars = new Map();
this._telemetry = new Telemetry();
this._initInspector = null;
this._inspector = null;
this._styleSheets = null;
+ this._netMonitorAPI = null;
// Map of frames (id => frame-info) and currently selected frame id.
this.frameMap = new Map();
this.selectedFrameId = null;
- // List of listeners for `devtools.network.onRequestFinished` WebExt API
- this._requestFinishedListeners = new Set();
-
this._toolRegistered = this._toolRegistered.bind(this);
this._toolUnregistered = this._toolUnregistered.bind(this);
this._onWillNavigate = this._onWillNavigate.bind(this);
this._refreshHostTitle = this._refreshHostTitle.bind(this);
this.toggleNoAutohide = this.toggleNoAutohide.bind(this);
this.showFramesMenu = this.showFramesMenu.bind(this);
this.handleKeyDownOnFramesButton = this.handleKeyDownOnFramesButton.bind(this);
this.showFramesMenuOnKeyDown = this.showFramesMenuOnKeyDown.bind(this);
@@ -2668,17 +2668,16 @@ Toolbox.prototype = {
this._applyServiceWorkersTestingSettings);
this._lastFocusedElement = null;
if (this._sourceMapURLService) {
this._sourceMapURLService.destroy();
this._sourceMapURLService = null;
}
-
if (this._sourceMapService) {
this._sourceMapService.stopSourceMapWorker();
this._sourceMapService = null;
}
if (this.webconsolePanel) {
this._saveSplitConsoleHeight();
this.webconsolePanel.removeEventListener("resize",
@@ -2767,16 +2766,21 @@ Toolbox.prototype = {
this._telemetry.destroy();
// Finish all outstanding tasks (which means finish destroying panels and
// then destroying the host, successfully or not) before destroying the
// target.
deferred.resolve(settleAll(outstanding)
.catch(console.error)
.then(() => {
+ let api = this._netMonitorAPI;
+ this._netMonitorAPI = null;
+ return api ? api.destroy() : null;
+ }, console.error)
+ .then(() => {
this._removeHostListeners();
// `location` may already be 'invalid' if the toolbox document is
// already in process of destruction. Otherwise if it is still
// around, ensure releasing toolbox document and triggering cleanup
// thanks to unload event. We do that precisely here, before
// nullifying the target as various cleanup code depends on the
// target attribute to be still
@@ -3026,84 +3030,95 @@ Toolbox.prototype = {
*/
viewSource: function(sourceURL, sourceLine) {
return viewSource.viewSource(this, sourceURL, sourceLine);
},
// Support for WebExtensions API (`devtools.network.*`)
/**
+ * Return Netmonitor API object. This object offers Network monitor
+ * public API that can be consumed by other panels or WE API.
+ */
+ getNetMonitorAPI: async function() {
+ let netPanel = this.getPanel("netmonitor");
+
+ // Return Net panel if it exists.
+ if (netPanel) {
+ return netPanel.panelWin.Netmonitor.api;
+ }
+
+ if (this._netMonitorAPI) {
+ return this._netMonitorAPI;
+ }
+
+ // Create and initialize Network monitor API object.
+ // This object is only connected to the backend - not to the UI.
+ this._netMonitorAPI = new NetMonitorAPI();
+ await this._netMonitorAPI.connect(this);
+
+ return this._netMonitorAPI;
+ },
+
+ /**
* Returns data (HAR) collected by the Network panel.
*/
getHARFromNetMonitor: async function() {
- let netPanel = this.getPanel("netmonitor");
-
- // The panel doesn't have to exist (it must be selected
- // by the user at least once to be created).
- // Return default empty HAR log in such case.
- if (!netPanel) {
- let har = await buildHarLog(Services.appinfo);
- return har.log;
- }
-
- // Use Netmonitor object to get the current HAR log.
- let har = await netPanel.panelWin.Netmonitor.getHar();
+ let netMonitor = await this.getNetMonitorAPI();
+ let har = await netMonitor.getHar();
+
+ // Return default empty HAR file if needed.
+ har = har || buildHarLog(Services.appinfo);
// Return the log directly to be compatible with
// Chrome WebExtension API.
return har.log;
},
/**
* Add listener for `onRequestFinished` events.
*
* @param {Object} listener
* The listener to be called it's expected to be
* a function that takes ({harEntry, requestId})
* as first argument.
*/
- addRequestFinishedListener: function(listener) {
- // Log console message informing the extension developer
- // that the Network panel needs to be selected at least
- // once in order to receive `onRequestFinished` events.
- let message = "The Network panel needs to be selected at least" +
- " once in order to receive 'onRequestFinished' events.";
- this.target.logWarningInPage(message, "har");
-
- // Add the listener into internal list.
- this._requestFinishedListeners.add(listener);
+ addRequestFinishedListener: async function(listener) {
+ let netMonitor = await this.getNetMonitorAPI();
+ netMonitor.addRequestFinishedListener(listener);
},
- removeRequestFinishedListener: function(listener) {
- this._requestFinishedListeners.delete(listener);
- },
-
- getRequestFinishedListeners: function() {
- return this._requestFinishedListeners;
+ removeRequestFinishedListener: async function(listener) {
+ let netMonitor = await this.getNetMonitorAPI();
+ netMonitor.removeRequestFinishedListener(listener);
+
+ // Destroy Network monitor API object if the following is true:
+ // 1) there is no listener
+ // 2) the Net panel doesn't exist/use the API object (if the panel
+ // exists it's also responsible for destroying it,
+ // see `NetMonitorPanel.open` for more details)
+ let netPanel = this.getPanel("netmonitor");
+ let hasListeners = netMonitor.hasRequestFinishedListeners();
+ if (this._netMonitorAPI && !hasListeners && !netPanel) {
+ this._netMonitorAPI.destroy();
+ this._netMonitorAPI = null;
+ }
},
/**
* Used to lazily fetch HTTP response content within
* `onRequestFinished` event listener.
*
* @param {String} requestId
* Id of the request for which the response content
* should be fetched.
*/
- fetchResponseContent: function(requestId) {
- let netPanel = this.getPanel("netmonitor");
-
- // The panel doesn't have to exist (it must be selected
- // by the user at least once to be created).
- // Return undefined content in such case.
- if (!netPanel) {
- return Promise.resolve({content: {}});
- }
-
- return netPanel.panelWin.Netmonitor.fetchResponseContent(requestId);
+ fetchResponseContent: async function(requestId) {
+ let netMonitor = await this.getNetMonitorAPI();
+ return netMonitor.fetchResponseContent(requestId);
},
// Support management of installed WebExtensions that provide a devtools_page.
/**
* List the subset of the active WebExtensions which have a devtools_page (used by
* toolbox-options.js to create the list of the tools provided by the enabled
* WebExtensions).
--- a/devtools/client/netmonitor/initializer.js
+++ b/devtools/client/netmonitor/initializer.js
@@ -1,205 +1,72 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* exported initialize */
"use strict";
/**
* This script is the entry point of Network monitor panel.
* See README.md for more information.
*/
const { BrowserLoader } = ChromeUtils.import(
"resource://devtools/client/shared/browser-loader.js", {});
const require = window.windowRequire = BrowserLoader({
baseURI: "resource://devtools/client/netmonitor/",
window,
}).require;
+const { NetMonitorAPI } = require("./src/api");
+const { NetMonitorApp } = require("./src/app");
const EventEmitter = require("devtools/shared/event-emitter");
-const { createFactory } = require("devtools/client/shared/vendor/react");
-const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
-const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
-const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
-const { Connector } = require("./src/connector/index");
-const { configureStore } = require("./src/create-store");
-const App = createFactory(require("./src/components/App"));
-const { EVENTS } = require("./src/constants");
-const {
- getDisplayedRequestById,
- getSortedRequests
-} = require("./src/selectors/index");
// Inject EventEmitter into global window.
EventEmitter.decorate(window);
-// Configure store/state object.
-let connector = new Connector();
-const store = configureStore(connector);
-const actions = bindActionCreators(require("./src/actions/index"), store.dispatch);
+/**
+ * This is the initialization point for the Network monitor.
+ *
+ * @param {Object} api Allows reusing existing API object.
+ */
+function initialize(api) {
+ const app = new NetMonitorApp(api);
-// Inject to global window for testing
-window.store = store;
-window.connector = connector;
-window.actions = actions;
+ // Inject to global window for testing
+ window.Netmonitor = app;
+ window.store = app.api.store;
+ window.connector = app.api.connector;
+ window.actions = app.api.actions;
+
+ return app;
+}
/**
- * Global Netmonitor object in this panel. This object can be consumed
- * by other panels (e.g. Console is using inspectRequest), by the
- * Launchpad (bootstrap), WebExtension API (getHAR), etc.
+ * The following code is used to open Network monitor in a tab.
+ * Like the Launchpad, but without Launchpad.
+ *
+ * For example:
+ * chrome://devtools/content/netmonitor/index.html?type=process
+ * loads the netmonitor for the parent process, exactly like the
+ * one in the browser toolbox
+ *
+ * It's also possible to connect to a tab.
+ * 1) go in about:debugging
+ * 2) In menu Tabs, click on a Debug button for particular tab
+ *
+ * This will open an about:devtools-toolbox url, from which you can
+ * take type and id query parameters and reuse them for the chrome url
+ * of the netmonitor
+ *
+ * chrome://devtools/content/netmonitor/index.html?type=tab&id=1234 URLs
+ * where 1234 is the tab id, you can retrieve from about:debugging#tabs links.
+ * Simply copy the id from about:devtools-toolbox?type=tab&id=1234 URLs.
*/
-window.Netmonitor = {
- bootstrap({ toolbox, panel }) {
- this.mount = document.querySelector("#mount");
- this.toolbox = toolbox;
-
- const connection = {
- tabConnection: {
- tabTarget: toolbox.target,
- },
- toolbox,
- panel,
- };
-
- const openLink = (link) => {
- let parentDoc = toolbox.doc;
- let iframe = parentDoc.getElementById("toolbox-panel-iframe-netmonitor");
- let top = iframe.ownerDocument.defaultView.top;
- top.openWebLinkIn(link, "tab");
- };
-
- const openSplitConsole = (err) => {
- toolbox.openSplitConsole().then(() => {
- toolbox.target.logErrorInPage(err, "har");
- });
- };
-
- this.onRequestAdded = this.onRequestAdded.bind(this);
- window.on(EVENTS.REQUEST_ADDED, this.onRequestAdded);
-
- // Render the root Application component.
- const sourceMapService = toolbox.sourceMapURLService;
- const app = App({
- actions,
- connector,
- openLink,
- openSplitConsole,
- sourceMapService
- });
- render(Provider({ store }, app), this.mount);
-
- // Connect to the Firefox backend by default.
- return connector.connectFirefox(connection, actions, store.getState);
- },
-
- destroy() {
- unmountComponentAtNode(this.mount);
- window.off(EVENTS.REQUEST_ADDED, this.onRequestAdded);
- return connector.disconnect();
- },
-
- // Support for WebExtensions API
-
- /**
- * Support for `devtools.network.getHAR` (get collected data as HAR)
- */
- getHar() {
- let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
- let state = store.getState();
-
- let options = {
- connector,
- items: getSortedRequests(state),
- // Always generate HAR log even if there are no requests.
- forceExport: true,
- };
-
- return HarExporter.getHar(options);
- },
-
- /**
- * Support for `devtools.network.onRequestFinished`. A hook for
- * every finished HTTP request used by WebExtensions API.
- */
- onRequestAdded(requestId) {
- let listeners = this.toolbox.getRequestFinishedListeners();
- if (!listeners.size) {
- return;
- }
-
- let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
- let options = {
- connector,
- includeResponseBodies: false,
- items: [getDisplayedRequestById(store.getState(), requestId)],
- };
-
- // Build HAR for specified request only.
- HarExporter.getHar(options).then(har => {
- let harEntry = har.log.entries[0];
- delete harEntry.pageref;
- listeners.forEach(listener => listener({
- harEntry,
- requestId,
- }));
- });
- },
-
- /**
- * Support for `Request.getContent` WebExt API (lazy loading response body)
- */
- fetchResponseContent(requestId) {
- return connector.requestData(requestId, "responseContent");
- },
-
- /**
- * Selects the specified request in the waterfall and opens the details view.
- * This is a firefox toolbox specific API, which providing an ability to inspect
- * a network request directly from other internal toolbox panel.
- *
- * @param {string} requestId The actor ID of the request to inspect.
- * @return {object} A promise resolved once the task finishes.
- */
- inspectRequest(requestId) {
- // Look for the request in the existing ones or wait for it to appear, if
- // the network monitor is still loading.
- return new Promise((resolve) => {
- let request = null;
- let inspector = () => {
- request = getDisplayedRequestById(store.getState(), requestId);
- if (!request) {
- // Reset filters so that the request is visible.
- actions.toggleRequestFilterType("all");
- request = getDisplayedRequestById(store.getState(), requestId);
- }
-
- // If the request was found, select it. Otherwise this function will be
- // called again once new requests arrive.
- if (request) {
- window.off(EVENTS.REQUEST_ADDED, inspector);
- actions.selectRequest(request.id);
- resolve();
- }
- };
-
- inspector();
-
- if (!request) {
- window.on(EVENTS.REQUEST_ADDED, inspector);
- }
- });
- }
-};
-
-// Implement support for:
-// chrome://devtools/content/netmonitor/index.html?type=tab&id=1234 URLs
-// where 1234 is the tab id, you can retrieve from about:debugging#tabs links.
-// Simply copy the id from about:devtools-toolbox?type=tab&id=1234 URLs.
// URL constructor doesn't support chrome: scheme
let href = window.location.href.replace(/chrome:/, "http://");
let url = new window.URL(href);
// If query parameters are given in a chrome tab, the inspector
// is running in standalone.
if (window.location.protocol === "chrome:" && url.search.length > 1) {
@@ -217,14 +84,20 @@ if (window.location.protocol === "chrome
// Create a fake toolbox object
let toolbox = {
target,
viewSourceInDebugger() {
throw new Error("toolbox.viewSourceInDebugger is not implement from a tab");
}
};
- window.Netmonitor.bootstrap({ toolbox });
+ let api = new NetMonitorAPI();
+ await api.connect(toolbox);
+ let app = window.initialize(api);
+ app.bootstrap({
+ toolbox,
+ document: window.document,
+ });
} catch (err) {
window.alert("Unable to start the network monitor:" + err);
}
})();
}
--- a/devtools/client/netmonitor/panel.js
+++ b/devtools/client/netmonitor/panel.js
@@ -9,22 +9,32 @@ function NetMonitorPanel(iframeWindow, t
this.toolbox = toolbox;
}
NetMonitorPanel.prototype = {
async open() {
if (!this.toolbox.target.isRemote) {
await this.toolbox.target.makeRemote();
}
- await this.panelWin.Netmonitor.bootstrap({
+
+ // Reuse an existing Network monitor API object if available.
+ // It could have been created for WE API before Net panel opens.
+ let api = await this.toolbox.getNetMonitorAPI();
+ let app = this.panelWin.initialize(api);
+
+ // Connect the application object to the UI.
+ await app.bootstrap({
toolbox: this.toolbox,
- panel: this,
+ document: this.panelWin.document,
});
+
+ // Ready to go!
this.emit("ready");
this.isReady = true;
+
return this;
},
async destroy() {
await this.panelWin.Netmonitor.destroy();
this.emit("destroyed");
return this;
},
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/api.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const { Connector } = require("./connector/index");
+const { configureStore } = require("./create-store");
+const { EVENTS } = require("./constants");
+const Actions = require("./actions/index");
+
+const {
+ getDisplayedRequestById,
+ getSortedRequests
+} = require("./selectors/index");
+
+/**
+ * API object for NetMonitor panel (like a facade). This object can be
+ * consumed by other panels, WebExtension API, etc.
+ *
+ * This object doesn't depend on the panel UI and can be created
+ * and used even if the Network panel UI doesn't exist.
+ */
+function NetMonitorAPI() {
+ EventEmitter.decorate(this);
+
+ // Connector to the backend.
+ this.connector = new Connector();
+
+ // Configure store/state object.
+ this.store = configureStore(this.connector);
+
+ // List of listeners for `devtools.network.onRequestFinished` WebExt API
+ this._requestFinishedListeners = new Set();
+
+ // Bind event handlers
+ this.onRequestAdded = this.onRequestAdded.bind(this);
+ this.actions = bindActionCreators(Actions, this.store.dispatch);
+}
+
+NetMonitorAPI.prototype = {
+ async connect(toolbox) {
+ // Bail out if already connected.
+ if (this.toolbox) {
+ return;
+ }
+
+ this.toolbox = toolbox;
+
+ // Register listener for new requests (utilized by WebExtension API).
+ this.on(EVENTS.REQUEST_ADDED, this.onRequestAdded);
+
+ // Initialize connection to the backend. Pass `this` as the owner,
+ // so this object can receive all emitted events.
+ const connection = {
+ tabConnection: {
+ tabTarget: toolbox.target,
+ },
+ toolbox,
+ owner: this,
+ };
+
+ await this.connectBackend(this.connector, connection, this.actions,
+ this.store.getState);
+ },
+
+ /**
+ * Clean up (unmount from DOM, remove listeners, disconnect).
+ */
+ async destroy() {
+ this.off(EVENTS.REQUEST_ADDED, this.onRequestAdded);
+
+ await this.connector.disconnect();
+
+ if (this.harExportConnector) {
+ await this.harExportConnector.disconnect();
+ }
+ },
+
+ /**
+ * Connect to the Firefox backend by default.
+ *
+ * As soon as connections to different back-ends is supported
+ * this function should be responsible for picking the right API.
+ */
+ async connectBackend(connector, connection, actions, getState) {
+ // The connection might happen during Toolbox initialization
+ // so make sure the target is ready.
+ await connection.tabConnection.tabTarget.makeRemote();
+ return connector.connectFirefox(connection, actions, getState);
+ },
+
+ // HAR
+
+ /**
+ * Support for `devtools.network.getHAR` (get collected data as HAR)
+ */
+ async getHar() {
+ let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
+ let state = this.store.getState();
+
+ let options = {
+ connector: this.connector,
+ items: getSortedRequests(state),
+ };
+
+ return HarExporter.getHar(options);
+ },
+
+ /**
+ * Support for `devtools.network.onRequestFinished`. A hook for
+ * every finished HTTP request used by WebExtensions API.
+ */
+ async onRequestAdded(requestId) {
+ if (!this._requestFinishedListeners.size) {
+ return;
+ }
+
+ let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
+
+ let connector = await this.getHarExportConnector();
+ let request = getDisplayedRequestById(this.store.getState(), requestId);
+ if (!request) {
+ console.error("HAR: request not found " + requestId);
+ return;
+ }
+
+ let options = {
+ connector,
+ includeResponseBodies: false,
+ items: [request],
+ };
+
+ let har = await HarExporter.getHar(options);
+
+ // There is page so remove the page reference.
+ let harEntry = har.log.entries[0];
+ delete harEntry.pageref;
+
+ this._requestFinishedListeners.forEach(listener => listener({
+ harEntry,
+ requestId,
+ }));
+ },
+
+ /**
+ * Support for `Request.getContent` WebExt API (lazy loading response body)
+ */
+ async fetchResponseContent(requestId) {
+ return this.connector.requestData(requestId, "responseContent");
+ },
+
+ /**
+ * Add listener for `onRequestFinished` events.
+ *
+ * @param {Object} listener
+ * The listener to be called it's expected to be
+ * a function that takes ({harEntry, requestId})
+ * as first argument.
+ */
+ addRequestFinishedListener: function(listener) {
+ this._requestFinishedListeners.add(listener);
+ },
+
+ removeRequestFinishedListener: function(listener) {
+ this._requestFinishedListeners.delete(listener);
+ },
+
+ hasRequestFinishedListeners: function() {
+ return this._requestFinishedListeners.size > 0;
+ },
+
+ /**
+ * Separate connector for HAR export.
+ */
+ async getHarExportConnector() {
+ if (this.harExportConnector) {
+ return this.harExportConnector;
+ }
+
+ const connection = {
+ tabConnection: {
+ tabTarget: this.toolbox.target,
+ },
+ toolbox: this.toolbox,
+ };
+
+ this.harExportConnector = new Connector();
+ await this.connectBackend(this.harExportConnector, connection);
+ return this.harExportConnector;
+ },
+};
+
+exports.NetMonitorAPI = NetMonitorAPI;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/app.js
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const App = createFactory(require("./components/App"));
+const { EVENTS } = require("./constants");
+
+const {
+ getDisplayedRequestById,
+} = require("./selectors/index");
+
+/**
+ * Global App object for Network panel. This object depends
+ * on the UI and can't be created independently.
+ *
+ * This object can be consumed by other panels (e.g. Console
+ * is using inspectRequest), by the Launchpad (bootstrap), etc.
+ *
+ * @param {Object} api An existing API object to be reused.
+ */
+function NetMonitorApp(api) {
+ this.api = api;
+}
+
+NetMonitorApp.prototype = {
+ async bootstrap({ toolbox, document }) {
+ // Get the root element for mounting.
+ this.mount = document.querySelector("#mount");
+
+ const openLink = (link) => {
+ let parentDoc = toolbox.doc;
+ let iframe = parentDoc.getElementById("toolbox-panel-iframe-netmonitor");
+ let top = iframe.ownerDocument.defaultView.top;
+ top.openUILinkIn(link, "tab");
+ };
+
+ const openSplitConsole = (err) => {
+ toolbox.openSplitConsole().then(() => {
+ toolbox.target.logErrorInPage(err, "har");
+ });
+ };
+
+ let {
+ actions,
+ connector,
+ store,
+ } = this.api;
+
+ const sourceMapService = toolbox.sourceMapURLService;
+ const app = App({
+ actions,
+ connector,
+ openLink,
+ openSplitConsole,
+ sourceMapService
+ });
+
+ // Render the root Application component.
+ render(Provider({ store: store }, app), this.mount);
+ },
+
+ /**
+ * Clean up (unmount from DOM, remove listeners, disconnect).
+ */
+ async destroy() {
+ unmountComponentAtNode(this.mount);
+
+ // Make sure to destroy the API object. It's usually destroyed
+ // in the Toolbox destroy method, but we need it here for case
+ // where the Network panel is initialized without the toolbox
+ // and running in a tab (see initialize.js for details).
+ await this.api.destroy();
+ },
+
+ /**
+ * Selects the specified request in the waterfall and opens the details view.
+ * This is a firefox toolbox specific API, which providing an ability to inspect
+ * a network request directly from other internal toolbox panel.
+ *
+ * @param {string} requestId The actor ID of the request to inspect.
+ * @return {object} A promise resolved once the task finishes.
+ */
+ async inspectRequest(requestId) {
+ let {
+ actions,
+ store,
+ } = this.api;
+
+ // Look for the request in the existing ones or wait for it to appear,
+ // if the network monitor is still loading.
+ return new Promise((resolve) => {
+ let request = null;
+ let inspector = () => {
+ request = getDisplayedRequestById(store.getState(), requestId);
+ if (!request) {
+ // Reset filters so that the request is visible.
+ actions.toggleRequestFilterType("all");
+ request = getDisplayedRequestById(store.getState(), requestId);
+ }
+
+ // If the request was found, select it. Otherwise this function will be
+ // called again once new requests arrive.
+ if (request) {
+ this.api.off(EVENTS.REQUEST_ADDED, inspector);
+ actions.selectRequest(request.id);
+ resolve();
+ }
+ };
+
+ inspector();
+
+ if (!request) {
+ this.api.on(EVENTS.REQUEST_ADDED, inspector);
+ }
+ });
+ }
+};
+
+exports.NetMonitorApp = NetMonitorApp;
--- a/devtools/client/netmonitor/src/connector/chrome-connector.js
+++ b/devtools/client/netmonitor/src/connector/chrome-connector.js
@@ -93,9 +93,9 @@ class ChromeConnector {
// TODO : implement.
}
viewSourceInDebugger() {
// TODO : implement.
}
}
-module.exports = new ChromeConnector();
+module.exports = ChromeConnector;
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -30,56 +30,72 @@ class FirefoxConnector {
this.requestData = this.requestData.bind(this);
this.getTimingMarker = this.getTimingMarker.bind(this);
// Internals
this.getLongString = this.getLongString.bind(this);
this.getNetworkRequest = this.getNetworkRequest.bind(this);
}
+ /**
+ * Connect to the backend.
+ *
+ * @param {Object} connection object with e.g. reference to the Toolbox.
+ * @param {Object} actions (optional) is used to fire Redux actions to update store.
+ * @param {Object} getState (optional) is used to get access to the state.
+ */
async connect(connection, actions, getState) {
this.actions = actions;
this.getState = getState;
this.tabTarget = connection.tabConnection.tabTarget;
this.toolbox = connection.toolbox;
- this.panel = connection.panel;
+
+ // The owner object (NetMonitorAPI) received all events.
+ this.owner = connection.owner;
this.webConsoleClient = this.tabTarget.activeConsole;
this.dataProvider = new FirefoxDataProvider({
webConsoleClient: this.webConsoleClient,
actions: this.actions,
+ owner: this.owner,
});
await this.addListeners();
// Listener for `will-navigate` event is (un)registered outside
// of the `addListeners` and `removeListeners` methods since
// these are used to pause/resume the connector.
// Paused network panel should be automatically resumed when page
// reload, so `will-navigate` listener needs to be there all the time.
- this.tabTarget.on("will-navigate", this.willNavigate);
- this.tabTarget.on("navigate", this.navigate);
+ if (this.tabTarget) {
+ this.tabTarget.on("will-navigate", this.willNavigate);
+ this.tabTarget.on("navigate", this.navigate);
+ }
- this.displayCachedEvents();
+ // Displaying cache events is only intended for the UI panel.
+ if (this.actions) {
+ this.displayCachedEvents();
+ }
}
async disconnect() {
- this.actions.batchReset();
+ if (this.actions) {
+ this.actions.batchReset();
+ }
await this.removeListeners();
if (this.tabTarget) {
this.tabTarget.off("will-navigate");
this.tabTarget = null;
}
this.webConsoleClient = null;
this.dataProvider = null;
- this.panel = null;
}
async pause() {
await this.removeListeners();
}
async resume() {
await this.addListeners();
@@ -127,53 +143,58 @@ class FirefoxConnector {
}
}
enableActions(enable) {
this.dataProvider.enableActions(enable);
}
willNavigate() {
- if (!Services.prefs.getBoolPref("devtools.netmonitor.persistlog")) {
- this.actions.batchReset();
- this.actions.clearRequests();
- } else {
- // If the log is persistent, just clear all accumulated timing markers.
- this.actions.clearTimingMarkers();
+ if (this.actions) {
+ if (!Services.prefs.getBoolPref("devtools.netmonitor.persistlog")) {
+ this.actions.batchReset();
+ this.actions.clearRequests();
+ } else {
+ // If the log is persistent, just clear all accumulated timing markers.
+ this.actions.clearTimingMarkers();
+ }
}
// Resume is done automatically on page reload/navigation.
- let state = this.getState();
- if (!state.requests.recording) {
- this.actions.toggleRecording();
+ if (this.actions && this.getState) {
+ let state = this.getState();
+ if (!state.requests.recording) {
+ this.actions.toggleRecording();
+ }
}
}
navigate() {
if (this.dataProvider.isPayloadQueueEmpty()) {
this.onReloaded();
return;
}
let listener = () => {
if (this.dataProvider && !this.dataProvider.isPayloadQueueEmpty()) {
return;
}
- window.off(EVENTS.PAYLOAD_READY, listener);
+ this.owner.off(EVENTS.PAYLOAD_READY, listener);
// Netmonitor may already be destroyed,
// so do not try to notify the listeners
if (this.dataProvider) {
this.onReloaded();
}
};
- window.on(EVENTS.PAYLOAD_READY, listener);
+ this.owner.on(EVENTS.PAYLOAD_READY, listener);
}
onReloaded() {
- if (this.panel) {
- this.panel.emit("reloaded");
+ let panel = this.toolbox.getPanel("netmonitor");
+ if (panel) {
+ panel.emit("reloaded");
}
}
/**
* Display any network events already in the cache.
*/
displayCachedEvents() {
for (let networkInfo of this.webConsoleClient.getNetworkEvents()) {
@@ -199,30 +220,37 @@ class FirefoxConnector {
onDocLoadingMarker(marker) {
// Translate marker into event similar to newer "docEvent" event sent by the console
// actor
let event = {
name: marker.name == "document::DOMContentLoaded" ?
"dom-interactive" : "dom-complete",
time: marker.unixTime / 1000
};
- this.actions.addTimingMarker(event);
- window.emit(EVENTS.TIMELINE_EVENT, event);
+
+ if (this.actions) {
+ this.actions.addTimingMarker(event);
+ }
+
+ this.emit(EVENTS.TIMELINE_EVENT, event);
}
/**
* The "DOMContentLoaded" and "Load" events sent by the console actor.
*
* Only used by FF60+.
*
* @param {object} marker
*/
onDocEvent(event) {
- this.actions.addTimingMarker(event);
- window.emit(EVENTS.TIMELINE_EVENT, event);
+ if (this.actions) {
+ this.actions.addTimingMarker(event);
+ }
+
+ this.emit(EVENTS.TIMELINE_EVENT, event);
}
/**
* Send a HTTP request data payload
*
* @param {object} data data payload would like to sent to backend
* @param {function} callback callback will be invoked after the request finished
*/
@@ -366,14 +394,32 @@ class FirefoxConnector {
* @param {object} request network request instance
* @param {string} type NetworkEventUpdate type
*/
requestData(request, type) {
return this.dataProvider.requestData(request, type);
}
getTimingMarker(name) {
+ if (!this.getState) {
+ return -1;
+ }
+
let state = this.getState();
return getDisplayedTimingMarker(state, name);
}
+
+ /**
+ * Fire events for the owner object.
+ */
+ emit(type, data) {
+ if (this.owner) {
+ this.owner.emit(type, data);
+ }
+
+ // Consumed mainly by tests.
+ if (typeof window != "undefined") {
+ window.emit(type, data);
+ }
+ }
}
-module.exports = new FirefoxConnector();
+module.exports = FirefoxConnector;
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -13,21 +13,29 @@ const { fetchHeaders } = require("../uti
* This object is responsible for fetching additional HTTP
* data from the backend over RDP protocol.
*
* The object also keeps track of RDP requests in-progress,
* so it's possible to determine whether all has been fetched
* or not.
*/
class FirefoxDataProvider {
- constructor({webConsoleClient, actions}) {
+ /**
+ * Constructor for data provider
+ *
+ * @param {Object} webConcoleClient represents the client object for Console actor.
+ * @param {Object} actions set of actions fired during data fetching process
+ * @params {Object} owner all events are fired on this object
+ */
+ constructor({webConsoleClient, actions, owner}) {
// Options
this.webConsoleClient = webConsoleClient;
- this.actions = actions;
+ this.actions = actions || {};
this.actionsEnabled = true;
+ this.owner = owner;
// Internal properties
this.payloadQueue = new Map();
// Map[key string => Promise] used by `requestData` to prevent requesting the same
// request data twice.
this.lazyRequestData = new Map();
@@ -79,17 +87,17 @@ class FirefoxDataProvider {
// FF59+ supports fetching the traces lazily via requestData.
stacktrace: cause.stacktrace,
fromCache,
fromServiceWorker,
}, true);
}
- emit(EVENTS.REQUEST_ADDED, id);
+ this.emit(EVENTS.REQUEST_ADDED, id);
}
/**
* Update a network request if it already exists in application state.
*
* @param {string} id request id
* @param {object} data data payload will be updated to application state
*/
@@ -309,17 +317,17 @@ class FirefoxDataProvider {
fromCache,
fromServiceWorker,
isXHR,
method,
startedDateTime,
url,
});
- emit(EVENTS.NETWORK_EVENT, actor);
+ this.emit(EVENTS.NETWORK_EVENT, actor);
}
/**
* The "networkEventUpdate" message type handler.
*
* @param {object} packet the message received from the server.
* @param {object} networkInfo the network request information.
*/
@@ -336,17 +344,17 @@ class FirefoxDataProvider {
this.pushRequestToQueue(actor, {
httpVersion: networkInfo.response.httpVersion,
remoteAddress: networkInfo.response.remoteAddress,
remotePort: networkInfo.response.remotePort,
status: networkInfo.response.status,
statusText: networkInfo.response.statusText,
headersSize: networkInfo.response.headersSize
});
- emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
+ this.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
break;
case "responseContent":
this.pushRequestToQueue(actor, {
contentSize: networkInfo.response.bodySize,
transferredSize: networkInfo.response.transferredSize,
mimeType: networkInfo.response.content.mimeType,
});
break;
@@ -362,17 +370,17 @@ class FirefoxDataProvider {
}
// This available field helps knowing when/if updateType property is arrived
// and can be requested via `requestData`
this.pushRequestToQueue(actor, { [`${updateType}Available`]: true });
this.onPayloadDataReceived(actor);
- emit(EVENTS.NETWORK_EVENT_UPDATED, actor);
+ this.emit(EVENTS.NETWORK_EVENT_UPDATED, actor);
}
/**
* Notify actions when messages from onNetworkEventUpdate are done, networkEventUpdate
* messages contain initial network info for each updateType and then we can invoke
* requestData to fetch its corresponded data lazily.
* Once all updateTypes of networkEventUpdate message are arrived, we flush merged
* request payload from pending queue and then update component.
@@ -388,17 +396,17 @@ class FirefoxDataProvider {
this.payloadQueue.delete(actor);
if (this.actionsEnabled && this.actions.updateRequest) {
await this.actions.updateRequest(actor, payload, true);
}
// This event is fired only once per request, once all the properties are fetched
// from `onNetworkEventUpdate`. There should be no more RDP requests after this.
- emit(EVENTS.PAYLOAD_READY, actor);
+ this.emit(EVENTS.PAYLOAD_READY, actor);
}
/**
* Public connector API to lazily request HTTP details from the backend.
*
* The method focus on:
* - calling the right actor method,
* - emitting an event to tell we start fetching some request data,
@@ -458,17 +466,17 @@ class FirefoxDataProvider {
// Calculate real name of the client getter.
let clientMethodName = `get${method.charAt(0).toUpperCase()}${method.slice(1)}`;
// The name of the callback that processes request response
let callbackMethodName = `on${method.charAt(0).toUpperCase()}${method.slice(1)}`;
// And the event to fire before updating this data
let updatingEventName = `UPDATING_${method.replace(/([A-Z])/g, "_$1").toUpperCase()}`;
// Emit event that tell we just start fetching some data
- emit(EVENTS[updatingEventName], actor);
+ this.emit(EVENTS[updatingEventName], actor);
let response = await new Promise((resolve, reject) => {
// Do a RDP request to fetch data from the actor.
if (typeof this.webConsoleClient[clientMethodName] === "function") {
// Make sure we fetch the real actor data instead of cloned actor
// e.g. CustomRequestPanel will clone a request with additional '-clone' actor id
this.webConsoleClient[clientMethodName](actor.replace("-clone", ""), (res) => {
if (res.error) {
@@ -495,131 +503,136 @@ class FirefoxDataProvider {
* Handles additional information received for a "requestHeaders" packet.
*
* @param {object} response the message received from the server.
*/
async onRequestHeaders(response) {
let payload = await this.updateRequest(response.from, {
requestHeaders: response
});
- emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
+ this.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
return payload.requestHeaders;
}
/**
* Handles additional information received for a "responseHeaders" packet.
*
* @param {object} response the message received from the server.
*/
async onResponseHeaders(response) {
let payload = await this.updateRequest(response.from, {
responseHeaders: response
});
- emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
+ this.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
return payload.responseHeaders;
}
/**
* Handles additional information received for a "requestCookies" packet.
*
* @param {object} response the message received from the server.
*/
async onRequestCookies(response) {
let payload = await this.updateRequest(response.from, {
requestCookies: response
});
- emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
+ this.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
return payload.requestCookies;
}
/**
* Handles additional information received for a "requestPostData" packet.
*
* @param {object} response the message received from the server.
*/
async onRequestPostData(response) {
let payload = await this.updateRequest(response.from, {
requestPostData: response
});
- emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
+ this.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
return payload.requestPostData;
}
/**
* Handles additional information received for a "securityInfo" packet.
*
* @param {object} response the message received from the server.
*/
async onSecurityInfo(response) {
let payload = await this.updateRequest(response.from, {
securityInfo: response.securityInfo
});
- emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
+ this.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
return payload.securityInfo;
}
/**
* Handles additional information received for a "responseCookies" packet.
*
* @param {object} response the message received from the server.
*/
async onResponseCookies(response) {
let payload = await this.updateRequest(response.from, {
responseCookies: response
});
- emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
+ this.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
return payload.responseCookies;
}
/**
* Handles additional information received via "getResponseContent" request.
*
* @param {object} response the message received from the server.
*/
async onResponseContent(response) {
let payload = await this.updateRequest(response.from, {
// We have to ensure passing mimeType as fetchResponseContent needs it from
// updateRequest. It will convert the LongString in `response.content.text` to a
// string.
mimeType: response.content.mimeType,
responseContent: response,
});
- emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
+ this.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
return payload.responseContent;
}
/**
* Handles additional information received for a "eventTimings" packet.
*
* @param {object} response the message received from the server.
*/
async onEventTimings(response) {
let payload = await this.updateRequest(response.from, {
eventTimings: response
});
- emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
+ this.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
return payload.eventTimings;
}
/**
* Handles information received for a "stackTrace" packet.
*
* @param {object} response the message received from the server.
*/
async onStackTrace(response) {
let payload = await this.updateRequest(response.from, {
stacktrace: response.stacktrace
});
- emit(EVENTS.RECEIVED_EVENT_STACKTRACE, response.from);
+ this.emit(EVENTS.RECEIVED_EVENT_STACKTRACE, response.from);
return payload.stacktrace;
}
-}
-/**
- * Guard 'emit' to avoid exception in non-window environment.
- */
-function emit(type, data) {
- if (typeof window != "undefined") {
- window.emit(type, data);
+ /**
+ * Fire events for the owner object.
+ */
+ emit(type, data) {
+ if (this.owner) {
+ this.owner.emit(type, data);
+ }
+
+ // Consumed mainly by tests.
+ if (typeof window != "undefined") {
+ window.emit(type, data);
+ }
}
}
module.exports = FirefoxDataProvider;
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -49,22 +49,24 @@ class Connector {
}
}
disconnect() {
this.connector && this.connector.disconnect();
}
connectChrome(connection, actions, getState) {
- this.connector = require("./chrome-connector");
+ let ChromeConnector = require("./chrome-connector");
+ this.connector = new ChromeConnector();
return this.connector.connect(connection, actions, getState);
}
connectFirefox(connection, actions, getState) {
- this.connector = require("./firefox-connector");
+ let FirefoxConnector = require("./firefox-connector");
+ this.connector = new FirefoxConnector();
return this.connector.connect(connection, actions, getState);
}
pause() {
return this.connector.pause();
}
resume() {
--- a/devtools/client/netmonitor/src/har/har-exporter.js
+++ b/devtools/client/netmonitor/src/har/har-exporter.js
@@ -121,17 +121,19 @@ const HarExporter = {
/**
* Get HAR data as JSON object.
*
* @param Object options
* Configuration object, see save() for detailed description.
*/
getHar: function(options) {
- return this.fetchHarData(options).then(JSON.parse);
+ return this.fetchHarData(options).then(data => {
+ return data ? JSON.parse(data) : null;
+ });
},
// Helpers
fetchHarData: function(options) {
// Generate page ID
options.id = options.id || uid++;
--- a/devtools/client/netmonitor/src/moz.build
+++ b/devtools/client/netmonitor/src/moz.build
@@ -10,11 +10,13 @@ DIRS += [
'middleware',
'reducers',
'selectors',
'utils',
'widgets',
]
DevToolsModules(
+ 'api.js',
+ 'app.js',
'constants.js',
'create-store.js',
)