Bug 1090949 - Give WebIDE full control over simulator addons. r=ochameau f=jryans
authorJan Keromnes <janx@linux.com>
Fri, 13 Feb 2015 03:54:00 +0100
changeset 256162 2fa362f5b1b8f4ebdf0c4e9dd0f34018869e3f91
parent 256161 d522b57c3cb7282a513418e214083d5a8c1eab9b
child 256163 5bf16b55dd0d31705f30e63fd4c64ecbc0257e22
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1090949
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1090949 - Give WebIDE full control over simulator addons. r=ochameau f=jryans
browser/devtools/webide/modules/runtimes.js
browser/devtools/webide/modules/simulator-process.js
browser/devtools/webide/modules/simulators.js
browser/devtools/webide/moz.build
browser/devtools/webide/webide-prefs.js
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -1,18 +1,18 @@
 /* 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, 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");
 loader.lazyRequireGetter(this, "AuthenticationResult",
   "devtools/toolkit/security/auth", true);
 loader.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/toolkit/DevToolsUtils");
 
@@ -188,36 +188,36 @@ exports.RuntimeScanners = RuntimeScanner
 /* SCANNERS */
 
 let SimulatorScanner = {
 
   _runtimes: [],
 
   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() {
     this.emit("runtime-list-updated");
   },
 
   _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() {
     return promise.resolve();
   },
 
   listRuntimes: function() {
     return this._runtimes;
@@ -537,38 +537,36 @@ 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;
   },
 };
 
 // For testing use only
 exports._SimulatorRuntime = SimulatorRuntime;
 
 let gLocalRuntime = {
   type: RuntimeTypes.LOCAL,
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/modules/simulator-process.js
@@ -0,0 +1,272 @@
+/* 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) {
+      file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+      file.append("b2g");
+      let binaries = {
+        win32: "b2g-bin.exe",
+        mac64: "B2G.app/Contents/MacOS/b2g-bin",
+        linux32: "b2g-bin",
+        linux64: "b2g-bin",
+      };
+      binaries[OS].split("/").forEach(node => file.append(node));
+    }
+    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) {
+      file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+      let version = this.addon.name.match(/\d+\.\d+/)[0].replace(/\./, "_");
+      file.append("resources");
+      file.append("fxos_" + version + "_simulator");
+      file.append("data");
+      file.append(OS == "linux32" ? "linux" : OS);
+      let binaries = {
+        win32: "b2g/b2g-bin.exe",
+        mac64: "B2G.app/Contents/MacOS/b2g-bin",
+        linux32: "b2g/b2g-bin",
+        linux64: "b2g/b2g-bin",
+      };
+      binaries[OS].split("/").forEach(node => file.append(node));
+    }
+    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;
new file mode 100644
--- /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];
+  },
+};
--- a/browser/devtools/webide/moz.build
+++ b/browser/devtools/webide/moz.build
@@ -15,16 +15,18 @@ MOCHITEST_CHROME_MANIFESTS += ['test/chr
 
 EXTRA_JS_MODULES.devtools.webide += [
     'modules/addons.js',
     'modules/app-manager.js',
     'modules/build.js',
     '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'
 ]
 
 JS_PREFERENCE_FILES += [
     'webide-prefs.js',
 ]
 
--- a/browser/devtools/webide/webide-prefs.js
+++ b/browser/devtools/webide/webide-prefs.js
@@ -8,23 +8,25 @@ pref("devtools.webide.templatesURL", "ht
 pref("devtools.webide.autoinstallADBHelper", true);
 pref("devtools.webide.autoinstallFxdtAdapters", true);
 pref("devtools.webide.autoConnectRuntime", true);
 pref("devtools.webide.restoreLastProject", true);
 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");
 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);
 pref("devtools.webide.widget.inNavbarByDefault", true);
 #else
 pref("devtools.webide.widget.enabled", false);
 pref("devtools.webide.widget.inNavbarByDefault", false);
 #endif