summaryrefslogtreecommitdiff
path: root/devtools/shared/client
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/client')
-rw-r--r--devtools/shared/client/connection-manager.js382
-rw-r--r--devtools/shared/client/main.js3123
-rw-r--r--devtools/shared/client/moz.build10
3 files changed, 3515 insertions, 0 deletions
diff --git a/devtools/shared/client/connection-manager.js b/devtools/shared/client/connection-manager.js
new file mode 100644
index 0000000000..ef242db853
--- /dev/null
+++ b/devtools/shared/client/connection-manager.js
@@ -0,0 +1,382 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 {Cc, Ci, Cu, Cr} = require("chrome");
+const EventEmitter = require("devtools/shared/event-emitter");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { DebuggerServer } = require("devtools/server/main");
+const { DebuggerClient } = require("devtools/shared/client/main");
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+
+const REMOTE_TIMEOUT = "devtools.debugger.remote-timeout";
+
+/**
+ * Connection Manager.
+ *
+ * To use this module:
+ * const {ConnectionManager} = require("devtools/shared/client/connection-manager");
+ *
+ * # ConnectionManager
+ *
+ * Methods:
+ * . Connection createConnection(host, port)
+ * . void destroyConnection(connection)
+ * . Number getFreeTCPPort()
+ *
+ * Properties:
+ * . Array connections
+ *
+ * # Connection
+ *
+ * A connection is a wrapper around a debugger client. It has a simple
+ * API to instantiate a connection to a debugger server. Once disconnected,
+ * no need to re-create a Connection object. Calling `connect()` again
+ * will re-create a debugger client.
+ *
+ * Methods:
+ * . connect() Connect to host:port. Expect a "connecting" event.
+ * If no host is not specified, a local pipe is used
+ * . connect(transport) Connect via transport. Expect a "connecting" event.
+ * . disconnect() Disconnect if connected. Expect a "disconnecting" event
+ *
+ * Properties:
+ * . host IP address or hostname
+ * . port Port
+ * . logs Current logs. "newlog" event notifies new available logs
+ * . store Reference to a local data store (see below)
+ * . keepConnecting Should the connection keep trying to connect?
+ * . timeoutDelay When should we give up (in ms)?
+ * 0 means wait forever.
+ * . encryption Should the connection be encrypted?
+ * . authentication What authentication scheme should be used?
+ * . authenticator The |Authenticator| instance used. Overriding
+ * properties of this instance may be useful to
+ * customize authentication UX for a specific use case.
+ * . advertisement The server's advertisement if found by discovery
+ * . status Connection status:
+ * Connection.Status.CONNECTED
+ * Connection.Status.DISCONNECTED
+ * Connection.Status.CONNECTING
+ * Connection.Status.DISCONNECTING
+ * Connection.Status.DESTROYED
+ *
+ * Events (as in event-emitter.js):
+ * . Connection.Events.CONNECTING Trying to connect to host:port
+ * . Connection.Events.CONNECTED Connection is successful
+ * . Connection.Events.DISCONNECTING Trying to disconnect from server
+ * . Connection.Events.DISCONNECTED Disconnected (at client request, or because of a timeout or connection error)
+ * . Connection.Events.STATUS_CHANGED The connection status (connection.status) has changed
+ * . Connection.Events.TIMEOUT Connection timeout
+ * . Connection.Events.HOST_CHANGED Host has changed
+ * . Connection.Events.PORT_CHANGED Port has changed
+ * . Connection.Events.NEW_LOG A new log line is available
+ *
+ */
+
+var ConnectionManager = {
+ _connections: new Set(),
+ createConnection: function (host, port) {
+ let c = new Connection(host, port);
+ c.once("destroy", (event) => this.destroyConnection(c));
+ this._connections.add(c);
+ this.emit("new", c);
+ return c;
+ },
+ destroyConnection: function (connection) {
+ if (this._connections.has(connection)) {
+ this._connections.delete(connection);
+ if (connection.status != Connection.Status.DESTROYED) {
+ connection.destroy();
+ }
+ }
+ },
+ get connections() {
+ return [...this._connections];
+ },
+ getFreeTCPPort: function () {
+ let serv = Cc["@mozilla.org/network/server-socket;1"]
+ .createInstance(Ci.nsIServerSocket);
+ serv.init(-1, true, -1);
+ let port = serv.port;
+ serv.close();
+ return port;
+ },
+};
+
+EventEmitter.decorate(ConnectionManager);
+
+var lastID = -1;
+
+function Connection(host, port) {
+ EventEmitter.decorate(this);
+ this.uid = ++lastID;
+ this.host = host;
+ this.port = port;
+ this._setStatus(Connection.Status.DISCONNECTED);
+ this._onDisconnected = this._onDisconnected.bind(this);
+ this._onConnected = this._onConnected.bind(this);
+ this._onTimeout = this._onTimeout.bind(this);
+ this.resetOptions();
+}
+
+Connection.Status = {
+ CONNECTED: "connected",
+ DISCONNECTED: "disconnected",
+ CONNECTING: "connecting",
+ DISCONNECTING: "disconnecting",
+ DESTROYED: "destroyed",
+};
+
+Connection.Events = {
+ CONNECTED: Connection.Status.CONNECTED,
+ DISCONNECTED: Connection.Status.DISCONNECTED,
+ CONNECTING: Connection.Status.CONNECTING,
+ DISCONNECTING: Connection.Status.DISCONNECTING,
+ DESTROYED: Connection.Status.DESTROYED,
+ TIMEOUT: "timeout",
+ STATUS_CHANGED: "status-changed",
+ HOST_CHANGED: "host-changed",
+ PORT_CHANGED: "port-changed",
+ NEW_LOG: "new_log"
+};
+
+Connection.prototype = {
+ logs: "",
+ log: function (str) {
+ let d = new Date();
+ let hours = ("0" + d.getHours()).slice(-2);
+ let minutes = ("0" + d.getMinutes()).slice(-2);
+ let seconds = ("0" + d.getSeconds()).slice(-2);
+ let timestamp = [hours, minutes, seconds].join(":") + ": ";
+ str = timestamp + str;
+ this.logs += "\n" + str;
+ this.emit(Connection.Events.NEW_LOG, str);
+ },
+
+ get client() {
+ return this._client;
+ },
+
+ get host() {
+ return this._host;
+ },
+
+ set host(value) {
+ if (this._host && this._host == value)
+ return;
+ this._host = value;
+ this.emit(Connection.Events.HOST_CHANGED);
+ },
+
+ get port() {
+ return this._port;
+ },
+
+ set port(value) {
+ if (this._port && this._port == value)
+ return;
+ this._port = value;
+ this.emit(Connection.Events.PORT_CHANGED);
+ },
+
+ get authentication() {
+ return this._authentication;
+ },
+
+ set authentication(value) {
+ this._authentication = value;
+ // Create an |Authenticator| of this type
+ if (!value) {
+ this.authenticator = null;
+ return;
+ }
+ let AuthenticatorType = DebuggerClient.Authenticators.get(value);
+ this.authenticator = new AuthenticatorType.Client();
+ },
+
+ get advertisement() {
+ return this._advertisement;
+ },
+
+ set advertisement(advertisement) {
+ // The full advertisement may contain more info than just the standard keys
+ // below, so keep a copy for use during connection later.
+ this._advertisement = advertisement;
+ if (advertisement) {
+ ["host", "port", "encryption", "authentication"].forEach(key => {
+ this[key] = advertisement[key];
+ });
+ }
+ },
+
+ /**
+ * Settings to be passed to |socketConnect| at connection time.
+ */
+ get socketSettings() {
+ let settings = {};
+ if (this.advertisement) {
+ // Use the advertisement as starting point if it exists, as it may contain
+ // extra data, like the server's cert.
+ Object.assign(settings, this.advertisement);
+ }
+ Object.assign(settings, {
+ host: this.host,
+ port: this.port,
+ encryption: this.encryption,
+ authenticator: this.authenticator
+ });
+ return settings;
+ },
+
+ timeoutDelay: Services.prefs.getIntPref(REMOTE_TIMEOUT),
+
+ resetOptions() {
+ this.keepConnecting = false;
+ this.timeoutDelay = Services.prefs.getIntPref(REMOTE_TIMEOUT);
+ this.encryption = false;
+ this.authentication = null;
+ this.advertisement = null;
+ },
+
+ disconnect: function (force) {
+ if (this.status == Connection.Status.DESTROYED) {
+ return;
+ }
+ clearTimeout(this._timeoutID);
+ if (this.status == Connection.Status.CONNECTED ||
+ this.status == Connection.Status.CONNECTING) {
+ this.log("disconnecting");
+ this._setStatus(Connection.Status.DISCONNECTING);
+ if (this._client) {
+ this._client.close();
+ }
+ }
+ },
+
+ connect: function (transport) {
+ if (this.status == Connection.Status.DESTROYED) {
+ return;
+ }
+ if (!this._client) {
+ this._customTransport = transport;
+ if (this._customTransport) {
+ this.log("connecting (custom transport)");
+ } else {
+ this.log("connecting to " + this.host + ":" + this.port);
+ }
+ this._setStatus(Connection.Status.CONNECTING);
+
+ if (this.timeoutDelay > 0) {
+ this._timeoutID = setTimeout(this._onTimeout, this.timeoutDelay);
+ }
+ this._clientConnect();
+ } else {
+ let msg = "Can't connect. Client is not fully disconnected";
+ this.log(msg);
+ throw new Error(msg);
+ }
+ },
+
+ destroy: function () {
+ this.log("killing connection");
+ clearTimeout(this._timeoutID);
+ this.keepConnecting = false;
+ if (this._client) {
+ this._client.close();
+ this._client = null;
+ }
+ this._setStatus(Connection.Status.DESTROYED);
+ },
+
+ _getTransport: Task.async(function* () {
+ if (this._customTransport) {
+ return this._customTransport;
+ }
+ if (!this.host) {
+ return DebuggerServer.connectPipe();
+ }
+ let settings = this.socketSettings;
+ let transport = yield DebuggerClient.socketConnect(settings);
+ return transport;
+ }),
+
+ _clientConnect: function () {
+ this._getTransport().then(transport => {
+ if (!transport) {
+ return;
+ }
+ this._client = new DebuggerClient(transport);
+ this._client.addOneTimeListener("closed", this._onDisconnected);
+ this._client.connect().then(this._onConnected);
+ }, e => {
+ // If we're continuously trying to connect, we expect the connection to be
+ // rejected a couple times, so don't log these.
+ if (!this.keepConnecting || e.result !== Cr.NS_ERROR_CONNECTION_REFUSED) {
+ console.error(e);
+ }
+ // In some cases, especially on Mac, the openOutputStream call in
+ // DebuggerClient.socketConnect may throw NS_ERROR_NOT_INITIALIZED.
+ // It occurs when we connect agressively to the simulator,
+ // and keep trying to open a socket to the server being started in
+ // the simulator.
+ this._onDisconnected();
+ });
+ },
+
+ get status() {
+ return this._status;
+ },
+
+ _setStatus: function (value) {
+ if (this._status && this._status == value)
+ return;
+ this._status = value;
+ this.emit(value);
+ this.emit(Connection.Events.STATUS_CHANGED, value);
+ },
+
+ _onDisconnected: function () {
+ this._client = null;
+ this._customTransport = null;
+
+ if (this._status == Connection.Status.CONNECTING && this.keepConnecting) {
+ setTimeout(() => this._clientConnect(), 100);
+ return;
+ }
+
+ clearTimeout(this._timeoutID);
+
+ switch (this.status) {
+ case Connection.Status.CONNECTED:
+ this.log("disconnected (unexpected)");
+ break;
+ case Connection.Status.CONNECTING:
+ this.log("connection error. Possible causes: USB port not connected, port not forwarded (adb forward), wrong host or port, remote debugging not enabled on the device.");
+ break;
+ default:
+ this.log("disconnected");
+ }
+ this._setStatus(Connection.Status.DISCONNECTED);
+ },
+
+ _onConnected: function () {
+ this.log("connected");
+ clearTimeout(this._timeoutID);
+ this._setStatus(Connection.Status.CONNECTED);
+ },
+
+ _onTimeout: function () {
+ this.log("connection timeout. Possible causes: didn't click on 'accept' (prompt).");
+ this.emit(Connection.Events.TIMEOUT);
+ this.disconnect();
+ },
+};
+
+exports.ConnectionManager = ConnectionManager;
+exports.Connection = Connection;
diff --git a/devtools/shared/client/main.js b/devtools/shared/client/main.js
new file mode 100644
index 0000000000..0db8e16c20
--- /dev/null
+++ b/devtools/shared/client/main.js
@@ -0,0 +1,3123 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Ci, Cu } = require("chrome");
+const Services = require("Services");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { getStack, callFunctionWithAsyncStack } = require("devtools/shared/platform/stack");
+
+const promise = Cu.import("resource://devtools/shared/deprecated-sync-thenables.js", {}).Promise;
+
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
+loader.lazyRequireGetter(this, "WebConsoleClient", "devtools/shared/webconsole/client", true);
+loader.lazyRequireGetter(this, "DebuggerSocket", "devtools/shared/security/socket", true);
+loader.lazyRequireGetter(this, "Authentication", "devtools/shared/security/auth");
+
+const noop = () => {};
+
+/**
+ * TODO: Get rid of this API in favor of EventTarget (bug 1042642)
+ *
+ * Add simple event notification to a prototype object. Any object that has
+ * some use for event notifications or the observer pattern in general can be
+ * augmented with the necessary facilities by passing its prototype to this
+ * function.
+ *
+ * @param aProto object
+ * The prototype object that will be modified.
+ */
+function eventSource(aProto) {
+ /**
+ * Add a listener to the event source for a given event.
+ *
+ * @param aName string
+ * The event to listen for.
+ * @param aListener function
+ * Called when the event is fired. If the same listener
+ * is added more than once, it will be called once per
+ * addListener call.
+ */
+ aProto.addListener = function (aName, aListener) {
+ if (typeof aListener != "function") {
+ throw TypeError("Listeners must be functions.");
+ }
+
+ if (!this._listeners) {
+ this._listeners = {};
+ }
+
+ this._getListeners(aName).push(aListener);
+ };
+
+ /**
+ * Add a listener to the event source for a given event. The
+ * listener will be removed after it is called for the first time.
+ *
+ * @param aName string
+ * The event to listen for.
+ * @param aListener function
+ * Called when the event is fired.
+ */
+ aProto.addOneTimeListener = function (aName, aListener) {
+ let l = (...args) => {
+ this.removeListener(aName, l);
+ aListener.apply(null, args);
+ };
+ this.addListener(aName, l);
+ };
+
+ /**
+ * Remove a listener from the event source previously added with
+ * addListener().
+ *
+ * @param aName string
+ * The event name used during addListener to add the listener.
+ * @param aListener function
+ * The callback to remove. If addListener was called multiple
+ * times, all instances will be removed.
+ */
+ aProto.removeListener = function (aName, aListener) {
+ if (!this._listeners || (aListener && !this._listeners[aName])) {
+ return;
+ }
+
+ if (!aListener) {
+ this._listeners[aName] = [];
+ }
+ else {
+ this._listeners[aName] =
+ this._listeners[aName].filter(function (l) { return l != aListener; });
+ }
+ };
+
+ /**
+ * Returns the listeners for the specified event name. If none are defined it
+ * initializes an empty list and returns that.
+ *
+ * @param aName string
+ * The event name.
+ */
+ aProto._getListeners = function (aName) {
+ if (aName in this._listeners) {
+ return this._listeners[aName];
+ }
+ this._listeners[aName] = [];
+ return this._listeners[aName];
+ };
+
+ /**
+ * Notify listeners of an event.
+ *
+ * @param aName string
+ * The event to fire.
+ * @param arguments
+ * All arguments will be passed along to the listeners,
+ * including the name argument.
+ */
+ aProto.emit = function () {
+ if (!this._listeners) {
+ return;
+ }
+
+ let name = arguments[0];
+ let listeners = this._getListeners(name).slice(0);
+
+ for (let listener of listeners) {
+ try {
+ listener.apply(null, arguments);
+ } catch (e) {
+ // Prevent a bad listener from interfering with the others.
+ DevToolsUtils.reportException("notify event '" + name + "'", e);
+ }
+ }
+ };
+}
+
+/**
+ * Set of protocol messages that affect thread state, and the
+ * state the actor is in after each message.
+ */
+const ThreadStateTypes = {
+ "paused": "paused",
+ "resumed": "attached",
+ "detached": "detached",
+ "running": "attached"
+};
+
+/**
+ * Set of protocol messages that are sent by the server without a prior request
+ * by the client.
+ */
+const UnsolicitedNotifications = {
+ "consoleAPICall": "consoleAPICall",
+ "eventNotification": "eventNotification",
+ "fileActivity": "fileActivity",
+ "lastPrivateContextExited": "lastPrivateContextExited",
+ "logMessage": "logMessage",
+ "networkEvent": "networkEvent",
+ "networkEventUpdate": "networkEventUpdate",
+ "newGlobal": "newGlobal",
+ "newScript": "newScript",
+ "tabDetached": "tabDetached",
+ "tabListChanged": "tabListChanged",
+ "reflowActivity": "reflowActivity",
+ "addonListChanged": "addonListChanged",
+ "workerListChanged": "workerListChanged",
+ "serviceWorkerRegistrationListChanged": "serviceWorkerRegistrationList",
+ "tabNavigated": "tabNavigated",
+ "frameUpdate": "frameUpdate",
+ "pageError": "pageError",
+ "documentLoad": "documentLoad",
+ "enteredFrame": "enteredFrame",
+ "exitedFrame": "exitedFrame",
+ "appOpen": "appOpen",
+ "appClose": "appClose",
+ "appInstall": "appInstall",
+ "appUninstall": "appUninstall",
+ "evaluationResult": "evaluationResult",
+ "newSource": "newSource",
+ "updatedSource": "updatedSource",
+};
+
+/**
+ * Set of pause types that are sent by the server and not as an immediate
+ * response to a client request.
+ */
+const UnsolicitedPauses = {
+ "resumeLimit": "resumeLimit",
+ "debuggerStatement": "debuggerStatement",
+ "breakpoint": "breakpoint",
+ "DOMEvent": "DOMEvent",
+ "watchpoint": "watchpoint",
+ "exception": "exception"
+};
+
+/**
+ * Creates a client for the remote debugging protocol server. This client
+ * provides the means to communicate with the server and exchange the messages
+ * required by the protocol in a traditional JavaScript API.
+ */
+const DebuggerClient = exports.DebuggerClient = function (aTransport)
+{
+ this._transport = aTransport;
+ this._transport.hooks = this;
+
+ // Map actor ID to client instance for each actor type.
+ this._clients = new Map();
+
+ this._pendingRequests = new Map();
+ this._activeRequests = new Map();
+ this._eventsEnabled = true;
+
+ this.traits = {};
+
+ this.request = this.request.bind(this);
+ this.localTransport = this._transport.onOutputStreamReady === undefined;
+
+ /*
+ * As the first thing on the connection, expect a greeting packet from
+ * the connection's root actor.
+ */
+ this.mainRoot = null;
+ this.expectReply("root", (aPacket) => {
+ this.mainRoot = new RootClient(this, aPacket);
+ this.emit("connected", aPacket.applicationType, aPacket.traits);
+ });
+};
+
+/**
+ * A declarative helper for defining methods that send requests to the server.
+ *
+ * @param aPacketSkeleton
+ * The form of the packet to send. Can specify fields to be filled from
+ * the parameters by using the |args| function.
+ * @param before
+ * The function to call before sending the packet. Is passed the packet,
+ * and the return value is used as the new packet. The |this| context is
+ * the instance of the client object we are defining a method for.
+ * @param after
+ * The function to call after the response is received. It is passed the
+ * response, and the return value is considered the new response that
+ * will be passed to the callback. The |this| context is the instance of
+ * the client object we are defining a method for.
+ * @return Request
+ * The `Request` object that is a Promise object and resolves once
+ * we receive the response. (See request method for more details)
+ */
+DebuggerClient.requester = function (aPacketSkeleton, config = {}) {
+ let { before, after } = config;
+ return DevToolsUtils.makeInfallible(function (...args) {
+ let outgoingPacket = {
+ to: aPacketSkeleton.to || this.actor
+ };
+
+ let maxPosition = -1;
+ for (let k of Object.keys(aPacketSkeleton)) {
+ if (aPacketSkeleton[k] instanceof DebuggerClient.Argument) {
+ let { position } = aPacketSkeleton[k];
+ outgoingPacket[k] = aPacketSkeleton[k].getArgument(args);
+ maxPosition = Math.max(position, maxPosition);
+ } else {
+ outgoingPacket[k] = aPacketSkeleton[k];
+ }
+ }
+
+ if (before) {
+ outgoingPacket = before.call(this, outgoingPacket);
+ }
+
+ return this.request(outgoingPacket, DevToolsUtils.makeInfallible((aResponse) => {
+ if (after) {
+ let { from } = aResponse;
+ aResponse = after.call(this, aResponse);
+ if (!aResponse.from) {
+ aResponse.from = from;
+ }
+ }
+
+ // The callback is always the last parameter.
+ let thisCallback = args[maxPosition + 1];
+ if (thisCallback) {
+ thisCallback(aResponse);
+ }
+ }, "DebuggerClient.requester request callback"));
+ }, "DebuggerClient.requester");
+};
+
+function args(aPos) {
+ return new DebuggerClient.Argument(aPos);
+}
+
+DebuggerClient.Argument = function (aPosition) {
+ this.position = aPosition;
+};
+
+DebuggerClient.Argument.prototype.getArgument = function (aParams) {
+ if (!(this.position in aParams)) {
+ throw new Error("Bad index into params: " + this.position);
+ }
+ return aParams[this.position];
+};
+
+// Expose these to save callers the trouble of importing DebuggerSocket
+DebuggerClient.socketConnect = function (options) {
+ // Defined here instead of just copying the function to allow lazy-load
+ return DebuggerSocket.connect(options);
+};
+DevToolsUtils.defineLazyGetter(DebuggerClient, "Authenticators", () => {
+ return Authentication.Authenticators;
+});
+DevToolsUtils.defineLazyGetter(DebuggerClient, "AuthenticationResult", () => {
+ return Authentication.AuthenticationResult;
+});
+
+DebuggerClient.prototype = {
+ /**
+ * Connect to the server and start exchanging protocol messages.
+ *
+ * @param aOnConnected function
+ * If specified, will be called when the greeting packet is
+ * received from the debugging server.
+ *
+ * @return Promise
+ * Resolves once connected with an array whose first element
+ * is the application type, by default "browser", and the second
+ * element is the traits object (help figure out the features
+ * and behaviors of the server we connect to. See RootActor).
+ */
+ connect: function (aOnConnected) {
+ let deferred = promise.defer();
+ this.emit("connect");
+
+ // Also emit the event on the |DebuggerClient| object (not on the instance),
+ // so it's possible to track all instances.
+ events.emit(DebuggerClient, "connect", this);
+
+ this.addOneTimeListener("connected", (aName, aApplicationType, aTraits) => {
+ this.traits = aTraits;
+ if (aOnConnected) {
+ aOnConnected(aApplicationType, aTraits);
+ }
+ deferred.resolve([aApplicationType, aTraits]);
+ });
+
+ this._transport.ready();
+ return deferred.promise;
+ },
+
+ /**
+ * Shut down communication with the debugging server.
+ *
+ * @param aOnClosed function
+ * If specified, will be called when the debugging connection
+ * has been closed. This parameter is deprecated - please use
+ * the returned Promise.
+ * @return Promise
+ * Resolves after the underlying transport is closed.
+ */
+ close: function (aOnClosed) {
+ let deferred = promise.defer();
+ if (aOnClosed) {
+ deferred.promise.then(aOnClosed);
+ }
+
+ // Disable detach event notifications, because event handlers will be in a
+ // cleared scope by the time they run.
+ this._eventsEnabled = false;
+
+ let cleanup = () => {
+ this._transport.close();
+ this._transport = null;
+ };
+
+ // If the connection is already closed,
+ // there is no need to detach client
+ // as we won't be able to send any message.
+ if (this._closed) {
+ cleanup();
+ deferred.resolve();
+ return deferred.promise;
+ }
+
+ this.addOneTimeListener("closed", deferred.resolve);
+
+ // Call each client's `detach` method by calling
+ // lastly registered ones first to give a chance
+ // to detach child clients first.
+ let clients = [...this._clients.values()];
+ this._clients.clear();
+ const detachClients = () => {
+ let client = clients.pop();
+ if (!client) {
+ // All clients detached.
+ cleanup();
+ return;
+ }
+ if (client.detach) {
+ client.detach(detachClients);
+ return;
+ }
+ detachClients();
+ };
+ detachClients();
+
+ return deferred.promise;
+ },
+
+ /*
+ * This function exists only to preserve DebuggerClient's interface;
+ * new code should say 'client.mainRoot.listTabs()'.
+ */
+ listTabs: function (aOnResponse) { return this.mainRoot.listTabs(aOnResponse); },
+
+ /*
+ * This function exists only to preserve DebuggerClient's interface;
+ * new code should say 'client.mainRoot.listAddons()'.
+ */
+ listAddons: function (aOnResponse) { return this.mainRoot.listAddons(aOnResponse); },
+
+ getTab: function (aFilter) { return this.mainRoot.getTab(aFilter); },
+
+ /**
+ * Attach to a tab actor.
+ *
+ * @param string aTabActor
+ * The actor ID for the tab to attach.
+ * @param function aOnResponse
+ * Called with the response packet and a TabClient
+ * (which will be undefined on error).
+ */
+ attachTab: function (aTabActor, aOnResponse = noop) {
+ if (this._clients.has(aTabActor)) {
+ let cachedTab = this._clients.get(aTabActor);
+ let cachedResponse = {
+ cacheDisabled: cachedTab.cacheDisabled,
+ javascriptEnabled: cachedTab.javascriptEnabled,
+ traits: cachedTab.traits,
+ };
+ DevToolsUtils.executeSoon(() => aOnResponse(cachedResponse, cachedTab));
+ return promise.resolve([cachedResponse, cachedTab]);
+ }
+
+ let packet = {
+ to: aTabActor,
+ type: "attach"
+ };
+ return this.request(packet).then(aResponse => {
+ let tabClient;
+ if (!aResponse.error) {
+ tabClient = new TabClient(this, aResponse);
+ this.registerClient(tabClient);
+ }
+ aOnResponse(aResponse, tabClient);
+ return [aResponse, tabClient];
+ });
+ },
+
+ attachWorker: function DC_attachWorker(aWorkerActor, aOnResponse = noop) {
+ let workerClient = this._clients.get(aWorkerActor);
+ if (workerClient !== undefined) {
+ let response = {
+ from: workerClient.actor,
+ type: "attached",
+ url: workerClient.url
+ };
+ DevToolsUtils.executeSoon(() => aOnResponse(response, workerClient));
+ return promise.resolve([response, workerClient]);
+ }
+
+ return this.request({ to: aWorkerActor, type: "attach" }).then(aResponse => {
+ if (aResponse.error) {
+ aOnResponse(aResponse, null);
+ return [aResponse, null];
+ }
+
+ let workerClient = new WorkerClient(this, aResponse);
+ this.registerClient(workerClient);
+ aOnResponse(aResponse, workerClient);
+ return [aResponse, workerClient];
+ });
+ },
+
+ /**
+ * Attach to an addon actor.
+ *
+ * @param string aAddonActor
+ * The actor ID for the addon to attach.
+ * @param function aOnResponse
+ * Called with the response packet and a AddonClient
+ * (which will be undefined on error).
+ */
+ attachAddon: function DC_attachAddon(aAddonActor, aOnResponse = noop) {
+ let packet = {
+ to: aAddonActor,
+ type: "attach"
+ };
+ return this.request(packet).then(aResponse => {
+ let addonClient;
+ if (!aResponse.error) {
+ addonClient = new AddonClient(this, aAddonActor);
+ this.registerClient(addonClient);
+ this.activeAddon = addonClient;
+ }
+ aOnResponse(aResponse, addonClient);
+ return [aResponse, addonClient];
+ });
+ },
+
+ /**
+ * Attach to a Web Console actor.
+ *
+ * @param string aConsoleActor
+ * The ID for the console actor to attach to.
+ * @param array aListeners
+ * The console listeners you want to start.
+ * @param function aOnResponse
+ * Called with the response packet and a WebConsoleClient
+ * instance (which will be undefined on error).
+ */
+ attachConsole:
+ function (aConsoleActor, aListeners, aOnResponse = noop) {
+ let packet = {
+ to: aConsoleActor,
+ type: "startListeners",
+ listeners: aListeners,
+ };
+
+ return this.request(packet).then(aResponse => {
+ let consoleClient;
+ if (!aResponse.error) {
+ if (this._clients.has(aConsoleActor)) {
+ consoleClient = this._clients.get(aConsoleActor);
+ } else {
+ consoleClient = new WebConsoleClient(this, aResponse);
+ this.registerClient(consoleClient);
+ }
+ }
+ aOnResponse(aResponse, consoleClient);
+ return [aResponse, consoleClient];
+ });
+ },
+
+ /**
+ * Attach to a global-scoped thread actor for chrome debugging.
+ *
+ * @param string aThreadActor
+ * The actor ID for the thread to attach.
+ * @param function aOnResponse
+ * Called with the response packet and a ThreadClient
+ * (which will be undefined on error).
+ * @param object aOptions
+ * Configuration options.
+ * - useSourceMaps: whether to use source maps or not.
+ */
+ attachThread: function (aThreadActor, aOnResponse = noop, aOptions = {}) {
+ if (this._clients.has(aThreadActor)) {
+ let client = this._clients.get(aThreadActor);
+ DevToolsUtils.executeSoon(() => aOnResponse({}, client));
+ return promise.resolve([{}, client]);
+ }
+
+ let packet = {
+ to: aThreadActor,
+ type: "attach",
+ options: aOptions
+ };
+ return this.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ var threadClient = new ThreadClient(this, aThreadActor);
+ this.registerClient(threadClient);
+ }
+ aOnResponse(aResponse, threadClient);
+ return [aResponse, threadClient];
+ });
+ },
+
+ /**
+ * Attach to a trace actor.
+ *
+ * @param string aTraceActor
+ * The actor ID for the tracer to attach.
+ * @param function aOnResponse
+ * Called with the response packet and a TraceClient
+ * (which will be undefined on error).
+ */
+ attachTracer: function (aTraceActor, aOnResponse = noop) {
+ if (this._clients.has(aTraceActor)) {
+ let client = this._clients.get(aTraceActor);
+ DevToolsUtils.executeSoon(() => aOnResponse({}, client));
+ return promise.resolve([{}, client]);
+ }
+
+ let packet = {
+ to: aTraceActor,
+ type: "attach"
+ };
+ return this.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ var traceClient = new TraceClient(this, aTraceActor);
+ this.registerClient(traceClient);
+ }
+ aOnResponse(aResponse, traceClient);
+ return [aResponse, traceClient];
+ });
+ },
+
+ /**
+ * Fetch the ChromeActor for the main process or ChildProcessActor for a
+ * a given child process ID.
+ *
+ * @param number aId
+ * The ID for the process to attach (returned by `listProcesses`).
+ * Connected to the main process if omitted, or is 0.
+ */
+ getProcess: function (aId) {
+ let packet = {
+ to: "root",
+ type: "getProcess"
+ };
+ if (typeof (aId) == "number") {
+ packet.id = aId;
+ }
+ return this.request(packet);
+ },
+
+ /**
+ * Release an object actor.
+ *
+ * @param string aActor
+ * The actor ID to send the request to.
+ * @param aOnResponse function
+ * If specified, will be called with the response packet when
+ * debugging server responds.
+ */
+ release: DebuggerClient.requester({
+ to: args(0),
+ type: "release"
+ }),
+
+ /**
+ * Send a request to the debugging server.
+ *
+ * @param aRequest object
+ * A JSON packet to send to the debugging server.
+ * @param aOnResponse function
+ * If specified, will be called with the JSON response packet when
+ * debugging server responds.
+ * @return Request
+ * This object emits a number of events to allow you to respond to
+ * different parts of the request lifecycle.
+ * It is also a Promise object, with a `then` method, that is resolved
+ * whenever a JSON or a Bulk response is received; and is rejected
+ * if the response is an error.
+ * Note: This return value can be ignored if you are using JSON alone,
+ * because the callback provided in |aOnResponse| will be bound to the
+ * "json-reply" event automatically.
+ *
+ * Events emitted:
+ * * json-reply: The server replied with a JSON packet, which is
+ * passed as event data.
+ * * bulk-reply: The server replied with bulk data, which you can read
+ * using the event data object containing:
+ * * actor: Name of actor that received the packet
+ * * type: Name of actor's method that was called on receipt
+ * * length: Size of the data to be read
+ * * stream: This input stream should only be used directly if you
+ * can ensure that you will read exactly |length| bytes
+ * and will not close the stream when reading is complete
+ * * done: If you use the stream directly (instead of |copyTo|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the
+ * transport will be closed. If an Error is supplied as a
+ * rejection value, it will be logged via |dumpn|. If you
+ * do use |copyTo|, resolving is taken care of for you
+ * when copying completes.
+ * * copyTo: A helper function for getting your data out of the
+ * stream that meets the stream handling requirements
+ * above, and has the following signature:
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @return Promise
+ * The promise is resolved when copying completes or
+ * rejected if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ */
+ request: function (aRequest, aOnResponse) {
+ if (!this.mainRoot) {
+ throw Error("Have not yet received a hello packet from the server.");
+ }
+ let type = aRequest.type || "";
+ if (!aRequest.to) {
+ throw Error("'" + type + "' request packet has no destination.");
+ }
+ if (this._closed) {
+ let msg = "'" + type + "' request packet to " +
+ "'" + aRequest.to + "' " +
+ "can't be sent as the connection is closed.";
+ let resp = { error: "connectionClosed", message: msg };
+ if (aOnResponse) {
+ aOnResponse(resp);
+ }
+ return promise.reject(resp);
+ }
+
+ let request = new Request(aRequest);
+ request.format = "json";
+ request.stack = getStack();
+ if (aOnResponse) {
+ request.on("json-reply", aOnResponse);
+ }
+
+ this._sendOrQueueRequest(request);
+
+ // Implement a Promise like API on the returned object
+ // that resolves/rejects on request response
+ let deferred = promise.defer();
+ function listenerJson(resp) {
+ request.off("json-reply", listenerJson);
+ request.off("bulk-reply", listenerBulk);
+ if (resp.error) {
+ deferred.reject(resp);
+ } else {
+ deferred.resolve(resp);
+ }
+ }
+ function listenerBulk(resp) {
+ request.off("json-reply", listenerJson);
+ request.off("bulk-reply", listenerBulk);
+ deferred.resolve(resp);
+ }
+ request.on("json-reply", listenerJson);
+ request.on("bulk-reply", listenerBulk);
+ request.then = deferred.promise.then.bind(deferred.promise);
+
+ return request;
+ },
+
+ /**
+ * Transmit streaming data via a bulk request.
+ *
+ * This method initiates the bulk send process by queuing up the header data.
+ * The caller receives eventual access to a stream for writing.
+ *
+ * Since this opens up more options for how the server might respond (it could
+ * send back either JSON or bulk data), and the returned Request object emits
+ * events for different stages of the request process that you may want to
+ * react to.
+ *
+ * @param request Object
+ * This is modeled after the format of JSON packets above, but does not
+ * actually contain the data, but is instead just a routing header:
+ * * actor: Name of actor that will receive the packet
+ * * type: Name of actor's method that should be called on receipt
+ * * length: Size of the data to be sent
+ * @return Request
+ * This object emits a number of events to allow you to respond to
+ * different parts of the request lifecycle.
+ *
+ * Events emitted:
+ * * bulk-send-ready: Ready to send bulk data to the server, using the
+ * event data object containing:
+ * * stream: This output stream should only be used directly if
+ * you can ensure that you will write exactly |length|
+ * bytes and will not close the stream when writing is
+ * complete
+ * * done: If you use the stream directly (instead of |copyFrom|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the
+ * transport will be closed. If an Error is supplied as
+ * a rejection value, it will be logged via |dumpn|. If
+ * you do use |copyFrom|, resolving is taken care of for
+ * you when copying completes.
+ * * copyFrom: A helper function for getting your data onto the
+ * stream that meets the stream handling requirements
+ * above, and has the following signature:
+ * @param input nsIAsyncInputStream
+ * The stream to copy from.
+ * @return Promise
+ * The promise is resolved when copying completes or
+ * rejected if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ * * json-reply: The server replied with a JSON packet, which is
+ * passed as event data.
+ * * bulk-reply: The server replied with bulk data, which you can read
+ * using the event data object containing:
+ * * actor: Name of actor that received the packet
+ * * type: Name of actor's method that was called on receipt
+ * * length: Size of the data to be read
+ * * stream: This input stream should only be used directly if you
+ * can ensure that you will read exactly |length| bytes
+ * and will not close the stream when reading is complete
+ * * done: If you use the stream directly (instead of |copyTo|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the
+ * transport will be closed. If an Error is supplied as a
+ * rejection value, it will be logged via |dumpn|. If you
+ * do use |copyTo|, resolving is taken care of for you
+ * when copying completes.
+ * * copyTo: A helper function for getting your data out of the
+ * stream that meets the stream handling requirements
+ * above, and has the following signature:
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @return Promise
+ * The promise is resolved when copying completes or
+ * rejected if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ */
+ startBulkRequest: function (request) {
+ if (!this.traits.bulk) {
+ throw Error("Server doesn't support bulk transfers");
+ }
+ if (!this.mainRoot) {
+ throw Error("Have not yet received a hello packet from the server.");
+ }
+ if (!request.type) {
+ throw Error("Bulk packet is missing the required 'type' field.");
+ }
+ if (!request.actor) {
+ throw Error("'" + request.type + "' bulk packet has no destination.");
+ }
+ if (!request.length) {
+ throw Error("'" + request.type + "' bulk packet has no length.");
+ }
+
+ request = new Request(request);
+ request.format = "bulk";
+
+ this._sendOrQueueRequest(request);
+
+ return request;
+ },
+
+ /**
+ * If a new request can be sent immediately, do so. Otherwise, queue it.
+ */
+ _sendOrQueueRequest(request) {
+ let actor = request.actor;
+ if (!this._activeRequests.has(actor)) {
+ this._sendRequest(request);
+ } else {
+ this._queueRequest(request);
+ }
+ },
+
+ /**
+ * Send a request.
+ * @throws Error if there is already an active request in flight for the same
+ * actor.
+ */
+ _sendRequest(request) {
+ let actor = request.actor;
+ this.expectReply(actor, request);
+
+ if (request.format === "json") {
+ this._transport.send(request.request);
+ return false;
+ }
+
+ this._transport.startBulkSend(request.request).then((...args) => {
+ request.emit("bulk-send-ready", ...args);
+ });
+ },
+
+ /**
+ * Queue a request to be sent later. Queues are only drained when an in
+ * flight request to a given actor completes.
+ */
+ _queueRequest(request) {
+ let actor = request.actor;
+ let queue = this._pendingRequests.get(actor) || [];
+ queue.push(request);
+ this._pendingRequests.set(actor, queue);
+ },
+
+ /**
+ * Attempt the next request to a given actor (if any).
+ */
+ _attemptNextRequest(actor) {
+ if (this._activeRequests.has(actor)) {
+ return;
+ }
+ let queue = this._pendingRequests.get(actor);
+ if (!queue) {
+ return;
+ }
+ let request = queue.shift();
+ if (queue.length === 0) {
+ this._pendingRequests.delete(actor);
+ }
+ this._sendRequest(request);
+ },
+
+ /**
+ * Arrange to hand the next reply from |aActor| to the handler bound to
+ * |aRequest|.
+ *
+ * DebuggerClient.prototype.request / startBulkRequest usually takes care of
+ * establishing the handler for a given request, but in rare cases (well,
+ * greetings from new root actors, is the only case at the moment) we must be
+ * prepared for a "reply" that doesn't correspond to any request we sent.
+ */
+ expectReply: function (aActor, aRequest) {
+ if (this._activeRequests.has(aActor)) {
+ throw Error("clashing handlers for next reply from " + uneval(aActor));
+ }
+
+ // If a handler is passed directly (as it is with the handler for the root
+ // actor greeting), create a dummy request to bind this to.
+ if (typeof aRequest === "function") {
+ let handler = aRequest;
+ aRequest = new Request();
+ aRequest.on("json-reply", handler);
+ }
+
+ this._activeRequests.set(aActor, aRequest);
+ },
+
+ // Transport hooks.
+
+ /**
+ * Called by DebuggerTransport to dispatch incoming packets as appropriate.
+ *
+ * @param aPacket object
+ * The incoming packet.
+ */
+ onPacket: function (aPacket) {
+ if (!aPacket.from) {
+ DevToolsUtils.reportException(
+ "onPacket",
+ new Error("Server did not specify an actor, dropping packet: " +
+ JSON.stringify(aPacket)));
+ return;
+ }
+
+ // If we have a registered Front for this actor, let it handle the packet
+ // and skip all the rest of this unpleasantness.
+ let front = this.getActor(aPacket.from);
+ if (front) {
+ front.onPacket(aPacket);
+ return;
+ }
+
+ // Check for "forwardingCancelled" here instead of using a client to handle it.
+ // This is necessary because we might receive this event while the client is closing,
+ // and the clients have already been removed by that point.
+ if (this.mainRoot &&
+ aPacket.from == this.mainRoot.actor &&
+ aPacket.type == "forwardingCancelled") {
+ this.purgeRequests(aPacket.prefix);
+ return;
+ }
+
+ if (this._clients.has(aPacket.from) && aPacket.type) {
+ let client = this._clients.get(aPacket.from);
+ let type = aPacket.type;
+ if (client.events.indexOf(type) != -1) {
+ client.emit(type, aPacket);
+ // we ignore the rest, as the client is expected to handle this packet.
+ return;
+ }
+ }
+
+ let activeRequest;
+ // See if we have a handler function waiting for a reply from this
+ // actor. (Don't count unsolicited notifications or pauses as
+ // replies.)
+ if (this._activeRequests.has(aPacket.from) &&
+ !(aPacket.type in UnsolicitedNotifications) &&
+ !(aPacket.type == ThreadStateTypes.paused &&
+ aPacket.why.type in UnsolicitedPauses)) {
+ activeRequest = this._activeRequests.get(aPacket.from);
+ this._activeRequests.delete(aPacket.from);
+ }
+
+ // If there is a subsequent request for the same actor, hand it off to the
+ // transport. Delivery of packets on the other end is always async, even
+ // in the local transport case.
+ this._attemptNextRequest(aPacket.from);
+
+ // Packets that indicate thread state changes get special treatment.
+ if (aPacket.type in ThreadStateTypes &&
+ this._clients.has(aPacket.from) &&
+ typeof this._clients.get(aPacket.from)._onThreadState == "function") {
+ this._clients.get(aPacket.from)._onThreadState(aPacket);
+ }
+
+ // TODO: Bug 1151156 - Remove once Gecko 40 is on b2g-stable.
+ if (!this.traits.noNeedToFakeResumptionOnNavigation) {
+ // On navigation the server resumes, so the client must resume as well.
+ // We achieve that by generating a fake resumption packet that triggers
+ // the client's thread state change listeners.
+ if (aPacket.type == UnsolicitedNotifications.tabNavigated &&
+ this._clients.has(aPacket.from) &&
+ this._clients.get(aPacket.from).thread) {
+ let thread = this._clients.get(aPacket.from).thread;
+ let resumption = { from: thread._actor, type: "resumed" };
+ thread._onThreadState(resumption);
+ }
+ }
+
+ // Only try to notify listeners on events, not responses to requests
+ // that lack a packet type.
+ if (aPacket.type) {
+ this.emit(aPacket.type, aPacket);
+ }
+
+ if (activeRequest) {
+ let emitReply = () => activeRequest.emit("json-reply", aPacket);
+ if (activeRequest.stack) {
+ callFunctionWithAsyncStack(emitReply, activeRequest.stack,
+ "DevTools RDP");
+ } else {
+ emitReply();
+ }
+ }
+ },
+
+ /**
+ * Called by the DebuggerTransport to dispatch incoming bulk packets as
+ * appropriate.
+ *
+ * @param packet object
+ * The incoming packet, which contains:
+ * * actor: Name of actor that will receive the packet
+ * * type: Name of actor's method that should be called on receipt
+ * * length: Size of the data to be read
+ * * stream: This input stream should only be used directly if you can
+ * ensure that you will read exactly |length| bytes and will
+ * not close the stream when reading is complete
+ * * done: If you use the stream directly (instead of |copyTo|
+ * below), you must signal completion by resolving /
+ * rejecting this deferred. If it's rejected, the transport
+ * will be closed. If an Error is supplied as a rejection
+ * value, it will be logged via |dumpn|. If you do use
+ * |copyTo|, resolving is taken care of for you when copying
+ * completes.
+ * * copyTo: A helper function for getting your data out of the stream
+ * that meets the stream handling requirements above, and has
+ * the following signature:
+ * @param output nsIAsyncOutputStream
+ * The stream to copy to.
+ * @return Promise
+ * The promise is resolved when copying completes or rejected
+ * if any (unexpected) errors occur.
+ * This object also emits "progress" events for each chunk
+ * that is copied. See stream-utils.js.
+ */
+ onBulkPacket: function (packet) {
+ let { actor, type, length } = packet;
+
+ if (!actor) {
+ DevToolsUtils.reportException(
+ "onBulkPacket",
+ new Error("Server did not specify an actor, dropping bulk packet: " +
+ JSON.stringify(packet)));
+ return;
+ }
+
+ // See if we have a handler function waiting for a reply from this
+ // actor.
+ if (!this._activeRequests.has(actor)) {
+ return;
+ }
+
+ let activeRequest = this._activeRequests.get(actor);
+ this._activeRequests.delete(actor);
+
+ // If there is a subsequent request for the same actor, hand it off to the
+ // transport. Delivery of packets on the other end is always async, even
+ // in the local transport case.
+ this._attemptNextRequest(actor);
+
+ activeRequest.emit("bulk-reply", packet);
+ },
+
+ /**
+ * Called by DebuggerTransport when the underlying stream is closed.
+ *
+ * @param aStatus nsresult
+ * The status code that corresponds to the reason for closing
+ * the stream.
+ */
+ onClosed: function () {
+ this._closed = true;
+ this.emit("closed");
+
+ this.purgeRequests();
+
+ // The |_pools| array on the client-side currently is used only by
+ // protocol.js to store active fronts, mirroring the actor pools found in
+ // the server. So, read all usages of "pool" as "protocol.js front".
+ //
+ // In the normal case where we shutdown cleanly, the toolbox tells each tool
+ // to close, and they each call |destroy| on any fronts they were using.
+ // When |destroy| or |cleanup| is called on a protocol.js front, it also
+ // removes itself from the |_pools| array. Once the toolbox has shutdown,
+ // the connection is closed, and we reach here. All fronts (should have
+ // been) |destroy|ed, so |_pools| should empty.
+ //
+ // If the connection instead aborts unexpectedly, we may end up here with
+ // all fronts used during the life of the connection. So, we call |cleanup|
+ // on them clear their state, reject pending requests, and remove themselves
+ // from |_pools|. This saves the toolbox from hanging indefinitely, in case
+ // it waits for some server response before shutdown that will now never
+ // arrive.
+ for (let pool of this._pools) {
+ pool.cleanup();
+ }
+ },
+
+ /**
+ * Purge pending and active requests in this client.
+ *
+ * @param prefix string (optional)
+ * If a prefix is given, only requests for actor IDs that start with the prefix
+ * will be cleaned up. This is useful when forwarding of a portion of requests
+ * is cancelled on the server.
+ */
+ purgeRequests(prefix = "") {
+ let reject = function (type, request) {
+ // Server can send packets on its own and client only pass a callback
+ // to expectReply, so that there is no request object.
+ let msg;
+ if (request.request) {
+ msg = "'" + request.request.type + "' " + type + " request packet" +
+ " to '" + request.actor + "' " +
+ "can't be sent as the connection just closed.";
+ } else {
+ msg = "server side packet can't be received as the connection just closed.";
+ }
+ let packet = { error: "connectionClosed", message: msg };
+ request.emit("json-reply", packet);
+ };
+
+ let pendingRequestsToReject = [];
+ this._pendingRequests.forEach((requests, actor) => {
+ if (!actor.startsWith(prefix)) {
+ return;
+ }
+ this._pendingRequests.delete(actor);
+ pendingRequestsToReject = pendingRequestsToReject.concat(requests);
+ });
+ pendingRequestsToReject.forEach(request => reject("pending", request));
+
+ let activeRequestsToReject = [];
+ this._activeRequests.forEach((request, actor) => {
+ if (!actor.startsWith(prefix)) {
+ return;
+ }
+ this._activeRequests.delete(actor);
+ activeRequestsToReject = activeRequestsToReject.concat(request);
+ });
+ activeRequestsToReject.forEach(request => reject("active", request));
+ },
+
+ registerClient: function (client) {
+ let actorID = client.actor;
+ if (!actorID) {
+ throw new Error("DebuggerServer.registerClient expects " +
+ "a client instance with an `actor` attribute.");
+ }
+ if (!Array.isArray(client.events)) {
+ throw new Error("DebuggerServer.registerClient expects " +
+ "a client instance with an `events` attribute " +
+ "that is an array.");
+ }
+ if (client.events.length > 0 && typeof (client.emit) != "function") {
+ throw new Error("DebuggerServer.registerClient expects " +
+ "a client instance with non-empty `events` array to" +
+ "have an `emit` function.");
+ }
+ if (this._clients.has(actorID)) {
+ throw new Error("DebuggerServer.registerClient already registered " +
+ "a client for this actor.");
+ }
+ this._clients.set(actorID, client);
+ },
+
+ unregisterClient: function (client) {
+ let actorID = client.actor;
+ if (!actorID) {
+ throw new Error("DebuggerServer.unregisterClient expects " +
+ "a Client instance with a `actor` attribute.");
+ }
+ this._clients.delete(actorID);
+ },
+
+ /**
+ * Actor lifetime management, echos the server's actor pools.
+ */
+ __pools: null,
+ get _pools() {
+ if (this.__pools) {
+ return this.__pools;
+ }
+ this.__pools = new Set();
+ return this.__pools;
+ },
+
+ addActorPool: function (pool) {
+ this._pools.add(pool);
+ },
+ removeActorPool: function (pool) {
+ this._pools.delete(pool);
+ },
+ getActor: function (actorID) {
+ let pool = this.poolFor(actorID);
+ return pool ? pool.get(actorID) : null;
+ },
+
+ poolFor: function (actorID) {
+ for (let pool of this._pools) {
+ if (pool.has(actorID)) return pool;
+ }
+ return null;
+ },
+
+ /**
+ * Currently attached addon.
+ */
+ activeAddon: null
+};
+
+eventSource(DebuggerClient.prototype);
+
+function Request(request) {
+ this.request = request;
+}
+
+Request.prototype = {
+
+ on: function (type, listener) {
+ events.on(this, type, listener);
+ },
+
+ off: function (type, listener) {
+ events.off(this, type, listener);
+ },
+
+ once: function (type, listener) {
+ events.once(this, type, listener);
+ },
+
+ emit: function (type, ...args) {
+ events.emit(this, type, ...args);
+ },
+
+ get actor() { return this.request.to || this.request.actor; }
+
+};
+
+/**
+ * Creates a tab client for the remote debugging protocol server. This client
+ * is a front to the tab actor created in the server side, hiding the protocol
+ * details in a traditional JavaScript API.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aForm object
+ * The protocol form for this tab.
+ */
+function TabClient(aClient, aForm) {
+ this.client = aClient;
+ this._actor = aForm.from;
+ this._threadActor = aForm.threadActor;
+ this.javascriptEnabled = aForm.javascriptEnabled;
+ this.cacheDisabled = aForm.cacheDisabled;
+ this.thread = null;
+ this.request = this.client.request;
+ this.traits = aForm.traits || {};
+ this.events = ["workerListChanged"];
+}
+
+TabClient.prototype = {
+ get actor() { return this._actor; },
+ get _transport() { return this.client._transport; },
+
+ /**
+ * Attach to a thread actor.
+ *
+ * @param object aOptions
+ * Configuration options.
+ * - useSourceMaps: whether to use source maps or not.
+ * @param function aOnResponse
+ * Called with the response packet and a ThreadClient
+ * (which will be undefined on error).
+ */
+ attachThread: function (aOptions = {}, aOnResponse = noop) {
+ if (this.thread) {
+ DevToolsUtils.executeSoon(() => aOnResponse({}, this.thread));
+ return promise.resolve([{}, this.thread]);
+ }
+
+ let packet = {
+ to: this._threadActor,
+ type: "attach",
+ options: aOptions
+ };
+ return this.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ this.thread = new ThreadClient(this, this._threadActor);
+ this.client.registerClient(this.thread);
+ }
+ aOnResponse(aResponse, this.thread);
+ return [aResponse, this.thread];
+ });
+ },
+
+ /**
+ * Detach the client from the tab actor.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ detach: DebuggerClient.requester({
+ type: "detach"
+ }, {
+ before: function (aPacket) {
+ if (this.thread) {
+ this.thread.detach();
+ }
+ return aPacket;
+ },
+ after: function (aResponse) {
+ this.client.unregisterClient(this);
+ return aResponse;
+ },
+ }),
+
+ /**
+ * Bring the window to the front.
+ */
+ focus: DebuggerClient.requester({
+ type: "focus"
+ }, {}),
+
+ /**
+ * Reload the page in this tab.
+ *
+ * @param [optional] object options
+ * An object with a `force` property indicating whether or not
+ * this reload should skip the cache
+ */
+ reload: function (options = { force: false }) {
+ return this._reload(options);
+ },
+ _reload: DebuggerClient.requester({
+ type: "reload",
+ options: args(0)
+ }),
+
+ /**
+ * Navigate to another URL.
+ *
+ * @param string url
+ * The URL to navigate to.
+ */
+ navigateTo: DebuggerClient.requester({
+ type: "navigateTo",
+ url: args(0)
+ }),
+
+ /**
+ * Reconfigure the tab actor.
+ *
+ * @param object aOptions
+ * A dictionary object of the new options to use in the tab actor.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ reconfigure: DebuggerClient.requester({
+ type: "reconfigure",
+ options: args(0)
+ }),
+
+ listWorkers: DebuggerClient.requester({
+ type: "listWorkers"
+ }),
+
+ attachWorker: function (aWorkerActor, aOnResponse) {
+ this.client.attachWorker(aWorkerActor, aOnResponse);
+ },
+
+ /**
+ * Resolve a location ({ url, line, column }) to its current
+ * source mapping location.
+ *
+ * @param {String} arg[0].url
+ * @param {Number} arg[0].line
+ * @param {Number?} arg[0].column
+ */
+ resolveLocation: DebuggerClient.requester({
+ type: "resolveLocation",
+ location: args(0)
+ }),
+};
+
+eventSource(TabClient.prototype);
+
+function WorkerClient(aClient, aForm) {
+ this.client = aClient;
+ this._actor = aForm.from;
+ this._isClosed = false;
+ this._url = aForm.url;
+
+ this._onClose = this._onClose.bind(this);
+
+ this.addListener("close", this._onClose);
+
+ this.traits = {};
+}
+
+WorkerClient.prototype = {
+ get _transport() {
+ return this.client._transport;
+ },
+
+ get request() {
+ return this.client.request;
+ },
+
+ get actor() {
+ return this._actor;
+ },
+
+ get url() {
+ return this._url;
+ },
+
+ get isClosed() {
+ return this._isClosed;
+ },
+
+ detach: DebuggerClient.requester({ type: "detach" }, {
+ after: function (aResponse) {
+ if (this.thread) {
+ this.client.unregisterClient(this.thread);
+ }
+ this.client.unregisterClient(this);
+ return aResponse;
+ },
+ }),
+
+ attachThread: function (aOptions = {}, aOnResponse = noop) {
+ if (this.thread) {
+ let response = [{
+ type: "connected",
+ threadActor: this.thread._actor,
+ consoleActor: this.consoleActor,
+ }, this.thread];
+ DevToolsUtils.executeSoon(() => aOnResponse(response));
+ return response;
+ }
+
+ // The connect call on server doesn't attach the thread as of version 44.
+ return this.request({
+ to: this._actor,
+ type: "connect",
+ options: aOptions,
+ }).then(connectReponse => {
+ if (connectReponse.error) {
+ aOnResponse(connectReponse, null);
+ return [connectResponse, null];
+ }
+
+ return this.request({
+ to: connectReponse.threadActor,
+ type: "attach",
+ options: aOptions
+ }).then(attachResponse => {
+ if (attachResponse.error) {
+ aOnResponse(attachResponse, null);
+ }
+
+ this.thread = new ThreadClient(this, connectReponse.threadActor);
+ this.consoleActor = connectReponse.consoleActor;
+ this.client.registerClient(this.thread);
+
+ aOnResponse(connectReponse, this.thread);
+ return [connectResponse, this.thread];
+ });
+ }, error => {
+ aOnResponse(error, null);
+ });
+ },
+
+ _onClose: function () {
+ this.removeListener("close", this._onClose);
+
+ if (this.thread) {
+ this.client.unregisterClient(this.thread);
+ }
+ this.client.unregisterClient(this);
+ this._isClosed = true;
+ },
+
+ reconfigure: function () {
+ return Promise.resolve();
+ },
+
+ events: ["close"]
+};
+
+eventSource(WorkerClient.prototype);
+
+function AddonClient(aClient, aActor) {
+ this._client = aClient;
+ this._actor = aActor;
+ this.request = this._client.request;
+ this.events = [];
+}
+
+AddonClient.prototype = {
+ get actor() { return this._actor; },
+ get _transport() { return this._client._transport; },
+
+ /**
+ * Detach the client from the addon actor.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ detach: DebuggerClient.requester({
+ type: "detach"
+ }, {
+ after: function (aResponse) {
+ if (this._client.activeAddon === this) {
+ this._client.activeAddon = null;
+ }
+ this._client.unregisterClient(this);
+ return aResponse;
+ },
+ })
+};
+
+/**
+ * A RootClient object represents a root actor on the server. Each
+ * DebuggerClient keeps a RootClient instance representing the root actor
+ * for the initial connection; DebuggerClient's 'listTabs' and
+ * 'listChildProcesses' methods forward to that root actor.
+ *
+ * @param aClient object
+ * The client connection to which this actor belongs.
+ * @param aGreeting string
+ * The greeting packet from the root actor we're to represent.
+ *
+ * Properties of a RootClient instance:
+ *
+ * @property actor string
+ * The name of this child's root actor.
+ * @property applicationType string
+ * The application type, as given in the root actor's greeting packet.
+ * @property traits object
+ * The traits object, as given in the root actor's greeting packet.
+ */
+function RootClient(aClient, aGreeting) {
+ this._client = aClient;
+ this.actor = aGreeting.from;
+ this.applicationType = aGreeting.applicationType;
+ this.traits = aGreeting.traits;
+}
+exports.RootClient = RootClient;
+
+RootClient.prototype = {
+ constructor: RootClient,
+
+ /**
+ * List the open tabs.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listTabs: DebuggerClient.requester({ type: "listTabs" }),
+
+ /**
+ * List the installed addons.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listAddons: DebuggerClient.requester({ type: "listAddons" }),
+
+ /**
+ * List the registered workers.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listWorkers: DebuggerClient.requester({ type: "listWorkers" }),
+
+ /**
+ * List the registered service workers.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listServiceWorkerRegistrations: DebuggerClient.requester({
+ type: "listServiceWorkerRegistrations"
+ }),
+
+ /**
+ * List the running processes.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ listProcesses: DebuggerClient.requester({ type: "listProcesses" }),
+
+ /**
+ * Fetch the TabActor for the currently selected tab, or for a specific
+ * tab given as first parameter.
+ *
+ * @param [optional] object aFilter
+ * A dictionary object with following optional attributes:
+ * - outerWindowID: used to match tabs in parent process
+ * - tabId: used to match tabs in child processes
+ * - tab: a reference to xul:tab element
+ * If nothing is specified, returns the actor for the currently
+ * selected tab.
+ */
+ getTab: function (aFilter) {
+ let packet = {
+ to: this.actor,
+ type: "getTab"
+ };
+
+ if (aFilter) {
+ if (typeof (aFilter.outerWindowID) == "number") {
+ packet.outerWindowID = aFilter.outerWindowID;
+ } else if (typeof (aFilter.tabId) == "number") {
+ packet.tabId = aFilter.tabId;
+ } else if ("tab" in aFilter) {
+ let browser = aFilter.tab.linkedBrowser;
+ if (browser.frameLoader.tabParent) {
+ // Tabs in child process
+ packet.tabId = browser.frameLoader.tabParent.tabId;
+ } else if (browser.outerWindowID) {
+ // <xul:browser> tabs in parent process
+ packet.outerWindowID = browser.outerWindowID;
+ } else {
+ // <iframe mozbrowser> tabs in parent process
+ let windowUtils = browser.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ packet.outerWindowID = windowUtils.outerWindowID;
+ }
+ } else {
+ // Throw if a filter object have been passed but without
+ // any clearly idenfified filter.
+ throw new Error("Unsupported argument given to getTab request");
+ }
+ }
+
+ return this.request(packet);
+ },
+
+ /**
+ * Description of protocol's actors and methods.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ protocolDescription: DebuggerClient.requester({ type: "protocolDescription" }),
+
+ /*
+ * Methods constructed by DebuggerClient.requester require these forwards
+ * on their 'this'.
+ */
+ get _transport() { return this._client._transport; },
+ get request() { return this._client.request; }
+};
+
+/**
+ * Creates a thread client for the remote debugging protocol server. This client
+ * is a front to the thread actor created in the server side, hiding the
+ * protocol details in a traditional JavaScript API.
+ *
+ * @param aClient DebuggerClient|TabClient
+ * The parent of the thread (tab for tab-scoped debuggers, DebuggerClient
+ * for chrome debuggers).
+ * @param aActor string
+ * The actor ID for this thread.
+ */
+function ThreadClient(aClient, aActor) {
+ this._parent = aClient;
+ this.client = aClient instanceof DebuggerClient ? aClient : aClient.client;
+ this._actor = aActor;
+ this._frameCache = [];
+ this._scriptCache = {};
+ this._pauseGrips = {};
+ this._threadGrips = {};
+ this.request = this.client.request;
+}
+
+ThreadClient.prototype = {
+ _state: "paused",
+ get state() { return this._state; },
+ get paused() { return this._state === "paused"; },
+
+ _pauseOnExceptions: false,
+ _ignoreCaughtExceptions: false,
+ _pauseOnDOMEvents: null,
+
+ _actor: null,
+ get actor() { return this._actor; },
+
+ get _transport() { return this.client._transport; },
+
+ _assertPaused: function (aCommand) {
+ if (!this.paused) {
+ throw Error(aCommand + " command sent while not paused. Currently " + this._state);
+ }
+ },
+
+ /**
+ * Resume a paused thread. If the optional aLimit parameter is present, then
+ * the thread will also pause when that limit is reached.
+ *
+ * @param [optional] object aLimit
+ * An object with a type property set to the appropriate limit (next,
+ * step, or finish) per the remote debugging protocol specification.
+ * Use null to specify no limit.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ _doResume: DebuggerClient.requester({
+ type: "resume",
+ resumeLimit: args(0)
+ }, {
+ before: function (aPacket) {
+ this._assertPaused("resume");
+
+ // Put the client in a tentative "resuming" state so we can prevent
+ // further requests that should only be sent in the paused state.
+ this._previousState = this._state;
+ this._state = "resuming";
+
+ if (this._pauseOnExceptions) {
+ aPacket.pauseOnExceptions = this._pauseOnExceptions;
+ }
+ if (this._ignoreCaughtExceptions) {
+ aPacket.ignoreCaughtExceptions = this._ignoreCaughtExceptions;
+ }
+ if (this._pauseOnDOMEvents) {
+ aPacket.pauseOnDOMEvents = this._pauseOnDOMEvents;
+ }
+ return aPacket;
+ },
+ after: function (aResponse) {
+ if (aResponse.error && this._state == "resuming") {
+ // There was an error resuming, update the state to the new one
+ // reported by the server, if given (only on wrongState), otherwise
+ // reset back to the previous state.
+ if (aResponse.state) {
+ this._state = ThreadStateTypes[aResponse.state];
+ } else {
+ this._state = this._previousState;
+ }
+ }
+ delete this._previousState;
+ return aResponse;
+ },
+ }),
+
+ /**
+ * Reconfigure the thread actor.
+ *
+ * @param object aOptions
+ * A dictionary object of the new options to use in the thread actor.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ reconfigure: DebuggerClient.requester({
+ type: "reconfigure",
+ options: args(0)
+ }),
+
+ /**
+ * Resume a paused thread.
+ */
+ resume: function (aOnResponse) {
+ return this._doResume(null, aOnResponse);
+ },
+
+ /**
+ * Resume then pause without stepping.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ resumeThenPause: function (aOnResponse) {
+ return this._doResume({ type: "break" }, aOnResponse);
+ },
+
+ /**
+ * Step over a function call.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ stepOver: function (aOnResponse) {
+ return this._doResume({ type: "next" }, aOnResponse);
+ },
+
+ /**
+ * Step into a function call.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ stepIn: function (aOnResponse) {
+ return this._doResume({ type: "step" }, aOnResponse);
+ },
+
+ /**
+ * Step out of a function call.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ stepOut: function (aOnResponse) {
+ return this._doResume({ type: "finish" }, aOnResponse);
+ },
+
+ /**
+ * Immediately interrupt a running thread.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ interrupt: function (aOnResponse) {
+ return this._doInterrupt(null, aOnResponse);
+ },
+
+ /**
+ * Pause execution right before the next JavaScript bytecode is executed.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ breakOnNext: function (aOnResponse) {
+ return this._doInterrupt("onNext", aOnResponse);
+ },
+
+ /**
+ * Interrupt a running thread.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ _doInterrupt: DebuggerClient.requester({
+ type: "interrupt",
+ when: args(0)
+ }),
+
+ /**
+ * Enable or disable pausing when an exception is thrown.
+ *
+ * @param boolean aFlag
+ * Enables pausing if true, disables otherwise.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ pauseOnExceptions: function (aPauseOnExceptions,
+ aIgnoreCaughtExceptions,
+ aOnResponse = noop) {
+ this._pauseOnExceptions = aPauseOnExceptions;
+ this._ignoreCaughtExceptions = aIgnoreCaughtExceptions;
+
+ // Otherwise send the flag using a standard resume request.
+ if (!this.paused) {
+ return this.interrupt(aResponse => {
+ if (aResponse.error) {
+ // Can't continue if pausing failed.
+ aOnResponse(aResponse);
+ return aResponse;
+ }
+ return this.resume(aOnResponse);
+ });
+ }
+
+ aOnResponse();
+ return promise.resolve();
+ },
+
+ /**
+ * Enable pausing when the specified DOM events are triggered. Disabling
+ * pausing on an event can be realized by calling this method with the updated
+ * array of events that doesn't contain it.
+ *
+ * @param array|string events
+ * An array of strings, representing the DOM event types to pause on,
+ * or "*" to pause on all DOM events. Pass an empty array to
+ * completely disable pausing on DOM events.
+ * @param function onResponse
+ * Called with the response packet in a future turn of the event loop.
+ */
+ pauseOnDOMEvents: function (events, onResponse = noop) {
+ this._pauseOnDOMEvents = events;
+ // If the debuggee is paused, the value of the array will be communicated in
+ // the next resumption. Otherwise we have to force a pause in order to send
+ // the array.
+ if (this.paused) {
+ DevToolsUtils.executeSoon(() => onResponse({}));
+ return {};
+ }
+ return this.interrupt(response => {
+ // Can't continue if pausing failed.
+ if (response.error) {
+ onResponse(response);
+ return response;
+ }
+ return this.resume(onResponse);
+ });
+ },
+
+ /**
+ * Send a clientEvaluate packet to the debuggee. Response
+ * will be a resume packet.
+ *
+ * @param string aFrame
+ * The actor ID of the frame where the evaluation should take place.
+ * @param string aExpression
+ * The expression that will be evaluated in the scope of the frame
+ * above.
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ eval: DebuggerClient.requester({
+ type: "clientEvaluate",
+ frame: args(0),
+ expression: args(1)
+ }, {
+ before: function (aPacket) {
+ this._assertPaused("eval");
+ // Put the client in a tentative "resuming" state so we can prevent
+ // further requests that should only be sent in the paused state.
+ this._state = "resuming";
+ return aPacket;
+ },
+ after: function (aResponse) {
+ if (aResponse.error) {
+ // There was an error resuming, back to paused state.
+ this._state = "paused";
+ }
+ return aResponse;
+ },
+ }),
+
+ /**
+ * Detach from the thread actor.
+ *
+ * @param function aOnResponse
+ * Called with the response packet.
+ */
+ detach: DebuggerClient.requester({
+ type: "detach"
+ }, {
+ after: function (aResponse) {
+ this.client.unregisterClient(this);
+ this._parent.thread = null;
+ return aResponse;
+ },
+ }),
+
+ /**
+ * Release multiple thread-lifetime object actors. If any pause-lifetime
+ * actors are included in the request, a |notReleasable| error will return,
+ * but all the thread-lifetime ones will have been released.
+ *
+ * @param array actors
+ * An array with actor IDs to release.
+ */
+ releaseMany: DebuggerClient.requester({
+ type: "releaseMany",
+ actors: args(0),
+ }),
+
+ /**
+ * Promote multiple pause-lifetime object actors to thread-lifetime ones.
+ *
+ * @param array actors
+ * An array with actor IDs to promote.
+ */
+ threadGrips: DebuggerClient.requester({
+ type: "threadGrips",
+ actors: args(0)
+ }),
+
+ /**
+ * Return the event listeners defined on the page.
+ *
+ * @param aOnResponse Function
+ * Called with the thread's response.
+ */
+ eventListeners: DebuggerClient.requester({
+ type: "eventListeners"
+ }),
+
+ /**
+ * Request the loaded sources for the current thread.
+ *
+ * @param aOnResponse Function
+ * Called with the thread's response.
+ */
+ getSources: DebuggerClient.requester({
+ type: "sources"
+ }),
+
+ /**
+ * Clear the thread's source script cache. A scriptscleared event
+ * will be sent.
+ */
+ _clearScripts: function () {
+ if (Object.keys(this._scriptCache).length > 0) {
+ this._scriptCache = {};
+ this.emit("scriptscleared");
+ }
+ },
+
+ /**
+ * Request frames from the callstack for the current thread.
+ *
+ * @param aStart integer
+ * The number of the youngest stack frame to return (the youngest
+ * frame is 0).
+ * @param aCount integer
+ * The maximum number of frames to return, or null to return all
+ * frames.
+ * @param aOnResponse function
+ * Called with the thread's response.
+ */
+ getFrames: DebuggerClient.requester({
+ type: "frames",
+ start: args(0),
+ count: args(1)
+ }),
+
+ /**
+ * An array of cached frames. Clients can observe the framesadded and
+ * framescleared event to keep up to date on changes to this cache,
+ * and can fill it using the fillFrames method.
+ */
+ get cachedFrames() { return this._frameCache; },
+
+ /**
+ * true if there are more stack frames available on the server.
+ */
+ get moreFrames() {
+ return this.paused && (!this._frameCache || this._frameCache.length == 0
+ || !this._frameCache[this._frameCache.length - 1].oldest);
+ },
+
+ /**
+ * Ensure that at least aTotal stack frames have been loaded in the
+ * ThreadClient's stack frame cache. A framesadded event will be
+ * sent when the stack frame cache is updated.
+ *
+ * @param aTotal number
+ * The minimum number of stack frames to be included.
+ * @param aCallback function
+ * Optional callback function called when frames have been loaded
+ * @returns true if a framesadded notification should be expected.
+ */
+ fillFrames: function (aTotal, aCallback = noop) {
+ this._assertPaused("fillFrames");
+ if (this._frameCache.length >= aTotal) {
+ return false;
+ }
+
+ let numFrames = this._frameCache.length;
+
+ this.getFrames(numFrames, aTotal - numFrames, (aResponse) => {
+ if (aResponse.error) {
+ aCallback(aResponse);
+ return;
+ }
+
+ let threadGrips = DevToolsUtils.values(this._threadGrips);
+
+ for (let i in aResponse.frames) {
+ let frame = aResponse.frames[i];
+ if (!frame.where.source) {
+ // Older servers use urls instead, so we need to resolve
+ // them to source actors
+ for (let grip of threadGrips) {
+ if (grip instanceof SourceClient && grip.url === frame.url) {
+ frame.where.source = grip._form;
+ }
+ }
+ }
+
+ this._frameCache[frame.depth] = frame;
+ }
+
+ // If we got as many frames as we asked for, there might be more
+ // frames available.
+ this.emit("framesadded");
+
+ aCallback(aResponse);
+ });
+
+ return true;
+ },
+
+ /**
+ * Clear the thread's stack frame cache. A framescleared event
+ * will be sent.
+ */
+ _clearFrames: function () {
+ if (this._frameCache.length > 0) {
+ this._frameCache = [];
+ this.emit("framescleared");
+ }
+ },
+
+ /**
+ * Return a ObjectClient object for the given object grip.
+ *
+ * @param aGrip object
+ * A pause-lifetime object grip returned by the protocol.
+ */
+ pauseGrip: function (aGrip) {
+ if (aGrip.actor in this._pauseGrips) {
+ return this._pauseGrips[aGrip.actor];
+ }
+
+ let client = new ObjectClient(this.client, aGrip);
+ this._pauseGrips[aGrip.actor] = client;
+ return client;
+ },
+
+ /**
+ * Get or create a long string client, checking the grip client cache if it
+ * already exists.
+ *
+ * @param aGrip Object
+ * The long string grip returned by the protocol.
+ * @param aGripCacheName String
+ * The property name of the grip client cache to check for existing
+ * clients in.
+ */
+ _longString: function (aGrip, aGripCacheName) {
+ if (aGrip.actor in this[aGripCacheName]) {
+ return this[aGripCacheName][aGrip.actor];
+ }
+
+ let client = new LongStringClient(this.client, aGrip);
+ this[aGripCacheName][aGrip.actor] = client;
+ return client;
+ },
+
+ /**
+ * Return an instance of LongStringClient for the given long string grip that
+ * is scoped to the current pause.
+ *
+ * @param aGrip Object
+ * The long string grip returned by the protocol.
+ */
+ pauseLongString: function (aGrip) {
+ return this._longString(aGrip, "_pauseGrips");
+ },
+
+ /**
+ * Return an instance of LongStringClient for the given long string grip that
+ * is scoped to the thread lifetime.
+ *
+ * @param aGrip Object
+ * The long string grip returned by the protocol.
+ */
+ threadLongString: function (aGrip) {
+ return this._longString(aGrip, "_threadGrips");
+ },
+
+ /**
+ * Clear and invalidate all the grip clients from the given cache.
+ *
+ * @param aGripCacheName
+ * The property name of the grip cache we want to clear.
+ */
+ _clearObjectClients: function (aGripCacheName) {
+ for (let id in this[aGripCacheName]) {
+ this[aGripCacheName][id].valid = false;
+ }
+ this[aGripCacheName] = {};
+ },
+
+ /**
+ * Invalidate pause-lifetime grip clients and clear the list of current grip
+ * clients.
+ */
+ _clearPauseGrips: function () {
+ this._clearObjectClients("_pauseGrips");
+ },
+
+ /**
+ * Invalidate thread-lifetime grip clients and clear the list of current grip
+ * clients.
+ */
+ _clearThreadGrips: function () {
+ this._clearObjectClients("_threadGrips");
+ },
+
+ /**
+ * Handle thread state change by doing necessary cleanup and notifying all
+ * registered listeners.
+ */
+ _onThreadState: function (aPacket) {
+ this._state = ThreadStateTypes[aPacket.type];
+ // The debugger UI may not be initialized yet so we want to keep
+ // the packet around so it knows what to pause state to display
+ // when it's initialized
+ this._lastPausePacket = aPacket.type === "resumed" ? null : aPacket;
+ this._clearFrames();
+ this._clearPauseGrips();
+ aPacket.type === ThreadStateTypes.detached && this._clearThreadGrips();
+ this.client._eventsEnabled && this.emit(aPacket.type, aPacket);
+ },
+
+ getLastPausePacket: function () {
+ return this._lastPausePacket;
+ },
+
+ /**
+ * Return an EnvironmentClient instance for the given environment actor form.
+ */
+ environment: function (aForm) {
+ return new EnvironmentClient(this.client, aForm);
+ },
+
+ /**
+ * Return an instance of SourceClient for the given source actor form.
+ */
+ source: function (aForm) {
+ if (aForm.actor in this._threadGrips) {
+ return this._threadGrips[aForm.actor];
+ }
+
+ return this._threadGrips[aForm.actor] = new SourceClient(this, aForm);
+ },
+
+ /**
+ * Request the prototype and own properties of mutlipleObjects.
+ *
+ * @param aOnResponse function
+ * Called with the request's response.
+ * @param actors [string]
+ * List of actor ID of the queried objects.
+ */
+ getPrototypesAndProperties: DebuggerClient.requester({
+ type: "prototypesAndProperties",
+ actors: args(0)
+ }),
+
+ events: ["newSource"]
+};
+
+eventSource(ThreadClient.prototype);
+
+/**
+ * Creates a tracing profiler client for the remote debugging protocol
+ * server. This client is a front to the trace actor created on the
+ * server side, hiding the protocol details in a traditional
+ * JavaScript API.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aActor string
+ * The actor ID for this thread.
+ */
+function TraceClient(aClient, aActor) {
+ this._client = aClient;
+ this._actor = aActor;
+ this._activeTraces = new Set();
+ this._waitingPackets = new Map();
+ this._expectedPacket = 0;
+ this.request = this._client.request;
+ this.events = [];
+}
+
+TraceClient.prototype = {
+ get actor() { return this._actor; },
+ get tracing() { return this._activeTraces.size > 0; },
+
+ get _transport() { return this._client._transport; },
+
+ /**
+ * Detach from the trace actor.
+ */
+ detach: DebuggerClient.requester({
+ type: "detach"
+ }, {
+ after: function (aResponse) {
+ this._client.unregisterClient(this);
+ return aResponse;
+ },
+ }),
+
+ /**
+ * Start a new trace.
+ *
+ * @param aTrace [string]
+ * An array of trace types to be recorded by the new trace.
+ *
+ * @param aName string
+ * The name of the new trace.
+ *
+ * @param aOnResponse function
+ * Called with the request's response.
+ */
+ startTrace: DebuggerClient.requester({
+ type: "startTrace",
+ name: args(1),
+ trace: args(0)
+ }, {
+ after: function (aResponse) {
+ if (aResponse.error) {
+ return aResponse;
+ }
+
+ if (!this.tracing) {
+ this._waitingPackets.clear();
+ this._expectedPacket = 0;
+ }
+ this._activeTraces.add(aResponse.name);
+
+ return aResponse;
+ },
+ }),
+
+ /**
+ * End a trace. If a name is provided, stop the named
+ * trace. Otherwise, stop the most recently started trace.
+ *
+ * @param aName string
+ * The name of the trace to stop.
+ *
+ * @param aOnResponse function
+ * Called with the request's response.
+ */
+ stopTrace: DebuggerClient.requester({
+ type: "stopTrace",
+ name: args(0)
+ }, {
+ after: function (aResponse) {
+ if (aResponse.error) {
+ return aResponse;
+ }
+
+ this._activeTraces.delete(aResponse.name);
+
+ return aResponse;
+ },
+ })
+};
+
+/**
+ * Grip clients are used to retrieve information about the relevant object.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aGrip object
+ * A pause-lifetime object grip returned by the protocol.
+ */
+function ObjectClient(aClient, aGrip)
+{
+ this._grip = aGrip;
+ this._client = aClient;
+ this.request = this._client.request;
+}
+exports.ObjectClient = ObjectClient;
+
+ObjectClient.prototype = {
+ get actor() { return this._grip.actor; },
+ get _transport() { return this._client._transport; },
+
+ valid: true,
+
+ get isFrozen() {
+ return this._grip.frozen;
+ },
+ get isSealed() {
+ return this._grip.sealed;
+ },
+ get isExtensible() {
+ return this._grip.extensible;
+ },
+
+ getDefinitionSite: DebuggerClient.requester({
+ type: "definitionSite"
+ }, {
+ before: function (aPacket) {
+ if (this._grip.class != "Function") {
+ throw new Error("getDefinitionSite is only valid for function grips.");
+ }
+ return aPacket;
+ }
+ }),
+
+ /**
+ * Request the names of a function's formal parameters.
+ *
+ * @param aOnResponse function
+ * Called with an object of the form:
+ * { parameterNames:[<parameterName>, ...] }
+ * where each <parameterName> is the name of a parameter.
+ */
+ getParameterNames: DebuggerClient.requester({
+ type: "parameterNames"
+ }, {
+ before: function (aPacket) {
+ if (this._grip["class"] !== "Function") {
+ throw new Error("getParameterNames is only valid for function grips.");
+ }
+ return aPacket;
+ },
+ }),
+
+ /**
+ * Request the names of the properties defined on the object and not its
+ * prototype.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getOwnPropertyNames: DebuggerClient.requester({
+ type: "ownPropertyNames"
+ }),
+
+ /**
+ * Request the prototype and own properties of the object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getPrototypeAndProperties: DebuggerClient.requester({
+ type: "prototypeAndProperties"
+ }),
+
+ /**
+ * Request a PropertyIteratorClient instance to ease listing
+ * properties for this object.
+ *
+ * @param options Object
+ * A dictionary object with various boolean attributes:
+ * - ignoreIndexedProperties Boolean
+ * If true, filters out Array items.
+ * e.g. properties names between `0` and `object.length`.
+ * - ignoreNonIndexedProperties Boolean
+ * If true, filters out items that aren't array items
+ * e.g. properties names that are not a number between `0`
+ * and `object.length`.
+ * - sort Boolean
+ * If true, the iterator will sort the properties by name
+ * before dispatching them.
+ * @param aOnResponse function Called with the client instance.
+ */
+ enumProperties: DebuggerClient.requester({
+ type: "enumProperties",
+ options: args(0)
+ }, {
+ after: function (aResponse) {
+ if (aResponse.iterator) {
+ return { iterator: new PropertyIteratorClient(this._client, aResponse.iterator) };
+ }
+ return aResponse;
+ },
+ }),
+
+ /**
+ * Request a PropertyIteratorClient instance to enumerate entries in a
+ * Map/Set-like object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ enumEntries: DebuggerClient.requester({
+ type: "enumEntries"
+ }, {
+ before: function (packet) {
+ if (!["Map", "WeakMap", "Set", "WeakSet"].includes(this._grip.class)) {
+ throw new Error("enumEntries is only valid for Map/Set-like grips.");
+ }
+ return packet;
+ },
+ after: function (response) {
+ if (response.iterator) {
+ return {
+ iterator: new PropertyIteratorClient(this._client, response.iterator)
+ };
+ }
+ return response;
+ }
+ }),
+
+ /**
+ * Request the property descriptor of the object's specified property.
+ *
+ * @param aName string The name of the requested property.
+ * @param aOnResponse function Called with the request's response.
+ */
+ getProperty: DebuggerClient.requester({
+ type: "property",
+ name: args(0)
+ }),
+
+ /**
+ * Request the prototype of the object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getPrototype: DebuggerClient.requester({
+ type: "prototype"
+ }),
+
+ /**
+ * Request the display string of the object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getDisplayString: DebuggerClient.requester({
+ type: "displayString"
+ }),
+
+ /**
+ * Request the scope of the object.
+ *
+ * @param aOnResponse function Called with the request's response.
+ */
+ getScope: DebuggerClient.requester({
+ type: "scope"
+ }, {
+ before: function (aPacket) {
+ if (this._grip.class !== "Function") {
+ throw new Error("scope is only valid for function grips.");
+ }
+ return aPacket;
+ },
+ }),
+
+ /**
+ * Request the promises directly depending on the current promise.
+ */
+ getDependentPromises: DebuggerClient.requester({
+ type: "dependentPromises"
+ }, {
+ before: function (aPacket) {
+ if (this._grip.class !== "Promise") {
+ throw new Error("getDependentPromises is only valid for promise " +
+ "grips.");
+ }
+ return aPacket;
+ }
+ }),
+
+ /**
+ * Request the stack to the promise's allocation point.
+ */
+ getPromiseAllocationStack: DebuggerClient.requester({
+ type: "allocationStack"
+ }, {
+ before: function (aPacket) {
+ if (this._grip.class !== "Promise") {
+ throw new Error("getAllocationStack is only valid for promise grips.");
+ }
+ return aPacket;
+ }
+ }),
+
+ /**
+ * Request the stack to the promise's fulfillment point.
+ */
+ getPromiseFulfillmentStack: DebuggerClient.requester({
+ type: "fulfillmentStack"
+ }, {
+ before: function (packet) {
+ if (this._grip.class !== "Promise") {
+ throw new Error("getPromiseFulfillmentStack is only valid for " +
+ "promise grips.");
+ }
+ return packet;
+ }
+ }),
+
+ /**
+ * Request the stack to the promise's rejection point.
+ */
+ getPromiseRejectionStack: DebuggerClient.requester({
+ type: "rejectionStack"
+ }, {
+ before: function (packet) {
+ if (this._grip.class !== "Promise") {
+ throw new Error("getPromiseRejectionStack is only valid for " +
+ "promise grips.");
+ }
+ return packet;
+ }
+ })
+};
+
+/**
+ * A PropertyIteratorClient provides a way to access to property names and
+ * values of an object efficiently, slice by slice.
+ * Note that the properties can be sorted in the backend,
+ * this is controled while creating the PropertyIteratorClient
+ * from ObjectClient.enumProperties.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aGrip Object
+ * A PropertyIteratorActor grip returned by the protocol via
+ * TabActor.enumProperties request.
+ */
+function PropertyIteratorClient(aClient, aGrip) {
+ this._grip = aGrip;
+ this._client = aClient;
+ this.request = this._client.request;
+}
+
+PropertyIteratorClient.prototype = {
+ get actor() { return this._grip.actor; },
+
+ /**
+ * Get the total number of properties available in the iterator.
+ */
+ get count() { return this._grip.count; },
+
+ /**
+ * Get one or more property names that correspond to the positions in the
+ * indexes parameter.
+ *
+ * @param indexes Array
+ * An array of property indexes.
+ * @param aCallback Function
+ * The function called when we receive the property names.
+ */
+ names: DebuggerClient.requester({
+ type: "names",
+ indexes: args(0)
+ }, {}),
+
+ /**
+ * Get a set of following property value(s).
+ *
+ * @param start Number
+ * The index of the first property to fetch.
+ * @param count Number
+ * The number of properties to fetch.
+ * @param aCallback Function
+ * The function called when we receive the property values.
+ */
+ slice: DebuggerClient.requester({
+ type: "slice",
+ start: args(0),
+ count: args(1)
+ }, {}),
+
+ /**
+ * Get all the property values.
+ *
+ * @param aCallback Function
+ * The function called when we receive the property values.
+ */
+ all: DebuggerClient.requester({
+ type: "all"
+ }, {}),
+};
+
+/**
+ * A LongStringClient provides a way to access "very long" strings from the
+ * debugger server.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aGrip Object
+ * A pause-lifetime long string grip returned by the protocol.
+ */
+function LongStringClient(aClient, aGrip) {
+ this._grip = aGrip;
+ this._client = aClient;
+ this.request = this._client.request;
+}
+exports.LongStringClient = LongStringClient;
+
+LongStringClient.prototype = {
+ get actor() { return this._grip.actor; },
+ get length() { return this._grip.length; },
+ get initial() { return this._grip.initial; },
+ get _transport() { return this._client._transport; },
+
+ valid: true,
+
+ /**
+ * Get the substring of this LongString from aStart to aEnd.
+ *
+ * @param aStart Number
+ * The starting index.
+ * @param aEnd Number
+ * The ending index.
+ * @param aCallback Function
+ * The function called when we receive the substring.
+ */
+ substring: DebuggerClient.requester({
+ type: "substring",
+ start: args(0),
+ end: args(1)
+ }),
+};
+
+/**
+ * A SourceClient provides a way to access the source text of a script.
+ *
+ * @param aClient ThreadClient
+ * The thread client parent.
+ * @param aForm Object
+ * The form sent across the remote debugging protocol.
+ */
+function SourceClient(aClient, aForm) {
+ this._form = aForm;
+ this._isBlackBoxed = aForm.isBlackBoxed;
+ this._isPrettyPrinted = aForm.isPrettyPrinted;
+ this._activeThread = aClient;
+ this._client = aClient.client;
+}
+
+SourceClient.prototype = {
+ get _transport() {
+ return this._client._transport;
+ },
+ get isBlackBoxed() {
+ return this._isBlackBoxed;
+ },
+ get isPrettyPrinted() {
+ return this._isPrettyPrinted;
+ },
+ get actor() {
+ return this._form.actor;
+ },
+ get request() {
+ return this._client.request;
+ },
+ get url() {
+ return this._form.url;
+ },
+
+ /**
+ * Black box this SourceClient's source.
+ *
+ * @param aCallback Function
+ * The callback function called when we receive the response from the server.
+ */
+ blackBox: DebuggerClient.requester({
+ type: "blackbox"
+ }, {
+ after: function (aResponse) {
+ if (!aResponse.error) {
+ this._isBlackBoxed = true;
+ if (this._activeThread) {
+ this._activeThread.emit("blackboxchange", this);
+ }
+ }
+ return aResponse;
+ }
+ }),
+
+ /**
+ * Un-black box this SourceClient's source.
+ *
+ * @param aCallback Function
+ * The callback function called when we receive the response from the server.
+ */
+ unblackBox: DebuggerClient.requester({
+ type: "unblackbox"
+ }, {
+ after: function (aResponse) {
+ if (!aResponse.error) {
+ this._isBlackBoxed = false;
+ if (this._activeThread) {
+ this._activeThread.emit("blackboxchange", this);
+ }
+ }
+ return aResponse;
+ }
+ }),
+
+ /**
+ * Get Executable Lines from a source
+ *
+ * @param aCallback Function
+ * The callback function called when we receive the response from the server.
+ */
+ getExecutableLines: function (cb = noop) {
+ let packet = {
+ to: this._form.actor,
+ type: "getExecutableLines"
+ };
+
+ return this._client.request(packet).then(res => {
+ cb(res.lines);
+ return res.lines;
+ });
+ },
+
+ /**
+ * Get a long string grip for this SourceClient's source.
+ */
+ source: function (aCallback = noop) {
+ let packet = {
+ to: this._form.actor,
+ type: "source"
+ };
+ return this._client.request(packet).then(aResponse => {
+ return this._onSourceResponse(aResponse, aCallback);
+ });
+ },
+
+ /**
+ * Pretty print this source's text.
+ */
+ prettyPrint: function (aIndent, aCallback = noop) {
+ const packet = {
+ to: this._form.actor,
+ type: "prettyPrint",
+ indent: aIndent
+ };
+ return this._client.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ this._isPrettyPrinted = true;
+ this._activeThread._clearFrames();
+ this._activeThread.emit("prettyprintchange", this);
+ }
+ return this._onSourceResponse(aResponse, aCallback);
+ });
+ },
+
+ /**
+ * Stop pretty printing this source's text.
+ */
+ disablePrettyPrint: function (aCallback = noop) {
+ const packet = {
+ to: this._form.actor,
+ type: "disablePrettyPrint"
+ };
+ return this._client.request(packet).then(aResponse => {
+ if (!aResponse.error) {
+ this._isPrettyPrinted = false;
+ this._activeThread._clearFrames();
+ this._activeThread.emit("prettyprintchange", this);
+ }
+ return this._onSourceResponse(aResponse, aCallback);
+ });
+ },
+
+ _onSourceResponse: function (aResponse, aCallback) {
+ if (aResponse.error) {
+ aCallback(aResponse);
+ return aResponse;
+ }
+
+ if (typeof aResponse.source === "string") {
+ aCallback(aResponse);
+ return aResponse;
+ }
+
+ let { contentType, source } = aResponse;
+ let longString = this._activeThread.threadLongString(source);
+ return longString.substring(0, longString.length).then(function (aResponse) {
+ if (aResponse.error) {
+ aCallback(aResponse);
+ return aReponse;
+ }
+
+ let response = {
+ source: aResponse.substring,
+ contentType: contentType
+ };
+ aCallback(response);
+ return response;
+ });
+ },
+
+ /**
+ * Request to set a breakpoint in the specified location.
+ *
+ * @param object aLocation
+ * The location and condition of the breakpoint in
+ * the form of { line[, column, condition] }.
+ * @param function aOnResponse
+ * Called with the thread's response.
+ */
+ setBreakpoint: function ({ line, column, condition, noSliding }, aOnResponse = noop) {
+ // A helper function that sets the breakpoint.
+ let doSetBreakpoint = aCallback => {
+ let root = this._client.mainRoot;
+ let location = {
+ line: line,
+ column: column
+ };
+
+ let packet = {
+ to: this.actor,
+ type: "setBreakpoint",
+ location: location,
+ condition: condition,
+ noSliding: noSliding
+ };
+
+ // Backwards compatibility: send the breakpoint request to the
+ // thread if the server doesn't support Debugger.Source actors.
+ if (!root.traits.debuggerSourceActors) {
+ packet.to = this._activeThread.actor;
+ packet.location.url = this.url;
+ }
+
+ return this._client.request(packet).then(aResponse => {
+ // Ignoring errors, since the user may be setting a breakpoint in a
+ // dead script that will reappear on a page reload.
+ let bpClient;
+ if (aResponse.actor) {
+ bpClient = new BreakpointClient(
+ this._client,
+ this,
+ aResponse.actor,
+ location,
+ root.traits.conditionalBreakpoints ? condition : undefined
+ );
+ }
+ aOnResponse(aResponse, bpClient);
+ if (aCallback) {
+ aCallback();
+ }
+ return [aResponse, bpClient];
+ });
+ };
+
+ // If the debuggee is paused, just set the breakpoint.
+ if (this._activeThread.paused) {
+ return doSetBreakpoint();
+ }
+ // Otherwise, force a pause in order to set the breakpoint.
+ return this._activeThread.interrupt().then(aResponse => {
+ if (aResponse.error) {
+ // Can't set the breakpoint if pausing failed.
+ aOnResponse(aResponse);
+ return aResponse;
+ }
+
+ const { type, why } = aResponse;
+ const cleanUp = type == "paused" && why.type == "interrupted"
+ ? () => this._activeThread.resume()
+ : noop;
+
+ return doSetBreakpoint(cleanUp);
+ });
+ }
+};
+
+/**
+ * Breakpoint clients are used to remove breakpoints that are no longer used.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aSourceClient SourceClient
+ * The source where this breakpoint exists
+ * @param aActor string
+ * The actor ID for this breakpoint.
+ * @param aLocation object
+ * The location of the breakpoint. This is an object with two properties:
+ * url and line.
+ * @param aCondition string
+ * The conditional expression of the breakpoint
+ */
+function BreakpointClient(aClient, aSourceClient, aActor, aLocation, aCondition) {
+ this._client = aClient;
+ this._actor = aActor;
+ this.location = aLocation;
+ this.location.actor = aSourceClient.actor;
+ this.location.url = aSourceClient.url;
+ this.source = aSourceClient;
+ this.request = this._client.request;
+
+ // The condition property should only exist if it's a truthy value
+ if (aCondition) {
+ this.condition = aCondition;
+ }
+}
+
+BreakpointClient.prototype = {
+
+ _actor: null,
+ get actor() { return this._actor; },
+ get _transport() { return this._client._transport; },
+
+ /**
+ * Remove the breakpoint from the server.
+ */
+ remove: DebuggerClient.requester({
+ type: "delete"
+ }),
+
+ /**
+ * Determines if this breakpoint has a condition
+ */
+ hasCondition: function () {
+ let root = this._client.mainRoot;
+ // XXX bug 990137: We will remove support for client-side handling of
+ // conditional breakpoints
+ if (root.traits.conditionalBreakpoints) {
+ return "condition" in this;
+ } else {
+ return "conditionalExpression" in this;
+ }
+ },
+
+ /**
+ * Get the condition of this breakpoint. Currently we have to
+ * support locally emulated conditional breakpoints until the
+ * debugger servers are updated (see bug 990137). We used a
+ * different property when moving it server-side to ensure that we
+ * are testing the right code.
+ */
+ getCondition: function () {
+ let root = this._client.mainRoot;
+ if (root.traits.conditionalBreakpoints) {
+ return this.condition;
+ } else {
+ return this.conditionalExpression;
+ }
+ },
+
+ /**
+ * Set the condition of this breakpoint
+ */
+ setCondition: function (gThreadClient, aCondition) {
+ let root = this._client.mainRoot;
+ let deferred = promise.defer();
+
+ if (root.traits.conditionalBreakpoints) {
+ let info = {
+ line: this.location.line,
+ column: this.location.column,
+ condition: aCondition
+ };
+
+ // Remove the current breakpoint and add a new one with the
+ // condition.
+ this.remove(aResponse => {
+ if (aResponse && aResponse.error) {
+ deferred.reject(aResponse);
+ return;
+ }
+
+ this.source.setBreakpoint(info, (aResponse, aNewBreakpoint) => {
+ if (aResponse && aResponse.error) {
+ deferred.reject(aResponse);
+ } else {
+ deferred.resolve(aNewBreakpoint);
+ }
+ });
+ });
+ } else {
+ // The property shouldn't even exist if the condition is blank
+ if (aCondition === "") {
+ delete this.conditionalExpression;
+ }
+ else {
+ this.conditionalExpression = aCondition;
+ }
+ deferred.resolve(this);
+ }
+
+ return deferred.promise;
+ }
+};
+
+eventSource(BreakpointClient.prototype);
+
+/**
+ * Environment clients are used to manipulate the lexical environment actors.
+ *
+ * @param aClient DebuggerClient
+ * The debugger client parent.
+ * @param aForm Object
+ * The form sent across the remote debugging protocol.
+ */
+function EnvironmentClient(aClient, aForm) {
+ this._client = aClient;
+ this._form = aForm;
+ this.request = this._client.request;
+}
+exports.EnvironmentClient = EnvironmentClient;
+
+EnvironmentClient.prototype = {
+
+ get actor() {
+ return this._form.actor;
+ },
+ get _transport() { return this._client._transport; },
+
+ /**
+ * Fetches the bindings introduced by this lexical environment.
+ */
+ getBindings: DebuggerClient.requester({
+ type: "bindings"
+ }),
+
+ /**
+ * Changes the value of the identifier whose name is name (a string) to that
+ * represented by value (a grip).
+ */
+ assign: DebuggerClient.requester({
+ type: "assign",
+ name: args(0),
+ value: args(1)
+ })
+};
+
+eventSource(EnvironmentClient.prototype);
diff --git a/devtools/shared/client/moz.build b/devtools/shared/client/moz.build
new file mode 100644
index 0000000000..aba5a2bfe2
--- /dev/null
+++ b/devtools/shared/client/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'connection-manager.js',
+ 'main.js',
+)