summaryrefslogtreecommitdiffstats
path: root/devtools/client/webide/modules/runtimes.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webide/modules/runtimes.js')
-rw-r--r--devtools/client/webide/modules/runtimes.js673
1 files changed, 673 insertions, 0 deletions
diff --git a/devtools/client/webide/modules/runtimes.js b/devtools/client/webide/modules/runtimes.js
new file mode 100644
index 000000000..a23337359
--- /dev/null
+++ b/devtools/client/webide/modules/runtimes.js
@@ -0,0 +1,673 @@
+/* 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} = require("chrome");
+const Services = require("Services");
+const {Devices} = require("resource://devtools/shared/apps/Devices.jsm");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const {DebuggerServer} = require("devtools/server/main");
+const {Simulators} = require("devtools/client/webide/modules/simulators");
+const discovery = require("devtools/shared/discovery/discovery");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+loader.lazyRequireGetter(this, "AuthenticationResult",
+ "devtools/shared/security/auth", true);
+loader.lazyRequireGetter(this, "DevToolsUtils",
+ "devtools/shared/DevToolsUtils");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+/**
+ * Runtime and Scanner API
+ *
+ * |RuntimeScanners| maintains a set of |Scanner| objects that produce one or
+ * more |Runtime|s to connect to. Add-ons can extend the set of known runtimes
+ * by registering additional |Scanner|s that emit them.
+ *
+ * Each |Scanner| must support the following API:
+ *
+ * enable()
+ * Bind any event handlers and start any background work the scanner needs to
+ * maintain an updated set of |Runtime|s.
+ * Called when the first consumer (such as WebIDE) actively interested in
+ * maintaining the |Runtime| list enables the registry.
+ * disable()
+ * Unbind any event handlers and stop any background work the scanner needs to
+ * maintain an updated set of |Runtime|s.
+ * Called when the last consumer (such as WebIDE) actively interested in
+ * maintaining the |Runtime| list disables the registry.
+ * emits "runtime-list-updated"
+ * If the set of runtimes a |Scanner| manages has changed, it must emit this
+ * event to notify consumers of changes.
+ * scan()
+ * Actively refreshes the list of runtimes the scanner knows about. If your
+ * scanner uses an active scanning approach (as opposed to listening for
+ * events when changes occur), the bulk of the work would be done here.
+ * @return Promise
+ * Should be resolved when scanning is complete. If scanning has no
+ * well-defined end point, you can resolve immediately, as long as
+ * update event is emitted later when changes are noticed.
+ * listRuntimes()
+ * Return the current list of runtimes known to the |Scanner| instance.
+ * @return Iterable
+ *
+ * Each |Runtime| must support the following API:
+ *
+ * |type| field
+ * The |type| must be one of the values from the |RuntimeTypes| object. This
+ * is used for Telemetry and to support displaying sets of |Runtime|s
+ * categorized by type.
+ * |id| field
+ * An identifier that is unique in the set of all runtimes with the same
+ * |type|. WebIDE tries to save the last used runtime via type + id, and
+ * tries to locate it again in the next session, so this value should attempt
+ * to be stable across Firefox sessions.
+ * |name| field
+ * A user-visible label to identify the runtime that will be displayed in a
+ * runtime list.
+ * |prolongedConnection| field
+ * A boolean value which should be |true| if the connection process is
+ * expected to take a unknown or large amount of time. A UI may use this as a
+ * hint to skip timeouts or other time-based code paths.
+ * connect()
+ * Configure the passed |connection| object with any settings need to
+ * successfully connect to the runtime, and call the |connection|'s connect()
+ * method.
+ * @param Connection connection
+ * A |Connection| object from the DevTools |ConnectionManager|.
+ * @return Promise
+ * Resolved once you've called the |connection|'s connect() method.
+ * configure() OPTIONAL
+ * Show a configuration screen if the runtime is configurable.
+ */
+
+/* SCANNER REGISTRY */
+
+var RuntimeScanners = {
+
+ _enabledCount: 0,
+ _scanners: new Set(),
+
+ get enabled() {
+ return !!this._enabledCount;
+ },
+
+ add(scanner) {
+ if (this.enabled) {
+ // Enable any scanner added while globally enabled
+ this._enableScanner(scanner);
+ }
+ this._scanners.add(scanner);
+ this._emitUpdated();
+ },
+
+ remove(scanner) {
+ this._scanners.delete(scanner);
+ if (this.enabled) {
+ // Disable any scanner removed while globally enabled
+ this._disableScanner(scanner);
+ }
+ this._emitUpdated();
+ },
+
+ has(scanner) {
+ return this._scanners.has(scanner);
+ },
+
+ scan() {
+ if (!this.enabled) {
+ return promise.resolve();
+ }
+
+ if (this._scanPromise) {
+ return this._scanPromise;
+ }
+
+ let promises = [];
+
+ for (let scanner of this._scanners) {
+ promises.push(scanner.scan());
+ }
+
+ this._scanPromise = promise.all(promises);
+
+ // Reset pending promise
+ this._scanPromise.then(() => {
+ this._scanPromise = null;
+ }, () => {
+ this._scanPromise = null;
+ });
+
+ return this._scanPromise;
+ },
+
+ listRuntimes: function* () {
+ for (let scanner of this._scanners) {
+ for (let runtime of scanner.listRuntimes()) {
+ yield runtime;
+ }
+ }
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ enable() {
+ if (this._enabledCount++ !== 0) {
+ // Already enabled scanners during a previous call
+ return;
+ }
+ this._emitUpdated = this._emitUpdated.bind(this);
+ for (let scanner of this._scanners) {
+ this._enableScanner(scanner);
+ }
+ },
+
+ _enableScanner(scanner) {
+ scanner.enable();
+ scanner.on("runtime-list-updated", this._emitUpdated);
+ },
+
+ disable() {
+ if (--this._enabledCount !== 0) {
+ // Already disabled scanners during a previous call
+ return;
+ }
+ for (let scanner of this._scanners) {
+ this._disableScanner(scanner);
+ }
+ },
+
+ _disableScanner(scanner) {
+ scanner.off("runtime-list-updated", this._emitUpdated);
+ scanner.disable();
+ },
+
+};
+
+EventEmitter.decorate(RuntimeScanners);
+
+exports.RuntimeScanners = RuntimeScanners;
+
+/* SCANNERS */
+
+var SimulatorScanner = {
+
+ _runtimes: [],
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ Simulators.on("updated", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ Simulators.off("updated", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ Simulators.findSimulators().then(simulators => {
+ this._runtimes = [];
+ for (let simulator of simulators) {
+ this._runtimes.push(new SimulatorRuntime(simulator));
+ }
+ this._emitUpdated();
+ });
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ }
+
+};
+
+EventEmitter.decorate(SimulatorScanner);
+RuntimeScanners.add(SimulatorScanner);
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This scanner will list all ADB devices as runtimes, even if they may or may
+ * not actually connect (since the |DeprecatedUSBRuntime| assumes a Firefox OS
+ * device).
+ */
+var DeprecatedAdbScanner = {
+
+ _runtimes: [],
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ Devices.on("register", this._updateRuntimes);
+ Devices.on("unregister", this._updateRuntimes);
+ Devices.on("addon-status-updated", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ Devices.off("register", this._updateRuntimes);
+ Devices.off("unregister", this._updateRuntimes);
+ Devices.off("addon-status-updated", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ this._runtimes = [];
+ for (let id of Devices.available()) {
+ let runtime = new DeprecatedUSBRuntime(id);
+ this._runtimes.push(runtime);
+ runtime.updateNameFromADB().then(() => {
+ this._emitUpdated();
+ }, () => {});
+ }
+ this._emitUpdated();
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ }
+
+};
+
+EventEmitter.decorate(DeprecatedAdbScanner);
+RuntimeScanners.add(DeprecatedAdbScanner);
+
+// ADB Helper 0.7.0 and later will replace this scanner on startup
+exports.DeprecatedAdbScanner = DeprecatedAdbScanner;
+
+/**
+ * This is a lazy ADB scanner shim which only tells the ADB Helper to start and
+ * stop as needed. The real scanner that lists devices lives in ADB Helper.
+ * ADB Helper 0.8.0 and later wait until these signals are received before
+ * starting ADB polling. For earlier versions, they have no effect.
+ */
+var LazyAdbScanner = {
+
+ enable() {
+ Devices.emit("adb-start-polling");
+ },
+
+ disable() {
+ Devices.emit("adb-stop-polling");
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return [];
+ }
+
+};
+
+EventEmitter.decorate(LazyAdbScanner);
+RuntimeScanners.add(LazyAdbScanner);
+
+var WiFiScanner = {
+
+ _runtimes: [],
+
+ init() {
+ this.updateRegistration();
+ Services.prefs.addObserver(this.ALLOWED_PREF, this, false);
+ },
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ discovery.on("devtools-device-added", this._updateRuntimes);
+ discovery.on("devtools-device-updated", this._updateRuntimes);
+ discovery.on("devtools-device-removed", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ discovery.off("devtools-device-added", this._updateRuntimes);
+ discovery.off("devtools-device-updated", this._updateRuntimes);
+ discovery.off("devtools-device-removed", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ this._runtimes = [];
+ for (let device of discovery.getRemoteDevicesWithService("devtools")) {
+ this._runtimes.push(new WiFiRuntime(device));
+ }
+ this._emitUpdated();
+ },
+
+ scan() {
+ discovery.scan();
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ },
+
+ ALLOWED_PREF: "devtools.remote.wifi.scan",
+
+ get allowed() {
+ return Services.prefs.getBoolPref(this.ALLOWED_PREF);
+ },
+
+ updateRegistration() {
+ if (this.allowed) {
+ RuntimeScanners.add(WiFiScanner);
+ } else {
+ RuntimeScanners.remove(WiFiScanner);
+ }
+ this._emitUpdated();
+ },
+
+ observe(subject, topic, data) {
+ if (data !== WiFiScanner.ALLOWED_PREF) {
+ return;
+ }
+ WiFiScanner.updateRegistration();
+ }
+
+};
+
+EventEmitter.decorate(WiFiScanner);
+WiFiScanner.init();
+
+exports.WiFiScanner = WiFiScanner;
+
+var StaticScanner = {
+ enable() {},
+ disable() {},
+ scan() { return promise.resolve(); },
+ listRuntimes() {
+ let runtimes = [gRemoteRuntime];
+ if (Services.prefs.getBoolPref("devtools.webide.enableLocalRuntime")) {
+ runtimes.push(gLocalRuntime);
+ }
+ return runtimes;
+ }
+};
+
+EventEmitter.decorate(StaticScanner);
+RuntimeScanners.add(StaticScanner);
+
+/* RUNTIMES */
+
+// These type strings are used for logging events to Telemetry.
+// You must update Histograms.json if new types are added.
+var RuntimeTypes = exports.RuntimeTypes = {
+ USB: "USB",
+ WIFI: "WIFI",
+ SIMULATOR: "SIMULATOR",
+ REMOTE: "REMOTE",
+ LOCAL: "LOCAL",
+ OTHER: "OTHER"
+};
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This runtime assumes it is connecting to a Firefox OS device.
+ */
+function DeprecatedUSBRuntime(id) {
+ this._id = id;
+}
+
+DeprecatedUSBRuntime.prototype = {
+ type: RuntimeTypes.USB,
+ get device() {
+ return Devices.getByName(this._id);
+ },
+ connect: function (connection) {
+ if (!this.device) {
+ return promise.reject(new Error("Can't find device: " + this.name));
+ }
+ return this.device.connect().then((port) => {
+ connection.host = "localhost";
+ connection.port = port;
+ connection.connect();
+ });
+ },
+ get id() {
+ return this._id;
+ },
+ get name() {
+ return this._productModel || this._id;
+ },
+ updateNameFromADB: function () {
+ if (this._productModel) {
+ return promise.reject();
+ }
+ let deferred = promise.defer();
+ if (this.device && this.device.shell) {
+ this.device.shell("getprop ro.product.model").then(stdout => {
+ this._productModel = stdout;
+ deferred.resolve();
+ }, () => {});
+ } else {
+ this._productModel = null;
+ deferred.reject();
+ }
+ return deferred.promise;
+ },
+};
+
+// For testing use only
+exports._DeprecatedUSBRuntime = DeprecatedUSBRuntime;
+
+function WiFiRuntime(deviceName) {
+ this.deviceName = deviceName;
+}
+
+WiFiRuntime.prototype = {
+ type: RuntimeTypes.WIFI,
+ // Mark runtime as taking a long time to connect
+ prolongedConnection: true,
+ connect: function (connection) {
+ let service = discovery.getRemoteService("devtools", this.deviceName);
+ if (!service) {
+ return promise.reject(new Error("Can't find device: " + this.name));
+ }
+ connection.advertisement = service;
+ connection.authenticator.sendOOB = this.sendOOB;
+ // Disable the default connection timeout, since QR scanning can take an
+ // unknown amount of time. This prevents spurious errors (even after
+ // eventual success) from being shown.
+ connection.timeoutDelay = 0;
+ connection.connect();
+ return promise.resolve();
+ },
+ get id() {
+ return this.deviceName;
+ },
+ get name() {
+ return this.deviceName;
+ },
+
+ /**
+ * During OOB_CERT authentication, a notification dialog like this is used to
+ * to display a token which the user must transfer through some mechanism to the
+ * server to authenticate the devices.
+ *
+ * This implementation presents the token as text for the user to transfer
+ * manually. For a mobile device, you should override this implementation with
+ * something more convenient, such as displaying a QR code.
+ *
+ * This method receives an object containing:
+ * @param host string
+ * The host name or IP address of the debugger server.
+ * @param port number
+ * The port number of the debugger server.
+ * @param cert object (optional)
+ * The server's cert details.
+ * @param authResult AuthenticationResult
+ * Authentication result sent from the server.
+ * @param oob object (optional)
+ * The token data to be transferred during OOB_CERT step 8:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * @return object containing:
+ * * close: Function to hide the notification
+ */
+ sendOOB(session) {
+ const WINDOW_ID = "devtools:wifi-auth";
+ let { authResult } = session;
+ // Only show in the PENDING state
+ if (authResult != AuthenticationResult.PENDING) {
+ throw new Error("Expected PENDING result, got " + authResult);
+ }
+
+ // Listen for the window our prompt opens, so we can close it programatically
+ let promptWindow;
+ let windowListener = {
+ onOpenWindow(xulWindow) {
+ let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function listener() {
+ win.removeEventListener("load", listener, false);
+ if (win.document.documentElement.getAttribute("id") != WINDOW_ID) {
+ return;
+ }
+ // Found the window
+ promptWindow = win;
+ Services.wm.removeListener(windowListener);
+ }, false);
+ },
+ onCloseWindow() {},
+ onWindowTitleChange() {}
+ };
+ Services.wm.addListener(windowListener);
+
+ // |openDialog| is typically a blocking API, so |executeSoon| to get around this
+ DevToolsUtils.executeSoon(() => {
+ // Height determines the size of the QR code. Force a minimum size to
+ // improve scanability.
+ const MIN_HEIGHT = 600;
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ let width = win.outerWidth * 0.8;
+ let height = Math.max(win.outerHeight * 0.5, MIN_HEIGHT);
+ win.openDialog("chrome://webide/content/wifi-auth.xhtml",
+ WINDOW_ID,
+ "modal=yes,width=" + width + ",height=" + height, session);
+ });
+
+ return {
+ close() {
+ if (!promptWindow) {
+ return;
+ }
+ promptWindow.close();
+ promptWindow = null;
+ }
+ };
+ }
+};
+
+// For testing use only
+exports._WiFiRuntime = WiFiRuntime;
+
+function SimulatorRuntime(simulator) {
+ this.simulator = simulator;
+}
+
+SimulatorRuntime.prototype = {
+ type: RuntimeTypes.SIMULATOR,
+ connect: function (connection) {
+ return this.simulator.launch().then(port => {
+ connection.host = "localhost";
+ connection.port = port;
+ connection.keepConnecting = true;
+ connection.once(Connection.Events.DISCONNECTED, e => this.simulator.kill());
+ connection.connect();
+ });
+ },
+ configure() {
+ Simulators.emit("configure", this.simulator);
+ },
+ get id() {
+ return this.simulator.id;
+ },
+ get name() {
+ return this.simulator.name;
+ },
+};
+
+// For testing use only
+exports._SimulatorRuntime = SimulatorRuntime;
+
+var gLocalRuntime = {
+ type: RuntimeTypes.LOCAL,
+ connect: function (connection) {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+ connection.host = null; // Force Pipe transport
+ connection.port = null;
+ connection.connect();
+ return promise.resolve();
+ },
+ get id() {
+ return "local";
+ },
+ get name() {
+ return Strings.GetStringFromName("local_runtime");
+ },
+};
+
+// For testing use only
+exports._gLocalRuntime = gLocalRuntime;
+
+var gRemoteRuntime = {
+ type: RuntimeTypes.REMOTE,
+ connect: function (connection) {
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (!win) {
+ return promise.reject(new Error("No WebIDE window found"));
+ }
+ let ret = {value: connection.host + ":" + connection.port};
+ let title = Strings.GetStringFromName("remote_runtime_promptTitle");
+ let message = Strings.GetStringFromName("remote_runtime_promptMessage");
+ let ok = Services.prompt.prompt(win, title, message, ret, null, {});
+ let [host, port] = ret.value.split(":");
+ if (!ok) {
+ return promise.reject({canceled: true});
+ }
+ if (!host || !port) {
+ return promise.reject(new Error("Invalid host or port"));
+ }
+ connection.host = host;
+ connection.port = port;
+ connection.connect();
+ return promise.resolve();
+ },
+ get name() {
+ return Strings.GetStringFromName("remote_runtime");
+ },
+};
+
+// For testing use only
+exports._gRemoteRuntime = gRemoteRuntime;