diff --git a/browser/devtools/webide/modules/runtimes.js b/browser/devtools/webide/modules/runtimes.js index d6d828cc31d..f6c3cf7ef20 100644 --- a/browser/devtools/webide/modules/runtimes.js +++ b/browser/devtools/webide/modules/runtimes.js @@ -5,9 +5,9 @@ const {Cu, Ci} = require("chrome"); const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm"); const {Services} = Cu.import("resource://gre/modules/Services.jsm"); -const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm"); -const {ConnectionManager, Connection} = require("devtools/client/connection-manager"); +const {Connection} = require("devtools/client/connection-manager"); const {DebuggerServer} = require("resource://gre/modules/devtools/dbg-server.jsm"); +const {Simulators} = require("devtools/webide/simulators"); const discovery = require("devtools/toolkit/discovery/discovery"); const EventEmitter = require("devtools/toolkit/event-emitter"); const promise = require("promise"); @@ -193,14 +193,12 @@ let SimulatorScanner = { enable() { this._updateRuntimes = this._updateRuntimes.bind(this); - Simulator.on("register", this._updateRuntimes); - Simulator.on("unregister", this._updateRuntimes); + Simulators.on("updated", this._updateRuntimes); this._updateRuntimes(); }, disable() { - Simulator.off("register", this._updateRuntimes); - Simulator.off("unregister", this._updateRuntimes); + Simulators.off("updated", this._updateRuntimes); }, _emitUpdated() { @@ -208,11 +206,13 @@ let SimulatorScanner = { }, _updateRuntimes() { - this._runtimes = []; - for (let name of Simulator.availableNames()) { - this._runtimes.push(new SimulatorRuntime(name)); - } - this._emitUpdated(); + Simulators.getAll().then(simulators => { + this._runtimes = []; + for (let simulator of simulators) { + this._runtimes.push(new SimulatorRuntime(simulator)); + } + this._emitUpdated(); + }); }, scan() { @@ -542,28 +542,26 @@ WiFiRuntime.prototype = { // For testing use only exports._WiFiRuntime = WiFiRuntime; -function SimulatorRuntime(name) { - this.name = name; +function SimulatorRuntime(simulator) { + this.simulator = simulator; } SimulatorRuntime.prototype = { type: RuntimeTypes.SIMULATOR, connect: function(connection) { - let port = ConnectionManager.getFreeTCPPort(); - let simulator = Simulator.getByName(this.name); - if (!simulator || !simulator.launch) { - return promise.reject(new Error("Can't find simulator: " + this.name)); - } - return simulator.launch({port: port}).then(() => { + return this.simulator.launch().then(port => { connection.host = "localhost"; connection.port = port; connection.keepConnecting = true; - connection.once(Connection.Events.DISCONNECTED, simulator.close); + connection.once(Connection.Events.DISCONNECTED, e => this.simulator.kill()); connection.connect(); }); }, get id() { - return this.name; + return this.simulator.id; + }, + get name() { + return this.simulator.name; }, }; diff --git a/browser/devtools/webide/modules/simulator-process.js b/browser/devtools/webide/modules/simulator-process.js new file mode 100644 index 00000000000..0ebae120b0e --- /dev/null +++ b/browser/devtools/webide/modules/simulator-process.js @@ -0,0 +1,273 @@ +/* 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 { Cc, Ci, Cu } = require("chrome"); + +const Environment = require("sdk/system/environment").env; +const Subprocess = require("sdk/system/child_process/subprocess"); +const { EventEmitter } = Cu.import("resource://gre/modules/devtools/event-emitter.js", {}); +const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +let platform = Services.appShell.hiddenDOMWindow.navigator.platform; +let OS = ""; +if (platform.indexOf("Win") != -1) { + OS = "win32"; +} else if (platform.indexOf("Mac") != -1) { + OS = "mac64"; +} else if (platform.indexOf("Linux") != -1) { + if (platform.indexOf("x86_64") != -1) { + OS = "linux64"; + } else { + OS = "linux32"; + } +} + +function SimulatorProcess() {} +SimulatorProcess.prototype = { + + // Check if B2G is running. + get isRunning() !!this.process, + + // Start the process and connect the debugger client. + run() { + + // Resolve B2G binary. + let b2g = this.b2gBinary; + if (!b2g || !b2g.exists()) { + throw Error("B2G executable not found."); + } + + this.once("stdout", function () { + if (OS == "mac64") { + console.debug("WORKAROUND run osascript to show b2g-desktop window on OS=='mac64'"); + // Escape double quotes and escape characters for use in AppleScript. + let path = b2g.path.replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); + + Subprocess.call({ + command: "/usr/bin/osascript", + arguments: ["-e", 'tell application "' + path + '" to activate'], + }); + } + }); + + this.on("stdout", (e, data) => this.log(e, data.trim())); + this.on("stderr", (e, data) => this.log(e, data.trim())); + + let environment; + if (OS.indexOf("linux") > -1) { + environment = ["TMPDIR=" + Services.dirsvc.get("TmpD", Ci.nsIFile).path]; + if ("DISPLAY" in Environment) { + environment.push("DISPLAY=" + Environment.DISPLAY); + } + } + + // Spawn a B2G instance. + this.process = Subprocess.call({ + command: b2g, + arguments: this.args, + environment: environment, + stdout: data => this.emit("stdout", data), + stderr: data => this.emit("stderr", data), + // On B2G instance exit, reset tracked process, remote debugger port and + // shuttingDown flag, then finally emit an exit event. + done: result => { + console.log("B2G terminated with " + result.exitCode); + this.process = null; + this.emit("exit", result.exitCode); + } + }); + }, + + // Request a B2G instance kill. + kill() { + let deferred = promise.defer(); + if (this.process) { + this.once("exit", (e, exitCode) => { + this.shuttingDown = false; + deferred.resolve(exitCode); + }); + if (!this.shuttingDown) { + this.shuttingDown = true; + this.emit("kill", null); + this.process.kill(); + } + return deferred.promise; + } else { + return promise.resolve(undefined); + } + }, + + // Maybe log output messages. + log(level, message) { + if (!Services.prefs.getBoolPref("devtools.webide.logSimulatorOutput")) { + return; + } + if (level === "stderr" || level === "error") { + console.error(message); + return; + } + console.log(message); + }, + + // Compute B2G CLI arguments. + get args() { + let args = []; + + let gaia = this.gaiaProfile; + if (!gaia || !gaia.exists()) { + throw Error("Gaia profile directory not found."); + } + args.push("-profile", gaia.path); + + args.push("-start-debugger-server", "" + this.options.port); + + // Ignore eventual zombie instances of b2g that are left over. + args.push("-no-remote"); + + return args; + }, +}; + +EventEmitter.decorate(SimulatorProcess.prototype); + + +function CustomSimulatorProcess(options) { + this.options = options; +} + +let CSPp = CustomSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype); + +// Compute B2G binary file handle. +Object.defineProperty(CSPp, "b2gBinary", { + get: function() { + let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile); + file.initWithPath(this.options.b2gBinary); + return file; + } +}); + +// Compute Gaia profile file handle. +Object.defineProperty(CSPp, "gaiaProfile", { + get: function() { + let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile); + file.initWithPath(this.options.gaiaProfile); + return file; + } +}); + +exports.CustomSimulatorProcess = CustomSimulatorProcess; + + +function AddonSimulatorProcess(addon, options) { + this.addon = addon; + this.options = options; +} + +let ASPp = AddonSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype); + +// Compute B2G binary file handle. +Object.defineProperty(ASPp, "b2gBinary", { + get: function() { + let file; + try { + let pref = "extensions." + this.addon.id + ".customRuntime"; + file = Services.prefs.getComplexValue(pref, Ci.nsIFile); + } catch(e) {} + + if (!file) { + let binaries = { + win32: "b2g-bin.exe", + mac64: "B2G.app/Contents/MacOS/b2g-bin", + linux32: "b2g-bin", + linux64: "b2g-bin", + }; + file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file; + file.append("b2g"); + file.append(binaries[OS]); + } + return file; + } +}); + +// Compute Gaia profile file handle. +Object.defineProperty(ASPp, "gaiaProfile", { + get: function() { + let file; + try { + let pref = "extensions." + this.addon.id + ".gaiaProfile"; + file = Services.prefs.getComplexValue(pref, Ci.nsIFile); + } catch(e) {} + + if (!file) { + file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file; + file.append("profile"); + } + return file; + } +}); + +exports.AddonSimulatorProcess = AddonSimulatorProcess; + + +function OldAddonSimulatorProcess(addon, options) { + this.addon = addon; + this.options = options; +} + +let OASPp = OldAddonSimulatorProcess.prototype = Object.create(AddonSimulatorProcess.prototype); + +// Compute B2G binary file handle. +Object.defineProperty(OASPp, "b2gBinary", { + get: function() { + let file; + try { + let pref = "extensions." + this.addon.id + ".customRuntime"; + file = Services.prefs.getComplexValue(pref, Ci.nsIFile); + } catch(e) {} + + if (!file) { + let version = this.addon.name.match(/\d+\.\d+/)[0].replace(/\./, "_"); + file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file; + file.append("resources"); + file.append("fxos_" + version + "_simulator"); + file.append("data"); + file.append(OS == "linux32" ? "linux" : OS); + if (OS == "mac64") { + file.append("B2G.app"); + file.append("Contents"); + file.append("MacOS"); + } else { + file.append("b2g"); + } + file.append("b2g-bin" + (OS == "win32" ? ".exe" : "")); + } + return file; + } +}); + +// Compute B2G CLI arguments. +Object.defineProperty(OASPp, "args", { + get: function() { + let args = []; + + let gaia = this.gaiaProfile; + if (!gaia || !gaia.exists()) { + throw Error("Gaia profile directory not found."); + } + args.push("-profile", gaia.path); + + args.push("-dbgport", "" + this.options.port); + + // Ignore eventual zombie instances of b2g that are left over. + args.push("-no-remote"); + + return args; + } +}); + +exports.OldAddonSimulatorProcess = OldAddonSimulatorProcess; diff --git a/browser/devtools/webide/modules/simulators.js b/browser/devtools/webide/modules/simulators.js new file mode 100644 index 00000000000..2e05e91e615 --- /dev/null +++ b/browser/devtools/webide/modules/simulators.js @@ -0,0 +1,96 @@ +/* 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/. */ + +const { Cu } = require("chrome"); +const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm"); +const { EventEmitter } = Cu.import("resource://gre/modules/devtools/event-emitter.js"); +const { ConnectionManager } = require("devtools/client/connection-manager"); +const { AddonSimulatorProcess, OldAddonSimulatorProcess } = require("devtools/webide/simulator-process"); +const promise = require("promise"); + +const SimulatorRegExp = new RegExp(Services.prefs.getCharPref("devtools.webide.simulatorAddonRegExp")); + +let Simulators = { + // TODO (Bug 1090949) Don't generate this list from installed simulator + // addons, but instead implement a persistent list of user-configured + // simulators. + getAll() { + let deferred = promise.defer(); + AddonManager.getAllAddons(addons => { + let simulators = []; + for (let addon of addons) { + if (SimulatorRegExp.exec(addon.id)) { + simulators.push(new Simulator(addon)); + } + } + // Sort simulators alphabetically by name. + simulators.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + }); + deferred.resolve(simulators); + }); + return deferred.promise; + }, +} +EventEmitter.decorate(Simulators); +exports.Simulators = Simulators; + +function update() { + Simulators.emit("updated"); +} +AddonManager.addAddonListener({ + onEnabled: update, + onDisabled: update, + onInstalled: update, + onUninstalled: update +}); + + +function Simulator(addon) { + this.addon = addon; +} + +Simulator.prototype = { + launch() { + // Close already opened simulation. + if (this.process) { + return this.kill().then(this.launch.bind(this)); + } + + let options = { + port: ConnectionManager.getFreeTCPPort() + }; + + if (this.version <= "1.3") { + // Support older simulator addons. + this.process = new OldAddonSimulatorProcess(this.addon, options); + } else { + this.process = new AddonSimulatorProcess(this.addon, options); + } + this.process.run(); + + return promise.resolve(options.port); + }, + + kill() { + let process = this.process; + if (!process) { + return promise.resolve(); + } + this.process = null; + return process.kill(); + }, + + get id() { + return this.addon.id; + }, + + get name() { + return this.addon.name.replace(" Simulator", ""); + }, + + get version() { + return this.name.match(/\d+\.\d+/)[0]; + }, +}; diff --git a/browser/devtools/webide/moz.build b/browser/devtools/webide/moz.build index e9804d09ca6..20a072793a9 100644 --- a/browser/devtools/webide/moz.build +++ b/browser/devtools/webide/moz.build @@ -20,6 +20,8 @@ EXTRA_JS_MODULES.devtools.webide += [ 'modules/config-view.js', 'modules/remote-resources.js', 'modules/runtimes.js', + 'modules/simulator-process.js', + 'modules/simulators.js', 'modules/tab-store.js', 'modules/utils.js' ] diff --git a/browser/devtools/webide/webide-prefs.js b/browser/devtools/webide/webide-prefs.js index 73a674449e9..6e21bec8b14 100644 --- a/browser/devtools/webide/webide-prefs.js +++ b/browser/devtools/webide/webide-prefs.js @@ -13,6 +13,7 @@ pref("devtools.webide.enableLocalRuntime", false); pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json"); pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi"); pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org"); +pref("devtools.webide.simulatorAddonRegExp", "fxos_(.*)_simulator@mozilla\.org$"); pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi"); pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org"); pref("devtools.webide.adaptersAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxdt-adapters/#OS#/fxdt-adapters-#OS#-latest.xpi"); @@ -20,6 +21,7 @@ pref("devtools.webide.adaptersAddonID", "fxdevtools-adapters@mozilla.org"); pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000"); pref("devtools.webide.lastConnectedRuntime", ""); pref("devtools.webide.lastSelectedProject", ""); +pref("devtools.webide.logSimulatorOutput", false); pref("devtools.webide.widget.autoinstall", true); #ifdef MOZ_DEV_EDITION pref("devtools.webide.widget.enabled", true);