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/. */
7 var EXPORTED_SYMBOLS = ["RemoteAgent"];
9 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
10 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
12 XPCOMUtils.defineLazyModuleGetters(this, {
13 FatalError: "chrome://remote/content/Error.jsm",
14 HttpServer: "chrome://remote/content/server/HTTPD.jsm",
15 JSONHandler: "chrome://remote/content/JSONHandler.jsm",
16 Log: "chrome://remote/content/Log.jsm",
17 NetUtil: "resource://gre/modules/NetUtil.jsm",
18 Observer: "chrome://remote/content/Observer.jsm",
19 Preferences: "resource://gre/modules/Preferences.jsm",
20 RecommendedPreferences: "chrome://remote/content/RecommendedPreferences.jsm",
21 TabObserver: "chrome://remote/content/WindowManager.jsm",
22 Targets: "chrome://remote/content/targets/Targets.jsm",
24 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
26 const ENABLED = "remote.enabled";
27 const FORCE_LOCAL = "remote.force-local";
29 const DEFAULT_HOST = "localhost";
30 const DEFAULT_PORT = 9222;
31 const LOOPBACKS = ["localhost", "127.0.0.1", "[::1]"];
33 class RemoteAgentClass {
35 if (!Preferences.get(ENABLED, false)) {
36 throw new Error("Remote agent is disabled by its preference");
38 if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
39 throw new Error("Remote agent can only be instantiated from the parent process");
46 this.server = new HttpServer();
47 this.targets = new Targets();
49 this.server.registerPrefixHandler("/json/", new JSONHandler(this));
51 this.tabs = new TabObserver({registerExisting: true});
52 this.tabs.on("open", (eventName, tab) => {
53 this.targets.connect(tab.linkedBrowser);
55 this.tabs.on("close", (eventName, tab) => {
56 this.targets.disconnect(tab.linkedBrowser);
59 this.targets.on("connect", (eventName, target) => {
61 throw new Error(`Target is missing 'path' attribute: ${target}`);
63 this.server.registerPathHandler(target.path, target);
65 this.targets.on("disconnect", (eventName, target) => {
66 // TODO: This removes the entry added by registerPathHandler, should rather expose
67 // an unregisterPathHandler method on nsHttpServer.
68 delete this.server._handler._overridePaths[target.path];
73 return !!this.server && !this.server._socketClosed;
76 async listen(address) {
77 if (!(address instanceof Ci.nsIURI)) {
78 throw new TypeError(`Expected nsIURI: ${address}`);
81 let {host, port} = address;
82 if (Preferences.get(FORCE_LOCAL) && !LOOPBACKS.includes(host)) {
83 throw new Error("Restricted to loopback devices");
86 // nsIServerSocket uses -1 for atomic port allocation
97 await this.tabs.start();
100 // Immediatly instantiate the main process target in order
101 // to be accessible via HTTP endpoint on startup
102 const mainTarget = this.targets.getMainProcessTarget();
104 this.server._start(port, host);
105 dump(`DevTools listening on ${mainTarget.wsDebuggerURL}\n`);
107 throw new Error(`Unable to start remote agent: ${e.message}`, e);
110 Preferences.set(RecommendedPreferences);
114 if (this.listening) {
116 // Disconnect the targets first in order to ensure closing all pending
117 // connection first. Otherwise Httpd's stop is not going to resolve.
118 this.targets.clear();
120 await this.server.stop();
122 Preferences.reset(Object.keys(RecommendedPreferences));
126 throw new Error(`Unable to stop agent: ${e.message}`, e);
129 log.info("Stopped listening");
137 return this.server.identity.primaryScheme;
144 return this.server.identity.primaryHost;
151 return this.server.identity.primaryPort;
154 // nsICommandLineHandler
156 async handle(cmdLine) {
157 function flag(name) {
158 const caseSensitive = true;
160 return cmdLine.handleFlagWithParam(name, caseSensitive);
162 return cmdLine.handleFlag(name, caseSensitive);
166 const remoteDebugger = flag("remote-debugger");
167 const remoteDebuggingPort = flag("remote-debugging-port");
169 if (remoteDebugger && remoteDebuggingPort) {
170 log.fatal("Conflicting flags --remote-debugger and --remote-debugging-port");
171 cmdLine.preventDefault = true;
175 if (!remoteDebugger && !remoteDebuggingPort) {
180 if (typeof remoteDebugger == "string") {
181 [host, port] = remoteDebugger.split(":");
182 } else if (typeof remoteDebuggingPort == "string") {
183 port = remoteDebuggingPort;
188 addr = NetUtil.newURI(`http://${host || DEFAULT_HOST}:${port || DEFAULT_PORT}/`);
190 log.fatal(`Expected address syntax [<host>]:<port>: ${remoteDebugger || remoteDebuggingPort}`);
191 cmdLine.preventDefault = true;
197 await Observer.once("sessionstore-windows-restored");
203 throw new FatalError(`Unable to start remote agent on ${addr.spec}: ${e.message}`, e);
208 return " --remote-debugger [<host>][:<port>]\n" +
209 " --remote-debugging-port <port> Start the Firefox remote agent, which is \n" +
210 " a low-level debugging interface based on the CDP protocol.\n" +
211 " Defaults to listen on localhost:9222.\n";
216 get QueryInterface() {
217 return ChromeUtils.generateQI([Ci.nsICommandLineHandler]);
221 var RemoteAgent = new RemoteAgentClass();