Bug 785124 - Pt 2 - JS changes to updater to allow storing update.mar to sdcard. r=marshall_law
authorDave Hylands <dhylands@gmail.com>
Fri, 14 Dec 2012 16:05:39 -0800
changeset 118383 82c54850f723a76881fcff1d828f5c55c4d4f9d7
parent 118382 eea4fefd010bfeda7dcb422fe6e96051f0c4f05a
child 118384 4dabceec29a319914a1d663c1298a05631150fe3
push id24166
push userMs2ger@gmail.com
push dateFri, 11 Jan 2013 13:57:41 +0000
treeherdermozilla-central@63c4b0f66a0c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarshall_law
bugs785124
milestone21.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 785124 - Pt 2 - JS changes to updater to allow storing update.mar to sdcard. r=marshall_law From 93958fee051e9355930edba538eabeb91f4b442d Mon Sep 17 00:00:00 2001 sdcard and lock sdcard while in use --- b2g/components/DirectoryProvider.js | 154 ++++++++++++++++++++++++++--- b2g/components/UpdatePrompt.js | 16 ++- toolkit/mozapps/update/nsUpdateService.js | 124 ++++++++++++++++++++++- 3 files changed, 276 insertions(+), 18 deletions(-) * * * Fix log stmt
b2g/components/DirectoryProvider.js
b2g/components/UpdatePrompt.js
toolkit/mozapps/update/nsUpdateService.js
--- a/b2g/components/DirectoryProvider.js
+++ b/b2g/components/DirectoryProvider.js
@@ -6,22 +6,37 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 const XRE_OS_UPDATE_APPLY_TO_DIR = "OSUpdApplyToD"
+const UPDATE_ARCHIVE_DIR = "UpdArchD"
 const LOCAL_DIR = "/data/local";
 
 XPCOMUtils.defineLazyServiceGetter(Services, "env",
                                    "@mozilla.org/process/environment;1",
                                    "nsIEnvironment");
 
+XPCOMUtils.defineLazyServiceGetter(Services, "volumeService",
+                                   "@mozilla.org/telephony/volume-service;1",
+                                   "nsIVolumeService");
+
+XPCOMUtils.defineLazyGetter(this, "gExtStorage", function dp_gExtStorage() {
+    return Services.env.get("EXTERNAL_STORAGE");
+});
+
+const VERBOSE = 1;
+let log =
+  VERBOSE ?
+  function log_dump(msg) { dump("DirectoryProvider: " + msg + "\n"); } :
+  function log_noop(msg) { };
+
 function DirectoryProvider() {
 }
 
 DirectoryProvider.prototype = {
   classID: Components.ID("{9181eb7c-6f87-11e1-90b1-4f59d80dd2e5}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
 
@@ -30,51 +45,162 @@ DirectoryProvider.prototype = {
     let localProps = ["cachePDir", "webappsDir", "PrefD", "indexedDBPDir",
                       "permissionDBPDir", "UpdRootD"];
     if (localProps.indexOf(prop) != -1) {
       let file = Cc["@mozilla.org/file/local;1"]
                    .createInstance(Ci.nsILocalFile)
       file.initWithPath(LOCAL_DIR);
       persistent.value = true;
       return file;
-    } else if (prop == "coreAppsDir") {
+    }
+    if (prop == "coreAppsDir") {
       let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile)
       file.initWithPath("/system/b2g");
       persistent.value = true;
       return file;
-    } else if (prop == XRE_OS_UPDATE_APPLY_TO_DIR) {
-      return this.getOSUpdateApplyToDir(persistent);
+    }
+    if (prop == XRE_OS_UPDATE_APPLY_TO_DIR ||
+        prop == UPDATE_ARCHIVE_DIR) {
+      let file = this.getUpdateDir(persistent);
+      return file;
     }
 #endif
+    return null;
+  },
+
+  // The VolumeService only exists on the device, and not on desktop
+  volumeHasFreeSpace: function dp_volumeHasFreeSpace(volumePath, requiredSpace) {
+    if (!volumePath) {
+      return false;
+    }
+    if (!Services.volumeService) {
+      return false;
+    }
+    let volume = Services.volumeService.getVolumeByPath(volumePath);
+    if (!volume || volume.state !== Ci.nsIVolume.STATE_MOUNTED) {
+      return false;
+    }
+    let stat = volume.getStats();
+    if (!stat) {
+      return false;
+    }
+    return requiredSpace <= stat.freeBytes;
+  },
+
+  findUpdateDirWithFreeSpace: function dp_findUpdateDirWithFreeSpace(requiredSpace) {
+    if (!Services.volumeService) {
+      return this.createUpdatesDir(LOCAL_DIR);
+    }
+    let activeUpdate = Services.um.activeUpdate;
+    if (this.volumeHasFreeSpace(gExtStorage, requiredSpace)) {
+      let extUpdateDir = this.createUpdatesDir(gExtStorage);
+      if (extUpdateDir !== null) {
+        return extUpdateDir;
+      }
+      log("Warning: " + gExtStorage + " has enough free space for update " +
+          activeUpdate.name + ", but is not writable");
+    }
+
+    if (this.volumeHasFreeSpace(LOCAL_DIR, requiredSpace)) {
+      let localUpdateDir = this.createUpdatesDir(LOCAL_DIR);
+      if (localUpdateDir !== null) {
+        return localUpdateDir;
+      }
+      log("Warning: " + LOCAL_DIR + " has enough free space for update " +
+          activeUpdate.name + ", but is not writable");
+    }
 
     return null;
   },
 
-  getOSUpdateApplyToDir: function dp_getOSUpdateApplyToDir(persistent) {
-    // TODO add logic to check available storage space,
-    // and iterate through pref(s) to find alternative dirs if
-    // necessary.
+  getUpdateDir: function dp_getUpdateDir(persistent) {
+    let defaultUpdateDir = this.getDefaultUpdateDir();
+    persistent.value = false;
+
+    let activeUpdate = Services.um.activeUpdate;
+    if (!activeUpdate) {
+      log("Warning: No active update found, using default update dir: " +
+          defaultUpdateDir);
+      return defaultUpdateDir;
+    }
+
+    let selectedPatch = activeUpdate.selectedPatch;
+    if (!selectedPatch) {
+      log("Warning: No selected patch, using default update dir: " +
+          defaultUpdateDir);
+      return defaultUpdateDir;
+    }
+
+    let requiredSpace = selectedPatch.size * 2;
+    let updateDir = this.findUpdateDirWithFreeSpace(requiredSpace, persistent);
+    if (updateDir) {
+      return updateDir;
+    }
+
+    // If we've gotten this far, there isn't enough free space to download the patch
+    // on either external storage or /data/local. All we can do is report the
+    // error and let upstream code handle it more gracefully.
+    log("Error: No volume found with " + requiredSpace + " bytes for downloading"+
+        " update " + activeUpdate.name);
+    throw Cr.NS_ERROR_FILE_TOO_BIG;
+  },
 
-    let path = Services.env.get("EXTERNAL_STORAGE");
+  createUpdatesDir: function dp_createUpdatesDir(root) {
+      let dir = Cc["@mozilla.org/file/local;1"]
+                   .createInstance(Ci.nsILocalFile);
+      dir.initWithPath(root);
+      if (!dir.isWritable()) {
+        return null;
+      }
+      dir.appendRelativePath("updates/0");
+      if (dir.exists()) {
+        if (dir.isDirectory() && dir.isWritable()) {
+          return dir;
+        }
+        // updates/0 is either a file or isn't writable. In either case we
+        // can't use it.
+        log("Error: " + dir.path + " is a file or isn't writable");
+        return null;
+      }
+      // updates/0 doesn't exist, and the parent is writable, so try to
+      // create it. This can fail if a file named updates exists.
+      try {
+        dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0770);
+      } catch (e) {
+        // The create failed for some reason. We can't use it.
+        log("Error: " + dir.path + " unable to create directory");
+        return null;
+      }
+      return dir;
+  },
+
+  getDefaultUpdateDir: function dp_getDefaultUpdateDir() {
+    let path = gExtStorage;
     if (!path) {
-      path = LOCAL_PATH;
+      path = LOCAL_DIR;
+    }
+
+    if (Services.volumeService) {
+      let extVolume = Services.volumeService.getVolumeByPath(path);
+      if (!extVolume) {
+        path = LOCAL_DIR;
+      }
     }
 
     let dir = Cc["@mozilla.org/file/local;1"]
                  .createInstance(Ci.nsILocalFile)
     dir.initWithPath(path);
 
-    if (!dir.exists() && path != LOCAL_PATH) {
-      // Fallback to LOCAL_PATH if we didn't fallback earlier
-      dir.initWithPath(LOCAL_PATH);
+    if (!dir.exists() && path != LOCAL_DIR) {
+      // Fallback to LOCAL_DIR if we didn't fallback earlier
+      dir.initWithPath(LOCAL_DIR);
 
       if (!dir.exists()) {
         throw Cr.NS_ERROR_FILE_NOT_FOUND;
       }
     }
 
     dir.appendRelativePath("updates");
-    persistent.value = false;
     return dir;
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DirectoryProvider]);
--- a/b2g/components/UpdatePrompt.js
+++ b/b2g/components/UpdatePrompt.js
@@ -17,16 +17,19 @@ let log =
   VERBOSE ?
   function log_dump(msg) { dump("UpdatePrompt: "+ msg +"\n"); } :
   function log_noop(msg) { };
 
 const PREF_APPLY_PROMPT_TIMEOUT = "b2g.update.apply-prompt-timeout";
 const PREF_APPLY_IDLE_TIMEOUT   = "b2g.update.apply-idle-timeout";
 
 const NETWORK_ERROR_OFFLINE = 111;
+const FILE_ERROR_TOO_BIG = 112;
+
+const STATE_DOWNLOADING = 'downloading';
 
 XPCOMUtils.defineLazyServiceGetter(Services, "aus",
                                    "@mozilla.org/updates/update-service;1",
                                    "nsIApplicationUpdateService");
 
 XPCOMUtils.defineLazyServiceGetter(Services, "um",
                                    "@mozilla.org/updates/update-manager;1",
                                    "nsIUpdateManager");
@@ -267,18 +270,27 @@ UpdatePrompt.prototype = {
     if (!aUpdate) {
       aUpdate = Services.um.activeUpdate;
       if (!aUpdate) {
         log("No active update found to download");
         return;
       }
     }
 
-    Services.aus.downloadUpdate(aUpdate, true);
-    Services.aus.addDownloadListener(this);
+    let status = Services.aus.downloadUpdate(aUpdate, true);
+    if (status == STATE_DOWNLOADING) {
+      Services.aus.addDownloadListener(this);
+      return;
+    }
+
+    log("Error downloading update " + aUpdate.name + ": " + aUpdate.errorCode);
+    if (aUpdate.errorCode == FILE_ERROR_TOO_BIG) {
+      aUpdate.statusText = "file-too-big";
+    }
+    this.showUpdateError(aUpdate);
   },
 
   handleDownloadCancel: function UP_handleDownloadCancel() {
     log("Pausing download");
     Services.aus.pauseDownload();
   },
 
   finishUpdate: function UP_finishUpdate() {
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -69,20 +69,28 @@ const KEY_APPDIR          = "XCurProcD";
 const KEY_GRED            = "GreD";
 
 #ifdef XP_WIN
 #define USE_UPDROOT
 #elifdef ANDROID
 #define USE_UPDROOT
 #endif
 
+#ifdef MOZ_WIDGET_GONK
+#define USE_UPDATE_ARCHIVE_DIR
+#endif
+
 #ifdef USE_UPDROOT
 const KEY_UPDROOT         = "UpdRootD";
 #endif
 
+#ifdef USE_UPDATE_ARCHIVE_DIR
+const KEY_UPDATE_ARCHIVE_DIR = "UpdArchD"
+#endif
+
 #ifdef XP_WIN
 #define SKIP_STAGE_UPDATES_TEST
 #elifdef MOZ_WIDGET_GONK
 // In Gonk, the updater will remount the /system partition to move staged files
 // into place, so we skip the test here to keep things isolated.
 #define SKIP_STAGE_UPDATES_TEST
 #endif
 
@@ -94,17 +102,18 @@ const UPDATED_DIR         = "updated";
 #endif
 const FILE_UPDATE_STATUS  = "update.status";
 const FILE_UPDATE_VERSION = "update.version";
 #ifdef MOZ_WIDGET_ANDROID
 const FILE_UPDATE_ARCHIVE = "update.apk";
 #else
 const FILE_UPDATE_ARCHIVE = "update.mar";
 #endif
-const FILE_UPDATE_LOG     = "update.log"
+const FILE_UPDATE_LINK    = "update.link";
+const FILE_UPDATE_LOG     = "update.log";
 const FILE_UPDATES_DB     = "updates.xml";
 const FILE_UPDATE_ACTIVE  = "active-update.xml";
 const FILE_PERMS_TEST     = "update.test";
 const FILE_LAST_LOG       = "last-update.log";
 const FILE_BACKUP_LOG     = "backup-update.log";
 const FILE_UPDATE_LOCALE  = "update.locale";
 
 const STATE_NONE            = "null";
@@ -150,16 +159,17 @@ const WRITE_ERROR_SHARING_VIOLATION_SIGN
 const WRITE_ERROR_SHARING_VIOLATION_NOPROCESSFORPID = 47;
 const WRITE_ERROR_SHARING_VIOLATION_NOPID           = 48;
 
 
 const CERT_ATTR_CHECK_FAILED_NO_UPDATE  = 100;
 const CERT_ATTR_CHECK_FAILED_HAS_UPDATE = 101;
 const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
 const NETWORK_ERROR_OFFLINE             = 111;
+const FILE_ERROR_TOO_BIG                = 112;
 
 const DOWNLOAD_CHUNK_SIZE           = 300000; // bytes
 const DOWNLOAD_BACKGROUND_INTERVAL  = 600;    // seconds
 const DOWNLOAD_FOREGROUND_INTERVAL  = 0;
 
 const UPDATE_WINDOW_NAME      = "Update:Wizard";
 
 // The number of consecutive failures when updating using the service before
@@ -169,16 +179,22 @@ const DEFAULT_SERVICE_MAX_ERRORS = 10;
 // The number of consecutive socket errors to allow before falling back to
 // downloading a different MAR file or failing if already downloading the full.
 const DEFAULT_SOCKET_MAX_ERRORS = 10;
 
 // The number of milliseconds to wait before retrying a connection error.
 const DEFAULT_UPDATE_RETRY_TIMEOUT = 2000;
 
 var gLocale     = null;
+#ifdef MOZ_B2G
+var gVolumeMountLock = null;
+XPCOMUtils.defineLazyGetter(this, "gExtStorage", function aus_gExtStorage() {
+    return Services.env.get("EXTERNAL_STORAGE");
+});
+#endif
 
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() {
   return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false);
 });
 
@@ -727,16 +743,84 @@ function readStatusFile(dir) {
  */
 function writeStatusFile(dir, state) {
   var statusFile = dir.clone();
   statusFile.append(FILE_UPDATE_STATUS);
   writeStringToFile(statusFile, state);
 }
 
 /**
+ * Reads the link file from the update.link file in the
+ * specified directory.
+ * @param   dir
+ *          The dir to look for an update.link file in
+ * @return  The contents of the update.link file.
+ */
+function readLinkFile(dir) {
+  var linkFile = dir.clone();
+  linkFile.append(FILE_UPDATE_LINK);
+  var link = readStringFromFile(linkFile) || STATE_NONE;
+  LOG("readLinkFile - link: " + link + ", path: " + linkFile.path);
+  return status;
+}
+
+/**
+ * Creates a link file, which allows the actual patch to live in
+ * a directory different from the update directory.
+ * @param   dir
+ *          The patch directory where the update.link file
+ *          should be written.
+ * @param   patchFile
+ *          The fully qualified filename of the patchfile.
+ */
+function writeLinkFile(dir, patchFile) {
+  var linkFile = dir.clone();
+  linkFile.append(FILE_UPDATE_LINK);
+  writeStringToFile(linkFile, patchFile.path);
+#ifdef MOZ_B2G
+  if (patchFile.path.indexOf(gExtStorage) == 0) {
+    // The patchfile is being stored on external storage. Try to lock it
+    // so that it doesn't get shared with the PC while we're downloading
+    // to it.
+    acquireSDCardMountLock();
+  }
+#endif
+}
+
+/**
+ * Acquires a VolumeMountLock for the sdcard volume.
+ *
+ * This prevents the SDCard from being shared with the PC while
+ * we're downloading the update.
+ */
+function acquireSDCardMountLock() {
+#ifdef MOZ_B2G
+  let volsvc = Cc["@mozilla.org/telephony/volume-service;1"].
+                    getService(Ci.nsIVolumeService);
+  if (volsvc) {
+    gSDCardMountLock = volsvc.createMountLock("sdcard");
+  }
+#endif
+}
+
+/**
+ * Releases any SDCard mount lock that we might have.
+ *
+ * This once again allows the SDCard to be shared with the PC.
+ */
+function releaseSDCardMountLock() {
+#ifdef MOZ_B2G
+  if (gSDCardMountLock) {
+    gSDCardMountLock.unlock();
+    gSDCardMountLock = null;
+  }
+#endif
+}
+
+/**
  * Determines if the service should be used to attempt an update
  * or not.  For now this is only when PREF_APP_UPDATE_SERVICE_ENABLED
  * is true and we have Firefox.
  *
  * @return  true if the service should be used for updates.
  */
 function shouldUseService() {
 #ifdef MOZ_MAINTENANCE_SERVICE
@@ -894,16 +978,17 @@ function cleanUpUpdatesDir(aBackgroundUp
     // updater.app, which is itself a directory.
     try {
       f.remove(true);
     }
     catch (e) {
       LOG("cleanUpUpdatesDir - failed to remove file " + f.path);
     }
   }
+  releaseSDCardMountLock();
 }
 
 /**
  * Clean up updates list and the updates directory.
  */
 function cleanupActiveUpdate() {
   // Move the update from the Active Update list into the Past Updates list.
   var um = Cc["@mozilla.org/updates/update-manager;1"].
@@ -2793,16 +2878,18 @@ UpdateManager.prototype = {
     // to shut down.
     if (update.state == STATE_APPLIED) {
       // Notify the user that an update has been staged and is ready for
       // installation (i.e. that they should restart the application). We do
       // not notify on failed update attempts.
       var prompter = Cc["@mozilla.org/updates/update-prompt;1"].
                      createInstance(Ci.nsIUpdatePrompt);
       prompter.showUpdateDownloaded(update, true);
+    } else {
+      releaseSDCardMountLock();
     }
   },
 
   classID: Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateManager, Ci.nsIObserver])
 };
 
 /**
@@ -3141,16 +3228,17 @@ Downloader.prototype = {
   cancel: function Downloader_cancel(cancelError) {
     LOG("Downloader: cancel");
     if (cancelError === undefined) {
       cancelError = Cr.NS_BINDING_ABORTED;
     }
     if (this._request && this._request instanceof Ci.nsIRequest) {
       this._request.cancel(cancelError);
     }
+    releaseSDCardMountLock();
   },
 
   /**
    * Whether or not a patch has been downloaded and staged for installation.
    */
   get patchIsStaged() {
     var readState = readStatusFile(getUpdatesDir());
     // Note that if we decide to download and apply new updates after another
@@ -3312,16 +3400,38 @@ Downloader.prototype = {
   /**
    * Whether or not we are currently downloading something.
    */
   get isBusy() {
     return this._request != null;
   },
 
   /**
+   * Get the nsIFile to use for downloading the active update's selected patch
+   */
+  _getUpdateArchiveFile: function Downloader__getUpdateArchiveFile() {
+    var updateArchive;
+#ifdef USE_UPDATE_ARCHIVE_DIR
+    try {
+      updateArchive = FileUtils.getDir(KEY_UPDATE_ARCHIVE_DIR, [], true);
+    } catch (e) {
+      if (e == Cr.NS_ERROR_FILE_TOO_BIG) {
+        this._update.errorCode = FILE_ERROR_TOO_BIG;
+      }
+      return null;
+    }
+#else
+    updateArchive = getUpdatesDir().clone();
+#endif
+
+    updateArchive.append(FILE_UPDATE_ARCHIVE);
+    return updateArchive;
+  },
+
+  /**
    * Download and stage the given update.
    * @param   update
    *          A nsIUpdate object to download a patch for. Cannot be null.
    */
   downloadUpdate: function Downloader_downloadUpdate(update) {
     LOG("UpdateService:_downloadUpdate");
     if (!update)
       throw Cr.NS_ERROR_NULL_POINTER;
@@ -3334,18 +3444,26 @@ Downloader.prototype = {
     // to download.
     this._patch = this._selectPatch(update, updateDir);
     if (!this._patch) {
       LOG("Downloader:downloadUpdate - no patch to download");
       return readStatusFile(updateDir);
     }
     this.isCompleteUpdate = this._patch.type == "complete";
 
-    var patchFile = updateDir.clone();
-    patchFile.append(FILE_UPDATE_ARCHIVE);
+    var patchFile = this._getUpdateArchiveFile();
+    if (!patchFile) {
+      return STATE_NONE;
+    }
+
+    if (patchFile.path.indexOf(updateDir.path) != 0) {
+      // The patchFile is in a directory which is different from the
+      // updateDir, create a link file.
+      writeLinkFile(updateDir, patchFile);
+    }
 
     var uri = Services.io.newURI(this._patch.URL, null, null);
 
     this._request = Cc["@mozilla.org/network/incremental-download;1"].
                     createInstance(Ci.nsIIncrementalDownload);
 
     LOG("Downloader:downloadUpdate - downloading from " + uri.spec + " to " +
         patchFile.path);