diff options
Diffstat (limited to '')
6 files changed, 924 insertions, 0 deletions
diff --git a/devtools/server/actors/webconsole/listeners/console-api.js b/devtools/server/actors/webconsole/listeners/console-api.js new file mode 100644 index 0000000000..3e5d0bc52f --- /dev/null +++ b/devtools/server/actors/webconsole/listeners/console-api.js @@ -0,0 +1,255 @@ +/* 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 { + CONSOLE_WORKER_IDS, + WebConsoleUtils, +} = require("resource://devtools/server/actors/webconsole/utils.js"); + +// The window.console API observer + +/** + * The window.console API observer. This allows the window.console API messages + * to be sent to the remote Web Console instance. + * + * @constructor + * @param nsIDOMWindow window + * Optional - the window object for which we are created. This is used + * for filtering out messages that belong to other windows. + * @param Function handler + * This function is invoked with one argument, the Console API message that comes + * from the observer service, whenever a relevant console API call is received. + * @param object filteringOptions + * Optional - The filteringOptions that this listener should listen to: + * - addonId: filter console messages based on the addonId. + * - excludeMessagesBoundToWindow: Set to true to filter out messages that + * are bound to a specific window. + * - matchExactWindow: Set to true to match the messages on a specific window (when + * `window` is defined) and not on the whole window tree. + */ +class ConsoleAPIListener { + constructor( + window, + handler, + { addonId, excludeMessagesBoundToWindow, matchExactWindow } = {} + ) { + this.window = window; + this.handler = handler; + this.addonId = addonId; + this.excludeMessagesBoundToWindow = excludeMessagesBoundToWindow; + this.matchExactWindow = matchExactWindow; + if (this.window) { + this.innerWindowId = WebConsoleUtils.getInnerWindowId(this.window); + } + } + + QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]); + + /** + * The content window for which we listen to window.console API calls. + * @type nsIDOMWindow + */ + window = null; + + /** + * The function which is notified of window.console API calls. It is invoked with one + * argument: the console API call object that comes from the ConsoleAPIStorage service. + * + * @type function + */ + handler = null; + + /** + * The addonId that we listen for. If not null then only messages from this + * console will be returned. + */ + addonId = null; + + /** + * Initialize the window.console API listener. + */ + init() { + const ConsoleAPIStorage = Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(Ci.nsIConsoleAPIStorage); + + // Note that the listener is process-wide. We will filter the messages as + // needed, see onConsoleAPILogEvent(). + this.onConsoleAPILogEvent = this.onConsoleAPILogEvent.bind(this); + ConsoleAPIStorage.addLogEventListener( + this.onConsoleAPILogEvent, + // We create a principal here to get the privileged principal of this + // script. Note that this is importantly *NOT* the principal of the + // content we are observing, as that would not have access to the + // message object created in ConsoleAPIStorage.jsm's scope. + Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) + ); + } + + /** + * The console API message listener. When messages are received from the + * ConsoleAPIStorage service we forward them to the remote Web Console instance. + * + * @param object message + * The message object receives from the ConsoleAPIStorage service. + */ + onConsoleAPILogEvent(message) { + if (!this.handler) { + return; + } + + // Here, wrappedJSObject is not a security wrapper but a property defined + // by the XPCOM component which allows us to unwrap the XPCOM interface and + // access the underlying JSObject. + const apiMessage = message.wrappedJSObject; + + if (!this.isMessageRelevant(apiMessage)) { + return; + } + + this.handler(apiMessage); + } + + /** + * Given a message, return true if this window should show it and false + * if it should be ignored. + * + * @param message + * The message from the Storage Service + * @return bool + * Do we care about this message? + */ + isMessageRelevant(message) { + const workerType = WebConsoleUtils.getWorkerType(message); + + if (this.window && workerType === "ServiceWorker") { + // For messages from Service Workers, message.ID is the + // scope, which can be used to determine whether it's controlling + // a window. + const scope = message.ID; + + if (!this.window.shouldReportForServiceWorkerScope(scope)) { + return false; + } + } + + // innerID can be of different type: + // - a number if the message is bound to a specific window + // - a worker type ([Shared|Service]Worker) if the message comes from a worker + // - a JSM filename + // if we want to filter on a specific window, ignore all non-worker messages that + // don't have a proper window id (for now, we receive the worker messages from the + // main process so we still want to get them, although their innerID isn't a number). + if (!workerType && typeof message.innerID !== "number" && this.window) { + return false; + } + + // Don't show ChromeWorker messages on WindowGlobal targets + if (workerType && this.window && message.chromeContext) { + return false; + } + + if (typeof message.innerID == "number") { + if ( + this.excludeMessagesBoundToWindow && + // If innerID is 0, the message isn't actually bound to a window. + message.innerID + ) { + return false; + } + + if (this.window) { + const matchesWindow = this.matchExactWindow + ? this.innerWindowId === message.innerID + : WebConsoleUtils.getInnerWindowIDsForFrames(this.window).includes( + message.innerID + ); + + if (!matchesWindow) { + // Not the same window! + return false; + } + } + } + + if (this.addonId) { + // ConsoleAPI.jsm messages contains a consoleID, (and it is currently + // used in Addon SDK add-ons), the standard 'console' object + // (which is used in regular webpages and in WebExtensions pages) + // contains the originAttributes of the source document principal. + + // Filtering based on the originAttributes used by + // the Console API object. + if (message.addonId == this.addonId) { + return true; + } + + // Filtering based on the old-style consoleID property used by + // the legacy Console JSM module. + if (message.consoleID && message.consoleID == `addon/${this.addonId}`) { + return true; + } + + return false; + } + + return true; + } + + /** + * Get the cached messages for the current inner window and its (i)frames. + * + * @param boolean [includePrivate=false] + * Tells if you want to also retrieve messages coming from private + * windows. Defaults to false. + * @return array + * The array of cached messages. + */ + getCachedMessages(includePrivate = false) { + let messages = []; + const ConsoleAPIStorage = Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(Ci.nsIConsoleAPIStorage); + + // if !this.window, we're in a browser console. Retrieve all events + // for filtering based on privacy. + if (!this.window) { + messages = ConsoleAPIStorage.getEvents(); + } else if (this.matchExactWindow) { + messages = ConsoleAPIStorage.getEvents(this.innerWindowId); + } else { + WebConsoleUtils.getInnerWindowIDsForFrames(this.window).forEach(id => { + messages = messages.concat(ConsoleAPIStorage.getEvents(id)); + }); + } + + CONSOLE_WORKER_IDS.forEach(id => { + messages = messages.concat(ConsoleAPIStorage.getEvents(id)); + }); + + messages = messages.filter(msg => { + return this.isMessageRelevant(msg); + }); + + if (includePrivate) { + return messages; + } + + return messages.filter(m => !m.private); + } + + /** + * Destroy the console API listener. + */ + destroy() { + const ConsoleAPIStorage = Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(Ci.nsIConsoleAPIStorage); + ConsoleAPIStorage.removeLogEventListener(this.onConsoleAPILogEvent); + this.window = this.handler = null; + } +} +exports.ConsoleAPIListener = ConsoleAPIListener; diff --git a/devtools/server/actors/webconsole/listeners/console-file-activity.js b/devtools/server/actors/webconsole/listeners/console-file-activity.js new file mode 100644 index 0000000000..7e5ae0d1a8 --- /dev/null +++ b/devtools/server/actors/webconsole/listeners/console-file-activity.js @@ -0,0 +1,126 @@ +/* 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"; + +/** + * A WebProgressListener that listens for file loads. + * + * @constructor + * @param object window + * The window for which we need to track file loads. + * @param object owner + * The listener owner which needs to implement: + * - onFileActivity(aFileURI) + */ +function ConsoleFileActivityListener(window, owner) { + this.window = window; + this.owner = owner; +} +exports.ConsoleFileActivityListener = ConsoleFileActivityListener; + +ConsoleFileActivityListener.prototype = { + /** + * Tells if the console progress listener is initialized or not. + * @private + * @type boolean + */ + _initialized: false, + + _webProgress: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + /** + * Initialize the ConsoleFileActivityListener. + * @private + */ + _init() { + if (this._initialized) { + return; + } + + this._webProgress = this.window.docShell.QueryInterface(Ci.nsIWebProgress); + this._webProgress.addProgressListener( + this, + Ci.nsIWebProgress.NOTIFY_STATE_ALL + ); + + this._initialized = true; + }, + + /** + * Start a monitor/tracker related to the current nsIWebProgressListener + * instance. + */ + startMonitor() { + this._init(); + }, + + /** + * Stop monitoring. + */ + stopMonitor() { + this.destroy(); + }, + + onStateChange(progress, request, state, status) { + if (!this.owner) { + return; + } + + this._checkFileActivity(progress, request, state, status); + }, + + /** + * Check if there is any file load, given the arguments of + * nsIWebProgressListener.onStateChange. If the state change tells that a file + * URI has been loaded, then the remote Web Console instance is notified. + * @private + */ + _checkFileActivity(progress, request, state, status) { + if (!(state & Ci.nsIWebProgressListener.STATE_START)) { + return; + } + + let uri = null; + if (request instanceof Ci.imgIRequest) { + const imgIRequest = request.QueryInterface(Ci.imgIRequest); + uri = imgIRequest.URI; + } else if (request instanceof Ci.nsIChannel) { + const nsIChannel = request.QueryInterface(Ci.nsIChannel); + uri = nsIChannel.URI; + } + + if (!uri || (!uri.schemeIs("file") && !uri.schemeIs("ftp"))) { + return; + } + + this.owner.onFileActivity(uri.spec); + }, + + /** + * Destroy the ConsoleFileActivityListener. + */ + destroy() { + if (!this._initialized) { + return; + } + + this._initialized = false; + + try { + this._webProgress.removeProgressListener(this); + } catch (ex) { + // This can throw during browser shutdown. + } + + this._webProgress = null; + this.window = null; + this.owner = null; + }, +}; diff --git a/devtools/server/actors/webconsole/listeners/console-reflow.js b/devtools/server/actors/webconsole/listeners/console-reflow.js new file mode 100644 index 0000000000..8404a70b4a --- /dev/null +++ b/devtools/server/actors/webconsole/listeners/console-reflow.js @@ -0,0 +1,90 @@ +/* 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"; + +/** + * A ReflowObserver that listens for reflow events from the page. + * Implements nsIReflowObserver. + * + * @constructor + * @param object window + * The window for which we need to track reflow. + * @param object owner + * The listener owner which needs to implement: + * - onReflowActivity(reflowInfo) + */ + +function ConsoleReflowListener(window, listener) { + this.docshell = window.docShell; + this.listener = listener; + this.docshell.addWeakReflowObserver(this); +} + +exports.ConsoleReflowListener = ConsoleReflowListener; + +ConsoleReflowListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIReflowObserver", + "nsISupportsWeakReference", + ]), + docshell: null, + listener: null, + + /** + * Forward reflow event to listener. + * + * @param DOMHighResTimeStamp start + * @param DOMHighResTimeStamp end + * @param boolean interruptible + */ + sendReflow(start, end, interruptible) { + const frame = Components.stack.caller.caller; + + let filename = frame ? frame.filename : null; + + if (filename) { + // Because filename could be of the form "xxx.js -> xxx.js -> xxx.js", + // we only take the last part. + filename = filename.split(" ").pop(); + } + + this.listener.onReflowActivity({ + interruptible, + start, + end, + sourceURL: filename, + sourceLine: frame ? frame.lineNumber : null, + functionName: frame ? frame.name : null, + }); + }, + + /** + * On uninterruptible reflow + * + * @param DOMHighResTimeStamp start + * @param DOMHighResTimeStamp end + */ + reflow(start, end) { + this.sendReflow(start, end, false); + }, + + /** + * On interruptible reflow + * + * @param DOMHighResTimeStamp start + * @param DOMHighResTimeStamp end + */ + reflowInterruptible(start, end) { + this.sendReflow(start, end, true); + }, + + /** + * Unregister listener. + */ + destroy() { + this.docshell.removeWeakReflowObserver(this); + this.listener = this.docshell = null; + }, +}; diff --git a/devtools/server/actors/webconsole/listeners/console-service.js b/devtools/server/actors/webconsole/listeners/console-service.js new file mode 100644 index 0000000000..11ced5611f --- /dev/null +++ b/devtools/server/actors/webconsole/listeners/console-service.js @@ -0,0 +1,193 @@ +/* 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 { + isWindowIncluded, +} = require("resource://devtools/shared/layout/utils.js"); +const { + WebConsoleUtils, +} = require("resource://devtools/server/actors/webconsole/utils.js"); + +// The page errors listener + +/** + * The nsIConsoleService listener. This is used to send all of the console + * messages (JavaScript, CSS and more) to the remote Web Console instance. + * + * @constructor + * @param nsIDOMWindow [window] + * Optional - the window object for which we are created. This is used + * for filtering out messages that belong to other windows. + * @param Function handler + * This function is invoked with one argument, the nsIConsoleMessage, whenever a + * relevant message is received. + * @param object filteringOptions + * Optional - The filteringOptions that this listener should listen to: + * - matchExactWindow: Set to true to match the messages on a specific window (when + * `window` is defined) and not on the whole window tree. + */ +class ConsoleServiceListener { + constructor(window, handler, { matchExactWindow } = {}) { + this.window = window; + this.handler = handler; + this.matchExactWindow = matchExactWindow; + } + + QueryInterface = ChromeUtils.generateQI([Ci.nsIConsoleListener]); + + /** + * The content window for which we listen to page errors. + * @type nsIDOMWindow + */ + window = null; + + /** + * The function which is notified of messages from the console service. + * @type function + */ + handler = null; + + /** + * Initialize the nsIConsoleService listener. + */ + init() { + Services.console.registerListener(this); + } + + /** + * The nsIConsoleService observer. This method takes all the script error + * messages belonging to the current window and sends them to the remote Web + * Console instance. + * + * @param nsIConsoleMessage message + * The message object coming from the nsIConsoleService. + */ + observe(message) { + if (!this.handler) { + return; + } + + if (this.window) { + if ( + !(message instanceof Ci.nsIScriptError) || + !message.outerWindowID || + !this.isCategoryAllowed(message.category) + ) { + return; + } + + const errorWindow = Services.wm.getOuterWindowWithId( + message.outerWindowID + ); + + if (!errorWindow) { + return; + } + + if (this.matchExactWindow && this.window !== errorWindow) { + return; + } + + if (!isWindowIncluded(this.window, errorWindow)) { + return; + } + } + + // Don't display messages triggered by eager evaluation. + if (message.sourceName === "debugger eager eval code") { + return; + } + this.handler(message); + } + + /** + * Check if the given message category is allowed to be tracked or not. + * We ignore chrome-originating errors as we only care about content. + * + * @param string category + * The message category you want to check. + * @return boolean + * True if the category is allowed to be logged, false otherwise. + */ + isCategoryAllowed(category) { + if (!category) { + return false; + } + + switch (category) { + case "XPConnect JavaScript": + case "component javascript": + case "chrome javascript": + case "chrome registration": + return false; + } + + return true; + } + + /** + * Get the cached page errors for the current inner window and its (i)frames. + * + * @param boolean [includePrivate=false] + * Tells if you want to also retrieve messages coming from private + * windows. Defaults to false. + * @return array + * The array of cached messages. Each element is an nsIScriptError or + * an nsIConsoleMessage + */ + getCachedMessages(includePrivate = false) { + const errors = Services.console.getMessageArray() || []; + + // if !this.window, we're in a browser console. Still need to filter + // private messages. + if (!this.window) { + return errors.filter(error => { + if (error instanceof Ci.nsIScriptError) { + if (!includePrivate && error.isFromPrivateWindow) { + return false; + } + } + + return true; + }); + } + + const ids = this.matchExactWindow + ? [WebConsoleUtils.getInnerWindowId(this.window)] + : WebConsoleUtils.getInnerWindowIDsForFrames(this.window); + + return errors.filter(error => { + if (error instanceof Ci.nsIScriptError) { + if (!includePrivate && error.isFromPrivateWindow) { + return false; + } + if ( + ids && + (!ids.includes(error.innerWindowID) || + !this.isCategoryAllowed(error.category)) + ) { + return false; + } + } else if (ids?.[0]) { + // If this is not an nsIScriptError and we need to do window-based + // filtering we skip this message. + return false; + } + + return true; + }); + } + + /** + * Remove the nsIConsoleService listener. + */ + destroy() { + Services.console.unregisterListener(this); + this.handler = this.window = null; + } +} + +exports.ConsoleServiceListener = ConsoleServiceListener; diff --git a/devtools/server/actors/webconsole/listeners/document-events.js b/devtools/server/actors/webconsole/listeners/document-events.js new file mode 100644 index 0000000000..1c1f926436 --- /dev/null +++ b/devtools/server/actors/webconsole/listeners/document-events.js @@ -0,0 +1,247 @@ +/* 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/. */ + +// XPCNativeWrapper is not defined globally in ESLint as it may be going away. +// See bug 1481337. +/* global XPCNativeWrapper */ + +"use strict"; + +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); + +/** + * About "navigationStart - ${WILL_NAVIGATE_TIME_SHIFT}ms": + * Unfortunately dom-loading's navigationStart timestamp is older than the navigationStart we receive from will-navigate. + * + * That's because we record `navigationStart` before will-navigate code is called. + * And will-navigate code don't have access to performance.timing.navigationStart that dom-loading is using. + * The `performance.timing.navigationStart` is recorded earlier from `DocumentLoadListener.SetNavigating`, here: + * https://searchfox.org/mozilla-central/rev/9b430bb1a11d7152cab2af4574f451ffb906b052/netwerk/ipc/DocumentLoadListener.cpp#907-908 + * https://searchfox.org/mozilla-central/rev/9b430bb1a11d7152cab2af4574f451ffb906b052/netwerk/ipc/DocumentLoadListener.cpp#820-823 + * While this function is being called via `nsIWebProgressListener.onStateChange`, here: + * https://searchfox.org/mozilla-central/rev/9b430bb1a11d7152cab2af4574f451ffb906b052/netwerk/ipc/DocumentLoadListener.cpp#934-939 + * And we record the navigationStart timestamp from onStateChange by using Date.now(), which is more recent + * than performance.timing.navigationStart. + * + * We do this workaround because all DOCUMENT_EVENT comes with a "time" timestamp. + * Each event relates to a particular event in the lifecycle of documents and are supposed to follow a particular order: + * - will-navigate (on the previous target) + * - dom-loading (on the new target) + * - dom-interactive + * - dom-complete + * And some tests are asserting this. + */ +const WILL_NAVIGATE_TIME_SHIFT = 20; +exports.WILL_NAVIGATE_TIME_SHIFT = WILL_NAVIGATE_TIME_SHIFT; + +/** + * Forward `DOMContentLoaded` and `load` events with precise timing + * of when events happened according to window.performance numbers. + * + * @constructor + * @param WindowGlobalTarget targetActor + */ +function DocumentEventsListener(targetActor) { + this.targetActor = targetActor; + + EventEmitter.decorate(this); + this.onWillNavigate = this.onWillNavigate.bind(this); + this.onWindowReady = this.onWindowReady.bind(this); + this.onContentLoaded = this.onContentLoaded.bind(this); + this.onLoad = this.onLoad.bind(this); +} + +exports.DocumentEventsListener = DocumentEventsListener; + +DocumentEventsListener.prototype = { + listen() { + // When EFT is enabled, the Target Actor won't dispatch any will-navigate/window-ready event + // Instead listen to WebProgressListener interface directly, so that we can later drop the whole + // DebuggerProgressListener interface in favor of this class. + // Also, do not wait for "load" event as it can be blocked in case of error during the load + // or when calling window.stop(). We still want to emit "dom-complete" in these scenarios. + if (this.targetActor.ignoreSubFrames) { + // Ignore listening to anything if the page is already fully loaded. + // This can be the case when opening DevTools against an already loaded page + // or when doing bfcache navigations. + if (this.targetActor.window.document.readyState != "complete") { + this.webProgress = this.targetActor.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + this.webProgress.addProgressListener( + this, + Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT + ); + } + } else { + // Listen to will-navigate and do not emit a fake one as we only care about upcoming navigation + this.targetActor.on("will-navigate", this.onWillNavigate); + + // Listen to window-ready and then fake one in order to notify about dom-loading for the existing document + this.targetActor.on("window-ready", this.onWindowReady); + } + // The target actor already emitted a window-ready for the top document when instantiating. + // So fake one for the top document right away. + this.onWindowReady({ + window: this.targetActor.window, + isTopLevel: true, + }); + }, + + onWillNavigate({ + window, + isTopLevel, + newURI, + navigationStart, + isFrameSwitching, + }) { + // Ignore iframes + if (!isTopLevel) { + return; + } + + this.emit("will-navigate", { + time: navigationStart - WILL_NAVIGATE_TIME_SHIFT, + newURI, + isFrameSwitching, + }); + }, + + onWindowReady({ window, isTopLevel, isFrameSwitching }) { + // Ignore iframes + if (!isTopLevel) { + return; + } + + const time = window.performance.timing.navigationStart; + + this.emit("dom-loading", { + time, + isFrameSwitching, + }); + + const { readyState } = window.document; + if (readyState != "interactive" && readyState != "complete") { + // When EFT is enabled, we track this event via the WebProgressListener interface. + if (!this.targetActor.ignoreSubFrames) { + window.addEventListener( + "DOMContentLoaded", + e => this.onContentLoaded(e, isFrameSwitching), + { + once: true, + } + ); + } + } else { + this.onContentLoaded({ target: window.document }, isFrameSwitching); + } + if (readyState != "complete") { + // When EFT is enabled, we track the load event via the WebProgressListener interface. + if (!this.targetActor.ignoreSubFrames) { + window.addEventListener("load", e => this.onLoad(e, isFrameSwitching), { + once: true, + }); + } + } else { + this.onLoad({ target: window.document }, isFrameSwitching); + } + }, + + onContentLoaded(event, isFrameSwitching) { + if (this.destroyed) { + return; + } + // milliseconds since the UNIX epoch, when the parser finished its work + // on the main document, that is when its Document.readyState changes to + // 'interactive' and the corresponding readystatechange event is thrown + const window = event.target.defaultView; + const time = window.performance.timing.domInteractive; + this.emit("dom-interactive", { time, isFrameSwitching }); + }, + + onLoad(event, isFrameSwitching) { + if (this.destroyed) { + return; + } + // milliseconds since the UNIX epoch, when the parser finished its work + // on the main document, that is when its Document.readyState changes to + // 'complete' and the corresponding readystatechange event is thrown + const window = event.target.defaultView; + const time = window.performance.timing.domComplete; + this.emit("dom-complete", { + time, + isFrameSwitching, + hasNativeConsoleAPI: this.hasNativeConsoleAPI(window), + }); + }, + + onStateChange(progress, request, flag, status) { + progress.QueryInterface(Ci.nsIDocShell); + // Ignore destroyed, or progress for same-process iframes + if (progress.isBeingDestroyed() || progress != this.webProgress) { + return; + } + + const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; + const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; + const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW; + const window = progress.DOMWindow; + if (isDocument && isStop) { + const time = window.performance.timing.domInteractive; + this.emit("dom-interactive", { time }); + } else if (isWindow && isStop) { + const time = window.performance.timing.domComplete; + this.emit("dom-complete", { + time, + hasNativeConsoleAPI: this.hasNativeConsoleAPI(window), + }); + } + }, + + /** + * Tells if the window.console object is native or overwritten by script in + * the page. + * + * @param nsIDOMWindow window + * The window object you want to check. + * @return boolean + * True if the window.console object is native, or false otherwise. + */ + hasNativeConsoleAPI(window) { + let isNative = false; + try { + // We are very explicitly examining the "console" property of + // the non-Xrayed object here. + const console = window.wrappedJSObject.console; + // In xpcshell tests, console ends up being undefined and XPCNativeWrapper + // crashes in debug builds. + if (console) { + isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE === true; + } + } catch (ex) { + // ignore + } + return isNative; + }, + + destroy() { + // Also use a flag to silent onContentLoad and onLoad events + this.destroyed = true; + this.targetActor.off("will-navigate", this.onWillNavigate); + this.targetActor.off("window-ready", this.onWindowReady); + if (this.webProgress) { + this.webProgress.removeProgressListener( + this, + Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT + ); + } + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; diff --git a/devtools/server/actors/webconsole/listeners/moz.build b/devtools/server/actors/webconsole/listeners/moz.build new file mode 100644 index 0000000000..089de4a087 --- /dev/null +++ b/devtools/server/actors/webconsole/listeners/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + "console-api.js", + "console-file-activity.js", + "console-reflow.js", + "console-service.js", + "document-events.js", +) |