Bug 1588242 [wpt PR 19652] - [tools] Correct filtering logic for building docs, a...
[gecko.git] / remote / RemoteAgent.jsm
bloba87000c6c644fb10481bf5a51dd3d8c75efff554
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/. */
5 "use strict";
7 var EXPORTED_SYMBOLS = ["RemoteAgent", "RemoteAgentFactory"];
9 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10 const { XPCOMUtils } = ChromeUtils.import(
11   "resource://gre/modules/XPCOMUtils.jsm"
14 XPCOMUtils.defineLazyModuleGetters(this, {
15   FatalError: "chrome://remote/content/Error.jsm",
16   HttpServer: "chrome://remote/content/server/HTTPD.jsm",
17   JSONHandler: "chrome://remote/content/JSONHandler.jsm",
18   Log: "chrome://remote/content/Log.jsm",
19   NetUtil: "resource://gre/modules/NetUtil.jsm",
20   Observer: "chrome://remote/content/Observer.jsm",
21   Preferences: "resource://gre/modules/Preferences.jsm",
22   RecommendedPreferences: "chrome://remote/content/RecommendedPreferences.jsm",
23   Targets: "chrome://remote/content/targets/Targets.jsm",
24 });
25 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
27 const ENABLED = "remote.enabled";
28 const FORCE_LOCAL = "remote.force-local";
30 const DEFAULT_HOST = "localhost";
31 const DEFAULT_PORT = 9222;
32 const LOOPBACKS = ["localhost", "127.0.0.1", "[::1]"];
34 class RemoteAgentClass {
35   async init() {
36     if (!Preferences.get(ENABLED, false)) {
37       throw new Error("Remote agent is disabled by its preference");
38     }
39     if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
40       throw new Error(
41         "Remote agent can only be instantiated from the parent process"
42       );
43     }
45     if (this.server) {
46       return;
47     }
49     this.server = new HttpServer();
50     this.targets = new Targets();
52     // Register the static HTTP endpoints like /json/version or /json/list
53     this.server.registerPrefixHandler("/json/", new JSONHandler(this));
55     // Register the dynamic HTTP endpoint of each target.
56     // These are WebSocket URL where the HTTP request will be morphed
57     // into a WebSocket connectiong after an handshake.
58     this.targets.on("target-created", (eventName, target) => {
59       if (!target.path) {
60         throw new Error(`Target is missing 'path' attribute: ${target}`);
61       }
62       this.server.registerPathHandler(target.path, target);
63     });
64     this.targets.on("target-destroyed", (eventName, target) => {
65       // TODO: This removes the entry added by registerPathHandler, should rather expose
66       // an unregisterPathHandler method on nsHttpServer.
67       delete this.server._handler._overridePaths[target.path];
68     });
69   }
71   get listening() {
72     return !!this.server && !this.server._socketClosed;
73   }
75   async listen(address) {
76     if (!(address instanceof Ci.nsIURI)) {
77       throw new TypeError(`Expected nsIURI: ${address}`);
78     }
80     let { host, port } = address;
81     if (Preferences.get(FORCE_LOCAL) && !LOOPBACKS.includes(host)) {
82       throw new Error("Restricted to loopback devices");
83     }
85     // nsIServerSocket uses -1 for atomic port allocation
86     if (port === 0) {
87       port = -1;
88     }
90     if (this.listening) {
91       return;
92     }
94     await this.init();
96     // Start watching for targets *after* registering the target listeners
97     // as this will fire event for already-existing targets.
98     await this.targets.watchForTargets();
100     try {
101       // Immediatly instantiate the main process target in order
102       // to be accessible via HTTP endpoint on startup
103       const mainTarget = this.targets.getMainProcessTarget();
105       this.server._start(port, host);
106       dump(`DevTools listening on ${mainTarget.wsDebuggerURL}\n`);
107     } catch (e) {
108       throw new Error(`Unable to start remote agent: ${e.message}`, e);
109     }
111     Preferences.set(RecommendedPreferences);
112   }
114   async close() {
115     if (this.listening) {
116       try {
117         // Destroy all the targets first in order to ensure closing all pending
118         // connections first. Otherwise Httpd's stop is not going to resolve.
119         this.targets.destructor();
121         await this.server.stop();
123         Preferences.reset(Object.keys(RecommendedPreferences));
124       } catch (e) {
125         throw new Error(`Unable to stop agent: ${e.message}`, e);
126       }
128       log.info("Stopped listening");
129     }
130   }
132   get scheme() {
133     if (!this.server) {
134       return null;
135     }
136     return this.server.identity.primaryScheme;
137   }
139   get host() {
140     if (!this.server) {
141       return null;
142     }
143     return this.server.identity.primaryHost;
144   }
146   get port() {
147     if (!this.server) {
148       return null;
149     }
150     return this.server.identity.primaryPort;
151   }
153   // nsICommandLineHandler
155   async handle(cmdLine) {
156     function flag(name) {
157       const caseSensitive = true;
158       try {
159         return cmdLine.handleFlagWithParam(name, caseSensitive);
160       } catch (e) {
161         return cmdLine.handleFlag(name, caseSensitive);
162       }
163     }
165     const remoteDebugger = flag("remote-debugger");
166     const remoteDebuggingPort = flag("remote-debugging-port");
168     if (remoteDebugger && remoteDebuggingPort) {
169       log.fatal(
170         "Conflicting flags --remote-debugger and --remote-debugging-port"
171       );
172       cmdLine.preventDefault = true;
173       return;
174     }
176     if (!remoteDebugger && !remoteDebuggingPort) {
177       return;
178     }
180     let host, port;
181     if (typeof remoteDebugger == "string") {
182       [host, port] = remoteDebugger.split(":");
183     } else if (typeof remoteDebuggingPort == "string") {
184       port = remoteDebuggingPort;
185     }
187     let addr;
188     try {
189       addr = NetUtil.newURI(
190         `http://${host || DEFAULT_HOST}:${port || DEFAULT_PORT}/`
191       );
192     } catch (e) {
193       log.fatal(
194         `Expected address syntax [<host>]:<port>: ${remoteDebugger ||
195           remoteDebuggingPort}`
196       );
197       cmdLine.preventDefault = true;
198       return;
199     }
201     await Observer.once("sessionstore-windows-restored");
203     try {
204       this.listen(addr);
205     } catch (e) {
206       this.close();
207       throw new FatalError(
208         `Unable to start remote agent on ${addr.spec}: ${e.message}`,
209         e
210       );
211     }
212   }
214   get helpInfo() {
215     return (
216       "  --remote-debugger [<host>][:<port>]\n" +
217       "  --remote-debugging-port <port> Start the Firefox remote agent, which is \n" +
218       "                     a low-level debugging interface based on the CDP protocol.\n" +
219       "                     Defaults to listen on localhost:9222.\n"
220     );
221   }
223   // XPCOM
225   get QueryInterface() {
226     return ChromeUtils.generateQI([Ci.nsICommandLineHandler]);
227   }
230 var RemoteAgent = new RemoteAgentClass();
232 // This is used by the XPCOM codepath which expects a constructor
233 var RemoteAgentFactory = function() {
234   return RemoteAgent;