/* -*- 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 { //