diff options
Diffstat (limited to 'devtools/server/actors/resources/utils')
3 files changed, 418 insertions, 0 deletions
diff --git a/devtools/server/actors/resources/utils/content-process-storage.js b/devtools/server/actors/resources/utils/content-process-storage.js new file mode 100644 index 0000000000..cf07fe3e48 --- /dev/null +++ b/devtools/server/actors/resources/utils/content-process-storage.js @@ -0,0 +1,218 @@ +/* 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 { storageTypePool } = require("devtools/server/actors/storage"); + +// ms of delay to throttle updates +const BATCH_DELAY = 200; + +class ContentProcessStorage { + constructor(storageKey, storageType) { + this.storageKey = storageKey; + this.storageType = storageType; + } + + async watch(targetActor, { onAvailable, onUpdated, onDestroyed }) { + const ActorConstructor = storageTypePool.get(this.storageKey); + this.actor = new ActorConstructor({ + get conn() { + return targetActor.conn; + }, + get windows() { + // about:blank pages that are included via an iframe, do not get their + // own process, and they will be present in targetActor.windows. + // We need to ignore them unless they are the top level page. + // Otherwise about:blank loads with the same principal as their parent document + // and would expose the same storage values as its parent. + const windows = targetActor.windows.filter(win => { + const isTopPage = win.parent === win; + return isTopPage || win.location.href !== "about:blank"; + }); + + return windows; + }, + get window() { + return targetActor.window; + }, + get document() { + return this.window.document; + }, + get originAttributes() { + return this.document.effectiveStoragePrincipal.originAttributes; + }, + + update(action, storeType, data) { + if (!this.boundUpdate) { + this.boundUpdate = {}; + } + + if (action === "cleared") { + const response = {}; + response[this.storageKey] = data; + + onDestroyed([ + { + // needs this so the resource gets passed as an actor + // ...storages[storageKey], + ...storage, + clearedHostsOrPaths: data, + }, + ]); + } + + if (this.batchTimer) { + clearTimeout(this.batchTimer); + } + + if (!this.boundUpdate[action]) { + this.boundUpdate[action] = {}; + } + if (!this.boundUpdate[action][storeType]) { + this.boundUpdate[action][storeType] = {}; + } + for (const host in data) { + if (!this.boundUpdate[action][storeType][host]) { + this.boundUpdate[action][storeType][host] = []; + } + for (const name of data[host]) { + if (!this.boundUpdate[action][storeType][host].includes(name)) { + this.boundUpdate[action][storeType][host].push(name); + } + } + } + + if (action === "added") { + // If the same store name was previously deleted or changed, but now + // is added somehow, don't send the deleted or changed update + this._removeNamesFromUpdateList("deleted", storeType, data); + this._removeNamesFromUpdateList("changed", storeType, data); + } else if ( + action === "changed" && + this.boundUpdate?.added?.[storeType] + ) { + // If something got added and changed at the same time, then remove + // those items from changed instead. + this._removeNamesFromUpdateList( + "changed", + storeType, + this.boundUpdate.added[storeType] + ); + } else if (action === "deleted") { + // If any item got deleted, or a host got deleted, there's no point + // in sending added or changed upate, so we remove them. + this._removeNamesFromUpdateList("added", storeType, data); + this._removeNamesFromUpdateList("changed", storeType, data); + + for (const host in data) { + if ( + data[host].length === 0 && + this.boundUpdate?.added?.[storeType]?.[host] + ) { + delete this.boundUpdate.added[storeType][host]; + } + + if ( + data[host].length === 0 && + this.boundUpdate?.changed?.[storeType]?.[host] + ) { + delete this.boundUpdate.changed[storeType][host]; + } + } + } + + this.batchTimer = setTimeout(() => { + clearTimeout(this.batchTimer); + onUpdated([ + { + // needs this so the resource gets passed as an actor + // ...storages[storageKey], + ...storage, + added: this.boundUpdate.added, + changed: this.boundUpdate.changed, + deleted: this.boundUpdate.deleted, + }, + ]); + this.boundUpdate = {}; + }, BATCH_DELAY); + + return null; + }, + /** + * This method removes data from the this.boundUpdate object in the same + * manner like this.update() adds data to it. + * + * @param {string} action + * The type of change. One of "added", "changed" or "deleted" + * @param {string} storeType + * The storage actor for which you want to remove the updates data. + * @param {object} data + * The update object. This object is of the following format: + * - { + * <host1>: [<store_names1>, <store_name2>...], + * <host2>: [<store_names34>...], + * } + * Where host1, host2 are the hosts which you want to remove and + * [<store_namesX] is an array of the names of the store objects. + **/ + _removeNamesFromUpdateList(action, storeType, data) { + for (const host in data) { + if (this.boundUpdate?.[action]?.[storeType]?.[host]) { + for (const name in data[host]) { + const index = this.boundUpdate[action][storeType][host].indexOf( + name + ); + if (index > -1) { + this.boundUpdate[action][storeType][host].splice(index, 1); + } + } + if (!this.boundUpdate[action][storeType][host].length) { + delete this.boundUpdate[action][storeType][host]; + } + } + } + return null; + }, + + on() { + targetActor.on.apply(this, arguments); + }, + off() { + targetActor.off.apply(this, arguments); + }, + once() { + targetActor.once.apply(this, arguments); + }, + }); + + // We have to manage the actor manually, because ResourceWatcher doesn't + // use the protocol.js specification. + // resource-available-form is typed as "json" + // So that we have to manually handle stuff that would normally be + // automagically done by procotol.js + // 1) Manage the actor in order to have an actorID on it + targetActor.manage(this.actor); + // 2) Convert to JSON "form" + const form = this.actor.form(); + + // NOTE: this is hoisted, so the `update` method above may use it. + const storage = form; + + // All resources should have a resourceType, resourceId and resourceKey + // attributes, so available/updated/destroyed callbacks work properly. + storage.resourceType = this.storageType; + storage.resourceId = this.storageType; + storage.resourceKey = this.storageKey; + + onAvailable([storage]); + } + + destroy() { + this.actor?.destroy(); + this.actor = null; + } +} + +module.exports = ContentProcessStorage; diff --git a/devtools/server/actors/resources/utils/moz.build b/devtools/server/actors/resources/utils/moz.build new file mode 100644 index 0000000000..b57360d218 --- /dev/null +++ b/devtools/server/actors/resources/utils/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( + "content-process-storage.js", + "nsi-console-listener-watcher.js", +) + +with Files("nsi-console-listener-watcher.js"): + BUG_COMPONENT = ("DevTools", "Console") diff --git a/devtools/server/actors/resources/utils/nsi-console-listener-watcher.js b/devtools/server/actors/resources/utils/nsi-console-listener-watcher.js new file mode 100644 index 0000000000..352d622c30 --- /dev/null +++ b/devtools/server/actors/resources/utils/nsi-console-listener-watcher.js @@ -0,0 +1,187 @@ +/* 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 { Ci, Cu } = require("chrome"); +const Services = require("Services"); +const ChromeUtils = require("ChromeUtils"); + +const { createStringGrip } = require("devtools/server/actors/object/utils"); + +const { + getActorIdForInternalSourceId, +} = require("devtools/server/actors/utils/dbg-source"); + +class nsIConsoleListenerWatcher { + /** + * Start watching for all messages related to a given Target Actor. + * This will notify about existing messages, as well as those created in the future. + * + * @param TargetActor targetActor + * The target actor from which we should observe messages + * @param Object options + * Dictionary object with following attributes: + * - onAvailable: mandatory function + * This will be called for each resource. + */ + async watch(targetActor, { onAvailable }) { + if (!this.shouldHandleTarget(targetActor)) { + return; + } + + // The following code expects the ThreadActor to be instantiated (in prepareStackForRemote) + // The Thread Actor is instantiated via Target.attach, but we should probably review + // this and only instantiate the actor instead of attaching the target. + if (!targetActor.threadActor) { + targetActor.attach(); + } + + // Create the consoleListener. + const listener = { + QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]), + observe: message => { + if (!this.shouldHandleMessage(targetActor, message)) { + return; + } + + onAvailable([this.buildResource(targetActor, message)]); + }, + }; + + // Retrieve the cached messages just before registering the listener, so we don't get + // duplicated messages. + const cachedMessages = Services.console.getMessageArray() || []; + Services.console.registerListener(listener); + this.listener = listener; + + // Remove unwanted cache messages and send an array of resources. + const messages = []; + for (const message of cachedMessages) { + if (!this.shouldHandleMessage(targetActor, message)) { + continue; + } + + messages.push(this.buildResource(targetActor, message)); + } + onAvailable(messages); + } + + /** + * Return false if the watcher shouldn't be created. + * + * @param {TargetActor} targetActor + * @return {Boolean} + */ + shouldHandleTarget(targetActor) { + return true; + } + + /** + * Return true if you want the passed message to be handled by the watcher. This should + * be implemented on the child class. + * + * @param {TargetActor} targetActor + * @param {nsIScriptError|nsIConsoleMessage} message + * @return {Boolean} + */ + shouldHandleMessage(targetActor, message) { + throw new Error( + "'shouldHandleMessage' should be implemented in the class that extends nsIConsoleListenerWatcher" + ); + } + + /** + * Prepare the resource to be sent to the client. This should be implemented on the + * child class. + * + * @param targetActor + * @param nsIScriptError|nsIConsoleMessage message + * @return object + * The object you can send to the remote client. + */ + buildResource(targetActor, message) { + throw new Error( + "'buildResource' should be implemented in the class that extends nsIConsoleListenerWatcher" + ); + } + + /** + * Prepare a SavedFrame stack to be sent to the client. + * + * @param {TargetActor} targetActor + * @param {SavedFrame} errorStack + * Stack for an error we need to send to the client. + * @return object + * The object you can send to the remote client. + */ + prepareStackForRemote(targetActor, errorStack) { + // Convert stack objects to the JSON attributes expected by client code + // Bug 1348885: If the global from which this error came from has been + // nuked, stack is going to be a dead wrapper. + if (!errorStack || (Cu && Cu.isDeadWrapper(errorStack))) { + return null; + } + const stack = []; + let s = errorStack; + while (s) { + stack.push({ + filename: s.source, + sourceId: getActorIdForInternalSourceId(targetActor, s.sourceId), + lineNumber: s.line, + columnNumber: s.column, + functionName: s.functionDisplayName, + asyncCause: s.asyncCause ? s.asyncCause : undefined, + }); + s = s.parent || s.asyncParent; + } + return stack; + } + + /** + * Prepare error notes to be sent to the client. + * + * @param {TargetActor} targetActor + * @param {nsIArray<nsIScriptErrorNote>} errorNotes + * @return object + * The object you can send to the remote client. + */ + prepareNotesForRemote(targetActor, errorNotes) { + if (!errorNotes?.length) { + return null; + } + + const notes = []; + for (let i = 0, len = errorNotes.length; i < len; i++) { + const note = errorNotes.queryElementAt(i, Ci.nsIScriptErrorNote); + notes.push({ + messageBody: createStringGrip(targetActor, note.errorMessage), + frame: { + source: note.sourceName, + sourceId: getActorIdForInternalSourceId(targetActor, note.sourceId), + line: note.lineNumber, + column: note.columnNumber, + }, + }); + } + return notes; + } + + isProcessTarget(targetActor) { + const { typeName } = targetActor; + return ( + typeName === "parentProcessTarget" || typeName === "contentProcessTarget" + ); + } + + /** + * Stop watching for messages. + */ + destroy() { + if (this.listener) { + Services.console.unregisterListener(this.listener); + } + } +} +module.exports = nsIConsoleListenerWatcher; |