tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

devtools-server.js (16315B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 var {
      8  ActorRegistry,
      9 } = require("resource://devtools/server/actors/utils/actor-registry.js");
     10 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
     11 var { dumpn } = DevToolsUtils;
     12 
     13 loader.lazyRequireGetter(
     14  this,
     15  "DevToolsServerConnection",
     16  "resource://devtools/server/devtools-server-connection.js",
     17  true
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  "Authentication",
     22  "resource://devtools/shared/security/auth.js"
     23 );
     24 loader.lazyRequireGetter(
     25  this,
     26  "LocalDebuggerTransport",
     27  "resource://devtools/shared/transport/local-transport.js",
     28  true
     29 );
     30 loader.lazyRequireGetter(
     31  this,
     32  "ChildDebuggerTransport",
     33  "resource://devtools/shared/transport/child-transport.js",
     34  true
     35 );
     36 loader.lazyRequireGetter(
     37  this,
     38  "JsWindowActorTransport",
     39  "resource://devtools/shared/transport/js-window-actor-transport.js",
     40  true
     41 );
     42 loader.lazyRequireGetter(
     43  this,
     44  "WorkerThreadWorkerDebuggerTransport",
     45  "resource://devtools/shared/transport/worker-transport.js",
     46  true
     47 );
     48 
     49 const CONTENT_PROCESS_SERVER_STARTUP_SCRIPT =
     50  "resource://devtools/server/startup/content-process.js";
     51 
     52 loader.lazyRequireGetter(
     53  this,
     54  "EventEmitter",
     55  "resource://devtools/shared/event-emitter.js"
     56 );
     57 
     58 /**
     59 * DevToolsServer is a singleton that has several responsibilities. It will
     60 * register the DevTools server actors that are relevant to the context.
     61 * It can also create other DevToolsServer, that will live in the same
     62 * environment as the debugged target (content page, worker...).
     63 *
     64 * For instance a regular Toolbox will be linked to DevToolsClient connected to
     65 * a DevToolsServer running in the same process as the Toolbox (main process).
     66 * But another DevToolsServer will be created in the same process as the page
     67 * targeted by the Toolbox.
     68 *
     69 * Despite being a singleton, the DevToolsServer still has a lifecycle and a
     70 * state. When a consumer needs to spawn a DevToolsServer, the init() method
     71 * should be called. Then you should either call registerAllActors or
     72 * registerActors to setup the server.
     73 * When the server is no longer needed, destroy() should be called.
     74 *
     75 */
     76 var DevToolsServer = {
     77  _listeners: [],
     78  _initialized: false,
     79  // Map of global actor names to actor constructors.
     80  globalActorFactories: {},
     81  // Map of target-scoped actor names to actor constructors.
     82  targetScopedActorFactories: {},
     83 
     84  LONG_STRING_LENGTH: 10000,
     85  LONG_STRING_INITIAL_LENGTH: 1000,
     86  LONG_STRING_READ_LENGTH: 65 * 1024,
     87 
     88  /**
     89   * The windowtype of the chrome window to use for actors that use the global
     90   * window (i.e the global style editor). Set this to your main window type,
     91   * for example "navigator:browser".
     92   */
     93  chromeWindowType: "navigator:browser",
     94 
     95  /**
     96   * Allow debugging chrome of (parent or child) processes.
     97   */
     98  allowChromeProcess: false,
     99 
    100  /**
    101   * Flag used to check if the server can be destroyed when all connections have been
    102   * removed. Firefox on Android runs a single shared DevToolsServer, and should not be
    103   * closed even if no client is connected.
    104   */
    105  keepAlive: false,
    106 
    107  /**
    108   * We run a special server in child process whose main actor is an instance
    109   * of WindowGlobalTargetActor, but that isn't a root actor. Instead there is no root
    110   * actor registered on DevToolsServer.
    111   */
    112  get rootlessServer() {
    113    return !this.createRootActor;
    114  },
    115 
    116  /**
    117   * Initialize the devtools server.
    118   */
    119  init() {
    120    if (this.initialized) {
    121      return;
    122    }
    123 
    124    this._connections = {};
    125    ActorRegistry.init(this._connections);
    126    this._nextConnID = 0;
    127 
    128    this._initialized = true;
    129    this._onSocketListenerAccepted = this._onSocketListenerAccepted.bind(this);
    130 
    131    if (!isWorker) {
    132      // Mochitests watch this observable in order to register the custom actor
    133      // highlighter-test-actor.js.
    134      // Services.obs is not available in workers.
    135      const subject = { wrappedJSObject: ActorRegistry };
    136      Services.obs.notifyObservers(subject, "devtools-server-initialized");
    137    }
    138  },
    139 
    140  get protocol() {
    141    return require("resource://devtools/shared/protocol.js");
    142  },
    143 
    144  get initialized() {
    145    return this._initialized;
    146  },
    147 
    148  hasConnection() {
    149    return this._connections && !!Object.keys(this._connections).length;
    150  },
    151 
    152  hasConnectionForPrefix(prefix) {
    153    return this._connections && !!this._connections[prefix + "/"];
    154  },
    155  /**
    156   * Performs cleanup tasks before shutting down the devtools server. Such tasks
    157   * include clearing any actor constructors added at runtime. This method
    158   * should be called whenever a devtools server is no longer useful, to avoid
    159   * memory leaks. After this method returns, the devtools server must be
    160   * initialized again before use.
    161   */
    162  destroy() {
    163    if (!this._initialized) {
    164      return;
    165    }
    166    this._initialized = false;
    167 
    168    for (const connection of Object.values(this._connections)) {
    169      connection.close();
    170    }
    171 
    172    ActorRegistry.destroy();
    173    this.closeAllSocketListeners();
    174 
    175    // Unregister all listeners
    176    this.off("connectionchange");
    177 
    178    dumpn("DevTools server is shut down.");
    179  },
    180 
    181  /**
    182   * Raises an exception if the server has not been properly initialized.
    183   */
    184  _checkInit() {
    185    if (!this._initialized) {
    186      throw new Error("DevToolsServer has not been initialized.");
    187    }
    188 
    189    if (!this.rootlessServer && !this.createRootActor) {
    190      throw new Error(
    191        "Use DevToolsServer.setRootActor() to add a root actor " +
    192          "implementation."
    193      );
    194    }
    195  },
    196 
    197  /**
    198   * Register different type of actors. Only register the one that are not already
    199   * registered.
    200   *
    201   * @param root boolean
    202   *        Registers the root actor from webbrowser module, which is used to
    203   *        connect to and fetch any other actor.
    204   * @param browser boolean
    205   *        Registers all the parent process actors useful for debugging the
    206   *        runtime itself, like preferences and addons actors.
    207   * @param target boolean
    208   *        Registers all the target-scoped actors like console, script, etc.
    209   *        for debugging a target context.
    210   */
    211  registerActors({ root, browser, target }) {
    212    if (browser) {
    213      ActorRegistry.addBrowserActors();
    214    }
    215 
    216    if (root) {
    217      const {
    218        createRootActor,
    219      } = require("resource://devtools/server/actors/webbrowser.js");
    220      this.setRootActor(createRootActor);
    221    }
    222 
    223    if (target) {
    224      ActorRegistry.addTargetScopedActors();
    225    }
    226  },
    227 
    228  /**
    229   * Register all possible actors for this DevToolsServer.
    230   */
    231  registerAllActors() {
    232    this.registerActors({ root: true, browser: true, target: true });
    233  },
    234 
    235  get listeningSockets() {
    236    return this._listeners.length;
    237  },
    238 
    239  /**
    240   * Add a SocketListener instance to the server's set of active
    241   * SocketListeners.  This is called by a SocketListener after it is opened.
    242   */
    243  addSocketListener(listener) {
    244    if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
    245      throw new Error("Can't add a SocketListener, remote debugging disabled");
    246    }
    247    this._checkInit();
    248 
    249    listener.on("accepted", this._onSocketListenerAccepted);
    250    this._listeners.push(listener);
    251  },
    252 
    253  /**
    254   * Remove a SocketListener instance from the server's set of active
    255   * SocketListeners.  This is called by a SocketListener after it is closed.
    256   */
    257  removeSocketListener(listener) {
    258    // Remove connections that were accepted in the listener.
    259    for (const connID of Object.getOwnPropertyNames(this._connections)) {
    260      const connection = this._connections[connID];
    261      // When calling connection.close on a previous element,
    262      // this may unregister some of the following other connections in `_connections`
    263      // and make them be null here.
    264      if (!connection) {
    265        continue;
    266      }
    267      if (connection.isAcceptedBy(listener)) {
    268        connection.close();
    269      }
    270    }
    271 
    272    this._listeners = this._listeners.filter(l => l !== listener);
    273    listener.off("accepted", this._onSocketListenerAccepted);
    274  },
    275 
    276  /**
    277   * Closes and forgets all previously opened listeners.
    278   *
    279   * @return boolean
    280   *         Whether any listeners were actually closed.
    281   */
    282  closeAllSocketListeners() {
    283    if (!this.listeningSockets) {
    284      return false;
    285    }
    286 
    287    for (const listener of this._listeners) {
    288      listener.close();
    289    }
    290 
    291    return true;
    292  },
    293 
    294  _onSocketListenerAccepted(transport, listener) {
    295    this._onConnection(transport, null, false, listener);
    296  },
    297 
    298  /**
    299   * Creates a new connection to the local debugger speaking over a fake
    300   * transport. This connection results in straightforward calls to the onPacket
    301   * handlers of each side.
    302   *
    303   * @param prefix string [optional]
    304   *    If given, all actors in this connection will have names starting
    305   *    with |prefix + '/'|.
    306   * @returns a client-side DebuggerTransport for communicating with
    307   *    the newly-created connection.
    308   */
    309  connectPipe(prefix) {
    310    this._checkInit();
    311 
    312    const serverTransport = new LocalDebuggerTransport();
    313    const clientTransport = new LocalDebuggerTransport(serverTransport);
    314    serverTransport.other = clientTransport;
    315    const connection = this._onConnection(serverTransport, prefix);
    316 
    317    // I'm putting this here because I trust you.
    318    //
    319    // There are times, when using a local connection, when you're going
    320    // to be tempted to just get direct access to the server.  Resist that
    321    // temptation!  If you succumb to that temptation, you will make the
    322    // fine developers that work on Fennec and Firefox OS sad.  They're
    323    // professionals, they'll try to act like they understand, but deep
    324    // down you'll know that you hurt them.
    325    //
    326    // This reference allows you to give in to that temptation.  There are
    327    // times this makes sense: tests, for example, and while porting a
    328    // previously local-only codebase to the remote protocol.
    329    //
    330    // But every time you use this, you will feel the shame of having
    331    // used a property that starts with a '_'.
    332    clientTransport._serverConnection = connection;
    333 
    334    return clientTransport;
    335  },
    336 
    337  /**
    338   * In a content child process, create a new connection that exchanges
    339   * nsIMessageSender messages with our parent process.
    340   *
    341   * @param prefix
    342   *    The prefix we should use in our nsIMessageSender message names and
    343   *    actor names. This connection will use messages named
    344   *    "debug:<prefix>:packet", and all its actors will have names
    345   *    beginning with "<prefix>/".
    346   */
    347  connectToParent(prefix, scopeOrManager) {
    348    this._checkInit();
    349 
    350    const transport = isWorker
    351      ? new WorkerThreadWorkerDebuggerTransport(scopeOrManager, prefix)
    352      : new ChildDebuggerTransport(scopeOrManager, prefix);
    353 
    354    return this._onConnection(transport, prefix, true);
    355  },
    356 
    357  connectToParentWindowActor(jsWindowChildActor, forwardingPrefix) {
    358    this._checkInit();
    359    const transport = new JsWindowActorTransport(
    360      jsWindowChildActor,
    361      forwardingPrefix
    362    );
    363 
    364    return this._onConnection(transport, forwardingPrefix, true);
    365  },
    366 
    367  /**
    368   * Check if the server is running in the child process.
    369   */
    370  get isInChildProcess() {
    371    return (
    372      Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
    373    );
    374  },
    375 
    376  /**
    377   * Create a new debugger connection for the given transport. Called after
    378   * connectPipe(), from connectToParent, or from an incoming socket
    379   * connection handler.
    380   *
    381   * If present, |forwardingPrefix| is a forwarding prefix that a parent
    382   * server is using to recognizes messages intended for this server. Ensure
    383   * that all our actors have names beginning with |forwardingPrefix + '/'|.
    384   * In particular, the root actor's name will be |forwardingPrefix + '/root'|.
    385   */
    386  _onConnection(
    387    transport,
    388    forwardingPrefix,
    389    noRootActor = false,
    390    socketListener = null
    391  ) {
    392    let connID;
    393    if (forwardingPrefix) {
    394      connID = forwardingPrefix + "/";
    395    } else {
    396      // Multiple servers can be started at the same time, and when that's the
    397      // case, they are loaded in separate devtools loaders.
    398      // So, use the current loader ID to prefix the connection ID and make it
    399      // unique.
    400      connID = "server" + loader.id + ".conn" + this._nextConnID++ + ".";
    401    }
    402 
    403    // Notify the platform code that DevTools is running in the current process
    404    // when we are wiring the very first connection
    405    if (!this.hasConnection()) {
    406      ChromeUtils.notifyDevToolsOpened();
    407    }
    408 
    409    const conn = new DevToolsServerConnection(
    410      connID,
    411      transport,
    412      socketListener
    413    );
    414    this._connections[connID] = conn;
    415 
    416    // Create a root actor for the connection and send the hello packet.
    417    if (!noRootActor) {
    418      conn.rootActor = this.createRootActor(conn);
    419      if (forwardingPrefix) {
    420        conn.rootActor.actorID = forwardingPrefix + "/root";
    421      } else {
    422        conn.rootActor.actorID = "root";
    423      }
    424      conn.addActor(conn.rootActor);
    425      transport.send(conn.rootActor.sayHello());
    426    }
    427    transport.ready();
    428 
    429    this.emit("connectionchange", "opened", conn);
    430    return conn;
    431  },
    432 
    433  /**
    434   * Remove the connection from the debugging server.
    435   */
    436  _connectionClosed(connection) {
    437    delete this._connections[connection.prefix];
    438    this.emit("connectionchange", "closed", connection);
    439 
    440    const hasConnection = this.hasConnection();
    441 
    442    // Notify the platform code that we stopped running DevTools code in the current process
    443    if (!hasConnection) {
    444      ChromeUtils.notifyDevToolsClosed();
    445    }
    446 
    447    // If keepAlive isn't explicitely set to true, destroy the server once its
    448    // last connection closes. Multiple JSWindowActor may use the same DevToolsServer
    449    // and in this case, let the server destroy itself once the last connection closes.
    450    // Otherwise we set keepAlive to true when starting a listening server, receiving
    451    // client connections. Typically when running server on phones, or on desktop
    452    // via `--start-debugger-server`.
    453    if (hasConnection || this.keepAlive) {
    454      return;
    455    }
    456 
    457    this.destroy();
    458  },
    459 
    460  // DevToolsServer extension API.
    461 
    462  setRootActor(actorFactory) {
    463    this.createRootActor = actorFactory;
    464  },
    465 
    466  /**
    467   * Called when DevTools are unloaded to remove the contend process server startup script
    468   * for the list of scripts loaded for each new content process. Will also remove message
    469   * listeners from already loaded scripts.
    470   */
    471  removeContentServerScript() {
    472    Services.ppmm.removeDelayedProcessScript(
    473      CONTENT_PROCESS_SERVER_STARTUP_SCRIPT
    474    );
    475    try {
    476      Services.ppmm.broadcastAsyncMessage("debug:close-content-server");
    477    } catch (e) {
    478      // Nothing to do
    479    }
    480  },
    481 
    482  /**
    483   * Searches all active connections for an actor matching an ID.
    484   *
    485   * ⚠ TO BE USED ONLY FROM SERVER CODE OR TESTING ONLY! ⚠`
    486   *
    487   * This is helpful for some tests which depend on reaching into the server to check some
    488   * properties of an actor, and it is also used by the actors related to the
    489   * DevTools WebExtensions API to be able to interact with the actors created for the
    490   * panels natively provided by the DevTools Toolbox.
    491   */
    492  searchAllConnectionsForActor(actorID) {
    493    // NOTE: the actor IDs are generated with the following format:
    494    //
    495    //   `server${loaderID}.conn${ConnectionID}${ActorPrefix}${ActorID}`
    496    //
    497    // as an optimization we can come up with a regexp to query only
    498    // the right connection via its id.
    499    for (const connID of Object.getOwnPropertyNames(this._connections)) {
    500      const actor = this._connections[connID].getActor(actorID);
    501      if (actor) {
    502        return actor;
    503      }
    504    }
    505    return null;
    506  },
    507 };
    508 
    509 // Expose these to save callers the trouble of importing DebuggerSocket
    510 DevToolsUtils.defineLazyGetter(DevToolsServer, "Authenticators", () => {
    511  return Authentication.Authenticators;
    512 });
    513 DevToolsUtils.defineLazyGetter(DevToolsServer, "AuthenticationResult", () => {
    514  return Authentication.AuthenticationResult;
    515 });
    516 
    517 EventEmitter.decorate(DevToolsServer);
    518 
    519 exports.DevToolsServer = DevToolsServer;