summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/resources/utils
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/resources/utils')
-rw-r--r--devtools/server/actors/resources/utils/content-process-storage.js218
-rw-r--r--devtools/server/actors/resources/utils/moz.build13
-rw-r--r--devtools/server/actors/resources/utils/nsi-console-listener-watcher.js187
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;