toolkit/webapps/MacNativeApp.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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 file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 const USER_LIB_DIR = OS.Constants.Path.macUserLibDir;
     6 const LOCAL_APP_DIR = OS.Constants.Path.macLocalApplicationsDir;
     8 /**
     9  * Constructor for the Mac native app shell
    10  *
    11  * @param aApp {Object} the app object provided to the install function
    12  * @param aManifest {Object} the manifest data provided by the web app
    13  * @param aCategories {Array} array of app categories
    14  * @param aRegistryDir {String} (optional) path to the registry
    15  */
    16 function NativeApp(aApp, aManifest, aCategories, aRegistryDir) {
    17   CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir);
    19   // The ${ProfileDir} is: sanitized app name + "-" + manifest url hash
    20   this.appProfileDir = OS.Path.join(USER_LIB_DIR, "Application Support",
    21                                     this.uniqueName);
    22   this.configJson = "webapp.json";
    24   this.contentsDir = "Contents";
    25   this.macOSDir = OS.Path.join(this.contentsDir, "MacOS");
    26   this.resourcesDir = OS.Path.join(this.contentsDir, "Resources");
    27   this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns");
    28   this.zipFile = OS.Path.join(this.resourcesDir, "application.zip");
    29 }
    31 NativeApp.prototype = {
    32   __proto__: CommonNativeApp.prototype,
    33   /*
    34    * The _rootInstallDir property is the path of the directory where we install
    35    * apps. In production code, it's "/Applications". In tests, it's
    36    * "~/Applications" because on build machines we don't have enough privileges
    37    * to write to the global "/Applications" directory.
    38    */
    39   _rootInstallDir: LOCAL_APP_DIR,
    41   /**
    42    * Creates a native installation of the web app in the OS
    43    *
    44    * @param aManifest {Object} the manifest data provided by the web app
    45    * @param aZipPath {String} path to the zip file for packaged apps (undefined
    46    *                          for hosted apps)
    47    */
    48   install: Task.async(function*(aManifest, aZipPath) {
    49     if (this._dryRun) {
    50       return;
    51     }
    53     // If the application is already installed, this is a reinstallation.
    54     if (WebappOSUtils.getInstallPath(this.app)) {
    55       return yield this.prepareUpdate(aManifest, aZipPath);
    56     }
    58     this._setData(aManifest);
    60     let localAppDir = getFile(this._rootInstallDir);
    61     if (!localAppDir.isWritable()) {
    62       throw("Not enough privileges to install apps");
    63     }
    65     let destinationName = yield getAvailableFileName([ this._rootInstallDir ],
    66                                                      this.appNameAsFilename,
    67                                                      ".app");
    69     let installDir = OS.Path.join(this._rootInstallDir, destinationName);
    71     let dir = getFile(TMP_DIR, this.appNameAsFilename + ".app");
    72     dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
    73     let tmpDir = dir.path;
    75     try {
    76       yield this._createDirectoryStructure(tmpDir);
    77       this._copyPrebuiltFiles(tmpDir);
    78       yield this._createConfigFiles(tmpDir);
    80       if (aZipPath) {
    81         yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile));
    82       }
    84       yield this._getIcon(tmpDir);
    85     } catch (ex) {
    86       yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
    87       throw ex;
    88     }
    90     this._removeInstallation(true, installDir);
    92     try {
    93       // Move the temp installation directory to the /Applications directory
    94       yield this._applyTempInstallation(tmpDir, installDir);
    95     } catch (ex) {
    96       this._removeInstallation(false, installDir);
    97       yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
    98       throw ex;
    99     }
   100   }),
   102   /**
   103    * Creates an update in a temporary directory to be applied later.
   104    *
   105    * @param aManifest {Object} the manifest data provided by the web app
   106    * @param aZipPath {String} path to the zip file for packaged apps (undefined
   107    *                          for hosted apps)
   108    */
   109   prepareUpdate: Task.async(function*(aManifest, aZipPath) {
   110     if (this._dryRun) {
   111       return;
   112     }
   114     this._setData(aManifest);
   116     let [ oldUniqueName, installDir ] = WebappOSUtils.getLaunchTarget(this.app);
   117     if (!installDir) {
   118       throw ERR_NOT_INSTALLED;
   119     }
   121     if (this.uniqueName != oldUniqueName) {
   122       // Bug 919799: If the app is still in the registry, migrate its data to
   123       // the new format.
   124       throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME;
   125     }
   127     let updateDir = OS.Path.join(installDir, "update");
   128     yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
   129     yield OS.File.makeDir(updateDir);
   131     try {
   132       yield this._createDirectoryStructure(updateDir);
   133       this._copyPrebuiltFiles(updateDir);
   134       yield this._createConfigFiles(updateDir);
   136       if (aZipPath) {
   137         yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile));
   138       }
   140       yield this._getIcon(updateDir);
   141     } catch (ex) {
   142       yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
   143       throw ex;
   144     }
   145   }),
   147   /**
   148    * Applies an update.
   149    */
   150   applyUpdate: Task.async(function*() {
   151     if (this._dryRun) {
   152       return;
   153     }
   155     let installDir = WebappOSUtils.getInstallPath(this.app);
   156     let updateDir = OS.Path.join(installDir, "update");
   158     let backupDir = yield this._backupInstallation(installDir);
   160     try {
   161       // Move the update directory to the /Applications directory
   162       yield this._applyTempInstallation(updateDir, installDir);
   163     } catch (ex) {
   164       yield this._restoreInstallation(backupDir, installDir);
   165       throw ex;
   166     } finally {
   167       yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
   168       yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
   169     }
   170   }),
   172   _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) {
   173     yield OS.File.move(OS.Path.join(aTmpDir, this.configJson),
   174                        OS.Path.join(this.appProfileDir, this.configJson));
   176     yield moveDirectory(aTmpDir, aInstallDir);
   177   }),
   179   _removeInstallation: function(keepProfile, aInstallDir) {
   180     let filesToRemove = [ aInstallDir ];
   182     if (!keepProfile) {
   183       filesToRemove.push(this.appProfileDir);
   184     }
   186     removeFiles(filesToRemove);
   187   },
   189   _backupInstallation: Task.async(function*(aInstallDir) {
   190     let backupDir = OS.Path.join(aInstallDir, "backup");
   191     yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
   192     yield OS.File.makeDir(backupDir);
   194     yield moveDirectory(OS.Path.join(aInstallDir, this.contentsDir),
   195                         backupDir);
   196     yield OS.File.move(OS.Path.join(this.appProfileDir, this.configJson),
   197                        OS.Path.join(backupDir, this.configJson));
   199     return backupDir;
   200   }),
   202   _restoreInstallation: Task.async(function*(aBackupDir, aInstallDir) {
   203     yield OS.File.move(OS.Path.join(aBackupDir, this.configJson),
   204                        OS.Path.join(this.appProfileDir, this.configJson));
   205     yield moveDirectory(aBackupDir,
   206                         OS.Path.join(aInstallDir, this.contentsDir));
   207   }),
   209   _createDirectoryStructure: Task.async(function*(aDir) {
   210     yield OS.File.makeDir(this.appProfileDir,
   211                           { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
   213     yield OS.File.makeDir(OS.Path.join(aDir, this.contentsDir),
   214                           { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
   216     yield OS.File.makeDir(OS.Path.join(aDir, this.macOSDir),
   217                           { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
   219     yield OS.File.makeDir(OS.Path.join(aDir, this.resourcesDir),
   220                           { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
   221   }),
   223   _copyPrebuiltFiles: function(aDir) {
   224     let destDir = getFile(aDir, this.macOSDir);
   225     let stub = getFile(this.runtimeFolder, "webapprt-stub");
   226     stub.copyTo(destDir, "webapprt");
   227   },
   229   _createConfigFiles: function(aDir) {
   230     // ${ProfileDir}/webapp.json
   231     yield writeToFile(OS.Path.join(aDir, this.configJson),
   232                       JSON.stringify(this.webappJson));
   234     // ${InstallDir}/Contents/MacOS/webapp.ini
   235     let applicationINI = getFile(aDir, this.macOSDir, "webapp.ini");
   237     let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
   238                  getService(Ci.nsIINIParserFactory).
   239                  createINIParser(applicationINI).
   240                  QueryInterface(Ci.nsIINIParserWriter);
   241     writer.setString("Webapp", "Name", this.appName);
   242     writer.setString("Webapp", "Profile", this.uniqueName);
   243     writer.writeFile();
   244     applicationINI.permissions = PERMS_FILE;
   246     // ${InstallDir}/Contents/Info.plist
   247     let infoPListContent = '<?xml version="1.0" encoding="UTF-8"?>\n\
   248 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n\
   249 <plist version="1.0">\n\
   250   <dict>\n\
   251     <key>CFBundleDevelopmentRegion</key>\n\
   252     <string>English</string>\n\
   253     <key>CFBundleDisplayName</key>\n\
   254     <string>' + escapeXML(this.appName) + '</string>\n\
   255     <key>CFBundleExecutable</key>\n\
   256     <string>webapprt</string>\n\
   257     <key>CFBundleIconFile</key>\n\
   258     <string>appicon</string>\n\
   259     <key>CFBundleIdentifier</key>\n\
   260     <string>' + escapeXML(this.uniqueName) + '</string>\n\
   261     <key>CFBundleInfoDictionaryVersion</key>\n\
   262     <string>6.0</string>\n\
   263     <key>CFBundleName</key>\n\
   264     <string>' + escapeXML(this.appName) + '</string>\n\
   265     <key>CFBundlePackageType</key>\n\
   266     <string>APPL</string>\n\
   267     <key>CFBundleVersion</key>\n\
   268     <string>0</string>\n\
   269     <key>NSHighResolutionCapable</key>\n\
   270     <true/>\n\
   271     <key>NSPrincipalClass</key>\n\
   272     <string>GeckoNSApplication</string>\n\
   273     <key>FirefoxBinary</key>\n\
   274 #expand     <string>__MOZ_MACBUNDLE_ID__</string>\n\
   275   </dict>\n\
   276 </plist>';
   278     yield writeToFile(OS.Path.join(aDir, this.contentsDir, "Info.plist"),
   279                       infoPListContent);
   280   },
   282   /**
   283    * Process the icon from the imageStream as retrieved from
   284    * the URL by getIconForApp(). This will bundle the icon to the
   285    * app package at Contents/Resources/appicon.icns.
   286    *
   287    * @param aMimeType     the icon mimetype
   288    * @param aImageStream  the stream for the image data
   289    * @param aDir          the directory where the icon should be stored
   290    */
   291   _processIcon: function(aMimeType, aIcon, aDir) {
   292     let deferred = Promise.defer();
   294     function conversionDone(aSubject, aTopic) {
   295       if (aTopic == "process-finished") {
   296         deferred.resolve();
   297       } else {
   298         deferred.reject("Failure converting icon, exit code: " + aSubject.exitValue);
   299       }
   300     }
   302     let process = Cc["@mozilla.org/process/util;1"].
   303                   createInstance(Ci.nsIProcess);
   304     let sipsFile = getFile("/usr/bin/sips");
   306     process.init(sipsFile);
   307     process.runAsync(["-s", "format", "icns",
   308                       aIcon.path,
   309                       "--out", OS.Path.join(aDir, this.iconFile),
   310                       "-z", "128", "128"],
   311                       9, conversionDone);
   313     return deferred.promise;
   314   }
   315 }

mercurial