summaryrefslogtreecommitdiffstats
path: root/devtools/shared/protocol
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/protocol')
-rw-r--r--devtools/shared/protocol/Actor.js260
-rw-r--r--devtools/shared/protocol/Actor/generateActorSpec.js62
-rw-r--r--devtools/shared/protocol/Actor/moz.build8
-rw-r--r--devtools/shared/protocol/Front.js411
-rw-r--r--devtools/shared/protocol/Front/FrontClassWithSpec.js118
-rw-r--r--devtools/shared/protocol/Front/moz.build8
-rw-r--r--devtools/shared/protocol/Pool.js220
-rw-r--r--devtools/shared/protocol/Request.js169
-rw-r--r--devtools/shared/protocol/Response.js119
-rw-r--r--devtools/shared/protocol/lazy-pool.js224
-rw-r--r--devtools/shared/protocol/moz.build22
-rw-r--r--devtools/shared/protocol/tests/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/protocol/tests/xpcshell/head.js99
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js79
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_async.js192
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_children.js700
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_index.js52
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js52
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js27
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js310
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js316
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js98
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_types.js65
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js41
-rw-r--r--devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js183
-rw-r--r--devtools/shared/protocol/tests/xpcshell/xpcshell.ini19
-rw-r--r--devtools/shared/protocol/types.js587
-rw-r--r--devtools/shared/protocol/utils.js44
28 files changed, 4491 insertions, 0 deletions
diff --git a/devtools/shared/protocol/Actor.js b/devtools/shared/protocol/Actor.js
new file mode 100644
index 0000000000..ea37f6fea2
--- /dev/null
+++ b/devtools/shared/protocol/Actor.js
@@ -0,0 +1,260 @@
+/* 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";
+
+var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
+
+/**
+ * Keep track of which actorSpecs have been created. If a replica of a spec
+ * is created, it can be caught, and specs which inherit from other specs will
+ * not overwrite eachother.
+ */
+var actorSpecs = new WeakMap();
+
+exports.actorSpecs = actorSpecs;
+
+/**
+ * An actor in the actor tree.
+ *
+ * @param optional conn
+ * Either a DevToolsServerConnection or a DevToolsClient. Must have
+ * addActorPool, removeActorPool, and poolFor.
+ * conn can be null if the subclass provides a conn property.
+ * @constructor
+ */
+
+class Actor extends Pool {
+ constructor(conn, spec) {
+ super(conn);
+
+ this.typeName = spec.typeName;
+
+ // Will contain the actor's ID
+ this.actorID = null;
+
+ // Ensure computing requestTypes only one time per class
+ const proto = Object.getPrototypeOf(this);
+ if (!proto.requestTypes) {
+ proto.requestTypes = generateRequestTypes(spec);
+ }
+
+ // Forward events to the connection.
+ if (spec.events) {
+ for (const [name, request] of spec.events.entries()) {
+ this.on(name, (...args) => {
+ this._sendEvent(name, request, ...args);
+ });
+ }
+ }
+ }
+
+ toString() {
+ return "[Actor " + this.typeName + "/" + this.actorID + "]";
+ }
+
+ _sendEvent(name, request, ...args) {
+ if (this.isDestroyed()) {
+ console.error(
+ `Tried to send a '${name}' event on an already destroyed actor` +
+ ` '${this.typeName}'`
+ );
+ return;
+ }
+ let packet;
+ try {
+ packet = request.write(args, this);
+ } catch (ex) {
+ console.error("Error sending event: " + name);
+ throw ex;
+ }
+ packet.from = packet.from || this.actorID;
+ this.conn.send(packet);
+
+ // This can really be a hot path, even computing the marker label can
+ // have some performance impact.
+ // Guard against missing `Services.profiler` because Services is mocked to
+ // an empty object in the worker loader.
+ if (Services.profiler?.IsActive()) {
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Actor",
+ null,
+ `${this.typeName}.${name}`
+ );
+ }
+ }
+
+ destroy() {
+ super.destroy();
+ this.actorID = null;
+ this._isDestroyed = true;
+ }
+
+ /**
+ * Override this method in subclasses to serialize the actor.
+ * @param [optional] string hint
+ * Optional string to customize the form.
+ * @returns A jsonable object.
+ */
+ form(hint) {
+ return { actor: this.actorID };
+ }
+
+ writeError(error, typeName, method) {
+ console.error(
+ `Error while calling actor '${typeName}'s method '${method}'`,
+ error.message || error
+ );
+ // Also log the error object as-is in order to log the server side stack
+ // nicely in the console, while the previous log will log the client side stack only.
+ if (error.stack) {
+ console.error(error);
+ }
+
+ // Do not try to send the error if the actor is destroyed
+ // as the connection is probably also destroyed and may throw.
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ this.conn.send({
+ from: this.actorID,
+ // error.error -> errors created using the throwError() helper
+ // error.name -> errors created using `new Error` or Components.exception
+ // typeof(error)=="string" -> a method thrown like this `throw "a string"`
+ error:
+ error.error ||
+ error.name ||
+ (typeof error == "string" ? error : "unknownError"),
+ message: error.message,
+ // error.fileName -> regular Error instances
+ // error.filename -> errors created using Components.exception
+ fileName: error.fileName || error.filename,
+ lineNumber: error.lineNumber,
+ columnNumber: error.columnNumber,
+ });
+ }
+
+ _queueResponse(create) {
+ const pending = this._pendingResponse || Promise.resolve(null);
+ const response = create(pending);
+ this._pendingResponse = response;
+ }
+
+ /**
+ * Throw an error with the passed message and attach an `error` property to the Error
+ * object so it can be consumed by the writeError function.
+ * @param {String} error: A string (usually a single word serving as an id) that will
+ * be assign to error.error.
+ * @param {String} message: The string that will be passed to the Error constructor.
+ * @throws This always throw.
+ */
+ throwError(error, message) {
+ const err = new Error(message);
+ err.error = error;
+ throw err;
+ }
+}
+
+exports.Actor = Actor;
+
+/**
+ * Generate the "requestTypes" object used by DevToolsServerConnection to implement RDP.
+ * When a RDP packet is received for calling an actor method, this lookup for
+ * the method name in this object and call the function holded on this attribute.
+ *
+ * @params {Object} actorSpec
+ * The procotol-js actor specific coming from devtools/shared/specs/*.js files
+ * This describes the types for methods and events implemented by all actors.
+ * @return {Object} requestTypes
+ * An object where attributes are actor method names
+ * and values are function implementing these methods.
+ * These methods receive a RDP Packet (JSON-serializable object) and a DevToolsServerConnection.
+ * We expect them to return a promise that reserves with the response object
+ * to send back to the client (JSON-serializable object).
+ */
+var generateRequestTypes = function (actorSpec) {
+ // Generate request handlers for each method definition
+ const requestTypes = Object.create(null);
+ actorSpec.methods.forEach(spec => {
+ const handler = function (packet, conn) {
+ try {
+ const startTime = isWorker ? null : Cu.now();
+ let args;
+ try {
+ args = spec.request.read(packet, this);
+ } catch (ex) {
+ console.error("Error reading request: " + packet.type);
+ throw ex;
+ }
+
+ if (!this[spec.name]) {
+ throw new Error(
+ `Spec for '${actorSpec.typeName}' specifies a '${spec.name}'` +
+ ` method that isn't implemented by the actor`
+ );
+ }
+ const ret = this[spec.name].apply(this, args);
+
+ const sendReturn = retToSend => {
+ if (spec.oneway) {
+ // No need to send a response.
+ return;
+ }
+ if (this.isDestroyed()) {
+ console.error(
+ `Tried to send a '${spec.name}' method reply on an already destroyed actor` +
+ ` '${this.typeName}'`
+ );
+ return;
+ }
+
+ let response;
+ try {
+ response = spec.response.write(retToSend, this);
+ } catch (ex) {
+ console.error("Error writing response to: " + spec.name);
+ throw ex;
+ }
+ response.from = this.actorID;
+ // If spec.release has been specified, destroy the object.
+ if (spec.release) {
+ try {
+ this.destroy();
+ } catch (e) {
+ this.writeError(e, actorSpec.typeName, spec.name);
+ return;
+ }
+ }
+
+ conn.send(response);
+
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Actor",
+ startTime,
+ `${actorSpec.typeName}:${spec.name}()`
+ );
+ };
+
+ this._queueResponse(p => {
+ return p
+ .then(() => ret)
+ .then(sendReturn)
+ .catch(e => this.writeError(e, actorSpec.typeName, spec.name));
+ });
+ } catch (e) {
+ this._queueResponse(p => {
+ return p.then(() =>
+ this.writeError(e, actorSpec.typeName, spec.name)
+ );
+ });
+ }
+ };
+
+ requestTypes[spec.request.type] = handler;
+ });
+
+ return requestTypes;
+};
+exports.generateRequestTypes = generateRequestTypes;
diff --git a/devtools/shared/protocol/Actor/generateActorSpec.js b/devtools/shared/protocol/Actor/generateActorSpec.js
new file mode 100644
index 0000000000..9b3b166c8b
--- /dev/null
+++ b/devtools/shared/protocol/Actor/generateActorSpec.js
@@ -0,0 +1,62 @@
+/* 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";
+
+var { Request } = require("resource://devtools/shared/protocol/Request.js");
+const { Response } = require("resource://devtools/shared/protocol/Response.js");
+var {
+ types,
+ registeredTypes,
+} = require("resource://devtools/shared/protocol/types.js");
+
+/**
+ * Generates an actor specification from an actor description.
+ */
+var generateActorSpec = function (actorDesc) {
+ const actorSpec = {
+ typeName: actorDesc.typeName,
+ methods: [],
+ };
+
+ // Find additional method specifications
+ if (actorDesc.methods) {
+ for (const name in actorDesc.methods) {
+ const methodSpec = actorDesc.methods[name];
+ const spec = {};
+
+ spec.name = methodSpec.name || name;
+ spec.request = new Request(
+ Object.assign({ type: spec.name }, methodSpec.request || undefined)
+ );
+ spec.response = new Response(methodSpec.response || undefined);
+ spec.release = methodSpec.release;
+ spec.oneway = methodSpec.oneway;
+
+ actorSpec.methods.push(spec);
+ }
+ }
+
+ // Find event specifications
+ if (actorDesc.events) {
+ actorSpec.events = new Map();
+ for (const name in actorDesc.events) {
+ const eventRequest = actorDesc.events[name];
+ Object.freeze(eventRequest);
+ actorSpec.events.set(
+ name,
+ new Request(Object.assign({ type: name }, eventRequest))
+ );
+ }
+ }
+
+ if (!registeredTypes.has(actorSpec.typeName)) {
+ types.addActorType(actorSpec.typeName);
+ }
+ registeredTypes.get(actorSpec.typeName).actorSpec = actorSpec;
+
+ return actorSpec;
+};
+
+exports.generateActorSpec = generateActorSpec;
diff --git a/devtools/shared/protocol/Actor/moz.build b/devtools/shared/protocol/Actor/moz.build
new file mode 100644
index 0000000000..3e3c46d49b
--- /dev/null
+++ b/devtools/shared/protocol/Actor/moz.build
@@ -0,0 +1,8 @@
+# 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(
+ "generateActorSpec.js",
+)
diff --git a/devtools/shared/protocol/Front.js b/devtools/shared/protocol/Front.js
new file mode 100644
index 0000000000..32aed37b35
--- /dev/null
+++ b/devtools/shared/protocol/Front.js
@@ -0,0 +1,411 @@
+/* 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";
+
+var { settleAll } = require("resource://devtools/shared/DevToolsUtils.js");
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
+var {
+ getStack,
+ callFunctionWithAsyncStack,
+} = require("resource://devtools/shared/platform/stack.js");
+const defer = require("resource://devtools/shared/defer.js");
+
+/**
+ * Base class for client-side actor fronts.
+ *
+ * @param [DevToolsClient|null] conn
+ * The conn must either be DevToolsClient or null. Must have
+ * addActorPool, removeActorPool, and poolFor.
+ * conn can be null if the subclass provides a conn property.
+ * @param [Target|null] target
+ * If we are instantiating a target-scoped front, this is a reference to the front's
+ * Target instance, otherwise this is null.
+ * @param [Front|null] parentFront
+ * The parent front. This is only available if the Front being initialized is a child
+ * of a parent front.
+ * @constructor
+ */
+class Front extends Pool {
+ constructor(conn = null, targetFront = null, parentFront = null) {
+ super(conn);
+ if (!conn) {
+ throw new Error("Front without conn");
+ }
+ this.actorID = null;
+ // The targetFront attribute represents the debuggable context. Only target-scoped
+ // fronts and their children fronts will have the targetFront attribute set.
+ this.targetFront = targetFront;
+ // The parentFront attribute points to its parent front. Only children of
+ // target-scoped fronts will have the parentFront attribute set.
+ this.parentFront = parentFront;
+ this._requests = [];
+
+ // Front listener functions registered via `watchFronts`
+ this._frontCreationListeners = null;
+ this._frontDestructionListeners = null;
+
+ // List of optional listener for each event, that is processed immediatly on packet
+ // receival, before emitting event via EventEmitter on the Front.
+ // These listeners are register via Front.before function.
+ // Map(Event Name[string] => Event Listener[function])
+ this._beforeListeners = new Map();
+
+ // This flag allows to check if the `initialize` method has resolved.
+ // Used to avoid notifying about initialized fronts in `watchFronts`.
+ this._initializeResolved = false;
+ }
+
+ /**
+ * Return the parent front.
+ */
+ getParent() {
+ return this.parentFront && this.parentFront.actorID
+ ? this.parentFront
+ : null;
+ }
+
+ destroy() {
+ // Prevent destroying twice if a `forwardCancelling` event has already been received
+ // and already called `baseFrontClassDestroy`
+ this.baseFrontClassDestroy();
+
+ // Keep `clearEvents` out of baseFrontClassDestroy as we still want TargetMixin to be
+ // able to emit `target-destroyed` after we called baseFrontClassDestroy from DevToolsClient.purgeRequests.
+ this.clearEvents();
+ }
+
+ // This method is also called from `DevToolsClient`, when a connector is destroyed
+ // and we should:
+ // - reject all pending request made to the remote process/target/thread.
+ // - avoid trying to do new request against this remote context.
+ // - unmanage this front, so that DevToolsClient.getFront no longer returns it.
+ //
+ // When a connector is destroyed a `forwardCancelling` RDP event is sent by the server.
+ // This is done in a distinct method from `destroy` in order to do all that immediately,
+ // even if `Front.destroy` is overloaded by an async method.
+ baseFrontClassDestroy() {
+ // Reject all outstanding requests, they won't make sense after
+ // the front is destroyed.
+ while (this._requests.length) {
+ const { deferred, to, type, stack } = this._requests.shift();
+ // Note: many tests are ignoring `Connection closed` promise rejections,
+ // via PromiseTestUtils.allowMatchingRejectionsGlobally.
+ // Do not update the message without updating the tests.
+ const msg =
+ "Connection closed, pending request to " +
+ to +
+ ", type " +
+ type +
+ " failed" +
+ "\n\nRequest stack:\n" +
+ stack.formattedStack;
+ deferred.reject(new Error(msg));
+ }
+
+ if (this.actorID) {
+ super.destroy();
+ this.actorID = null;
+ }
+ this._isDestroyed = true;
+
+ this.targetFront = null;
+ this.parentFront = null;
+ this._frontCreationListeners = null;
+ this._frontDestructionListeners = null;
+ this._beforeListeners = null;
+ }
+
+ async manage(front, form, ctx) {
+ if (!front.actorID) {
+ throw new Error(
+ "Can't manage front without an actor ID.\n" +
+ "Ensure server supports " +
+ front.typeName +
+ "."
+ );
+ }
+
+ if (front.parentFront && front.parentFront !== this) {
+ throw new Error(
+ `${this.actorID} (${this.typeName}) can't manage ${front.actorID}
+ (${front.typeName}) since it has a different parentFront ${
+ front.parentFront
+ ? front.parentFront.actorID + "(" + front.parentFront.typeName + ")"
+ : "<no parentFront>"
+ }`
+ );
+ }
+
+ super.manage(front);
+
+ if (typeof front.initialize == "function") {
+ await front.initialize();
+ }
+ front._initializeResolved = true;
+
+ // Ensure calling form() *before* notifying about this front being just created.
+ // We exprect the front to be fully initialized, especially via its form attributes.
+ // But do that *after* calling manage() so that the front is already registered
+ // in Pools and can be fetched by its ID, in case a child actor, created in form()
+ // tries to get a reference to its parent via the actor ID.
+ if (form) {
+ front.form(form, ctx);
+ }
+
+ // Call listeners registered via `watchFronts` method
+ // (ignore if this front has been destroyed)
+ if (this._frontCreationListeners) {
+ this._frontCreationListeners.emit(front.typeName, front);
+ }
+ }
+
+ async unmanage(front) {
+ super.unmanage(front);
+
+ // Call listeners registered via `watchFronts` method
+ if (this._frontDestructionListeners) {
+ this._frontDestructionListeners.emit(front.typeName, front);
+ }
+ }
+
+ /*
+ * Listen for the creation and/or destruction of fronts matching one of the provided types.
+ *
+ * @param {String} typeName
+ * Actor type to watch.
+ * @param {Function} onAvailable (optional)
+ * Callback fired when a front has been just created or was already available.
+ * The function is called with one arguments, the front.
+ * @param {Function} onDestroy (optional)
+ * Callback fired in case of front destruction.
+ * The function is called with the same argument than onAvailable.
+ */
+ watchFronts(typeName, onAvailable, onDestroy) {
+ if (this.isDestroyed()) {
+ // The front was already destroyed, bail out.
+ console.error(
+ `Tried to call watchFronts for the '${typeName}' type on an ` +
+ `already destroyed front '${this.typeName}'.`
+ );
+ return;
+ }
+
+ if (onAvailable) {
+ // First fire the callback on fronts with the correct type and which have
+ // been initialized. If initialize() is still in progress, the front will
+ // be emitted via _frontCreationListeners shortly after.
+ for (const front of this.poolChildren()) {
+ if (front.typeName == typeName && front._initializeResolved) {
+ onAvailable(front);
+ }
+ }
+
+ if (!this._frontCreationListeners) {
+ this._frontCreationListeners = new EventEmitter();
+ }
+ // Then register the callback for fronts instantiated in the future
+ this._frontCreationListeners.on(typeName, onAvailable);
+ }
+
+ if (onDestroy) {
+ if (!this._frontDestructionListeners) {
+ this._frontDestructionListeners = new EventEmitter();
+ }
+ this._frontDestructionListeners.on(typeName, onDestroy);
+ }
+ }
+
+ /**
+ * Stop listening for the creation and/or destruction of a given type of fronts.
+ * See `watchFronts()` for documentation of the arguments.
+ */
+ unwatchFronts(typeName, onAvailable, onDestroy) {
+ if (this.isDestroyed()) {
+ // The front was already destroyed, bail out.
+ console.error(
+ `Tried to call unwatchFronts for the '${typeName}' type on an ` +
+ `already destroyed front '${this.typeName}'.`
+ );
+ return;
+ }
+
+ if (onAvailable && this._frontCreationListeners) {
+ this._frontCreationListeners.off(typeName, onAvailable);
+ }
+ if (onDestroy && this._frontDestructionListeners) {
+ this._frontDestructionListeners.off(typeName, onDestroy);
+ }
+ }
+
+ /**
+ * Register an event listener that will be called immediately on packer receival.
+ * The given callback is going to be called before emitting the event via EventEmitter
+ * API on the Front. Event emitting will be delayed if the callback is async.
+ * Only one such listener can be registered per type of event.
+ *
+ * @param String type
+ * Event emitted by the actor to intercept.
+ * @param Function callback
+ * Function that will process the event.
+ */
+ before(type, callback) {
+ if (this._beforeListeners.has(type)) {
+ throw new Error(
+ `Can't register multiple before listeners for "${type}".`
+ );
+ }
+ this._beforeListeners.set(type, callback);
+ }
+
+ toString() {
+ return "[Front for " + this.typeName + "/" + this.actorID + "]";
+ }
+
+ /**
+ * Update the actor from its representation.
+ * Subclasses should override this.
+ */
+ form(form) {}
+
+ /**
+ * Send a packet on the connection.
+ */
+ send(packet) {
+ if (packet.to) {
+ this.conn._transport.send(packet);
+ } else {
+ packet.to = this.actorID;
+ // The connection might be closed during the promise resolution
+ if (this.conn && this.conn._transport) {
+ this.conn._transport.send(packet);
+ }
+ }
+ }
+
+ /**
+ * Send a two-way request on the connection.
+ */
+ request(packet) {
+ const deferred = defer();
+ // Save packet basics for debugging
+ const { to, type } = packet;
+ this._requests.push({
+ deferred,
+ to: to || this.actorID,
+ type,
+ stack: getStack(),
+ });
+ this.send(packet);
+ return deferred.promise;
+ }
+
+ /**
+ * Handler for incoming packets from the client's actor.
+ */
+ onPacket(packet) {
+ if (this.isDestroyed()) {
+ // If the Front was already destroyed, all the requests have been purged
+ // and rejected with detailed error messages in baseFrontClassDestroy.
+ return;
+ }
+
+ // Pick off event packets
+ const type = packet.type || undefined;
+ if (this._clientSpec.events && this._clientSpec.events.has(type)) {
+ const event = this._clientSpec.events.get(packet.type);
+ let args;
+ try {
+ args = event.request.read(packet, this);
+ } catch (ex) {
+ console.error("Error reading event: " + packet.type);
+ console.exception(ex);
+ throw ex;
+ }
+ // Check for "pre event" callback to be processed before emitting events on fronts
+ // Use event.name instead of packet.type to use specific event name instead of RDP
+ // packet's type.
+ const beforeEvent = this._beforeListeners.get(event.name);
+ if (beforeEvent) {
+ const result = beforeEvent.apply(this, args);
+ // Check to see if the beforeEvent returned a promise -- if so,
+ // wait for their resolution before emitting. Otherwise, emit synchronously.
+ if (result && typeof result.then == "function") {
+ result.then(() => {
+ super.emit(event.name, ...args);
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Front",
+ null,
+ `${this.typeName}.${event.name}`
+ );
+ });
+ return;
+ }
+ }
+
+ super.emit(event.name, ...args);
+ ChromeUtils.addProfilerMarker(
+ "DevTools:RDP Front",
+ null,
+ `${this.typeName}.${event.name}`
+ );
+ return;
+ }
+
+ // Remaining packets must be responses.
+ if (this._requests.length === 0) {
+ const msg =
+ "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
+ const err = Error(msg);
+ console.error(err);
+ throw err;
+ }
+
+ const { deferred, stack } = this._requests.shift();
+ callFunctionWithAsyncStack(
+ () => {
+ if (packet.error) {
+ let message;
+ if (packet.error && packet.message) {
+ message =
+ "Protocol error (" + packet.error + "): " + packet.message;
+ } else {
+ message = packet.error;
+ }
+ message += " from: " + this.actorID;
+ if (packet.fileName) {
+ const { fileName, columnNumber, lineNumber } = packet;
+ message += ` (${fileName}:${lineNumber}:${columnNumber})`;
+ }
+ const packetError = new Error(message);
+ deferred.reject(packetError);
+ } else {
+ deferred.resolve(packet);
+ }
+ },
+ stack,
+ "DevTools RDP"
+ );
+ }
+
+ hasRequests() {
+ return !!this._requests.length;
+ }
+
+ /**
+ * Wait for all current requests from this front to settle. This is especially useful
+ * for tests and other utility environments that may not have events or mechanisms to
+ * await the completion of requests without this utility.
+ *
+ * @return Promise
+ * Resolved when all requests have settled.
+ */
+ waitForRequestsToSettle() {
+ return settleAll(this._requests.map(({ deferred }) => deferred.promise));
+ }
+}
+
+exports.Front = Front;
diff --git a/devtools/shared/protocol/Front/FrontClassWithSpec.js b/devtools/shared/protocol/Front/FrontClassWithSpec.js
new file mode 100644
index 0000000000..55091be3e4
--- /dev/null
+++ b/devtools/shared/protocol/Front/FrontClassWithSpec.js
@@ -0,0 +1,118 @@
+/* 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";
+
+var { Front } = require("resource://devtools/shared/protocol/Front.js");
+
+/**
+ * Generates request methods as described by the given actor specification on
+ * the given front prototype. Returns the front prototype.
+ */
+var generateRequestMethods = function (actorSpec, frontProto) {
+ if (frontProto._actorSpec) {
+ throw new Error("frontProto called twice on the same front prototype!");
+ }
+
+ frontProto.typeName = actorSpec.typeName;
+
+ // Generate request methods.
+ const methods = actorSpec.methods;
+ methods.forEach(spec => {
+ const name = spec.name;
+
+ frontProto[name] = function (...args) {
+ // If the front is destroyed, the request will not be able to complete.
+ if (this.isDestroyed()) {
+ throw new Error(
+ `Can not send request '${name}' because front '${this.typeName}' is already destroyed.`
+ );
+ }
+
+ const startTime = Cu.now();
+ let packet;
+ try {
+ packet = spec.request.write(args, this);
+ } catch (ex) {
+ console.error("Error writing request: " + name);
+ throw ex;
+ }
+ if (spec.oneway) {
+ // Fire-and-forget oneway packets.
+ this.send(packet);
+ return undefined;
+ }
+
+ return this.request(packet).then(response => {
+ let ret;
+ if (!this.conn) {
+ throw new Error("Missing conn on " + this);
+ }
+ if (this.isDestroyed()) {
+ throw new Error(
+ `Can not interpret '${name}' response because front '${this.typeName}' is already destroyed.`
+ );
+ }
+ try {
+ ret = spec.response.read(response, this);
+ } catch (ex) {
+ console.error("Error reading response to: " + name + "\n" + ex);
+ throw ex;
+ }
+ ChromeUtils.addProfilerMarker(
+ "RDP Front",
+ startTime,
+ `${this.typeName}:${name}()`
+ );
+ return ret;
+ });
+ };
+
+ // Release methods should call the destroy function on return.
+ if (spec.release) {
+ const fn = frontProto[name];
+ frontProto[name] = function (...args) {
+ return fn.apply(this, args).then(result => {
+ this.destroy();
+ return result;
+ });
+ };
+ }
+ });
+
+ // Process event specifications
+ frontProto._clientSpec = {};
+
+ const actorEvents = actorSpec.events;
+ if (actorEvents) {
+ frontProto._clientSpec.events = new Map();
+
+ for (const [name, request] of actorEvents) {
+ frontProto._clientSpec.events.set(request.type, {
+ name,
+ request,
+ });
+ }
+ }
+
+ frontProto._actorSpec = actorSpec;
+
+ return frontProto;
+};
+
+/**
+ * Create a front class for the given actor specification and front prototype.
+ *
+ * @param object actorSpec
+ * The actor specification you're creating a front for.
+ * @param object proto
+ * The object prototype. Must have a 'typeName' property,
+ * should have method definitions, can have event definitions.
+ */
+var FrontClassWithSpec = function (actorSpec) {
+ class OneFront extends Front {}
+ generateRequestMethods(actorSpec, OneFront.prototype);
+ return OneFront;
+};
+exports.FrontClassWithSpec = FrontClassWithSpec;
diff --git a/devtools/shared/protocol/Front/moz.build b/devtools/shared/protocol/Front/moz.build
new file mode 100644
index 0000000000..75b36c5eab
--- /dev/null
+++ b/devtools/shared/protocol/Front/moz.build
@@ -0,0 +1,8 @@
+# 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(
+ "FrontClassWithSpec.js",
+)
diff --git a/devtools/shared/protocol/Pool.js b/devtools/shared/protocol/Pool.js
new file mode 100644
index 0000000000..b5cb4c3eb1
--- /dev/null
+++ b/devtools/shared/protocol/Pool.js
@@ -0,0 +1,220 @@
+/* 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";
+
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+/**
+ * Actor and Front implementations
+ */
+
+/**
+ * A protocol object that can manage the lifetime of other protocol
+ * objects. Pools are used on both sides of the connection to help coordinate lifetimes.
+ *
+ * @param {DevToolsServerConnection|DevToolsClient} [conn]
+ * Either a DevToolsServerConnection or a DevToolsClient. Must have
+ * addActorPool, removeActorPool, and poolFor.
+ * conn can be null if the subclass provides a conn property.
+ * @param {String} [label]
+ * An optional label for the Pool.
+ * @constructor
+ */
+class Pool extends EventEmitter {
+ constructor(conn, label) {
+ super();
+
+ if (conn) {
+ this.conn = conn;
+ }
+ this.label = label;
+
+ // Will be individually flipped to true by Actor/Front classes.
+ // Will also only be exposed via Actor/Front::isDestroyed().
+ this._isDestroyed = false;
+ }
+
+ __poolMap = null;
+ parentPool = null;
+
+ /**
+ * Return the parent pool for this client.
+ */
+ getParent() {
+ return this.parentPool;
+ }
+
+ /**
+ * A pool is at the top of its pool hierarchy if it has:
+ * - no parent
+ * - or it is its own parent
+ */
+ isTopPool() {
+ const parent = this.getParent();
+ return !parent || parent === this;
+ }
+
+ poolFor(actorID) {
+ return this.conn.poolFor(actorID);
+ }
+
+ /**
+ * Override this if you want actors returned by this actor
+ * to belong to a different actor by default.
+ */
+ marshallPool() {
+ return this;
+ }
+
+ /**
+ * Pool is the base class for all actors, even leaf nodes.
+ * If the child map is actually referenced, go ahead and create
+ * the stuff needed by the pool.
+ */
+ get _poolMap() {
+ if (this.__poolMap) {
+ return this.__poolMap;
+ }
+ this.__poolMap = new Map();
+ this.conn.addActorPool(this);
+ return this.__poolMap;
+ }
+
+ /**
+ * Add an actor as a child of this pool.
+ */
+ manage(actor) {
+ if (!actor.actorID) {
+ actor.actorID = this.conn.allocID(actor.typeName);
+ } else {
+ // If the actor is already registered in a pool, remove it without destroying it.
+ // This happens for example when an addon is reloaded. To see this behavior, take a
+ // look at devtools/server/tests/xpcshell/test_addon_reload.js
+
+ const parent = actor.getParent();
+ if (parent && parent !== this) {
+ parent.unmanage(actor);
+ }
+ }
+
+ this._poolMap.set(actor.actorID, actor);
+ actor.parentPool = this;
+ }
+
+ unmanageChildren(FrontType) {
+ for (const front of this.poolChildren()) {
+ if (!FrontType || front instanceof FrontType) {
+ this.unmanage(front);
+ }
+ }
+ }
+
+ /**
+ * Remove an actor as a child of this pool.
+ */
+ unmanage(actor) {
+ if (this.__poolMap) {
+ this.__poolMap.delete(actor.actorID);
+ }
+ actor.parentPool = null;
+ }
+
+ // true if the given actor ID exists in the pool.
+ has(actorID) {
+ return this.__poolMap && this._poolMap.has(actorID);
+ }
+
+ /**
+ * Search for an actor in this pool, given an actorID
+ * @param {String} actorID
+ * @returns {Actor/null} Returns null if the actor wasn't found
+ */
+ getActorByID(actorID) {
+ if (this.__poolMap) {
+ return this._poolMap.get(actorID);
+ }
+ return null;
+ }
+
+ // Generator that yields each non-self child of the pool.
+ *poolChildren() {
+ if (!this.__poolMap) {
+ return;
+ }
+ for (const actor of this.__poolMap.values()) {
+ // Self-owned actors are ok, but don't need visiting twice.
+ if (actor === this) {
+ continue;
+ }
+ yield actor;
+ }
+ }
+
+ isDestroyed() {
+ // Note: _isDestroyed is only flipped from Actor and Front subclasses for
+ // now, so this method should not be called on pure Pool instances.
+ // See Bug 1717811.
+ return this._isDestroyed;
+ }
+
+ /**
+ * Pools can override this method in order to opt-out of a destroy sequence.
+ *
+ * For instance, Fronts are destroyed during the toolbox destroy. However when
+ * the toolbox is destroyed, the document holding the toolbox is also
+ * destroyed. So it should not be necessary to cleanup Fronts during toolbox
+ * destroy.
+ *
+ * For the time being, Fronts (or Pools in general) which want to opt-out of
+ * toolbox destroy can override this method and check the value of
+ * `this.conn.isToolboxDestroy`.
+ */
+ skipDestroy() {
+ return false;
+ }
+
+ /**
+ * Destroy this item, removing it from a parent if it has one,
+ * and destroying all children if necessary.
+ */
+ destroy() {
+ const parent = this.getParent();
+ if (parent) {
+ parent.unmanage(this);
+ }
+ if (!this.__poolMap) {
+ return;
+ }
+ // Immediately clear the poolmap so that we bail out early if the code is reentrant.
+ const poolMap = this.__poolMap;
+ this.__poolMap = null;
+
+ for (const actor of poolMap.values()) {
+ // Self-owned actors are ok, but don't need destroying twice.
+ if (actor === this) {
+ continue;
+ }
+
+ // Some pool-managed values don't extend Pool and won't have skipDestroy
+ // defined. For instance, the root actor and the lazy actors.
+ if (typeof actor.skipDestroy === "function" && actor.skipDestroy()) {
+ continue;
+ }
+
+ const destroy = actor.destroy;
+ if (destroy) {
+ // Disconnect destroy while we're destroying in case of (misbehaving)
+ // circular ownership.
+ actor.destroy = null;
+ destroy.call(actor);
+ actor.destroy = destroy;
+ }
+ }
+ this.conn.removeActorPool(this);
+ this.conn = null;
+ }
+}
+
+exports.Pool = Pool;
diff --git a/devtools/shared/protocol/Request.js b/devtools/shared/protocol/Request.js
new file mode 100644
index 0000000000..20befd938a
--- /dev/null
+++ b/devtools/shared/protocol/Request.js
@@ -0,0 +1,169 @@
+/* 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 { extend } = require("resource://devtools/shared/extend.js");
+var {
+ findPlaceholders,
+ getPath,
+} = require("resource://devtools/shared/protocol/utils.js");
+var { types } = require("resource://devtools/shared/protocol/types.js");
+
+/**
+ * Manages a request template.
+ *
+ * @param object template
+ * The request template.
+ * @construcor
+ */
+var Request = function (template = {}) {
+ this.type = template.type;
+ this.template = template;
+ this.args = findPlaceholders(template, Arg);
+};
+
+Request.prototype = {
+ /**
+ * Write a request.
+ *
+ * @param array fnArgs
+ * The function arguments to place in the request.
+ * @param object ctx
+ * The object making the request.
+ * @returns a request packet.
+ */
+ write(fnArgs, ctx) {
+ const ret = {};
+ for (const key in this.template) {
+ const value = this.template[key];
+ if (value instanceof Arg) {
+ ret[key] = value.write(
+ value.index in fnArgs ? fnArgs[value.index] : undefined,
+ ctx,
+ key
+ );
+ } else if (key == "type") {
+ ret[key] = value;
+ } else {
+ throw new Error(
+ "Request can only an object with `Arg` or `Option` properties"
+ );
+ }
+ }
+ return ret;
+ },
+
+ /**
+ * Read a request.
+ *
+ * @param object packet
+ * The request packet.
+ * @param object ctx
+ * The object making the request.
+ * @returns an arguments array
+ */
+ read(packet, ctx) {
+ const fnArgs = [];
+ for (const templateArg of this.args) {
+ const arg = templateArg.placeholder;
+ const path = templateArg.path;
+ const name = path[path.length - 1];
+ arg.read(getPath(packet, path), ctx, fnArgs, name);
+ }
+ return fnArgs;
+ },
+};
+
+exports.Request = Request;
+
+/**
+ * Request/Response templates and generation
+ *
+ * Request packets are specified as json templates with
+ * Arg and Option placeholders where arguments should be
+ * placed.
+ *
+ * Reponse packets are also specified as json templates,
+ * with a RetVal placeholder where the return value should be
+ * placed.
+ */
+
+/**
+ * Placeholder for simple arguments.
+ *
+ * @param number index
+ * The argument index to place at this position.
+ * @param type type
+ * The argument should be marshalled as this type.
+ * @constructor
+ */
+var Arg = function (index, type) {
+ this.index = index;
+ // Prevent force loading all Arg types by accessing it only when needed
+ loader.lazyGetter(this, "type", function () {
+ return types.getType(type);
+ });
+};
+
+Arg.prototype = {
+ write(arg, ctx) {
+ return this.type.write(arg, ctx);
+ },
+
+ read(v, ctx, outArgs) {
+ outArgs[this.index] = this.type.read(v, ctx);
+ },
+};
+
+// Outside of protocol.js, Arg is called as factory method, without the new keyword.
+exports.Arg = function (index, type) {
+ return new Arg(index, type);
+};
+
+/**
+ * Placeholder for an options argument value that should be hoisted
+ * into the packet.
+ *
+ * If provided in a method specification:
+ *
+ * { optionArg: Option(1)}
+ *
+ * Then arguments[1].optionArg will be placed in the packet in this
+ * value's place.
+ *
+ * @param number index
+ * The argument index of the options value.
+ * @param type type
+ * The argument should be marshalled as this type.
+ * @constructor
+ */
+var Option = function (index, type) {
+ Arg.call(this, index, type);
+};
+
+Option.prototype = extend(Arg.prototype, {
+ write(arg, ctx, name) {
+ // Ignore if arg is undefined or null; allow other falsy values
+ if (arg == undefined || arg[name] == undefined) {
+ return undefined;
+ }
+ const v = arg[name];
+ return this.type.write(v, ctx);
+ },
+ read(v, ctx, outArgs, name) {
+ if (outArgs[this.index] === undefined) {
+ outArgs[this.index] = {};
+ }
+ if (v === undefined) {
+ return;
+ }
+ outArgs[this.index][name] = this.type.read(v, ctx);
+ },
+});
+
+// Outside of protocol.js, Option is called as factory method, without the new keyword.
+exports.Option = function (index, type) {
+ return new Option(index, type);
+};
diff --git a/devtools/shared/protocol/Response.js b/devtools/shared/protocol/Response.js
new file mode 100644
index 0000000000..b456fe6149
--- /dev/null
+++ b/devtools/shared/protocol/Response.js
@@ -0,0 +1,119 @@
+/* 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";
+
+var {
+ findPlaceholders,
+ getPath,
+} = require("resource://devtools/shared/protocol/utils.js");
+var { types } = require("resource://devtools/shared/protocol/types.js");
+
+/**
+ * Manages a response template.
+ *
+ * @param object template
+ * The response template.
+ * @construcor
+ */
+var Response = function (template = {}) {
+ this.template = template;
+ if (this.template instanceof RetVal && this.template.isArrayType()) {
+ throw Error("Arrays should be wrapped in objects");
+ }
+
+ const placeholders = findPlaceholders(template, RetVal);
+ if (placeholders.length > 1) {
+ throw Error("More than one RetVal specified in response");
+ }
+ const placeholder = placeholders.shift();
+ if (placeholder) {
+ this.retVal = placeholder.placeholder;
+ this.path = placeholder.path;
+ }
+};
+
+Response.prototype = {
+ /**
+ * Write a response for the given return value.
+ *
+ * @param val ret
+ * The return value.
+ * @param object ctx
+ * The object writing the response.
+ */
+ write(ret, ctx) {
+ // Consider that `template` is either directly a `RetVal`,
+ // or a dictionary with may be one `RetVal`.
+ if (this.template instanceof RetVal) {
+ return this.template.write(ret, ctx);
+ }
+ const result = {};
+ for (const key in this.template) {
+ const value = this.template[key];
+ if (value instanceof RetVal) {
+ result[key] = value.write(ret, ctx);
+ } else {
+ throw new Error(
+ "Response can only be a `RetVal` instance or an object " +
+ "with one property being a `RetVal` instance."
+ );
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Read a return value from the given response.
+ *
+ * @param object packet
+ * The response packet.
+ * @param object ctx
+ * The object reading the response.
+ */
+ read(packet, ctx) {
+ if (!this.retVal) {
+ return undefined;
+ }
+ const v = getPath(packet, this.path);
+ return this.retVal.read(v, ctx);
+ },
+};
+
+exports.Response = Response;
+
+/**
+ * Placeholder for return values in a response template.
+ *
+ * @param type type
+ * The return value should be marshalled as this type.
+ */
+var RetVal = function (type) {
+ this._type = type;
+ // Prevent force loading all RetVal types by accessing it only when needed
+ loader.lazyGetter(this, "type", function () {
+ return types.getType(type);
+ });
+};
+
+RetVal.prototype = {
+ write(v, ctx) {
+ return this.type.write(v, ctx);
+ },
+
+ read(v, ctx) {
+ return this.type.read(v, ctx);
+ },
+
+ isArrayType() {
+ // `_type` should always be a string, but a few incorrect RetVal calls
+ // pass `0`. See Bug 1677703.
+ return typeof this._type === "string" && this._type.startsWith("array:");
+ },
+};
+
+// Outside of protocol.js, RetVal is called as factory method, without the new keyword.
+exports.RetVal = function (type) {
+ return new RetVal(type);
+};
diff --git a/devtools/shared/protocol/lazy-pool.js b/devtools/shared/protocol/lazy-pool.js
new file mode 100644
index 0000000000..0829fef1e0
--- /dev/null
+++ b/devtools/shared/protocol/lazy-pool.js
@@ -0,0 +1,224 @@
+/* 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 { extend } = require("devtools/shared/extend");
+const { Pool } = require("devtools/shared/protocol");
+
+/**
+ * A Special Pool for RootActor and WindowGlobalTargetActor, which allows lazy loaded
+ * actors to be added to the pool.
+ *
+ * Like the Pool, this is a protocol object that can manage the lifetime of other protocol
+ * objects. Pools are used on both sides of the connection to help coordinate lifetimes.
+ *
+ * @param conn
+ * Is a DevToolsServerConnection. Must have
+ * addActorPool, removeActorPool, and poolFor.
+ * @constructor
+ */
+function LazyPool(conn) {
+ this.conn = conn;
+}
+
+LazyPool.prototype = extend(Pool.prototype, {
+ // The actor for a given actor id stored in this pool
+ getActorByID(actorID) {
+ if (this.__poolMap) {
+ const entry = this._poolMap.get(actorID);
+ if (entry instanceof LazyActor) {
+ return entry.createActor();
+ }
+ return entry;
+ }
+ return null;
+ },
+});
+
+exports.LazyPool = LazyPool;
+
+/**
+ * Populate |parent._extraActors| as specified by |registeredActors|, reusing whatever
+ * actors are already there. Add all actors in the final extra actors table to
+ * |pool|. _extraActors is treated as a cache for lazy actors
+ *
+ * The target actor uses this to instantiate actors that other
+ * parts of the browser have specified with ActorRegistry.addTargetScopedActor
+ *
+ * @param factories
+ * An object whose own property names are the names of properties to add to
+ * some reply packet (say, a target actor grip or the "listTabs" response
+ * form), and whose own property values are actor constructor functions, as
+ * documented for addTargetScopedActor
+ *
+ * @param parent
+ * The parent TargetActor with which the new actors
+ * will be associated. It should support whatever API the |factories|
+ * constructor functions might be interested in, as it is passed to them.
+ * For the sake of CommonCreateExtraActors itself, it should have at least
+ * the following properties:
+ *
+ * - _extraActors
+ * An object whose own property names are factory table (and packet)
+ * property names, and whose values are no-argument actor constructors,
+ * of the sort that one can add to a Pool.
+ *
+ * - conn
+ * The DevToolsServerConnection in which the new actors will participate.
+ *
+ * - actorID
+ * The actor's name, for use as the new actors' parentID.
+ * @param pool
+ * An object which implements the protocol.js Pool interface, and has the
+ * following properties
+ *
+ * - manage
+ * a function which adds a given actor to an actor pool
+ */
+function createExtraActors(registeredActors, pool, parent) {
+ // Walk over global actors added by extensions.
+ const nameMap = {};
+ for (const name in registeredActors) {
+ let actor = parent._extraActors[name];
+ if (!actor) {
+ // Register another factory, but this time specific to this connection.
+ // It creates a fake actor that looks like an regular actor in the pool,
+ // but without actually instantiating the actor.
+ // It will only be instantiated on the first request made to the actor.
+ actor = new LazyActor(registeredActors[name], parent, pool);
+ parent._extraActors[name] = actor;
+ }
+
+ // If the actor already exists in the pool, it may have been instantiated,
+ // so make sure not to overwrite it by a non-instantiated version.
+ if (!pool.has(actor.actorID)) {
+ pool.manage(actor);
+ }
+ nameMap[name] = actor.actorID;
+ }
+ return nameMap;
+}
+
+exports.createExtraActors = createExtraActors;
+
+/**
+ * Creates an "actor-like" object which responds in the same way as an ordinary actor
+ * but has fewer capabilities (ie, does not manage lifetimes or have it's own pool).
+ *
+ *
+ * @param factories
+ * An object whose own property names are the names of properties to add to
+ * some reply packet (say, a target actor grip or the "listTabs" response
+ * form), and whose own property values are actor constructor functions, as
+ * documented for addTargetScopedActor
+ *
+ * @param parent
+ * The parent TargetActor with which the new actors
+ * will be associated. It should support whatever API the |factories|
+ * constructor functions might be interested in, as it is passed to them.
+ * For the sake of CommonCreateExtraActors itself, it should have at least
+ * the following properties:
+ *
+ * - _extraActors
+ * An object whose own property names are factory table (and packet)
+ * property names, and whose values are no-argument actor constructors,
+ * of the sort that one can add to a Pool.
+ *
+ * - conn
+ * The DevToolsServerConnection in which the new actors will participate.
+ *
+ * - actorID
+ * The actor's name, for use as the new actors' parentID.
+ * @param pool
+ * An object which implements the protocol.js Pool interface, and has the
+ * following properties
+ *
+ * - manage
+ * a function which adds a given actor to an actor pool
+ */
+
+function LazyActor(factory, parent, pool) {
+ this._options = factory.options;
+ this._parentActor = parent;
+ this._name = factory.name;
+ this._pool = pool;
+
+ // needed for taking a place in a pool
+ this.typeName = factory.name;
+}
+
+LazyActor.prototype = {
+ loadModule(id) {
+ const options = this._options;
+ try {
+ return require(id);
+ // Fetch the actor constructor
+ } catch (e) {
+ throw new Error(
+ `Unable to load actor module '${options.id}'\n${e.message}\n${e.stack}\n`
+ );
+ }
+ },
+
+ getConstructor() {
+ const options = this._options;
+ if (options.constructorFun) {
+ // Actor definition registered by testing helpers
+ return options.constructorFun;
+ }
+ // Lazy actor definition, where options contains all the information
+ // required to load the actor lazily.
+ // Exposes `name` attribute in order to allow removeXXXActor to match
+ // the actor by its actor constructor name.
+ this.name = options.constructorName;
+ const module = this.loadModule(options.id);
+ const constructor = module[options.constructorName];
+ if (!constructor) {
+ throw new Error(
+ `Unable to find actor constructor named '${this.name}'. (Is it exported?)`
+ );
+ }
+ return constructor;
+ },
+
+ /**
+ * Return the parent pool for this lazy actor.
+ */
+ getParent() {
+ return this.conn && this.conn.poolFor(this.actorID);
+ },
+
+ /**
+ * This will only happen if the actor is destroyed before it is created
+ * We do not want to use the Pool destruction method, because this actor
+ * has no pool. However, it might have a parent that should unmange this
+ * actor
+ */
+ destroy() {
+ const parent = this.getParent();
+ if (parent) {
+ parent.unmanage(this);
+ }
+ },
+
+ createActor() {
+ // Fetch the actor constructor
+ const Constructor = this.getConstructor();
+ // Instantiate a new actor instance
+ const conn = this._parentActor.conn;
+ // this should be taken care of once all actors are moved to protocol.js
+ const instance = new Constructor(conn, this._parentActor);
+ instance.conn = conn;
+
+ // We want the newly-constructed actor to completely replace the factory
+ // actor. Reusing the existing actor ID will make sure Pool.manage
+ // replaces the old actor with the new actor.
+ instance.actorID = this.actorID;
+
+ this._pool.manage(instance);
+
+ return instance;
+ },
+};
diff --git a/devtools/shared/protocol/moz.build b/devtools/shared/protocol/moz.build
new file mode 100644
index 0000000000..87bf999c25
--- /dev/null
+++ b/devtools/shared/protocol/moz.build
@@ -0,0 +1,22 @@
+# 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/.
+
+DIRS += [
+ "Actor",
+ "Front",
+]
+
+DevToolsModules(
+ "Actor.js",
+ "Front.js",
+ "lazy-pool.js",
+ "Pool.js",
+ "Request.js",
+ "Response.js",
+ "types.js",
+ "utils.js",
+)
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
diff --git a/devtools/shared/protocol/tests/xpcshell/.eslintrc.js b/devtools/shared/protocol/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/protocol/tests/xpcshell/head.js b/devtools/shared/protocol/tests/xpcshell/head.js
new file mode 100644
index 0000000000..dd055ddb42
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/head.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+
+function dumpn(msg) {
+ dump("DBG-TEST: " + msg + "\n");
+}
+
+function connectPipeTracing() {
+ return new TracingTransport(DevToolsServer.connectPipe());
+}
+
+/**
+ * Mock the `Transport` class in order to intercept all the packet
+ * getting in and out and then being able to assert them and dump them.
+ */
+function TracingTransport(childTransport) {
+ this.hooks = null;
+ this.child = childTransport;
+ this.child.hooks = this;
+
+ this.expectations = [];
+ this.packets = [];
+ this.checkIndex = 0;
+}
+
+TracingTransport.prototype = {
+ // Remove actor names
+ normalize(packet) {
+ return JSON.parse(
+ JSON.stringify(packet, (key, value) => {
+ if (key === "to" || key === "from" || key === "actor") {
+ return "<actorid>";
+ }
+ return value;
+ })
+ );
+ },
+ send(packet) {
+ this.packets.push({
+ type: "sent",
+ packet: this.normalize(packet),
+ });
+ return this.child.send(packet);
+ },
+ close() {
+ return this.child.close();
+ },
+ ready() {
+ return this.child.ready();
+ },
+ onPacket(packet) {
+ this.packets.push({
+ type: "received",
+ packet: this.normalize(packet),
+ });
+ this.hooks.onPacket(packet);
+ },
+ onTransportClosed() {
+ if (this.hooks.onTransportClosed) {
+ this.hooks.onTransportClosed();
+ }
+ },
+
+ expectSend(expected) {
+ const packet = this.packets[this.checkIndex++];
+ Assert.equal(packet.type, "sent");
+ deepEqual(packet.packet, this.normalize(expected));
+ },
+
+ expectReceive(expected) {
+ const packet = this.packets[this.checkIndex++];
+ Assert.equal(packet.type, "received");
+ deepEqual(packet.packet, this.normalize(expected));
+ },
+
+ // Write your tests, call dumpLog at the end, inspect the output,
+ // then sprinkle the calls through the right places in your test.
+ dumpLog() {
+ for (const entry of this.packets) {
+ if (entry.type === "sent") {
+ dumpn("trace.expectSend(" + entry.packet + ");");
+ } else {
+ dumpn("trace.expectReceive(" + entry.packet + ");");
+ }
+ }
+ },
+};
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js
new file mode 100644
index 0000000000..ce237e1c00
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_abort.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Outstanding requests should be rejected when the connection aborts
+ * unexpectedly.
+ */
+
+var protocol = require("resource://devtools/shared/protocol.js");
+var { RetVal } = protocol;
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ this.sequence = 0;
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ simpleReturn() {
+ return this.sequence++;
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function () {
+ DevToolsServer.createRootActor = conn => new RootActor(conn);
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ await client.connect();
+
+ const rootFront = client.mainRoot;
+
+ const onSimpleReturn = rootFront.simpleReturn();
+ trace.close();
+
+ try {
+ await onSimpleReturn;
+ ok(false, "Connection was aborted, request shouldn't resolve");
+ } catch (e) {
+ const error = e.toString();
+ ok(true, "Connection was aborted, request rejected correctly");
+ ok(error.includes("Request stack:"), "Error includes request stack");
+ ok(error.includes("test_protocol_abort.js"), "Stack includes this test");
+ }
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_async.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_async.js
new file mode 100644
index 0000000000..dd7196710b
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_async.js
@@ -0,0 +1,192 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Make sure we get replies in the same order that we sent their
+ * requests even when earlier requests take several event ticks to
+ * complete.
+ */
+
+const { waitForTick } = require("resource://devtools/shared/DevToolsUtils.js");
+const protocol = require("resource://devtools/shared/protocol.js");
+const { Arg, RetVal } = protocol;
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ promiseReturn: {
+ request: { toWait: Arg(0, "number") },
+ response: { value: RetVal("number") },
+ },
+ simpleThrow: {
+ response: { value: RetVal("number") },
+ },
+ promiseThrow: {
+ request: { toWait: Arg(0, "number") },
+ response: { value: RetVal("number") },
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ this.sequence = 0;
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ simpleReturn() {
+ return this.sequence++;
+ }
+
+ // Guarantee that this resolves after simpleReturn returns.
+ async promiseReturn(toWait) {
+ const sequence = this.sequence++;
+
+ // Wait until the number of requests specified by toWait have
+ // happened, to test queuing.
+ while (this.sequence - sequence < toWait) {
+ await waitForTick();
+ }
+
+ return sequence;
+ }
+
+ simpleThrow() {
+ throw new Error(this.sequence++);
+ }
+
+ // Guarantee that this resolves after simpleReturn returns.
+ promiseThrow(toWait) {
+ return this.promiseReturn(toWait).then(Promise.reject);
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function () {
+ DevToolsServer.createRootActor = conn => new RootActor(conn);
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ await client.connect();
+
+ const rootFront = client.mainRoot;
+
+ const calls = [];
+ let sequence = 0;
+
+ // Execute a call that won't finish processing until 2
+ // more calls have happened
+ calls.push(
+ rootFront.promiseReturn(2).then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 0);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ // Put a few requests into the backlog
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 1);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 2);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ calls.push(
+ rootFront.simpleThrow().then(
+ () => {
+ Assert.ok(false, "simpleThrow shouldn't succeed!");
+ },
+ error => {
+ // Check right return order
+ Assert.equal(sequence++, 3);
+ }
+ )
+ );
+
+ calls.push(
+ rootFront.promiseThrow(2).then(
+ () => {
+ Assert.ok(false, "promiseThrow shouldn't succeed!");
+ },
+ error => {
+ // Check right return order
+ Assert.equal(sequence++, 4);
+ Assert.ok(true, "simple throw should throw");
+ }
+ )
+ );
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 5);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ // Break up the backlog with a long request that waits
+ // for another simpleReturn before completing
+ calls.push(
+ rootFront.promiseReturn(1).then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 6);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ calls.push(
+ rootFront.simpleReturn().then(ret => {
+ // Check right return order
+ Assert.equal(sequence, 7);
+ // Check request handling order
+ Assert.equal(ret, sequence++);
+ })
+ );
+
+ await Promise.all(calls);
+ await client.close();
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js
new file mode 100644
index 0000000000..728e58c6b9
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_children.js
@@ -0,0 +1,700 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Test simple requests using the protocol helpers.
+ */
+const protocol = require("resource://devtools/shared/protocol.js");
+const { types, Arg, RetVal } = protocol;
+
+// Predeclaring the actor type so that it can be used in the
+// implementation of the child actor.
+types.addActorType("childActor");
+types.addActorType("otherChildActor");
+types.addPolymorphicType("polytype", ["childActor", "otherChildActor"]);
+
+const childSpec = protocol.generateActorSpec({
+ typeName: "childActor",
+
+ events: {
+ event1: {
+ a: Arg(0),
+ b: Arg(1),
+ c: Arg(2),
+ },
+ event2: {
+ a: Arg(0),
+ b: Arg(1),
+ c: Arg(2),
+ },
+ "named-event": {
+ type: "namedEvent",
+ a: Arg(0),
+ b: Arg(1),
+ c: Arg(2),
+ },
+ "object-event": {
+ type: "objectEvent",
+ detail: Arg(0, "childActor#actorid"),
+ },
+ "array-object-event": {
+ type: "arrayObjectEvent",
+ detail: Arg(0, "array:childActor#actorid"),
+ },
+ },
+
+ methods: {
+ echo: {
+ request: { str: Arg(0) },
+ response: { str: RetVal("string") },
+ },
+ getDetail1: {
+ response: {
+ child: RetVal("childActor#actorid"),
+ },
+ },
+ getDetail2: {
+ response: {
+ child: RetVal("childActor#actorid"),
+ },
+ },
+ getIDDetail: {
+ response: {
+ idDetail: RetVal("childActor#actorid"),
+ },
+ },
+ getIntArray: {
+ request: { inputArray: Arg(0, "array:number") },
+ response: {
+ intArray: RetVal("array:number"),
+ },
+ },
+ getSibling: {
+ request: { id: Arg(0) },
+ response: { sibling: RetVal("childActor") },
+ },
+ emitEvents: {
+ response: { value: RetVal("string") },
+ },
+ release: {
+ release: true,
+ },
+ },
+});
+
+class ChildActor extends protocol.Actor {
+ constructor(conn, id) {
+ super(conn, childSpec);
+ this.childID = id;
+ }
+
+ // Actors returned by this actor should be owned by the root actor.
+ marshallPool() {
+ return this.getParent();
+ }
+
+ toString() {
+ return "[ChildActor " + this.childID + "]";
+ }
+
+ destroy() {
+ super.destroy();
+ this.destroyed = true;
+ }
+
+ form() {
+ return {
+ actor: this.actorID,
+ childID: this.childID,
+ };
+ }
+
+ echo(str) {
+ return str;
+ }
+
+ getDetail1() {
+ return this;
+ }
+
+ getDetail2() {
+ return this;
+ }
+
+ getIDDetail() {
+ return this;
+ }
+
+ getIntArray(inputArray) {
+ // Test that protocol.js converts an iterator to an array.
+ const f = function* () {
+ for (const i of inputArray) {
+ yield 2 * i;
+ }
+ };
+ return f();
+ }
+
+ getSibling(id) {
+ return this.getParent().getChild(id);
+ }
+
+ emitEvents() {
+ this.emit("event1", 1, 2, 3);
+ this.emit("event2", 4, 5, 6);
+ this.emit("named-event", 1, 2, 3);
+ this.emit("object-event", this);
+ this.emit("array-object-event", [this]);
+ return "correct response";
+ }
+
+ release() {}
+}
+
+class ChildFront extends protocol.FrontClassWithSpec(childSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._parentFront = parentFront;
+
+ this.before("event1", this.onEvent1.bind(this));
+ this.before("event2", this.onEvent2a.bind(this));
+ this.on("event2", this.onEvent2b.bind(this));
+ }
+
+ destroy() {
+ this.destroyed = true;
+ // Call parent's destroy, which may be re-entrant and recall this function
+ this._parentFront.destroy();
+ super.destroy();
+ }
+
+ marshallPool() {
+ return this.getParent();
+ }
+
+ toString() {
+ return "[child front " + this.childID + "]";
+ }
+
+ form(form) {
+ this.childID = form.childID;
+ }
+
+ onEvent1(a, b, c) {
+ this.event1arg3 = c;
+ }
+
+ onEvent2a(a, b, c) {
+ return Promise.resolve().then(() => {
+ this.event2arg3 = c;
+ });
+ }
+
+ onEvent2b(a, b, c) {
+ this.event2arg2 = b;
+ }
+}
+protocol.registerFront(ChildFront);
+
+const otherChildSpec = protocol.generateActorSpec({
+ typeName: "otherChildActor",
+ methods: {
+ getOtherChild: {
+ request: {},
+ response: { sibling: RetVal("otherChildActor") },
+ },
+ },
+ events: {},
+});
+
+class OtherChildActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, otherChildSpec);
+ }
+
+ getOtherChild() {
+ return new OtherChildActor(this.conn);
+ }
+}
+
+class OtherChildFront extends protocol.FrontClassWithSpec(otherChildSpec) {}
+protocol.registerFront(OtherChildFront);
+
+types.addDictType("manyChildrenDict", {
+ child5: "childActor",
+ more: "array:childActor",
+});
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ getChild: {
+ request: { str: Arg(0) },
+ response: { actor: RetVal("childActor") },
+ },
+ getOtherChild: {
+ request: {},
+ response: { sibling: RetVal("otherChildActor") },
+ },
+ getChildren: {
+ request: { ids: Arg(0, "array:string") },
+ response: { children: RetVal("array:childActor") },
+ },
+ getChildren2: {
+ request: { ids: Arg(0, "array:childActor") },
+ response: { children: RetVal("array:childActor") },
+ },
+ getManyChildren: {
+ response: RetVal("manyChildrenDict"),
+ },
+ getPolymorphism: {
+ request: { id: Arg(0, "number") },
+ response: { child: RetVal("polytype") },
+ },
+ requestPolymorphism: {
+ request: {
+ id: Arg(0, "number"),
+ actor: Arg(1, "polytype"),
+ },
+ response: { child: RetVal("polytype") },
+ },
+ },
+});
+
+let rootActor = null;
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ rootActor = this;
+ this.actorID = "root";
+ this._children = {};
+ }
+
+ toString() {
+ return "[root actor]";
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ getChild(id) {
+ if (id in this._children) {
+ return this._children[id];
+ }
+ const child = new ChildActor(this.conn, id);
+ this._children[id] = child;
+ return child;
+ }
+
+ // Other child actor won't all be own by the root actor
+ // and can have their own children
+ getOtherChild() {
+ return new OtherChildActor(this.conn);
+ }
+
+ getChildren(ids) {
+ return ids.map(id => this.getChild(id));
+ }
+
+ getChildren2(ids) {
+ const f = function* () {
+ for (const c of ids) {
+ yield c;
+ }
+ };
+ return f();
+ }
+
+ getManyChildren() {
+ return {
+ // note that this isn't in the specialization array.
+ foo: "bar",
+ child5: this.getChild("child5"),
+ more: [this.getChild("child6"), this.getChild("child7")],
+ };
+ }
+
+ getPolymorphism(id) {
+ if (id == 0) {
+ return new ChildActor(this.conn, id);
+ } else if (id == 1) {
+ return new OtherChildActor(this.conn);
+ }
+ throw new Error("Unexpected id");
+ }
+
+ requestPolymorphism(id, actor) {
+ if (id == 0 && actor instanceof ChildActor) {
+ return actor;
+ } else if (id == 1 && actor instanceof OtherChildActor) {
+ return actor;
+ }
+ throw new Error("Unexpected id or actor");
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.actorID = "root";
+ // Root actor owns itself.
+ this.manage(this);
+ }
+
+ toString() {
+ return "[root front]";
+ }
+}
+
+let rootFront, childFront;
+function expectRootChildren(size) {
+ Assert.equal(rootActor._poolMap.size, size);
+ Assert.equal(rootFront._poolMap.size, size + 1);
+ if (childFront) {
+ Assert.equal(childFront._poolMap.size, 0);
+ }
+}
+protocol.registerFront(RootFront);
+
+function childrenOfType(pool, type) {
+ const children = [...rootFront.poolChildren()];
+ return children.filter(child => child instanceof type);
+}
+
+add_task(async function () {
+ DevToolsServer.createRootActor = conn => {
+ return new RootActor(conn);
+ };
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ const [applicationType] = await client.connect();
+ trace.expectReceive({
+ from: "<actorid>",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ });
+ Assert.equal(applicationType, "xpcshell-tests");
+
+ rootFront = client.mainRoot;
+
+ await testSimpleChildren(trace);
+ await testDetail(trace);
+ await testSibling(trace);
+ await testEvents(trace);
+ await testManyChildren(trace);
+ await testGenerator(trace);
+ await testPolymorphism(trace);
+ await testUnmanageChildren(trace);
+ // Execute that assertion very last as it destroy the root front and actor
+ await testDestroy(trace);
+
+ await client.close();
+});
+
+async function testSimpleChildren(trace) {
+ childFront = await rootFront.getChild("child1");
+ trace.expectSend({ type: "getChild", str: "child1", to: "<actorid>" });
+ trace.expectReceive({ actor: "<actorid>", from: "<actorid>" });
+
+ Assert.ok(childFront instanceof ChildFront);
+ Assert.equal(childFront.childID, "child1");
+ expectRootChildren(1);
+
+ // Request the child again, make sure the same is returned.
+ let ret = await rootFront.getChild("child1");
+ trace.expectSend({ type: "getChild", str: "child1", to: "<actorid>" });
+ trace.expectReceive({ actor: "<actorid>", from: "<actorid>" });
+
+ expectRootChildren(1);
+ Assert.ok(ret === childFront);
+
+ ret = await childFront.echo("hello");
+ trace.expectSend({ type: "echo", str: "hello", to: "<actorid>" });
+ trace.expectReceive({ str: "hello", from: "<actorid>" });
+
+ Assert.equal(ret, "hello");
+}
+
+async function testDetail(trace) {
+ let ret = await childFront.getDetail1();
+ trace.expectSend({ type: "getDetail1", to: "<actorid>" });
+ trace.expectReceive({ child: childFront.actorID, from: "<actorid>" });
+ Assert.ok(ret === childFront);
+
+ ret = await childFront.getDetail2();
+ trace.expectSend({ type: "getDetail2", to: "<actorid>" });
+ trace.expectReceive({ child: childFront.actorID, from: "<actorid>" });
+ Assert.ok(ret === childFront);
+
+ ret = await childFront.getIDDetail();
+ trace.expectSend({ type: "getIDDetail", to: "<actorid>" });
+ trace.expectReceive({
+ idDetail: childFront.actorID,
+ from: "<actorid>",
+ });
+ Assert.ok(ret === childFront);
+}
+
+async function testSibling(trace) {
+ await childFront.getSibling("siblingID");
+ trace.expectSend({
+ type: "getSibling",
+ id: "siblingID",
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ sibling: { actor: "<actorid>", childID: "siblingID" },
+ from: "<actorid>",
+ });
+
+ expectRootChildren(2);
+}
+
+async function testEvents(trace) {
+ const ret = await rootFront.getChildren(["child1", "child2"]);
+ trace.expectSend({
+ type: "getChildren",
+ ids: ["child1", "child2"],
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ children: [
+ { actor: "<actorid>", childID: "child1" },
+ { actor: "<actorid>", childID: "child2" },
+ ],
+ from: "<actorid>",
+ });
+
+ expectRootChildren(3);
+ Assert.ok(ret[0] === childFront);
+ Assert.ok(ret[1] !== childFront);
+ Assert.ok(ret[1] instanceof ChildFront);
+
+ // On both children, listen to events. We're only
+ // going to trigger events on the first child, so an event
+ // triggered on the second should cause immediate failures.
+
+ const set = new Set([
+ "event1",
+ "event2",
+ "named-event",
+ "object-event",
+ "array-object-event",
+ ]);
+
+ childFront.on("event1", (a, b, c) => {
+ Assert.equal(a, 1);
+ Assert.equal(b, 2);
+ Assert.equal(c, 3);
+ // Verify that the pre-event handler was called.
+ Assert.equal(childFront.event1arg3, 3);
+ set.delete("event1");
+ });
+ childFront.on("event2", (a, b, c) => {
+ Assert.equal(a, 4);
+ Assert.equal(b, 5);
+ Assert.equal(c, 6);
+ // Verify that the async pre-event handler was called,
+ // setting the property before this handler was called.
+ Assert.equal(childFront.event2arg3, 6);
+ // And check that the sync preEvent with the same name is also
+ // executed
+ Assert.equal(childFront.event2arg2, 5);
+ set.delete("event2");
+ });
+ childFront.on("named-event", (a, b, c) => {
+ Assert.equal(a, 1);
+ Assert.equal(b, 2);
+ Assert.equal(c, 3);
+ set.delete("named-event");
+ });
+ childFront.on("object-event", obj => {
+ Assert.ok(obj === childFront);
+ set.delete("object-event");
+ });
+ childFront.on("array-object-event", array => {
+ Assert.ok(array[0] === childFront);
+ set.delete("array-object-event");
+ });
+
+ const fail = function () {
+ do_throw("Unexpected event");
+ };
+ ret[1].on("event1", fail);
+ ret[1].on("event2", fail);
+ ret[1].on("named-event", fail);
+ ret[1].on("object-event", fail);
+ ret[1].on("array-object-event", fail);
+
+ await childFront.emitEvents();
+ trace.expectSend({ type: "emitEvents", to: "<actorid>" });
+ trace.expectReceive({
+ type: "event1",
+ a: 1,
+ b: 2,
+ c: 3,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "event2",
+ a: 4,
+ b: 5,
+ c: 6,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "namedEvent",
+ a: 1,
+ b: 2,
+ c: 3,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "objectEvent",
+ detail: childFront.actorID,
+ from: "<actorid>",
+ });
+ trace.expectReceive({
+ type: "arrayObjectEvent",
+ detail: [childFront.actorID],
+ from: "<actorid>",
+ });
+ trace.expectReceive({ value: "correct response", from: "<actorid>" });
+
+ Assert.equal(set.size, 0);
+}
+
+async function testManyChildren(trace) {
+ const ret = await rootFront.getManyChildren();
+ trace.expectSend({ type: "getManyChildren", to: "<actorid>" });
+ trace.expectReceive({
+ foo: "bar",
+ child5: { actor: "<actorid>", childID: "child5" },
+ more: [
+ { actor: "<actorid>", childID: "child6" },
+ { actor: "<actorid>", childID: "child7" },
+ ],
+ from: "<actorid>",
+ });
+
+ // Check all the crazy stuff we did in getManyChildren
+ Assert.equal(ret.foo, "bar");
+ Assert.equal(ret.child5.childID, "child5");
+ Assert.equal(ret.more[0].childID, "child6");
+ Assert.equal(ret.more[1].childID, "child7");
+}
+
+async function testGenerator(trace) {
+ // Test accepting a generator.
+ const f = function* () {
+ for (const i of [1, 2, 3, 4, 5]) {
+ yield i;
+ }
+ };
+ let ret = await childFront.getIntArray(f());
+ Assert.equal(ret.length, 5);
+ const expected = [2, 4, 6, 8, 10];
+ for (let i = 0; i < 5; ++i) {
+ Assert.equal(ret[i], expected[i]);
+ }
+
+ const ids = await rootFront.getChildren(["child1", "child2"]);
+ const f2 = function* () {
+ for (const id of ids) {
+ yield id;
+ }
+ };
+ ret = await rootFront.getChildren2(f2());
+ Assert.equal(ret.length, 2);
+ Assert.ok(ret[0] === childFront);
+ Assert.ok(ret[1] !== childFront);
+ Assert.ok(ret[1] instanceof ChildFront);
+}
+
+async function testPolymorphism(trace) {
+ // Check polymorphic types returned by an actor
+ const firstChild = await rootFront.getPolymorphism(0);
+ Assert.ok(firstChild instanceof ChildFront);
+
+ // Check polymorphic types passed to a front
+ const sameFirstChild = await rootFront.requestPolymorphism(0, firstChild);
+ Assert.ok(sameFirstChild instanceof ChildFront);
+ Assert.equal(sameFirstChild, firstChild);
+
+ // Same with the second possible type
+ const secondChild = await rootFront.getPolymorphism(1);
+ Assert.ok(secondChild instanceof OtherChildFront);
+
+ const sameSecondChild = await rootFront.requestPolymorphism(1, secondChild);
+ Assert.ok(sameSecondChild instanceof OtherChildFront);
+ Assert.equal(sameSecondChild, secondChild);
+
+ // Check that any other type is rejected
+ Assert.throws(() => {
+ rootFront.requestPolymorphism(0, null);
+ }, /Was expecting one of these actors 'childActor,otherChildActor' but instead got an empty value/);
+ Assert.throws(() => {
+ rootFront.requestPolymorphism(0, 42);
+ }, /Was expecting one of these actors 'childActor,otherChildActor' but instead got value: '42'/);
+ Assert.throws(() => {
+ rootFront.requestPolymorphism(0, rootFront);
+ }, /Was expecting one of these actors 'childActor,otherChildActor' but instead got an actor of type: 'root'/);
+}
+
+async function testUnmanageChildren(trace) {
+ // There is already one front of type OtherChildFront
+ Assert.equal(childrenOfType(rootFront, OtherChildFront).length, 1);
+
+ // Create another front of type OtherChildFront
+ const front = await rootFront.getPolymorphism(1);
+ Assert.ok(front instanceof OtherChildFront);
+ Assert.equal(childrenOfType(rootFront, OtherChildFront).length, 2);
+
+ // Remove all fronts of type OtherChildFront
+ rootFront.unmanageChildren(OtherChildFront);
+ Assert.ok(
+ !front.isDestroyed(),
+ "Unmanaged front is not considered as destroyed"
+ );
+ Assert.equal(childrenOfType(rootFront, OtherChildFront).length, 0);
+}
+
+async function testDestroy(trace) {
+ const front = await rootFront.getOtherChild();
+ const otherChildFront = await front.getOtherChild();
+ Assert.equal(
+ otherChildFront.getParent(),
+ front,
+ "the child is a children of first front"
+ );
+
+ front.destroy();
+ Assert.ok(front.isDestroyed(), "sibling is correctly reported as destroyed");
+ Assert.ok(!front.getParent(), "sibling has no more parent declared");
+ Assert.ok(otherChildFront.isDestroyed(), "the child is also destroyed");
+ Assert.ok(
+ !otherChildFront.getParent(),
+ "the child also has no more parent declared"
+ );
+ Assert.ok(
+ !otherChildFront.parentPool,
+ "the child also has its parentPool attribute nullified"
+ );
+
+ // Verify that re-entrant Front.destroy doesn't throw, nor loop
+ // Execute that very last as it will destroy the root actor and front
+ const sibling = await childFront.getSibling("siblingID");
+ sibling.destroy();
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_index.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_index.js
new file mode 100644
index 0000000000..ef566d6b97
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_index.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const { lazyLoadFront } = require("resource://devtools/shared/specs/index.js");
+const Types =
+ require("resource://devtools/shared/specs/index.js").__TypesForTests;
+const { getType } = require("resource://devtools/shared/protocol.js").types;
+
+function run_test() {
+ test_index_is_alphabetically_sorted();
+ test_specs();
+ test_fronts();
+}
+
+// Check alphabetic order of specs defined in devtools/shared/specs/index.js,
+// in order to ease its maintenance and readability.
+function test_index_is_alphabetically_sorted() {
+ let lastSpec = "";
+ for (const type of Types) {
+ const spec = type.spec;
+ if (lastSpec && spec < lastSpec) {
+ ok(false, `Spec definition for "${spec}" should be before "${lastSpec}"`);
+ }
+ lastSpec = spec;
+ }
+ ok(true, "Specs index is alphabetically sorted");
+}
+
+function test_specs() {
+ for (const type of Types) {
+ for (const typeName of type.types) {
+ ok(!!getType(typeName), `${typeName} spec is defined`);
+ }
+ }
+ ok(true, "Specs are all accessible");
+}
+
+function test_fronts() {
+ for (const item of Types) {
+ if (!item.front) {
+ continue;
+ }
+ for (const typeName of item.types) {
+ lazyLoadFront(typeName);
+ const type = getType(typeName);
+ ok(!!type, `Front for ${typeName} has a spec`);
+ ok(type.frontClass, `${typeName} has a front correctly defined`);
+ }
+ }
+ ok(true, "Front are all accessible");
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js
new file mode 100644
index 0000000000..6b530f0a61
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_invalid_response.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const protocol = require("resource://devtools/shared/protocol.js");
+const { RetVal } = protocol;
+
+// Test invalid response specs throw when generating the Actor specification.
+
+// Test top level array response
+add_task(async function () {
+ Assert.throws(() => {
+ protocol.generateActorSpec({
+ typeName: "invalidArrayResponse",
+ methods: {
+ invalidMethod: {
+ response: RetVal("array:string"),
+ },
+ },
+ });
+ }, /Arrays should be wrapped in objects/);
+
+ protocol.generateActorSpec({
+ typeName: "validArrayResponse",
+ methods: {
+ validMethod: {
+ response: {
+ someArray: RetVal("array:string"),
+ },
+ },
+ },
+ });
+ ok(true, "Arrays wrapped in object are valid response packets");
+});
+
+// Test response with several placeholders
+add_task(async function () {
+ Assert.throws(() => {
+ protocol.generateActorSpec({
+ typeName: "tooManyPlaceholdersResponse",
+ methods: {
+ invalidMethod: {
+ response: {
+ prop1: RetVal("json"),
+ prop2: RetVal("json"),
+ },
+ },
+ },
+ });
+ }, /More than one RetVal specified in response/);
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js
new file mode 100644
index 0000000000..bd887ba88a
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_lifecycle.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+const { Front } = require("resource://devtools/shared/protocol/Front.js");
+
+add_task(async function () {
+ // Front constructor expect to be provided a client object
+ const client = {};
+ const front = new Front(client);
+ ok(
+ !front.isDestroyed(),
+ "Blank front with no actor ID is not considered as destroyed"
+ );
+ front.destroy();
+ ok(front.isDestroyed(), "Front is destroyed");
+
+ const actor = new Actor(null, { typeName: "actor", methods: [] });
+ ok(
+ !actor.isDestroyed(),
+ "Blank actor with no actor ID is not considered as destroyed"
+ );
+ actor.destroy();
+ ok(actor.isDestroyed(), "Actor is destroyed");
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js
new file mode 100644
index 0000000000..cda1708520
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_longstring.js
@@ -0,0 +1,310 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable max-nested-callbacks */
+
+"use strict";
+
+/**
+ * Test simple requests using the protocol helpers.
+ */
+var protocol = require("resource://devtools/shared/protocol.js");
+var { RetVal, Arg } = protocol;
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+var {
+ LongStringActor,
+} = require("resource://devtools/server/actors/string.js");
+
+// The test implicitly relies on this.
+require("resource://devtools/client/fronts/string.js");
+
+DevToolsServer.LONG_STRING_LENGTH =
+ DevToolsServer.LONG_STRING_INITIAL_LENGTH =
+ DevToolsServer.LONG_STRING_READ_LENGTH =
+ 5;
+
+var SHORT_STR = "abc";
+var LONG_STR = "abcdefghijklmnop";
+
+var rootActor = null;
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ events: {
+ "string-event": {
+ str: Arg(0, "longstring"),
+ },
+ },
+
+ methods: {
+ shortString: {
+ response: { value: RetVal("longstring") },
+ },
+ longString: {
+ response: { value: RetVal("longstring") },
+ },
+ emitShortString: {
+ oneway: true,
+ },
+ emitLongString: {
+ oneway: true,
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ rootActor = this;
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ shortString() {
+ return new LongStringActor(this.conn, SHORT_STR);
+ }
+
+ longString() {
+ return new LongStringActor(this.conn, LONG_STR);
+ }
+
+ emitShortString() {
+ EventEmitter.emit(
+ this,
+ "string-event",
+ new LongStringActor(this.conn, SHORT_STR)
+ );
+ }
+
+ emitLongString() {
+ EventEmitter.emit(
+ this,
+ "string-event",
+ new LongStringActor(this.conn, LONG_STR)
+ );
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+function run_test() {
+ DevToolsServer.createRootActor = conn => {
+ return new RootActor(conn);
+ };
+
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ let rootFront;
+
+ let strfront = null;
+
+ const expectRootChildren = function (size) {
+ Assert.equal(rootActor.__poolMap.size, size + 1);
+ Assert.equal(rootFront.__poolMap.size, size + 1);
+ };
+
+ client.connect().then(([applicationType, traits]) => {
+ rootFront = client.mainRoot;
+
+ // Root actor has no children yet.
+ expectRootChildren(0);
+
+ trace.expectReceive({
+ from: "<actorid>",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ });
+ Assert.equal(applicationType, "xpcshell-tests");
+ rootFront
+ .shortString()
+ .then(ret => {
+ trace.expectSend({ type: "shortString", to: "<actorid>" });
+ trace.expectReceive({ value: "abc", from: "<actorid>" });
+
+ // Should only own the one reference (itself) at this point.
+ expectRootChildren(0);
+ strfront = ret;
+ })
+ .then(() => {
+ return strfront.string();
+ })
+ .then(ret => {
+ Assert.equal(ret, SHORT_STR);
+ })
+ .then(() => {
+ return rootFront.longString();
+ })
+ .then(ret => {
+ trace.expectSend({ type: "longString", to: "<actorid>" });
+ trace.expectReceive({
+ value: {
+ type: "longString",
+ actor: "<actorid>",
+ length: 16,
+ initial: "abcde",
+ },
+ from: "<actorid>",
+ });
+
+ strfront = ret;
+ // Should own a reference to itself and an extra string now.
+ expectRootChildren(1);
+ })
+ .then(() => {
+ return strfront.string();
+ })
+ .then(ret => {
+ trace.expectSend({
+ type: "substring",
+ start: 5,
+ end: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "fghij", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 10,
+ end: 15,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "klmno", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 15,
+ end: 20,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "p", from: "<actorid>" });
+
+ Assert.equal(ret, LONG_STR);
+ })
+ .then(() => {
+ return strfront.release();
+ })
+ .then(() => {
+ trace.expectSend({ type: "release", to: "<actorid>" });
+ trace.expectReceive({ from: "<actorid>" });
+
+ // That reference should be removed now.
+ expectRootChildren(0);
+ })
+ .then(() => {
+ return new Promise(resolve => {
+ rootFront.once("string-event", str => {
+ trace.expectSend({ type: "emitShortString", to: "<actorid>" });
+ trace.expectReceive({
+ type: "string-event",
+ str: "abc",
+ from: "<actorid>",
+ });
+
+ Assert.ok(!!str);
+ strfront = str;
+ // Shouldn't generate any new references
+ expectRootChildren(0);
+ // will generate no packets.
+ strfront.string().then(value => {
+ resolve(value);
+ });
+ });
+ rootFront.emitShortString();
+ });
+ })
+ .then(value => {
+ Assert.equal(value, SHORT_STR);
+ })
+ .then(() => {
+ // Will generate no packets
+ return strfront.release();
+ })
+ .then(() => {
+ return new Promise(resolve => {
+ rootFront.once("string-event", str => {
+ trace.expectSend({ type: "emitLongString", to: "<actorid>" });
+ trace.expectReceive({
+ type: "string-event",
+ str: {
+ type: "longString",
+ actor: "<actorid>",
+ length: 16,
+ initial: "abcde",
+ },
+ from: "<actorid>",
+ });
+
+ Assert.ok(!!str);
+ // Should generate one new reference
+ expectRootChildren(1);
+ strfront = str;
+ strfront.string().then(value => {
+ trace.expectSend({
+ type: "substring",
+ start: 5,
+ end: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "fghij", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 10,
+ end: 15,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "klmno", from: "<actorid>" });
+ trace.expectSend({
+ type: "substring",
+ start: 15,
+ end: 20,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ substring: "p", from: "<actorid>" });
+
+ resolve(value);
+ });
+ });
+ rootFront.emitLongString();
+ });
+ })
+ .then(value => {
+ Assert.equal(value, LONG_STR);
+ })
+ .then(() => {
+ return strfront.release();
+ })
+ .then(() => {
+ trace.expectSend({ type: "release", to: "<actorid>" });
+ trace.expectReceive({ from: "<actorid>" });
+ expectRootChildren(0);
+ })
+ .then(() => {
+ client.close().then(() => {
+ do_test_finished();
+ });
+ })
+ .catch(err => {
+ do_report_unexpected_exception(err, "Failure executing test");
+ });
+ });
+ do_test_pending();
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js
new file mode 100644
index 0000000000..523d147f6c
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_simple.js
@@ -0,0 +1,316 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test simple requests using the protocol helpers.
+ */
+
+var protocol = require("resource://devtools/shared/protocol.js");
+var { Arg, Option, RetVal } = protocol;
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ events: {
+ oneway: { a: Arg(0) },
+ falsyOptions: {
+ zero: Option(0),
+ farce: Option(0),
+ },
+ },
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ promiseReturn: {
+ response: { value: RetVal("number") },
+ },
+ simpleArgs: {
+ request: {
+ firstArg: Arg(0),
+ secondArg: Arg(1),
+ },
+ response: RetVal(),
+ },
+ optionArgs: {
+ request: {
+ option1: Option(0),
+ option2: Option(0),
+ },
+ response: RetVal(),
+ },
+ optionalArgs: {
+ request: {
+ a: Arg(0),
+ b: Arg(1, "nullable:number"),
+ },
+ response: {
+ value: RetVal("number"),
+ },
+ },
+ arrayArgs: {
+ request: {
+ a: Arg(0, "array:number"),
+ },
+ response: {
+ arrayReturn: RetVal("array:number"),
+ },
+ },
+ nestedArrayArgs: {
+ request: { a: Arg(0, "array:array:number") },
+ response: { value: RetVal("array:array:number") },
+ },
+ renamedEcho: {
+ request: {
+ type: "echo",
+ a: Arg(0),
+ },
+ response: {
+ value: RetVal("string"),
+ },
+ },
+ testOneWay: {
+ request: { a: Arg(0) },
+ oneway: true,
+ },
+ emitFalsyOptions: {
+ oneway: true,
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ simpleReturn() {
+ return 1;
+ }
+
+ promiseReturn() {
+ return Promise.resolve(1);
+ }
+
+ simpleArgs(a, b) {
+ return { firstResponse: a + 1, secondResponse: b + 1 };
+ }
+
+ optionArgs(options) {
+ return { option1: options.option1, option2: options.option2 };
+ }
+
+ optionalArgs(a, b = 200) {
+ return b;
+ }
+
+ arrayArgs(a) {
+ return a;
+ }
+
+ nestedArrayArgs(a) {
+ return a;
+ }
+
+ /**
+ * Test that the 'type' part of the request packet works
+ * correctly when the type isn't the same as the method name
+ */
+ renamedEcho(a) {
+ if (this.conn.currentPacket.type != "echo") {
+ return "goodbye";
+ }
+ return a;
+ }
+
+ testOneWay(a) {
+ // Emit to show that we got this message, because there won't be a response.
+ EventEmitter.emit(this, "oneway", a);
+ }
+
+ emitFalsyOptions() {
+ EventEmitter.emit(this, "falsyOptions", { zero: 0, farce: false });
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function () {
+ DevToolsServer.createRootActor = conn => {
+ return new RootActor(conn);
+ };
+ DevToolsServer.init();
+
+ protocol.types.getType("array:array:array:number");
+ protocol.types.getType("array:array:array:number");
+
+ Assert.throws(
+ () => protocol.types.getType("unknown"),
+ /Unknown type:/,
+ "Should throw for unknown type"
+ );
+ Assert.throws(
+ () => protocol.types.getType("array:unknown"),
+ /Unknown type:/,
+ "Should throw for unknown type"
+ );
+ Assert.throws(
+ () => protocol.types.getType("unknown:number"),
+ /Unknown collection type:/,
+ "Should throw for unknown collection type"
+ );
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+
+ const [applicationType] = await client.connect();
+ trace.expectReceive({
+ from: "<actorid>",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ });
+ Assert.equal(applicationType, "xpcshell-tests");
+
+ const rootFront = client.mainRoot;
+
+ let ret = await rootFront.simpleReturn();
+ trace.expectSend({ type: "simpleReturn", to: "<actorid>" });
+ trace.expectReceive({ value: 1, from: "<actorid>" });
+ Assert.equal(ret, 1);
+
+ ret = await rootFront.promiseReturn();
+ trace.expectSend({ type: "promiseReturn", to: "<actorid>" });
+ trace.expectReceive({ value: 1, from: "<actorid>" });
+ Assert.equal(ret, 1);
+
+ Assert.throws(
+ () => rootFront.simpleArgs(5),
+ /undefined passed where a value is required/,
+ "Should throw if simpleArgs is missing an argument."
+ );
+
+ ret = await rootFront.simpleArgs(5, 10);
+ trace.expectSend({
+ type: "simpleArgs",
+ firstArg: 5,
+ secondArg: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ firstResponse: 6,
+ secondResponse: 11,
+ from: "<actorid>",
+ });
+ Assert.equal(ret.firstResponse, 6);
+ Assert.equal(ret.secondResponse, 11);
+
+ ret = await rootFront.optionArgs({
+ option1: 5,
+ option2: 10,
+ });
+ trace.expectSend({
+ type: "optionArgs",
+ option1: 5,
+ option2: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ option1: 5, option2: 10, from: "<actorid>" });
+ Assert.equal(ret.option1, 5);
+ Assert.equal(ret.option2, 10);
+
+ ret = await rootFront.optionArgs({});
+ trace.expectSend({ type: "optionArgs", to: "<actorid>" });
+ trace.expectReceive({ from: "<actorid>" });
+ Assert.ok(typeof ret.option1 === "undefined");
+ Assert.ok(typeof ret.option2 === "undefined");
+
+ // Explicitly call an optional argument...
+ ret = await rootFront.optionalArgs(5, 10);
+ trace.expectSend({
+ type: "optionalArgs",
+ a: 5,
+ b: 10,
+ to: "<actorid>",
+ });
+ trace.expectReceive({ value: 10, from: "<actorid>" });
+ Assert.equal(ret, 10);
+
+ // Now don't pass the optional argument, expect the default.
+ ret = await rootFront.optionalArgs(5);
+ trace.expectSend({ type: "optionalArgs", a: 5, to: "<actorid>" });
+ trace.expectReceive({ value: 200, from: "<actorid>" });
+ Assert.equal(ret, 200);
+
+ ret = await rootFront.arrayArgs([0, 1, 2, 3, 4, 5]);
+ trace.expectSend({
+ type: "arrayArgs",
+ a: [0, 1, 2, 3, 4, 5],
+ to: "<actorid>",
+ });
+ trace.expectReceive({
+ arrayReturn: [0, 1, 2, 3, 4, 5],
+ from: "<actorid>",
+ });
+ Assert.equal(ret[0], 0);
+ Assert.equal(ret[5], 5);
+
+ ret = await rootFront.arrayArgs([[5]]);
+ trace.expectSend({ type: "arrayArgs", a: [[5]], to: "<actorid>" });
+ trace.expectReceive({ arrayReturn: [[5]], from: "<actorid>" });
+ Assert.equal(ret[0][0], 5);
+
+ const str = await rootFront.renamedEcho("hello");
+ trace.expectSend({ type: "echo", a: "hello", to: "<actorid>" });
+ trace.expectReceive({ value: "hello", from: "<actorid>" });
+ Assert.equal(str, "hello");
+
+ const onOneWay = rootFront.once("oneway");
+ Assert.ok(typeof rootFront.testOneWay("hello") === "undefined");
+ const response = await onOneWay;
+ trace.expectSend({ type: "testOneWay", a: "hello", to: "<actorid>" });
+ trace.expectReceive({
+ type: "oneway",
+ a: "hello",
+ from: "<actorid>",
+ });
+ Assert.equal(response, "hello");
+
+ const onFalsyOptions = rootFront.once("falsyOptions");
+ rootFront.emitFalsyOptions();
+ const res = await onFalsyOptions;
+ trace.expectSend({ type: "emitFalsyOptions", to: "<actorid>" });
+ trace.expectReceive({
+ type: "falsyOptions",
+ farce: false,
+ zero: 0,
+ from: "<actorid>",
+ });
+
+ Assert.ok(res.zero === 0);
+ Assert.ok(res.farce === false);
+
+ await client.close();
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js
new file mode 100644
index 0000000000..faf8402ea6
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_stack.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Client request stacks should span the entire process from before making the
+ * request to handling the reply from the server. The server frames are not
+ * included, nor can they be in most cases, since the server can be a remote
+ * device.
+ */
+
+var protocol = require("resource://devtools/shared/protocol.js");
+var { RetVal } = protocol;
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ simpleReturn: {
+ response: { value: RetVal() },
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ // Root actor owns itself.
+ this.manage(this);
+ this.actorID = "root";
+ this.sequence = 0;
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ simpleReturn() {
+ return this.sequence++;
+ }
+}
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+function run_test() {
+ DevToolsServer.createRootActor = conn => new RootActor(conn);
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ let rootFront;
+
+ client.connect().then(function onConnect() {
+ rootFront = client.mainRoot;
+
+ rootFront
+ .simpleReturn()
+ .then(
+ () => {
+ let stack = Components.stack;
+ while (stack) {
+ info(stack.name);
+ if (stack.name.includes("onConnect")) {
+ // Reached back to outer function before request
+ ok(true, "Complete stack");
+ return;
+ }
+ stack = stack.asyncCaller || stack.caller;
+ }
+ ok(false, "Incomplete stack");
+ },
+ () => {
+ ok(false, "Request failed unexpectedly");
+ }
+ )
+ .then(() => {
+ client.close().then(() => {
+ do_test_finished();
+ });
+ });
+ });
+
+ do_test_pending();
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js
new file mode 100644
index 0000000000..4a62c5e073
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_types.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const { types } = require("resource://devtools/shared/protocol.js");
+
+function run_test() {
+ types.addActorType("myActor1");
+ types.addActorType("myActor2");
+ types.addActorType("myActor3");
+
+ types.addPolymorphicType("ptype1", ["myActor1", "myActor2"]);
+ const ptype1 = types.getType("ptype1");
+ Assert.equal(ptype1.name, "ptype1");
+ Assert.equal(ptype1.category, "polymorphic");
+
+ types.addPolymorphicType("ptype2", ["myActor1", "myActor2", "myActor3"]);
+ const ptype2 = types.getType("ptype2");
+ Assert.equal(ptype2.name, "ptype2");
+ Assert.equal(ptype2.category, "polymorphic");
+
+ // Polymorphic types only accept actor types
+ try {
+ types.addPolymorphicType("ptype", ["myActor1", "myActor4"]);
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Unknown type: myActor4");
+ }
+ try {
+ types.addPolymorphicType("ptype", ["myActor1", "string"]);
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'myActor1,string', the type 'string' isn't an actor"
+ );
+ }
+ try {
+ types.addPolymorphicType("ptype", ["myActor1", "boolean"]);
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'myActor1,boolean', the type 'boolean' isn't an actor"
+ );
+ }
+
+ // Polymorphic types are not compatible with array or nullables
+ try {
+ types.addPolymorphicType("ptype", ["array:myActor1", "myActor2"]);
+ Assert.ok(false, "addType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'array:myActor1,myActor2', the type 'array:myActor1' isn't an actor"
+ );
+ }
+ try {
+ types.addPolymorphicType("ptype", ["nullable:myActor1", "myActor2"]);
+ Assert.ok(false, "addType should fail");
+ } catch (ex) {
+ Assert.equal(
+ ex.toString(),
+ "Error: In polymorphic type 'nullable:myActor1,myActor2', the type 'nullable:myActor1' isn't an actor"
+ );
+ }
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js
new file mode 100644
index 0000000000..060a1743b1
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_unregister.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const { types } = require("resource://devtools/shared/protocol.js");
+
+function run_test() {
+ types.addType("test", {
+ read: v => "successful read: " + v,
+ write: v => "successful write: " + v,
+ });
+
+ // Verify the type registered correctly.
+
+ const type = types.getType("test");
+ const arrayType = types.getType("array:test");
+ Assert.equal(type.read("foo"), "successful read: foo");
+ Assert.equal(arrayType.read(["foo"])[0], "successful read: foo");
+
+ types.removeType("test");
+
+ Assert.equal(type.name, "DEFUNCT:test");
+ try {
+ types.getType("test");
+ Assert.ok(false, "getType should fail");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Unknown type: test");
+ }
+
+ try {
+ type.read("foo");
+ Assert.ok(false, "type.read should have thrown an exception.");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Using defunct type: test");
+ }
+
+ try {
+ arrayType.read(["foo"]);
+ Assert.ok(false, "array:test.read should have thrown an exception.");
+ } catch (ex) {
+ Assert.equal(ex.toString(), "Error: Using defunct type: test");
+ }
+}
diff --git a/devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js b/devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js
new file mode 100644
index 0000000000..16f98f176b
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/test_protocol_watchFronts.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test Front.watchFronts method.
+ */
+
+const protocol = require("resource://devtools/shared/protocol.js");
+const { RetVal } = protocol;
+
+const childSpec = protocol.generateActorSpec({
+ typeName: "childActor",
+
+ methods: {
+ release: {
+ release: true,
+ },
+ },
+});
+
+class ChildActor extends protocol.Actor {
+ constructor(conn, id) {
+ super(conn, childSpec);
+ this.childID = id;
+ }
+
+ release() {}
+
+ form() {
+ return {
+ actor: this.actorID,
+ childID: this.childID,
+ foo: "bar",
+ };
+ }
+}
+
+const rootSpec = protocol.generateActorSpec({
+ typeName: "root",
+
+ methods: {
+ createChild: {
+ request: {},
+ response: { actor: RetVal("childActor") },
+ },
+ },
+});
+
+class RootActor extends protocol.Actor {
+ constructor(conn) {
+ super(conn, rootSpec);
+
+ this.actorID = "root";
+
+ // Root actor owns itself.
+ this.manage(this);
+
+ this.sequence = 0;
+ }
+
+ sayHello() {
+ return {
+ from: "root",
+ applicationType: "xpcshell-tests",
+ traits: [],
+ };
+ }
+
+ createChild() {
+ return new ChildActor(this.conn, this.sequence++);
+ }
+}
+
+class ChildFront extends protocol.FrontClassWithSpec(childSpec) {
+ form(form) {
+ this.childID = form.childID;
+ this.foo = form.foo;
+ }
+}
+protocol.registerFront(ChildFront);
+
+class RootFront extends protocol.FrontClassWithSpec(rootSpec) {
+ constructor(client) {
+ super(client);
+ this.actorID = "root";
+ // Root owns itself.
+ this.manage(this);
+ }
+}
+protocol.registerFront(RootFront);
+
+add_task(async function run_test() {
+ DevToolsServer.createRootActor = conn => new RootActor(conn);
+ DevToolsServer.init();
+
+ const trace = connectPipeTracing();
+ const client = new DevToolsClient(trace);
+ await client.connect();
+
+ const rootFront = client.mainRoot;
+
+ const fronts = [];
+ const listener = front => {
+ equal(
+ front.foo,
+ "bar",
+ "Front's form is set before watchFronts listeners are called"
+ );
+ fronts.push(front);
+ };
+ rootFront.watchFronts("childActor", listener);
+
+ const firstChild = await rootFront.createChild();
+ ok(
+ firstChild instanceof ChildFront,
+ "createChild returns a ChildFront instance"
+ );
+ equal(firstChild.childID, 0, "First child has ID=0");
+
+ equal(
+ fronts.length,
+ 1,
+ "watchFronts fires the callback, even if the front is created in the future"
+ );
+ equal(
+ fronts[0],
+ firstChild,
+ "watchFronts fires the callback with the right front instance"
+ );
+
+ const watchFrontsAfter = await new Promise(resolve => {
+ rootFront.watchFronts("childActor", resolve);
+ });
+ equal(
+ watchFrontsAfter,
+ firstChild,
+ "watchFronts fires the callback, even if the front is already created, " +
+ " with the same front instance"
+ );
+
+ equal(
+ fronts.length,
+ 1,
+ "There is still only one front reported from the first listener"
+ );
+
+ const secondChild = await rootFront.createChild();
+
+ equal(
+ fronts.length,
+ 2,
+ "After a second call to createChild, two fronts are reported"
+ );
+ equal(fronts[1], secondChild, "And the new front is the right instance");
+
+ // Test unregistering a front listener
+ rootFront.unwatchFronts("childActor", listener);
+
+ const thirdChild = await rootFront.createChild();
+ equal(
+ fronts.length,
+ 2,
+ "After calling unwatchFronts, the listener is no longer called"
+ );
+
+ // Test front destruction
+ const destroyed = [];
+ rootFront.watchFronts("childActor", null, front => {
+ destroyed.push(front);
+ });
+ await thirdChild.release();
+ equal(
+ destroyed.length,
+ 1,
+ "After the destruction of the front, one destruction is reported"
+ );
+ equal(destroyed[0], thirdChild, "And the destroyed front is the right one");
+
+ trace.close();
+ await client.close();
+});
diff --git a/devtools/shared/protocol/tests/xpcshell/xpcshell.ini b/devtools/shared/protocol/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..9af8c5de49
--- /dev/null
+++ b/devtools/shared/protocol/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+
+[test_protocol_abort.js]
+[test_protocol_async.js]
+[test_protocol_children.js]
+[test_protocol_index.js]
+[test_protocol_invalid_response.js]
+[test_protocol_lifecycle.js]
+[test_protocol_longstring.js]
+[test_protocol_simple.js]
+[test_protocol_stack.js]
+[test_protocol_types.js]
+[test_protocol_unregister.js]
+[test_protocol_watchFronts.js]
diff --git a/devtools/shared/protocol/types.js b/devtools/shared/protocol/types.js
new file mode 100644
index 0000000000..41764fbd79
--- /dev/null
+++ b/devtools/shared/protocol/types.js
@@ -0,0 +1,587 @@
+/* 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";
+
+var { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+var {
+ lazyLoadSpec,
+ lazyLoadFront,
+} = require("resource://devtools/shared/specs/index.js");
+
+/**
+ * Types: named marshallers/demarshallers.
+ *
+ * Types provide a 'write' function that takes a js representation and
+ * returns a protocol representation, and a "read" function that
+ * takes a protocol representation and returns a js representation.
+ *
+ * The read and write methods are also passed a context object that
+ * represent the actor or front requesting the translation.
+ *
+ * Types are referred to with a typestring. Basic types are
+ * registered by name using addType, and more complex types can
+ * be generated by adding detail to the type name.
+ */
+
+var types = Object.create(null);
+exports.types = types;
+
+var registeredTypes = (types.registeredTypes = new Map());
+
+exports.registeredTypes = registeredTypes;
+
+/**
+ * Return the type object associated with a given typestring.
+ * If passed a type object, it will be returned unchanged.
+ *
+ * Types can be registered with addType, or can be created on
+ * the fly with typestrings. Examples:
+ *
+ * boolean
+ * threadActor
+ * threadActor#detail
+ * array:threadActor
+ * array:array:threadActor#detail
+ *
+ * @param [typestring|type] type
+ * Either a typestring naming a type or a type object.
+ *
+ * @returns a type object.
+ */
+types.getType = function (type) {
+ if (!type) {
+ return types.Primitive;
+ }
+
+ if (typeof type !== "string") {
+ return type;
+ }
+
+ // If already registered, we're done here.
+ let reg = registeredTypes.get(type);
+ if (reg) {
+ return reg;
+ }
+
+ // Try to lazy load the spec, if not already loaded.
+ if (lazyLoadSpec(type)) {
+ // If a spec module was lazy loaded, it will synchronously call
+ // generateActorSpec, and set the type in `registeredTypes`.
+ reg = registeredTypes.get(type);
+ if (reg) {
+ return reg;
+ }
+ }
+
+ // New type, see if it's a collection type:
+ const sep = type.indexOf(":");
+ if (sep >= 0) {
+ const collection = type.substring(0, sep);
+ const subtype = types.getType(type.substring(sep + 1));
+
+ if (collection === "array") {
+ return types.addArrayType(subtype);
+ } else if (collection === "nullable") {
+ return types.addNullableType(subtype);
+ }
+
+ throw Error("Unknown collection type: " + collection);
+ }
+
+ // Not a collection, might be actor detail
+ const pieces = type.split("#", 2);
+ if (pieces.length > 1) {
+ if (pieces[1] != "actorid") {
+ throw new Error(
+ "Unsupported detail, only support 'actorid', got: " + pieces[1]
+ );
+ }
+ return types.addActorDetail(type, pieces[0], pieces[1]);
+ }
+
+ throw Error("Unknown type: " + type);
+};
+
+/**
+ * Don't allow undefined when writing primitive types to packets. If
+ * you want to allow undefined, use a nullable type.
+ */
+function identityWrite(v) {
+ if (v === undefined) {
+ throw Error("undefined passed where a value is required");
+ }
+ // This has to handle iterator->array conversion because arrays of
+ // primitive types pass through here.
+ if (v && typeof v.next === "function") {
+ return [...v];
+ }
+ return v;
+}
+
+/**
+ * Add a type to the type system.
+ *
+ * When registering a type, you can provide `read` and `write` methods.
+ *
+ * The `read` method will be passed a JS object value from the JSON
+ * packet and must return a native representation. The `write` method will
+ * be passed a native representation and should provide a JSONable value.
+ *
+ * These methods will both be passed a context. The context is the object
+ * performing or servicing the request - on the server side it will be
+ * an Actor, on the client side it will be a Front.
+ *
+ * @param typestring name
+ * Name to register
+ * @param object typeObject
+ * An object whose properties will be stored in the type, including
+ * the `read` and `write` methods.
+ * @param object options
+ * Can specify `thawed` to prevent the type from being frozen.
+ *
+ * @returns a type object that can be used in protocol definitions.
+ */
+types.addType = function (name, typeObject = {}, options = {}) {
+ if (registeredTypes.has(name)) {
+ throw Error("Type '" + name + "' already exists.");
+ }
+
+ const type = Object.assign(
+ {
+ toString() {
+ return "[protocol type:" + name + "]";
+ },
+ name,
+ primitive: !(typeObject.read || typeObject.write),
+ read: identityWrite,
+ write: identityWrite,
+ },
+ typeObject
+ );
+
+ registeredTypes.set(name, type);
+
+ return type;
+};
+
+/**
+ * Remove a type previously registered with the system.
+ * Primarily useful for types registered by addons.
+ */
+types.removeType = function (name) {
+ // This type may still be referenced by other types, make sure
+ // those references don't work.
+ const type = registeredTypes.get(name);
+
+ type.name = "DEFUNCT:" + name;
+ type.category = "defunct";
+ type.primitive = false;
+ type.read = type.write = function () {
+ throw new Error("Using defunct type: " + name);
+ };
+
+ registeredTypes.delete(name);
+};
+
+/**
+ * Add an array type to the type system.
+ *
+ * getType() will call this function if provided an "array:<type>"
+ * typestring.
+ *
+ * @param type subtype
+ * The subtype to be held by the array.
+ */
+types.addArrayType = function (subtype) {
+ subtype = types.getType(subtype);
+
+ const name = "array:" + subtype.name;
+
+ // Arrays of primitive types are primitive types themselves.
+ if (subtype.primitive) {
+ return types.addType(name);
+ }
+ return types.addType(name, {
+ category: "array",
+ read: (v, ctx) => {
+ if (v && typeof v.next === "function") {
+ v = [...v];
+ }
+ return v.map(i => subtype.read(i, ctx));
+ },
+ write: (v, ctx) => {
+ if (v && typeof v.next === "function") {
+ v = [...v];
+ }
+ return v.map(i => subtype.write(i, ctx));
+ },
+ });
+};
+
+/**
+ * Add a dict type to the type system. This allows you to serialize
+ * a JS object that contains non-primitive subtypes.
+ *
+ * Properties of the value that aren't included in the specializations
+ * will be serialized as primitive values.
+ *
+ * @param object specializations
+ * A dict of property names => type
+ */
+types.addDictType = function (name, specializations) {
+ const specTypes = {};
+ for (const prop in specializations) {
+ try {
+ specTypes[prop] = types.getType(specializations[prop]);
+ } catch (e) {
+ // Types may not be defined yet. Sometimes, we define the type *after* using it, but
+ // also, we have cyclic definitions on types. So lazily load them when they are not
+ // immediately available.
+ loader.lazyGetter(specTypes, prop, () => {
+ return types.getType(specializations[prop]);
+ });
+ }
+ }
+ return types.addType(name, {
+ category: "dict",
+ specializations,
+ read: (v, ctx) => {
+ const ret = {};
+ for (const prop in v) {
+ if (prop in specTypes) {
+ ret[prop] = specTypes[prop].read(v[prop], ctx);
+ } else {
+ ret[prop] = v[prop];
+ }
+ }
+ return ret;
+ },
+
+ write: (v, ctx) => {
+ const ret = {};
+ for (const prop in v) {
+ if (prop in specTypes) {
+ ret[prop] = specTypes[prop].write(v[prop], ctx);
+ } else {
+ ret[prop] = v[prop];
+ }
+ }
+ return ret;
+ },
+ });
+};
+
+/**
+ * Register an actor type with the type system.
+ *
+ * Types are marshalled differently when communicating server->client
+ * than they are when communicating client->server. The server needs
+ * to provide useful information to the client, so uses the actor's
+ * `form` method to get a json representation of the actor. When
+ * making a request from the client we only need the actor ID string.
+ *
+ * This function can be called before the associated actor has been
+ * constructed, but the read and write methods won't work until
+ * the associated addActorImpl or addActorFront methods have been
+ * called during actor/front construction.
+ *
+ * @param string name
+ * The typestring to register.
+ */
+types.addActorType = function (name) {
+ // We call addActorType from:
+ // FrontClassWithSpec when registering front synchronously,
+ // generateActorSpec when defining specs,
+ // specs modules to register actor type early to use them in other types
+ if (registeredTypes.has(name)) {
+ return registeredTypes.get(name);
+ }
+ const type = types.addType(name, {
+ _actor: true,
+ category: "actor",
+ read: (v, ctx, detail) => {
+ // If we're reading a request on the server side, just
+ // find the actor registered with this actorID.
+ if (ctx instanceof Actor) {
+ return ctx.conn.getActor(v);
+ }
+
+ // Reading a response on the client side, check for an
+ // existing front on the connection, and create the front
+ // if it isn't found.
+ const actorID = typeof v === "string" ? v : v.actor;
+ // `ctx.conn` is a DevToolsClient
+ let front = ctx.conn.getFrontByID(actorID);
+
+ // When the type `${name}#actorid` is used, `v` is a string refering to the
+ // actor ID. We cannot read form information in this case and the actorID was
+ // already set when creating the front, so no need to do anything.
+ let form = null;
+ if (detail != "actorid") {
+ form = identityWrite(v);
+ }
+
+ if (!front) {
+ // If front isn't instantiated yet, create one.
+ // Try lazy loading front if not already loaded.
+ // The front module will synchronously call `FrontClassWithSpec` and
+ // augment `type` with the `frontClass` attribute.
+ if (!type.frontClass) {
+ lazyLoadFront(name);
+ }
+
+ const parentFront = ctx.marshallPool();
+ const targetFront = parentFront.isTargetFront
+ ? parentFront
+ : parentFront.targetFront;
+
+ // Use intermediate Class variable to please eslint requiring
+ // a capital letter for all constructors.
+ const Class = type.frontClass;
+ front = new Class(ctx.conn, targetFront, parentFront);
+ front.actorID = actorID;
+
+ parentFront.manage(front, form, ctx);
+ } else if (form) {
+ front.form(form, ctx);
+ }
+
+ return front;
+ },
+ write: (v, ctx, detail) => {
+ // If returning a response from the server side, make sure
+ // the actor is added to a parent object and return its form.
+ if (v instanceof Actor) {
+ if (v.isDestroyed()) {
+ throw new Error(
+ `Attempted to write a response containing a destroyed actor`
+ );
+ }
+ if (!v.actorID) {
+ ctx.marshallPool().manage(v);
+ }
+ if (detail == "actorid") {
+ return v.actorID;
+ }
+ return identityWrite(v.form(detail));
+ }
+
+ // Writing a request from the client side, just send the actor id.
+ return v.actorID;
+ },
+ });
+ return type;
+};
+
+types.addPolymorphicType = function (name, subtypes) {
+ // Assert that all subtypes are actors, as the marshalling implementation depends on that.
+ for (const subTypeName of subtypes) {
+ const subtype = types.getType(subTypeName);
+ if (subtype.category != "actor") {
+ throw new Error(
+ `In polymorphic type '${subtypes.join(
+ ","
+ )}', the type '${subTypeName}' isn't an actor`
+ );
+ }
+ }
+
+ return types.addType(name, {
+ category: "polymorphic",
+ read: (value, ctx) => {
+ // `value` is either a string which is an Actor ID or a form object
+ // where `actor` is an actor ID
+ const actorID = typeof value === "string" ? value : value.actor;
+ if (!actorID) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'`
+ );
+ }
+
+ // Extract the typeName out of the actor ID, which should be composed like this
+ // ${DevToolsServerConnectionPrefix}.${typeName}${Number}
+ const typeName = actorID.match(/\.([a-zA-Z]+)\d+$/)[1];
+ if (!subtypes.includes(typeName)) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
+ );
+ }
+
+ const subtype = types.getType(typeName);
+ return subtype.read(value, ctx);
+ },
+ write: (value, ctx) => {
+ if (!value) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got an empty value.`
+ );
+ }
+ // value is either an `Actor` or a `Front` and both classes exposes a `typeName`
+ const typeName = value.typeName;
+ if (!typeName) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'. Did you pass a form instead of an Actor?`
+ );
+ }
+
+ if (!subtypes.includes(typeName)) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
+ );
+ }
+
+ const subtype = types.getType(typeName);
+ return subtype.write(value, ctx);
+ },
+ });
+};
+types.addNullableType = function (subtype) {
+ subtype = types.getType(subtype);
+ return types.addType("nullable:" + subtype.name, {
+ category: "nullable",
+ read: (value, ctx) => {
+ if (value == null) {
+ return value;
+ }
+ return subtype.read(value, ctx);
+ },
+ write: (value, ctx) => {
+ if (value == null) {
+ return value;
+ }
+ return subtype.write(value, ctx);
+ },
+ });
+};
+
+/**
+ * Register an actor detail type. This is just like an actor type, but
+ * will pass a detail hint to the actor's form method during serialization/
+ * deserialization.
+ *
+ * This is called by getType() when passed an 'actorType#detail' string.
+ *
+ * @param string name
+ * The typestring to register this type as.
+ * @param type actorType
+ * The actor type you'll be detailing.
+ * @param string detail
+ * The detail to pass.
+ */
+types.addActorDetail = function (name, actorType, detail) {
+ actorType = types.getType(actorType);
+ if (!actorType._actor) {
+ throw Error(
+ `Details only apply to actor types, tried to add detail '${detail}' ` +
+ `to ${actorType.name}`
+ );
+ }
+ return types.addType(name, {
+ _actor: true,
+ category: "detail",
+ read: (v, ctx) => actorType.read(v, ctx, detail),
+ write: (v, ctx) => actorType.write(v, ctx, detail),
+ });
+};
+
+// Add a few named primitive types.
+types.Primitive = types.addType("primitive");
+types.String = types.addType("string");
+types.Number = types.addType("number");
+types.Boolean = types.addType("boolean");
+types.JSON = types.addType("json");
+
+exports.registerFront = function (cls) {
+ const { typeName } = cls.prototype;
+ if (!registeredTypes.has(typeName)) {
+ types.addActorType(typeName);
+ }
+ registeredTypes.get(typeName).frontClass = cls;
+};
+
+/**
+ * Instantiate a front of the given type.
+ *
+ * @param DevToolsClient client
+ * The DevToolsClient instance to use.
+ * @param string typeName
+ * The type name of the front to instantiate. This is defined in its specifiation.
+ * @returns Front
+ * The created front.
+ */
+function createFront(client, typeName, target = null) {
+ const type = types.getType(typeName);
+ if (!type) {
+ throw new Error(`No spec for front type '${typeName}'.`);
+ } else if (!type.frontClass) {
+ lazyLoadFront(typeName);
+ }
+
+ // Use intermediate Class variable to please eslint requiring
+ // a capital letter for all constructors.
+ const Class = type.frontClass;
+ return new Class(client, target, target);
+}
+
+/**
+ * Instantiate a global (preference, device) or target-scoped (webconsole, inspector)
+ * front of the given type by picking its actor ID out of either the target or root
+ * front's form.
+ *
+ * @param DevToolsClient client
+ * The DevToolsClient instance to use.
+ * @param string typeName
+ * The type name of the front to instantiate. This is defined in its specifiation.
+ * @param json form
+ * If we want to instantiate a global actor's front, this is the root front's form,
+ * otherwise we are instantiating a target-scoped front from the target front's form.
+ * @param [Target|null] target
+ * If we are instantiating a target-scoped front, this is a reference to the front's
+ * Target instance, otherwise this is null.
+ */
+async function getFront(client, typeName, form, target = null) {
+ const front = createFront(client, typeName, target);
+ const { formAttributeName } = front;
+ if (!formAttributeName) {
+ throw new Error(`Can't find the form attribute name for ${typeName}`);
+ }
+ // Retrieve the actor ID from root or target actor's form
+ front.actorID = form[formAttributeName];
+ if (!front.actorID) {
+ throw new Error(
+ `Can't find the actor ID for ${typeName} from root or target` +
+ ` actor's form.`
+ );
+ }
+
+ if (!target) {
+ await front.manage(front);
+ } else {
+ await target.manage(front);
+ }
+
+ return front;
+}
+exports.getFront = getFront;
+
+/**
+ * Create a RootFront.
+ *
+ * @param DevToolsClient client
+ * The DevToolsClient instance to use.
+ * @param Object packet
+ * @returns RootFront
+ */
+function createRootFront(client, packet) {
+ const rootFront = createFront(client, "root");
+ rootFront.form(packet);
+
+ // Root Front is a special case, managing itself as it doesn't have any parent.
+ // It will register itself to DevToolsClient as a Pool via Front._poolMap.
+ rootFront.manage(rootFront);
+
+ return rootFront;
+}
+exports.createRootFront = createRootFront;
diff --git a/devtools/shared/protocol/utils.js b/devtools/shared/protocol/utils.js
new file mode 100644
index 0000000000..3433c2f446
--- /dev/null
+++ b/devtools/shared/protocol/utils.js
@@ -0,0 +1,44 @@
+/* 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";
+
+/**
+ * Find Placeholders in the template and save them along with their
+ * paths.
+ */
+function findPlaceholders(template, constructor, path = [], placeholders = []) {
+ if (!template || typeof template != "object") {
+ return placeholders;
+ }
+
+ if (template instanceof constructor) {
+ placeholders.push({ placeholder: template, path: [...path] });
+ return placeholders;
+ }
+
+ for (const name in template) {
+ path.push(name);
+ findPlaceholders(template[name], constructor, path, placeholders);
+ path.pop();
+ }
+
+ return placeholders;
+}
+
+exports.findPlaceholders = findPlaceholders;
+
+/**
+ * Get the value at a given path, or undefined if not found.
+ */
+function getPath(obj, path) {
+ for (const name of path) {
+ if (!(name in obj)) {
+ return undefined;
+ }
+ obj = obj[name];
+ }
+ return obj;
+}
+exports.getPath = getPath;