DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (375f9429ba87)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var EXPORTED_SYMBOLS = ["RemoteAgent", "RemoteAgentFactory"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);

XPCOMUtils.defineLazyModuleGetters(this, {
  FatalError: "chrome://remote/content/Error.jsm",
  HttpServer: "chrome://remote/content/server/HTTPD.jsm",
  JSONHandler: "chrome://remote/content/JSONHandler.jsm",
  Log: "chrome://remote/content/Log.jsm",
  NetUtil: "resource://gre/modules/NetUtil.jsm",
  Observer: "chrome://remote/content/Observer.jsm",
  Preferences: "resource://gre/modules/Preferences.jsm",
  RecommendedPreferences: "chrome://remote/content/RecommendedPreferences.jsm",
  Targets: "chrome://remote/content/targets/Targets.jsm",
});
XPCOMUtils.defineLazyGetter(this, "log", Log.get);

const ENABLED = "remote.enabled";
const FORCE_LOCAL = "remote.force-local";

const DEFAULT_HOST = "localhost";
const DEFAULT_PORT = 9222;
const LOOPBACKS = ["localhost", "127.0.0.1", "[::1]"];

class RemoteAgentClass {
  async init() {
    if (!Preferences.get(ENABLED, false)) {
      throw new Error("Remote agent is disabled by its preference");
    }
    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
      throw new Error(
        "Remote agent can only be instantiated from the parent process"
      );
    }

    if (this.server) {
      return;
    }

    this.server = new HttpServer();
    this.targets = new Targets();

    // Register the static HTTP endpoints like /json/version or /json/list
    this.server.registerPrefixHandler("/json/", new JSONHandler(this));

    // Register the dynamic HTTP endpoint of each target.
    // These are WebSocket URL where the HTTP request will be morphed
    // into a WebSocket connectiong after an handshake.
    this.targets.on("target-created", (eventName, target) => {
      if (!target.path) {
        throw new Error(`Target is missing 'path' attribute: ${target}`);
      }
      this.server.registerPathHandler(target.path, target);
    });
    this.targets.on("target-destroyed", (eventName, target) => {
      // TODO: This removes the entry added by registerPathHandler, should rather expose
      // an unregisterPathHandler method on nsHttpServer.
      delete this.server._handler._overridePaths[target.path];
    });
  }

  get listening() {
    return !!this.server && !this.server._socketClosed;
  }

  async listen(address) {
    if (!(address instanceof Ci.nsIURI)) {
      throw new TypeError(`Expected nsIURI: ${address}`);
    }

    let { host, port } = address;
    if (Preferences.get(FORCE_LOCAL) && !LOOPBACKS.includes(host)) {
      throw new Error("Restricted to loopback devices");
    }

    // nsIServerSocket uses -1 for atomic port allocation
    if (port === 0) {
      port = -1;
    }

    if (this.listening) {
      return;
    }

    await this.init();

    // Start watching for targets *after* registering the target listeners
    // as this will fire event for already-existing targets.
    await this.targets.watchForTargets();

    try {
      // Immediatly instantiate the main process target in order
      // to be accessible via HTTP endpoint on startup
      const mainTarget = this.targets.getMainProcessTarget();

      this.server._start(port, host);
      dump(`DevTools listening on ${mainTarget.wsDebuggerURL}\n`);
    } catch (e) {
      throw new Error(`Unable to start remote agent: ${e.message}`, e);
    }

    Preferences.set(RecommendedPreferences);
  }

  async close() {
    if (this.listening) {
      try {
        // Destroy all the targets first in order to ensure closing all pending
        // connections first. Otherwise Httpd's stop is not going to resolve.
        this.targets.destructor();

        await this.server.stop();

        Preferences.reset(Object.keys(RecommendedPreferences));
      } catch (e) {
        throw new Error(`Unable to stop agent: ${e.message}`, e);
      }

      log.info("Stopped listening");
    }
  }

  get scheme() {
    if (!this.server) {
      return null;
    }
    return this.server.identity.primaryScheme;
  }

  get host() {
    if (!this.server) {
      return null;
    }
    return this.server.identity.primaryHost;
  }

  get port() {
    if (!this.server) {
      return null;
    }
    return this.server.identity.primaryPort;
  }

  // nsICommandLineHandler

  async handle(cmdLine) {
    function flag(name) {
      const caseSensitive = true;
      try {
        return cmdLine.handleFlagWithParam(name, caseSensitive);
      } catch (e) {
        return cmdLine.handleFlag(name, caseSensitive);
      }
    }

    const remoteDebugger = flag("remote-debugger");
    const remoteDebuggingPort = flag("remote-debugging-port");

    if (remoteDebugger && remoteDebuggingPort) {
      log.fatal(
        "Conflicting flags --remote-debugger and --remote-debugging-port"
      );
      cmdLine.preventDefault = true;
      return;
    }

    if (!remoteDebugger && !remoteDebuggingPort) {
      return;
    }

    let host, port;
    if (typeof remoteDebugger == "string") {
      [host, port] = remoteDebugger.split(":");
    } else if (typeof remoteDebuggingPort == "string") {
      port = remoteDebuggingPort;
    }

    let addr;
    try {
      addr = NetUtil.newURI(
        `http://${host || DEFAULT_HOST}:${port || DEFAULT_PORT}/`
      );
    } catch (e) {
      log.fatal(
        `Expected address syntax [<host>]:<port>: ${remoteDebugger ||
          remoteDebuggingPort}`
      );
      cmdLine.preventDefault = true;
      return;
    }

    await Observer.once("sessionstore-windows-restored");

    try {
      this.listen(addr);
    } catch (e) {
      this.close();
      throw new FatalError(
        `Unable to start remote agent on ${addr.spec}: ${e.message}`,
        e
      );
    }
  }

  get helpInfo() {
    return (
      "  --remote-debugger [<host>][:<port>]\n" +
      "  --remote-debugging-port <port> Start the Firefox remote agent, which is \n" +
      "                     a low-level debugging interface based on the CDP protocol.\n" +
      "                     Defaults to listen on localhost:9222.\n"
    );
  }

  // XPCOM

  get QueryInterface() {
    return ChromeUtils.generateQI([Ci.nsICommandLineHandler]);
  }
}

var RemoteAgent = new RemoteAgentClass();

// This is used by the XPCOM codepath which expects a constructor
var RemoteAgentFactory = function() {
  return RemoteAgent;
};