Mypal/devtools/client/netmonitor/netmonitor-controller.js

821 lines
25 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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/. */
/* globals window, document, NetMonitorView, gStore, Actions */
/* exported loader */
"use strict";
var { utils: Cu } = Components;
// Descriptions for what this frontend is currently doing.
const ACTIVITY_TYPE = {
// Standing by and handling requests normally.
NONE: 0,
// Forcing the target to reload with cache enabled or disabled.
RELOAD: {
WITH_CACHE_ENABLED: 1,
WITH_CACHE_DISABLED: 2,
WITH_CACHE_DEFAULT: 3
},
// Enabling or disabling the cache without triggering a reload.
ENABLE_CACHE: 3,
DISABLE_CACHE: 4
};
var BrowserLoaderModule = {};
Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
var { loader, require } = BrowserLoaderModule.BrowserLoader({
baseURI: "resource://devtools/client/netmonitor/",
window
});
const promise = require("promise");
const Services = require("Services");
/* eslint-disable mozilla/reject-some-requires */
const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
const EventEmitter = require("devtools/shared/event-emitter");
const Editor = require("devtools/client/sourceeditor/editor");
const {TimelineFront} = require("devtools/shared/fronts/timeline");
const {Task} = require("devtools/shared/task");
const {Prefs} = require("./prefs");
const {EVENTS} = require("./events");
const Actions = require("./actions/index");
XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
XPCOMUtils.defineConstant(this, "ACTIVITY_TYPE", ACTIVITY_TYPE);
XPCOMUtils.defineConstant(this, "Editor", Editor);
XPCOMUtils.defineConstant(this, "Prefs", Prefs);
XPCOMUtils.defineLazyModuleGetter(this, "Chart",
"resource://devtools/client/shared/widgets/Chart.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
Object.defineProperty(this, "NetworkHelper", {
get: function () {
return require("devtools/shared/webconsole/network-helper");
},
configurable: true,
enumerable: true
});
/**
* Object defining the network monitor controller components.
*/
var NetMonitorController = {
/**
* Initializes the view and connects the monitor client.
*
* @return object
* A promise that is resolved when the monitor finishes startup.
*/
startupNetMonitor: Task.async(function* () {
if (this._startup) {
return this._startup.promise;
}
this._startup = promise.defer();
{
NetMonitorView.initialize();
yield this.connect();
}
this._startup.resolve();
return undefined;
}),
/**
* Destroys the view and disconnects the monitor client from the server.
*
* @return object
* A promise that is resolved when the monitor finishes shutdown.
*/
shutdownNetMonitor: Task.async(function* () {
if (this._shutdown) {
return this._shutdown.promise;
}
this._shutdown = promise.defer();
{
NetMonitorView.destroy();
this.TargetEventsHandler.disconnect();
this.NetworkEventsHandler.disconnect();
yield this.disconnect();
}
this._shutdown.resolve();
return undefined;
}),
/**
* Initiates remote or chrome network monitoring based on the current target,
* wiring event handlers as necessary. Since the TabTarget will have already
* started listening to network requests by now, this is largely
* netmonitor-specific initialization.
*
* @return object
* A promise that is resolved when the monitor finishes connecting.
*/
connect: Task.async(function* () {
if (this._connection) {
return this._connection.promise;
}
this._connection = promise.defer();
// Some actors like AddonActor or RootActor for chrome debugging
// aren't actual tabs.
if (this._target.isTabActor) {
this.tabClient = this._target.activeTab;
}
let connectTimeline = () => {
// Don't start up waiting for timeline markers if the server isn't
// recent enough to emit the markers we're interested in.
if (this._target.getTrait("documentLoadingMarkers")) {
this.timelineFront = new TimelineFront(this._target.client,
this._target.form);
return this.timelineFront.start({ withDocLoadingEvents: true });
}
return undefined;
};
this.webConsoleClient = this._target.activeConsole;
yield connectTimeline();
this.TargetEventsHandler.connect();
this.NetworkEventsHandler.connect();
window.emit(EVENTS.CONNECTED);
this._connection.resolve();
this._connected = true;
return undefined;
}),
/**
* Disconnects the debugger client and removes event handlers as necessary.
*/
disconnect: Task.async(function* () {
if (this._disconnection) {
return this._disconnection.promise;
}
this._disconnection = promise.defer();
// Wait for the connection to finish first.
if (!this.isConnected()) {
yield this._connection.promise;
}
// When debugging local or a remote instance, the connection is closed by
// the RemoteTarget. The webconsole actor is stopped on disconnect.
this.tabClient = null;
this.webConsoleClient = null;
// The timeline front wasn't initialized and started if the server wasn't
// recent enough to emit the markers we were interested in.
if (this._target.getTrait("documentLoadingMarkers")) {
yield this.timelineFront.destroy();
this.timelineFront = null;
}
this._disconnection.resolve();
this._connected = false;
return undefined;
}),
/**
* Checks whether the netmonitor connection is active.
* @return boolean
*/
isConnected: function () {
return !!this._connected;
},
/**
* Gets the activity currently performed by the frontend.
* @return number
*/
getCurrentActivity: function () {
return this._currentActivity || ACTIVITY_TYPE.NONE;
},
/**
* Triggers a specific "activity" to be performed by the frontend.
* This can be, for example, triggering reloads or enabling/disabling cache.
*
* @param number type
* The activity type. See the ACTIVITY_TYPE const.
* @return object
* A promise resolved once the activity finishes and the frontend
* is back into "standby" mode.
*/
triggerActivity: function (type) {
// Puts the frontend into "standby" (when there's no particular activity).
let standBy = () => {
this._currentActivity = ACTIVITY_TYPE.NONE;
};
// Waits for a series of "navigation start" and "navigation stop" events.
let waitForNavigation = () => {
let deferred = promise.defer();
this._target.once("will-navigate", () => {
this._target.once("navigate", () => {
deferred.resolve();
});
});
return deferred.promise;
};
// Reconfigures the tab, optionally triggering a reload.
let reconfigureTab = options => {
let deferred = promise.defer();
this._target.activeTab.reconfigure(options, deferred.resolve);
return deferred.promise;
};
// Reconfigures the tab and waits for the target to finish navigating.
let reconfigureTabAndWaitForNavigation = options => {
options.performReload = true;
let navigationFinished = waitForNavigation();
return reconfigureTab(options).then(() => navigationFinished);
};
if (type == ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT) {
return reconfigureTabAndWaitForNavigation({}).then(standBy);
}
if (type == ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED) {
this._currentActivity = ACTIVITY_TYPE.ENABLE_CACHE;
this._target.once("will-navigate", () => {
this._currentActivity = type;
});
return reconfigureTabAndWaitForNavigation({
cacheDisabled: false,
performReload: true
}).then(standBy);
}
if (type == ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED) {
this._currentActivity = ACTIVITY_TYPE.DISABLE_CACHE;
this._target.once("will-navigate", () => {
this._currentActivity = type;
});
return reconfigureTabAndWaitForNavigation({
cacheDisabled: true,
performReload: true
}).then(standBy);
}
if (type == ACTIVITY_TYPE.ENABLE_CACHE) {
this._currentActivity = type;
return reconfigureTab({
cacheDisabled: false,
performReload: false
}).then(standBy);
}
if (type == ACTIVITY_TYPE.DISABLE_CACHE) {
this._currentActivity = type;
return reconfigureTab({
cacheDisabled: true,
performReload: false
}).then(standBy);
}
this._currentActivity = ACTIVITY_TYPE.NONE;
return promise.reject(new Error("Invalid activity type"));
},
/**
* Selects the specified request in the waterfall and opens the details view.
*
* @param string requestId
* The actor ID of the request to inspect.
* @return object
* A promise resolved once the task finishes.
*/
inspectRequest: function (requestId) {
// Look for the request in the existing ones or wait for it to appear, if
// the network monitor is still loading.
let deferred = promise.defer();
let request = null;
let inspector = function () {
let predicate = i => i.value === requestId;
request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
if (!request) {
// Reset filters so that the request is visible.
gStore.dispatch(Actions.toggleFilterType("all"));
request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
}
// 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);
NetMonitorView.RequestsMenu.selectedItem = request;
deferred.resolve();
}
};
inspector();
if (!request) {
window.on(EVENTS.REQUEST_ADDED, inspector);
}
return deferred.promise;
},
/**
* Getter that tells if the server supports sending custom network requests.
* @type boolean
*/
get supportsCustomRequest() {
return this.webConsoleClient &&
(this.webConsoleClient.traits.customNetworkRequest ||
!this._target.isApp);
},
/**
* Getter that tells if the server includes the transferred (compressed /
* encoded) response size.
* @type boolean
*/
get supportsTransferredResponseSize() {
return this.webConsoleClient &&
this.webConsoleClient.traits.transferredResponseSize;
},
/**
* Getter that tells if the server can do network performance statistics.
* @type boolean
*/
get supportsPerfStats() {
return this.tabClient &&
(this.tabClient.traits.reconfigure || !this._target.isApp);
},
/**
* Open a given source in Debugger
*/
viewSourceInDebugger(sourceURL, sourceLine) {
return this._toolbox.viewSourceInDebugger(sourceURL, sourceLine);
}
};
/**
* Functions handling target-related lifetime events.
*/
function TargetEventsHandler() {
this._onTabNavigated = this._onTabNavigated.bind(this);
this._onTabDetached = this._onTabDetached.bind(this);
}
TargetEventsHandler.prototype = {
get target() {
return NetMonitorController._target;
},
/**
* Listen for events emitted by the current tab target.
*/
connect: function () {
dumpn("TargetEventsHandler is connecting...");
this.target.on("close", this._onTabDetached);
this.target.on("navigate", this._onTabNavigated);
this.target.on("will-navigate", this._onTabNavigated);
},
/**
* Remove events emitted by the current tab target.
*/
disconnect: function () {
if (!this.target) {
return;
}
dumpn("TargetEventsHandler is disconnecting...");
this.target.off("close", this._onTabDetached);
this.target.off("navigate", this._onTabNavigated);
this.target.off("will-navigate", this._onTabNavigated);
},
/**
* Called for each location change in the monitored tab.
*
* @param string type
* Packet type.
* @param object packet
* Packet received from the server.
*/
_onTabNavigated: function (type, packet) {
switch (type) {
case "will-navigate": {
// Reset UI.
if (!Services.prefs.getBoolPref("devtools.webconsole.persistlog")) {
NetMonitorView.RequestsMenu.reset();
NetMonitorView.Sidebar.toggle(false);
} else {
// If the log is persistent, just clear some informations.
NetMonitorView.RequestsMenu.resetNotPersistent();
}
// Switch to the default network traffic inspector view.
if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) {
NetMonitorView.showNetworkInspectorView();
}
// Clear any accumulated markers.
NetMonitorController.NetworkEventsHandler.clearMarkers();
gStore.dispatch(Actions.clearTimingMarkers());
window.emit(EVENTS.TARGET_WILL_NAVIGATE);
break;
}
case "navigate": {
window.emit(EVENTS.TARGET_DID_NAVIGATE);
break;
}
}
},
/**
* Called when the monitored tab is closed.
*/
_onTabDetached: function () {
NetMonitorController.shutdownNetMonitor();
}
};
/**
* Functions handling target network events.
*/
function NetworkEventsHandler() {
this._markers = [];
this._onNetworkEvent = this._onNetworkEvent.bind(this);
this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
this._onDocLoadingMarker = this._onDocLoadingMarker.bind(this);
this._onRequestHeaders = this._onRequestHeaders.bind(this);
this._onRequestCookies = this._onRequestCookies.bind(this);
this._onRequestPostData = this._onRequestPostData.bind(this);
this._onResponseHeaders = this._onResponseHeaders.bind(this);
this._onResponseCookies = this._onResponseCookies.bind(this);
this._onResponseContent = this._onResponseContent.bind(this);
this._onEventTimings = this._onEventTimings.bind(this);
}
NetworkEventsHandler.prototype = {
get client() {
return NetMonitorController._target.client;
},
get webConsoleClient() {
return NetMonitorController.webConsoleClient;
},
get timelineFront() {
return NetMonitorController.timelineFront;
},
get firstDocumentDOMContentLoadedTimestamp() {
let marker = this._markers.filter(e => {
return e.name == "document::DOMContentLoaded";
})[0];
return marker ? marker.unixTime / 1000 : -1;
},
get firstDocumentLoadTimestamp() {
let marker = this._markers.filter(e => e.name == "document::Load")[0];
return marker ? marker.unixTime / 1000 : -1;
},
/**
* Connect to the current target client.
*/
connect: function () {
dumpn("NetworkEventsHandler is connecting...");
this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
if (this.timelineFront) {
this.timelineFront.on("doc-loading", this._onDocLoadingMarker);
}
this._displayCachedEvents();
},
/**
* Disconnect from the client.
*/
disconnect: function () {
if (!this.client) {
return;
}
dumpn("NetworkEventsHandler is disconnecting...");
this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
if (this.timelineFront) {
this.timelineFront.off("doc-loading", this._onDocLoadingMarker);
}
},
/**
* Display any network events already in the cache.
*/
_displayCachedEvents: function () {
for (let cachedEvent of this.webConsoleClient.getNetworkEvents()) {
// First add the request to the timeline.
this._onNetworkEvent("networkEvent", cachedEvent);
// Then replay any updates already received.
for (let update of cachedEvent.updates) {
this._onNetworkEventUpdate("networkEventUpdate", {
packet: {
updateType: update
},
networkInfo: cachedEvent
});
}
}
},
/**
* The "DOMContentLoaded" and "Load" events sent by the timeline actor.
* @param object marker
*/
_onDocLoadingMarker: function (marker) {
window.emit(EVENTS.TIMELINE_EVENT, marker);
this._markers.push(marker);
gStore.dispatch(Actions.addTimingMarker(marker));
},
/**
* The "networkEvent" message type handler.
*
* @param string type
* Message type.
* @param object networkInfo
* The network request information.
*/
_onNetworkEvent: function (type, networkInfo) {
let { actor,
startedDateTime,
request: { method, url },
isXHR,
cause,
fromCache,
fromServiceWorker
} = networkInfo;
NetMonitorView.RequestsMenu.addRequest(
actor, startedDateTime, method, url, isXHR, cause, fromCache,
fromServiceWorker
);
window.emit(EVENTS.NETWORK_EVENT, actor);
},
/**
* The "networkEventUpdate" message type handler.
*
* @param string type
* Message type.
* @param object packet
* The message received from the server.
* @param object networkInfo
* The network request information.
*/
_onNetworkEventUpdate: function (type, { packet, networkInfo }) {
let { actor } = networkInfo;
switch (packet.updateType) {
case "requestHeaders":
this.webConsoleClient.getRequestHeaders(actor, this._onRequestHeaders);
window.emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
break;
case "requestCookies":
this.webConsoleClient.getRequestCookies(actor, this._onRequestCookies);
window.emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
break;
case "requestPostData":
this.webConsoleClient.getRequestPostData(actor,
this._onRequestPostData);
window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
break;
case "securityInfo":
NetMonitorView.RequestsMenu.updateRequest(actor, {
securityState: networkInfo.securityInfo,
});
this.webConsoleClient.getSecurityInfo(actor, this._onSecurityInfo);
window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
break;
case "responseHeaders":
this.webConsoleClient.getResponseHeaders(actor,
this._onResponseHeaders);
window.emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
break;
case "responseCookies":
this.webConsoleClient.getResponseCookies(actor,
this._onResponseCookies);
window.emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
break;
case "responseStart":
NetMonitorView.RequestsMenu.updateRequest(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
});
window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
break;
case "responseContent":
NetMonitorView.RequestsMenu.updateRequest(actor, {
contentSize: networkInfo.response.bodySize,
transferredSize: networkInfo.response.transferredSize,
mimeType: networkInfo.response.content.mimeType
});
this.webConsoleClient.getResponseContent(actor,
this._onResponseContent);
window.emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
break;
case "eventTimings":
NetMonitorView.RequestsMenu.updateRequest(actor, {
totalTime: networkInfo.totalTime
});
this.webConsoleClient.getEventTimings(actor, this._onEventTimings);
window.emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
break;
}
},
/**
* Handles additional information received for a "requestHeaders" packet.
*
* @param object response
* The message received from the server.
*/
_onRequestHeaders: function (response) {
NetMonitorView.RequestsMenu.updateRequest(response.from, {
requestHeaders: response
}, () => {
window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
});
},
/**
* Handles additional information received for a "requestCookies" packet.
*
* @param object response
* The message received from the server.
*/
_onRequestCookies: function (response) {
NetMonitorView.RequestsMenu.updateRequest(response.from, {
requestCookies: response
}, () => {
window.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
});
},
/**
* Handles additional information received for a "requestPostData" packet.
*
* @param object response
* The message received from the server.
*/
_onRequestPostData: function (response) {
NetMonitorView.RequestsMenu.updateRequest(response.from, {
requestPostData: response
}, () => {
window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
});
},
/**
* Handles additional information received for a "securityInfo" packet.
*
* @param object response
* The message received from the server.
*/
_onSecurityInfo: function (response) {
NetMonitorView.RequestsMenu.updateRequest(response.from, {
securityInfo: response.securityInfo
}, () => {
window.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
});
},
/**
* Handles additional information received for a "responseHeaders" packet.
*
* @param object response
* The message received from the server.
*/
_onResponseHeaders: function (response) {
NetMonitorView.RequestsMenu.updateRequest(response.from, {
responseHeaders: response
}, () => {
window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
});
},
/**
* Handles additional information received for a "responseCookies" packet.
*
* @param object response
* The message received from the server.
*/
_onResponseCookies: function (response) {
NetMonitorView.RequestsMenu.updateRequest(response.from, {
responseCookies: response
}, () => {
window.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
});
},
/**
* Handles additional information received for a "responseContent" packet.
*
* @param object response
* The message received from the server.
*/
_onResponseContent: function (response) {
NetMonitorView.RequestsMenu.updateRequest(response.from, {
responseContent: response
}, () => {
window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
});
},
/**
* Handles additional information received for a "eventTimings" packet.
*
* @param object response
* The message received from the server.
*/
_onEventTimings: function (response) {
NetMonitorView.RequestsMenu.updateRequest(response.from, {
eventTimings: response
}, () => {
window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
});
},
/**
* Clears all accumulated markers.
*/
clearMarkers: function () {
this._markers.length = 0;
},
/**
* Fetches the full text of a LongString.
*
* @param object | string stringGrip
* The long string grip containing the corresponding actor.
* If you pass in a plain string (by accident or because you're lazy),
* then a promise of the same string is simply returned.
* @return object Promise
* A promise that is resolved when the full string contents
* are available, or rejected if something goes wrong.
*/
getString: function (stringGrip) {
return this.webConsoleClient.getString(stringGrip);
}
};
/**
* Returns true if this is document is in RTL mode.
* @return boolean
*/
XPCOMUtils.defineLazyGetter(window, "isRTL", function () {
return window.getComputedStyle(document.documentElement, null)
.direction == "rtl";
});
/**
* Convenient way of emitting events from the panel window.
*/
EventEmitter.decorate(this);
/**
* Preliminary setup for the NetMonitorController object.
*/
NetMonitorController.TargetEventsHandler = new TargetEventsHandler();
NetMonitorController.NetworkEventsHandler = new NetworkEventsHandler();
/**
* Export some properties to the global scope for easier access.
*/
Object.defineProperties(window, {
"gNetwork": {
get: function () {
return NetMonitorController.NetworkEventsHandler;
},
configurable: true
}
});
/**
* Helper method for debugging.
* @param string
*/
function dumpn(str) {
if (wantLogging) {
dump("NET-FRONTEND: " + str + "\n");
}
}
var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");