From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- devtools/shared/client/connection-manager.js | 382 ++++ devtools/shared/client/main.js | 3123 ++++++++++++++++++++++++++ devtools/shared/client/moz.build | 10 + 3 files changed, 3515 insertions(+) create mode 100644 devtools/shared/client/connection-manager.js create mode 100644 devtools/shared/client/main.js create mode 100644 devtools/shared/client/moz.build (limited to 'devtools/shared/client') diff --git a/devtools/shared/client/connection-manager.js b/devtools/shared/client/connection-manager.js new file mode 100644 index 000000000..ef242db85 --- /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 000000000..0db8e16c2 --- /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) { + // tabs in parent process + packet.outerWindowID = browser.outerWindowID; + } else { + //