Bug 731541. Implement webapp installer for Windows. r=bbondy a=blassey
authorFelipe Gomes <felipc@gmail.com>
Wed, 18 Apr 2012 15:17:54 -0700
changeset 91934 364a452825725e96dc5e9d3171b5a6ff63b2275e
parent 91933 0c61704a02ea8467dd81571b16d8a4e65ec96af2
child 91935 c04a467c48ac5c15d16da548f24203ef55df670e
push id22490
push usereakhgari@mozilla.com
push dateThu, 19 Apr 2012 13:58:35 +0000
treeherdermozilla-central@ee38392142ed [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbbondy, blassey
bugs731541
milestone14.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 731541. Implement webapp installer for Windows. r=bbondy a=blassey
browser/modules/WebappsInstaller.jsm
--- a/browser/modules/WebappsInstaller.jsm
+++ b/browser/modules/WebappsInstaller.jsm
@@ -17,17 +17,19 @@ let WebappsInstaller = {
    * Creates a native installation of the web app in the OS
    *
    * @param aData the manifest data provided by the web app
    *
    * @returns bool true on success, false if an error was thrown
    */
   install: function(aData) {
 
-#ifdef XP_MACOSX
+#ifdef XP_WIN
+    let shell = new WinNativeApp(aData);
+#elifdef XP_MACOSX
     let shell = new MacNativeApp(aData);
 #else
     return false;
 #endif
 
     try {
       shell.install();
     } catch (ex) {
@@ -97,17 +99,343 @@ function NativeApp(aData) {
   }
   this.shortDescription = sanitize(shortDesc);
 
   this.manifest = app.manifest;
 
   this.profileFolder = Services.dirsvc.get("ProfD", Ci.nsIFile);
 }
 
-#ifdef XP_MACOSX
+#ifdef XP_WIN
+/*************************************
+ * Windows app installer
+ *
+ * The Windows installation process will generate the following files:
+ *
+ * ${FolderName} = app-origin;protocol;port
+ *                 e.g.: subdomain.example.com;http;-1
+ *
+ * %APPDATA%/${FolderName}
+ *   - webapp.ini
+ *   - webapp.json
+ *   - ${AppName}.exe
+ *   - ${AppName}.lnk
+ *   / uninstall
+ *     - webapp-uninstaller.exe
+ *     - shortcut_logs.ini
+ *   / chrome/icons/default/
+ *     - topwindow.ico
+ *
+ * After the app runs for the first time, a profiles/ folder will also be
+ * created which will host the user profile for this app.
+ */
+
+/**
+ * Constructor for the Windows native app shell
+ *
+ * @param aData the data object provided by the web app with
+ *              all the app settings and specifications.
+ */
+function WinNativeApp(aData) {
+  NativeApp.call(this, aData);
+  this._init();
+}
+
+WinNativeApp.prototype = {
+  /**
+   * Install the app in the system by creating the folder structure,
+   *
+   */
+  install: function() {
+    // Remove previously installed app (for update purposes)
+    this._removeInstallation();
+
+    try {
+      this._createDirectoryStructure();
+      this._copyPrebuiltFiles();
+      this._createConfigFiles();
+      this._createShortcutFiles();
+      this._writeSystemKeys();
+    } catch (ex) {
+      this._removeInstallation();
+      throw(ex);
+    }
+
+    getIconForApp(this, function() {});
+  },
+
+  /**
+   * Initializes properties that will be used during the installation process,
+   * such as paths and filenames.
+   */
+  _init: function() {
+    let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi");
+
+    this.appNameAsFilename = this.appNameAsFilename.replace(filenameRE, "");
+    if (this.appNameAsFilename == "") {
+      this.appNameAsFilename = "webapp";
+    }
+
+    // The ${InstallDir} format is as follows:
+    //  host of the app origin + ";" +
+    //  protocol + ";" +
+    //  port (-1 for default port)
+    this.installDir = Services.dirsvc.get("AppData", Ci.nsIFile);
+    this.installDir.append(this.launchURI.host + ";" + 
+                           this.launchURI.scheme + ";" +
+                           this.launchURI.port);
+
+    this.uninstallDir = this.installDir.clone();
+    this.uninstallDir.append("uninstall");
+
+    this.uninstallerFile = this.installDir.clone();
+    this.uninstallerFile.append("webapp-uninstaller.exe");
+
+    this.iconFile = this.installDir.clone();
+    this.iconFile.append("chrome");
+    this.iconFile.append("icons");
+    this.iconFile.append("default");
+    this.iconFile.append("topwindow.ico");
+
+    this.processFolder = Services.dirsvc.get("CurProcD", Ci.nsIFile);
+
+    this.desktopShortcut = Services.dirsvc.get("Desk", Ci.nsILocalFile);
+    this.desktopShortcut.append(this.appNameAsFilename + ".lnk");
+    this.desktopShortcut.followLinks = false;
+
+    this.startMenuShortcut = Services.dirsvc.get("Progs", Ci.nsILocalFile);
+    this.startMenuShortcut.append(this.appNameAsFilename + ".lnk");
+    this.startMenuShortcut.followLinks = false;
+
+    this.uninstallSubkeyStr = this.launchURI.scheme + "://" +
+                              this.launchURI.host + ":" +
+                              this.launchURI.port;
+  },
+
+  /**
+   * Remove the current installation
+   */
+  _removeInstallation : function() {
+    let uninstallKey;
+    try {
+      uninstallKey = Cc["@mozilla.org/windows-registry-key;1"]
+                     .createInstance(Ci.nsIWindowsRegKey);
+      uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER,
+                        "SOFTWARE\\Microsoft\\Windows\\" +
+                        "CurrentVersion\\Uninstall",
+                        uninstallKey.ACCESS_WRITE);
+      if(uninstallKey.hasChild(this.uninstallSubkeyStr)) {
+        uninstallKey.removeChild(this.uninstallSubkeyStr);
+      }
+    } finally {
+      if(uninstallKey)
+        uninstallKey.close();
+    }
+
+    try {
+      if(this.installDir.exists()) {
+        let dir = this.installDir.QueryInterface(Ci.nsILocalFile);
+        // We need to set followLinks to false so that the shortcut
+        // files can be removed even after the .exe it was pointing
+        // to was removed.
+        dir.followLinks = false;
+        dir.remove(true);
+      }
+    } catch(ex) {
+    }
+
+    try {
+      if(this.desktopShortcut && this.desktopShortcut.exists()) {
+        this.desktopShortcut.remove(false);
+      }
+
+      if(this.startMenuShortcut && this.startMenuShortcut.exists()) {
+        this.startMenuShortcut.remove(false);
+      }
+    } catch(ex) {
+    }
+  },
+
+  /**
+   * Creates the main directory structure.
+   */
+  _createDirectoryStructure: function() {
+    this.installDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+    this.uninstallDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+  },
+
+  /**
+   * Copy the pre-built files into their destination folders.
+   */
+  _copyPrebuiltFiles: function() {
+    let webapprt = this.processFolder.clone();
+    webapprt.append("webapprt-stub.exe");
+    webapprt.copyTo(this.installDir, this.appNameAsFilename + ".exe");
+
+    let uninstaller = this.processFolder.clone();
+    uninstaller.append("webapp-uninstaller.exe");
+    uninstaller.copyTo(this.uninstallDir, this.uninstallerFile.leafName);
+  },
+
+  /**
+   * Creates the configuration files into their destination folders.
+   */
+  _createConfigFiles: function() {
+    // ${InstallDir}/webapp.json
+    let json = {
+      "registryDir": this.profileFolder.path,
+      "app": this.app
+    };
+
+    let configJson = this.installDir.clone();
+    configJson.append("webapp.json");
+    writeToFile(configJson, JSON.stringify(json), function() {});
+
+    // ${InstallDir}/webapp.ini
+    let webappINI = this.installDir.clone().QueryInterface(Ci.nsILocalFile);
+    webappINI.append("webapp.ini");
+
+    let factory = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]
+                    .getService(Ci.nsIINIParserFactory);
+
+    let writer = factory.createINIParser(webappINI).QueryInterface(Ci.nsIINIParserWriter);
+    writer.setString("Webapp", "Name", this.appName);
+    writer.setString("Webapp", "Profile", this.installDir.leafName);
+    writer.setString("Webapp", "Executable", this.appNameAsFilename + ".exe");
+    writer.setString("WebappRT", "InstallDir", this.processFolder.path);
+    writer.setString("Branding", "BrandFullName", this.appName);
+    writer.setString("Branding", "BrandShortName", this.appName);
+    writer.writeFile();
+
+    // ${UninstallDir}/shortcut_logs.ini
+    let shortcutLogsINI = this.uninstallDir.clone().QueryInterface(Ci.nsILocalFile);
+    shortcutLogsINI.append("shortcut_logs.ini");
+
+    writer = factory.createINIParser(shortcutLogsINI).QueryInterface(Ci.nsIINIParserWriter);
+    writer.setString("STARTMENU", "Shortcut", this.appNameAsFilename + ".lnk");
+    writer.setString("DESKTOP", "Shortcut", this.appNameAsFilename + ".lnk");
+    writer.setString("TASKBAR", "Migrated", "true");
+
+    writer = null;
+    factory = null;
+
+    // ${UninstallDir}/uninstall.log
+    let uninstallContent = 
+      "File: \\webapp.ini\r\n" +
+      "File: \\webapp.json\r\n" +
+      "File: \\webapprt.old\r\n" +
+      "File: \\chrome\\icons\\default\\topwindow.ico";
+    let uninstallLog = this.uninstallDir.clone();
+    uninstallLog.append("uninstall.log");
+    writeToFile(uninstallLog, uninstallContent, function() {});
+  },
+
+  /**
+   * Writes the keys to the system registry that are necessary for the app operation
+   * and uninstall process.
+   */
+  _writeSystemKeys: function() {
+    let parentKey;
+    let uninstallKey;
+    let subKey;
+
+    try {
+      parentKey = Cc["@mozilla.org/windows-registry-key;1"]
+                  .createInstance(Ci.nsIWindowsRegKey);
+      parentKey.open(parentKey.ROOT_KEY_CURRENT_USER,
+                     "SOFTWARE\\Microsoft\\Windows\\CurrentVersion",
+                     parentKey.ACCESS_WRITE);
+      uninstallKey = parentKey.createChild("Uninstall", parentKey.ACCESS_WRITE)
+      subKey = uninstallKey.createChild(this.uninstallSubkeyStr, uninstallKey.ACCESS_WRITE);
+
+      subKey.writeStringValue("DisplayName", this.appName);
+
+      subKey.writeStringValue("UninstallString", this.uninstallerFile.path);
+      subKey.writeStringValue("InstallLocation", this.installDir.path);
+      subKey.writeStringValue("AppFilename", this.appNameAsFilename);
+
+      if(this.iconFile) {
+        subKey.writeStringValue("DisplayIcon", this.iconFile.path);
+      }
+
+      subKey.writeIntValue("NoModify", 1);
+      subKey.writeIntValue("NoRepair", 1);
+    } catch(ex) {
+      throw(ex);
+    } finally {
+      if(subKey) subKey.close();
+      if(uninstallKey) uninstallKey.close();
+      if(parentKey) parentKey.close();
+    }
+  },
+
+  /**
+   * Creates a shortcut file inside the app installation folder and makes
+   * two copies of it: one into the desktop and one into the start menu.
+   */
+  _createShortcutFiles: function() {
+    let shortcut = this.installDir.clone().QueryInterface(Ci.nsILocalFileWin);
+    shortcut.append(this.appNameAsFilename + ".lnk");
+
+    let target = this.installDir.clone();
+    target.append(this.appNameAsFilename + ".exe");
+
+    /* function nsILocalFileWin.setShortcut(targetFile, workingDir, args,
+                                            description, iconFile, iconIndex) */
+
+    shortcut.setShortcut(target, this.installDir.clone(), null,
+                         this.shortDescription, this.iconFile, 0);
+
+    let desktop = Services.dirsvc.get("Desk", Ci.nsILocalFile);
+    let startMenu = Services.dirsvc.get("Progs", Ci.nsILocalFile);
+
+    shortcut.copyTo(desktop, this.appNameAsFilename + ".lnk");
+    shortcut.copyTo(startMenu, this.appNameAsFilename + ".lnk");
+  },
+
+  /**
+   * This variable specifies if the icon retrieval process should
+   * use a temporary file in the system or a binary stream. This
+   * is accessed by a common function in WebappsIconHelpers.js and
+   * is different for each platform.
+   */
+  useTmpForIcon: false,
+
+  /**
+   * Process the icon from the imageStream as retrieved from
+   * the URL by getIconForApp(). This will save the icon to the
+   * topwindow.ico file.
+   *
+   * @param aMimeType     ahe icon mimetype
+   * @param aImageStream  the stream for the image data
+   * @param aCallback     a callback function to be called
+   *                      after the process finishes
+   */
+  processIcon: function(aMimeType, aImageStream, aCallback) {
+    let iconStream;
+    try {
+      let imgTools = Cc["@mozilla.org/image/tools;1"]
+                       .createInstance(Ci.imgITools);
+      let imgContainer = { value: null };
+
+      imgTools.decodeImageData(aImageStream, aMimeType, imgContainer);
+      iconStream = imgTools.encodeImage(imgContainer.value,
+                                        "image/vnd.microsoft.icon",
+                                        "format=bmp;bpp=32");
+    } catch (e) {
+      throw("processIcon - Failure converting icon (" + e + ")");
+    }
+
+    this.iconFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+    let outputStream = FileUtils.openSafeFileOutputStream(this.iconFile);
+    NetUtil.asyncCopy(iconStream, outputStream);
+  }
+}
+
+#elifdef XP_MACOSX
 
 function MacNativeApp(aData) {
   NativeApp.call(this, aData);
   this._init();
 }
 
 MacNativeApp.prototype = {
   _init: function() {