Bug 1112937 - Fail updates on Windows if an exclusive lock on the binary cannot be obtained draft
authorKirk Steuber <ksteuber@mozilla.com>
Tue, 10 Oct 2017 09:20:44 -0700
changeset 689674 ab68d574742ec9cdaa1aec7b2288e00d5f969636
parent 689673 7f5546fbc167e76e0936c744929d5679aaba386c
child 738375 f1378ae3e28e8b49c9430a74dade51a3e32418a3
push id87086
push userksteuber@mozilla.com
push dateTue, 31 Oct 2017 21:44:03 +0000
bugs1112937
milestone58.0a1
Bug 1112937 - Fail updates on Windows if an exclusive lock on the binary cannot be obtained This patch implements the changes to the updater and associated tests to fix the multiprofile update problem on Windows. The problem is that if two instances of Firefox are running with different profiles and one instance tries to apply an update, then the update will be applied despite the other running instance (which will likely manifest problems, probably when attempting to start new processess). This patch changes the update mechanism such that if it cannot get an exclusive lock on the binary to be updated, the update will fail. However, if the update continues to fail 24 hours after the first failure, a force file will be written out causing the next update attempt to update despite other instances. MozReview-Commit-ID: 9Aihv5pXgz4
toolkit/locales/en-US/chrome/mozapps/update/updates.properties
toolkit/mozapps/update/common/errors.h
toolkit/mozapps/update/nsUpdateService.js
toolkit/mozapps/update/tests/data/sharedUpdateXML.js
toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
toolkit/mozapps/update/tests/unit_base_updater/marAppInUseFailureComplete_win.js
toolkit/mozapps/update/tests/unit_base_updater/marAppInUseForceSuccessComplete_win.js
toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsFailureComplete_win.js
toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsForceSuccessComplete_win.js
toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini
toolkit/mozapps/update/tests/unit_service_updater/marAppInUseFailureCompleteSvc_win.js
toolkit/mozapps/update/tests/unit_service_updater/marAppInUseForceSuccessCompleteSvc_win.js
toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini
toolkit/mozapps/update/updater/updater.cpp
--- a/toolkit/locales/en-US/chrome/mozapps/update/updates.properties
+++ b/toolkit/locales/en-US/chrome/mozapps/update/updates.properties
@@ -64,16 +64,17 @@ applyingUpdate=Applying update…
 
 updatesfound_minor.title=Update Available
 updatesfound_major.title=New Version Available
 
 installSuccess=The Update was successfully installed
 installPending=Install Pending
 patchApplyFailure=The Update could not be installed (patch apply failed)
 elevationFailure=You don’t have the permissions necessary to install this update. Please contact your system administrator.
+appInUseFailure=Another instance of the application prevented updating.
 
 # LOCALIZATION NOTE: %S is the amount downloaded so far
 # example: Paused —  879 KB of 2.1 MB
 downloadPausedStatus=Paused —  %S
 
 check_error-200=Update XML file malformed (200)
 check_error-403=Access denied (403)
 check_error-404=Update XML file not found (404)
--- a/toolkit/mozapps/update/common/errors.h
+++ b/toolkit/mozapps/update/common/errors.h
@@ -91,16 +91,19 @@
 #define INVALID_APPLYTO_DIR_STAGED_ERROR 72
 #define LOCK_ERROR_PATCH_FILE 73
 #define INVALID_APPLYTO_DIR_ERROR 74
 #define INVALID_INSTALL_DIR_PATH_ERROR 75
 #define INVALID_WORKING_DIR_PATH_ERROR 76
 #define INVALID_CALLBACK_PATH_ERROR 77
 #define INVALID_CALLBACK_DIR_ERROR 78
 
+#define CALLBACK_ERROR_WAIT_PID 81
+#define CALLBACK_ERROR_SHARING_VIOLATION 82
+
 // Error codes 80 through 99 are reserved for nsUpdateService.js
 
 // The following error codes are only used by updater.exe
 // when a fallback key exists for tests.
 #define FALLBACKKEY_UNKNOWN_ERROR 100
 #define FALLBACKKEY_REGPATH_ERROR 101
 #define FALLBACKKEY_NOKEY_ERROR 102
 #define FALLBACKKEY_SERVICE_NO_STOP_ERROR 103
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -43,32 +43,35 @@ const PREF_APP_UPDATE_SERVICE_ENABLED   
 const PREF_APP_UPDATE_SERVICE_ERRORS       = "app.update.service.errors";
 const PREF_APP_UPDATE_SERVICE_MAXERRORS    = "app.update.service.maxErrors";
 const PREF_APP_UPDATE_SILENT               = "app.update.silent";
 const PREF_APP_UPDATE_SOCKET_MAXERRORS     = "app.update.socket.maxErrors";
 const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT  = "app.update.socket.retryTimeout";
 const PREF_APP_UPDATE_STAGING_ENABLED      = "app.update.staging.enabled";
 const PREF_APP_UPDATE_URL                  = "app.update.url";
 const PREF_APP_UPDATE_URL_DETAILS          = "app.update.url.details";
+const PREF_APP_UPDATE_IN_USE_FAIL_TIME     = "app.update.inUseFailure.time";
+const PREF_APP_UPDATE_IN_USE_FAIL_VERSION  = "app.update.inUseFailure.version";
 
 const URI_BRAND_PROPERTIES      = "chrome://branding/locale/brand.properties";
 const URI_UPDATE_HISTORY_DIALOG = "chrome://mozapps/content/update/history.xul";
 const URI_UPDATE_NS             = "http://www.mozilla.org/2005/app-update";
 const URI_UPDATE_PROMPT_DIALOG  = "chrome://mozapps/content/update/updates.xul";
 const URI_UPDATES_PROPERTIES    = "chrome://mozapps/locale/update/updates.properties";
 
 const KEY_UPDROOT         = "UpdRootD";
 const KEY_EXECUTABLE      = "XREExeF";
 
 const DIR_UPDATES         = "updates";
 
 const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
 const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
 const FILE_LAST_UPDATE_LOG   = "last-update.log";
 const FILE_UPDATES_XML       = "updates.xml";
+const FILE_UPDATE_FORCE      = "update.force";
 const FILE_UPDATE_LOG        = "update.log";
 const FILE_UPDATE_MAR        = "update.mar";
 const FILE_UPDATE_STATUS     = "update.status";
 const FILE_UPDATE_TEST       = "update.test";
 const FILE_UPDATE_VERSION    = "update.version";
 
 const STATE_NONE            = "null";
 const STATE_DOWNLOADING     = "downloading";
@@ -105,16 +108,18 @@ const WRITE_ERROR_DELETE_FILE           
 const WRITE_ERROR_OPEN_PATCH_FILE          = 63;
 const WRITE_ERROR_PATCH_FILE               = 64;
 const WRITE_ERROR_APPLY_DIR_PATH           = 65;
 const WRITE_ERROR_CALLBACK_PATH            = 66;
 const WRITE_ERROR_FILE_ACCESS_DENIED       = 67;
 const WRITE_ERROR_DIR_ACCESS_DENIED        = 68;
 const WRITE_ERROR_DELETE_BACKUP            = 69;
 const WRITE_ERROR_EXTRACT                  = 70;
+const CALLBACK_ERROR_WAIT_PID              = 81;
+const CALLBACK_ERROR_SHARING_VIOLATION     = 82;
 
 // Array of write errors to simplify checks for write errors
 const WRITE_ERRORS = [WRITE_ERROR,
                       WRITE_ERROR_ACCESS_DENIED,
                       WRITE_ERROR_CALLBACK_APP,
                       WRITE_ERROR_FILE_COPY,
                       WRITE_ERROR_DELETE_FILE,
                       WRITE_ERROR_OPEN_PATCH_FILE,
@@ -168,16 +173,21 @@ const DEFAULT_SOCKET_MAX_ERRORS = 10;
 
 // The number of milliseconds to wait before retrying a connection error.
 const DEFAULT_SOCKET_RETRYTIMEOUT = 2000;
 
 // Default maximum number of elevation cancelations per update version before
 // giving up.
 const DEFAULT_CANCELATIONS_OSX_MAX = 3;
 
+// When an update fails because files are in use, we eventually want to force an
+// update. This value specifies the delay between the first failure and the
+// decision to force the next update (in milliseconds).
+const SHARING_VIOLATION_FORCE_DELAY = 24 * 60 * 60 * 1000;  // 24 hours
+
 // This maps app IDs to their respective notification topic which signals when
 // the application's user interface has been displayed.
 const APPID_TO_TOPIC = {
   // Firefox
   "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "sessionstore-windows-restored",
   // SeaMonkey
   "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "sessionstore-windows-restored",
   // Thunderbird
@@ -860,16 +870,21 @@ function cleanupActiveUpdate() {
   // Move the update from the Active Update list into the Past Updates list.
   var um = Cc["@mozilla.org/updates/update-manager;1"].
            getService(Ci.nsIUpdateManager);
   // Setting |activeUpdate| to null will move the active update to the update
   // history.
   um.activeUpdate = null;
   um.saveUpdates();
 
+  // Unset prefs indicating previous install failure now that the install has
+  // succeeded
+  Services.prefs.clearUserPref(PREF_APP_UPDATE_IN_USE_FAIL_TIME);
+  Services.prefs.clearUserPref(PREF_APP_UPDATE_IN_USE_FAIL_VERSION);
+
   // Now trash the updates directory, since we're done with it
   cleanUpUpdatesDir();
 }
 
 /**
  * An enumeration of items in a JS array.
  * @constructor
  */
@@ -1021,16 +1036,54 @@ function handleUpdateFailure(update, err
       Services.prefs.setIntPref(PREF_APP_UPDATE_SERVICE_ERRORS,
                                 failCount);
     }
 
     writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
     return true;
   }
 
+  if (update.errorCode == CALLBACK_ERROR_WAIT_PID ||
+      update.errorCode == CALLBACK_ERROR_SHARING_VIOLATION) {
+    update.statusText = gUpdateBundle.GetStringFromName("appInUseFailure");
+    // Since staging won't work successfully when the process is in use don't
+    // try to perform a replace request.
+    // Comment needs to go with the force code.
+    writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
+
+    let previousFailTime = Services.prefs.getIntPref(PREF_APP_UPDATE_IN_USE_FAIL_TIME, null);
+    let previousFailVersion = Services.prefs.getCharPref(PREF_APP_UPDATE_IN_USE_FAIL_VERSION, "");
+    if (previousFailVersion == update.appVersion) {
+      if (Date.now() - previousFailTime >= SHARING_VIOLATION_FORCE_DELAY) {
+        let updateDir;
+        try {
+          updateDir = getUpdatesDir();
+        } catch (e) {
+          LOG("handleUpdateFailure - unable to get the updates patch " +
+              "directory. Exception: " + e);
+        }
+        if (updateDir) {
+          let forceFile = updateDir;
+          forceFile.append(FILE_UPDATE_FORCE);
+          forceFile.create();
+        }
+      }
+    } else {
+      // This is the first failure we have experienced from this cause, for this
+      // update. Keep a record of the time it happened in these prefs.
+      Services.prefs.setIntPref(PREF_APP_UPDATE_IN_USE_FAIL_TIME, Date.now());
+      Services.prefs.setCharPref(PREF_APP_UPDATE_IN_USE_FAIL_VERSION,
+                                 update.appVersion);
+    }
+
+    let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+                   createInstance(Ci.nsIUpdatePrompt);
+    prompter.showUpdateError(update);
+  }
+
   if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ERRORS)) {
     Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS);
   }
 
   return false;
 }
 
 /**
--- a/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
+++ b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
@@ -39,16 +39,18 @@ const SERVICE_INVALID_APPLYTO_DIR_ERROR 
 const SERVICE_INVALID_INSTALL_DIR_PATH_ERROR   = 55;
 const SERVICE_INVALID_WORKING_DIR_PATH_ERROR   = 56;
 const INVALID_APPLYTO_DIR_STAGED_ERROR         = 72;
 const INVALID_APPLYTO_DIR_ERROR                = 74;
 const INVALID_INSTALL_DIR_PATH_ERROR           = 75;
 const INVALID_WORKING_DIR_PATH_ERROR           = 76;
 const INVALID_CALLBACK_PATH_ERROR              = 77;
 const INVALID_CALLBACK_DIR_ERROR               = 78;
+const CALLBACK_ERROR_WAIT_PID                  = 81;
+const CALLBACK_ERROR_SHARING_VIOLATION         = 82;
 
 // Error codes 80 through 99 are reserved for nsUpdateService.js and are not
 // defined in common/errors.h
 const ERR_OLDER_VERSION_OR_SAME_BUILD      = 90;
 const ERR_UPDATE_STATE_NONE                = 91;
 const ERR_CHANNEL_CHANGE                   = 92;
 
 const STATE_FAILED_DELIMETER = ": ";
@@ -60,16 +62,20 @@ const STATE_FAILED_CRC_ERROR =
 const STATE_FAILED_READ_ERROR =
   STATE_FAILED + STATE_FAILED_DELIMETER + READ_ERROR;
 const STATE_FAILED_WRITE_ERROR =
   STATE_FAILED + STATE_FAILED_DELIMETER + WRITE_ERROR;
 const STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR =
   STATE_FAILED + STATE_FAILED_DELIMETER + MAR_CHANNEL_MISMATCH_ERROR;
 const STATE_FAILED_VERSION_DOWNGRADE_ERROR =
   STATE_FAILED + STATE_FAILED_DELIMETER + VERSION_DOWNGRADE_ERROR;
+const STATE_FAILED_CALLBACK_ERROR_WAIT_PID =
+  STATE_FAILED + STATE_FAILED_DELIMETER + CALLBACK_ERROR_WAIT_PID;
+const STATE_FAILED_CALLBACK_ERROR_SHARING_VIOLATION =
+  STATE_FAILED + STATE_FAILED_DELIMETER + CALLBACK_ERROR_SHARING_VIOLATION;
 const STATE_FAILED_SERVICE_COULD_NOT_COPY_UPDATER =
   STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_COULD_NOT_COPY_UPDATER;
 const STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR =
   STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR;
 const STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR =
   STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_APPLYTO_DIR_ERROR;
 const STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR =
   STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_INSTALL_DIR_PATH_ERROR;
--- a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
+++ b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
@@ -89,17 +89,21 @@ const CALL_QUIT = "calling QuitProgressU
 const ERR_UPDATE_IN_PROGRESS = "Update already in progress! Exiting";
 const ERR_RENAME_FILE = "rename_file: failed to rename file";
 const ERR_ENSURE_COPY = "ensure_copy: failed to copy the file";
 const ERR_UNABLE_OPEN_DEST = "unable to open destination file";
 const ERR_BACKUP_DISCARD = "backup_discard: unable to remove";
 const ERR_MOVE_DESTDIR_7 = "Moving destDir to tmpDir failed, err: 7";
 const ERR_BACKUP_CREATE_7 = "backup_create failed: 7";
 const ERR_LOADSOURCEFILE_FAILED = "LoadSourceFile failed";
-const ERR_PARENT_PID_PERSISTS = "The parent process didn't exit! Continuing with update.";
+
+const ERR_CALLBACK_OPEN = "NS_main: callback app file open attempt";
+const ERR_CALLBACK_IN_USE = "NS_main: callback app file in use, failed to exclusively open executable file:";
+const ERR_NO_PID_EXIT = "The parent process didn't exit and the update is not being forced.";
+const ERR_PARENT_PID_PERSISTS = "The parent process didn't exit, but the update is being forced.";
 
 const LOG_SVC_SUCCESSFUL_LAUNCH = "Process was started... waiting on result.";
 
 // Typical end of a message when calling assert
 const MSG_SHOULD_EQUAL = " should equal the expected value";
 const MSG_SHOULD_EXIST = "the file or directory should exist";
 const MSG_SHOULD_NOT_EXIST = "the file or directory should not exist";
 
@@ -3104,19 +3108,21 @@ function checkUpdateLogContents(aCompare
  *
  * @param   aCheckString
  *          The string to check if the update log contains.
  */
 function checkUpdateLogContains(aCheckString) {
   let updateLog = getUpdateLog(FILE_LAST_UPDATE_LOG);
   let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
   updateLogContents = replaceLogPaths(updateLogContents);
-  Assert.notEqual(updateLogContents.indexOf(aCheckString), -1,
-                  "the update log contents should contain value: " +
-                  aCheckString);
+  if (updateLogContents.indexOf(aCheckString) == -1) {
+    logTestInfo(updateLogContents);
+    Assert.ok(false,
+              `The update log contents should contain value: ${aCheckString}`);
+  }
 }
 
 /**
  * Helper function for updater binary tests for verifying the state of files and
  * directories after a successful update.
  *
  * @param   aGetFileFunc
  *          The function used to get the files in the directory to be checked.
--- a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseFailureComplete_win.js
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseFailureComplete_win.js
@@ -5,58 +5,52 @@
 /* Application in use complete MAR file patch apply success test */
 
 function run_test() {
   if (!setupTestCommon()) {
     return;
   }
   gTestFiles = gTestFilesCompleteSuccess;
   gTestDirs = gTestDirsCompleteSuccess;
+  setTestFilesAndDirsForFailure();
   setupUpdaterTest(FILE_COMPLETE_MAR, false);
 }
 
 /**
  * Called after the call to setupUpdaterTest finishes.
  */
 function setupUpdaterTestFinished() {
   runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false);
 }
 
 /**
  * Called after the call to waitForHelperSleep finishes.
  */
 function waitForHelperSleepFinished() {
-  runUpdate(STATE_SUCCEEDED, false, 0, true);
+  runUpdate(STATE_FAILED_CALLBACK_ERROR_SHARING_VIOLATION, false, 1, true);
 }
 
 /**
  * Called after the call to runUpdate finishes.
  */
 function runUpdateFinished() {
   waitForHelperExit();
 }
 
 /**
  * Called after the call to waitForHelperExit finishes.
  */
 function waitForHelperExitFinished() {
-  checkPostUpdateAppLog();
-}
-
-/**
- * Called after the call to checkPostUpdateAppLog finishes.
- */
-function checkPostUpdateAppLogFinished() {
-  checkAppBundleModTime();
   standardInit();
-  checkPostUpdateRunningFile(true);
-  checkFilesAfterUpdateSuccess(getApplyDirFile);
-  checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+  checkPostUpdateRunningFile(false);
+  checkFilesAfterUpdateFailure(getApplyDirFile);
+  checkUpdateLogContains(ERR_CALLBACK_OPEN);
+  checkUpdateLogContains(ERR_CALLBACK_IN_USE);
   do_execute_soon(waitForUpdateXMLFiles);
 }
 
 /**
  * Called after the call to waitForUpdateXMLFiles finishes.
  */
 function waitForUpdateXMLFilesFinished() {
-  checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1);
+  checkUpdateManager(STATE_NONE, false, STATE_PENDING, CALLBACK_ERROR_SHARING_VIOLATION, 1);
   checkCallbackLog();
 }
--- a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseForceSuccessComplete_win.js
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseForceSuccessComplete_win.js
@@ -12,16 +12,19 @@ function run_test() {
   gTestDirs = gTestDirsCompleteSuccess;
   setupUpdaterTest(FILE_COMPLETE_MAR, false);
 }
 
 /**
  * Called after the call to setupUpdaterTest finishes.
  */
 function setupUpdaterTestFinished() {
+  let forceFile = getUpdatesPatchDir();
+  forceFile.append("update.force");
+  forceFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
   runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false);
 }
 
 /**
  * Called after the call to waitForHelperSleep finishes.
  */
 function waitForHelperSleepFinished() {
   runUpdate(STATE_SUCCEEDED, false, 0, true);
--- a/toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsFailureComplete_win.js
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsFailureComplete_win.js
@@ -5,16 +5,17 @@
 /* Application in use complete MAR file patch apply success test */
 
 function run_test() {
   if (!setupTestCommon()) {
     return;
   }
   gTestFiles = gTestFilesCompleteSuccess;
   gTestDirs = gTestDirsCompleteSuccess;
+  setTestFilesAndDirsForFailure();
   setupUpdaterTest(FILE_COMPLETE_MAR, false);
 }
 
 /**
  * Called after the call to setupUpdaterTest finishes.
  */
 function setupUpdaterTestFinished() {
   runHelperPIDPersists(DIR_RESOURCES + gCallbackBinFile, false);
@@ -23,43 +24,36 @@ function setupUpdaterTestFinished() {
 /**
  * Called after the call to waitForHelperSleep finishes.
  */
 function waitForHelperSleepFinished() {
   if (!gPIDPersistProcess.pid) {
     do_execute_soon(waitForHelperSleepFinished);
     return;
   }
-  runUpdate(STATE_SUCCEEDED, false, 0, true);
+  runUpdate(STATE_FAILED_CALLBACK_ERROR_WAIT_PID, false, 1, true);
 }
 
 /**
  * Called after the call to runUpdate finishes.
  */
 function runUpdateFinished() {
   waitForHelperExit();
 }
 
 /**
  * Called after the call to waitForHelperExit finishes.
  */
 function waitForHelperExitFinished() {
-  checkPostUpdateAppLog();
-}
-
-/**
- * Called after the call to checkPostUpdateAppLog finishes.
- */
-function checkPostUpdateAppLogFinished() {
   standardInit();
-  checkPostUpdateRunningFile(true);
-  checkFilesAfterUpdateSuccess(getApplyDirFile);
-  checkUpdateLogContains(ERR_PARENT_PID_PERSISTS);
+  checkPostUpdateRunningFile(false);
+  checkFilesAfterUpdateFailure(getApplyDirFile);
+  checkUpdateLogContains(ERR_NO_PID_EXIT);
   do_execute_soon(waitForUpdateXMLFiles);
 }
 
 /**
  * Called after the call to waitForUpdateXMLFiles finishes.
  */
 function waitForUpdateXMLFilesFinished() {
-  checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1);
+  checkUpdateManager(STATE_NONE, false, STATE_PENDING, CALLBACK_ERROR_WAIT_PID, 1);
   checkCallbackLog();
 }
--- a/toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsForceSuccessComplete_win.js
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsForceSuccessComplete_win.js
@@ -12,16 +12,19 @@ function run_test() {
   gTestDirs = gTestDirsCompleteSuccess;
   setupUpdaterTest(FILE_COMPLETE_MAR, false);
 }
 
 /**
  * Called after the call to setupUpdaterTest finishes.
  */
 function setupUpdaterTestFinished() {
+  let forceFile = getUpdatesPatchDir();
+  forceFile.append("update.force");
+  forceFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
   runHelperPIDPersists(DIR_RESOURCES + gCallbackBinFile, false);
 }
 
 /**
  * Called after the call to waitForHelperSleep finishes.
  */
 function waitForHelperSleepFinished() {
   if (!gPIDPersistProcess.pid) {
--- a/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini
+++ b/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini
@@ -40,17 +40,22 @@ reason = Windows only test
 skip-if = os != 'win'
 reason = Windows only test
 [marCallbackAppStageSuccessComplete_win.js]
 skip-if = os != 'win'
 reason = Windows only test
 [marCallbackAppStageSuccessPartial_win.js]
 skip-if = os != 'win'
 reason = Windows only test
-[marAppInUseSuccessComplete.js]
+[marAppInUseForceSuccessComplete_win.js]
+skip-if = os != 'win'
+reason = Windows only test
+[marAppInUseFailureComplete_win.js]
+skip-if = os != 'win'
+reason = Windows only test
 [marAppInUseStageSuccessComplete_unix.js]
 skip-if = os == 'win'
 reason = not a Windows test
 [marAppInUseStageFailureComplete_win.js]
 skip-if = os != 'win'
 reason = Windows only test
 [marFileLockedFailureComplete_win.js]
 skip-if = os != 'win'
@@ -83,17 +88,20 @@ reason = Windows only test
 skip-if = os != 'win'
 reason = Windows only test
 [marRMRFDirFileInUseStageFailureComplete_win.js]
 skip-if = os != 'win'
 reason = Windows only test
 [marRMRFDirFileInUseStageFailurePartial_win.js]
 skip-if = os != 'win'
 reason = Windows only test
-[marPIDPersistsSuccessComplete_win.js]
+[marPIDPersistsFailureComplete_win.js]
+skip-if = os != 'win'
+reason = Windows only test
+[marPIDPersistsForceSuccessComplete_win.js]
 skip-if = os != 'win'
 reason = Windows only test
 [marAppApplyDirLockedStageFailure_win.js]
 skip-if = os != 'win'
 reason = Windows only test
 [marAppApplyUpdateAppBinInUseStageSuccess_win.js]
 skip-if = os != 'win'
 reason = Windows only test
--- a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseFailureCompleteSvc_win.js
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseFailureCompleteSvc_win.js
@@ -5,58 +5,52 @@
 /* Application in use complete MAR file patch apply success test */
 
 function run_test() {
   if (!setupTestCommon()) {
     return;
   }
   gTestFiles = gTestFilesCompleteSuccess;
   gTestDirs = gTestDirsCompleteSuccess;
+  setTestFilesAndDirsForFailure();
   setupUpdaterTest(FILE_COMPLETE_MAR, false);
 }
 
 /**
  * Called after the call to setupUpdaterTest finishes.
  */
 function setupUpdaterTestFinished() {
   runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false);
 }
 
 /**
  * Called after the call to waitForHelperSleep finishes.
  */
 function waitForHelperSleepFinished() {
-  runUpdate(STATE_SUCCEEDED, false, 0, true);
+  runUpdate(STATE_FAILED_CALLBACK_ERROR_SHARING_VIOLATION, false, 1, true);
 }
 
 /**
  * Called after the call to runUpdate finishes.
  */
 function runUpdateFinished() {
   waitForHelperExit();
 }
 
 /**
  * Called after the call to waitForHelperExit finishes.
  */
 function waitForHelperExitFinished() {
-  checkPostUpdateAppLog();
-}
-
-/**
- * Called after the call to checkPostUpdateAppLog finishes.
- */
-function checkPostUpdateAppLogFinished() {
-  checkAppBundleModTime();
   standardInit();
-  checkPostUpdateRunningFile(true);
-  checkFilesAfterUpdateSuccess(getApplyDirFile);
-  checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+  checkPostUpdateRunningFile(false);
+  checkFilesAfterUpdateFailure(getApplyDirFile);
+  checkUpdateLogContains(ERR_CALLBACK_OPEN);
+  checkUpdateLogContains(ERR_CALLBACK_IN_USE);
   do_execute_soon(waitForUpdateXMLFiles);
 }
 
 /**
  * Called after the call to waitForUpdateXMLFiles finishes.
  */
 function waitForUpdateXMLFilesFinished() {
-  checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1);
+  checkUpdateManager(STATE_NONE, false, STATE_PENDING, CALLBACK_ERROR_SHARING_VIOLATION, 1);
   checkCallbackLog();
 }
--- a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseForceSuccessCompleteSvc_win.js
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseForceSuccessCompleteSvc_win.js
@@ -12,16 +12,19 @@ function run_test() {
   gTestDirs = gTestDirsCompleteSuccess;
   setupUpdaterTest(FILE_COMPLETE_MAR, false);
 }
 
 /**
  * Called after the call to setupUpdaterTest finishes.
  */
 function setupUpdaterTestFinished() {
+  let forceFile = getUpdatesPatchDir();
+  forceFile.append("update.force");
+  forceFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
   runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false);
 }
 
 /**
  * Called after the call to waitForHelperSleep finishes.
  */
 function waitForHelperSleepFinished() {
   runUpdate(STATE_SUCCEEDED, false, 0, true);
--- a/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini
+++ b/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini
@@ -41,17 +41,19 @@ run-sequentially = Uses the Mozilla Main
 [marCallbackAppSuccessCompleteSvc_win.js]
 run-sequentially = Uses the Mozilla Maintenance Service.
 [marCallbackAppSuccessPartialSvc_win.js]
 run-sequentially = Uses the Mozilla Maintenance Service.
 [marCallbackAppStageSuccessCompleteSvc_win.js]
 run-sequentially = Uses the Mozilla Maintenance Service.
 [marCallbackAppStageSuccessPartialSvc_win.js]
 run-sequentially = Uses the Mozilla Maintenance Service.
-[marAppInUseSuccessCompleteSvc.js]
+[marAppInUseForceSuccessCompleteSvc_win.js]
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marAppInUseFailureCompleteSvc_win.js]
 run-sequentially = Uses the Mozilla Maintenance Service.
 [marAppInUseStageFailureCompleteSvc_win.js]
 run-sequentially = Uses the Mozilla Maintenance Service.
 [marFileLockedFailureCompleteSvc_win.js]
 run-sequentially = Uses the Mozilla Maintenance Service.
 [marFileLockedFailurePartialSvc_win.js]
 run-sequentially = Uses the Mozilla Maintenance Service.
 [marFileLockedStageFailureCompleteSvc_win.js]
--- a/toolkit/mozapps/update/updater/updater.cpp
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -2953,36 +2953,53 @@ int NS_main(int argc, NS_tchar **argv)
            "a child of the installation directory! Exiting."));
       LogFinish();
       return 1;
     }
   }
 #endif
 
 #ifdef XP_WIN
+  bool forceUpdate = false;
+  NS_tchar forceFile[MAXPATHLEN];
+  NS_tsnprintf(forceFile, sizeof(forceFile)/sizeof(forceFile[0]),
+               NS_T("%s/update.force"), gPatchDirPath);
+  if (!NS_taccess(forceFile, F_OK)) {
+    forceUpdate = true;
+  }
+
   if (pid > 0) {
     HANDLE parent = OpenProcess(SYNCHRONIZE, false, (DWORD) pid);
     // May return nullptr if the parent process has already gone away.
     // Otherwise, wait for the parent process to exit before starting the
     // update.
     if (parent) {
       DWORD waitTime = PARENT_WAIT;
 #ifdef TEST_UPDATER
       if (EnvHasValue("MOZ_TEST_SHORTER_WAIT_PID")) {
         // Use a shorter time to wait for the PID to exit for the test.
         waitTime = 100;
       }
 #endif
       DWORD result = WaitForSingleObject(parent, waitTime);
       CloseHandle(parent);
       if (result != WAIT_OBJECT_0) {
-        // Continue to update since the parent application sometimes doesn't
-        // exit (see bug 1375242) so any fixes to the parent application will
-        // be applied instead of leaving the client in a broken state.
-        LOG(("The parent process didn't exit! Continuing with update."));
+        if (forceUpdate) {
+          LOG(("The parent process didn't exit, but the update is being forced."));
+        } else {
+          LOG(("The parent process didn't exit and the update is not being forced."));
+          LogFinish();
+          WriteStatusFile(CALLBACK_ERROR_WAIT_PID);
+
+          LaunchCallbackApp(argv[5],
+                            argc - callbackIndex,
+                            argv + callbackIndex,
+                            sUsingService);
+          return 1;
+        }
       }
     }
   }
 #else
   if (pid > 0)
     waitpid(pid, nullptr, 0);
 #endif
 
@@ -3499,22 +3516,27 @@ int NS_main(int argc, NS_tchar **argv)
              "File: " LOG_S ". Last error: %d", retries,
              targetPath, lastWriteError));
 
         Sleep(100);
       } while (++retries <= max_retries);
 
       // CreateFileW will fail if the callback executable is already in use.
       if (callbackFile == INVALID_HANDLE_VALUE) {
-        // Only fail the update if the last error was not a sharing violation.
-        if (lastWriteError != ERROR_SHARING_VIOLATION) {
+        // If CreateFileW failed, the update must fail with one exception:
+        // If the error is a sharing violation and we are forcing the update
+        // despite the sharing violation.
+        if (!(lastWriteError == ERROR_SHARING_VIOLATION && forceUpdate)) {
           LOG(("NS_main: callback app file in use, failed to exclusively open " \
-               "executable file: " LOG_S, argv[callbackIndex]));
+               "executable file: " LOG_S ", err: %d", argv[callbackIndex],
+               lastWriteError));
           LogFinish();
-          if (lastWriteError == ERROR_ACCESS_DENIED) {
+          if (lastWriteError == ERROR_SHARING_VIOLATION) {
+            WriteStatusFile(CALLBACK_ERROR_SHARING_VIOLATION);
+          } else if (lastWriteError == ERROR_ACCESS_DENIED) {
             WriteStatusFile(WRITE_ERROR_ACCESS_DENIED);
           } else {
             WriteStatusFile(WRITE_ERROR_CALLBACK_APP);
           }
 
           NS_tremove(gCallbackBackupPath);
           EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
           LaunchCallbackApp(argv[5],