diff options
Diffstat (limited to 'toolkit/mozapps/update')
361 files changed, 47762 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/AppUpdater.jsm b/toolkit/mozapps/update/AppUpdater.jsm new file mode 100644 index 0000000000..1a6a0f300b --- /dev/null +++ b/toolkit/mozapps/update/AppUpdater.jsm @@ -0,0 +1,947 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["AppUpdater"]; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +var gLogfileOutputStream; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const PREF_APP_UPDATE_LOG = "app.update.log"; +const PREF_APP_UPDATE_LOG_FILE = "app.update.log.file"; +const KEY_PROFILE_DIR = "ProfD"; +const FILE_UPDATE_MESSAGES = "update_messages.log"; +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", +}); +XPCOMUtils.defineLazyGetter(lazy, "gLogEnabled", function aus_gLogEnabled() { + return ( + Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) || + Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false) + ); +}); +XPCOMUtils.defineLazyGetter( + lazy, + "gLogfileEnabled", + function aus_gLogfileEnabled() { + return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false); + } +); + +const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx"; +const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never"; + +class AbortError extends Error { + constructor(...params) { + super(...params); + this.name = this.constructor.name; + } +} + +/** + * `AbortablePromise`s automatically add themselves to this set on construction + * and remove themselves when they settle. + */ +var gPendingAbortablePromises = new Set(); + +/** + * Creates a Promise that can be resolved immediately with an abort method. + * + * Note that the underlying Promise will probably still run to completion since + * there isn't any general way to abort Promises. So if it is possible to abort + * the operation instead or in addition to using this class, that is preferable. + */ +class AbortablePromise { + #abortFn; + #promise; + #hasCompleted = false; + + constructor(promise) { + let abortPromise = new Promise((resolve, reject) => { + this.#abortFn = () => reject(new AbortError()); + }); + this.#promise = Promise.race([promise, abortPromise]); + this.#promise = this.#promise.finally(() => { + this.#hasCompleted = true; + gPendingAbortablePromises.delete(this); + }); + gPendingAbortablePromises.add(this); + } + + abort() { + if (this.#hasCompleted) { + return; + } + this.#abortFn(); + } + + /** + * This can be `await`ed on to get the result of the `AbortablePromise`. It + * will resolve with the value that the Promise provided to the constructor + * resolves with. + */ + get promise() { + return this.#promise; + } + + /** + * Will be `true` if the Promise provided to the constructor has resolved or + * `abort()` has been called. Otherwise `false`. + */ + get hasCompleted() { + return this.#hasCompleted; + } +} + +function makeAbortable(promise) { + let abortable = new AbortablePromise(promise); + return abortable.promise; +} + +function abortAllPromises() { + for (const promise of gPendingAbortablePromises) { + promise.abort(); + } +} + +/** + * This class checks for app updates in the foreground. It has several public + * methods for checking for updates, downloading updates, stopping the current + * update, and getting the current update status. It can also register + * listeners that will be called back as different stages of updates occur. + */ +class AppUpdater { + #listeners = new Set(); + #status = AppUpdater.STATUS.NEVER_CHECKED; + // This will basically be set to `true` when `AppUpdater.check` is called and + // back to `false` right before it returns. + // It is also set to `true` during an update swap and back to `false` when the + // swap completes. + #updateBusy = false; + // When settings require that the user be asked for permission to download + // updates and we have an update to download, we will assign a function. + // Calling this function allows the download to proceed. + #permissionToDownloadGivenFn = null; + #_update = null; + // Keeps track of if we have an `update-swap` listener connected. We only + // connect it when the status is `READY_TO_RESTART`, but we can't use that to + // tell if its connected because we might be in the middle of an update swap + // in which case the status will have temporarily changed. + #swapListenerConnected = false; + + constructor() { + try { + this.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]); + + // This one call observes PREF_APP_UPDATE_LOG and PREF_APP_UPDATE_LOG_FILE + Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this, /* ownWeak */ true); + } catch (e) { + this.#onException(e); + } + } + + #onException(exception) { + try { + this.#update = null; + + if (this.#swapListenerConnected) { + LOG("AppUpdater:#onException - Removing update-swap listener"); + Services.obs.removeObserver(this, "update-swap"); + this.#swapListenerConnected = false; + } + + if (exception instanceof AbortError) { + // This should be where we end up if `AppUpdater.stop()` is called while + // `AppUpdater.check` is running or during an update swap. + LOG( + "AppUpdater:#onException - Caught AbortError. Setting status " + + "NEVER_CHECKED" + ); + this.#setStatus(AppUpdater.STATUS.NEVER_CHECKED); + } else { + LOG( + "AppUpdater:#onException - Exception caught. Setting status " + + "INTERNAL_ERROR" + ); + console.error(exception); + this.#setStatus(AppUpdater.STATUS.INTERNAL_ERROR); + } + } catch (e) { + LOG( + "AppUpdater:#onException - Caught additional exception while " + + "handling previous exception" + ); + console.error(e); + } + } + + /** + * This can be accessed by consumers to inspect the update that is being + * prepared for installation. It will always be null if `AppUpdater.check` + * hasn't been called yet. `AppUpdater.check` will set it to an instance of + * nsIUpdate once there is one available. This may be immediate, if an update + * is already downloading or has been downloaded. It may be delayed if an + * update check needs to be performed first. It also may remain null if the + * browser is up to date or if the update check fails. + * + * Regarding the difference between `AppUpdater.update`, `AppUpdater.#update`, + * and `AppUpdater.#_update`: + * - `AppUpdater.update` and `AppUpdater.#update` are effectively identical + * except that `AppUpdater.update` is readonly since it should not be + * changed from outside this class. + * - `AppUpdater.#_update` should only ever be modified by the setter for + * `AppUpdater.#update` in order to ensure that the "foregroundDownload" + * property is set on assignment. + * The quick and easy rule for using these is to always use `#update` + * internally and (of course) always use `update` externally. + */ + get update() { + return this.#update; + } + + get #update() { + return this.#_update; + } + + set #update(update) { + this.#_update = update; + if (this.#_update) { + this.#_update.QueryInterface(Ci.nsIWritablePropertyBag); + this.#_update.setProperty("foregroundDownload", "true"); + } + } + + /** + * The main entry point for checking for updates. As different stages of the + * check and possible subsequent update occur, the updater's status is set and + * listeners are called. + * + * Note that this is the correct entry point, regardless of the current state + * of the updater. Although the function name suggests that this function will + * start an update check, it will only do that if we aren't already in the + * update process. Otherwise, it will simply monitor the update process, + * update its own status, and call listeners. + * + * This function is async and will resolve when the update is ready to + * install, or a failure state is reached. + * However, most callers probably don't actually want to track its progress by + * awaiting on this function. More likely, it is desired to kick this function + * off without awaiting and add a listener via addListener. This allows the + * caller to see when the updater is checking for an update, downloading it, + * etc rather than just knowing "now it's running" and "now it's done". + * + * Note that calling this function while this instance is already performing + * or monitoring an update check/download will have no effect. In other words, + * it is only really necessary/useful to call this function when the status is + * `NEVER_CHECKED` or `NO_UPDATES_FOUND`. + */ + async check() { + try { + // We don't want to end up with multiple instances of the same `async` + // functions waiting on the same events, so if we are already busy going + // through the update state, don't enter this function. This must not + // be in the try/catch that sets #updateBusy to false in its finally + // block. + if (this.#updateBusy) { + return; + } + } catch (e) { + this.#onException(e); + } + + try { + this.#updateBusy = true; + this.#update = null; + + if (this.#swapListenerConnected) { + LOG("AppUpdater:check - Removing update-swap listener"); + Services.obs.removeObserver(this, "update-swap"); + this.#swapListenerConnected = false; + } + + if (!AppConstants.MOZ_UPDATER || this.#updateDisabledByPackage) { + LOG( + "AppUpdater:check -" + + "AppConstants.MOZ_UPDATER=" + + AppConstants.MOZ_UPDATER + + "this.#updateDisabledByPackage: " + + this.#updateDisabledByPackage + ); + this.#setStatus(AppUpdater.STATUS.NO_UPDATER); + return; + } + + if (this.aus.disabled) { + LOG("AppUpdater:check - AUS disabled"); + this.#setStatus(AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY); + return; + } + + let updateState = this.aus.currentState; + let stateName = this.aus.getStateName(updateState); + LOG(`AppUpdater:check - currentState=${stateName}`); + + if (updateState == Ci.nsIApplicationUpdateService.STATE_PENDING) { + LOG("AppUpdater:check - ready for restart"); + this.#onReadyToRestart(); + return; + } + + if (this.aus.isOtherInstanceHandlingUpdates) { + LOG("AppUpdater:check - this.aus.isOtherInstanceHandlingUpdates"); + this.#setStatus(AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES); + return; + } + + if (updateState == Ci.nsIApplicationUpdateService.STATE_DOWNLOADING) { + LOG("AppUpdater:check - downloading"); + this.#update = this.um.downloadingUpdate; + await this.#downloadUpdate(); + return; + } + + if (updateState == Ci.nsIApplicationUpdateService.STATE_STAGING) { + LOG("AppUpdater:check - staging"); + this.#update = this.um.readyUpdate; + await this.#awaitStagingComplete(); + return; + } + + // Clear prefs that could prevent a user from discovering available updates. + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + this.#setStatus(AppUpdater.STATUS.CHECKING); + LOG("AppUpdater:check - starting update check"); + let check = this.checker.checkForUpdates(this.checker.FOREGROUND_CHECK); + let result; + try { + result = await makeAbortable(check.result); + } catch (e) { + // If we are aborting, stop the update check on our way out. + if (e instanceof AbortError) { + this.checker.stopCheck(check.id); + } + throw e; + } + + if (!result.checksAllowed) { + // This shouldn't happen. The cases where this can happen should be + // handled specifically, above. + LOG("AppUpdater:check - !checksAllowed; INTERNAL_ERROR"); + this.#setStatus(AppUpdater.STATUS.INTERNAL_ERROR); + return; + } + + if (!result.succeeded) { + LOG("AppUpdater:check - Update check failed; CHECKING_FAILED"); + this.#setStatus(AppUpdater.STATUS.CHECKING_FAILED); + return; + } + + LOG("AppUpdater:check - Update check succeeded"); + this.#update = this.aus.selectUpdate(result.updates); + if (!this.#update) { + LOG("AppUpdater:check - result: NO_UPDATES_FOUND"); + this.#setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND); + return; + } + + if (this.#update.unsupported) { + LOG("AppUpdater:check - result: UNSUPPORTED SYSTEM"); + this.#setStatus(AppUpdater.STATUS.UNSUPPORTED_SYSTEM); + return; + } + + if (!this.aus.canApplyUpdates) { + LOG("AppUpdater:check - result: MANUAL_UPDATE"); + this.#setStatus(AppUpdater.STATUS.MANUAL_UPDATE); + return; + } + + let updateAuto = await makeAbortable( + lazy.UpdateUtils.getAppUpdateAutoEnabled() + ); + if (!updateAuto || this.aus.manualUpdateOnly) { + LOG( + "AppUpdater:check - Need to wait for user approval to start the " + + "download." + ); + + let downloadPermissionPromise = new Promise(resolve => { + this.#permissionToDownloadGivenFn = resolve; + }); + // There are other interfaces through which the user can start the + // download, so we want to listen both for permission, and for the + // download to independently start. + let downloadStartPromise = Promise.race([ + downloadPermissionPromise, + this.aus.stateTransition, + ]); + + this.#setStatus(AppUpdater.STATUS.DOWNLOAD_AND_INSTALL); + + await makeAbortable(downloadStartPromise); + LOG("AppUpdater:check - Got user approval. Proceeding with download"); + // If we resolved because of `aus.stateTransition`, we may actually be + // downloading a different update now. + if (this.um.downloadingUpdate) { + this.#update = this.um.downloadingUpdate; + } + } else { + LOG( + "AppUpdater:check - updateAuto is active and " + + "manualUpdateOnlydateOnly is inactive. Start the download." + ); + } + await this.#downloadUpdate(); + } catch (e) { + this.#onException(e); + } finally { + this.#updateBusy = false; + } + } + + /** + * This only has an effect if the status is `DOWNLOAD_AND_INSTALL`.This + * indicates that the user has configured Firefox not to download updates + * without permission, and we are waiting the user's permission. + * This function should be called if and only if the user's permission was + * given as it will allow the update download to proceed. + */ + allowUpdateDownload() { + if (this.#permissionToDownloadGivenFn) { + this.#permissionToDownloadGivenFn(); + } + } + + // true if updating is disabled because we're running in an app package. + // This is distinct from aus.disabled because we need to avoid + // messages being shown to the user about an "administrator" handling + // updates; packaged apps may be getting updated by an administrator or they + // may not be, and we don't have a good way to tell the difference from here, + // so we err to the side of less confusion for unmanaged users. + get #updateDisabledByPackage() { + return Services.sysinfo.getProperty("isPackagedApp"); + } + + // true when updating in background is enabled. + get #updateStagingEnabled() { + LOG( + "AppUpdater:#updateStagingEnabled" + + "canStageUpdates: " + + this.aus.canStageUpdates + ); + return ( + !this.aus.disabled && + !this.#updateDisabledByPackage && + this.aus.canStageUpdates + ); + } + + /** + * Downloads an update mar or connects to an in-progress download. + * Doesn't resolve until the update is ready to install, or a failure state + * is reached. + */ + async #downloadUpdate() { + this.#setStatus(AppUpdater.STATUS.DOWNLOADING); + + let success = await this.aus.downloadUpdate(this.#update, false); + if (!success) { + LOG("AppUpdater:#downloadUpdate - downloadUpdate failed."); + this.#setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED); + return; + } + + await this.#awaitDownloadComplete(); + } + + /** + * Listens for a download to complete. + * Doesn't resolve until the update is ready to install, or a failure state + * is reached. + */ + async #awaitDownloadComplete() { + let updateState = this.aus.currentState; + if ( + updateState != Ci.nsIApplicationUpdateService.STATE_DOWNLOADING && + updateState != Ci.nsIApplicationUpdateService.STATE_SWAP + ) { + throw new Error( + "AppUpdater:#awaitDownloadComplete invoked in unexpected state: " + + this.aus.getStateName(updateState) + ); + } + + // We may already be in the `DOWNLOADING` state, depending on how we entered + // this function. But we actually want to alert the listeners again even if + // we are because `this.update.selectedPatch` is null early in the + // downloading state, but it should be set by now and listeners may want to + // update UI based on that. + this.#setStatus(AppUpdater.STATUS.DOWNLOADING); + + const updateDownloadProgress = (progress, progressMax) => { + this.#setStatus(AppUpdater.STATUS.DOWNLOADING, progress, progressMax); + }; + + const progressObserver = { + onStartRequest(aRequest) { + LOG( + `AppUpdater:#awaitDownloadComplete.observer.onStartRequest - ` + + `aRequest: ${aRequest}` + ); + }, + + onStatus(aRequest, aStatus, aStatusArg) { + LOG( + `AppUpdater:#awaitDownloadComplete.observer.onStatus ` + + `- aRequest: ${aRequest}, aStatus: ${aStatus}, ` + + `aStatusArg: ${aStatusArg}` + ); + }, + + onProgress(aRequest, aProgress, aProgressMax) { + LOG( + `AppUpdater:#awaitDownloadComplete.observer.onProgress ` + + `- aRequest: ${aRequest}, aProgress: ${aProgress}, ` + + `aProgressMax: ${aProgressMax}` + ); + updateDownloadProgress(aProgress, aProgressMax); + }, + + onStopRequest(aRequest, aStatusCode) { + LOG( + `AppUpdater:#awaitDownloadComplete.observer.onStopRequest ` + + `- aRequest: ${aRequest}, aStatusCode: ${aStatusCode}` + ); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIProgressEventSink", + "nsIRequestObserver", + ]), + }; + + let listenForProgress = + updateState == Ci.nsIApplicationUpdateService.STATE_DOWNLOADING; + + if (listenForProgress) { + this.aus.addDownloadListener(progressObserver); + LOG("AppUpdater:#awaitDownloadComplete - Registered download listener"); + } + + LOG("AppUpdater:#awaitDownloadComplete - Waiting for state transition."); + try { + await makeAbortable(this.aus.stateTransition); + } finally { + if (listenForProgress) { + this.aus.removeDownloadListener(progressObserver); + LOG("AppUpdater:#awaitDownloadComplete - Download listener removed"); + } + } + + updateState = this.aus.currentState; + LOG( + "AppUpdater:#awaitDownloadComplete - State transition seen. New state: " + + this.aus.getStateName(updateState) + ); + + switch (updateState) { + case Ci.nsIApplicationUpdateService.STATE_IDLE: + LOG( + "AppUpdater:#awaitDownloadComplete - Setting status DOWNLOAD_FAILED." + ); + this.#setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED); + break; + case Ci.nsIApplicationUpdateService.STATE_STAGING: + LOG("AppUpdater:#awaitDownloadComplete - awaiting staging completion."); + await this.#awaitStagingComplete(); + break; + case Ci.nsIApplicationUpdateService.STATE_PENDING: + LOG("AppUpdater:#awaitDownloadComplete - ready to restart."); + this.#onReadyToRestart(); + break; + default: + LOG( + "AppUpdater:#awaitDownloadComplete - Setting status INTERNAL_ERROR." + ); + this.#setStatus(AppUpdater.STATUS.INTERNAL_ERROR); + break; + } + } + + /** + * This function registers an observer that watches for the staging process + * to complete. Once it does, it sets the status to either request that the + * user restarts to install the update on success, request that the user + * manually download and install the newer version, or automatically download + * a complete update if applicable. + * Doesn't resolve until the update is ready to install, or a failure state + * is reached. + */ + async #awaitStagingComplete() { + let updateState = this.aus.currentState; + if (updateState != Ci.nsIApplicationUpdateService.STATE_STAGING) { + throw new Error( + "AppUpdater:#awaitStagingComplete invoked in unexpected state: " + + this.aus.getStateName(updateState) + ); + } + + LOG("AppUpdater:#awaitStagingComplete - Setting status STAGING."); + this.#setStatus(AppUpdater.STATUS.STAGING); + + LOG("AppUpdater:#awaitStagingComplete - Waiting for state transition."); + await makeAbortable(this.aus.stateTransition); + + updateState = this.aus.currentState; + LOG( + "AppUpdater:#awaitStagingComplete - State transition seen. New state: " + + this.aus.getStateName(updateState) + ); + + switch (updateState) { + case Ci.nsIApplicationUpdateService.STATE_PENDING: + LOG("AppUpdater:#awaitStagingComplete - ready for restart"); + this.#onReadyToRestart(); + break; + case Ci.nsIApplicationUpdateService.STATE_IDLE: + LOG("AppUpdater:#awaitStagingComplete - DOWNLOAD_FAILED"); + this.#setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED); + break; + case Ci.nsIApplicationUpdateService.STATE_DOWNLOADING: + // We've fallen back to downloading the complete update because the + // partial update failed to be staged. Return to the downloading stage. + LOG( + "AppUpdater:#awaitStagingComplete - Partial update must have " + + "failed to stage. Downloading complete update." + ); + await this.#awaitDownloadComplete(); + break; + default: + LOG( + "AppUpdater:#awaitStagingComplete - Setting status INTERNAL_ERROR." + ); + this.#setStatus(AppUpdater.STATUS.INTERNAL_ERROR); + break; + } + } + + #onReadyToRestart() { + let updateState = this.aus.currentState; + if (updateState != Ci.nsIApplicationUpdateService.STATE_PENDING) { + throw new Error( + "AppUpdater:#onReadyToRestart invoked in unexpected state: " + + this.aus.getStateName(updateState) + ); + } + + LOG("AppUpdater:#onReadyToRestart - Setting status READY_FOR_RESTART."); + if (this.#swapListenerConnected) { + LOG( + "AppUpdater:#onReadyToRestart - update-swap listener already attached" + ); + } else { + this.#swapListenerConnected = true; + LOG("AppUpdater:#onReadyToRestart - Attaching update-swap listener"); + Services.obs.addObserver(this, "update-swap", /* ownsWeak */ true); + } + this.#setStatus(AppUpdater.STATUS.READY_FOR_RESTART); + } + + /** + * Stops the current check for updates and any ongoing download. + * + * If this is called before `AppUpdater.check()` is called or after it + * resolves, this should have no effect. If this is called while `check()` is + * still running, `AppUpdater` will return to the NEVER_CHECKED state. We + * don't really want to leave it in any of the intermediary states after we + * have disconnected all the listeners that would allow those states to ever + * change. + */ + stop() { + LOG("AppUpdater:stop called"); + if (this.#swapListenerConnected) { + LOG("AppUpdater:stop - Removing update-swap listener"); + Services.obs.removeObserver(this, "update-swap"); + this.#swapListenerConnected = false; + } + abortAllPromises(); + } + + /** + * {AppUpdater.STATUS} The status of the current check or update. + * + * Note that until AppUpdater.check has been called, this will always be set + * to NEVER_CHECKED. + */ + get status() { + return this.#status; + } + + /** + * Adds a listener function that will be called back on status changes as + * different stages of updates occur. The function will be called without + * arguments for most status changes; see the comments around the STATUS value + * definitions below. This is safe to call multiple times with the same + * function. It will be added only once. + * + * @param {function} listener + * The listener function to add. + */ + addListener(listener) { + this.#listeners.add(listener); + } + + /** + * Removes a listener. This is safe to call multiple times with the same + * function, or with a function that was never added. + * + * @param {function} listener + * The listener function to remove. + */ + removeListener(listener) { + this.#listeners.delete(listener); + } + + /** + * Sets the updater's current status and calls listeners. + * + * @param {AppUpdater.STATUS} status + * The new updater status. + * @param {*} listenerArgs + * Arguments to pass to listeners. + */ + #setStatus(status, ...listenerArgs) { + this.#status = status; + for (let listener of this.#listeners) { + listener(status, ...listenerArgs); + } + return status; + } + + observe(subject, topic, status) { + LOG( + "AppUpdater:observe " + + "- subject: " + + subject + + ", topic: " + + topic + + ", status: " + + status + ); + switch (topic) { + case "update-swap": + // This is asynchronous, but we don't really want to wait for it in this + // observer. + this.#handleUpdateSwap(); + break; + case "nsPref:changed": + if ( + status == PREF_APP_UPDATE_LOG || + status == PREF_APP_UPDATE_LOG_FILE + ) { + lazy.gLogEnabled; // Assigning this before it is lazy-loaded is an error. + lazy.gLogEnabled = + Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) || + Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false); + } + break; + } + } + + async #handleUpdateSwap() { + try { + // This must not be in the try/catch that sets #updateBusy to `false` in + // its finally block. + // There really shouldn't be any way to enter this function when + // `#updateBusy` is `true`. But let's just be safe because we don't want + // to ever end up with two things running at once. + if (this.#updateBusy) { + return; + } + } catch (e) { + this.#onException(e); + } + + try { + this.#updateBusy = true; + + // During an update swap, the new update will initially be stored in + // `downloadingUpdate`. Part way through, it will be moved into + // `readyUpdate` and `downloadingUpdate` will be set to `null`. + this.#update = this.um.downloadingUpdate; + if (!this.#update) { + this.#update = this.um.readyUpdate; + } + + await this.#awaitDownloadComplete(); + } catch (e) { + this.#onException(e); + } finally { + this.#updateBusy = false; + } + } +} + +XPCOMUtils.defineLazyServiceGetter( + AppUpdater.prototype, + "aus", + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService" +); +XPCOMUtils.defineLazyServiceGetter( + AppUpdater.prototype, + "checker", + "@mozilla.org/updates/update-checker;1", + "nsIUpdateChecker" +); +XPCOMUtils.defineLazyServiceGetter( + AppUpdater.prototype, + "um", + "@mozilla.org/updates/update-manager;1", + "nsIUpdateManager" +); + +AppUpdater.STATUS = { + // Updates are allowed and there's no downloaded or staged update, but the + // AppUpdater hasn't checked for updates yet, so it doesn't know more than + // that. + NEVER_CHECKED: 0, + + // The updater isn't available (AppConstants.MOZ_UPDATER is falsey). + NO_UPDATER: 1, + + // "appUpdate" is not allowed by policy. + UPDATE_DISABLED_BY_POLICY: 2, + + // Another app instance is handling updates. + OTHER_INSTANCE_HANDLING_UPDATES: 3, + + // There's an update, but it's not supported on this system. + UNSUPPORTED_SYSTEM: 4, + + // The user must apply updates manually. + MANUAL_UPDATE: 5, + + // The AppUpdater is checking for updates. + CHECKING: 6, + + // The AppUpdater checked for updates and none were found. + NO_UPDATES_FOUND: 7, + + // The AppUpdater is downloading an update. Listeners are notified of this + // status as a download starts. They are also notified on download progress, + // and in that case they are passed two arguments: the current download + // progress and the total download size. + DOWNLOADING: 8, + + // The AppUpdater tried to download an update but it failed. + DOWNLOAD_FAILED: 9, + + // There's an update available, but the user wants us to ask them to download + // and install it. + DOWNLOAD_AND_INSTALL: 10, + + // An update is staging. + STAGING: 11, + + // An update is downloaded and staged and will be applied on restart. + READY_FOR_RESTART: 12, + + // Essential components of the updater are failing and preventing us from + // updating. + INTERNAL_ERROR: 13, + + // Failed to check for updates, network timeout, dns errors could cause this + CHECKING_FAILED: 14, + + /** + * Is the given `status` a terminal state in the update state machine? + * + * A terminal state means that the `check()` method has completed. + * + * N.b.: `DOWNLOAD_AND_INSTALL` is not considered terminal because the normal + * flow is that Firefox will show UI prompting the user to install, and when + * the user interacts, the `check()` method will continue through the update + * state machine. + * + * @returns {boolean} `true` if `status` is terminal. + */ + isTerminalStatus(status) { + return ![ + AppUpdater.STATUS.CHECKING, + AppUpdater.STATUS.DOWNLOAD_AND_INSTALL, + AppUpdater.STATUS.DOWNLOADING, + AppUpdater.STATUS.NEVER_CHECKED, + AppUpdater.STATUS.STAGING, + ].includes(status); + }, + + /** + * Turn the given `status` into a string for debugging. + * + * @returns {?string} representation of given numerical `status`. + */ + debugStringFor(status) { + for (let [k, v] of Object.entries(AppUpdater.STATUS)) { + if (v == status) { + return k; + } + } + return null; + }, +}; + +/** + * Logs a string to the error console. If enabled, also logs to the update + * messages file. + * @param string + * The string to write to the error console. + */ +function LOG(string) { + if (lazy.gLogEnabled) { + dump("*** AUS:AUM " + string + "\n"); + if (!Cu.isInAutomation) { + Services.console.logStringMessage("AUS:AUM " + string); + } + + if (lazy.gLogfileEnabled) { + if (!gLogfileOutputStream) { + let logfile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile); + logfile.append(FILE_UPDATE_MESSAGES); + gLogfileOutputStream = FileUtils.openAtomicFileOutputStream(logfile); + } + + try { + let encoded = new TextEncoder().encode(string + "\n"); + gLogfileOutputStream.write(encoded, encoded.length); + gLogfileOutputStream.flush(); + } catch (e) { + dump("*** AUS:AUM Unable to write to messages file: " + e + "\n"); + Services.console.logStringMessage( + "AUS:AUM Unable to write to messages file: " + e + ); + } + } + } +} diff --git a/toolkit/mozapps/update/BackgroundTask_backgroundupdate.sys.mjs b/toolkit/mozapps/update/BackgroundTask_backgroundupdate.sys.mjs new file mode 100644 index 0000000000..e94039c546 --- /dev/null +++ b/toolkit/mozapps/update/BackgroundTask_backgroundupdate.sys.mjs @@ -0,0 +1,420 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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 { BackgroundUpdate } = ChromeUtils.import( + "resource://gre/modules/BackgroundUpdate.jsm" +); +const { EXIT_CODE } = BackgroundUpdate; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AppUpdater: "resource://gre/modules/AppUpdater.jsm", + ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "UpdateService", + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService" +); + +XPCOMUtils.defineLazyGetter(lazy, "log", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + let consoleOptions = { + // tip: set maxLogLevel to "debug" and use log.debug() to create detailed + // messages during development. See LOG_LEVELS in Console.sys.mjs for details. + maxLogLevel: "error", + maxLogLevelPref: "app.update.background.loglevel", + prefix: "BackgroundUpdate", + }; + return new ConsoleAPI(consoleOptions); +}); + +export const backgroundTaskTimeoutSec = Services.prefs.getIntPref( + "app.update.background.timeoutSec", + 10 * 60 +); + +/** + * Verify that pre-conditions to update this installation (both persistent and + * transient) are fulfilled, and if they are all fulfilled, pump the update + * loop. + * + * This means checking for, downloading, and potentially applying updates. + * + * @returns {boolean} - `true` if an update loop was completed. + */ +async function _attemptBackgroundUpdate() { + let SLUG = "_attemptBackgroundUpdate"; + + // Here's where we do `post-update-processing`. Creating the stub invokes the + // `UpdateServiceStub()` constructor, which handles various migrations (which should not be + // necessary, but we want to run for consistency and any migrations added in the future) and then + // dispatches `post-update-processing` (if appropriate). We want to do this very early, so that + // the real update service is in its fully initialized state before any usage. + lazy.log.debug( + `${SLUG}: creating UpdateServiceStub() for "post-update-processing"` + ); + Cc["@mozilla.org/updates/update-service-stub;1"].createInstance( + Ci.nsISupports + ); + + lazy.log.debug( + `${SLUG}: checking for preconditions necessary to update this installation` + ); + let reasons = await BackgroundUpdate._reasonsToNotUpdateInstallation(); + + if (BackgroundUpdate._force()) { + // We want to allow developers and testers to monkey with the system. + lazy.log.debug( + `${SLUG}: app.update.background.force=true, ignoring reasons: ${JSON.stringify( + reasons + )}` + ); + reasons = []; + } + + reasons.sort(); + for (let reason of reasons) { + Glean.backgroundUpdate.reasons.add(reason); + } + + let enabled = !reasons.length; + if (!enabled) { + lazy.log.info( + `${SLUG}: not running background update task: '${JSON.stringify( + reasons + )}'` + ); + + return false; + } + + let result = new Promise(resolve => { + let appUpdater = new lazy.AppUpdater(); + + let _appUpdaterListener = (status, progress, progressMax) => { + let stringStatus = lazy.AppUpdater.STATUS.debugStringFor(status); + Glean.backgroundUpdate.states.add(stringStatus); + Glean.backgroundUpdate.finalState.set(stringStatus); + + if (lazy.AppUpdater.STATUS.isTerminalStatus(status)) { + lazy.log.debug( + `${SLUG}: background update transitioned to terminal status ${status}: ${stringStatus}` + ); + appUpdater.removeListener(_appUpdaterListener); + appUpdater.stop(); + resolve(true); + } else if (status == lazy.AppUpdater.STATUS.CHECKING) { + // The usual initial flow for the Background Update Task is to kick off + // the update download and immediately exit. For consistency, we are + // going to enforce this flow. So if we are just now checking for + // updates, we will limit the updater such that it cannot start staging, + // even if we immediately download the entire update. + lazy.log.debug( + `${SLUG}: This session will be limited to downloading updates only.` + ); + lazy.UpdateService.onlyDownloadUpdatesThisSession = true; + } else if ( + status == lazy.AppUpdater.STATUS.DOWNLOADING && + (lazy.UpdateService.onlyDownloadUpdatesThisSession || + (progress !== undefined && progressMax !== undefined)) + ) { + // We get a DOWNLOADING callback with no progress or progressMax values + // when we initially switch to the DOWNLOADING state. But when we get + // onProgress notifications, progress and progressMax will be defined. + // Remember to keep in mind that progressMax is a required value that + // we can count on being meaningful, but it will be set to -1 for BITS + // transfers that haven't begun yet. + if ( + lazy.UpdateService.onlyDownloadUpdatesThisSession || + progressMax < 0 || + progress != progressMax + ) { + lazy.log.debug( + `${SLUG}: Download in progress. Exiting task while download ` + + `transfers` + ); + // If the download is still in progress, we don't want the Background + // Update Task to hang around waiting for it to complete. + lazy.UpdateService.onlyDownloadUpdatesThisSession = true; + + appUpdater.removeListener(_appUpdaterListener); + appUpdater.stop(); + resolve(true); + } else { + lazy.log.debug(`${SLUG}: Download has completed!`); + } + } else { + lazy.log.debug( + `${SLUG}: background update transitioned to status ${status}: ${stringStatus}` + ); + } + }; + appUpdater.addListener(_appUpdaterListener); + + appUpdater.check(); + }); + + return result; +} + +/** + * Maybe submit a "background-update" custom Glean ping. + * + * If data reporting upload in general is enabled Glean will submit a ping. To determine if + * telemetry is enabled, Glean will look at the relevant pref, which was mirrored from the default + * profile. Note that the Firefox policy mechanism will manage this pref, locking it to particular + * values as appropriate. + */ +export async function maybeSubmitBackgroundUpdatePing() { + let SLUG = "maybeSubmitBackgroundUpdatePing"; + + // It should be possible to turn AUSTLMY data into Glean data, but mapping histograms isn't + // trivial, so we don't do it at this time. Bug 1703313. + + // Including a reason allows to differentiate pings sent as part of the task + // and pings queued and sent by Glean on a different schedule. + GleanPings.backgroundUpdate.submit("backgroundupdate_task"); + + lazy.log.info(`${SLUG}: submitted "background-update" ping`); +} + +export async function runBackgroundTask(commandLine) { + let SLUG = "runBackgroundTask"; + lazy.log.error(`${SLUG}: backgroundupdate`); + + // Help debugging. This is a pared down version of + // `dataProviders.application` in `Troubleshoot.sys.mjs`. When adding to this + // debugging data, try to follow the form from that module. + let data = { + name: Services.appinfo.name, + osVersion: + Services.sysinfo.getProperty("name") + + " " + + Services.sysinfo.getProperty("version") + + " " + + Services.sysinfo.getProperty("build"), + version: AppConstants.MOZ_APP_VERSION_DISPLAY, + buildID: Services.appinfo.appBuildID, + distributionID: Services.prefs + .getDefaultBranch("") + .getCharPref("distribution.id", ""), + updateChannel: lazy.UpdateUtils.UpdateChannel, + UpdRootD: Services.dirsvc.get("UpdRootD", Ci.nsIFile).path, + }; + lazy.log.debug(`${SLUG}: current configuration`, data); + + // Other instances running are a transient precondition (during this invocation). We'd prefer to + // check this later, as a reason for not updating, but Glean is not tested in multi-process + // environments and while its storage (backed by rkv) can in theory support multiple processes, it + // is not clear that it in fact does support multiple processes. So we are conservative here. + // There is a potential time-of-check/time-of-use race condition here, but if process B starts + // after we pass this test, that process should exit after it gets to this check, avoiding + // multiple processes using the same Glean storage. If and when more and longer-running + // background tasks become common, we may need to be more fine-grained and share just the Glean + // storage resource. + lazy.log.debug(`${SLUG}: checking if other instance is running`); + let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + if (syncManager.isOtherInstanceRunning()) { + lazy.log.error(`${SLUG}: another instance is running`); + return EXIT_CODE.OTHER_INSTANCE; + } + + // Here we mirror specific prefs from the default profile into our temporary profile. We want to + // do this early because some of the prefs may impact internals such as log levels. Generally, + // however, we want prefs from the default profile to not impact the mechanics of checking for, + // downloading, and applying updates, since such prefs should be be per-installation prefs, using + // the mechanisms of Bug 1691486. Sadly using this mechanism for many relevant prefs (namely + // `app.update.BITS.enabled` and `app.update.service.enabled`) is difficult: see Bug 1657533. + // + // We also read any Nimbus targeting snapshot from the default profile. + let defaultProfileTargetingSnapshot = {}; + try { + let defaultProfilePrefs; + await lazy.BackgroundTasksUtils.withProfileLock(async lock => { + let predicate = name => { + return ( + name.startsWith("app.update.") || // For obvious reasons. + name.startsWith("datareporting.") || // For Glean. + name.startsWith("logging.") || // For Glean. + name.startsWith("telemetry.fog.") || // For Glean. + name.startsWith("app.partner.") || // For our metrics. + name === "app.shield.optoutstudies.enabled" || // For Nimbus. + name === "services.settings.server" || // For Remote Settings via Nimbus. + name === "services.settings.preview_enabled" || // For Remote Settings via Nimbus. + name === "messaging-system.rsexperimentloader.collection_id" // For Firefox Messaging System. + ); + }; + + defaultProfilePrefs = await lazy.BackgroundTasksUtils.readPreferences( + predicate, + lock + ); + let telemetryClientID = await lazy.BackgroundTasksUtils.readTelemetryClientID( + lock + ); + Glean.backgroundUpdate.clientId.set(telemetryClientID); + + // Read targeting snapshot, collect background update specific telemetry. Never throws. + defaultProfileTargetingSnapshot = await BackgroundUpdate.readFirefoxMessagingSystemTargetingSnapshot( + lock + ); + }); + + for (let [name, value] of Object.entries(defaultProfilePrefs)) { + switch (typeof value) { + case "boolean": + Services.prefs.setBoolPref(name, value); + break; + case "number": + Services.prefs.setIntPref(name, value); + break; + case "string": + Services.prefs.setCharPref(name, value); + break; + default: + throw new Error( + `Pref from default profile with name "${name}" has unrecognized type` + ); + } + } + } catch (e) { + if (!lazy.BackgroundTasksUtils.hasDefaultProfile()) { + lazy.log.error(`${SLUG}: caught exception; no default profile exists`, e); + return EXIT_CODE.DEFAULT_PROFILE_DOES_NOT_EXIST; + } + + if (e.name == "CannotLockProfileError") { + lazy.log.error( + `${SLUG}: caught exception; could not lock default profile`, + e + ); + return EXIT_CODE.DEFAULT_PROFILE_CANNOT_BE_LOCKED; + } + + lazy.log.error( + `${SLUG}: caught exception reading preferences and telemetry client ID from default profile`, + e + ); + return EXIT_CODE.DEFAULT_PROFILE_CANNOT_BE_READ; + } + + // Now that we have prefs from the default profile, we can configure Firefox-on-Glean. + + // Glean has a preinit queue for metric operations that happen before init, so + // this is safe. We want to have these metrics set before the first possible + // time we might send (built-in) pings. + await BackgroundUpdate.recordUpdateEnvironment(); + + // The final leaf is for the benefit of `FileUtils`. To help debugging, use + // the `GLEAN_LOG_PINGS` and `GLEAN_DEBUG_VIEW_TAG` environment variables: see + // https://mozilla.github.io/glean/book/user/debugging/index.html. + let gleanRoot = lazy.FileUtils.getFile("UpdRootD", [ + "backgroundupdate", + "datareporting", + "glean", + "__dummy__", + ]).parent.path; + Services.fog.initializeFOG(gleanRoot, "firefox.desktop.background.update"); + + // For convenience, mirror our loglevel. + let logLevel = Services.prefs.getCharPref( + "app.update.background.loglevel", + "error" + ); + const logLevelPrefs = [ + "browser.newtabpage.activity-stream.asrouter.debugLogLevel", + "messaging-system.log", + "services.settings.loglevel", + "toolkit.backgroundtasks.loglevel", + ]; + for (let logLevelPref of logLevelPrefs) { + lazy.log.info(`${SLUG}: setting ${logLevelPref}=${logLevel}`); + Services.prefs.setCharPref(logLevelPref, logLevel); + } + + // The langpack updating mechanism expects the addons manager, but in background task mode, the + // addons manager is not present. Since we can't update langpacks from the background task + // temporary profile, we disable the langpack updating mechanism entirely. This relies on the + // default profile being the only profile that schedules the OS-level background task and ensuring + // the task is not scheduled when langpacks are present. Non-default profiles that have langpacks + // installed may experience the issues that motivated Bug 1647443. If this turns out to be a + // significant problem in the wild, we could store more information about profiles and their + // active langpacks to disable background updates in more cases, maybe in per-installation prefs. + Services.prefs.setBoolPref("app.update.langpack.enabled", false); + + let result = EXIT_CODE.SUCCESS; + + let stringStatus = lazy.AppUpdater.STATUS.debugStringFor( + lazy.AppUpdater.STATUS.NEVER_CHECKED + ); + Glean.backgroundUpdate.states.add(stringStatus); + Glean.backgroundUpdate.finalState.set(stringStatus); + + try { + await _attemptBackgroundUpdate(); + + lazy.log.info(`${SLUG}: attempted background update`); + Glean.backgroundUpdate.exitCodeSuccess.set(true); + + try { + // Now that we've pumped the update loop, we can start Nimbus and the Firefox Messaging System + // and see if we should message the user. This minimizes the risk of messaging impacting the + // function of the background update system. + await lazy.BackgroundTasksUtils.enableNimbus( + commandLine, + defaultProfileTargetingSnapshot.environment + ); + + await lazy.BackgroundTasksUtils.enableFirefoxMessagingSystem( + defaultProfileTargetingSnapshot.environment + ); + } catch (f) { + // Try to make it easy to witness errors in this system. We can pass through any exception + // without disrupting (future) background updates. + // + // Most meaningful issues with the Nimbus/experiments system will be reported via Glean + // events. + lazy.log.warn( + `${SLUG}: exception raised from Nimbus/Firefox Messaging System`, + f + ); + throw f; + } + } catch (e) { + // TODO: in the future, we might want to classify failures into transient and persistent and + // backoff the update task in the face of continuous persistent errors. + lazy.log.error(`${SLUG}: caught exception attempting background update`, e); + + result = EXIT_CODE.EXCEPTION; + Glean.backgroundUpdate.exitCodeException.set(true); + } finally { + // This is the point to report telemetry, assuming that the default profile's data reporting + // configuration allows it. + await maybeSubmitBackgroundUpdatePing(); + } + + // TODO: ensure the update service has persisted its state before we exit. Bug 1700846. + // TODO: ensure that Glean's upload mechanism is aware of Gecko shutdown. Bug 1703572. + await lazy.ExtensionUtils.promiseTimeout(500); + + return result; +} diff --git a/toolkit/mozapps/update/BackgroundUpdate.jsm b/toolkit/mozapps/update/BackgroundUpdate.jsm new file mode 100644 index 0000000000..12cb764428 --- /dev/null +++ b/toolkit/mozapps/update/BackgroundUpdate.jsm @@ -0,0 +1,813 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* 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"; + +var EXPORTED_SYMBOLS = ["BackgroundUpdate"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { EXIT_CODE } = ChromeUtils.import( + "resource://gre/modules/BackgroundTasksManager.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + JSONFile: "resource://gre/modules/JSONFile.sys.mjs", + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.jsm", + ASRouterTargeting: "resource://activity-stream/lib/ASRouterTargeting.jsm", + BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.jsm", + TaskScheduler: "resource://gre/modules/TaskScheduler.jsm", +}); + +XPCOMUtils.defineLazyGetter(lazy, "log", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + let consoleOptions = { + // tip: set maxLogLevel to "debug" and use log.debug() to create detailed + // messages during development. See LOG_LEVELS in Console.sys.mjs for details. + maxLogLevel: "error", + maxLogLevelPref: "app.update.background.loglevel", + prefix: "BackgroundUpdate", + }; + return new ConsoleAPI(consoleOptions); +}); + +XPCOMUtils.defineLazyGetter(lazy, "localization", () => { + return new Localization( + ["branding/brand.ftl", "toolkit/updates/backgroundupdate.ftl"], + true + ); +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "UpdateService", + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService" +); + +// We may want to change the definition of the task over time. When we do this, +// we need to remove and re-register the task. We will make sure this happens +// by storing the installed version number of the task to a pref and comparing +// that version number to the current version. If they aren't equal, we know +// that we have to re-register the task. +const TASK_DEF_CURRENT_VERSION = 3; +const TASK_INSTALLED_VERSION_PREF = + "app.update.background.lastInstalledTaskVersion"; + +var BackgroundUpdate = { + _initialized: false, + + get taskId() { + let taskId = "backgroundupdate"; + if (AppConstants.platform == "win") { + // In the future, we might lift this to TaskScheduler Win impl, so that + // all tasks associated with this installation look consistent in the + // Windows Task Scheduler UI. + taskId = `${AppConstants.MOZ_APP_DISPLAYNAME_DO_NOT_USE} Background Update`; + } + return taskId; + }, + + /** + * Whether this installation has an App and a GRE omnijar. + * + * Installations without an omnijar are generally developer builds and should not be updated. + * + * @returns {boolean} - true if this installation has an App and a GRE omnijar. + */ + async _hasOmnijar() { + const appOmniJar = lazy.FileUtils.getFile("XCurProcD", [ + AppConstants.OMNIJAR_NAME, + ]); + const greOmniJar = lazy.FileUtils.getFile("GreD", [ + AppConstants.OMNIJAR_NAME, + ]); + + let bothExist = + (await IOUtils.exists(appOmniJar.path)) && + (await IOUtils.exists(greOmniJar.path)); + + return bothExist; + }, + + _force() { + // We want to allow developers and testers to monkey with the system. + return Services.prefs.getBoolPref("app.update.background.force", false); + }, + + /** + * Check eligibility criteria determining if this installation should be updated using the + * background updater. + * + * These reasons should not factor in transient reasons, for example if there are currently multiple + * Firefox instances running. + * + * Both the browser proper and the backgroundupdate background task invoke this function, so avoid + * using profile specifics here. Profile specifics that the background task specifically sources + * from the default profile are available here. + * + * @returns [string] - descriptions of failed criteria; empty if all criteria were met. + */ + async _reasonsToNotUpdateInstallation() { + let SLUG = "_reasonsToNotUpdateInstallation"; + let reasons = []; + + lazy.log.debug(`${SLUG}: checking app.update.auto`); + let updateAuto = await lazy.UpdateUtils.getAppUpdateAutoEnabled(); + if (!updateAuto) { + reasons.push(this.REASON.NO_APP_UPDATE_AUTO); + } + + lazy.log.debug(`${SLUG}: checking app.update.background.enabled`); + let updateBackground = await lazy.UpdateUtils.readUpdateConfigSetting( + "app.update.background.enabled" + ); + if (!updateBackground) { + reasons.push(this.REASON.NO_APP_UPDATE_BACKGROUND_ENABLED); + } + + const bts = + "@mozilla.org/backgroundtasks;1" in Cc && + Cc["@mozilla.org/backgroundtasks;1"].getService(Ci.nsIBackgroundTasks); + + lazy.log.debug(`${SLUG}: checking for MOZ_BACKGROUNDTASKS`); + if (!AppConstants.MOZ_BACKGROUNDTASKS || !bts) { + reasons.push(this.REASON.NO_MOZ_BACKGROUNDTASKS); + } + + // The methods exposed by the update service named like `canX` answer the + // question "can I do action X RIGHT NOW", where-as we want the variants + // named like `canUsuallyX` to answer the question "can I usually do X, now + // and in the future". + let updateService = Cc["@mozilla.org/updates/update-service;1"].getService( + Ci.nsIApplicationUpdateService + ); + + lazy.log.debug( + `${SLUG}: checking that updates are not disabled by policy, testing ` + + `configuration, or abnormal runtime environment` + ); + if (!updateService.canUsuallyCheckForUpdates) { + reasons.push(this.REASON.CANNOT_USUALLY_CHECK); + } + + lazy.log.debug( + `${SLUG}: checking that we can make progress: updates can stage and/or apply` + ); + if ( + !updateService.canUsuallyStageUpdates && + !updateService.canUsuallyApplyUpdates + ) { + reasons.push(this.REASON.CANNOT_USUALLY_STAGE_AND_CANNOT_USUALLY_APPLY); + } + + if (AppConstants.platform == "win") { + if (!updateService.canUsuallyUseBits) { + // There's no technical reason to require BITS, but the experience + // without BITS will be worse because the background tasks will run + // while downloading, consuming valuable resources. + reasons.push(this.REASON.WINDOWS_CANNOT_USUALLY_USE_BITS); + } + } + + lazy.log.debug(`${SLUG}: checking that this installation has an omnijar`); + if (!(await this._hasOmnijar())) { + reasons.push(this.REASON.NO_OMNIJAR); + } + + if (updateService.manualUpdateOnly) { + reasons.push(this.REASON.MANUAL_UPDATE_ONLY); + } + + return reasons; + }, + + /** + * Check if this particular profile should schedule tasks to update this installation using the + * background updater. + * + * Only the browser proper should invoke this function, not background tasks, so this is the place + * to use profile specifics. + * + * @returns [string] - descriptions of failed criteria; empty if all criteria were met. + */ + async _reasonsToNotScheduleUpdates() { + let SLUG = "_reasonsToNotScheduleUpdates"; + let reasons = []; + + const bts = + "@mozilla.org/backgroundtasks;1" in Cc && + Cc["@mozilla.org/backgroundtasks;1"].getService(Ci.nsIBackgroundTasks); + + if (bts && bts.isBackgroundTaskMode) { + throw new Components.Exception( + `Not available in --backgroundtask mode`, + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + + // No default profile happens under xpcshell but also when running local + // builds. It's unexpected in the wild so we track it separately. + if (!lazy.BackgroundTasksUtils.hasDefaultProfile()) { + reasons.push(this.REASON.NO_DEFAULT_PROFILE_EXISTS); + } + + if (!lazy.BackgroundTasksUtils.currentProfileIsDefaultProfile()) { + reasons.push(this.REASON.NOT_DEFAULT_PROFILE); + } + + lazy.log.debug(`${SLUG}: checking app.update.langpack.enabled`); + let updateLangpack = Services.prefs.getBoolPref( + "app.update.langpack.enabled", + true + ); + if (updateLangpack) { + lazy.log.debug( + `${SLUG}: app.update.langpack.enabled=true, checking that no langpacks are installed` + ); + + let langpacks = await lazy.AddonManager.getAddonsByTypes(["locale"]); + lazy.log.debug(`${langpacks.length} langpacks installed`); + if (langpacks.length) { + reasons.push(this.REASON.LANGPACK_INSTALLED); + } + } + + let serviceRegKeyExists; + try { + serviceRegKeyExists = Cc["@mozilla.org/updates/update-processor;1"] + .createInstance(Ci.nsIUpdateProcessor) + .getServiceRegKeyExists(); + } catch (ex) { + lazy.log.error( + `${SLUG}: Failed to check for Maintenance Service Registry Key: ${ex}` + ); + serviceRegKeyExists = false; + } + if (!serviceRegKeyExists) { + reasons.push(this.REASON.SERVICE_REGISTRY_KEY_MISSING); + } + + return reasons; + }, + + /** + * Register a background update task. + * + * @param {string} [taskId] + * The task identifier; defaults to the platform-specific background update task ID. + * @return {object} non-null if the background task was registered. + */ + async _registerBackgroundUpdateTask(taskId = this.taskId) { + let binary = Services.dirsvc.get("XREExeF", Ci.nsIFile); + let args = [ + "--MOZ_LOG", + // Note: `maxsize:1` means 1Mb total size, trimmed to 512kb on overflow. + "sync,prependheader,timestamp,append,maxsize:1,Dump:5", + "--MOZ_LOG_FILE", + // The full path might hit command line length limits, but also makes it + // much easier to find the relevant log file when starting from the + // Windows Task Scheduler UI. + lazy.FileUtils.getFile("UpdRootD", ["backgroundupdate.moz_log"]).path, + "--backgroundtask", + "backgroundupdate", + ]; + + let workingDirectory = lazy.FileUtils.getDir("UpdRootD", [], true).path; + + let description = await lazy.localization.formatValue( + "backgroundupdate-task-description" + ); + + // Let the task run for a maximum of 20 minutes before the task scheduler + // stops it. + let executionTimeoutSec = 20 * 60; + + let result = await lazy.TaskScheduler.registerTask( + taskId, + binary.path, + // Keep this default in sync with the preference in firefox.js. + Services.prefs.getIntPref("app.update.background.interval", 60 * 60 * 7), + { + workingDirectory, + args, + description, + executionTimeoutSec, + } + ); + + Services.prefs.setIntPref( + TASK_INSTALLED_VERSION_PREF, + TASK_DEF_CURRENT_VERSION + ); + + return result; + }, + + /** + * Background Update is controlled by the per-installation pref + * "app.update.background.enabled". When Background Update was still in the + * experimental phase, the default value of this pref may have been changed. + * Now that the feature has been rolled out, we need to make sure that the + * desired default value is restored. + */ + async ensureExperimentToRolloutTransitionPerformed() { + if (!lazy.UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED) { + return; + } + const transitionPerformedPref = "app.update.background.rolledout"; + if (Services.prefs.getBoolPref(transitionPerformedPref, false)) { + // writeUpdateConfigSetting serializes access to the config file. Because + // of this, we can safely return here without worrying about another call + // to this function that might still be in progress. + return; + } + Services.prefs.setBoolPref(transitionPerformedPref, true); + + const defaultValue = + lazy.UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"] + .defaultValue; + await lazy.UpdateUtils.writeUpdateConfigSetting( + "app.update.background.enabled", + defaultValue, + { setDefaultOnly: true } + ); + + // To be thorough, remove any traces of the pref that used to control the + // default value that we just set. We don't want any users to have the + // impression that that pref is still useful. + Services.prefs.clearUserPref("app.update.background.scheduling.enabled"); + }, + + async observe(subject, topic, data) { + let whatChanged; + switch (topic) { + case "nsPref:changed": + whatChanged = `per-profile pref ${data}`; + break; + + case "auto-update-config-change": + whatChanged = `per-installation pref app.update.auto`; + break; + + case "background-update-config-change": + whatChanged = `per-installation pref app.update.background.enabled`; + break; + } + + lazy.log.debug( + `observe: ${whatChanged} may have changed; invoking maybeScheduleBackgroundUpdateTask` + ); + return this.maybeScheduleBackgroundUpdateTask(); + }, + + /** + * Maybe schedule (or unschedule) background tasks using OS-level task scheduling mechanisms. + * + * @return {boolean} true if a task is now scheduled, false otherwise. + */ + async maybeScheduleBackgroundUpdateTask() { + let SLUG = "maybeScheduleBackgroundUpdateTask"; + + await this.ensureExperimentToRolloutTransitionPerformed(); + + lazy.log.info( + `${SLUG}: checking eligibility before scheduling background update task` + ); + + let previousEnabled; + let successfullyReadPrevious; + try { + previousEnabled = await lazy.TaskScheduler.taskExists(this.taskId); + successfullyReadPrevious = true; + } catch (ex) { + successfullyReadPrevious = false; + } + + const previousReasons = Services.prefs.getCharPref( + "app.update.background.previous.reasons", + null + ); + + if (!this._initialized) { + Services.obs.addObserver(this, "auto-update-config-change"); + Services.obs.addObserver(this, "background-update-config-change"); + + // Witness when our own prefs change. + Services.prefs.addObserver("app.update.background.force", this); + Services.prefs.addObserver("app.update.background.interval", this); + + // Witness when the langpack updating feature is changed. + Services.prefs.addObserver("app.update.langpack.enabled", this); + + // Witness when langpacks come and go. + const onAddonEvent = async addon => { + if (addon.type != "locale") { + return; + } + lazy.log.debug( + `${SLUG}: langpacks may have changed; invoking maybeScheduleBackgroundUpdateTask` + ); + // No need to await this promise. + this.maybeScheduleBackgroundUpdateTask(); + }; + const addonsListener = { + onEnabled: onAddonEvent, + onDisabled: onAddonEvent, + onInstalled: onAddonEvent, + onUninstalled: onAddonEvent, + }; + lazy.AddonManager.addAddonListener(addonsListener); + + this._initialized = true; + } + + lazy.log.debug( + `${SLUG}: checking for reasons to not update this installation` + ); + let reasons = await this._reasonsToNotUpdateInstallation(); + + lazy.log.debug( + `${SLUG}: checking for reasons to not schedule background updates with this profile` + ); + let moreReasons = await this._reasonsToNotScheduleUpdates(); + reasons.push(...moreReasons); + + let enabled = !reasons.length; + + if (this._force()) { + // We want to allow developers and testers to monkey with the system. + lazy.log.debug( + `${SLUG}: app.update.background.force=true, ignoring reasons: ${JSON.stringify( + reasons + )}` + ); + reasons = []; + enabled = true; + } + + let updatePreviousPrefs = () => { + if (reasons.length) { + Services.prefs.setCharPref( + "app.update.background.previous.reasons", + JSON.stringify(reasons) + ); + } else { + Services.prefs.clearUserPref("app.update.background.previous.reasons"); + } + }; + + try { + // Interacting with `TaskScheduler.jsm` can throw, so we'll catch. + if (!enabled) { + lazy.log.info( + `${SLUG}: not scheduling background update: '${JSON.stringify( + reasons + )}'` + ); + + if (!successfullyReadPrevious || previousEnabled) { + await lazy.TaskScheduler.deleteTask(this.taskId); + lazy.log.debug( + `${SLUG}: witnessed falling (enabled -> disabled) edge; deleted task ${this.taskId}.` + ); + } + + updatePreviousPrefs(); + + return false; + } + + if (successfullyReadPrevious && previousEnabled) { + let taskInstalledVersion = Services.prefs.getIntPref( + TASK_INSTALLED_VERSION_PREF, + 1 + ); + if (taskInstalledVersion == TASK_DEF_CURRENT_VERSION) { + lazy.log.info( + `${SLUG}: background update was previously enabled; not registering task.` + ); + + return true; + } + lazy.log.info( + `${SLUG}: Detected task version change from ` + + `${taskInstalledVersion} to ${TASK_DEF_CURRENT_VERSION}. ` + + `Removing task so the new version can be registered` + ); + try { + await lazy.TaskScheduler.deleteTask(this.taskId); + } catch (e) { + lazy.log.error(`${SLUG}: Error removing old task: ${e}`); + } + try { + // When the update directory was moved, we migrated the old contents + // to the new location. This can potentially happen in a background + // task. However, we also need to re-register the background task + // with the task scheduler in order to update the MOZ_LOG_FILE value + // to point to the new location. If the task runs before Firefox has + // a chance to re-register the task, the log file may be recreated in + // the old location. In practice, this would be unusual, because + // MOZ_LOG_FILE will not create the parent directories necessary to + // put a log file in the specified location. But just to be safe, + // we'll do some cleanup when we re-register the task to make sure + // that no log file is hanging around in the old location. + let oldUpdateDir = lazy.FileUtils.getDir("OldUpdRootD", [], false); + let oldLog = oldUpdateDir.clone(); + oldLog.append("backgroundupdate.moz_log"); + + if (oldLog.exists()) { + oldLog.remove(false); + // We may have created some directories in order to put this log + // file in this location. Clean them up if they are empty. + // (If we pass false for the recurse parameter, directories with + // contents will not be removed) + // + // Potentially removes "C:\ProgramData\Mozilla\updates\<hash>" + oldUpdateDir.remove(false); + // Potentially removes "C:\ProgramData\Mozilla\updates" + oldUpdateDir.parent.remove(false); + // Potentially removes "C:\ProgramData\Mozilla" + oldUpdateDir.parent.parent.remove(false); + } + } catch (ex) { + lazy.log.warn( + `${SLUG}: Ignoring error encountered attempting to remove stale log file: ${ex}` + ); + } + } + + lazy.log.info( + `${SLUG}: background update was previously disabled for reasons: '${previousReasons}'` + ); + + await this._registerBackgroundUpdateTask(this.taskId); + lazy.log.info( + `${SLUG}: witnessed rising (disabled -> enabled) edge; registered task ${this.taskId}` + ); + + updatePreviousPrefs(); + + return true; + } catch (e) { + lazy.log.error( + `${SLUG}: exiting after uncaught exception in maybeScheduleBackgroundUpdateTask!`, + e + ); + + return false; + } + }, + + /** + * Record parts of the update environment for our custom Glean ping. + * + * This is just like the Telemetry Environment, but pared down to what we're + * likely to use in background update-specific analyses. + * + * Right now this is only for use in the background update task, but after Bug + * 1703313 (migrating AUS telemetry to be Glean-aware) we might use it more + * generally. + */ + async recordUpdateEnvironment() { + try { + Glean.update.serviceEnabled.set( + Services.prefs.getBoolPref("app.update.service.enabled", false) + ); + } catch (e) { + // It's fine if some or all of these are missing. + } + + // In the background update task, this should always be enabled, but let's + // find out if there's an error in the system. + Glean.update.autoDownload.set( + await lazy.UpdateUtils.getAppUpdateAutoEnabled() + ); + Glean.update.backgroundUpdate.set( + await lazy.UpdateUtils.readUpdateConfigSetting( + "app.update.background.enabled" + ) + ); + + Glean.update.channel.set(lazy.UpdateUtils.UpdateChannel); + Glean.update.enabled.set( + !Services.policies || Services.policies.isAllowed("appUpdate") + ); + + Glean.update.canUsuallyApplyUpdates.set( + lazy.UpdateService.canUsuallyApplyUpdates + ); + Glean.update.canUsuallyCheckForUpdates.set( + lazy.UpdateService.canUsuallyCheckForUpdates + ); + Glean.update.canUsuallyStageUpdates.set( + lazy.UpdateService.canUsuallyStageUpdates + ); + Glean.update.canUsuallyUseBits.set(lazy.UpdateService.canUsuallyUseBits); + }, + + /** + * Schedule periodic snapshotting of the Firefox Messaging System + * targeting configuration. + * + * The background update task will target messages based on the + * latest snapshot of the default profile's targeting configuration. + */ + async scheduleFirefoxMessagingSystemTargetingSnapshotting() { + let SLUG = "scheduleFirefoxMessagingSystemTargetingSnapshotting"; + let path = PathUtils.join(PathUtils.profileDir, "targeting.snapshot.json"); + + let snapshot = new lazy.JSONFile({ + beforeSave: async () => { + if (Services.startup.shuttingDown) { + // Collecting targeting information can be slow and cause shutdown + // crashes. Just write what we have in that case. During shutdown, + // the regular log apparatus is not available, so use `dump`. + if (lazy.log.shouldLog("debug")) { + dump( + `${SLUG}: shutting down, so not updating Firefox Messaging System targeting information from beforeSave\n` + ); + } + return; + } + + lazy.log.debug( + `${SLUG}: preparing to write Firefox Messaging System targeting information to ${path}` + ); + + // Merge latest data into existing data. This data may be partial, due + // to runtime errors and abbreviated collections, especially when + // shutting down. We accept the risk of incomplete or even internally + // inconsistent data: it's generally better to have stale data (and + // potentially target a user as they appeared in the past) than to block + // shutdown for more accurate results. An alternate approach would be + // to restrict the targeting data collected, but it's hard to + // distinguish expensive collection operations and the system loses + // flexibility when restrictions of this type are added. + let latestData = await lazy.ASRouterTargeting.getEnvironmentSnapshot(); + // We expect to always have data, but: belt-and-braces. + if (snapshot?.data?.environment) { + Object.assign(snapshot.data.environment, latestData.environment); + } else { + snapshot.data = latestData; + } + }, + path, + }); + + // We don't `load`, since we don't care about reading existing (now stale) + // data. + snapshot.data = await lazy.ASRouterTargeting.getEnvironmentSnapshot(); + + // Persist. + snapshot.saveSoon(); + + // Continue persisting periodically. `JSONFile.sys.mjs` will also persist one + // last time before shutdown. + this._targetingSnapshottingTimer = Cc[ + "@mozilla.org/timer;1" + ].createInstance(Ci.nsITimer); + + // Hold a reference to prevent GC. + this._targetingSnapshottingTimer.initWithCallback( + () => { + if (Services.startup.shuttingDown) { + // Collecting targeting information can be slow and cause shutdown + // crashes, so if we're shutting down, don't try to collect. During + // shutdown, the regular log apparatus is not available, so use `dump`. + if (lazy.log.shouldLog("debug")) { + dump( + `${SLUG}: shutting down, so not updating Firefox Messaging System targeting information from timer\n` + ); + } + return; + } + + snapshot.saveSoon(); + }, + // By default, snapshot Firefox Messaging System targeting for use by the + // background update task every 30 minutes. + Services.prefs.getIntPref( + "app.update.background.messaging.targeting.snapshot.intervalSec", + 1800 + ) * 1000, + Ci.nsITimer.TYPE_REPEATING_SLACK_LOW_PRIORITY + ); + }, + + /** + * Reads the snapshotted Firefox Messaging System targeting out of a profile. + * Collects background update specific telemetry. Never throws. + * + * If no `lock` is given, the default profile is locked and the preferences + * read from it. If `lock` is given, read from the given lock's directory. + * + * @param {nsIProfileLock} [lock] optional lock to use + * @returns {object} possibly empty targeting snapshot. + */ + async readFirefoxMessagingSystemTargetingSnapshot(lock = null) { + let SLUG = "readFirefoxMessagingSystemTargetingSnapshot"; + + let defaultProfileTargetingSnapshot = {}; + + Glean.backgroundUpdate.targetingExists.set(false); + Glean.backgroundUpdate.targetingException.set(true); + try { + defaultProfileTargetingSnapshot = await lazy.BackgroundTasksUtils.readFirefoxMessagingSystemTargetingSnapshot( + lock + ); + Glean.backgroundUpdate.targetingExists.set(true); + Glean.backgroundUpdate.targetingException.set(false); + + if (defaultProfileTargetingSnapshot?.version) { + Glean.backgroundUpdate.targetingVersion.set( + defaultProfileTargetingSnapshot.version + ); + } + if (defaultProfileTargetingSnapshot?.environment?.firefoxVersion) { + Glean.backgroundUpdate.targetingEnvFirefoxVersion.set( + defaultProfileTargetingSnapshot.environment.firefoxVersion + ); + } + if (defaultProfileTargetingSnapshot?.environment?.currentDate) { + Glean.backgroundUpdate.targetingEnvCurrentDate.set( + // Glean date times are provided in nanoseconds, `getTime()` yields + // milliseconds (after the Unix epoch). + new Date( + defaultProfileTargetingSnapshot.environment.currentDate + ).getTime() * 1000 + ); + } + if (defaultProfileTargetingSnapshot?.environment?.profileAgeCreated) { + Glean.backgroundUpdate.targetingEnvProfileAge.set( + // Glean date times are provided in nanoseconds, `profileAgeCreated` + // is in milliseconds (after the Unix epoch). + defaultProfileTargetingSnapshot.environment.profileAgeCreated * 1000 + ); + } + } catch (f) { + if (DOMException.isInstance(f) && f.name === "NotFoundError") { + Glean.backgroundUpdate.targetingException.set(false); + lazy.log.info(`${SLUG}: no default profile targeting snapshot exists`); + } else { + lazy.log.warn( + `${SLUG}: ignoring exception reading default profile targeting snapshot`, + f + ); + } + } + + return defaultProfileTargetingSnapshot; + }, +}; + +BackgroundUpdate.REASON = { + CANNOT_USUALLY_CHECK: + "cannot usually check for updates due to policy, testing configuration, or runtime environment", + CANNOT_USUALLY_STAGE_AND_CANNOT_USUALLY_APPLY: + "updates cannot usually stage and cannot usually apply", + LANGPACK_INSTALLED: + "app.update.langpack.enabled=true and at least one langpack is installed", + MANUAL_UPDATE_ONLY: "the ManualAppUpdateOnly policy is enabled", + NO_DEFAULT_PROFILE_EXISTS: "no default profile exists", + NOT_DEFAULT_PROFILE: "not default profile", + NO_APP_UPDATE_AUTO: "app.update.auto=false", + NO_APP_UPDATE_BACKGROUND_ENABLED: "app.update.background.enabled=false", + NO_MOZ_BACKGROUNDTASKS: "MOZ_BACKGROUNDTASKS=0", + NO_OMNIJAR: "no omnijar", + SERVICE_REGISTRY_KEY_MISSING: + "the maintenance service registry key is not present", + WINDOWS_CANNOT_USUALLY_USE_BITS: "on Windows but cannot usually use BITS", +}; + +/** + * Specific exit codes for `--backgroundtask backgroundupdate`. + * + * These help distinguish common failure cases. In particular, they distinguish + * "default profile does not exist" from "default profile cannot be locked" from + * more general errors reading from the default profile. + */ +BackgroundUpdate.EXIT_CODE = { + ...EXIT_CODE, + // We clone the other exit codes simply so we can use one object for all the codes. + DEFAULT_PROFILE_DOES_NOT_EXIST: 11, + DEFAULT_PROFILE_CANNOT_BE_LOCKED: 12, + DEFAULT_PROFILE_CANNOT_BE_READ: 13, + // Another instance is running. + OTHER_INSTANCE: 21, +}; diff --git a/toolkit/mozapps/update/UpdateListener.jsm b/toolkit/mozapps/update/UpdateListener.jsm new file mode 100644 index 0000000000..3e8c0c9879 --- /dev/null +++ b/toolkit/mozapps/update/UpdateListener.jsm @@ -0,0 +1,486 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["UpdateListener"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { clearTimeout, setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "AppUpdateService", + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService" +); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "UpdateManager", + "@mozilla.org/updates/update-manager;1", + "nsIUpdateManager" +); + +const PREF_APP_UPDATE_UNSUPPORTED_URL = "app.update.unsupported.url"; +const PREF_APP_UPDATE_SUPPRESS_PROMPTS = "app.update.suppressPrompts"; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "SUPPRESS_PROMPTS", + PREF_APP_UPDATE_SUPPRESS_PROMPTS, + false +); + +// Setup the hamburger button badges for updates. +var UpdateListener = { + timeouts: [], + + restartDoorhangerShown: false, + + // Once a restart badge/doorhanger is scheduled, these store the time that + // they were scheduled at (as milliseconds elapsed since the UNIX epoch). This + // allows us to resume the badge/doorhanger timers rather than restarting + // them from the beginning when a new update comes along. + updateFirstReadyTime: null, + + // If PREF_APP_UPDATE_SUPPRESS_PROMPTS is true, we'll dispatch a notification + // prompt 14 days from the last build time, or 7 days from the last update + // time; whichever is sooner. It's hardcoded here to make sure update prompts + // can't be suppressed permanently without knowledge of the consequences. + promptDelayMsFromBuild: 14 * 24 * 60 * 60 * 1000, // 14 days + + promptDelayMsFromUpdate: 7 * 24 * 60 * 60 * 1000, // 7 days + + // If the last update time or current build time is more than 1 day in the + // future, it has probably been manipulated and should be distrusted. + promptMaxFutureVariation: 24 * 60 * 60 * 1000, // 1 day + + latestUpdate: null, + + availablePromptScheduled: false, + + get badgeWaitTime() { + return Services.prefs.getIntPref("app.update.badgeWaitTime", 4 * 24 * 3600); // 4 days + }, + + get suppressedPromptDelay() { + // Return the time (in milliseconds) after which a suppressed prompt should + // be shown. Either 14 days from the last build time, or 7 days from the + // last update time; whichever comes sooner. If build time is not available + // and valid, schedule according to update time instead. If neither is + // available and valid, schedule the prompt for right now. Times are checked + // against the current time, since if the OS time is correct and nothing has + // been manipulated, the build time and update time will always be in the + // past. If the build time or update time is an hour in the future, it could + // just be a timezone issue. But if it is more than 24 hours in the future, + // it's probably due to attempted manipulation. + let now = Date.now(); + let buildId = AppConstants.MOZ_BUILDID; + let buildTime = + new Date( + buildId.slice(0, 4), + buildId.slice(4, 6) - 1, + buildId.slice(6, 8), + buildId.slice(8, 10), + buildId.slice(10, 12), + buildId.slice(12, 14) + ).getTime() ?? 0; + let updateTime = lazy.UpdateManager.getUpdateAt(0)?.installDate ?? 0; + // Check that update/build times are at most 24 hours after now. + if (buildTime - now > this.promptMaxFutureVariation) { + buildTime = 0; + } + if (updateTime - now > this.promptMaxFutureVariation) { + updateTime = 0; + } + let promptTime = now; + // If both times are available, choose the sooner. + if (updateTime && buildTime) { + promptTime = Math.min( + buildTime + this.promptDelayMsFromBuild, + updateTime + this.promptDelayMsFromUpdate + ); + } else if (updateTime || buildTime) { + // When the update time is missing, this installation was probably just + // installed and hasn't been updated yet. Ideally, we would instead set + // promptTime to installTime + this.promptDelayMsFromUpdate. But it's + // easier to get the build time than the install time. And on Nightly, the + // times ought to be fairly close together anyways. + promptTime = (updateTime || buildTime) + this.promptDelayMsFromUpdate; + } + return promptTime - now; + }, + + maybeShowUnsupportedNotification() { + // Persist the unsupported notification across sessions. If at some point an + // update is found this pref is cleared and the notification won't be shown. + let url = Services.prefs.getCharPref(PREF_APP_UPDATE_UNSUPPORTED_URL, null); + if (url) { + this.showUpdateNotification("unsupported", true, true, win => + this.openUnsupportedUpdateUrl(win, url) + ); + } + }, + + reset() { + this.clearPendingAndActiveNotifications(); + this.restartDoorhangerShown = false; + this.updateFirstReadyTime = null; + }, + + clearPendingAndActiveNotifications() { + lazy.AppMenuNotifications.removeNotification(/^update-/); + this.clearCallbacks(); + }, + + clearCallbacks() { + this.timeouts.forEach(t => clearTimeout(t)); + this.timeouts = []; + this.availablePromptScheduled = false; + }, + + addTimeout(time, callback) { + this.timeouts.push( + setTimeout(() => { + this.clearCallbacks(); + callback(); + }, time) + ); + }, + + requestRestart() { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + + if (!cancelQuit.data) { + Services.startup.quit( + Services.startup.eAttemptQuit | Services.startup.eRestart + ); + } + }, + + openManualUpdateUrl(win) { + let manualUpdateUrl = Services.urlFormatter.formatURLPref( + "app.update.url.manual" + ); + win.openURL(manualUpdateUrl); + }, + + openUnsupportedUpdateUrl(win, detailsURL) { + win.openURL(detailsURL); + }, + + showUpdateNotification( + type, + mainActionDismiss, + dismissed, + mainAction, + beforeShowDoorhanger + ) { + const addTelemetry = id => { + // No telemetry for the "downloading" state. + if (type !== "downloading") { + // Histogram category labels can't have dashes in them. + let telemetryType = type.replaceAll("-", ""); + Services.telemetry.getHistogramById(id).add(telemetryType); + } + }; + let action = { + callback(win, fromDoorhanger) { + if (fromDoorhanger) { + addTelemetry("UPDATE_NOTIFICATION_MAIN_ACTION_DOORHANGER"); + } else { + addTelemetry("UPDATE_NOTIFICATION_MAIN_ACTION_MENU"); + } + mainAction(win); + }, + dismiss: mainActionDismiss, + }; + + let secondaryAction = { + callback() { + addTelemetry("UPDATE_NOTIFICATION_DISMISSED"); + }, + dismiss: true, + }; + lazy.AppMenuNotifications.showNotification( + "update-" + type, + action, + secondaryAction, + { dismissed, beforeShowDoorhanger } + ); + if (dismissed) { + addTelemetry("UPDATE_NOTIFICATION_BADGE_SHOWN"); + } else { + addTelemetry("UPDATE_NOTIFICATION_SHOWN"); + } + }, + + showRestartNotification(update, dismissed) { + let notification = lazy.AppUpdateService.isOtherInstanceHandlingUpdates + ? "other-instance" + : "restart"; + if (!dismissed) { + this.restartDoorhangerShown = true; + } + this.showUpdateNotification(notification, true, dismissed, () => + this.requestRestart() + ); + }, + + showUpdateAvailableNotification(update, dismissed) { + this.showUpdateNotification("available", false, dismissed, () => { + // This is asynchronous, but we are just going to kick it off. + lazy.AppUpdateService.downloadUpdate(update, true); + }); + }, + + showManualUpdateNotification(update, dismissed) { + this.showUpdateNotification("manual", false, dismissed, win => + this.openManualUpdateUrl(win) + ); + }, + + showUnsupportedUpdateNotification(update, dismissed) { + if (!update || !update.detailsURL) { + console.error( + "The update for an unsupported notification must have a " + + "detailsURL attribute." + ); + return; + } + let url = update.detailsURL; + if ( + url != Services.prefs.getCharPref(PREF_APP_UPDATE_UNSUPPORTED_URL, null) + ) { + Services.prefs.setCharPref(PREF_APP_UPDATE_UNSUPPORTED_URL, url); + this.showUpdateNotification("unsupported", true, dismissed, win => + this.openUnsupportedUpdateUrl(win, url) + ); + } + }, + + showUpdateDownloadingNotification() { + this.showUpdateNotification("downloading", true, true, () => { + // The user clicked on the "Downloading update" app menu item. + // Code in browser/components/customizableui/content/panelUI.js + // receives the following notification and opens the about dialog. + Services.obs.notifyObservers(null, "show-update-progress"); + }); + }, + + scheduleUpdateAvailableNotification(update) { + // Show a badge/banner-only notification immediately. + this.showUpdateAvailableNotification(update, true); + // Track the latest update, since we will almost certainly have a new update + // 7 days from now. In a common scenario, update 1 triggers the timer. + // Updates 2, 3, 4, and 5 come without opening a prompt, since one is + // already scheduled. Then, the timer ends and the prompt that was triggered + // by update 1 is opened. But rather than downloading update 1, of course, + // it will download update 5, the latest update. + this.latestUpdate = update; + // Only schedule one doorhanger at a time. If we don't, then a new + // doorhanger would be scheduled at least once per day. If the user + // downloads the first update, we don't want to keep alerting them. + if (!this.availablePromptScheduled) { + this.addTimeout(Math.max(0, this.suppressedPromptDelay), () => { + // If we downloaded or installed an update via the badge or banner + // while the timer was running, bail out of showing the doorhanger. + if ( + lazy.UpdateManager.downloadingUpdate || + lazy.UpdateManager.readyUpdate + ) { + return; + } + this.showUpdateAvailableNotification(this.latestUpdate, false); + }); + this.availablePromptScheduled = true; + } + }, + + handleUpdateError(update, status) { + switch (status) { + case "download-attempt-failed": + this.clearCallbacks(); + this.showUpdateAvailableNotification(update, false); + break; + case "download-attempts-exceeded": + this.clearCallbacks(); + this.showManualUpdateNotification(update, false); + break; + case "elevation-attempt-failed": + this.clearCallbacks(); + this.showRestartNotification(false); + break; + case "elevation-attempts-exceeded": + this.clearCallbacks(); + this.showManualUpdateNotification(update, false); + break; + case "check-attempts-exceeded": + case "unknown": + case "bad-perms": + // Background update has failed, let's show the UI responsible for + // prompting the user to update manually. + this.clearCallbacks(); + this.showManualUpdateNotification(update, false); + break; + } + }, + + handleUpdateStagedOrDownloaded(update, status) { + switch (status) { + case "applied": + case "pending": + case "applied-service": + case "pending-service": + case "pending-elevate": + case "success": + this.clearCallbacks(); + + let initialBadgeWaitTimeMs = this.badgeWaitTime * 1000; + let initialDoorhangerWaitTimeMs = update.promptWaitTime * 1000; + let now = Date.now(); + + if (!this.updateFirstReadyTime) { + this.updateFirstReadyTime = now; + } + + let badgeWaitTimeMs = Math.max( + 0, + this.updateFirstReadyTime + initialBadgeWaitTimeMs - now + ); + let doorhangerWaitTimeMs = Math.max( + 0, + this.updateFirstReadyTime + initialDoorhangerWaitTimeMs - now + ); + + // On Nightly only, permit disabling doorhangers for update restart + // notifications by setting PREF_APP_UPDATE_SUPPRESS_PROMPTS + if (AppConstants.NIGHTLY_BUILD && lazy.SUPPRESS_PROMPTS) { + this.showRestartNotification(update, true); + } else if (badgeWaitTimeMs < doorhangerWaitTimeMs) { + this.addTimeout(badgeWaitTimeMs, () => { + // Skip the badge if we're waiting for another instance. + if (!lazy.AppUpdateService.isOtherInstanceHandlingUpdates) { + this.showRestartNotification(update, true); + } + + if (!this.restartDoorhangerShown) { + // doorhangerWaitTimeMs is relative to when we initially received + // the event. Since we've already waited badgeWaitTimeMs, subtract + // that from doorhangerWaitTimeMs. + let remainingTime = doorhangerWaitTimeMs - badgeWaitTimeMs; + this.addTimeout(remainingTime, () => { + this.showRestartNotification(update, false); + }); + } + }); + } else { + this.addTimeout(doorhangerWaitTimeMs, () => { + this.showRestartNotification(update, this.restartDoorhangerShown); + }); + } + break; + } + }, + + handleUpdateAvailable(update, status) { + switch (status) { + case "show-prompt": + // If an update is available, show an update available doorhanger unless + // PREF_APP_UPDATE_SUPPRESS_PROMPTS is true (only on Nightly). + if (AppConstants.NIGHTLY_BUILD && lazy.SUPPRESS_PROMPTS) { + this.scheduleUpdateAvailableNotification(update); + } else { + this.showUpdateAvailableNotification(update, false); + } + break; + case "cant-apply": + this.clearCallbacks(); + this.showManualUpdateNotification(update, false); + break; + case "unsupported": + this.clearCallbacks(); + this.showUnsupportedUpdateNotification(update, false); + break; + } + }, + + handleUpdateDownloading(status) { + switch (status) { + case "downloading": + this.showUpdateDownloadingNotification(); + break; + case "idle": + this.clearPendingAndActiveNotifications(); + break; + } + }, + + handleUpdateSwap() { + // This function is called because we just finished downloading an update + // (possibly) when another update was already ready. + // At some point, we may want to have some sort of intermediate + // notification to display here so that the badge doesn't just disappear. + // Currently, this function just hides update notifications and clears + // the callback timers so that notifications will not be shown. We want to + // clear the restart notification so the user doesn't try to restart to + // update during staging. We want to clear any other notifications too, + // since none of them make sense to display now. + // Our observer will fire again when the update is either ready to install + // or an error has been encountered. + this.clearPendingAndActiveNotifications(); + }, + + observe(subject, topic, status) { + let update = subject && subject.QueryInterface(Ci.nsIUpdate); + + switch (topic) { + case "update-available": + if (status != "unsupported") { + // An update check has found an update so clear the unsupported pref + // in case it is set. + Services.prefs.clearUserPref(PREF_APP_UPDATE_UNSUPPORTED_URL); + } + this.handleUpdateAvailable(update, status); + break; + case "update-downloading": + this.handleUpdateDownloading(status); + break; + case "update-staged": + case "update-downloaded": + // An update check has found an update and downloaded / staged the + // update so clear the unsupported pref in case it is set. + Services.prefs.clearUserPref(PREF_APP_UPDATE_UNSUPPORTED_URL); + this.handleUpdateStagedOrDownloaded(update, status); + break; + case "update-error": + this.handleUpdateError(update, status); + break; + case "update-swap": + this.handleUpdateSwap(); + break; + } + }, +}; diff --git a/toolkit/mozapps/update/UpdateService.jsm b/toolkit/mozapps/update/UpdateService.jsm new file mode 100644 index 0000000000..aaaaeda9bf --- /dev/null +++ b/toolkit/mozapps/update/UpdateService.jsm @@ -0,0 +1,7163 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { AUSTLMY } = ChromeUtils.import( + "resource://gre/modules/UpdateTelemetry.jsm" +); +const { + Bits, + BitsRequest, + BitsUnknownError, + BitsVerificationError, +} = ChromeUtils.import("resource://gre/modules/Bits.jsm"); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", + CertUtils: "resource://gre/modules/CertUtils.sys.mjs", + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", + WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.jsm", + ctypes: "resource://gre/modules/ctypes.jsm", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "AUS", + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService" +); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "UM", + "@mozilla.org/updates/update-manager;1", + "nsIUpdateManager" +); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "CheckSvc", + "@mozilla.org/updates/update-checker;1", + "nsIUpdateChecker" +); + +if (AppConstants.ENABLE_WEBDRIVER) { + XPCOMUtils.defineLazyServiceGetter( + lazy, + "Marionette", + "@mozilla.org/remote/marionette;1", + "nsIMarionette" + ); + + XPCOMUtils.defineLazyServiceGetter( + lazy, + "RemoteAgent", + "@mozilla.org/remote/agent;1", + "nsIRemoteAgent" + ); +} else { + lazy.Marionette = { running: false }; + lazy.RemoteAgent = { running: false }; +} + +const UPDATESERVICE_CID = Components.ID( + "{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}" +); + +const PREF_APP_UPDATE_ALTUPDATEDIRPATH = "app.update.altUpdateDirPath"; +const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors"; +const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors"; +const PREF_APP_UPDATE_BITS_ENABLED = "app.update.BITS.enabled"; +const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations"; +const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx"; +const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max"; +const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED = + "app.update.checkOnlyInstance.enabled"; +const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_INTERVAL = + "app.update.checkOnlyInstance.interval"; +const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT = + "app.update.checkOnlyInstance.timeout"; +const PREF_APP_UPDATE_DISABLEDFORTESTING = "app.update.disabledForTesting"; +const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS = "app.update.download.attempts"; +const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts"; +const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never"; +const PREF_APP_UPDATE_ELEVATE_VERSION = "app.update.elevate.version"; +const PREF_APP_UPDATE_ELEVATE_ATTEMPTS = "app.update.elevate.attempts"; +const PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS = "app.update.elevate.maxAttempts"; +const PREF_APP_UPDATE_LANGPACK_ENABLED = "app.update.langpack.enabled"; +const PREF_APP_UPDATE_LANGPACK_TIMEOUT = "app.update.langpack.timeout"; +const PREF_APP_UPDATE_LOG = "app.update.log"; +const PREF_APP_UPDATE_LOG_FILE = "app.update.log.file"; +const PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD = "app.update.notifyDuringDownload"; +const PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED = + "app.update.noWindowAutoRestart.enabled"; +const PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_DELAY_MS = + "app.update.noWindowAutoRestart.delayMs"; +const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime"; +const PREF_APP_UPDATE_SERVICE_ENABLED = "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_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_DETAILS = "app.update.url.details"; +const PREF_NETWORK_PROXY_TYPE = "network.proxy.type"; + +const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties"; +const URI_UPDATE_NS = "http://www.mozilla.org/2005/app-update"; +const URI_UPDATES_PROPERTIES = + "chrome://mozapps/locale/update/updates.properties"; + +const KEY_EXECUTABLE = "XREExeF"; +const KEY_PROFILE_DIR = "ProfD"; +const KEY_UPDROOT = "UpdRootD"; + +const DIR_UPDATES = "updates"; + +const FILE_ACTIVE_UPDATE_XML = "active-update.xml"; +const FILE_BACKUP_UPDATE_LOG = "backup-update.log"; +const FILE_BT_RESULT = "bt.result"; +const FILE_LAST_UPDATE_LOG = "last-update.log"; +const FILE_UPDATES_XML = "updates.xml"; +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 FILE_UPDATE_MESSAGES = "update_messages.log"; + +const STATE_NONE = "null"; +const STATE_DOWNLOADING = "downloading"; +const STATE_PENDING = "pending"; +const STATE_PENDING_SERVICE = "pending-service"; +const STATE_PENDING_ELEVATE = "pending-elevate"; +const STATE_APPLYING = "applying"; +const STATE_APPLIED = "applied"; +const STATE_APPLIED_SERVICE = "applied-service"; +const STATE_SUCCEEDED = "succeeded"; +const STATE_DOWNLOAD_FAILED = "download-failed"; +const STATE_FAILED = "failed"; + +// BITS will keep retrying a download after transient errors, unless this much +// time has passed since there has been download progress. +// Similarly to ...POLL_RATE_MS below, we are much more aggressive when the user +// is watching the download progress. +const BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS = 3600; // 1 hour +const BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS = 5; + +// These value control how frequently we get updates from the BITS client on +// the progress made downloading. The difference between the two is that the +// active interval is the one used when the user is watching. The idle interval +// is the one used when no one is watching. +const BITS_IDLE_POLL_RATE_MS = 1000; +const BITS_ACTIVE_POLL_RATE_MS = 200; + +// The values below used by this code are from common/updatererrors.h +const WRITE_ERROR = 7; +const ELEVATION_CANCELED = 9; +const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24; +const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25; +const SERVICE_UPDATER_SIGN_ERROR = 26; +const SERVICE_UPDATER_COMPARE_ERROR = 27; +const SERVICE_UPDATER_IDENTITY_ERROR = 28; +const SERVICE_STILL_APPLYING_ON_SUCCESS = 29; +const SERVICE_STILL_APPLYING_ON_FAILURE = 30; +const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31; +const SERVICE_COULD_NOT_LOCK_UPDATER = 32; +const SERVICE_INSTALLDIR_ERROR = 33; +const WRITE_ERROR_ACCESS_DENIED = 35; +const WRITE_ERROR_CALLBACK_APP = 37; +const UNEXPECTED_STAGING_ERROR = 43; +const DELETE_ERROR_STAGING_LOCK_FILE = 44; +const SERVICE_COULD_NOT_COPY_UPDATER = 49; +const SERVICE_STILL_APPLYING_TERMINATED = 50; +const SERVICE_STILL_APPLYING_NO_EXIT_CODE = 51; +const SERVICE_COULD_NOT_IMPERSONATE = 58; +const WRITE_ERROR_FILE_COPY = 61; +const WRITE_ERROR_DELETE_FILE = 62; +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; + +// Error codes 80 through 99 are reserved for UpdateService.jsm and are not +// defined in common/updatererrors.h +const ERR_UPDATER_CRASHED = 89; +const ERR_OLDER_VERSION_OR_SAME_BUILD = 90; +const ERR_UPDATE_STATE_NONE = 91; +const ERR_CHANNEL_CHANGE = 92; +const INVALID_UPDATER_STATE_CODE = 98; +const INVALID_UPDATER_STATUS_CODE = 99; + +const SILENT_UPDATE_NEEDED_ELEVATION_ERROR = 105; +const WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION = 106; + +// 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, + WRITE_ERROR_PATCH_FILE, + WRITE_ERROR_APPLY_DIR_PATH, + WRITE_ERROR_CALLBACK_PATH, + WRITE_ERROR_FILE_ACCESS_DENIED, + WRITE_ERROR_DIR_ACCESS_DENIED, + WRITE_ERROR_DELETE_BACKUP, + WRITE_ERROR_EXTRACT, + WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION, +]; + +// Array of write errors to simplify checks for service errors +const SERVICE_ERRORS = [ + SERVICE_UPDATER_COULD_NOT_BE_STARTED, + SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS, + SERVICE_UPDATER_SIGN_ERROR, + SERVICE_UPDATER_COMPARE_ERROR, + SERVICE_UPDATER_IDENTITY_ERROR, + SERVICE_STILL_APPLYING_ON_SUCCESS, + SERVICE_STILL_APPLYING_ON_FAILURE, + SERVICE_UPDATER_NOT_FIXED_DRIVE, + SERVICE_COULD_NOT_LOCK_UPDATER, + SERVICE_INSTALLDIR_ERROR, + SERVICE_COULD_NOT_COPY_UPDATER, + SERVICE_STILL_APPLYING_TERMINATED, + SERVICE_STILL_APPLYING_NO_EXIT_CODE, + SERVICE_COULD_NOT_IMPERSONATE, +]; + +// Custom update error codes +const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110; +const NETWORK_ERROR_OFFLINE = 111; + +// Error codes should be < 1000. Errors above 1000 represent http status codes +const HTTP_ERROR_OFFSET = 1000; + +// The is an HRESULT error that may be returned from the BITS interface +// indicating that access was denied. Often, this error code is returned when +// attempting to access a job created by a different user. +const HRESULT_E_ACCESSDENIED = -2147024891; + +const DOWNLOAD_CHUNK_SIZE = 300000; // bytes + +// The number of consecutive failures when updating using the service before +// setting the app.update.service.enabled preference to false. +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_SOCKET_RETRYTIMEOUT = 2000; + +// Default maximum number of elevation cancelations per update version before +// giving up. +const DEFAULT_CANCELATIONS_OSX_MAX = 3; + +// 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 + "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "mail-startup-done", +}; + +// The interval for the update xml write deferred task. +const XML_SAVER_INTERVAL_MS = 200; + +// How long after a patch has downloaded should we wait for language packs to +// update before proceeding anyway. +const LANGPACK_UPDATE_DEFAULT_TIMEOUT = 300000; + +// Interval between rechecks for other instances after the initial check finds +// at least one other instance. +const ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes + +// Wait this long after detecting that another instance is running (having been +// polling that entire time) before giving up and applying the update anyway. +const ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS = 6 * 60 * 60 * 1000; // 6 hours + +// The other instance check timeout can be overridden via a pref, but we limit +// that value to this so that the pref can't effectively disable the feature. +const ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS = 2 * 24 * 60 * 60 * 1000; // 2 days + +// Values to use when polling for staging. See `pollForStagingEnd` for more +// details. +const STAGING_POLLING_MIN_INTERVAL_MS = 15 * 1000; // 15 seconds +const STAGING_POLLING_MAX_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes +const STAGING_POLLING_ATTEMPTS_PER_INTERVAL = 5; +const STAGING_POLLING_MAX_DURATION_MS = 1 * 60 * 60 * 1000; // 1 hour + +var gUpdateMutexHandle = null; +// This is the file stream used for the log file. +var gLogfileOutputStream; +// This value will be set to true if it appears that BITS is being used by +// another user to download updates. We don't really want two users using BITS +// at once. Computers with many users (ex: a school computer), should not end +// up with dozens of BITS jobs. +var gBITSInUseByAnotherUser = false; +// The update service can be invoked as part of a standalone headless background +// task. In this context, when the background task kicks off an update +// download, we don't want it to move on to staging. As soon as the download has +// kicked off, the task begins shutting down and, even if the the download +// completes incredibly quickly, we don't want staging to begin while we are +// shutting down. That isn't a well tested scenario and it's possible that it +// could leave us in a bad state. +let gOnlyDownloadUpdatesThisSession = false; +// This will be the backing for `nsIApplicationUpdateService.currentState` +var gUpdateState = Ci.nsIApplicationUpdateService.STATE_IDLE; + +/** + * Simple container and constructor for a Promise and its resolve function. + */ +class SelfContainedPromise { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } +} + +// This will contain a `SelfContainedPromise` that will be used to back +// `nsIApplicationUpdateService.stateTransition`. +var gStateTransitionPromise = new SelfContainedPromise(); + +XPCOMUtils.defineLazyGetter(lazy, "gLogEnabled", function aus_gLogEnabled() { + return ( + Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) || + Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false) + ); +}); + +XPCOMUtils.defineLazyGetter( + lazy, + "gLogfileEnabled", + function aus_gLogfileEnabled() { + return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false); + } +); + +XPCOMUtils.defineLazyGetter( + lazy, + "gUpdateBundle", + function aus_gUpdateBundle() { + return Services.strings.createBundle(URI_UPDATES_PROPERTIES); + } +); + +/** + * gIsBackgroundTaskMode will be true if Firefox is currently running as a + * background task. Otherwise it will be false. + */ +XPCOMUtils.defineLazyGetter( + lazy, + "gIsBackgroundTaskMode", + function aus_gCurrentlyRunningAsBackgroundTask() { + if (!("@mozilla.org/backgroundtasks;1" in Cc)) { + return false; + } + const bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + if (!bts) { + return false; + } + return bts.isBackgroundTaskMode; + } +); + +/** + * Changes `nsIApplicationUpdateService.currentState` and causes + * `nsIApplicationUpdateService.stateTransition` to resolve. + */ +function transitionState(newState) { + if (newState == gUpdateState) { + LOG("transitionState - Not transitioning state because it isn't changing."); + return; + } + LOG( + `transitionState - "${lazy.AUS.getStateName(gUpdateState)}" -> ` + + `"${lazy.AUS.getStateName(newState)}".` + ); + gUpdateState = newState; + // Assign the new Promise before we resolve the old one just to make sure that + // anything that runs as a result of `resolve` doesn't end up waiting on the + // Promise that already resolved. + let oldStateTransitionPromise = gStateTransitionPromise; + gStateTransitionPromise = new SelfContainedPromise(); + oldStateTransitionPromise.resolve(); +} + +/** + * When a plain JS object is passed through xpconnect the other side sees a + * wrapped version of the object instead of the real object. Since these two + * objects are different they act as different keys for Map and WeakMap. However + * xpconnect gives us a way to get the underlying JS object from the wrapper so + * this function returns the JS object regardless of whether passed the JS + * object or its wrapper for use in places where it is unclear which one you + * have. + */ +function unwrap(obj) { + return obj.wrappedJSObject ?? obj; +} + +/** + * When an update starts to download (and if the feature is enabled) the add-ons + * manager starts downloading updated language packs for the new application + * version. A promise is used to track whether those updates are complete so the + * front-end is only notified that an application update is ready once the + * language pack updates have been staged. + * + * In order to be able to access that promise from various places in the update + * service they are cached in this map using the nsIUpdate object as a weak + * owner. Note that the key should always be the result of calling the above + * unwrap function on the nsIUpdate to ensure a consistent object is used as the + * key. + * + * When the language packs finish staging the nsIUpdate entriy is removed from + * this map so if the entry is still there then language pack updates are in + * progress. + */ +const LangPackUpdates = new WeakMap(); + +/** + * When we're polling to see if other running instances of the application have + * exited, there's no need to ever start polling again in parallel. To prevent + * doing that, we keep track of the promise that resolves when polling completes + * and return that if a second simultaneous poll is requested, so that the + * multiple callers end up waiting for the same promise to resolve. + */ +let gOtherInstancePollPromise; + +/** + * Query the update sync manager to see if another instance of this same + * installation of this application is currently running, under the context of + * any operating system user (not just the current one). + * This function immediately returns the current, instantaneous status of any + * other instances. + * + * @return true if at least one other instance is running, false if not + */ +function isOtherInstanceRunning(callback) { + const checkEnabled = Services.prefs.getBoolPref( + PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED, + true + ); + if (!checkEnabled) { + LOG("isOtherInstanceRunning - disabled by pref, skipping check"); + return false; + } + + try { + let syncManager = Cc[ + "@mozilla.org/updates/update-sync-manager;1" + ].getService(Ci.nsIUpdateSyncManager); + return syncManager.isOtherInstanceRunning(); + } catch (ex) { + LOG(`isOtherInstanceRunning - sync manager failed with exception: ${ex}`); + return false; + } +} + +/** + * Query the update sync manager to see if another instance of this same + * installation of this application is currently running, under the context of + * any operating system user (not just the one running this instance). + * This function polls for the status of other instances continually + * (asynchronously) until either none exist or a timeout expires. + * + * @return a Promise that resolves with false if at any point during polling no + * other instances can be found, or resolves with true if the timeout + * expires when other instances are still running + */ +function waitForOtherInstances() { + // If we're already in the middle of a poll, reuse it rather than start again. + if (gOtherInstancePollPromise) { + return gOtherInstancePollPromise; + } + + let timeout = Services.prefs.getIntPref( + PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT, + ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS + ); + // Don't allow the pref to set a super high timeout and break this feature. + if (timeout > ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS) { + timeout = ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS; + } + + let interval = Services.prefs.getIntPref( + PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_INTERVAL, + ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS + ); + // Don't allow an interval longer than the timeout. + interval = Math.min(interval, timeout); + + let iterations = 0; + const maxIterations = Math.ceil(timeout / interval); + + gOtherInstancePollPromise = new Promise(function(resolve, reject) { + let poll = function() { + iterations++; + if (!isOtherInstanceRunning()) { + LOG("waitForOtherInstances - no other instances found, exiting"); + resolve(false); + gOtherInstancePollPromise = undefined; + } else if (iterations >= maxIterations) { + LOG( + "waitForOtherInstances - timeout expired while other instances " + + "are still running" + ); + resolve(true); + gOtherInstancePollPromise = undefined; + } else if (iterations + 1 == maxIterations && timeout % interval != 0) { + // In case timeout isn't a multiple of interval, set the next timeout + // for the remainder of the time rather than for the usual interval. + lazy.setTimeout(poll, timeout % interval); + } else { + lazy.setTimeout(poll, interval); + } + }; + + LOG("waitForOtherInstances - beginning polling"); + poll(); + }); + + return gOtherInstancePollPromise; +} + +/** + * Tests to make sure that we can write to a given directory. + * + * @param updateTestFile a test file in the directory that needs to be tested. + * @param createDirectory whether a test directory should be created. + * @throws if we don't have right access to the directory. + */ +function testWriteAccess(updateTestFile, createDirectory) { + const NORMAL_FILE_TYPE = Ci.nsIFile.NORMAL_FILE_TYPE; + const DIRECTORY_TYPE = Ci.nsIFile.DIRECTORY_TYPE; + if (updateTestFile.exists()) { + updateTestFile.remove(false); + } + updateTestFile.create( + createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE, + createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE + ); + updateTestFile.remove(false); +} + +/** + * Windows only function that closes a Win32 handle. + * + * @param handle The handle to close + */ +function closeHandle(handle) { + if (handle) { + let lib = lazy.ctypes.open("kernel32.dll"); + let CloseHandle = lib.declare( + "CloseHandle", + lazy.ctypes.winapi_abi, + lazy.ctypes.int32_t /* success */, + lazy.ctypes.void_t.ptr + ); /* handle */ + CloseHandle(handle); + lib.close(); + } +} + +/** + * Windows only function that creates a mutex. + * + * @param aName + * The name for the mutex. + * @param aAllowExisting + * If false the function will close the handle and return null. + * @return The Win32 handle to the mutex. + */ +function createMutex(aName, aAllowExisting = true) { + if (AppConstants.platform != "win") { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + const INITIAL_OWN = 1; + const ERROR_ALREADY_EXISTS = 0xb7; + let lib = lazy.ctypes.open("kernel32.dll"); + let CreateMutexW = lib.declare( + "CreateMutexW", + lazy.ctypes.winapi_abi, + lazy.ctypes.void_t.ptr /* return handle */, + lazy.ctypes.void_t.ptr /* security attributes */, + lazy.ctypes.int32_t /* initial owner */, + lazy.ctypes.char16_t.ptr + ); /* name */ + + let handle = CreateMutexW(null, INITIAL_OWN, aName); + let alreadyExists = lazy.ctypes.winLastError == ERROR_ALREADY_EXISTS; + if (handle && !handle.isNull() && !aAllowExisting && alreadyExists) { + closeHandle(handle); + handle = null; + } + lib.close(); + + if (handle && handle.isNull()) { + handle = null; + } + + return handle; +} + +/** + * Windows only function that determines a unique mutex name for the + * installation. + * + * @param aGlobal + * true if the function should return a global mutex. A global mutex is + * valid across different sessions. + * @return Global mutex path + */ +function getPerInstallationMutexName(aGlobal = true) { + if (AppConstants.platform != "win") { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + hasher.init(hasher.SHA1); + + let exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile); + + let converter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var data = converter.convertToByteArray(exeFile.path.toLowerCase()); + + hasher.update(data, data.length); + return ( + (aGlobal ? "Global\\" : "") + "MozillaUpdateMutex-" + hasher.finish(true) + ); +} + +/** + * Whether or not the current instance has the update mutex. The update mutex + * gives protection against 2 applications from the same installation updating: + * 1) Running multiple profiles from the same installation path + * 2) Two applications running in 2 different user sessions from the same path + * + * @return true if this instance holds the update mutex + */ +function hasUpdateMutex() { + if (AppConstants.platform != "win") { + return true; + } + if (!gUpdateMutexHandle) { + gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false); + } + return !!gUpdateMutexHandle; +} + +/** + * Determines whether or not all descendants of a directory are writeable. + * Note: Does not check the root directory itself for writeability. + * + * @return true if all descendants are writeable, false otherwise + */ +function areDirectoryEntriesWriteable(aDir) { + let items = aDir.directoryEntries; + while (items.hasMoreElements()) { + let item = items.nextFile; + if (!item.isWritable()) { + LOG("areDirectoryEntriesWriteable - unable to write to " + item.path); + return false; + } + if (item.isDirectory() && !areDirectoryEntriesWriteable(item)) { + return false; + } + } + return true; +} + +/** + * OSX only function to determine if the user requires elevation to be able to + * write to the application bundle. + * + * @return true if elevation is required, false otherwise + */ +function getElevationRequired() { + if (AppConstants.platform != "macosx") { + return false; + } + + try { + // Recursively check that the application bundle (and its descendants) can + // be written to. + LOG( + "getElevationRequired - recursively testing write access on " + + getInstallDirRoot().path + ); + if ( + !getInstallDirRoot().isWritable() || + !areDirectoryEntriesWriteable(getInstallDirRoot()) + ) { + LOG( + "getElevationRequired - unable to write to application bundle, " + + "elevation required" + ); + return true; + } + } catch (ex) { + LOG( + "getElevationRequired - unable to write to application bundle, " + + "elevation required. Exception: " + + ex + ); + return true; + } + LOG( + "getElevationRequired - able to write to application bundle, elevation " + + "not required" + ); + return false; +} + +/** + * A promise that resolves when language packs are downloading or if no language + * packs are being downloaded. + */ +function promiseLangPacksUpdated(update) { + let promise = LangPackUpdates.get(unwrap(update)); + if (promise) { + LOG( + "promiseLangPacksUpdated - waiting for language pack updates to stage." + ); + return promise; + } + + // In case callers rely on a promise just return an already resolved promise. + return Promise.resolve(); +} + +/** + * Determines whether or not an update can be applied. This is always true on + * Windows when the service is used. On Mac OS X and Linux, if the user has + * write access to the update directory this will return true because on OSX we + * offer users the option to perform an elevated update when necessary and on + * Linux the update directory is located in the application directory. + * + * @return true if an update can be applied, false otherwise + */ +function getCanApplyUpdates() { + try { + // Check if it is possible to write to the update directory so clients won't + // repeatedly try to apply an update without the ability to complete the + // update process which requires write access to the update directory. + let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]); + LOG("getCanApplyUpdates - testing write access " + updateTestFile.path); + testWriteAccess(updateTestFile, false); + } catch (e) { + LOG( + "getCanApplyUpdates - unable to apply updates without write " + + "access to the update directory. Exception: " + + e + ); + return false; + } + + if (AppConstants.platform == "macosx") { + LOG( + "getCanApplyUpdates - bypass the write since elevation can be used " + + "on Mac OS X" + ); + return true; + } + + if (shouldUseService()) { + LOG( + "getCanApplyUpdates - bypass the write checks because the Windows " + + "Maintenance Service can be used" + ); + return true; + } + + try { + if (AppConstants.platform == "win") { + // On Windows when the maintenance service isn't used updates can still be + // performed in a location requiring admin privileges by the client + // accepting a UAC prompt from an elevation request made by the updater. + // Whether the client can elevate (e.g. has a split token) is determined + // in nsXULAppInfo::GetUserCanElevate which is located in nsAppRunner.cpp. + let userCanElevate = Services.appinfo.QueryInterface(Ci.nsIWinAppHelper) + .userCanElevate; + if (lazy.gIsBackgroundTaskMode) { + LOG( + "getCanApplyUpdates - in background task mode, assuming user can't elevate" + ); + userCanElevate = false; + } + if (!userCanElevate) { + // if we're unable to create the test file this will throw an exception. + let appDirTestFile = getAppBaseDir(); + appDirTestFile.append(FILE_UPDATE_TEST); + LOG("getCanApplyUpdates - testing write access " + appDirTestFile.path); + if (appDirTestFile.exists()) { + appDirTestFile.remove(false); + } + appDirTestFile.create( + Ci.nsIFile.NORMAL_FILE_TYPE, + FileUtils.PERMS_FILE + ); + appDirTestFile.remove(false); + } + } + } catch (e) { + LOG("getCanApplyUpdates - unable to apply updates. Exception: " + e); + // No write access to the installation directory + return false; + } + + LOG("getCanApplyUpdates - able to apply updates"); + return true; +} + +/** + * Whether or not the application can stage an update for the current session. + * These checks are only performed once per session due to using a lazy getter. + * + * @return true if updates can be staged for this session. + */ +XPCOMUtils.defineLazyGetter( + lazy, + "gCanStageUpdatesSession", + function aus_gCSUS() { + if (getElevationRequired()) { + LOG( + "gCanStageUpdatesSession - unable to stage updates because elevation " + + "is required." + ); + return false; + } + + try { + let updateTestFile; + if (AppConstants.platform == "macosx") { + updateTestFile = getUpdateFile([FILE_UPDATE_TEST]); + } else { + updateTestFile = getInstallDirRoot(); + updateTestFile.append(FILE_UPDATE_TEST); + } + LOG( + "gCanStageUpdatesSession - testing write access " + updateTestFile.path + ); + testWriteAccess(updateTestFile, true); + if (AppConstants.platform != "macosx") { + // On all platforms except Mac, we need to test the parent directory as + // well, as we need to be able to move files in that directory during the + // replacing step. + updateTestFile = getInstallDirRoot().parent; + updateTestFile.append(FILE_UPDATE_TEST); + LOG( + "gCanStageUpdatesSession - testing write access " + + updateTestFile.path + ); + updateTestFile.createUnique( + Ci.nsIFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY + ); + updateTestFile.remove(false); + } + } catch (e) { + LOG("gCanStageUpdatesSession - unable to stage updates. Exception: " + e); + // No write privileges + return false; + } + + LOG("gCanStageUpdatesSession - able to stage updates"); + return true; + } +); + +/** + * Whether or not the application can stage an update. + * + * @param {boolean} [transient] Whether transient factors such as the update + * mutex should be considered. + * @return true if updates can be staged. + */ +function getCanStageUpdates(transient = true) { + // If staging updates are disabled, then just bail out! + if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false)) { + LOG( + "getCanStageUpdates - staging updates is disabled by preference " + + PREF_APP_UPDATE_STAGING_ENABLED + ); + return false; + } + + if (AppConstants.platform == "win" && shouldUseService()) { + // No need to perform directory write checks, the maintenance service will + // be able to write to all directories. + LOG("getCanStageUpdates - able to stage updates using the service"); + return true; + } + + if (transient && !hasUpdateMutex()) { + LOG( + "getCanStageUpdates - unable to apply updates because another " + + "instance of the application is already handling updates for this " + + "installation." + ); + return false; + } + + return lazy.gCanStageUpdatesSession; +} + +/* + * Whether or not the application can use BITS to download updates. + * + * @param {boolean} [transient] Whether transient factors such as the update + * mutex should be considered. + * @return A string with one of these values: + * CanUseBits + * NoBits_NotWindows + * NoBits_FeatureOff + * NoBits_Pref + * NoBits_Proxy + * NoBits_OtherUser + * These strings are directly compatible with the categories for + * UPDATE_CAN_USE_BITS_EXTERNAL and UPDATE_CAN_USE_BITS_NOTIFY telemetry + * probes. If this function is made to return other values, they should + * also be added to the labels lists for those probes in Histograms.json + */ +function getCanUseBits(transient = true) { + if (AppConstants.platform != "win") { + LOG("getCanUseBits - Not using BITS because this is not Windows"); + return "NoBits_NotWindows"; + } + if (!AppConstants.MOZ_BITS_DOWNLOAD) { + LOG("getCanUseBits - Not using BITS because the feature is disabled"); + return "NoBits_FeatureOff"; + } + + if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED, true)) { + LOG("getCanUseBits - Not using BITS. Disabled by pref."); + return "NoBits_Pref"; + } + // Firefox support for passing proxies to BITS is still rudimentary. + // For now, disable BITS support on configurations that are not using the + // standard system proxy. + let defaultProxy = Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM; + if ( + Services.prefs.getIntPref(PREF_NETWORK_PROXY_TYPE, defaultProxy) != + defaultProxy && + !Cu.isInAutomation + ) { + LOG("getCanUseBits - Not using BITS because of proxy usage"); + return "NoBits_Proxy"; + } + if (transient && gBITSInUseByAnotherUser) { + LOG("getCanUseBits - Not using BITS. Already in use by another user"); + return "NoBits_OtherUser"; + } + LOG("getCanUseBits - BITS can be used to download updates"); + return "CanUseBits"; +} + +/** + * Logs a string to the error console. If enabled, also logs to the update + * messages file. + * @param string + * The string to write to the error console. + */ +function LOG(string) { + if (lazy.gLogEnabled) { + dump("*** AUS:SVC " + string + "\n"); + if (!Cu.isInAutomation) { + Services.console.logStringMessage("AUS:SVC " + string); + } + + if (lazy.gLogfileEnabled) { + if (!gLogfileOutputStream) { + let logfile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile); + logfile.append(FILE_UPDATE_MESSAGES); + gLogfileOutputStream = FileUtils.openAtomicFileOutputStream(logfile); + } + + try { + let encoded = new TextEncoder().encode(string + "\n"); + gLogfileOutputStream.write(encoded, encoded.length); + gLogfileOutputStream.flush(); + } catch (e) { + dump("*** AUS:SVC Unable to write to messages file: " + e + "\n"); + Services.console.logStringMessage( + "AUS:SVC Unable to write to messages file: " + e + ); + } + } + } +} + +/** + * Gets the specified directory at the specified hierarchy under the + * update root directory and creates it if it doesn't exist. + * @param pathArray + * An array of path components to locate beneath the directory + * specified by |key| + * @return nsIFile object for the location specified. + */ +function getUpdateDirCreate(pathArray) { + if (Cu.isInAutomation) { + // This allows tests to use an alternate updates directory so they can test + // startup behavior. + const MAGIC_TEST_ROOT_PREFIX = "<test-root>"; + const PREF_TEST_ROOT = "mochitest.testRoot"; + let alternatePath = Services.prefs.getCharPref( + PREF_APP_UPDATE_ALTUPDATEDIRPATH, + null + ); + if (alternatePath && alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) { + let testRoot = Services.prefs.getCharPref(PREF_TEST_ROOT); + let relativePath = alternatePath.substring(MAGIC_TEST_ROOT_PREFIX.length); + if (AppConstants.platform == "win") { + relativePath = relativePath.replace(/\//g, "\\"); + } + alternatePath = testRoot + relativePath; + let updateDir = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + updateDir.initWithPath(alternatePath); + for (let i = 0; i < pathArray.length; ++i) { + updateDir.append(pathArray[i]); + } + return updateDir; + } + } + + return FileUtils.getDir(KEY_UPDROOT, pathArray, true); +} + +/** + * Gets the application base directory. + * + * @return nsIFile object for the application base directory. + */ +function getAppBaseDir() { + return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent; +} + +/** + * Gets the root of the installation directory which is the application + * bundle directory on Mac OS X and the location of the application binary + * on all other platforms. + * + * @return nsIFile object for the directory + */ +function getInstallDirRoot() { + let dir = getAppBaseDir(); + if (AppConstants.platform == "macosx") { + // On Mac, we store the Updated.app directory inside the bundle directory. + dir = dir.parent.parent; + } + return dir; +} + +/** + * Gets the file at the specified hierarchy under the update root directory. + * @param pathArray + * An array of path components to locate beneath the directory + * specified by |key|. The last item in this array must be the + * leaf name of a file. + * @return nsIFile object for the file specified. The file is NOT created + * if it does not exist, however all required directories along + * the way are. + */ +function getUpdateFile(pathArray) { + let file = getUpdateDirCreate(pathArray.slice(0, -1)); + file.append(pathArray[pathArray.length - 1]); + return file; +} + +/** + * This function is designed to let us slightly clean up the mapping between + * strings and error codes. So that instead of having: + * check_error-2147500036=Connection aborted + * check_error-2152398850=Connection aborted + * We can have: + * check_error-connection_aborted=Connection aborted + * And map both of those error codes to it. + */ +function maybeMapErrorCode(code) { + switch (code) { + case Cr.NS_BINDING_ABORTED: + case Cr.NS_ERROR_ABORT: + return "connection_aborted"; + } + return code; +} + +/** + * Returns human readable status text from the updates.properties bundle + * based on an error code + * @param code + * The error code to look up human readable status text for + * @param defaultCode + * The default code to look up should human readable status text + * not exist for |code| + * @return A human readable status text string + */ +function getStatusTextFromCode(code, defaultCode) { + code = maybeMapErrorCode(code); + + let reason; + try { + reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + code); + LOG( + "getStatusTextFromCode - transfer error: " + reason + ", code: " + code + ); + } catch (e) { + defaultCode = maybeMapErrorCode(defaultCode); + + // Use the default reason + reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + defaultCode); + LOG( + "getStatusTextFromCode - transfer error: " + + reason + + ", default code: " + + defaultCode + ); + } + return reason; +} + +/** + * Get the Ready Update directory. This is the directory that an update + * should reside in after download has completed but before it has been + * installed and cleaned up. + * @return The ready updates directory, as a nsIFile object + */ +function getReadyUpdateDir() { + return getUpdateDirCreate([DIR_UPDATES, "0"]); +} + +/** + * Get the Downloading Update directory. This is the directory that an update + * should reside in during download. Once download is completed, it will be + * moved to the Ready Update directory. + * @return The downloading update directory, as a nsIFile object + */ +function getDownloadingUpdateDir() { + return getUpdateDirCreate([DIR_UPDATES, "downloading"]); +} + +/** + * Reads the update state from the update.status file in the specified + * directory. + * @param dir + * The dir to look for an update.status file in + * @return The status value of the update. + */ +function readStatusFile(dir) { + let statusFile = dir.clone(); + statusFile.append(FILE_UPDATE_STATUS); + let status = readStringFromFile(statusFile) || STATE_NONE; + LOG("readStatusFile - status: " + status + ", path: " + statusFile.path); + return status; +} + +/** + * Reads the binary transparency result file from the given directory. + * Removes the file if it is present (so don't call this twice and expect a + * result the second time). + * @param dir + * The dir to look for an update.bt file in + * @return A error code from verifying binary transparency information or null + * if the file was not present (indicating there was no error). + */ +function readBinaryTransparencyResult(dir) { + let binaryTransparencyResultFile = dir.clone(); + binaryTransparencyResultFile.append(FILE_BT_RESULT); + let result = readStringFromFile(binaryTransparencyResultFile); + LOG( + "readBinaryTransparencyResult - result: " + + result + + ", path: " + + binaryTransparencyResultFile.path + ); + // If result is non-null, the file exists. We should remove it to avoid + // double-reporting this result. + if (result) { + binaryTransparencyResultFile.remove(false); + } + return result; +} + +/** + * Writes the current update operation/state to a file in the patch + * directory, indicating to the patching system that operations need + * to be performed. + * @param dir + * The patch directory where the update.status file should be + * written. + * @param state + * The state value to write. + */ +function writeStatusFile(dir, state) { + let statusFile = dir.clone(); + statusFile.append(FILE_UPDATE_STATUS); + writeStringToFile(statusFile, state); +} + +/** + * Writes the update's application version to a file in the patch directory. If + * the update doesn't provide application version information via the + * appVersion attribute the string "null" will be written to the file. + * This value is compared during startup (in nsUpdateDriver.cpp) to determine if + * the update should be applied. Note that this won't provide protection from + * downgrade of the application for the nightly user case where the application + * version doesn't change. + * @param dir + * The patch directory where the update.version file should be + * written. + * @param version + * The version value to write. Will be the string "null" when the + * update doesn't provide the appVersion attribute in the update xml. + */ +function writeVersionFile(dir, version) { + let versionFile = dir.clone(); + versionFile.append(FILE_UPDATE_VERSION); + writeStringToFile(versionFile, version); +} + +/** + * Determines if the service should be used to attempt an update + * or not. + * + * @return true if the service should be used for updates. + */ +function shouldUseService() { + // This function will return true if the mantenance service should be used if + // all of the following conditions are met: + // 1) This build was done with the maintenance service enabled + // 2) The maintenance service is installed + // 3) The pref for using the service is enabled + if ( + !AppConstants.MOZ_MAINTENANCE_SERVICE || + !isServiceInstalled() || + !Services.prefs.getBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false) + ) { + LOG("shouldUseService - returning false"); + return false; + } + + LOG("shouldUseService - returning true"); + return true; +} + +/** + * Determines if the service is is installed. + * + * @return true if the service is installed. + */ +function isServiceInstalled() { + if (!AppConstants.MOZ_MAINTENANCE_SERVICE || AppConstants.platform != "win") { + LOG("isServiceInstalled - returning false"); + return false; + } + + let installed = 0; + try { + let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + wrk.open( + wrk.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\MaintenanceService", + wrk.ACCESS_READ | wrk.WOW64_64 + ); + installed = wrk.readIntValue("Installed"); + wrk.close(); + } catch (e) {} + installed = installed == 1; // convert to bool + LOG("isServiceInstalled - returning " + installed); + return installed; +} + +/** + * Gets the appropriate pending update state. Returns STATE_PENDING_SERVICE, + * STATE_PENDING_ELEVATE, or STATE_PENDING. + */ +function getBestPendingState() { + if (shouldUseService()) { + return STATE_PENDING_SERVICE; + } else if (getElevationRequired()) { + return STATE_PENDING_ELEVATE; + } + return STATE_PENDING; +} + +/** + * Removes the contents of the ready update directory and rotates the update + * logs when present. If the update.log exists in the patch directory this will + * move the last-update.log if it exists to backup-update.log in the parent + * directory of the patch directory and then move the update.log in the patch + * directory to last-update.log in the parent directory of the patch directory. + * + * @param aRemovePatchFiles (optional, defaults to true) + * When true the update's patch directory contents are removed. + */ +function cleanUpReadyUpdateDir(aRemovePatchFiles = true) { + let updateDir; + try { + updateDir = getReadyUpdateDir(); + } catch (e) { + LOG( + "cleanUpReadyUpdateDir - unable to get the updates patch directory. " + + "Exception: " + + e + ); + return; + } + + // Preserve the last update log file for debugging purposes. + let updateLogFile = updateDir.clone(); + updateLogFile.append(FILE_UPDATE_LOG); + if (updateLogFile.exists()) { + let dir = updateDir.parent; + let logFile = dir.clone(); + logFile.append(FILE_LAST_UPDATE_LOG); + if (logFile.exists()) { + try { + logFile.moveTo(dir, FILE_BACKUP_UPDATE_LOG); + } catch (e) { + LOG( + "cleanUpReadyUpdateDir - failed to rename file " + + logFile.path + + " to " + + FILE_BACKUP_UPDATE_LOG + ); + } + } + + try { + updateLogFile.moveTo(dir, FILE_LAST_UPDATE_LOG); + } catch (e) { + LOG( + "cleanUpReadyUpdateDir - failed to rename file " + + updateLogFile.path + + " to " + + FILE_LAST_UPDATE_LOG + ); + } + } + + if (aRemovePatchFiles) { + let dirEntries = updateDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + let file = dirEntries.nextFile; + // Now, recursively remove this file. The recursive removal is needed for + // Mac OSX because this directory will contain a copy of updater.app, + // which is itself a directory and the MozUpdater directory on platforms + // other than Windows. + try { + file.remove(true); + } catch (e) { + LOG("cleanUpReadyUpdateDir - failed to remove file " + file.path); + } + } + } +} + +/** + * Removes the contents of the update download directory. + * + */ +function cleanUpDownloadingUpdateDir() { + let updateDir; + try { + updateDir = getDownloadingUpdateDir(); + } catch (e) { + LOG( + "cleanUpDownloadUpdatesDir - unable to get the updates patch " + + "directory. Exception: " + + e + ); + return; + } + + let dirEntries = updateDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + let file = dirEntries.nextFile; + // Now, recursively remove this file. + try { + file.remove(true); + } catch (e) { + LOG("cleanUpDownloadUpdatesDir - failed to remove file " + file.path); + } + } +} + +/** + * Clean up the updates list and the directory that contains the update that + * is ready to be installed. + * + * Note - This function causes a state transition to either STATE_DOWNLOADING + * or STATE_NONE, depending on whether an update download is in progress. + */ +function cleanupReadyUpdate() { + // Move the update from the Active Update list into the Past Updates list. + if (lazy.UM.readyUpdate) { + LOG("cleanupReadyUpdate - Clearing readyUpdate"); + lazy.UM.addUpdateToHistory(lazy.UM.readyUpdate); + lazy.UM.readyUpdate = null; + } + lazy.UM.saveUpdates(); + + let readyUpdateDir = getReadyUpdateDir(); + let shouldSetDownloadingStatus = + lazy.UM.downloadingUpdate || + readStatusFile(readyUpdateDir) == STATE_DOWNLOADING; + + // Now trash the ready update directory, since we're done with it + cleanUpReadyUpdateDir(); + + // We need to handle two similar cases here. + // The first is where we clean up the ready updates directory while we are in + // the downloading state. In this case, we remove the update.status file that + // says we are downloading, even though we should remain in that state. + // The second case is when we clean up a ready update, but there is also a + // downloading update (in which case the update status file's state will + // reflect the state of the ready update, not the downloading one). In that + // case, instead of reverting to STATE_NONE (which is what we do by removing + // the status file), we should set our state to downloading. + if (shouldSetDownloadingStatus) { + LOG("cleanupReadyUpdate - Transitioning back to downloading state."); + transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING); + writeStatusFile(readyUpdateDir, STATE_DOWNLOADING); + } +} + +/** + * Clean up updates list and the directory that the currently downloading update + * is downloaded to. + * + * Note - This function may cause a state transition. If the current state is + * STATE_DOWNLOADING, this will cause it to change to STATE_NONE. + */ +function cleanupDownloadingUpdate() { + // Move the update from the Active Update list into the Past Updates list. + if (lazy.UM.downloadingUpdate) { + LOG("cleanupDownloadingUpdate - Clearing downloadingUpdate."); + lazy.UM.addUpdateToHistory(lazy.UM.downloadingUpdate); + lazy.UM.downloadingUpdate = null; + } + lazy.UM.saveUpdates(); + + // Now trash the update download directory, since we're done with it + cleanUpDownloadingUpdateDir(); + + // If the update status file says we are downloading, we should remove that + // too, since we aren't doing that anymore. + let readyUpdateDir = getReadyUpdateDir(); + let status = readStatusFile(readyUpdateDir); + if (status == STATE_DOWNLOADING) { + let statusFile = readyUpdateDir.clone(); + statusFile.append(FILE_UPDATE_STATUS); + statusFile.remove(); + } +} + +/** + * Clean up updates list, the ready update directory, and the downloading update + * directory. + * + * This is more efficient than calling + * cleanupReadyUpdate(); + * cleanupDownloadingUpdate(); + * because those need some special handling of the update status file to make + * sure that, for example, cleaning up a ready update doesn't make us forget + * that we are downloading an update. When we cleanup both updates, we don't + * need to worry about things like that. + * + * Note - This function causes a state transition to STATE_NONE. + */ +function cleanupActiveUpdates() { + // Move the update from the Active Update list into the Past Updates list. + if (lazy.UM.readyUpdate) { + LOG("cleanupActiveUpdates - Clearing readyUpdate"); + lazy.UM.addUpdateToHistory(lazy.UM.readyUpdate); + lazy.UM.readyUpdate = null; + } + if (lazy.UM.downloadingUpdate) { + LOG("cleanupActiveUpdates - Clearing downloadingUpdate."); + lazy.UM.addUpdateToHistory(lazy.UM.downloadingUpdate); + lazy.UM.downloadingUpdate = null; + } + lazy.UM.saveUpdates(); + + // Now trash both active update directories, since we're done with them + cleanUpReadyUpdateDir(); + cleanUpDownloadingUpdateDir(); +} + +/** + * Writes a string of text to a file. A newline will be appended to the data + * written to the file. This function only works with ASCII text. + * @param file An nsIFile indicating what file to write to. + * @param text A string containing the text to write to the file. + * @return true on success, false on failure. + */ +function writeStringToFile(file, text) { + try { + let fos = FileUtils.openSafeFileOutputStream(file); + text += "\n"; + fos.write(text, text.length); + FileUtils.closeSafeFileOutputStream(fos); + } catch (e) { + LOG(`writeStringToFile - Failed to write to file: "${file}". Error: ${e}"`); + return false; + } + return true; +} + +function readStringFromInputStream(inputStream) { + var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(inputStream); + var text = sis.read(sis.available()); + sis.close(); + if (text && text[text.length - 1] == "\n") { + text = text.slice(0, -1); + } + return text; +} + +/** + * Reads a string of text from a file. A trailing newline will be removed + * before the result is returned. This function only works with ASCII text. + */ +function readStringFromFile(file) { + if (!file.exists()) { + LOG("readStringFromFile - file doesn't exist: " + file.path); + return null; + } + var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); + return readStringFromInputStream(fis); +} + +/** + * Attempts to recover from an update error. If successful, `true` will be + * returned and AUS.currentState will be transitioned. + */ +function handleUpdateFailure(update) { + if (WRITE_ERRORS.includes(update.errorCode)) { + LOG( + "handleUpdateFailure - Failure is a write error. Setting state to pending" + ); + writeStatusFile(getReadyUpdateDir(), (update.state = STATE_PENDING)); + transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING); + return true; + } + + if (update.errorCode == SILENT_UPDATE_NEEDED_ELEVATION_ERROR) { + // There's no need to count attempts and escalate: it's expected that the + // background update task will try to update and fail due to required + // elevation repeatedly if, for example, the maintenance service is not + // available (or not functioning) and the installation requires privileges + // to update. + + let bestState = getBestPendingState(); + LOG( + "handleUpdateFailure - witnessed SILENT_UPDATE_NEEDED_ELEVATION_ERROR, " + + "returning to " + + bestState + ); + writeStatusFile(getReadyUpdateDir(), (update.state = bestState)); + + transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING); + // Return true to indicate a recoverable error. + return true; + } + + if (update.errorCode == ELEVATION_CANCELED) { + let elevationAttempts = Services.prefs.getIntPref( + PREF_APP_UPDATE_ELEVATE_ATTEMPTS, + 0 + ); + elevationAttempts++; + Services.prefs.setIntPref( + PREF_APP_UPDATE_ELEVATE_ATTEMPTS, + elevationAttempts + ); + let maxAttempts = Math.min( + Services.prefs.getIntPref(PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS, 2), + 10 + ); + + if (elevationAttempts > maxAttempts) { + LOG( + "handleUpdateFailure - notifying observers of error. " + + "topic: update-error, status: elevation-attempts-exceeded" + ); + Services.obs.notifyObservers( + update, + "update-error", + "elevation-attempts-exceeded" + ); + } else { + LOG( + "handleUpdateFailure - notifying observers of error. " + + "topic: update-error, status: elevation-attempt-failed" + ); + Services.obs.notifyObservers( + update, + "update-error", + "elevation-attempt-failed" + ); + } + + let cancelations = Services.prefs.getIntPref( + PREF_APP_UPDATE_CANCELATIONS, + 0 + ); + cancelations++; + Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations); + if (AppConstants.platform == "macosx") { + let osxCancelations = Services.prefs.getIntPref( + PREF_APP_UPDATE_CANCELATIONS_OSX, + 0 + ); + osxCancelations++; + Services.prefs.setIntPref( + PREF_APP_UPDATE_CANCELATIONS_OSX, + osxCancelations + ); + let maxCancels = Services.prefs.getIntPref( + PREF_APP_UPDATE_CANCELATIONS_OSX_MAX, + DEFAULT_CANCELATIONS_OSX_MAX + ); + // Prevent the preference from setting a value greater than 5. + maxCancels = Math.min(maxCancels, 5); + if (osxCancelations >= maxCancels) { + LOG( + "handleUpdateFailure - Too many OSX cancellations. Cleaning up " + + "ready update." + ); + cleanupReadyUpdate(); + return false; + } + LOG( + `handleUpdateFailure - OSX cancellation. Trying again by setting ` + + `status to "${STATE_PENDING_ELEVATE}".` + ); + writeStatusFile( + getReadyUpdateDir(), + (update.state = STATE_PENDING_ELEVATE) + ); + update.statusText = lazy.gUpdateBundle.GetStringFromName( + "elevationFailure" + ); + } else { + LOG( + "handleUpdateFailure - Failure because elevation was cancelled. " + + "again by setting status to pending." + ); + writeStatusFile(getReadyUpdateDir(), (update.state = STATE_PENDING)); + } + transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING); + return true; + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + + if (SERVICE_ERRORS.includes(update.errorCode)) { + var failCount = Services.prefs.getIntPref( + PREF_APP_UPDATE_SERVICE_ERRORS, + 0 + ); + var maxFail = Services.prefs.getIntPref( + PREF_APP_UPDATE_SERVICE_MAXERRORS, + DEFAULT_SERVICE_MAX_ERRORS + ); + // Prevent the preference from setting a value greater than 10. + maxFail = Math.min(maxFail, 10); + // As a safety, when the service reaches maximum failures, it will + // disable itself and fallback to using the normal update mechanism + // without the service. + if (failCount >= maxFail) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false); + Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); + } else { + failCount++; + Services.prefs.setIntPref(PREF_APP_UPDATE_SERVICE_ERRORS, failCount); + } + + LOG( + "handleUpdateFailure - Got a service error. Try to update without the " + + "service by setting the state to pending." + ); + writeStatusFile(getReadyUpdateDir(), (update.state = STATE_PENDING)); + transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING); + return true; + } + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); + } + + return false; +} + +/** + * Return the first UpdatePatch with the given type. + * @param update + * A nsIUpdate object to search through for a patch of the desired + * type. + * @param patch_type + * The type of the patch ("complete" or "partial") + * @return A nsIUpdatePatch object matching the type specified + */ +function getPatchOfType(update, patch_type) { + for (var i = 0; i < update.patchCount; ++i) { + var patch = update.getPatchAt(i); + if (patch && patch.type == patch_type) { + return patch; + } + } + return null; +} + +/** + * Fall back to downloading a complete update in case an update has failed. + * + * This will transition `AUS.currentState` to `STATE_DOWNLOADING` if there is + * another patch to download, or `STATE_IDLE` if there is not. + */ +async function handleFallbackToCompleteUpdate() { + // If we failed to install an update, we need to fall back to a complete + // update. If the install directory has been modified, more partial updates + // will fail for the same reason. Since we only download partial updates + // while there is already an update downloaded, we don't have to check the + // downloading update, we can be confident that we are not downloading the + // right thing at the moment. + + // The downloading update will be newer than the ready update, so use that + // update, if it exists. + let update = lazy.UM.downloadingUpdate || lazy.UM.readyUpdate; + if (!update) { + LOG( + "handleFallbackToCompleteUpdate - Unable to find an update to fall " + + "back to." + ); + return; + } + + LOG( + "handleFallbackToCompleteUpdate - Cleaning up active updates in " + + "preparation of falling back to complete update." + ); + await lazy.AUS.stopDownload(); + cleanupActiveUpdates(); + + if (!update.selectedPatch) { + // If we don't have a partial patch selected but a partial is available, + // _selectPatch() will download that instead of the complete patch. + let patch = getPatchOfType(update, "partial"); + if (patch) { + patch.selected = true; + } + } + + update.statusText = lazy.gUpdateBundle.GetStringFromName("patchApplyFailure"); + var oldType = update.selectedPatch ? update.selectedPatch.type : "complete"; + if (update.selectedPatch && oldType == "partial" && update.patchCount == 2) { + // Partial patch application failed, try downloading the complete + // update in the background instead. + LOG( + "handleFallbackToCompleteUpdate - install of partial patch " + + "failed, downloading complete patch" + ); + var success = await lazy.AUS.downloadUpdate(update); + if (!success) { + LOG( + "handleFallbackToCompleteUpdate - Starting complete patch download " + + "failed. Cleaning up downloading patch." + ); + cleanupDownloadingUpdate(); + } + } else { + LOG( + "handleFallbackToCompleteUpdate - install of complete or " + + "only one patch offered failed. Notifying observers. topic: " + + "update-error, status: unknown, " + + "update.patchCount: " + + update.patchCount + + ", " + + "oldType: " + + oldType + ); + transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE); + Services.obs.notifyObservers(update, "update-error", "unknown"); + } +} + +function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) { + let patchType = AUSTLMY.PATCH_UNKNOWN; + if (aUpdate && aUpdate.selectedPatch && aUpdate.selectedPatch.type) { + if (aUpdate.selectedPatch.type == "complete") { + patchType = AUSTLMY.PATCH_COMPLETE; + } else if (aUpdate.selectedPatch.type == "partial") { + patchType = AUSTLMY.PATCH_PARTIAL; + } + } + + let suffix = patchType + "_" + (aStartup ? AUSTLMY.STARTUP : AUSTLMY.STAGE); + let stateCode = 0; + let parts = aStatus.split(":"); + if (parts.length) { + switch (parts[0]) { + case STATE_NONE: + stateCode = 2; + break; + case STATE_DOWNLOADING: + stateCode = 3; + break; + case STATE_PENDING: + stateCode = 4; + break; + case STATE_PENDING_SERVICE: + stateCode = 5; + break; + case STATE_APPLYING: + stateCode = 6; + break; + case STATE_APPLIED: + stateCode = 7; + break; + case STATE_APPLIED_SERVICE: + stateCode = 9; + break; + case STATE_SUCCEEDED: + stateCode = 10; + break; + case STATE_DOWNLOAD_FAILED: + stateCode = 11; + break; + case STATE_FAILED: + stateCode = 12; + break; + case STATE_PENDING_ELEVATE: + stateCode = 13; + break; + // Note: Do not use stateCode 14 here. It is defined in + // UpdateTelemetry.jsm + default: + stateCode = 1; + } + + if (parts.length > 1) { + let statusErrorCode = INVALID_UPDATER_STATE_CODE; + if (parts[0] == STATE_FAILED) { + statusErrorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE; + } + AUSTLMY.pingStatusErrorCode(suffix, statusErrorCode); + } + } + let binaryTransparencyResult = readBinaryTransparencyResult( + getReadyUpdateDir() + ); + if (binaryTransparencyResult) { + AUSTLMY.pingBinaryTransparencyResult( + suffix, + parseInt(binaryTransparencyResult) + ); + } + AUSTLMY.pingStateCode(suffix, stateCode); +} + +/** + * This returns true if the passed update is the same version or older than the + * version and build ID values passed. Otherwise it returns false. + */ +function updateIsAtLeastAsOldAs(update, version, buildID) { + if (!update || !update.appVersion || !update.buildID) { + return false; + } + let versionComparison = Services.vc.compare(update.appVersion, version); + return ( + versionComparison < 0 || + (versionComparison == 0 && update.buildID == buildID) + ); +} + +/** + * This returns true if the passed update is the same version or older than + * currently installed Firefox version. + */ +function updateIsAtLeastAsOldAsCurrentVersion(update) { + return updateIsAtLeastAsOldAs( + update, + Services.appinfo.version, + Services.appinfo.appBuildID + ); +} + +/** + * This returns true if the passed update is the same version or older than + * the update that we have already downloaded (UpdateManager.readyUpdate). + * Returns false if no update has already been downloaded. + */ +function updateIsAtLeastAsOldAsReadyUpdate(update) { + if ( + !lazy.UM.readyUpdate || + !lazy.UM.readyUpdate.appVersion || + !lazy.UM.readyUpdate.buildID + ) { + return false; + } + return updateIsAtLeastAsOldAs( + update, + lazy.UM.readyUpdate.appVersion, + lazy.UM.readyUpdate.buildID + ); +} + +/** + * This function determines whether the error represented by the passed error + * code could potentially be recovered from or bypassed by updating without + * using the Maintenance Service (i.e. by showing a UAC prompt). + * We don't really want to show a UAC prompt, but it's preferable over the + * manual update doorhanger. So this function effectively distinguishes between + * which of those we should do if update staging failed. (The updater + * automatically falls back if the Maintenance Services fails, so this function + * doesn't handle that case) + * + * @param An integer error code from the update.status file. Should be one of + * the codes enumerated in updatererrors.h. + * @returns true if the code represents a Maintenance Service specific error. + * Otherwise, false. + */ +function isServiceSpecificErrorCode(errorCode) { + return ( + (errorCode >= 24 && errorCode <= 33) || (errorCode >= 49 && errorCode <= 58) + ); +} + +/** + * Normally when staging, `nsUpdateProcessor::WaitForProcess` waits for the + * staging process to complete by watching for its PID to terminate. + * However, there are less ideal situations. Notably, we might start the browser + * and find that update staging appears to already be in-progress. If that + * happens, we really want to pick up the update process from STATE_STAGING, + * but we don't really have any way of keeping an eye on the staging process + * other than to just poll the status file. + * + * Like `nsUpdateProcessor`, this calls `nsIUpdateManager.refreshUpdateStatus` + * after polling completes (regardless of result). + * + * It is also important to keep in mind that the updater might have crashed + * during staging, meaning that the status file will never change, no matter how + * long we keep polling. So we need to set an upper bound on how long we are + * willing to poll for. + * + * There are three situations that we want to avoid. + * (1) We don't want to set the poll interval too long. A user might be watching + * the user interface and waiting to restart to install the update. A long poll + * interval will cause them to have to wait longer than necessary. Especially + * since the expected total staging time is not that long. + * (2) We don't want to give up polling too early and give up on an update that + * will ultimately succeed. + * (3) We don't want to use a rapid polling interval over a long duration. + * + * To avoid these situations, we will start with a short polling interval, but + * will increase it the longer that we have to wait. Then if we hit the upper + * bound of polling, we will give up. + */ +function pollForStagingEnd() { + let pollingIntervalMs = STAGING_POLLING_MIN_INTERVAL_MS; + // Number of times to poll before increasing the polling interval. + let pollAttemptsAtIntervalRemaining = STAGING_POLLING_ATTEMPTS_PER_INTERVAL; + let timeElapsedMs = 0; + + let pollingFn = () => { + pollAttemptsAtIntervalRemaining -= 1; + // This isn't a perfectly accurate way of keeping time, but it does nicely + // sidestep dealing with issues of (non)monotonic time. + timeElapsedMs += pollingIntervalMs; + + if (timeElapsedMs >= STAGING_POLLING_MAX_DURATION_MS) { + lazy.UM.refreshUpdateStatus(); + return; + } + + if (readStatusFile(getReadyUpdateDir()) != STATE_APPLYING) { + lazy.UM.refreshUpdateStatus(); + return; + } + + if (pollAttemptsAtIntervalRemaining <= 0) { + pollingIntervalMs = Math.min( + pollingIntervalMs * 2, + STAGING_POLLING_MAX_INTERVAL_MS + ); + pollAttemptsAtIntervalRemaining = STAGING_POLLING_ATTEMPTS_PER_INTERVAL; + } + + lazy.setTimeout(pollingFn, pollingIntervalMs); + }; + + lazy.setTimeout(pollingFn, pollingIntervalMs); +} + +/** + * Update Patch + * @param patch + * A <patch> element to initialize this object with + * @throws if patch has a size of 0 + * @constructor + */ +function UpdatePatch(patch) { + this._properties = {}; + this.errorCode = 0; + this.finalURL = null; + this.state = STATE_NONE; + + for (let i = 0; i < patch.attributes.length; ++i) { + var attr = patch.attributes.item(i); + // If an undefined value is saved to the xml file it will be a string when + // it is read from the xml file. + if (attr.value == "undefined") { + continue; + } + switch (attr.name) { + case "xmlns": + // Don't save the XML namespace. + break; + case "selected": + this.selected = attr.value == "true"; + break; + case "size": + if (0 == parseInt(attr.value)) { + LOG("UpdatePatch:init - 0-sized patch!"); + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + this[attr.name] = attr.value; + break; + case "errorCode": + if (attr.value) { + let val = parseInt(attr.value); + // This will evaluate to false if the value is 0 but that's ok since + // this.errorCode is set to the default of 0 above. + if (val) { + this.errorCode = val; + } + } + break; + case "finalURL": + case "state": + case "type": + case "URL": + this[attr.name] = attr.value; + break; + default: + if (!this._attrNames.includes(attr.name)) { + // Set nsIPropertyBag properties that were read from the xml file. + this.setProperty(attr.name, attr.value); + } + break; + } + } +} +UpdatePatch.prototype = { + // nsIUpdatePatch attribute names used to prevent nsIWritablePropertyBag from + // over writing nsIUpdatePatch attributes. + _attrNames: [ + "errorCode", + "finalURL", + "selected", + "size", + "state", + "type", + "URL", + ], + + /** + * See nsIUpdateService.idl + */ + serialize: function UpdatePatch_serialize(updates) { + var patch = updates.createElementNS(URI_UPDATE_NS, "patch"); + patch.setAttribute("size", this.size); + patch.setAttribute("type", this.type); + patch.setAttribute("URL", this.URL); + // Don't write an errorCode if it evaluates to false since 0 is the same as + // no error code. + if (this.errorCode) { + patch.setAttribute("errorCode", this.errorCode); + } + // finalURL is not available until after the download has started + if (this.finalURL) { + patch.setAttribute("finalURL", this.finalURL); + } + // The selected patch is the only patch that should have this attribute. + if (this.selected) { + patch.setAttribute("selected", this.selected); + } + if (this.state != STATE_NONE) { + patch.setAttribute("state", this.state); + } + + for (let [name, value] of Object.entries(this._properties)) { + if (value.present && !this._attrNames.includes(name)) { + patch.setAttribute(name, value.data); + } + } + return patch; + }, + + /** + * See nsIWritablePropertyBag.idl + */ + setProperty: function UpdatePatch_setProperty(name, value) { + if (this._attrNames.includes(name)) { + throw Components.Exception( + "Illegal value '" + + name + + "' (attribute exists on nsIUpdatePatch) " + + "when calling method: [nsIWritablePropertyBag::setProperty]", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + this._properties[name] = { data: value, present: true }; + }, + + /** + * See nsIWritablePropertyBag.idl + */ + deleteProperty: function UpdatePatch_deleteProperty(name) { + if (this._attrNames.includes(name)) { + throw Components.Exception( + "Illegal value '" + + name + + "' (attribute exists on nsIUpdatePatch) " + + "when calling method: [nsIWritablePropertyBag::deleteProperty]", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + if (name in this._properties) { + this._properties[name].present = false; + } else { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + }, + + /** + * See nsIPropertyBag.idl + * + * Note: this only contains the nsIPropertyBag name / value pairs and not the + * nsIUpdatePatch name / value pairs. + */ + get enumerator() { + return this.enumerate(); + }, + + *enumerate() { + // An nsISupportsInterfacePointer is used so creating an array using + // Array.from will retain the QueryInterface for nsIProperty. + let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance( + Ci.nsISupportsInterfacePointer + ); + let qi = ChromeUtils.generateQI(["nsIProperty"]); + for (let [name, value] of Object.entries(this._properties)) { + if (value.present && !this._attrNames.includes(name)) { + // The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose + // elements are nsIProperty objects. Calling QueryInterface for + // nsIProperty on the object doesn't return to the caller an object that + // is already queried to nsIProperty but do it just in case it is fixed + // at some point. + ip.data = { name, value: value.data, QueryInterface: qi }; + yield ip.data.QueryInterface(Ci.nsIProperty); + } + } + }, + + /** + * See nsIPropertyBag.idl + * + * Note: returns null instead of throwing when the property doesn't exist to + * simplify code and to silence warnings in debug builds. + */ + getProperty: function UpdatePatch_getProperty(name) { + if (this._attrNames.includes(name)) { + throw Components.Exception( + "Illegal value '" + + name + + "' (attribute exists on nsIUpdatePatch) " + + "when calling method: [nsIWritablePropertyBag::getProperty]", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + if (name in this._properties && this._properties[name].present) { + return this._properties[name].data; + } + return null; + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIUpdatePatch", + "nsIPropertyBag", + "nsIWritablePropertyBag", + ]), +}; + +/** + * Update + * Implements nsIUpdate + * @param update + * An <update> element to initialize this object with + * @throws if the update contains no patches + * @constructor + */ +function Update(update) { + this._patches = []; + this._properties = {}; + this.isCompleteUpdate = false; + this.channel = "default"; + this.promptWaitTime = Services.prefs.getIntPref( + PREF_APP_UPDATE_PROMPTWAITTIME, + 43200 + ); + this.unsupported = false; + + // Null <update>, assume this is a message container and do no + // further initialization + if (!update) { + return; + } + + for (let i = 0; i < update.childNodes.length; ++i) { + let patchElement = update.childNodes.item(i); + if ( + patchElement.nodeType != patchElement.ELEMENT_NODE || + patchElement.localName != "patch" + ) { + continue; + } + + let patch; + try { + patch = new UpdatePatch(patchElement); + } catch (e) { + continue; + } + this._patches.push(patch); + } + + if (!this._patches.length && !update.hasAttribute("unsupported")) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + // Set the installDate value with the current time. If the update has an + // installDate attribute this will be replaced with that value if it doesn't + // equal 0. + this.installDate = new Date().getTime(); + this.patchCount = this._patches.length; + + for (let i = 0; i < update.attributes.length; ++i) { + let attr = update.attributes.item(i); + if (attr.name == "xmlns" || attr.value == "undefined") { + // Don't save the XML namespace or undefined values. + // If an undefined value is saved to the xml file it will be a string when + // it is read from the xml file. + continue; + } else if (attr.name == "detailsURL") { + this.detailsURL = attr.value; + } else if (attr.name == "installDate" && attr.value) { + let val = parseInt(attr.value); + if (val) { + this.installDate = val; + } + } else if (attr.name == "errorCode" && attr.value) { + let val = parseInt(attr.value); + if (val) { + // Set the value of |_errorCode| instead of |errorCode| since + // selectedPatch won't be available at this point and normally the + // nsIUpdatePatch will provide the errorCode. + this._errorCode = val; + } + } else if (attr.name == "isCompleteUpdate") { + this.isCompleteUpdate = attr.value == "true"; + } else if (attr.name == "promptWaitTime") { + if (!isNaN(attr.value)) { + this.promptWaitTime = parseInt(attr.value); + } + } else if (attr.name == "unsupported") { + this.unsupported = attr.value == "true"; + } else { + switch (attr.name) { + case "appVersion": + case "buildID": + case "channel": + case "displayVersion": + case "elevationFailure": + case "name": + case "previousAppVersion": + case "serviceURL": + case "statusText": + case "type": + this[attr.name] = attr.value; + break; + default: + if (!this._attrNames.includes(attr.name)) { + // Set nsIPropertyBag properties that were read from the xml file. + this.setProperty(attr.name, attr.value); + } + break; + } + } + } + + if (!this.previousAppVersion) { + this.previousAppVersion = Services.appinfo.version; + } + + if (!this.elevationFailure) { + this.elevationFailure = false; + } + + if (!this.detailsURL) { + try { + // Try using a default details URL supplied by the distribution + // if the update XML does not supply one. + this.detailsURL = Services.urlFormatter.formatURLPref( + PREF_APP_UPDATE_URL_DETAILS + ); + } catch (e) { + this.detailsURL = ""; + } + } + + if (!this.displayVersion) { + this.displayVersion = this.appVersion; + } + + if (!this.name) { + // When the update doesn't provide a name fallback to using + // "<App Name> <Update App Version>" + let brandBundle = Services.strings.createBundle(URI_BRAND_PROPERTIES); + let appName = brandBundle.GetStringFromName("brandShortName"); + this.name = lazy.gUpdateBundle.formatStringFromName("updateName", [ + appName, + this.displayVersion, + ]); + } +} +Update.prototype = { + // nsIUpdate attribute names used to prevent nsIWritablePropertyBag from over + // writing nsIUpdate attributes. + _attrNames: [ + "appVersion", + "buildID", + "channel", + "detailsURL", + "displayVersion", + "elevationFailure", + "errorCode", + "installDate", + "isCompleteUpdate", + "name", + "previousAppVersion", + "promptWaitTime", + "serviceURL", + "state", + "statusText", + "type", + "unsupported", + ], + + /** + * See nsIUpdateService.idl + */ + getPatchAt: function Update_getPatchAt(index) { + return this._patches[index]; + }, + + /** + * See nsIUpdateService.idl + * + * We use a copy of the state cached on this object in |_state| only when + * there is no selected patch, i.e. in the case when we could not load + * active updates from the update manager for some reason but still have + * the update.status file to work with. + */ + _state: "", + get state() { + if (this.selectedPatch) { + return this.selectedPatch.state; + } + return this._state; + }, + set state(state) { + if (this.selectedPatch) { + this.selectedPatch.state = state; + } + this._state = state; + }, + + /** + * See nsIUpdateService.idl + * + * We use a copy of the errorCode cached on this object in |_errorCode| only + * when there is no selected patch, i.e. in the case when we could not load + * active updates from the update manager for some reason but still have + * the update.status file to work with. + */ + _errorCode: 0, + get errorCode() { + if (this.selectedPatch) { + return this.selectedPatch.errorCode; + } + return this._errorCode; + }, + set errorCode(errorCode) { + if (this.selectedPatch) { + this.selectedPatch.errorCode = errorCode; + } + this._errorCode = errorCode; + }, + + /** + * See nsIUpdateService.idl + */ + get selectedPatch() { + for (let i = 0; i < this.patchCount; ++i) { + if (this._patches[i].selected) { + return this._patches[i]; + } + } + return null; + }, + + /** + * See nsIUpdateService.idl + */ + serialize: function Update_serialize(updates) { + // If appVersion isn't defined just return null. This happens when cleaning + // up invalid updates (e.g. incorrect channel). + if (!this.appVersion) { + return null; + } + let update = updates.createElementNS(URI_UPDATE_NS, "update"); + update.setAttribute("appVersion", this.appVersion); + update.setAttribute("buildID", this.buildID); + update.setAttribute("channel", this.channel); + update.setAttribute("detailsURL", this.detailsURL); + update.setAttribute("displayVersion", this.displayVersion); + update.setAttribute("installDate", this.installDate); + update.setAttribute("isCompleteUpdate", this.isCompleteUpdate); + update.setAttribute("name", this.name); + update.setAttribute("previousAppVersion", this.previousAppVersion); + update.setAttribute("promptWaitTime", this.promptWaitTime); + update.setAttribute("serviceURL", this.serviceURL); + update.setAttribute("type", this.type); + + if (this.statusText) { + update.setAttribute("statusText", this.statusText); + } + if (this.unsupported) { + update.setAttribute("unsupported", this.unsupported); + } + if (this.elevationFailure) { + update.setAttribute("elevationFailure", this.elevationFailure); + } + + for (let [name, value] of Object.entries(this._properties)) { + if (value.present && !this._attrNames.includes(name)) { + update.setAttribute(name, value.data); + } + } + + for (let i = 0; i < this.patchCount; ++i) { + update.appendChild(this.getPatchAt(i).serialize(updates)); + } + + updates.documentElement.appendChild(update); + return update; + }, + + /** + * See nsIWritablePropertyBag.idl + */ + setProperty: function Update_setProperty(name, value) { + if (this._attrNames.includes(name)) { + throw Components.Exception( + "Illegal value '" + + name + + "' (attribute exists on nsIUpdate) " + + "when calling method: [nsIWritablePropertyBag::setProperty]", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + this._properties[name] = { data: value, present: true }; + }, + + /** + * See nsIWritablePropertyBag.idl + */ + deleteProperty: function Update_deleteProperty(name) { + if (this._attrNames.includes(name)) { + throw Components.Exception( + "Illegal value '" + + name + + "' (attribute exists on nsIUpdate) " + + "when calling method: [nsIWritablePropertyBag::deleteProperty]", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + if (name in this._properties) { + this._properties[name].present = false; + } else { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + }, + + /** + * See nsIPropertyBag.idl + * + * Note: this only contains the nsIPropertyBag name value / pairs and not the + * nsIUpdate name / value pairs. + */ + get enumerator() { + return this.enumerate(); + }, + + *enumerate() { + // An nsISupportsInterfacePointer is used so creating an array using + // Array.from will retain the QueryInterface for nsIProperty. + let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance( + Ci.nsISupportsInterfacePointer + ); + let qi = ChromeUtils.generateQI(["nsIProperty"]); + for (let [name, value] of Object.entries(this._properties)) { + if (value.present && !this._attrNames.includes(name)) { + // The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose + // elements are nsIProperty objects. Calling QueryInterface for + // nsIProperty on the object doesn't return to the caller an object that + // is already queried to nsIProperty but do it just in case it is fixed + // at some point. + ip.data = { name, value: value.data, QueryInterface: qi }; + yield ip.data.QueryInterface(Ci.nsIProperty); + } + } + }, + + /** + * See nsIPropertyBag.idl + * Note: returns null instead of throwing when the property doesn't exist to + * simplify code and to silence warnings in debug builds. + */ + getProperty: function Update_getProperty(name) { + if (this._attrNames.includes(name)) { + throw Components.Exception( + "Illegal value '" + + name + + "' (attribute exists on nsIUpdate) " + + "when calling method: [nsIWritablePropertyBag::getProperty]", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + if (name in this._properties && this._properties[name].present) { + return this._properties[name].data; + } + return null; + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIUpdate", + "nsIPropertyBag", + "nsIWritablePropertyBag", + ]), +}; + +/** + * UpdateService + * A Service for managing the discovery and installation of software updates. + * @constructor + */ +function UpdateService() { + LOG("Creating UpdateService"); + // The observor notification to shut down the service must be before + // profile-before-change since nsIUpdateManager uses profile-before-change + // to shutdown and write the update xml files. + Services.obs.addObserver(this, "quit-application"); + // This one call observes PREF_APP_UPDATE_LOG and PREF_APP_UPDATE_LOG_FILE + Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this); + + this._logStatus(); +} + +UpdateService.prototype = { + /** + * The downloader we are using to download updates. There is only ever one of + * these. + */ + _downloader: null, + + /** + * Whether or not the service registered the "online" observer. + */ + _registeredOnlineObserver: false, + + /** + * The current number of consecutive socket errors + */ + _consecutiveSocketErrors: 0, + + /** + * A timer used to retry socket errors + */ + _retryTimer: null, + + /** + * Whether or not a background update check was initiated by the + * application update timer notification. + */ + _isNotify: true, + + /** + * Handle Observer Service notifications + * @param subject + * The subject of the notification + * @param topic + * The notification name + * @param data + * Additional data + */ + observe: async function AUS_observe(subject, topic, data) { + switch (topic) { + case "post-update-processing": + // This pref was not cleared out of profiles after it stopped being used + // (Bug 1420514), so clear it out on the next update to avoid confusion + // regarding its use. + Services.prefs.clearUserPref("app.update.enabled"); + Services.prefs.clearUserPref("app.update.BITS.inTrialGroup"); + + // Background tasks do not notify any delayed startup notifications. + if ( + !lazy.gIsBackgroundTaskMode && + Services.appinfo.ID in APPID_TO_TOPIC + ) { + // Delay post-update processing to ensure that possible update + // dialogs are shown in front of the app window, if possible. + // See bug 311614. + Services.obs.addObserver(this, APPID_TO_TOPIC[Services.appinfo.ID]); + break; + } + // intentional fallthrough + case "sessionstore-windows-restored": + case "mail-startup-done": + // Background tasks do not notify any delayed startup notifications. + if ( + !lazy.gIsBackgroundTaskMode && + Services.appinfo.ID in APPID_TO_TOPIC + ) { + Services.obs.removeObserver( + this, + APPID_TO_TOPIC[Services.appinfo.ID] + ); + } + // intentional fallthrough + case "test-post-update-processing": + // Clean up any extant updates + await this._postUpdateProcessing(); + break; + case "network:offline-status-changed": + await this._offlineStatusChanged(data); + break; + case "nsPref:changed": + if (data == PREF_APP_UPDATE_LOG || data == PREF_APP_UPDATE_LOG_FILE) { + lazy.gLogEnabled; // Assigning this before it is lazy-loaded is an error. + lazy.gLogEnabled = + Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) || + Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false); + } + if (data == PREF_APP_UPDATE_LOG_FILE) { + lazy.gLogfileEnabled; // Assigning this before it is lazy-loaded is an + // error. + lazy.gLogfileEnabled = Services.prefs.getBoolPref( + PREF_APP_UPDATE_LOG_FILE, + false + ); + if (lazy.gLogfileEnabled) { + this._logStatus(); + } + } + break; + case "quit-application": + Services.obs.removeObserver(this, topic); + Services.prefs.removeObserver(PREF_APP_UPDATE_LOG, this); + + if (AppConstants.platform == "win" && gUpdateMutexHandle) { + // If we hold the update mutex, let it go! + // The OS would clean this up sometime after shutdown, + // but that would have no guarantee on timing. + closeHandle(gUpdateMutexHandle); + gUpdateMutexHandle = null; + } + if (this._retryTimer) { + this._retryTimer.cancel(); + } + + // When downloading an update with nsIIncrementalDownload the download + // is stopped when the quit-application observer notification is + // received and networking hasn't started to shutdown. The download will + // be resumed the next time the application starts. Downloads using + // Windows BITS are not stopped since they don't require Firefox to be + // running to perform the download. + if (this._downloader) { + if (this._downloader.usingBits) { + await this._downloader.cleanup(); + } else { + // stopDownload() calls _downloader.cleanup() + await this.stopDownload(); + } + } + // Prevent leaking the downloader (bug 454964) + this._downloader = null; + // In case any update checks are in progress. + lazy.CheckSvc.stopAllChecks(); + + if (gLogfileOutputStream) { + gLogfileOutputStream.close(); + } + break; + case "test-close-handle-update-mutex": + if (Cu.isInAutomation) { + if (AppConstants.platform == "win" && gUpdateMutexHandle) { + LOG("UpdateService:observe - closing mutex handle for testing"); + closeHandle(gUpdateMutexHandle); + gUpdateMutexHandle = null; + } + } + break; + } + }, + + /** + * The following needs to happen during the post-update-processing + * notification from nsUpdateServiceStub.js: + * 1. post update processing + * 2. resume of a download that was in progress during a previous session + * 3. start of a complete update download after the failure to apply a partial + * update + */ + + /** + * Perform post-processing on updates lingering in the updates directory + * from a previous application session - either report install failures (and + * optionally attempt to fetch a different version if appropriate) or + * notify the user of install success. + */ + /* eslint-disable-next-line complexity */ + _postUpdateProcessing: async function AUS__postUpdateProcessing() { + if (this.disabled) { + // This function is a point when we can potentially enter the update + // system, even with update disabled. Make sure that we do not continue + // because update code can have side effects that are visible to the user + // and give the impression that updates are enabled. For example, if we + // can't write to the update directory, we might complain to the user that + // update is broken and they should reinstall. + return; + } + if (!this.canCheckForUpdates) { + LOG( + "UpdateService:_postUpdateProcessing - unable to check for " + + "updates... returning early" + ); + return; + } + let status = readStatusFile(getReadyUpdateDir()); + LOG(`UpdateService:_postUpdateProcessing - status = "${status}"`); + + if (!this.canApplyUpdates) { + LOG( + "UpdateService:_postUpdateProcessing - unable to apply " + + "updates... returning early" + ); + if (hasUpdateMutex()) { + // If the update is present in the update directory somehow, + // it would prevent us from notifying the user of further updates. + LOG( + "UpdateService:_postUpdateProcessing - Cleaning up active updates." + ); + cleanupActiveUpdates(); + } + return; + } + + let updates = []; + if (lazy.UM.readyUpdate) { + updates.push(lazy.UM.readyUpdate); + } + if (lazy.UM.downloadingUpdate) { + updates.push(lazy.UM.downloadingUpdate); + } + + if (status == STATE_NONE) { + // A status of STATE_NONE in _postUpdateProcessing means that the + // update.status file is present but there isn't an update in progress. + // This isn't an expected state, so if we find ourselves in it, we want + // to just clean things up to go back to a good state. + LOG( + "UpdateService:_postUpdateProcessing - Cleaning up unexpected state." + ); + if (!updates.length) { + updates.push(new Update(null)); + } + for (let update of updates) { + update.state = STATE_FAILED; + update.errorCode = ERR_UPDATE_STATE_NONE; + update.statusText = lazy.gUpdateBundle.GetStringFromName( + "statusFailed" + ); + } + let newStatus = STATE_FAILED + ": " + ERR_UPDATE_STATE_NONE; + pingStateAndStatusCodes(updates[0], true, newStatus); + cleanupActiveUpdates(); + return; + } + + let channelChanged = updates => { + for (let update of updates) { + if (update.channel != lazy.UpdateUtils.UpdateChannel) { + return true; + } + } + return false; + }; + if (channelChanged(updates)) { + LOG( + "UpdateService:_postUpdateProcessing - channel has changed, " + + "reloading default preferences to workaround bug 802022" + ); + // Workaround to get the distribution preferences loaded (Bug 774618). + // This can be removed after bug 802022 is fixed. Now that this code runs + // later during startup this code may no longer be necessary but it + // shouldn't be removed until after bug 802022 is fixed. + let prefSvc = Services.prefs.QueryInterface(Ci.nsIObserver); + prefSvc.observe(null, "reload-default-prefs", null); + if (channelChanged(updates)) { + let channel = lazy.UM.readyUpdate + ? lazy.UM.readyUpdate.channel + : lazy.UM.downloadingUpdate.channel; + LOG( + "UpdateService:_postUpdateProcessing - update channel is " + + "different than application's channel, removing update. update " + + "channel: " + + channel + + ", expected channel: " + + lazy.UpdateUtils.UpdateChannel + ); + // User switched channels, clear out the old active updates and remove + // partial downloads + for (let update of updates) { + update.state = STATE_FAILED; + update.errorCode = ERR_CHANNEL_CHANGE; + update.statusText = lazy.gUpdateBundle.GetStringFromName( + "statusFailed" + ); + } + let newStatus = STATE_FAILED + ": " + ERR_CHANNEL_CHANGE; + pingStateAndStatusCodes(updates[0], true, newStatus); + cleanupActiveUpdates(); + return; + } + } + + // Handle the case when the update is the same or older than the current + // version and nsUpdateDriver.cpp skipped updating due to the version being + // older than the current version. This also handles the general case when + // an update is for an older version or the same version and same build ID. + if ( + status == STATE_PENDING || + status == STATE_PENDING_SERVICE || + status == STATE_APPLIED || + status == STATE_APPLIED_SERVICE || + status == STATE_PENDING_ELEVATE || + status == STATE_DOWNLOADING + ) { + let tooOldUpdate; + if ( + updateIsAtLeastAsOldAs( + lazy.UM.readyUpdate, + Services.appinfo.version, + Services.appinfo.appBuildID + ) + ) { + tooOldUpdate = lazy.UM.readyUpdate; + } else if ( + updateIsAtLeastAsOldAs( + lazy.UM.downloadingUpdate, + Services.appinfo.version, + Services.appinfo.appBuildID + ) + ) { + tooOldUpdate = lazy.UM.downloadingUpdate; + } + if (tooOldUpdate) { + LOG( + "UpdateService:_postUpdateProcessing - removing update for older " + + "application version or same application version with same build " + + "ID. update application version: " + + tooOldUpdate.appVersion + + ", " + + "application version: " + + Services.appinfo.version + + ", update " + + "build ID: " + + tooOldUpdate.buildID + + ", application build ID: " + + Services.appinfo.appBuildID + ); + tooOldUpdate.state = STATE_FAILED; + tooOldUpdate.statusText = lazy.gUpdateBundle.GetStringFromName( + "statusFailed" + ); + tooOldUpdate.errorCode = ERR_OLDER_VERSION_OR_SAME_BUILD; + // This could be split out to report telemetry for each case. + let newStatus = STATE_FAILED + ": " + ERR_OLDER_VERSION_OR_SAME_BUILD; + pingStateAndStatusCodes(tooOldUpdate, true, newStatus); + // Cleanup both updates regardless of which one is too old. It's + // exceedingly unlikely that a user could end up in a state where one + // update is acceptable and the other isn't. And it makes this function + // considerably more complex to try to deal with that possibility. + cleanupActiveUpdates(); + return; + } + } + + pingStateAndStatusCodes( + status == STATE_DOWNLOADING + ? lazy.UM.downloadingUpdate + : lazy.UM.readyUpdate, + true, + status + ); + if (lazy.UM.downloadingUpdate || status == STATE_DOWNLOADING) { + if (status == STATE_SUCCEEDED) { + // If we successfully installed an update while we were downloading + // another update, the downloading update is now a partial MAR for + // a version that is no longer installed. We know that it's a partial + // MAR without checking because we currently only download partial MARs + // when an update has already been downloaded. + LOG( + "UpdateService:_postUpdateProcessing - removing downloading patch " + + "because we installed a different patch before it finished" + + "downloading." + ); + cleanupDownloadingUpdate(); + } else { + // Attempt to resume download + if (lazy.UM.downloadingUpdate) { + LOG( + "UpdateService:_postUpdateProcessing - resuming patch found in " + + "downloading state" + ); + let success = await this.downloadUpdate(lazy.UM.downloadingUpdate); + if (!success) { + LOG( + "UpdateService:_postUpdateProcessing - Failed to resume patch. " + + "Cleaning up downloading update." + ); + cleanupDownloadingUpdate(); + } + } else { + LOG( + "UpdateService:_postUpdateProcessing - Warning: found " + + "downloading state, but no downloading patch. Cleaning up " + + "active updates." + ); + // Put ourselves back in a good state. + cleanupActiveUpdates(); + } + if (status == STATE_DOWNLOADING) { + // Done dealing with the downloading update, and there is no ready + // update, so return early. + return; + } + } + } + + let update = lazy.UM.readyUpdate; + + if (status == STATE_APPLYING) { + // This indicates that the background updater service is in either of the + // following two states: + // 1. It is in the process of applying an update in the background, and + // we just happen to be racing against that. + // 2. It has failed to apply an update for some reason, and we hit this + // case because the updater process has set the update status to + // applying, but has never finished. + // In order to differentiate between these two states, we look at the + // state field of the update object. If it's "pending", then we know + // that this is the first time we're hitting this case, so we switch + // that state to "applying" and we just wait and hope for the best. + // If it's "applying", we know that we've already been here once, so + // we really want to start from a clean state. + if ( + update && + (update.state == STATE_PENDING || update.state == STATE_PENDING_SERVICE) + ) { + LOG( + "UpdateService:_postUpdateProcessing - patch found in applying " + + "state for the first time" + ); + update.state = STATE_APPLYING; + lazy.UM.saveUpdates(); + transitionState(Ci.nsIApplicationUpdateService.STATE_STAGING); + pollForStagingEnd(); + } else { + // We get here even if we don't have an update object + LOG( + "UpdateService:_postUpdateProcessing - patch found in applying " + + "state for the second time. Cleaning up ready update." + ); + cleanupReadyUpdate(); + } + return; + } + + if (!update) { + if (status != STATE_SUCCEEDED) { + LOG( + "UpdateService:_postUpdateProcessing - previous patch failed " + + "and no patch available. Cleaning up ready update." + ); + cleanupReadyUpdate(); + return; + } + LOG( + "UpdateService:_postUpdateProcessing - Update data missing. Creating " + + "an empty update object." + ); + update = new Update(null); + } + + let parts = status.split(":"); + update.state = parts[0]; + LOG( + `UpdateService:_postUpdateProcessing - Setting update's state from ` + + `the status file (="${update.state}")` + ); + if (update.state == STATE_FAILED && parts[1]) { + update.errorCode = parseInt(parts[1]); + LOG( + `UpdateService:_postUpdateProcessing - Setting update's errorCode ` + + `from the status file (="${update.errorCode}")` + ); + } + + if (status != STATE_SUCCEEDED) { + // Rotate the update logs so the update log isn't removed. By passing + // false the patch directory won't be removed. + cleanUpReadyUpdateDir(false); + } + + if (status == STATE_SUCCEEDED) { + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS); + } + update.statusText = lazy.gUpdateBundle.GetStringFromName( + "installSuccess" + ); + + // The only time that update is not a reference to readyUpdate is when + // readyUpdate is null. + if (!lazy.UM.readyUpdate) { + LOG( + "UpdateService:_postUpdateProcessing - Assigning successful update " + + "readyUpdate before cleaning it up." + ); + lazy.UM.readyUpdate = update; + } + + // Done with this update. Clean it up. + LOG( + "UpdateService:_postUpdateProcessing - Cleaning up successful ready " + + "update." + ); + cleanupReadyUpdate(); + + Services.prefs.setIntPref(PREF_APP_UPDATE_ELEVATE_ATTEMPTS, 0); + } else if (status == STATE_PENDING_ELEVATE) { + // In case the active-update.xml file is deleted. + if (!update) { + LOG( + "UpdateService:_postUpdateProcessing - status is pending-elevate " + + "but there isn't a ready update, removing update" + ); + cleanupReadyUpdate(); + } else { + transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING); + if (Services.startup.wasSilentlyStarted) { + // This check _should_ be unnecessary since we should not silently + // restart if state == pending-elevate. But the update elevation + // dialog is a way that we could potentially show UI on startup, even + // with no windows open. Which we really do not want to do on a silent + // restart. + // So this is defense in depth. + LOG( + "UpdateService:_postUpdateProcessing - status is " + + "pending-elevate, but this is a silent startup, so the " + + "elevation window has been suppressed." + ); + } else { + LOG( + "UpdateService:_postUpdateProcessing - status is " + + "pending-elevate. Showing Update elevation dialog." + ); + let uri = "chrome://mozapps/content/update/updateElevation.xhtml"; + let features = + "chrome,centerscreen,resizable=no,titlebar,toolbar=no,dialog=no"; + Services.ww.openWindow(null, uri, "Update:Elevation", features, null); + } + } + } else { + // If there was an I/O error it is assumed that the patch is not invalid + // and it is set to pending so an attempt to apply it again will happen + // when the application is restarted. + if (update.state == STATE_FAILED && update.errorCode) { + LOG( + "UpdateService:_postUpdateProcessing - Attempting handleUpdateFailure" + ); + if (handleUpdateFailure(update)) { + LOG( + "UpdateService:_postUpdateProcessing - handleUpdateFailure success." + ); + return; + } + } + + LOG( + "UpdateService:_postUpdateProcessing - Attempting to fall back to a " + + "complete update." + ); + // Something went wrong with the patch application process. + await handleFallbackToCompleteUpdate(); + } + }, + + /** + * Register an observer when the network comes online, so we can short-circuit + * the app.update.interval when there isn't connectivity + */ + _registerOnlineObserver: function AUS__registerOnlineObserver() { + if (this._registeredOnlineObserver) { + LOG( + "UpdateService:_registerOnlineObserver - observer already registered" + ); + return; + } + + LOG( + "UpdateService:_registerOnlineObserver - waiting for the network to " + + "be online, then forcing another check" + ); + + Services.obs.addObserver(this, "network:offline-status-changed"); + this._registeredOnlineObserver = true; + }, + + /** + * Called from the network:offline-status-changed observer. + */ + _offlineStatusChanged: async function AUS__offlineStatusChanged(status) { + if (status !== "online") { + return; + } + + Services.obs.removeObserver(this, "network:offline-status-changed"); + this._registeredOnlineObserver = false; + + LOG( + "UpdateService:_offlineStatusChanged - network is online, forcing " + + "another background check" + ); + + // the background checker is contained in notify + await this._attemptResume(); + }, + + /** + * See nsIUpdateService.idl + */ + onCheckComplete: async function AUS_onCheckComplete(result) { + if (result.succeeded) { + await this._selectAndInstallUpdate(result.updates); + return; + } + + if (!result.checksAllowed) { + LOG("UpdateService:onCheckComplete - checks not allowed"); + return; + } + + // On failure, result.updates is guaranteed to have exactly one update + // containing error information. + let update = result.updates[0]; + + LOG( + "UpdateService:onCheckComplete - error during background update. error " + + "code: " + + update.errorCode + + ", status text: " + + update.statusText + ); + + if (update.errorCode == NETWORK_ERROR_OFFLINE) { + // Register an online observer to try again + this._registerOnlineObserver(); + if (this._pingSuffix) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE); + } + return; + } + + // Send the error code to telemetry + AUSTLMY.pingCheckExError(this._pingSuffix, update.errorCode); + update.errorCode = BACKGROUNDCHECK_MULTIPLE_FAILURES; + let errCount = Services.prefs.getIntPref( + PREF_APP_UPDATE_BACKGROUNDERRORS, + 0 + ); + + // If we already have an update ready, we don't want to worry the user over + // update check failures. As far as the user knows, the update status is + // the status of the ready update. We don't want to confuse them by saying + // that an update check failed. + if (lazy.UM.readyUpdate) { + LOG( + "UpdateService:onCheckComplete - Ignoring error because another " + + "update is ready." + ); + return; + } + + errCount++; + Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount); + // Don't allow the preference to set a value greater than 20 for max errors. + let maxErrors = Math.min( + Services.prefs.getIntPref(PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10), + 20 + ); + + if (errCount >= maxErrors) { + LOG( + "UpdateService:onCheckComplete - notifying observers of error. " + + "topic: update-error, status: check-attempts-exceeded" + ); + Services.obs.notifyObservers( + update, + "update-error", + "check-attempts-exceeded" + ); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT); + } else { + LOG( + "UpdateService:onCheckComplete - notifying observers of error. " + + "topic: update-error, status: check-attempt-failed" + ); + Services.obs.notifyObservers( + update, + "update-error", + "check-attempt-failed" + ); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT); + } + }, + + /** + * Called when a connection should be resumed + */ + _attemptResume: async function AUS_attemptResume() { + LOG("UpdateService:_attemptResume"); + // If a download is in progress and we aren't already downloading it, then + // resume it. + if (this.isDownloading) { + // There is nothing to resume. We are already downloading. + LOG("UpdateService:_attemptResume - already downloading."); + return; + } + if ( + this._downloader && + this._downloader._patch && + this._downloader._patch.state == STATE_DOWNLOADING && + this._downloader._update + ) { + LOG( + "UpdateService:_attemptResume - _patch.state: " + + this._downloader._patch.state + ); + let success = await this.downloadUpdate(this._downloader._update); + LOG("UpdateService:_attemptResume - downloadUpdate success: " + success); + if (!success) { + LOG( + "UpdateService:_attemptResume - Resuming download failed. Cleaning " + + "up downloading update." + ); + cleanupDownloadingUpdate(); + } + return; + } + + // Kick off an update check + (async () => { + let check = lazy.CheckSvc.checkForUpdates(lazy.CheckSvc.BACKGROUND_CHECK); + await this.onCheckComplete(await check.result); + })(); + }, + + /** + * Notified when a timer fires + * @param timer + * The timer that fired + */ + notify: function AUS_notify(timer) { + this._checkForBackgroundUpdates(true); + }, + + /** + * See nsIUpdateService.idl + */ + checkForBackgroundUpdates: function AUS_checkForBackgroundUpdates() { + return this._checkForBackgroundUpdates(false); + }, + + // The suffix used for background update check telemetry histogram ID's. + get _pingSuffix() { + if (lazy.UM.readyUpdate) { + // Once an update has been downloaded, all later updates will be reported + // to telemetry as subsequent updates. We move the first update into + // readyUpdate as soon as the download is complete, so any update checks + // after readyUpdate is no longer null are subsequent update checks. + return AUSTLMY.SUBSEQUENT; + } + return this._isNotify ? AUSTLMY.NOTIFY : AUSTLMY.EXTERNAL; + }, + + /** + * Checks for updates in the background. + * @param isNotify + * Whether or not a background update check was initiated by the + * application update timer notification. + */ + _checkForBackgroundUpdates: function AUS__checkForBackgroundUpdates( + isNotify + ) { + if (!this.disabled && AppConstants.NIGHTLY_BUILD) { + // Scalar ID: update.suppress_prompts + AUSTLMY.pingSuppressPrompts(); + } + if (this.disabled || this.manualUpdateOnly) { + // Return immediately if we are disabled by policy. Otherwise, just the + // telemetry we try to collect below can potentially trigger a restart + // prompt if the update directory isn't writable. And we shouldn't be + // telling the user about update failures if update is disabled. + // See Bug 1599590. + // Note that we exit unconditionally here if we are only doing manual + // update checks, because manual update checking uses a completely + // different code path (AppUpdater.jsm creates its own nsIUpdateChecker), + // bypassing this function completely. + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_BY_POLICY); + return false; + } + + this._isNotify = isNotify; + + // Histogram IDs: + // UPDATE_PING_COUNT_EXTERNAL + // UPDATE_PING_COUNT_NOTIFY + // UPDATE_PING_COUNT_SUBSEQUENT + AUSTLMY.pingGeneric("UPDATE_PING_COUNT_" + this._pingSuffix, true, false); + + // Histogram IDs: + // UPDATE_UNABLE_TO_APPLY_EXTERNAL + // UPDATE_UNABLE_TO_APPLY_NOTIFY + // UPDATE_UNABLE_TO_APPLY_SUBSEQUENT + AUSTLMY.pingGeneric( + "UPDATE_UNABLE_TO_APPLY_" + this._pingSuffix, + getCanApplyUpdates(), + true + ); + // Histogram IDs: + // UPDATE_CANNOT_STAGE_EXTERNAL + // UPDATE_CANNOT_STAGE_NOTIFY + // UPDATE_CANNOT_STAGE_SUBSEQUENT + AUSTLMY.pingGeneric( + "UPDATE_CANNOT_STAGE_" + this._pingSuffix, + getCanStageUpdates(), + true + ); + if (AppConstants.platform == "win") { + // Histogram IDs: + // UPDATE_CAN_USE_BITS_EXTERNAL + // UPDATE_CAN_USE_BITS_NOTIFY + // UPDATE_CAN_USE_BITS_SUBSEQUENT + AUSTLMY.pingGeneric( + "UPDATE_CAN_USE_BITS_" + this._pingSuffix, + getCanUseBits() + ); + } + // Histogram IDs: + // UPDATE_INVALID_LASTUPDATETIME_EXTERNAL + // UPDATE_INVALID_LASTUPDATETIME_NOTIFY + // UPDATE_INVALID_LASTUPDATETIME_SUBSEQUENT + // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL + // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY + // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_SUBSEQUENT + AUSTLMY.pingLastUpdateTime(this._pingSuffix); + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY + // UPDATE_NOT_PREF_UPDATE_AUTO_SUBSEQUENT + lazy.UpdateUtils.getAppUpdateAutoEnabled().then(enabled => { + AUSTLMY.pingGeneric( + "UPDATE_NOT_PREF_UPDATE_AUTO_" + this._pingSuffix, + enabled, + true + ); + }); + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY + // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_SUBSEQUENT + AUSTLMY.pingBoolPref( + "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_" + this._pingSuffix, + PREF_APP_UPDATE_STAGING_ENABLED, + true, + true + ); + if (AppConstants.platform == "win" || AppConstants.platform == "macosx") { + // Histogram IDs: + // UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL + // UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY + // UPDATE_PREF_UPDATE_CANCELATIONS_SUBSEQUENT + AUSTLMY.pingIntPref( + "UPDATE_PREF_UPDATE_CANCELATIONS_" + this._pingSuffix, + PREF_APP_UPDATE_CANCELATIONS, + 0, + 0 + ); + } + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + // Histogram IDs: + // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL + // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY + // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_SUBSEQUENT + AUSTLMY.pingBoolPref( + "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_" + this._pingSuffix, + PREF_APP_UPDATE_SERVICE_ENABLED, + true + ); + // Histogram IDs: + // UPDATE_PREF_SERVICE_ERRORS_EXTERNAL + // UPDATE_PREF_SERVICE_ERRORS_NOTIFY + // UPDATE_PREF_SERVICE_ERRORS_SUBSEQUENT + AUSTLMY.pingIntPref( + "UPDATE_PREF_SERVICE_ERRORS_" + this._pingSuffix, + PREF_APP_UPDATE_SERVICE_ERRORS, + 0, + 0 + ); + if (AppConstants.platform == "win") { + // Histogram IDs: + // UPDATE_SERVICE_INSTALLED_EXTERNAL + // UPDATE_SERVICE_INSTALLED_NOTIFY + // UPDATE_SERVICE_INSTALLED_SUBSEQUENT + // UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL + // UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY + // UPDATE_SERVICE_MANUALLY_UNINSTALLED_SUBSEQUENT + AUSTLMY.pingServiceInstallStatus( + this._pingSuffix, + isServiceInstalled() + ); + } + } + + // If a download is in progress or the patch has been staged do nothing. + if (this.isDownloading) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADING); + return false; + } + + // Once we have downloaded a complete update, do not download further + // updates until the complete update is installed. This is important, + // because if we fall back from a partial update to a complete update, + // it might be because of changes to the patch directory (which would cause + // a failure to apply any partial MAR). So we really don't want to replace + // a downloaded complete update with a downloaded partial update. And we + // do not currently download complete updates if there is already a + // readyUpdate available. + if ( + lazy.UM.readyUpdate && + lazy.UM.readyUpdate.selectedPatch && + lazy.UM.readyUpdate.selectedPatch.type == "complete" + ) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED); + return false; + } + + // If we start downloading an update while the readyUpdate is staging, we + // run the risk of eventually wanting to overwrite readyUpdate with the + // downloadingUpdate while the readyUpdate is still staging. Then we would + // have to have a weird intermediate state where the downloadingUpdate has + // finished downloading, but can't be moved yet. It's simpler to just not + // start a new update if the old one is still staging. + if (this.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED); + return false; + } + + // Asynchronously kick off update checking + (async () => { + let validUpdateURL = true; + try { + await lazy.CheckSvc.getUpdateURL(lazy.CheckSvc.BACKGROUND_CHECK); + } catch (e) { + validUpdateURL = false; + } + + // The following checks are done here so they can be differentiated from + // foreground checks. + if (!lazy.UpdateUtils.OSVersion) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION); + } else if (!lazy.UpdateUtils.ABI) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI); + } else if (!validUpdateURL) { + AUSTLMY.pingCheckCode( + this._pingSuffix, + AUSTLMY.CHK_INVALID_DEFAULT_URL + ); + } else if (!hasUpdateMutex()) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX); + } else if (isOtherInstanceRunning()) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OTHER_INSTANCE); + } else if (!this.canCheckForUpdates) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK); + } + + let check = lazy.CheckSvc.checkForUpdates(lazy.CheckSvc.BACKGROUND_CHECK); + await this.onCheckComplete(await check.result); + })(); + + return true; + }, + + /** + * Determine the update from the specified updates that should be offered. + * If both valid major and minor updates are available the minor update will + * be offered. + * @param updates + * An array of available nsIUpdate items + * @return The nsIUpdate to offer. + */ + selectUpdate: function AUS_selectUpdate(updates) { + if (!updates.length) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_UPDATE_FOUND); + return null; + } + + // The ping for unsupported is sent after the call to showPrompt. + if (updates.length == 1 && updates[0].unsupported) { + return updates[0]; + } + + // Choose the newest of the available minor and major updates. + var majorUpdate = null; + var minorUpdate = null; + var vc = Services.vc; + let lastCheckCode = AUSTLMY.CHK_NO_COMPAT_UPDATE_FOUND; + + updates.forEach(function(aUpdate) { + // Ignore updates for older versions of the application and updates for + // the same version of the application with the same build ID. + if (updateIsAtLeastAsOldAsCurrentVersion(aUpdate)) { + LOG( + "UpdateService:selectUpdate - skipping update because the " + + "update's application version is not greater than the current " + + "application version" + ); + lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION; + return; + } + + if (updateIsAtLeastAsOldAsReadyUpdate(aUpdate)) { + LOG( + "UpdateService:selectUpdate - skipping update because the " + + "update's application version is not greater than that of the " + + "currently downloaded update" + ); + lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION; + return; + } + + if (lazy.UM.readyUpdate && !getPatchOfType(aUpdate, "partial")) { + LOG( + "UpdateService:selectUpdate - skipping update because no partial " + + "patch is available and an update has already been downloaded." + ); + lastCheckCode = AUSTLMY.CHK_NO_PARTIAL_PATCH; + return; + } + + switch (aUpdate.type) { + case "major": + if (!majorUpdate) { + majorUpdate = aUpdate; + } else if ( + vc.compare(majorUpdate.appVersion, aUpdate.appVersion) <= 0 + ) { + majorUpdate = aUpdate; + } + break; + case "minor": + if (!minorUpdate) { + minorUpdate = aUpdate; + } else if ( + vc.compare(minorUpdate.appVersion, aUpdate.appVersion) <= 0 + ) { + minorUpdate = aUpdate; + } + break; + default: + LOG( + "UpdateService:selectUpdate - skipping unknown update type: " + + aUpdate.type + ); + lastCheckCode = AUSTLMY.CHK_UPDATE_INVALID_TYPE; + break; + } + }); + + let update = minorUpdate || majorUpdate; + if (AppConstants.platform == "macosx" && update) { + if (getElevationRequired()) { + let installAttemptVersion = Services.prefs.getCharPref( + PREF_APP_UPDATE_ELEVATE_VERSION, + null + ); + if (vc.compare(installAttemptVersion, update.appVersion) != 0) { + Services.prefs.setCharPref( + PREF_APP_UPDATE_ELEVATE_VERSION, + update.appVersion + ); + if ( + Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX) + ) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + } else { + let numCancels = Services.prefs.getIntPref( + PREF_APP_UPDATE_CANCELATIONS_OSX, + 0 + ); + let rejectedVersion = Services.prefs.getCharPref( + PREF_APP_UPDATE_ELEVATE_NEVER, + "" + ); + let maxCancels = Services.prefs.getIntPref( + PREF_APP_UPDATE_CANCELATIONS_OSX_MAX, + DEFAULT_CANCELATIONS_OSX_MAX + ); + if (numCancels >= maxCancels) { + LOG( + "UpdateService:selectUpdate - the user requires elevation to " + + "install this update, but the user has exceeded the max " + + "number of elevation attempts." + ); + update.elevationFailure = true; + AUSTLMY.pingCheckCode( + this._pingSuffix, + AUSTLMY.CHK_ELEVATION_DISABLED_FOR_VERSION + ); + } else if (vc.compare(rejectedVersion, update.appVersion) == 0) { + LOG( + "UpdateService:selectUpdate - the user requires elevation to " + + "install this update, but elevation is disabled for this " + + "version." + ); + update.elevationFailure = true; + AUSTLMY.pingCheckCode( + this._pingSuffix, + AUSTLMY.CHK_ELEVATION_OPTOUT_FOR_VERSION + ); + } else { + LOG( + "UpdateService:selectUpdate - the user requires elevation to " + + "install the update." + ); + } + } + } else { + // Clear elevation-related prefs since they no longer apply (the user + // may have gained write access to the Firefox directory or an update + // was executed with a different profile). + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_VERSION)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_VERSION); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + } + } else if (!update) { + AUSTLMY.pingCheckCode(this._pingSuffix, lastCheckCode); + } + + return update; + }, + + /** + * Determine which of the specified updates should be installed and begin the + * download/installation process or notify the user about the update. + * @param updates + * An array of available updates + */ + _selectAndInstallUpdate: async function AUS__selectAndInstallUpdate(updates) { + // Return early if there's an active update. The user is already aware and + // is downloading or performed some user action to prevent notification. + if (lazy.UM.downloadingUpdate) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_HAS_ACTIVEUPDATE); + return; + } + + if (this.disabled) { + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_BY_POLICY); + LOG( + "UpdateService:_selectAndInstallUpdate - not prompting because " + + "update is disabled" + ); + return; + } + + var update = this.selectUpdate(updates); + if (!update || update.elevationFailure) { + return; + } + + if (update.unsupported) { + LOG( + "UpdateService:_selectAndInstallUpdate - update not supported for " + + "this system. Notifying observers. topic: update-available, " + + "status: unsupported" + ); + Services.obs.notifyObservers(update, "update-available", "unsupported"); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED); + return; + } + + if (!getCanApplyUpdates()) { + LOG( + "UpdateService:_selectAndInstallUpdate - the user is unable to " + + "apply updates... prompting. Notifying observers. " + + "topic: update-available, status: cant-apply" + ); + Services.obs.notifyObservers(null, "update-available", "cant-apply"); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY); + return; + } + + /** + * From this point on there are two possible outcomes: + * 1. download and install the update automatically + * 2. notify the user about the availability of an update + * + * Notes: + * a) if the app.update.auto preference is false then automatic download and + * install is disabled and the user will be notified. + * + * If the update when it is first read does not have an appVersion attribute + * the following deprecated behavior will occur: + * Update Type Outcome + * Major Notify + * Minor Auto Install + */ + let updateAuto = await lazy.UpdateUtils.getAppUpdateAutoEnabled(); + if (!updateAuto) { + LOG( + "UpdateService:_selectAndInstallUpdate - prompting because silent " + + "install is disabled. Notifying observers. topic: update-available, " + + "status: show-prompt" + ); + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF); + Services.obs.notifyObservers(update, "update-available", "show-prompt"); + return; + } + + LOG("UpdateService:_selectAndInstallUpdate - download the update"); + let success = await this.downloadUpdate(update); + if (!success && !this.isDownloading) { + LOG( + "UpdateService:_selectAndInstallUpdate - Failed to start downloading " + + "update. Cleaning up downloading update." + ); + cleanupDownloadingUpdate(); + } + AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DOWNLOAD_UPDATE); + }, + + get disabledForTesting() { + return ( + (Cu.isInAutomation || + lazy.Marionette.running || + lazy.RemoteAgent.running) && + Services.prefs.getBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false) + ); + }, + + /** + * See nsIUpdateService.idl + */ + get disabled() { + return ( + (Services.policies && !Services.policies.isAllowed("appUpdate")) || + this.disabledForTesting || + Services.sysinfo.getProperty("isPackagedApp") + ); + }, + + /** + * See nsIUpdateService.idl + */ + get manualUpdateOnly() { + return ( + Services.policies && !Services.policies.isAllowed("autoAppUpdateChecking") + ); + }, + + /** + * See nsIUpdateService.idl + */ + get canUsuallyCheckForUpdates() { + if (this.disabled) { + LOG( + "UpdateService.canUsuallyCheckForUpdates - unable to automatically check " + + "for updates, the option has been disabled by the administrator." + ); + return false; + } + + // If we don't know the binary platform we're updating, we can't update. + if (!lazy.UpdateUtils.ABI) { + LOG( + "UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " + + "unknown ABI" + ); + return false; + } + + // If we don't know the OS version we're updating, we can't update. + if (!lazy.UpdateUtils.OSVersion) { + LOG( + "UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " + + "unknown OS version" + ); + return false; + } + + LOG("UpdateService.canUsuallyCheckForUpdates - able to check for updates"); + return true; + }, + + /** + * See nsIUpdateService.idl + */ + get canCheckForUpdates() { + if (!this.canUsuallyCheckForUpdates) { + return false; + } + + if (!hasUpdateMutex()) { + LOG( + "UpdateService.canCheckForUpdates - unable to check for updates, " + + "unable to acquire update mutex" + ); + return false; + } + + if (isOtherInstanceRunning()) { + // This doesn't block update checks, but we will have to wait until either + // the other instance is gone or we time out waiting for it. + LOG( + "UpdateService.canCheckForUpdates - another instance is holding the " + + "lock, will need to wait for it prior to checking for updates" + ); + } + + LOG("UpdateService.canCheckForUpdates - able to check for updates"); + return true; + }, + + /** + * See nsIUpdateService.idl + */ + get elevationRequired() { + return getElevationRequired(); + }, + + /** + * See nsIUpdateService.idl + */ + get canUsuallyApplyUpdates() { + return getCanApplyUpdates(); + }, + + /** + * See nsIUpdateService.idl + */ + get canApplyUpdates() { + return ( + this.canUsuallyApplyUpdates && + hasUpdateMutex() && + !isOtherInstanceRunning() + ); + }, + + /** + * See nsIUpdateService.idl + */ + get canUsuallyStageUpdates() { + return getCanStageUpdates(false); + }, + + /** + * See nsIUpdateService.idl + */ + get canStageUpdates() { + return getCanStageUpdates(); + }, + + /** + * See nsIUpdateService.idl + */ + get canUsuallyUseBits() { + return getCanUseBits(false) == "CanUseBits"; + }, + + /** + * See nsIUpdateService.idl + */ + get canUseBits() { + return getCanUseBits() == "CanUseBits"; + }, + + /** + * See nsIUpdateService.idl + */ + get isOtherInstanceHandlingUpdates() { + return !hasUpdateMutex() || isOtherInstanceRunning(); + }, + + /** + * A set of download listeners to be notified by this._downloader when it + * receives nsIRequestObserver or nsIProgressEventSink method calls. + * + * These are stored on the UpdateService rather than on the Downloader, + * because they ought to persist across multiple Downloader instances. + */ + _downloadListeners: new Set(), + + /** + * See nsIUpdateService.idl + */ + addDownloadListener: function AUS_addDownloadListener(listener) { + let oldSize = this._downloadListeners.size; + this._downloadListeners.add(listener); + + if (this._downloadListeners.size == oldSize) { + LOG( + "UpdateService:addDownloadListener - Warning: Didn't add duplicate " + + "listener" + ); + return; + } + + if (this._downloader) { + this._downloader.onDownloadListenerAdded(); + } + }, + + /** + * See nsIUpdateService.idl + */ + removeDownloadListener: function AUS_removeDownloadListener(listener) { + let elementRemoved = this._downloadListeners.delete(listener); + + if (!elementRemoved) { + LOG( + "UpdateService:removeDownloadListener - Warning: Didn't remove " + + "non-existent listener" + ); + return; + } + + if (this._downloader) { + this._downloader.onDownloadListenerRemoved(); + } + }, + + /** + * Returns a boolean indicating whether there are any download listeners + */ + get hasDownloadListeners() { + return !!this._downloadListeners.length; + }, + + /* + * Calls the provided function once with each download listener that is + * currently registered. + */ + forEachDownloadListener: function AUS_forEachDownloadListener(fn) { + // Make a shallow copy in case listeners remove themselves. + let listeners = new Set(this._downloadListeners); + listeners.forEach(fn); + }, + + /** + * See nsIUpdateService.idl + */ + downloadUpdate: async function AUS_downloadUpdate(update) { + if (!update) { + throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER); + } + + // Don't download the update if the update's version is less than the + // current application's version or the update's version is the same as the + // application's version and the build ID is the same as the application's + // build ID. If we already have an update ready, we want to apply those + // same checks against the version of the ready update, so that we don't + // download an update that isn't newer than the one we already have. + if (updateIsAtLeastAsOldAsCurrentVersion(update)) { + LOG( + "UpdateService:downloadUpdate - Skipping download of update since " + + "it is for an earlier or same application version and build ID.\n" + + "current application version: " + + Services.appinfo.version + + "\n" + + "update application version : " + + update.appVersion + + "\n" + + "current build ID: " + + Services.appinfo.appBuildID + + "\n" + + "update build ID : " + + update.buildID + ); + return false; + } + if (updateIsAtLeastAsOldAsReadyUpdate(update)) { + LOG( + "UpdateService:downloadUpdate - not downloading update because the " + + "update that's already been downloaded is the same version or " + + "newer.\n" + + "currently downloaded update application version: " + + lazy.UM.readyUpdate.appVersion + + "\n" + + "available update application version : " + + update.appVersion + + "\n" + + "currently downloaded update build ID: " + + lazy.UM.readyUpdate.buildID + + "\n" + + "available update build ID : " + + update.buildID + ); + return false; + } + + // If a download request is in progress vs. a download ready to resume + if (this.isDownloading) { + if (update.isCompleteUpdate == this._downloader.isCompleteUpdate) { + LOG( + "UpdateService:downloadUpdate - no support for downloading more " + + "than one update at a time" + ); + return true; + } + this._downloader.cancel(); + } + this._downloader = new Downloader(this); + return this._downloader.downloadUpdate(update); + }, + + /** + * See nsIUpdateService.idl + */ + stopDownload: async function AUS_stopDownload() { + if (this.isDownloading) { + await this._downloader.cancel(); + } else if (this._retryTimer) { + // Download status is still considered as 'downloading' during retry. + // We need to cancel both retry and download at this stage. + this._retryTimer.cancel(); + this._retryTimer = null; + if (this._downloader) { + await this._downloader.cancel(); + } + } + if (this._downloader) { + await this._downloader.cleanup(); + } + this._downloader = null; + }, + + /** + * Note that this is different from checking if `currentState` is + * `STATE_DOWNLOADING` because if we are downloading a second update, this + * will be `true` while `currentState` will be `STATE_PENDING`. + */ + get isDownloading() { + return this._downloader && this._downloader.isBusy; + }, + + _logStatus: function AUS__logStatus() { + if (!lazy.gLogEnabled) { + return; + } + if (this.disabled) { + LOG("Current UpdateService status: disabled"); + // Return early if UpdateService is disabled by policy. Otherwise some of + // the getters we call to display status information may discover that the + // update directory is not writable, which automatically results in the + // permissions being fixed. Which we shouldn't really be doing if update + // is disabled by policy. + return; + } + LOG("Logging current UpdateService status:"); + // These getters print their own logging + this.canCheckForUpdates; + this.canApplyUpdates; + this.canStageUpdates; + LOG("Elevation required: " + this.elevationRequired); + LOG( + "Other instance of the application currently running: " + + this.isOtherInstanceHandlingUpdates + ); + LOG("Downloading: " + !!this.isDownloading); + if (this._downloader && this._downloader.isBusy) { + LOG("Downloading complete update: " + this._downloader.isCompleteUpdate); + LOG("Downloader using BITS: " + this._downloader.usingBits); + if (this._downloader._patch) { + // This will print its own logging + this._downloader._canUseBits(this._downloader._patch); + + // Downloader calls QueryInterface(Ci.nsIWritablePropertyBag) on + // its _patch member as soon as it is assigned, so no need to do so + // again here. + let bitsResult = this._downloader._patch.getProperty("bitsResult"); + if (bitsResult != null) { + LOG("Patch BITS result: " + bitsResult); + } + let internalResult = this._downloader._patch.getProperty( + "internalResult" + ); + if (internalResult != null) { + LOG("Patch nsIIncrementalDownload result: " + internalResult); + } + } + } + LOG("End of UpdateService status"); + }, + + /** + * See nsIUpdateService.idl + */ + get onlyDownloadUpdatesThisSession() { + return gOnlyDownloadUpdatesThisSession; + }, + + /** + * See nsIUpdateService.idl + */ + set onlyDownloadUpdatesThisSession(newValue) { + gOnlyDownloadUpdatesThisSession = newValue; + }, + + /** + * See nsIUpdateService.idl + */ + getStateName(state) { + switch (state) { + case Ci.nsIApplicationUpdateService.STATE_IDLE: + return "STATE_IDLE"; + case Ci.nsIApplicationUpdateService.STATE_DOWNLOADING: + return "STATE_DOWNLOADING"; + case Ci.nsIApplicationUpdateService.STATE_STAGING: + return "STATE_STAGING"; + case Ci.nsIApplicationUpdateService.STATE_PENDING: + return "STATE_PENDING"; + case Ci.nsIApplicationUpdateService.STATE_SWAP: + return "STATE_SWAP"; + } + return `[unknown update state: ${state}]`; + }, + + /** + * See nsIUpdateService.idl + */ + get currentState() { + return gUpdateState; + }, + + /** + * See nsIUpdateService.idl + */ + get stateTransition() { + return gStateTransitionPromise.promise; + }, + + classID: UPDATESERVICE_CID, + + QueryInterface: ChromeUtils.generateQI([ + "nsIApplicationUpdateService", + "nsITimerCallback", + "nsIObserver", + ]), +}; + +/** + * A service to manage active and past updates. + * @constructor + */ +function UpdateManager() { + // Load the active-update.xml file to see if there is an active update. + let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML); + if (activeUpdates.length) { + // Set the active update directly on the var used to cache the value. + this._readyUpdate = activeUpdates[0]; + if (activeUpdates.length >= 2) { + this._downloadingUpdate = activeUpdates[1]; + } + let status = readStatusFile(getReadyUpdateDir()); + LOG(`UpdateManager:UpdateManager - status = "${status}"`); + // This check is performed here since UpdateService:_postUpdateProcessing + // won't be called when there isn't an update.status file. + if (status == STATE_NONE) { + // Under some edgecases such as Windows system restore the + // active-update.xml will contain a pending update without the status + // file. To recover from this situation clean the updates dir and move + // the active update to the update history. + LOG( + "UpdateManager:UpdateManager - Found update data with no status " + + "file. Cleaning up..." + ); + this._readyUpdate.state = STATE_FAILED; + this._readyUpdate.errorCode = ERR_UPDATE_STATE_NONE; + this._readyUpdate.statusText = lazy.gUpdateBundle.GetStringFromName( + "statusFailed" + ); + let newStatus = STATE_FAILED + ": " + ERR_UPDATE_STATE_NONE; + pingStateAndStatusCodes(this._readyUpdate, true, newStatus); + this.addUpdateToHistory(this._readyUpdate); + this._readyUpdate = null; + this.saveUpdates(); + cleanUpReadyUpdateDir(); + cleanUpDownloadingUpdateDir(); + } else if (status == STATE_DOWNLOADING) { + // The first update we read out of activeUpdates may not be the ready + // update, it may be the downloading update. + if (this._downloadingUpdate) { + // If the first update we read is a downloading update, it's + // unexpected to have read another active update. That would seem to + // indicate that we were downloading two updates at once, which we don't + // do. + LOG( + "UpdateManager:UpdateManager - Warning: Found and discarded a " + + "second downloading update." + ); + } + this._downloadingUpdate = this._readyUpdate; + this._readyUpdate = null; + } + } + + LOG( + "UpdateManager:UpdateManager - Initialized downloadingUpdate to " + + this._downloadingUpdate + ); + if (this._downloadingUpdate) { + LOG( + "UpdateManager:UpdateManager - Initialized downloadingUpdate state to " + + this._downloadingUpdate.state + ); + } + LOG( + "UpdateManager:UpdateManager - Initialized readyUpdate to " + + this._readyUpdate + ); + if (this._readyUpdate) { + LOG( + "UpdateManager:UpdateManager - Initialized readyUpdate state to " + + this._readyUpdate.state + ); + } +} +UpdateManager.prototype = { + /** + * The nsIUpdate object for the update that has been downloaded. + */ + _readyUpdate: null, + + /** + * The nsIUpdate object for the update currently being downloaded. + */ + _downloadingUpdate: null, + + /** + * Whether the update history stored in _updates has changed since it was + * loaded. + */ + _updatesDirty: false, + + /** + * See nsIObserver.idl + */ + observe: function UM_observe(subject, topic, data) { + // Hack to be able to run and cleanup tests by reloading the update data. + if (topic == "um-reload-update-data") { + if (!Cu.isInAutomation) { + return; + } + LOG("UpdateManager:observe - Reloading update data."); + if (this._updatesXMLSaver) { + this._updatesXMLSaver.disarm(); + } + + let updates = []; + this._updatesDirty = true; + this._readyUpdate = null; + this._downloadingUpdate = null; + transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE); + if (data != "skip-files") { + let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML); + if (activeUpdates.length) { + this._readyUpdate = activeUpdates[0]; + if (activeUpdates.length >= 2) { + this._downloadingUpdate = activeUpdates[1]; + } + let status = readStatusFile(getReadyUpdateDir()); + LOG(`UpdateManager:observe - Got status = ${status}`); + if (status == STATE_DOWNLOADING) { + this._downloadingUpdate = this._readyUpdate; + this._readyUpdate = null; + transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING); + } else if ( + [ + STATE_PENDING, + STATE_PENDING_SERVICE, + STATE_PENDING_ELEVATE, + STATE_APPLIED, + STATE_APPLIED_SERVICE, + ].includes(status) + ) { + transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING); + } + } + updates = this._loadXMLFileIntoArray(FILE_UPDATES_XML); + } + this._updatesCache = updates; + + LOG( + "UpdateManager:observe - Reloaded downloadingUpdate as " + + this._downloadingUpdate + ); + if (this._downloadingUpdate) { + LOG( + "UpdateManager:observe - Reloaded downloadingUpdate state as " + + this._downloadingUpdate.state + ); + } + LOG( + "UpdateManager:observe - Reloaded readyUpdate as " + this._readyUpdate + ); + if (this._readyUpdate) { + LOG( + "UpdateManager:observe - Reloaded readyUpdate state as " + + this._readyUpdate.state + ); + } + } + }, + + /** + * Loads an updates.xml formatted file into an array of nsIUpdate items. + * @param fileName + * The file name in the updates directory to load. + * @return The array of nsIUpdate items held in the file. + */ + _loadXMLFileIntoArray: function UM__loadXMLFileIntoArray(fileName) { + let updates = []; + let file = getUpdateFile([fileName]); + if (!file.exists()) { + LOG( + "UpdateManager:_loadXMLFileIntoArray - XML file does not exist. " + + "path: " + + file.path + ); + return updates; + } + + // Open the active-update.xml file with both read and write access so + // opening it will fail if it isn't possible to also write to the file. When + // opening it fails it means that it isn't possible to update and the code + // below will return early without loading the active-update.xml. This will + // also make it so notifications to update manually will still be shown. + let mode = + fileName == FILE_ACTIVE_UPDATE_XML + ? FileUtils.MODE_RDWR + : FileUtils.MODE_RDONLY; + let fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + try { + fileStream.init(file, mode, FileUtils.PERMS_FILE, 0); + } catch (e) { + LOG( + "UpdateManager:_loadXMLFileIntoArray - error initializing file " + + "stream. Exception: " + + e + ); + return updates; + } + try { + var parser = new DOMParser(); + var doc = parser.parseFromStream( + fileStream, + "UTF-8", + fileStream.available(), + "text/xml" + ); + + var updateCount = doc.documentElement.childNodes.length; + for (var i = 0; i < updateCount; ++i) { + var updateElement = doc.documentElement.childNodes.item(i); + if ( + updateElement.nodeType != updateElement.ELEMENT_NODE || + updateElement.localName != "update" + ) { + continue; + } + + let update; + try { + update = new Update(updateElement); + } catch (e) { + LOG("UpdateManager:_loadXMLFileIntoArray - invalid update"); + continue; + } + updates.push(update); + } + } catch (ex) { + LOG( + "UpdateManager:_loadXMLFileIntoArray - error constructing update " + + "list. Exception: " + + ex + ); + } + fileStream.close(); + if (!updates.length) { + LOG( + "UpdateManager:_loadXMLFileIntoArray - update xml file " + + fileName + + " exists but doesn't contain any updates" + ); + // The file exists but doesn't contain any updates so remove it. + try { + file.remove(false); + } catch (e) { + LOG( + "UpdateManager:_loadXMLFileIntoArray - error removing " + + fileName + + " file. Exception: " + + e + ); + } + } + return updates; + }, + + /** + * Loads the update history from the updates.xml file into a cache. + */ + _getUpdates() { + if (!this._updatesCache) { + this._updatesCache = this._loadXMLFileIntoArray(FILE_UPDATES_XML); + } + return this._updatesCache; + }, + + /** + * See nsIUpdateService.idl + */ + getUpdateAt: function UM_getUpdateAt(aIndex) { + return this._getUpdates()[aIndex]; + }, + + /** + * See nsIUpdateService.idl + */ + getUpdateCount() { + return this._getUpdates().length; + }, + + /** + * See nsIUpdateService.idl + */ + get readyUpdate() { + return this._readyUpdate; + }, + set readyUpdate(aUpdate) { + this._readyUpdate = aUpdate; + }, + + /** + * See nsIUpdateService.idl + */ + get downloadingUpdate() { + return this._downloadingUpdate; + }, + set downloadingUpdate(aUpdate) { + this._downloadingUpdate = aUpdate; + }, + + /** + * See nsIUpdateService.idl + */ + addUpdateToHistory(aUpdate) { + this._updatesDirty = true; + let updates = this._getUpdates(); + updates.unshift(aUpdate); + // Limit the update history to 10 updates. + updates.splice(10); + }, + + /** + * Serializes an array of updates to an XML file or removes the file if the + * array length is 0. + * @param updates + * An array of nsIUpdate objects + * @param fileName + * The file name in the updates directory to write to. + * @return true on success, false on error + */ + _writeUpdatesToXMLFile: async function UM__writeUpdatesToXMLFile( + updates, + fileName + ) { + let file; + try { + file = getUpdateFile([fileName]); + } catch (e) { + LOG( + "UpdateManager:_writeUpdatesToXMLFile - Unable to get XML file - " + + "Exception: " + + e + ); + return false; + } + if (!updates.length) { + LOG( + "UpdateManager:_writeUpdatesToXMLFile - no updates to write. " + + "removing file: " + + file.path + ); + try { + await IOUtils.remove(file.path); + } catch (e) { + LOG( + "UpdateManager:_writeUpdatesToXMLFile - Delete file exception: " + e + ); + return false; + } + return true; + } + + const EMPTY_UPDATES_DOCUMENT_OPEN = + '<?xml version="1.0"?><updates xmlns="' + URI_UPDATE_NS + '">'; + const EMPTY_UPDATES_DOCUMENT_CLOSE = "</updates>"; + try { + var parser = new DOMParser(); + var doc = parser.parseFromString( + EMPTY_UPDATES_DOCUMENT_OPEN + EMPTY_UPDATES_DOCUMENT_CLOSE, + "text/xml" + ); + + for (var i = 0; i < updates.length; ++i) { + doc.documentElement.appendChild(updates[i].serialize(doc)); + } + + var xml = + EMPTY_UPDATES_DOCUMENT_OPEN + + doc.documentElement.innerHTML + + EMPTY_UPDATES_DOCUMENT_CLOSE; + // If the destination file existed and is removed while the following is + // being performed the copy of the tmp file to the destination file will + // fail. + await IOUtils.writeUTF8(file.path, xml, { + tmpPath: file.path + ".tmp", + }); + await IOUtils.setPermissions(file.path, FileUtils.PERMS_FILE); + } catch (e) { + LOG("UpdateManager:_writeUpdatesToXMLFile - Exception: " + e); + return false; + } + return true; + }, + + _updatesXMLSaver: null, + _updatesXMLSaverCallback: null, + /** + * See nsIUpdateService.idl + */ + saveUpdates: function UM_saveUpdates() { + if (!this._updatesXMLSaver) { + this._updatesXMLSaverCallback = () => this._updatesXMLSaver.finalize(); + + this._updatesXMLSaver = new lazy.DeferredTask( + () => this._saveUpdatesXML(), + XML_SAVER_INTERVAL_MS + ); + lazy.AsyncShutdown.profileBeforeChange.addBlocker( + "UpdateManager: writing update xml data", + this._updatesXMLSaverCallback + ); + } else { + this._updatesXMLSaver.disarm(); + } + + this._updatesXMLSaver.arm(); + }, + + /** + * Saves the active-updates.xml and updates.xml when the updates history has + * been modified files. + */ + _saveUpdatesXML: function UM__saveUpdatesXML() { + // This mechanism for how we store the updates might seem a bit odd, since, + // if only one update is stored, we don't know if it's the ready update or + // the downloading update. However, we can determine which it is by reading + // update.status. If we read STATE_DOWNLOADING, it must be a downloading + // update and otherwise it's a ready update. This method has the additional + // advantage of requiring no migration from when we used to only store a + // single active update. + let updates = []; + if (this._readyUpdate) { + updates.push(this._readyUpdate); + } + if (this._downloadingUpdate) { + updates.push(this._downloadingUpdate); + } + + // The active update stored in the active-update.xml file will change during + // the lifetime of an active update and the file should always be updated + // when saveUpdates is called. + let promises = []; + promises[0] = this._writeUpdatesToXMLFile(updates, FILE_ACTIVE_UPDATE_XML); + // The update history stored in the updates.xml file should only need to be + // updated when an active update has been added to it in which case + // |_updatesDirty| will be true. + if (this._updatesDirty) { + this._updatesDirty = false; + promises[1] = this._writeUpdatesToXMLFile( + this._getUpdates(), + FILE_UPDATES_XML + ); + } + return Promise.all(promises); + }, + + /** + * See nsIUpdateService.idl + */ + refreshUpdateStatus: async function UM_refreshUpdateStatus() { + try { + LOG("UpdateManager:refreshUpdateStatus - Staging done."); + + var update = this._readyUpdate; + if (!update) { + LOG("UpdateManager:refreshUpdateStatus - Missing ready update?"); + return; + } + + var status = readStatusFile(getReadyUpdateDir()); + pingStateAndStatusCodes(update, false, status); + LOG(`UpdateManager:refreshUpdateStatus - status = ${status}`); + + let parts = status.split(":"); + update.state = parts[0]; + if (update.state == STATE_APPLYING) { + LOG( + "UpdateManager:refreshUpdateStatus - Staging appears to have crashed." + ); + update.state = STATE_FAILED; + update.errorCode = ERR_UPDATER_CRASHED; + } else if (update.state == STATE_FAILED) { + LOG("UpdateManager:refreshUpdateStatus - Staging failed."); + if (parts[1]) { + update.errorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE; + } else { + update.errorCode = INVALID_UPDATER_STATUS_CODE; + } + } + + // Rotate the update logs so the update log isn't removed if a complete + // update is downloaded. By passing false the patch directory won't be + // removed. + cleanUpReadyUpdateDir(false); + + if (update.state == STATE_FAILED) { + if ( + update.errorCode == DELETE_ERROR_STAGING_LOCK_FILE || + update.errorCode == UNEXPECTED_STAGING_ERROR + ) { + update.state = getBestPendingState(); + writeStatusFile(getReadyUpdateDir(), update.state); + LOG( + `UpdateManager:refreshUpdateStatus - Unexpected staging error. ` + + `Setting status to "${update.state}"` + ); + } else if (isServiceSpecificErrorCode(update.errorCode)) { + // Sometimes when staging, we might encounter an error that is + // specific to the Maintenance Service. If this happens, we should try + // to update without the Service. + LOG( + `UpdateManager:refreshUpdateStatus - Encountered service ` + + `specific error code: ${update.errorCode}. Will try installing ` + + `update without the Maintenance Service. Setting state to pending` + ); + update.state = STATE_PENDING; + writeStatusFile(getReadyUpdateDir(), update.state); + } else { + LOG( + "UpdateManager:refreshUpdateStatus - Attempting handleUpdateFailure" + ); + if (!handleUpdateFailure(update)) { + LOG( + "UpdateManager:refreshUpdateStatus - handleUpdateFailure " + + "failed. Attempting to fall back to complete update." + ); + await handleFallbackToCompleteUpdate(); + } + } + } + if (update.state == STATE_APPLIED && shouldUseService()) { + LOG( + `UpdateManager:refreshUpdateStatus - Staging successful. ` + + `Setting status to "${STATE_APPLIED_SERVICE}"` + ); + writeStatusFile( + getReadyUpdateDir(), + (update.state = STATE_APPLIED_SERVICE) + ); + } + + // Now that the active update's properties have been updated write the + // active-update.xml to disk. Since there have been no changes to the + // update history the updates.xml will not be written to disk. + this.saveUpdates(); + + // Send an observer notification which the app update doorhanger uses to + // display a restart notification after any langpacks have staged. + await promiseLangPacksUpdated(update); + + if ( + update.state == STATE_APPLIED || + update.state == STATE_APPLIED_SERVICE || + update.state == STATE_PENDING || + update.state == STATE_PENDING_SERVICE || + update.state == STATE_PENDING_ELEVATE + ) { + LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_PENDING"); + transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING); + } + + LOG( + "UpdateManager:refreshUpdateStatus - Notifying observers that " + + "the update was staged. topic: update-staged, status: " + + update.state + ); + Services.obs.notifyObservers(update, "update-staged", update.state); + } finally { + // This function being called is the one thing that tells us that staging + // is done so be very sure that we don't exit it leaving the current + // state at STATE_STAGING. + // The only cases where we haven't already done a state transition are + // error cases, so if another state isn't set, assume that we hit an error + // and aborted the update. + if ( + lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING + ) { + LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_IDLE"); + transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE); + } + } + }, + + /** + * See nsIUpdateService.idl + */ + elevationOptedIn: function UM_elevationOptedIn() { + // The user has been been made aware that the update requires elevation. + let update = this._readyUpdate; + if (!update) { + return; + } + let status = readStatusFile(getReadyUpdateDir()); + let parts = status.split(":"); + update.state = parts[0]; + if (update.state == STATE_PENDING_ELEVATE) { + LOG("UpdateManager:elevationOptedIn - Setting state to pending."); + // Proceed with the pending update. + // Note: STATE_PENDING_ELEVATE stands for "pending user's approval to + // proceed with an elevated update". As long as we see this state, we will + // notify the user of the availability of an update that requires + // elevation. |elevationOptedIn| (this function) is called when the user + // gives us approval to proceed, so we want to switch to STATE_PENDING. + // The updater then detects whether or not elevation is required and + // displays the elevation prompt if necessary. This last step does not + // depend on the state in the status file. + writeStatusFile(getReadyUpdateDir(), STATE_PENDING); + } else { + LOG("UpdateManager:elevationOptedIn - Not in pending-elevate state."); + } + }, + + /** + * See nsIUpdateService.idl + */ + cleanupDownloadingUpdate: function UM_cleanupDownloadingUpdate() { + LOG( + "UpdateManager:cleanupDownloadingUpdate - cleaning up downloading update." + ); + cleanupDownloadingUpdate(); + }, + + /** + * See nsIUpdateService.idl + */ + cleanupReadyUpdate: function UM_cleanupReadyUpdate() { + LOG("UpdateManager:cleanupReadyUpdate - cleaning up ready update."); + cleanupReadyUpdate(); + }, + + classID: Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}"), + QueryInterface: ChromeUtils.generateQI(["nsIUpdateManager", "nsIObserver"]), +}; + +/** + * CheckerService + * Provides an interface for checking for new updates. When more checks are + * made while an equivalent check is already in-progress, they will be coalesced + * into a single update check request. + */ +class CheckerService { + #nextUpdateCheckId = 1; + + // Most of the update checking data is looked up via a "request key". This + // allows us to lookup the request key for a particular check id, since + // multiple checks can correspond to a single request. + // When a check is cancelled or completed, it will be removed from this + // object. + #requestKeyByCheckId = {}; + + // This object will relate request keys to update check data objects. The + // format of the update check data objects is defined by + // #makeUpdateCheckDataObject, below. + // When an update request is cancelled (by all of the corresponding update + // checks being cancelled) or completed, its key will be removed from this + // object. + #updateCheckData = {}; + + #makeUpdateCheckDataObject(type, promise) { + return { type, promise, request: null }; + } + + /** + * Indicates whether the passed parameter is one of the valid enumerated + * values that indicates a type of update check. + */ + #validUpdateCheckType(checkType) { + return [ + Ci.nsIUpdateChecker.BACKGROUND_CHECK, + Ci.nsIUpdateChecker.FOREGROUND_CHECK, + ].includes(checkType); + } + + #getCanMigrate() { + if (AppConstants.platform != "win") { + return false; + } + + // The first element of the array is whether the build target is 32 or 64 + // bit and the third element of the array is whether the client's Windows OS + // system processor is 32 or 64 bit. + let aryABI = lazy.UpdateUtils.ABI.split("-"); + if (aryABI[0] != "x86" || aryABI[2] != "x64") { + return false; + } + + let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + + let regPath = + "SOFTWARE\\Mozilla\\" + Services.appinfo.name + "\\32to64DidMigrate"; + let regValHKCU = lazy.WindowsRegistry.readRegKey( + wrk.ROOT_KEY_CURRENT_USER, + regPath, + "Never", + wrk.WOW64_32 + ); + let regValHKLM = lazy.WindowsRegistry.readRegKey( + wrk.ROOT_KEY_LOCAL_MACHINE, + regPath, + "Never", + wrk.WOW64_32 + ); + // The Never registry key value allows configuring a system to never migrate + // any of the installations. + if (regValHKCU === 1 || regValHKLM === 1) { + LOG( + "CheckerService:#getCanMigrate - all installations should not be " + + "migrated" + ); + return false; + } + + let appBaseDirPath = getAppBaseDir().path; + regValHKCU = lazy.WindowsRegistry.readRegKey( + wrk.ROOT_KEY_CURRENT_USER, + regPath, + appBaseDirPath, + wrk.WOW64_32 + ); + regValHKLM = lazy.WindowsRegistry.readRegKey( + wrk.ROOT_KEY_LOCAL_MACHINE, + regPath, + appBaseDirPath, + wrk.WOW64_32 + ); + // When the registry value is 1 for the installation directory path value + // name then the installation has already been migrated once or the system + // was configured to not migrate that installation. + if (regValHKCU === 1 || regValHKLM === 1) { + LOG( + "CheckerService:#getCanMigrate - this installation should not be " + + "migrated" + ); + return false; + } + + // When the registry value is 0 for the installation directory path value + // name then the installation has updated to Firefox 56 and can be migrated. + if (regValHKCU === 0 || regValHKLM === 0) { + LOG("CheckerService:#getCanMigrate - this installation can be migrated"); + return true; + } + + LOG( + "CheckerService:#getCanMigrate - no registry entries for this " + + "installation" + ); + return false; + } + + /** + * See nsIUpdateService.idl + */ + async getUpdateURL(checkType) { + LOG("CheckerService:getUpdateURL - checkType: " + checkType); + if (!this.#validUpdateCheckType(checkType)) { + LOG("CheckerService:getUpdateURL - Invalid checkType"); + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + let url = Services.appinfo.updateURL; + let updatePin; + + if (Services.policies) { + let policies = Services.policies.getActivePolicies(); + if (policies) { + if ("AppUpdateURL" in policies) { + url = policies.AppUpdateURL.toString(); + } + if ("AppUpdatePin" in policies) { + updatePin = policies.AppUpdatePin; + + // Scalar ID: update.version_pin + AUSTLMY.pingPinPolicy(updatePin); + } + } + } + + if (!url) { + LOG("CheckerService:getUpdateURL - update URL not defined"); + return null; + } + + url = await lazy.UpdateUtils.formatUpdateURL(url); + + if (checkType == Ci.nsIUpdateChecker.FOREGROUND_CHECK) { + url += (url.includes("?") ? "&" : "?") + "force=1"; + } + + if (this.#getCanMigrate()) { + url += (url.includes("?") ? "&" : "?") + "mig64=1"; + } + + if (updatePin) { + url += + (url.includes("?") ? "&" : "?") + + "pin=" + + encodeURIComponent(updatePin); + } + + LOG("CheckerService:getUpdateURL - update URL: " + url); + return url; + } + + /** + * See nsIUpdateService.idl + */ + + checkForUpdates(checkType) { + LOG("CheckerService:checkForUpdates - checkType: " + checkType); + if (!this.#validUpdateCheckType(checkType)) { + LOG("CheckerService:checkForUpdates - Invalid checkType"); + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + let checkId = this.#nextUpdateCheckId; + this.#nextUpdateCheckId += 1; + + // `checkType == FOREGROUND_CHECK`` can override `canCheckForUpdates`. But + // nothing should override enterprise policies. + if (lazy.AUS.disabled) { + LOG("CheckerService:checkForUpdates - disabled by policy"); + return this.#getChecksNotAllowedObject(checkId); + } + if ( + checkType == Ci.nsIUpdateChecker.BACKGROUND_CHECK && + !lazy.AUS.canCheckForUpdates + ) { + LOG("CheckerService:checkForUpdates - !canCheckForUpdates"); + return this.#getChecksNotAllowedObject(checkId); + } + + // We want to combine simultaneous requests, but only ones that are + // equivalent. If, say, one of them uses the force parameter and one + // doesn't, we want those two requests to remain separate. This key will + // allow us to map equivalent requests together. It is also the key that we + // use to lookup the update check data in this.#updateCheckData. + let requestKey = checkType; + + if (requestKey in this.#updateCheckData) { + LOG( + `CheckerService:checkForUpdates - Connecting check id ${checkId} to ` + + `existing check request.` + ); + } else { + LOG( + `CheckerService:checkForUpdates - Making new check request for check ` + + `id ${checkId}.` + ); + this.#updateCheckData[requestKey] = this.#makeUpdateCheckDataObject( + checkType, + this.#updateCheck(checkType, requestKey) + ); + } + + this.#requestKeyByCheckId[checkId] = requestKey; + + return { + id: checkId, + result: this.#updateCheckData[requestKey].promise, + QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheck"]), + }; + } + + #getChecksNotAllowedObject(checkId) { + return { + id: checkId, + result: Promise.resolve( + Object.freeze({ + checksAllowed: false, + succeeded: false, + request: null, + updates: [], + QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]), + }) + ), + QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheck"]), + }; + } + + async #updateCheck(checkType, requestKey) { + await waitForOtherInstances(); + + let url; + try { + url = await this.getUpdateURL(checkType); + } catch (ex) {} + + if (!url) { + LOG("CheckerService:#updateCheck - !url"); + return this.#getCheckFailedObject("update_url_not_available"); + } + + let request = new XMLHttpRequest(); + request.open("GET", url, true); + request.channel.notificationCallbacks = new lazy.CertUtils.BadCertHandler( + false + ); + // Prevent the request from reading from the cache. + request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + // Prevent the request from writing to the cache. + request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + // Disable cutting edge features, like TLS 1.3, where middleboxes might + // brick us + request.channel.QueryInterface( + Ci.nsIHttpChannelInternal + ).beConservative = true; + + request.overrideMimeType("text/xml"); + // The Cache-Control header is only interpreted by proxies and the + // final destination. It does not help if a resource is already + // cached locally. + request.setRequestHeader("Cache-Control", "no-cache"); + // HTTP/1.0 servers might not implement Cache-Control and + // might only implement Pragma: no-cache + request.setRequestHeader("Pragma", "no-cache"); + + const UPDATE_CHECK_LOAD_SUCCESS = 1; + const UPDATE_CHECK_LOAD_ERROR = 2; + const UPDATE_CHECK_CANCELLED = 3; + + let result = await new Promise(resolve => { + // It's important that nothing potentially asynchronous happens between + // checking if the request has been cancelled and starting the request. + // If an update check cancellation happens before dispatching the request + // and we end up dispatching it anyways, we will never call cancel on the + // request later and the cancellation effectively won't happen. + if (!(requestKey in this.#updateCheckData)) { + LOG( + "CheckerService:#updateCheck - check was cancelled before request " + + "was able to start" + ); + resolve(UPDATE_CHECK_CANCELLED); + return; + } + + let onLoad = event => { + request.removeEventListener("load", onLoad); + LOG("CheckerService:#updateCheck - request got 'load' event"); + resolve(UPDATE_CHECK_LOAD_SUCCESS); + }; + request.addEventListener("load", onLoad); + let onError = event => { + request.removeEventListener("error", onLoad); + LOG("CheckerService:#updateCheck - request got 'error' event"); + resolve(UPDATE_CHECK_LOAD_ERROR); + }; + request.addEventListener("error", onError); + + LOG("CheckerService:#updateCheck - sending request to: " + url); + request.send(null); + this.#updateCheckData[requestKey].request = request; + }); + + // Remove all entries for this request key. This marks the request and the + // associated check ids as no longer in-progress. + delete this.#updateCheckData[requestKey]; + for (const checkId of Object.keys(this.#requestKeyByCheckId)) { + if (this.#requestKeyByCheckId[checkId] == requestKey) { + delete this.#requestKeyByCheckId[checkId]; + } + } + + if (result == UPDATE_CHECK_CANCELLED) { + return this.#getCheckFailedObject(Cr.NS_BINDING_ABORTED); + } + + if (result == UPDATE_CHECK_LOAD_ERROR) { + let status = this.#getChannelStatus(request); + LOG("CheckerService:#updateCheck - Failed. request.status: " + status); + + // Set MitM pref. + try { + let secInfo = request.channel.securityInfo; + if (secInfo.serverCert && secInfo.serverCert.issuerName) { + Services.prefs.setStringPref( + "security.pki.mitm_canary_issuer", + secInfo.serverCert.issuerName + ); + } + } catch (e) { + LOG("CheckerService:#updateCheck - Getting secInfo failed."); + } + + return this.#getCheckFailedObject(status, 404, request); + } + + LOG("CheckerService:#updateCheck - request completed downloading document"); + Services.prefs.clearUserPref("security.pki.mitm_canary_issuer"); + // Check whether there is a mitm, i.e. check whether the root cert is + // built-in or not. + try { + let sslStatus = request.channel.securityInfo; + if (sslStatus && sslStatus.succeededCertChain) { + let rootCert = null; + // The root cert is the last cert in the chain. + if (sslStatus.succeededCertChain.length) { + rootCert = + sslStatus.succeededCertChain[ + sslStatus.succeededCertChain.length - 1 + ]; + } + if (rootCert) { + Services.prefs.setBoolPref( + "security.pki.mitm_detected", + !rootCert.isBuiltInRoot + ); + } + } + } catch (e) { + LOG("CheckerService:#updateCheck - Getting sslStatus failed."); + } + + let updates; + try { + // Analyze the resulting DOM and determine the set of updates. + updates = this.#parseUpdates(request); + } catch (e) { + LOG( + "CheckerService:#updateCheck - there was a problem checking for " + + "updates. Exception: " + + e + ); + let status = this.#getChannelStatus(request); + // If we can't find an error string specific to this status code, + // just use the 200 message from above, which means everything + // "looks" fine but there was probably an XML error or a bogus file. + return this.#getCheckFailedObject(status, 200, request); + } + + LOG( + "CheckerService:#updateCheck - number of updates available: " + + updates.length + ); + + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS); + } + + return Object.freeze({ + checksAllowed: true, + succeeded: true, + request, + updates, + QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]), + }); + } + + /** + * @param errorCode + * The error code to include in the return value. If possible, we + * will get the update status text based on this error code. + * @param defaultCode + * Optional. The error code to use to get the status text if there + * isn't status text available for `errorCode`. + * @param request + * The XMLHttpRequest used to check for updates. Or null, if one was + * never constructed. + * @returns An nsIUpdateCheckResult object indicating an error, using the + * error data passed to this function. + */ + #getCheckFailedObject( + errorCode, + defaultCode = Cr.NS_BINDING_FAILED, + request = null + ) { + let update = new Update(null); + update.errorCode = errorCode; + update.statusText = getStatusTextFromCode(errorCode, defaultCode); + + if (errorCode == Cr.NS_ERROR_OFFLINE) { + // We use a separate constant here because nsIUpdate.errorCode is signed + update.errorCode = NETWORK_ERROR_OFFLINE; + } else if (this.#isHttpStatusCode(errorCode)) { + update.errorCode = HTTP_ERROR_OFFSET + errorCode; + } + + return Object.freeze({ + checksAllowed: true, + succeeded: false, + request, + updates: [update], + QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]), + }); + } + + /** + * Returns the status code for the XMLHttpRequest + */ + #getChannelStatus(request) { + var status = 0; + try { + status = request.status; + } catch (e) {} + + if (status == 0) { + status = request.channel.QueryInterface(Ci.nsIRequest).status; + } + return status; + } + + #isHttpStatusCode(status) { + return status >= 100 && status <= 599; + } + + /** + * @param request + * The XMLHttpRequest that successfully loaded the update XML. + * @returns An array of 0 or more nsIUpdate objects describing the available + * updates. + * @throws If the XML document element node name is not updates. + */ + #parseUpdates(request) { + let updatesElement = request.responseXML.documentElement; + if (!updatesElement) { + LOG("CheckerService:#parseUpdates - empty updates document?!"); + return []; + } + + if (updatesElement.nodeName != "updates") { + LOG("CheckerService:#parseUpdates - unexpected node name!"); + throw new Error( + "Unexpected node name, expected: updates, got: " + + updatesElement.nodeName + ); + } + + let updates = []; + for (const updateElement of updatesElement.childNodes) { + if ( + updateElement.nodeType != updateElement.ELEMENT_NODE || + updateElement.localName != "update" + ) { + continue; + } + + let update; + try { + update = new Update(updateElement); + } catch (e) { + LOG("CheckerService:#parseUpdates - invalid <update/>, ignoring..."); + continue; + } + update.serviceURL = request.responseURL; + update.channel = lazy.UpdateUtils.UpdateChannel; + updates.push(update); + } + + return updates; + } + + /** + * See nsIUpdateService.idl + */ + stopCheck(checkId) { + if (!(checkId in this.#requestKeyByCheckId)) { + LOG(`CheckerService:stopCheck - Non-existent check id ${checkId}`); + return; + } + LOG(`CheckerService:stopCheck - Cancelling check id ${checkId}`); + let requestKey = this.#requestKeyByCheckId[checkId]; + delete this.#requestKeyByCheckId[checkId]; + if (Object.values(this.#requestKeyByCheckId).includes(requestKey)) { + LOG( + `CheckerService:stopCheck - Not actually cancelling request because ` + + `other check id's depend on it.` + ); + } else { + LOG( + `CheckerService:stopCheck - This is the last check using this ` + + `request. Cancelling the request now.` + ); + let request = this.#updateCheckData[requestKey].request; + delete this.#updateCheckData[requestKey]; + if (request) { + LOG(`CheckerService:stopCheck - Aborting XMLHttpRequest`); + request.abort(); + } else { + LOG( + `CheckerService:stopCheck - Not aborting XMLHttpRequest. It ` + + `doesn't appear to have started yet.` + ); + } + } + } + + /** + * See nsIUpdateService.idl + */ + stopAllChecks() { + LOG("CheckerService:stopAllChecks - stopping all checks."); + for (const checkId of Object.keys(this.#requestKeyByCheckId)) { + this.stopCheck(checkId); + } + } + + classID = Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}"); + QueryInterface = ChromeUtils.generateQI(["nsIUpdateChecker"]); +} + +/** + * Manages the download of updates + * @param background + * Whether or not this downloader is operating in background + * update mode. + * @param updateService + * The update service that created this downloader. + * @constructor + */ +function Downloader(updateService) { + LOG("Creating Downloader"); + this.updateService = updateService; +} +Downloader.prototype = { + /** + * The nsIUpdatePatch that we are downloading + */ + _patch: null, + + /** + * The nsIUpdate that we are downloading + */ + _update: null, + + /** + * The nsIRequest object handling the download. + */ + _request: null, + + /** + * Whether or not the update being downloaded is a complete replacement of + * the user's existing installation or a patch representing the difference + * between the new version and the previous version. + */ + isCompleteUpdate: null, + + /** + * We get the nsIRequest from nsIBITS asynchronously. When downloadUpdate has + * been called, but this._request is not yet valid, _pendingRequest will be + * a promise that will resolve when this._request has been set. + */ + _pendingRequest: null, + + /** + * When using BITS, cancel actions happen asynchronously. This variable + * keeps track of any cancel action that is in-progress. + * If the cancel action fails, this will be set back to null so that the + * action can be attempted again. But if the cancel action succeeds, the + * resolved promise will remain stored in this variable to prevent cancel + * from being called twice (which, for BITS, is an error). + */ + _cancelPromise: null, + + /** + * BITS receives progress notifications slowly, unless a user is watching. + * This tracks what frequency notifications are happening at. + * + * This is needed because BITS downloads are started asynchronously. + * Specifically, this is needed to prevent a situation where the download is + * still starting (Downloader._pendingRequest has not resolved) when the first + * observer registers itself. Without this variable, there is no way of + * knowing whether the download was started as Active or Idle and, therefore, + * we don't know if we need to start Active mode when _pendingRequest + * resolves. + */ + _bitsActiveNotifications: false, + + /** + * This is a function that when called will stop the update process from + * waiting for language pack updates. This is for safety to ensure that a + * problem in the add-ons manager doesn't delay updates by much. + */ + _langPackTimeout: null, + + /** + * If gOnlyDownloadUpdatesThisSession is true, we prevent the update process + * from progressing past the downloading stage. If the download finishes, + * pretend that it hasn't in order to keep the current update in the + * "downloading" state. + */ + _pretendingDownloadIsNotDone: false, + + /** + * Cancels the active download. + * + * For a BITS download, this will cancel and remove the download job. For + * an nsIIncrementalDownload, this will stop the download, but leaves the + * data around to allow the transfer to be resumed later. + */ + cancel: async function Downloader_cancel(cancelError) { + LOG("Downloader: cancel"); + if (cancelError === undefined) { + cancelError = Cr.NS_BINDING_ABORTED; + } + if (this.usingBits) { + // If a cancel action is already in progress, just return when that + // promise resolved. Trying to cancel the same request twice is an error. + if (this._cancelPromise) { + await this._cancelPromise; + return; + } + + if (this._pendingRequest) { + await this._pendingRequest; + } + if (this._patch.getProperty("bitsId") != null) { + // Make sure that we don't try to resume this download after it was + // cancelled. + this._patch.deleteProperty("bitsId"); + } + try { + this._cancelPromise = this._request.cancelAsync(cancelError); + await this._cancelPromise; + } catch (e) { + // On success, we will not set the cancel promise to null because + // we want to prevent two cancellations of the same request. But + // retrying after a failed cancel is not an error, so we will set the + // cancel promise to null in the failure case. + this._cancelPromise = null; + throw e; + } + } else if (this._request && this._request instanceof Ci.nsIRequest) { + // Normally, cancelling an nsIIncrementalDownload results in it stopping + // the download but leaving the downloaded data so that we can resume the + // download later. If we've already finished the download, there is no + // transfer to stop. + // Note that this differs from the BITS case. Cancelling a BITS job, even + // when the transfer has completed, results in all data being deleted. + // Therefore, even if the transfer has completed, cancelling a BITS job + // has effects that we must not skip. + if (this._pretendingDownloadIsNotDone) { + LOG( + "Downloader: cancel - Ignoring cancel request of finished download" + ); + } else { + this._request.cancel(cancelError); + } + } + }, + + /** + * Verify the downloaded file. We assume that the download is complete at + * this point. + */ + _verifyDownload: function Downloader__verifyDownload() { + LOG("Downloader:_verifyDownload called"); + if (!this._request) { + AUSTLMY.pingDownloadCode( + this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_VERIFY_NO_REQUEST + ); + return false; + } + + let destination = getDownloadingUpdateDir(); + destination.append(FILE_UPDATE_MAR); + + // Ensure that the file size matches the expected file size. + if (destination.fileSize != this._patch.size) { + LOG("Downloader:_verifyDownload downloaded size != expected size."); + AUSTLMY.pingDownloadCode( + this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL + ); + return false; + } + + LOG("Downloader:_verifyDownload downloaded size == expected size."); + return true; + }, + + /** + * Select the patch to use given the current state of updateDir and the given + * set of update patches. + * @param update + * A nsIUpdate object to select a patch from + * @param updateDir + * A nsIFile representing the update directory + * @return A nsIUpdatePatch object to download + */ + _selectPatch: function Downloader__selectPatch(update, updateDir) { + // Given an update to download, we will always try to download the patch + // for a partial update over the patch for a full update. + + // Look to see if any of the patches in the Update object has been + // pre-selected for download, otherwise we must figure out which one + // to select ourselves. + var selectedPatch = update.selectedPatch; + + var state = selectedPatch ? selectedPatch.state : STATE_NONE; + + // If this is a patch that we know about, then select it. If it is a patch + // that we do not know about, then remove it and use our default logic. + var useComplete = false; + if (selectedPatch) { + LOG( + "Downloader:_selectPatch - found existing patch with state: " + state + ); + if (state == STATE_DOWNLOADING) { + LOG("Downloader:_selectPatch - resuming download"); + return selectedPatch; + } + if ( + state == STATE_PENDING || + state == STATE_PENDING_SERVICE || + state == STATE_PENDING_ELEVATE || + state == STATE_APPLIED || + state == STATE_APPLIED_SERVICE + ) { + LOG("Downloader:_selectPatch - already downloaded"); + return null; + } + + // When downloading the patch failed using BITS, there hasn't been an + // attempt to download the patch using the internal application download + // mechanism, and an attempt to stage or apply the patch hasn't failed + // which indicates that a different patch should be downloaded since + // re-downloading the same patch with the internal application download + // mechanism will likely also fail when trying to stage or apply it then + // try to download the same patch using the internal application download + // mechanism. + selectedPatch.QueryInterface(Ci.nsIWritablePropertyBag); + if ( + selectedPatch.getProperty("bitsResult") != null && + selectedPatch.getProperty("internalResult") == null && + !selectedPatch.errorCode + ) { + LOG( + "Downloader:_selectPatch - Falling back to non-BITS download " + + "mechanism for the same patch due to existing BITS result: " + + selectedPatch.getProperty("bitsResult") + ); + return selectedPatch; + } + + if (update && selectedPatch.type == "complete") { + // This is a pretty fatal error. Just bail. + LOG("Downloader:_selectPatch - failed to apply complete patch!"); + cleanupDownloadingUpdate(); + return null; + } + + // Something went wrong when we tried to apply the previous patch. + // Try the complete patch next time. + useComplete = true; + selectedPatch = null; + } + + // If we were not able to discover an update from a previous download, we + // select the best patch from the given set. + var partialPatch = getPatchOfType(update, "partial"); + if (!useComplete) { + selectedPatch = partialPatch; + } + if (!selectedPatch) { + if (lazy.UM.readyUpdate) { + // If we already have a ready update, we download partials only. + LOG( + "Downloader:_selectPatch - not selecting a complete patch because " + + "this is not the first download of the session" + ); + return null; + } + + if (partialPatch) { + partialPatch.selected = false; + } + selectedPatch = getPatchOfType(update, "complete"); + } + + // if update only contains a partial patch, selectedPatch == null here if + // the partial patch has been attempted and fails and we're trying to get a + // complete patch + if (selectedPatch) { + selectedPatch.selected = true; + update.isCompleteUpdate = selectedPatch.type == "complete"; + } + + LOG( + "Downloader:_selectPatch - Patch selected. Assigning update to " + + "downloadingUpdate." + ); + lazy.UM.downloadingUpdate = update; + + return selectedPatch; + }, + + /** + * Whether or not the user wants to be notified that an update is being + * downloaded. + */ + get _notifyDuringDownload() { + return Services.prefs.getBoolPref( + PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD, + false + ); + }, + + _notifyDownloadStatusObservers: function Downloader_notifyDownloadStatusObservers() { + if (this._notifyDuringDownload) { + let status = this.updateService.isDownloading ? "downloading" : "idle"; + Services.obs.notifyObservers(this._update, "update-downloading", status); + } + }, + + /** + * Whether or not we are currently downloading something. + */ + get isBusy() { + return this._request != null || this._pendingRequest != null; + }, + + get usingBits() { + return this._pendingRequest != null || this._request instanceof BitsRequest; + }, + + /** + * Returns true if the specified patch can be downloaded with BITS. + */ + _canUseBits: function Downloader__canUseBits(patch) { + if (getCanUseBits() != "CanUseBits") { + // This will have printed its own logging. No need to print more. + return false; + } + // Regardless of success or failure, don't download the same patch with BITS + // twice. + if (patch.getProperty("bitsResult") != null) { + LOG( + "Downloader:_canUseBits - Not using BITS because it was already tried" + ); + return false; + } + LOG("Downloader:_canUseBits - Patch is able to use BITS download"); + return true; + }, + + /** + * Instruct the add-ons manager to start downloading language pack updates in + * preparation for the current update. + */ + _startLangPackUpdates: function Downloader__startLangPackUpdates() { + if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_LANGPACK_ENABLED, false)) { + return; + } + + // A promise that we can resolve at some point to time out the language pack + // update process. + let timeoutPromise = new Promise(resolve => { + this._langPackTimeout = resolve; + }); + + let update = unwrap(this._update); + + let existing = LangPackUpdates.get(update); + if (existing) { + // We have already started staging lang packs for this update, no need to + // do it again. + return; + } + + // Note that we don't care about success or failure here, either way we will + // continue with the update process. + let langPackPromise = lazy.AddonManager.stageLangpacksForAppUpdate( + update.appVersion, + update.appVersion + ) + .catch(error => { + LOG( + `Add-ons manager threw exception while updating language packs: ${error}` + ); + }) + .finally(() => { + this._langPackTimeout = null; + + if (TelemetryStopwatch.running("UPDATE_LANGPACK_OVERTIME", update)) { + TelemetryStopwatch.finish("UPDATE_LANGPACK_OVERTIME", update); + } + }); + + LangPackUpdates.set( + update, + Promise.race([langPackPromise, timeoutPromise]) + ); + }, + + /** + * Download and stage the given update. + * @param update + * A nsIUpdate object to download a patch for. Cannot be null. + */ + downloadUpdate: async function Downloader_downloadUpdate(update) { + LOG("UpdateService:downloadUpdate"); + if (!update) { + AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE); + throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER); + } + + var updateDir = getDownloadingUpdateDir(); + + this._update = update; + + // This function may return null, which indicates that there are no patches + // to download. + this._patch = this._selectPatch(update, updateDir); + if (!this._patch) { + LOG("Downloader:downloadUpdate - no patch to download"); + AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE_PATCH); + return false; + } + // The update and the patch implement nsIWritablePropertyBag. Expose that + // interface immediately after a patch is assigned so that + // this.(_patch|_update).(get|set)Property can always safely be called. + this._update.QueryInterface(Ci.nsIWritablePropertyBag); + this._patch.QueryInterface(Ci.nsIWritablePropertyBag); + + if ( + this._update.getProperty("disableBackgroundUpdates") != null && + lazy.gIsBackgroundTaskMode + ) { + LOG( + "Downloader:downloadUpdate - Background update disabled by update " + + "advertisement" + ); + return false; + } + + this.isCompleteUpdate = this._patch.type == "complete"; + + let canUseBits = false; + // Allow the advertised update to disable BITS. + if (this._update.getProperty("disableBITS") != null) { + LOG( + "Downloader:downloadUpdate - BITS downloads disabled by update " + + "advertisement" + ); + } else { + canUseBits = this._canUseBits(this._patch); + } + + if (!canUseBits) { + this._pendingRequest = null; + + let patchFile = updateDir.clone(); + patchFile.append(FILE_UPDATE_MAR); + + if (lazy.gIsBackgroundTaskMode) { + // We don't normally run a background update if we can't use BITS, but + // this branch is possible because we do fall back from BITS failures by + // attempting an internal download. + // If this happens, we are just going to need to wait for interactive + // Firefox to download the update. We don't, however, want to be in the + // "downloading" state when interactive Firefox runs because we want to + // download the newest update available which, at that point, may not be + // the one that we are currently trying to download. + // However, we can't just unconditionally clobber the current update + // because interactive Firefox might already be part way through an + // internal update download, and we definitely don't want to interrupt + // that. + let readyUpdateDir = getReadyUpdateDir(); + let status = readStatusFile(readyUpdateDir); + // nsIIncrementalDownload doesn't use an intermediate download location + // for partially downloaded files. If we have started an update + // download with it, it will be available at its ultimate location. + if (!(status == STATE_DOWNLOADING && patchFile.exists())) { + LOG( + "Downloader:downloadUpdate - Can't download with internal " + + "downloader from a background task. Cleaning up downloading " + + "update." + ); + cleanupDownloadingUpdate(); + } + return false; + } + + // The interval is 0 since there is no need to throttle downloads. + let interval = 0; + + LOG( + "Downloader:downloadUpdate - Starting nsIIncrementalDownload with " + + "url: " + + this._patch.URL + + ", path: " + + patchFile.path + + ", interval: " + + interval + ); + let uri = Services.io.newURI(this._patch.URL); + + this._request = Cc[ + "@mozilla.org/network/incremental-download;1" + ].createInstance(Ci.nsIIncrementalDownload); + this._request.init(uri, patchFile, DOWNLOAD_CHUNK_SIZE, interval); + this._request.start(this, null); + } else { + let noProgressTimeout = BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS; + let monitorInterval = BITS_IDLE_POLL_RATE_MS; + this._bitsActiveNotifications = false; + // The monitor's timeout should be much greater than the longest monitor + // poll interval. If the timeout is too short, delay in the pipe to the + // update agent might cause BITS to falsely report an error, causing an + // unnecessary fallback to nsIIncrementalDownload. + let monitorTimeout = Math.max(10 * monitorInterval, 10 * 60 * 1000); + if (this.hasDownloadListeners) { + noProgressTimeout = BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS; + monitorInterval = BITS_ACTIVE_POLL_RATE_MS; + this._bitsActiveNotifications = true; + } + + let updateRootDir = FileUtils.getDir(KEY_UPDROOT, [], true); + let jobName = "MozillaUpdate " + updateRootDir.leafName; + let updatePath = updateDir.path; + if (!Bits.initialized) { + Bits.init(jobName, updatePath, monitorTimeout); + } + + this._cancelPromise = null; + + let bitsId = this._patch.getProperty("bitsId"); + if (bitsId) { + LOG( + "Downloader:downloadUpdate - Connecting to in-progress download. " + + "BITS ID: " + + bitsId + ); + + this._pendingRequest = Bits.monitorDownload( + bitsId, + monitorInterval, + this, + null + ); + } else { + LOG( + "Downloader:downloadUpdate - Starting BITS download with url: " + + this._patch.URL + + ", updateDir: " + + updatePath + + ", filename: " + + FILE_UPDATE_MAR + ); + + this._pendingRequest = Bits.startDownload( + this._patch.URL, + FILE_UPDATE_MAR, + Ci.nsIBits.PROXY_PRECONFIG, + noProgressTimeout, + monitorInterval, + this, + null + ); + } + let request; + try { + request = await this._pendingRequest; + } catch (error) { + if ( + (error.type == Ci.nsIBits.ERROR_TYPE_FAILED_TO_GET_BITS_JOB || + error.type == Ci.nsIBits.ERROR_TYPE_FAILED_TO_CONNECT_TO_BCM) && + error.action == Ci.nsIBits.ERROR_ACTION_MONITOR_DOWNLOAD && + error.stage == Ci.nsIBits.ERROR_STAGE_BITS_CLIENT && + error.codeType == Ci.nsIBits.ERROR_CODE_TYPE_HRESULT && + error.code == HRESULT_E_ACCESSDENIED + ) { + LOG( + "Downloader:downloadUpdate - Failed to connect to existing " + + "BITS job. It is likely owned by another user." + ); + // This isn't really a failure code since the BITS job may be working + // just fine on another account, so convert this to a code that + // indicates that. This will make it easier to identify in telemetry. + error.type = Ci.nsIBits.ERROR_TYPE_ACCESS_DENIED_EXPECTED; + error.codeType = Ci.nsIBits.ERROR_CODE_TYPE_NONE; + error.code = null; + // When we detect this situation, disable BITS until Firefox shuts + // down. There are a couple of reasons for this. First, without any + // kind of flag, we enter an infinite loop here where we keep trying + // BITS over and over again (normally setting bitsResult prevents + // this, but we don't know the result of the BITS job, so we don't + // want to set that). Second, since we are trying to update, this + // process must have the update mutex. We don't ever give up the + // update mutex, so even if the other user starts Firefox, they will + // not complete the BITS job while this Firefox instance is around. + gBITSInUseByAnotherUser = true; + } else { + this._patch.setProperty("bitsResult", Cr.NS_ERROR_FAILURE); + lazy.UM.saveUpdates(); + + LOG( + "Downloader:downloadUpdate - Failed to start to BITS job. " + + "Error: " + + error + ); + } + + this._pendingRequest = null; + + AUSTLMY.pingBitsError(this.isCompleteUpdate, error); + + // Try download again with nsIIncrementalDownload + return this.downloadUpdate(this._update); + } + + this._request = request; + this._patch.setProperty("bitsId", request.bitsId); + + LOG( + "Downloader:downloadUpdate - BITS download running. BITS ID: " + + request.bitsId + ); + + if (this.hasDownloadListeners) { + this._maybeStartActiveNotifications(); + } else { + this._maybeStopActiveNotifications(); + } + + lazy.UM.saveUpdates(); + this._pendingRequest = null; + } + + if (!lazy.UM.readyUpdate) { + LOG("Downloader:downloadUpdate - Setting status to downloading"); + writeStatusFile(getReadyUpdateDir(), STATE_DOWNLOADING); + } + if (this._patch.state != STATE_DOWNLOADING) { + LOG("Downloader:downloadUpdate - Setting state to downloading"); + this._patch.state = STATE_DOWNLOADING; + lazy.UM.saveUpdates(); + } + + // If we are downloading a second update, we don't change the state until + // STATE_SWAP. + if (lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_PENDING) { + LOG( + "Downloader:downloadUpdate - not setting state because download is " + + "already pending." + ); + } else { + LOG( + "Downloader:downloadUpdate - setting currentState to STATE_DOWNLOADING" + ); + transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING); + } + + this._startLangPackUpdates(); + + this._notifyDownloadStatusObservers(); + + return true; + }, + + /** + * This is run when a download listener is added. + */ + onDownloadListenerAdded: function Downloader_onDownloadListenerAdded() { + // Increase the status update frequency when someone starts listening + this._maybeStartActiveNotifications(); + }, + + /** + * This is run when a download listener is removed. + */ + onDownloadListenerRemoved: function Downloader_onDownloadListenerRemoved() { + // Decrease the status update frequency when no one is listening + if (!this.hasDownloadListeners) { + this._maybeStopActiveNotifications(); + } + }, + + get hasDownloadListeners() { + return this.updateService.hasDownloadListeners; + }, + + /** + * This speeds up BITS progress notifications in response to a user watching + * the notifications. + */ + _maybeStartActiveNotifications: async function Downloader__maybeStartActiveNotifications() { + if ( + this.usingBits && + !this._bitsActiveNotifications && + this.hasDownloadListeners && + this._request + ) { + LOG( + "Downloader:_maybeStartActiveNotifications - Starting active " + + "notifications" + ); + this._bitsActiveNotifications = true; + await Promise.all([ + this._request + .setNoProgressTimeout(BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS) + .catch(error => { + LOG( + "Downloader:_maybeStartActiveNotifications - Failed to set " + + "no progress timeout. Error: " + + error + ); + }), + this._request + .changeMonitorInterval(BITS_ACTIVE_POLL_RATE_MS) + .catch(error => { + LOG( + "Downloader:_maybeStartActiveNotifications - Failed to increase " + + "status update frequency. Error: " + + error + ); + }), + ]); + } + }, + + /** + * This slows down BITS progress notifications in response to a user no longer + * watching the notifications. + */ + _maybeStopActiveNotifications: async function Downloader__maybeStopActiveNotifications() { + if ( + this.usingBits && + this._bitsActiveNotifications && + !this.hasDownloadListeners && + this._request + ) { + LOG( + "Downloader:_maybeStopActiveNotifications - Stopping active " + + "notifications" + ); + this._bitsActiveNotifications = false; + await Promise.all([ + this._request + .setNoProgressTimeout(BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS) + .catch(error => { + LOG( + "Downloader:_maybeStopActiveNotifications - Failed to set " + + "no progress timeout: " + + error + ); + }), + this._request + .changeMonitorInterval(BITS_IDLE_POLL_RATE_MS) + .catch(error => { + LOG( + "Downloader:_maybeStopActiveNotifications - Failed to decrease " + + "status update frequency: " + + error + ); + }), + ]); + } + }, + + /** + * When the async request begins + * @param request + * The nsIRequest object for the transfer + */ + onStartRequest: function Downloader_onStartRequest(request) { + if (this.usingBits) { + LOG("Downloader:onStartRequest"); + } else { + LOG( + "Downloader:onStartRequest - original URI spec: " + + request.URI.spec + + ", final URI spec: " + + request.finalURI.spec + ); + // Set finalURL in onStartRequest if it is different. + if (this._patch.finalURL != request.finalURI.spec) { + this._patch.finalURL = request.finalURI.spec; + lazy.UM.saveUpdates(); + } + } + + this.updateService.forEachDownloadListener(listener => { + listener.onStartRequest(request); + }); + }, + + /** + * When new data has been downloaded + * @param request + * The nsIRequest object for the transfer + * @param progress + * The current number of bytes transferred + * @param maxProgress + * The total number of bytes that must be transferred + */ + onProgress: function Downloader_onProgress(request, progress, maxProgress) { + LOG("Downloader:onProgress - progress: " + progress + "/" + maxProgress); + + if (progress > this._patch.size) { + LOG( + "Downloader:onProgress - progress: " + + progress + + " is higher than patch size: " + + this._patch.size + ); + AUSTLMY.pingDownloadCode( + this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_PATCH_SIZE_LARGER + ); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Wait until the transfer has started (progress > 0) to verify maxProgress + // so that we don't check it before it is available (in which case, -1 would + // have been passed). + if (progress > 0 && maxProgress != this._patch.size) { + LOG( + "Downloader:onProgress - maxProgress: " + + maxProgress + + " is not equal to expected patch size: " + + this._patch.size + ); + AUSTLMY.pingDownloadCode( + this.isCompleteUpdate, + AUSTLMY.DWNLD_ERR_PATCH_SIZE_NOT_EQUAL + ); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + return; + } + + this.updateService.forEachDownloadListener(listener => { + if (listener instanceof Ci.nsIProgressEventSink) { + listener.onProgress(request, progress, maxProgress); + } + }); + this.updateService._consecutiveSocketErrors = 0; + }, + + /** + * When we have new status text + * @param request + * The nsIRequest object for the transfer + * @param status + * A status code + * @param statusText + * Human readable version of |status| + */ + onStatus: function Downloader_onStatus(request, status, statusText) { + LOG( + "Downloader:onStatus - status: " + status + ", statusText: " + statusText + ); + + this.updateService.forEachDownloadListener(listener => { + if (listener instanceof Ci.nsIProgressEventSink) { + listener.onStatus(request, status, statusText); + } + }); + }, + + /** + * When data transfer ceases + * @param request + * The nsIRequest object for the transfer + * @param status + * Status code containing the reason for the cessation. + */ + /* eslint-disable-next-line complexity */ + onStopRequest: async function Downloader_onStopRequest(request, status) { + if (gOnlyDownloadUpdatesThisSession) { + LOG( + "Downloader:onStopRequest - End of update download detected and " + + "ignored because we are restricted to update downloads this " + + "session. We will continue with this update next session." + ); + // In order to keep the update from progressing past the downloading + // stage, we will pretend that the download is still going. + // A lot of this work is done for us by just not setting this._request to + // null, which usually signals that the transfer has completed. + this._pretendingDownloadIsNotDone = true; + // This notification is currently used only for testing. + Services.obs.notifyObservers(null, "update-download-restriction-hit"); + return; + } + + if (!this.usingBits) { + LOG( + "Downloader:onStopRequest - downloader: nsIIncrementalDownload, " + + "original URI spec: " + + request.URI.spec + + ", final URI spec: " + + request.finalURI.spec + + ", status: " + + status + ); + } else { + LOG("Downloader:onStopRequest - downloader: BITS, status: " + status); + } + + let bitsCompletionError; + if (this.usingBits) { + if (Components.isSuccessCode(status)) { + try { + await request.complete(); + } catch (e) { + LOG( + "Downloader:onStopRequest - Unable to complete BITS download: " + e + ); + status = Cr.NS_ERROR_FAILURE; + bitsCompletionError = e; + } + } else { + // BITS jobs that failed to complete should still have cancel called on + // them to remove the job. + try { + await this.cancel(); + } catch (e) { + // This will fail if the job stopped because it was cancelled. + // Even if this is a "real" error, there isn't really anything to do + // about it, and it's not really a big problem. It just means that the + // BITS job will stay around until it is removed automatically + // (default of 90 days). + } + } + } + + var state = this._patch.state; + var shouldShowPrompt = false; + var shouldRegisterOnlineObserver = false; + var shouldRetrySoon = false; + var deleteActiveUpdate = false; + let migratedToReadyUpdate = false; + let nonDownloadFailure = false; + var retryTimeout = Services.prefs.getIntPref( + PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT, + DEFAULT_SOCKET_RETRYTIMEOUT + ); + // Prevent the preference from setting a value greater than 10000. + retryTimeout = Math.min(retryTimeout, 10000); + var maxFail = Services.prefs.getIntPref( + PREF_APP_UPDATE_SOCKET_MAXERRORS, + DEFAULT_SOCKET_MAX_ERRORS + ); + // Prevent the preference from setting a value greater than 20. + maxFail = Math.min(maxFail, 20); + LOG( + "Downloader:onStopRequest - status: " + + status + + ", " + + "current fail: " + + this.updateService._consecutiveSocketErrors + + ", " + + "max fail: " + + maxFail + + ", " + + "retryTimeout: " + + retryTimeout + ); + if (Components.isSuccessCode(status)) { + if (this._verifyDownload()) { + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, AUSTLMY.DWNLD_SUCCESS); + + LOG( + "Downloader:onStopRequest - Clearing readyUpdate in preparation of " + + "moving downloadingUpdate into readyUpdate." + ); + + // Clear out any old update before we notify anyone about the new one. + // It will be invalid in a moment anyways when we call + // `cleanUpReadyUpdateDir()`. + lazy.UM.readyUpdate = null; + + // We're about to clobber the ready update so we can replace it with the + // downloading update that just finished. We need to let observers know + // about this. + if ( + lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_PENDING + ) { + transitionState(Ci.nsIApplicationUpdateService.STATE_SWAP); + } + Services.obs.notifyObservers(this._update, "update-swap"); + + // Swap the downloading update into the ready update directory. + cleanUpReadyUpdateDir(); + let downloadedMar = getDownloadingUpdateDir(); + downloadedMar.append(FILE_UPDATE_MAR); + let readyDir = getReadyUpdateDir(); + try { + downloadedMar.moveTo(readyDir, FILE_UPDATE_MAR); + migratedToReadyUpdate = true; + } catch (e) { + migratedToReadyUpdate = false; + } + + if (migratedToReadyUpdate) { + AUSTLMY.pingMoveResult(AUSTLMY.MOVE_RESULT_SUCCESS); + state = getBestPendingState(); + shouldShowPrompt = !getCanStageUpdates(); + + // Tell the updater.exe we're ready to apply. + LOG( + `Downloader:onStopRequest - Ready to apply. Setting state to ` + + `"${state}".` + ); + writeStatusFile(getReadyUpdateDir(), state); + writeVersionFile(getReadyUpdateDir(), this._update.appVersion); + this._update.installDate = new Date().getTime(); + this._update.statusText = lazy.gUpdateBundle.GetStringFromName( + "installPending" + ); + Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0); + } else { + LOG( + "Downloader:onStopRequest - failed to move the downloading " + + "update to the ready update directory." + ); + AUSTLMY.pingMoveResult(AUSTLMY.MOVE_RESULT_UNKNOWN_FAILURE); + + state = STATE_DOWNLOAD_FAILED; + status = Cr.NS_ERROR_FILE_COPY_OR_MOVE_FAILED; + + const mfCode = "move_failed"; + let message = getStatusTextFromCode(mfCode, mfCode); + this._update.statusText = message; + + nonDownloadFailure = true; + deleteActiveUpdate = true; + + cleanUpDownloadingUpdateDir(); + } + } else { + LOG("Downloader:onStopRequest - download verification failed"); + state = STATE_DOWNLOAD_FAILED; + status = Cr.NS_ERROR_CORRUPTED_CONTENT; + + // Yes, this code is a string. + const vfCode = "verification_failed"; + var message = getStatusTextFromCode(vfCode, vfCode); + this._update.statusText = message; + + if (this._update.isCompleteUpdate || this._update.patchCount != 2) { + LOG("Downloader:onStopRequest - No alternative patch to try"); + deleteActiveUpdate = true; + } + + // Destroy the updates directory, since we're done with it. + cleanUpDownloadingUpdateDir(); + } + } else if (status == Cr.NS_ERROR_OFFLINE) { + // Register an online observer to try again. + // The online observer will continue the incremental download by + // calling downloadUpdate on the active update which continues + // downloading the file from where it was. + LOG("Downloader:onStopRequest - offline, register online observer: true"); + AUSTLMY.pingDownloadCode( + this.isCompleteUpdate, + AUSTLMY.DWNLD_RETRY_OFFLINE + ); + shouldRegisterOnlineObserver = true; + deleteActiveUpdate = false; + + // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED, + // NS_ERROR_NET_RESET and NS_ERROR_DOCUMENT_NOT_CACHED can be returned + // when disconnecting the internet while a download of a MAR is in + // progress. There may be others but I have not encountered them during + // testing. + } else if ( + (status == Cr.NS_ERROR_NET_TIMEOUT || + status == Cr.NS_ERROR_CONNECTION_REFUSED || + status == Cr.NS_ERROR_NET_RESET || + status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) && + this.updateService._consecutiveSocketErrors < maxFail + ) { + LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true"); + let dwnldCode = AUSTLMY.DWNLD_RETRY_CONNECTION_REFUSED; + if (status == Cr.NS_ERROR_NET_TIMEOUT) { + dwnldCode = AUSTLMY.DWNLD_RETRY_NET_TIMEOUT; + } else if (status == Cr.NS_ERROR_NET_RESET) { + dwnldCode = AUSTLMY.DWNLD_RETRY_NET_RESET; + } else if (status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) { + dwnldCode = AUSTLMY.DWNLD_ERR_DOCUMENT_NOT_CACHED; + } + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode); + shouldRetrySoon = true; + deleteActiveUpdate = false; + } else if (status != Cr.NS_BINDING_ABORTED && status != Cr.NS_ERROR_ABORT) { + if ( + status == Cr.NS_ERROR_FILE_ACCESS_DENIED || + status == Cr.NS_ERROR_FILE_READ_ONLY + ) { + LOG("Downloader:onStopRequest - permission error"); + nonDownloadFailure = true; + } else { + LOG("Downloader:onStopRequest - non-verification failure"); + } + + let dwnldCode = AUSTLMY.DWNLD_ERR_BINDING_ABORTED; + if (status == Cr.NS_ERROR_ABORT) { + dwnldCode = AUSTLMY.DWNLD_ERR_ABORT; + } + AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode); + + // Some sort of other failure, log this in the |statusText| property + state = STATE_DOWNLOAD_FAILED; + + // XXXben - if |request| (The Incremental Download) provided a means + // for accessing the http channel we could do more here. + + this._update.statusText = getStatusTextFromCode( + status, + Cr.NS_BINDING_FAILED + ); + + // Destroy the updates directory, since we're done with it. + cleanUpDownloadingUpdateDir(); + + deleteActiveUpdate = true; + } + if (!this.usingBits) { + LOG(`Downloader:onStopRequest - Setting internalResult to ${status}`); + this._patch.setProperty("internalResult", status); + } else { + LOG(`Downloader:onStopRequest - Setting bitsResult to ${status}`); + this._patch.setProperty("bitsResult", status); + + // If we failed when using BITS, we want to override the retry decision + // since we need to retry with nsIncrementalDownload before we give up. + // However, if the download was cancelled, don't retry. If the transfer + // was cancelled, we don't want it to restart on its own. + if ( + !Components.isSuccessCode(status) && + status != Cr.NS_BINDING_ABORTED && + status != Cr.NS_ERROR_ABORT + ) { + deleteActiveUpdate = false; + shouldRetrySoon = true; + } + + // Send BITS Telemetry + if (Components.isSuccessCode(status)) { + AUSTLMY.pingBitsSuccess(this.isCompleteUpdate); + } else { + let error; + if (bitsCompletionError) { + error = bitsCompletionError; + } else if (status == Cr.NS_ERROR_CORRUPTED_CONTENT) { + error = new BitsVerificationError(); + } else { + error = request.transferError; + if (!error) { + error = new BitsUnknownError(); + } + } + AUSTLMY.pingBitsError(this.isCompleteUpdate, error); + } + } + + LOG("Downloader:onStopRequest - setting state to: " + state); + if (this._patch.state != state) { + this._patch.state = state; + } + if (deleteActiveUpdate) { + LOG("Downloader:onStopRequest - Clearing downloadingUpdate."); + this._update.installDate = new Date().getTime(); + lazy.UM.addUpdateToHistory(lazy.UM.downloadingUpdate); + lazy.UM.downloadingUpdate = null; + } else if ( + lazy.UM.downloadingUpdate && + lazy.UM.downloadingUpdate.state != state + ) { + lazy.UM.downloadingUpdate.state = state; + } + if (migratedToReadyUpdate) { + LOG( + "Downloader:onStopRequest - Moving downloadingUpdate into readyUpdate" + ); + lazy.UM.readyUpdate = lazy.UM.downloadingUpdate; + lazy.UM.downloadingUpdate = null; + } + lazy.UM.saveUpdates(); + + // Only notify listeners about the stopped state if we + // aren't handling an internal retry. + if (!shouldRetrySoon && !shouldRegisterOnlineObserver) { + this.updateService.forEachDownloadListener(listener => { + listener.onStopRequest(request, status); + }); + } + + this._request = null; + + // This notification must happen after _request is set to null so that + // the correct this.updateService.isDownloading value is available in + // _notifyDownloadStatusObservers(). + this._notifyDownloadStatusObservers(); + + if (state == STATE_DOWNLOAD_FAILED) { + var allFailed = true; + // Don't bother retrying the download if we got an error that isn't + // download related. + if (!nonDownloadFailure) { + // If we haven't already, attempt to download without BITS + if (request instanceof BitsRequest) { + LOG( + "Downloader:onStopRequest - BITS download failed. Falling back " + + "to nsIIncrementalDownload" + ); + let success = await this.downloadUpdate(this._update); + if (!success) { + LOG( + "Downloader:onStopRequest - Failed to fall back to " + + "nsIIncrementalDownload. Cleaning up downloading update." + ); + cleanupDownloadingUpdate(); + } else { + allFailed = false; + } + } + + // Check if there is a complete update patch that can be downloaded. + if ( + allFailed && + !this._update.isCompleteUpdate && + this._update.patchCount == 2 + ) { + LOG( + "Downloader:onStopRequest - verification of patch failed, " + + "downloading complete update patch" + ); + this._update.isCompleteUpdate = true; + let success = await this.downloadUpdate(this._update); + + if (!success) { + LOG( + "Downloader:onStopRequest - Failed to fall back to complete " + + "patch. Cleaning up downloading update." + ); + cleanupDownloadingUpdate(); + } else { + allFailed = false; + } + } + } + + if (allFailed) { + let downloadAttempts = Services.prefs.getIntPref( + PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, + 0 + ); + downloadAttempts++; + Services.prefs.setIntPref( + PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, + downloadAttempts + ); + let maxAttempts = Math.min( + Services.prefs.getIntPref(PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS, 2), + 10 + ); + + transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE); + + if (downloadAttempts > maxAttempts) { + LOG( + "Downloader:onStopRequest - notifying observers of error. " + + "topic: update-error, status: download-attempts-exceeded, " + + "downloadAttempts: " + + downloadAttempts + + " " + + "maxAttempts: " + + maxAttempts + ); + Services.obs.notifyObservers( + this._update, + "update-error", + "download-attempts-exceeded" + ); + } else { + this._update.selectedPatch.selected = false; + LOG( + "Downloader:onStopRequest - notifying observers of error. " + + "topic: update-error, status: download-attempt-failed" + ); + Services.obs.notifyObservers( + this._update, + "update-error", + "download-attempt-failed" + ); + } + // We don't care about language pack updates now. + this._langPackTimeout = null; + LangPackUpdates.delete(unwrap(this._update)); + + // Prevent leaking the update object (bug 454964). + this._update = null; + + // allFailed indicates that we didn't (successfully) call downloadUpdate + // to try to download a different MAR. In this case, this Downloader + // is no longer being used. + this.updateService._downloader = null; + } + // A complete download has been initiated or the failure was handled. + return; + } + + // If the download has succeeded or failed, we are done with this Downloader + // object. However, in some cases (ex: network disconnection), we will + // attempt to resume using this same Downloader. + if (state != STATE_DOWNLOADING) { + this.updateService._downloader = null; + } + + if ( + state == STATE_PENDING || + state == STATE_PENDING_SERVICE || + state == STATE_PENDING_ELEVATE + ) { + if (getCanStageUpdates()) { + LOG( + "Downloader:onStopRequest - attempting to stage update: " + + this._update.name + ); + // Stage the update + let stagingStarted = true; + try { + Cc["@mozilla.org/updates/update-processor;1"] + .createInstance(Ci.nsIUpdateProcessor) + .processUpdate(); + } catch (e) { + LOG( + "Downloader:onStopRequest - failed to stage update. Exception: " + e + ); + stagingStarted = false; + } + if (stagingStarted) { + transitionState(Ci.nsIApplicationUpdateService.STATE_STAGING); + } else { + // Fail gracefully in case the application does not support the update + // processor service. + shouldShowPrompt = true; + } + } + } + + // If we're still waiting on language pack updates then run a timer to time + // out the attempt after an appropriate amount of time. + if (this._langPackTimeout) { + // Start a timer to measure how much longer it takes for the language + // packs to stage. + TelemetryStopwatch.start( + "UPDATE_LANGPACK_OVERTIME", + unwrap(this._update), + { inSeconds: true } + ); + + lazy.setTimeout( + this._langPackTimeout, + Services.prefs.getIntPref( + PREF_APP_UPDATE_LANGPACK_TIMEOUT, + LANGPACK_UPDATE_DEFAULT_TIMEOUT + ) + ); + } + + // Do this after *everything* else, since it will likely cause the app + // to shut down. + if (shouldShowPrompt) { + // Wait for language packs to stage before showing any prompt to restart. + let update = this._update; + promiseLangPacksUpdated(update).then(() => { + LOG( + "Downloader:onStopRequest - Notifying observers that " + + "an update was downloaded. topic: update-downloaded, status: " + + update.state + ); + transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING); + Services.obs.notifyObservers(update, "update-downloaded", update.state); + }); + } + + if (shouldRegisterOnlineObserver) { + LOG("Downloader:onStopRequest - Registering online observer"); + this.updateService._registerOnlineObserver(); + } else if (shouldRetrySoon) { + LOG("Downloader:onStopRequest - Retrying soon"); + this.updateService._consecutiveSocketErrors++; + if (this.updateService._retryTimer) { + this.updateService._retryTimer.cancel(); + } + this.updateService._retryTimer = Cc[ + "@mozilla.org/timer;1" + ].createInstance(Ci.nsITimer); + this.updateService._retryTimer.initWithCallback( + async () => { + await this.updateService._attemptResume(); + }, + retryTimeout, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } else { + // Prevent leaking the update object (bug 454964) + this._update = null; + } + }, + + /** + * This function should be called when shutting down so that resources get + * freed properly. + */ + cleanup: async function Downloader_cleanup() { + if (this.usingBits) { + if (this._pendingRequest) { + await this._pendingRequest; + } + this._request.shutdown(); + } + }, + + /** + * See nsIInterfaceRequestor.idl + */ + getInterface: function Downloader_getInterface(iid) { + // The network request may require proxy authentication, so provide the + // default nsIAuthPrompt if requested. + if (iid.equals(Ci.nsIAuthPrompt)) { + var prompt = Cc[ + "@mozilla.org/network/default-auth-prompt;1" + ].createInstance(); + return prompt.QueryInterface(iid); + } + throw Components.Exception("", Cr.NS_NOINTERFACE); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsIProgressEventSink", + "nsIInterfaceRequestor", + ]), +}; + +// On macOS, all browser windows can be closed without Firefox exiting. If it +// is left in this state for a while and an update is pending, we should restart +// Firefox on our own to apply the update. This class will do that +// automatically. +class RestartOnLastWindowClosed { + #enabled = false; + #hasShutdown = false; + + #restartTimer = null; + #restartTimerExpired = false; + + constructor() { + this.#maybeEnableOrDisable(); + + Services.prefs.addObserver( + PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED, + this + ); + Services.obs.addObserver(this, "quit-application"); + } + + shutdown() { + LOG("RestartOnLastWindowClosed.shutdown - Shutting down"); + this.#hasShutdown = true; + + Services.prefs.removeObserver( + PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED, + this + ); + Services.obs.removeObserver(this, "quit-application"); + + this.#maybeEnableOrDisable(); + } + + get shouldEnable() { + if (AppConstants.platform != "macosx") { + return false; + } + if (this.#hasShutdown) { + return false; + } + return Services.prefs.getBoolPref( + PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED, + false + ); + } + + get enabled() { + return this.#enabled; + } + + observe(subject, topic, data) { + switch (topic) { + case "nsPref:changed": + if (data == PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED) { + this.#maybeEnableOrDisable(); + } + break; + case "quit-application": + this.shutdown(); + break; + case "domwindowclosed": + this.#onWindowClose(); + break; + case "domwindowopened": + this.#onWindowOpen(); + break; + case "update-downloaded": + case "update-staged": + this.#onUpdateReady(data); + break; + } + } + + // Returns true if any windows are open. Otherwise, false. + #windowsAreOpen() { + // eslint-disable-next-line no-unused-vars + for (const win of Services.wm.getEnumerator(null)) { + return true; + } + return false; + } + + // Enables or disables this class's functionality based on the value of + // this.shouldEnable. Does nothing if the class is already in the right state + // (i.e. if the class should be enabled and already is, or should be disabled + // and already is). + #maybeEnableOrDisable() { + if (this.shouldEnable) { + if (this.#enabled) { + return; + } + LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Enabling"); + + Services.obs.addObserver(this, "domwindowclosed"); + Services.obs.addObserver(this, "domwindowopened"); + Services.obs.addObserver(this, "update-downloaded"); + Services.obs.addObserver(this, "update-staged"); + + this.#restartTimer = null; + this.#restartTimerExpired = false; + + this.#enabled = true; + + // Synchronize with external state. + this.#onWindowClose(); + } else { + if (!this.#enabled) { + return; + } + LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Disabling"); + + Services.obs.removeObserver(this, "domwindowclosed"); + Services.obs.removeObserver(this, "domwindowopened"); + Services.obs.removeObserver(this, "update-downloaded"); + Services.obs.removeObserver(this, "update-staged"); + + this.#enabled = false; + + if (this.#restartTimer) { + this.#restartTimer.cancel(); + } + this.#restartTimer = null; + } + } + + // Note: Since we keep track of the update state even when this class is + // disabled, this function will run even in that case. + #onUpdateReady(updateState) { + // Note that we do not count pending-elevate as a ready state, because we + // cannot silently restart in that state. + if ( + [ + STATE_APPLIED, + STATE_PENDING, + STATE_APPLIED_SERVICE, + STATE_PENDING_SERVICE, + ].includes(updateState) + ) { + if (this.#enabled) { + LOG("RestartOnLastWindowClosed.#onUpdateReady - update ready"); + this.#maybeRestartBrowser(); + } + } else if (this.#enabled) { + LOG( + `RestartOnLastWindowClosed.#onUpdateReady - Not counting update as ` + + `ready because the state is ${updateState}` + ); + } + } + + #onWindowClose() { + if (!this.#windowsAreOpen()) { + this.#onLastWindowClose(); + } + } + + #onLastWindowClose() { + if (this.#restartTimer || this.#restartTimerExpired) { + LOG( + "RestartOnLastWindowClosed.#onLastWindowClose - Restart timer is " + + "either already running or has already expired" + ); + return; + } + + let timeout = Services.prefs.getIntPref( + PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_DELAY_MS, + 5 * 60 * 1000 + ); + + LOG( + "RestartOnLastWindowClosed.#onLastWindowClose - Last window closed. " + + "Starting restart timer" + ); + this.#restartTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.#restartTimer.initWithCallback( + () => this.#onRestartTimerExpire(), + timeout, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } + + #onWindowOpen() { + if (this.#restartTimer) { + LOG( + "RestartOnLastWindowClosed.#onWindowOpen - Window opened. Cancelling " + + "restart timer." + ); + this.#restartTimer.cancel(); + } + this.#restartTimer = null; + this.#restartTimerExpired = false; + } + + #onRestartTimerExpire() { + LOG("RestartOnLastWindowClosed.#onRestartTimerExpire - Timer Expired"); + + this.#restartTimer = null; + this.#restartTimerExpired = true; + this.#maybeRestartBrowser(); + } + + #maybeRestartBrowser() { + if (!this.#restartTimerExpired) { + LOG( + "RestartOnLastWindowClosed.#maybeRestartBrowser - Still waiting for " + + "all windows to be closed and restartTimer to expire. " + + "(not restarting)" + ); + return; + } + + if (lazy.AUS.currentState != Ci.nsIApplicationUpdateService.STATE_PENDING) { + LOG( + "RestartOnLastWindowClosed.#maybeRestartBrowser - No update ready. " + + "(not restarting)" + ); + return; + } + + if (getElevationRequired()) { + // We check for STATE_PENDING_ELEVATE elsewhere, but this is actually + // different from that because it is technically possible that the user + // gave permission to elevate, but we haven't actually elevated yet. + // This is a bit of a corner case. We only call elevationOptedIn() right + // before we restart to apply the update immediately. But it is possible + // that something could stop the browser from shutting down. + LOG( + "RestartOnLastWindowClosed.#maybeRestartBrowser - This update will " + + "require user elevation (not restarting)" + ); + return; + } + + if (this.#windowsAreOpen()) { + LOG( + "RestartOnLastWindowClosed.#maybeRestartBrowser - Window " + + "unexpectedly still open! (not restarting)" + ); + return; + } + + if (!this.shouldEnable) { + LOG( + "RestartOnLastWindowClosed.#maybeRestartBrowser - Unexpectedly " + + "attempted to restart when RestartOnLastWindowClosed ought to be " + + "disabled! (not restarting)" + ); + return; + } + + LOG("RestartOnLastWindowClosed.#maybeRestartBrowser - Restarting now"); + Services.telemetry.scalarAdd("update.no_window_auto_restarts", 1); + Services.startup.quit( + Ci.nsIAppStartup.eAttemptQuit | + Ci.nsIAppStartup.eRestart | + Ci.nsIAppStartup.eSilently + ); + } +} +// Nothing actually uses this variable at the moment, but let's make sure that +// we hold the reference to the RestartOnLastWindowClosed instance somewhere. +// eslint-disable-next-line no-unused-vars +let restartOnLastWindowClosed = new RestartOnLastWindowClosed(); + +var EXPORTED_SYMBOLS = ["UpdateService", "CheckerService", "UpdateManager"]; diff --git a/toolkit/mozapps/update/UpdateServiceStub.jsm b/toolkit/mozapps/update/UpdateServiceStub.jsm new file mode 100644 index 0000000000..8e64ff207e --- /dev/null +++ b/toolkit/mozapps/update/UpdateServiceStub.jsm @@ -0,0 +1,430 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const DIR_UPDATES = "updates"; +const FILE_UPDATE_STATUS = "update.status"; +const FILE_UPDATE_MESSAGES = "update_messages.log"; +const FILE_BACKUP_MESSAGES = "update_messages_old.log"; + +const KEY_UPDROOT = "UpdRootD"; +const KEY_OLD_UPDROOT = "OldUpdRootD"; +const KEY_PROFILE_DIR = "ProfD"; + +// The pref prefix below should have the hash of the install path appended to +// ensure that this is a per-installation pref (i.e. to ensure that migration +// happens for every install rather than once per profile) +const PREF_PREFIX_UPDATE_DIR_MIGRATED = "app.update.migrated.updateDir3."; +const PREF_APP_UPDATE_ALTUPDATEDIRPATH = "app.update.altUpdateDirPath"; +const PREF_APP_UPDATE_LOG = "app.update.log"; +const PREF_APP_UPDATE_FILE_LOGGING = "app.update.log.file"; + +const lazy = {}; + +XPCOMUtils.defineLazyGetter(lazy, "gLogEnabled", function aus_gLogEnabled() { + return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false); +}); + +function getUpdateBaseDirNoCreate() { + if (Cu.isInAutomation) { + // This allows tests to use an alternate updates directory so they can test + // startup behavior. + const MAGIC_TEST_ROOT_PREFIX = "<test-root>"; + const PREF_TEST_ROOT = "mochitest.testRoot"; + let alternatePath = Services.prefs.getCharPref( + PREF_APP_UPDATE_ALTUPDATEDIRPATH, + null + ); + if (alternatePath && alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) { + let testRoot = Services.prefs.getCharPref(PREF_TEST_ROOT); + let relativePath = alternatePath.substring(MAGIC_TEST_ROOT_PREFIX.length); + if (AppConstants.platform == "win") { + relativePath = relativePath.replace(/\//g, "\\"); + } + alternatePath = testRoot + relativePath; + let updateDir = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + updateDir.initWithPath(alternatePath); + LOG( + "getUpdateBaseDirNoCreate returning test directory, path: " + + updateDir.path + ); + return updateDir; + } + } + + return FileUtils.getDir(KEY_UPDROOT, [], false); +} + +function UpdateServiceStub() { + let updateDir = getUpdateBaseDirNoCreate(); + let prefUpdateDirMigrated = + PREF_PREFIX_UPDATE_DIR_MIGRATED + updateDir.leafName; + + let statusFile = updateDir; + statusFile.append(DIR_UPDATES); + statusFile.append("0"); + statusFile.append(FILE_UPDATE_STATUS); + updateDir = null; // We don't need updateDir anymore, plus now its nsIFile + // contains the status file's path + + // We may need to migrate update data + if ( + AppConstants.platform == "win" && + !Services.prefs.getBoolPref(prefUpdateDirMigrated, false) + ) { + Services.prefs.setBoolPref(prefUpdateDirMigrated, true); + try { + migrateUpdateDirectory(); + } catch (ex) { + // For the most part, migrateUpdateDirectory() catches its own errors. + // But there are technically things that could happen that might not be + // caught, like nsIFile.parent or nsIFile.append could unexpectedly fail. + // So we will catch any errors here, just in case. + LOG( + `UpdateServiceStub:UpdateServiceStub Failed to migrate update ` + + `directory. Exception: ${ex}` + ); + } + } + + // Prevent file logging from persisting for more than a session by disabling + // it on startup. + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_FILE_LOGGING, false)) { + deactivateUpdateLogFile(); + } + + // If the update.status file exists then initiate post update processing. + if (statusFile.exists()) { + let aus = Cc["@mozilla.org/updates/update-service;1"] + .getService(Ci.nsIApplicationUpdateService) + .QueryInterface(Ci.nsIObserver); + aus.observe(null, "post-update-processing", ""); + } +} +UpdateServiceStub.prototype = { + observe() {}, + classID: Components.ID("{e43b0010-04ba-4da6-b523-1f92580bc150}"), + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), +}; + +var EXPORTED_SYMBOLS = ["UpdateServiceStub"]; + +function deactivateUpdateLogFile() { + LOG("Application update file logging being automatically turned off"); + Services.prefs.setBoolPref(PREF_APP_UPDATE_FILE_LOGGING, false); + let logFile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile); + logFile.append(FILE_UPDATE_MESSAGES); + + try { + logFile.moveTo(null, FILE_BACKUP_MESSAGES); + } catch (e) { + LOG( + "Failed to backup update messages log (" + + e + + "). Attempting to " + + "remove it." + ); + try { + logFile.remove(false); + } catch (e) { + LOG("Also failed to remove the update messages log: " + e); + } + } +} + +/** + * This function should be called when there are files in the old update + * directory that may need to be migrated to the new update directory. + */ +function migrateUpdateDirectory() { + LOG("UpdateServiceStub:migrateUpdateDirectory Performing migration"); + + let sourceRootDir = FileUtils.getDir(KEY_OLD_UPDROOT, [], false); + let destRootDir = FileUtils.getDir(KEY_UPDROOT, [], false); + let hash = destRootDir.leafName; + + if (!sourceRootDir.exists()) { + // Nothing to migrate. + return; + } + + // List of files to migrate. Each is specified as a list of path components. + const toMigrate = [ + ["updates.xml"], + ["active-update.xml"], + ["update-config.json"], + ["updates", "last-update.log"], + ["updates", "backup-update.log"], + ["updates", "downloading", FILE_UPDATE_STATUS], + ["updates", "downloading", "update.mar"], + ["updates", "0", FILE_UPDATE_STATUS], + ["updates", "0", "update.mar"], + ["updates", "0", "update.version"], + ["updates", "0", "update.log"], + ["backgroundupdate", "datareporting", "glean", "db", "data.safe.bin"], + ]; + + // Before we copy anything, double check that a different profile hasn't + // already performed migration. If we don't have the necessary permissions to + // remove the pre-migration files, we don't want to copy any old files and + // potentially make the current update state inconsistent. + for (let pathComponents of toMigrate) { + // Assemble the destination nsIFile. + let destFile = destRootDir.clone(); + for (let pathComponent of pathComponents) { + destFile.append(pathComponent); + } + + if (destFile.exists()) { + LOG( + `UpdateServiceStub:migrateUpdateDirectory Aborting migration because ` + + `"${destFile.path}" already exists.` + ); + return; + } + } + + // Before we migrate everything in toMigrate, there are a few things that + // need special handling. + let sourceRootParent = sourceRootDir.parent.parent; + let destRootParent = destRootDir.parent.parent; + + let profileCountFile = sourceRootParent.clone(); + profileCountFile.append(`profile_count_${hash}.json`); + migrateFile(profileCountFile, destRootParent); + + const updatePingPrefix = `uninstall_ping_${hash}_`; + const updatePingSuffix = ".json"; + try { + for (let file of sourceRootParent.directoryEntries) { + if ( + file.leafName.startsWith(updatePingPrefix) && + file.leafName.endsWith(updatePingSuffix) + ) { + migrateFile(file, destRootParent); + } + } + } catch (ex) { + // migrateFile should catch its own errors, but it is possible that + // sourceRootParent.directoryEntries could throw. + LOG( + `UpdateServiceStub:migrateUpdateDirectory Failed to migrate uninstall ` + + `ping. Exception: ${ex}` + ); + } + + // Migrate "backgroundupdate.moz_log" and child process logs like + // "backgroundupdate.child-1.moz_log". + const backgroundLogPrefix = `backgroundupdate`; + const backgroundLogSuffix = ".moz_log"; + try { + for (let file of sourceRootDir.directoryEntries) { + if ( + file.leafName.startsWith(backgroundLogPrefix) && + file.leafName.endsWith(backgroundLogSuffix) + ) { + migrateFile(file, destRootDir); + } + } + } catch (ex) { + LOG( + `UpdateServiceStub:migrateUpdateDirectory Failed to migrate background ` + + `log file. Exception: ${ex}` + ); + } + + const pendingPingRelDir = + "backgroundupdate\\datareporting\\glean\\pending_pings"; + let pendingPingSourceDir = sourceRootDir.clone(); + pendingPingSourceDir.appendRelativePath(pendingPingRelDir); + let pendingPingDestDir = destRootDir.clone(); + pendingPingDestDir.appendRelativePath(pendingPingRelDir); + // Pending ping filenames are UUIDs. + const pendingPingFilenameRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; + if (pendingPingSourceDir.exists()) { + try { + for (let file of pendingPingSourceDir.directoryEntries) { + if (pendingPingFilenameRegex.test(file.leafName)) { + migrateFile(file, pendingPingDestDir); + } + } + } catch (ex) { + // migrateFile should catch its own errors, but it is possible that + // pendingPingSourceDir.directoryEntries could throw. + LOG( + `UpdateServiceStub:migrateUpdateDirectory Failed to migrate ` + + `pending pings. Exception: ${ex}` + ); + } + } + + // Migrate everything in toMigrate. + for (let pathComponents of toMigrate) { + let filename = pathComponents.pop(); + + // Assemble the source and destination nsIFile's. + let sourceFile = sourceRootDir.clone(); + let destDir = destRootDir.clone(); + for (let pathComponent of pathComponents) { + sourceFile.append(pathComponent); + destDir.append(pathComponent); + } + sourceFile.append(filename); + + migrateFile(sourceFile, destDir); + } + + // There is no reason to keep this file, and it often hangs around and could + // interfere with cleanup. + let updateLockFile = sourceRootParent.clone(); + updateLockFile.append(`UpdateLock-${hash}`); + try { + updateLockFile.remove(false); + } catch (ex) {} + + // We want to recursively remove empty directories out of the sourceRootDir. + // And if that was the only remaining update directory in sourceRootParent, + // we want to remove that too. But we don't want to recurse into other update + // directories in sourceRootParent. + // + // Potentially removes "C:\ProgramData\Mozilla\updates\<hash>" and + // subdirectories. + cleanupDir(sourceRootDir, true); + // Potentially removes "C:\ProgramData\Mozilla\updates" + cleanupDir(sourceRootDir.parent, false); + // Potentially removes "C:\ProgramData\Mozilla" + cleanupDir(sourceRootParent, false); +} + +/** + * Attempts to move the source file to the destination directory. If the file + * cannot be moved, we attempt to copy it and remove the original. All errors + * are logged, but no exceptions are thrown. Both arguments must be of type + * nsIFile and are expected to be regular files. + * + * Non-existent files are silently ignored. + * + * The reason that we are migrating is to deal with problematic inherited + * permissions. But, luckily, neither nsIFile.moveTo nor nsIFile.copyTo preserve + * inherited permissions. + */ +function migrateFile(sourceFile, destDir) { + if (!sourceFile.exists()) { + return; + } + + if (sourceFile.isDirectory()) { + LOG( + `UpdateServiceStub:migrateFile Aborting attempt to migrate ` + + `"${sourceFile.path}" because it is a directory.` + ); + return; + } + + // Create destination directory. + try { + // Pass an arbitrary value for permissions. Windows doesn't use octal + // permissions, so that value doesn't really do anything. + destDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) { + LOG( + `UpdateServiceStub:migrateFile Unable to create destination ` + + `directory "${destDir.path}": ${ex}` + ); + } + } + + try { + sourceFile.moveTo(destDir, null); + return; + } catch (ex) {} + + try { + sourceFile.copyTo(destDir, null); + } catch (ex) { + LOG( + `UpdateServiceStub:migrateFile Failed to migrate file from ` + + `"${sourceFile.path}" to "${destDir.path}". Exception: ${ex}` + ); + return; + } + + try { + sourceFile.remove(false); + } catch (ex) { + LOG( + `UpdateServiceStub:migrateFile Successfully migrated file from ` + + `"${sourceFile.path}" to "${destDir.path}", but was unable to remove ` + + `the original. Exception: ${ex}` + ); + } +} + +/** + * If recurse is true, recurses through the directory's contents. Any empty + * directories are removed. Directories with remaining files are left behind. + * + * If recurse if false, we delete the directory passed as long as it is empty. + * + * All errors are silenced and not thrown. + * + * Returns true if the directory passed in was removed. Otherwise false. + */ +function cleanupDir(dir, recurse) { + let directoryEmpty = true; + try { + for (let file of dir.directoryEntries) { + if (!recurse) { + // If we aren't recursing, bail out after we find a single file. The + // directory isn't empty so we can't delete it, and we aren't going to + // clean out and remove any other directories. + return false; + } + if (file.isDirectory()) { + if (!cleanupDir(file, recurse)) { + directoryEmpty = false; + } + } else { + directoryEmpty = false; + } + } + } catch (ex) { + // If any of our nsIFile calls fail, just err on the side of caution and + // don't delete anything. + return false; + } + + if (directoryEmpty) { + try { + dir.remove(false); + return true; + } catch (ex) {} + } + return false; +} + +/** + * Logs a string to the error console. + * @param string + * The string to write to the error console. + */ +function LOG(string) { + if (lazy.gLogEnabled) { + dump("*** AUS:SVC " + string + "\n"); + Services.console.logStringMessage("AUS:SVC " + string); + } +} diff --git a/toolkit/mozapps/update/UpdateTelemetry.jsm b/toolkit/mozapps/update/UpdateTelemetry.jsm new file mode 100644 index 0000000000..a41173cde8 --- /dev/null +++ b/toolkit/mozapps/update/UpdateTelemetry.jsm @@ -0,0 +1,655 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["AUSTLMY"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { BitsError, BitsUnknownError } = ChromeUtils.import( + "resource://gre/modules/Bits.jsm" +); + +var AUSTLMY = { + // Telemetry for the application update background update check occurs when + // the background update timer fires after the update interval which is + // determined by the app.update.interval preference and its telemetry + // histogram IDs have the suffix '_NOTIFY'. + // Telemetry for the externally initiated background update check occurs when + // a call is made to |checkForBackgroundUpdates| which is typically initiated + // by an application when it has determined that the application should have + // received an update. This has separate telemetry so it is possible to + // analyze using the telemetry data systems that have not been updating when + // they should have. + + // The update check was performed by the call to checkForBackgroundUpdates in + // nsUpdateService.js. + EXTERNAL: "EXTERNAL", + // The update check was performed by the call to notify in nsUpdateService.js. + NOTIFY: "NOTIFY", + // The update check was performed after an update is already ready. There is + // currently no way for a user to initiate an update check when there is a + // ready update (the UI just prompts you to install the ready update). So + // subsequent update checks are necessarily "notify" update checks, not + // "external" ones. + SUBSEQUENT: "SUBSEQUENT", + + /** + * Values for the UPDATE_CHECK_CODE_NOTIFY and UPDATE_CHECK_CODE_EXTERNAL + * Telemetry histograms. + */ + // No update found (no notification) + CHK_NO_UPDATE_FOUND: 0, + // Update will be downloaded in the background (background download) + CHK_DOWNLOAD_UPDATE: 1, + // Showing prompt due to preference (update notification) + CHK_SHOWPROMPT_PREF: 3, + // Already has an active update in progress (no notification) + CHK_HAS_ACTIVEUPDATE: 8, + // A background download is already in progress (no notification) + CHK_IS_DOWNLOADING: 9, + // An update is already staged (no notification) + CHK_IS_STAGED: 10, + // An update is already downloaded (no notification) + CHK_IS_DOWNLOADED: 11, + // Note: codes 12-13 were removed along with the |app.update.enabled| pref. + // Unable to check for updates per hasUpdateMutex() (no notification) + CHK_NO_MUTEX: 14, + // Unable to check for updates per gCanCheckForUpdates (no notification). This + // should be covered by other codes and is recorded just in case. + CHK_UNABLE_TO_CHECK: 15, + // Note: code 16 was removed when the feature for disabling updates for the + // session was removed. + // Unable to perform a background check while offline (no notification) + CHK_OFFLINE: 17, + // Note: codes 18 - 21 were removed along with the certificate checking code. + // General update check failure and threshold reached + // (check failure notification) + CHK_GENERAL_ERROR_PROMPT: 22, + // General update check failure and threshold not reached (no notification) + CHK_GENERAL_ERROR_SILENT: 23, + // No compatible update found though there were updates (no notification) + CHK_NO_COMPAT_UPDATE_FOUND: 24, + // Update found for a previous version (no notification) + CHK_UPDATE_PREVIOUS_VERSION: 25, + // Update found without a type attribute (no notification) + CHK_UPDATE_INVALID_TYPE: 27, + // The system is no longer supported (system unsupported notification) + CHK_UNSUPPORTED: 28, + // Unable to apply updates (manual install to update notification) + CHK_UNABLE_TO_APPLY: 29, + // Unable to check for updates due to no OS version (no notification) + CHK_NO_OS_VERSION: 30, + // Unable to check for updates due to no OS ABI (no notification) + CHK_NO_OS_ABI: 31, + // Invalid update url (no notification) + CHK_INVALID_DEFAULT_URL: 32, + // Update elevation failures or cancelations threshold reached for this + // version, OSX only (no notification) + CHK_ELEVATION_DISABLED_FOR_VERSION: 35, + // User opted out of elevated updates for the available update version, OSX + // only (no notification) + CHK_ELEVATION_OPTOUT_FOR_VERSION: 36, + // Update checks disabled by enterprise policy + CHK_DISABLED_BY_POLICY: 37, + // Update check failed due to write error + CHK_ERR_WRITE_FAILURE: 38, + // Update check was delayed because another instance of the application is + // currently running + CHK_OTHER_INSTANCE: 39, + // Cannot yet download update because no partial patch is available and an + // update has already been downloaded. + CHK_NO_PARTIAL_PATCH: 40, + + /** + * Submit a telemetry ping for the update check result code or a telemetry + * ping for a count type histogram count when no update was found. The no + * update found ping is separate since it is the typical result, is less + * interesting than the other result codes, and it is easier to analyze the + * other codes without including it. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_CHECK_CODE_EXTERNAL + * UPDATE_CHECK_CODE_NOTIFY + * UPDATE_CHECK_CODE_SUBSEQUENT + * UPDATE_CHECK_NO_UPDATE_EXTERNAL + * UPDATE_CHECK_NO_UPDATE_NOTIFY + * UPDATE_CHECK_NO_UPDATE_SUBSEQUENT + * @param aCode + * An integer value as defined by the values that start with CHK_ in + * the above section. + */ + pingCheckCode: function UT_pingCheckCode(aSuffix, aCode) { + try { + if (aCode == this.CHK_NO_UPDATE_FOUND) { + let id = "UPDATE_CHECK_NO_UPDATE_" + aSuffix; + // count type histogram + Services.telemetry.getHistogramById(id).add(); + } else { + let id = "UPDATE_CHECK_CODE_" + aSuffix; + // enumerated type histogram + Services.telemetry.getHistogramById(id).add(aCode); + } + } catch (e) { + console.error(e); + } + }, + + /** + * Submit a telemetry ping for a failed update check's unhandled error code + * when the pingCheckCode is CHK_GENERAL_ERROR_SILENT. The histogram is a + * keyed count type with key names that are prefixed with 'AUS_CHECK_EX_ERR_'. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_CHECK_EXTENDED_ERROR_EXTERNAL + * UPDATE_CHECK_EXTENDED_ERROR_NOTIFY + * UPDATE_CHECK_EXTENDED_ERROR_SUBSEQUENT + * @param aCode + * The extended error value return by a failed update check. + */ + pingCheckExError: function UT_pingCheckExError(aSuffix, aCode) { + try { + let id = "UPDATE_CHECK_EXTENDED_ERROR_" + aSuffix; + let val = "AUS_CHECK_EX_ERR_" + aCode; + // keyed count type histogram + Services.telemetry.getKeyedHistogramById(id).add(val); + } catch (e) { + console.error(e); + } + }, + + // The state code and if present the status error code were read on startup. + STARTUP: "STARTUP", + // The state code and status error code if present were read after staging. + STAGE: "STAGE", + + // Patch type Complete + PATCH_COMPLETE: "COMPLETE", + // Patch type partial + PATCH_PARTIAL: "PARTIAL", + // Patch type unknown + PATCH_UNKNOWN: "UNKNOWN", + + /** + * Values for the UPDATE_DOWNLOAD_CODE_COMPLETE, UPDATE_DOWNLOAD_CODE_PARTIAL, + * and UPDATE_DOWNLOAD_CODE_UNKNOWN Telemetry histograms. + */ + DWNLD_SUCCESS: 0, + DWNLD_RETRY_OFFLINE: 1, + DWNLD_RETRY_NET_TIMEOUT: 2, + DWNLD_RETRY_CONNECTION_REFUSED: 3, + DWNLD_RETRY_NET_RESET: 4, + DWNLD_ERR_NO_UPDATE: 5, + DWNLD_ERR_NO_UPDATE_PATCH: 6, + DWNLD_ERR_PATCH_SIZE_LARGER: 8, + DWNLD_ERR_PATCH_SIZE_NOT_EQUAL: 9, + DWNLD_ERR_BINDING_ABORTED: 10, + DWNLD_ERR_ABORT: 11, + DWNLD_ERR_DOCUMENT_NOT_CACHED: 12, + DWNLD_ERR_VERIFY_NO_REQUEST: 13, + DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL: 14, + DWNLD_ERR_WRITE_FAILURE: 15, + // Temporary failure code to see if there are failures without an update phase + DWNLD_UNKNOWN_PHASE_ERR_WRITE_FAILURE: 40, + + /** + * Submit a telemetry ping for the update download result code. + * + * @param aIsComplete + * If true the histogram is for a patch type complete, if false the + * histogram is for a patch type partial, and when undefined the + * histogram is for an unknown patch type. This is used to determine + * the histogram ID out of the following histogram IDs: + * UPDATE_DOWNLOAD_CODE_COMPLETE + * UPDATE_DOWNLOAD_CODE_PARTIAL + * UPDATE_DOWNLOAD_CODE_UNKNOWN + * @param aCode + * An integer value as defined by the values that start with DWNLD_ in + * the above section. + */ + pingDownloadCode: function UT_pingDownloadCode(aIsComplete, aCode) { + let patchType = this.PATCH_UNKNOWN; + if (aIsComplete === true) { + patchType = this.PATCH_COMPLETE; + } else if (aIsComplete === false) { + patchType = this.PATCH_PARTIAL; + } + try { + let id = "UPDATE_DOWNLOAD_CODE_" + patchType; + // enumerated type histogram + Services.telemetry.getHistogramById(id).add(aCode); + } catch (e) { + console.error(e); + } + }, + + // Previous state codes are defined in pingStateAndStatusCodes() in + // nsUpdateService.js + STATE_WRITE_FAILURE: 14, + + /** + * Submit a telemetry ping for the update status state code. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_STATE_CODE_COMPLETE_STARTUP + * UPDATE_STATE_CODE_PARTIAL_STARTUP + * UPDATE_STATE_CODE_UNKNOWN_STARTUP + * UPDATE_STATE_CODE_COMPLETE_STAGE + * UPDATE_STATE_CODE_PARTIAL_STAGE + * UPDATE_STATE_CODE_UNKNOWN_STAGE + * @param aCode + * An integer value as defined by the values that start with STATE_ in + * the above section for the update state from the update.status file. + */ + pingStateCode: function UT_pingStateCode(aSuffix, aCode) { + try { + let id = "UPDATE_STATE_CODE_" + aSuffix; + // enumerated type histogram + Services.telemetry.getHistogramById(id).add(aCode); + } catch (e) { + console.error(e); + } + }, + + /** + * Submit a telemetry ping for the update status error code. This does not + * submit a success value which can be determined from the state code. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_STATUS_ERROR_CODE_COMPLETE_STARTUP + * UPDATE_STATUS_ERROR_CODE_PARTIAL_STARTUP + * UPDATE_STATUS_ERROR_CODE_UNKNOWN_STARTUP + * UPDATE_STATUS_ERROR_CODE_COMPLETE_STAGE + * UPDATE_STATUS_ERROR_CODE_PARTIAL_STAGE + * UPDATE_STATUS_ERROR_CODE_UNKNOWN_STAGE + * @param aCode + * An integer value for the error code from the update.status file. + */ + pingStatusErrorCode: function UT_pingStatusErrorCode(aSuffix, aCode) { + try { + let id = "UPDATE_STATUS_ERROR_CODE_" + aSuffix; + // enumerated type histogram + Services.telemetry.getHistogramById(id).add(aCode); + } catch (e) { + console.error(e); + } + }, + + /** + * Submit a telemetry ping for a failing binary transparency result. + * + * @param aSuffix + * Key to use on the update.binarytransparencyresult collection. + * Must be one of "COMPLETE_STARTUP", "PARTIAL_STARTUP", + * "UNKNOWN_STARTUP", "COMPLETE_STAGE", "PARTIAL_STAGE", + * "UNKNOWN_STAGE". + * @param aCode + * An integer value for the error code from the update.bt file. + */ + pingBinaryTransparencyResult: function UT_pingBinaryTransparencyResult( + aSuffix, + aCode + ) { + try { + let id = "update.binarytransparencyresult"; + let key = aSuffix.toLowerCase().replace("_", "-"); + Services.telemetry.keyedScalarSet(id, key, aCode); + } catch (e) { + console.error(e); + } + }, + + /** + * Records a failed BITS update download using Telemetry. + * In addition to the BITS Result histogram, this also sends an + * update.bitshresult scalar value. + * + * @param aIsComplete + * If true the histogram is for a patch type complete, if false the + * histogram is for a patch type partial. This will determine the + * histogram id out of the following histogram ids: + * UPDATE_BITS_RESULT_COMPLETE + * UPDATE_BITS_RESULT_PARTIAL + * This value is also used to determine the key for the keyed scalar + * update.bitshresult (key is either "COMPLETE" or "PARTIAL") + * @param aError + * The BitsError that occurred. See Bits.jsm for details on BitsError. + */ + pingBitsError: function UT_pingBitsError(aIsComplete, aError) { + if (AppConstants.platform != "win") { + console.error( + "Warning: Attempted to submit BITS telemetry on a " + + "non-Windows platform" + ); + return; + } + if (!(aError instanceof BitsError)) { + console.error("Error sending BITS Error ping: Error is not a BitsError"); + aError = new BitsUnknownError(); + } + // Coerce the error to integer + let type = +aError.type; + if (isNaN(type)) { + console.error( + "Error sending BITS Error ping: Either error is not a " + + "BitsError, or error type is not an integer." + ); + type = Ci.nsIBits.ERROR_TYPE_UNKNOWN; + } else if (type == Ci.nsIBits.ERROR_TYPE_SUCCESS) { + console.error( + "Error sending BITS Error ping: The error type must not " + + "be the success type." + ); + type = Ci.nsIBits.ERROR_TYPE_UNKNOWN; + } + this._pingBitsResult(aIsComplete, type); + + if (aError.codeType == Ci.nsIBits.ERROR_CODE_TYPE_HRESULT) { + let scalarKey; + if (aIsComplete) { + scalarKey = this.PATCH_COMPLETE; + } else { + scalarKey = this.PATCH_PARTIAL; + } + try { + Services.telemetry.keyedScalarSet( + "update.bitshresult", + scalarKey, + aError.code + ); + } catch (e) { + console.error(e); + } + } + }, + + /** + * Records a successful BITS update download using Telemetry. + * + * @param aIsComplete + * If true the histogram is for a patch type complete, if false the + * histogram is for a patch type partial. This will determine the + * histogram id out of the following histogram ids: + * UPDATE_BITS_RESULT_COMPLETE + * UPDATE_BITS_RESULT_PARTIAL + */ + pingBitsSuccess: function UT_pingBitsSuccess(aIsComplete) { + if (AppConstants.platform != "win") { + console.error( + "Warning: Attempted to submit BITS telemetry on a " + + "non-Windows platform" + ); + return; + } + this._pingBitsResult(aIsComplete, Ci.nsIBits.ERROR_TYPE_SUCCESS); + }, + + /** + * This is the helper function that does all the work for pingBitsError and + * pingBitsSuccess. It submits a telemetry ping indicating the result of the + * BITS update download. + * + * @param aIsComplete + * If true the histogram is for a patch type complete, if false the + * histogram is for a patch type partial. This will determine the + * histogram id out of the following histogram ids: + * UPDATE_BITS_RESULT_COMPLETE + * UPDATE_BITS_RESULT_PARTIAL + * @param aResultType + * The result code. This will be one of the ERROR_TYPE_* values defined + * in the nsIBits interface. + */ + _pingBitsResult: function UT_pingBitsResult(aIsComplete, aResultType) { + let patchType; + if (aIsComplete) { + patchType = this.PATCH_COMPLETE; + } else { + patchType = this.PATCH_PARTIAL; + } + try { + let id = "UPDATE_BITS_RESULT_" + patchType; + Services.telemetry.getHistogramById(id).add(aResultType); + } catch (e) { + console.error(e); + } + }, + + /** + * Submit the interval in days since the last notification for this background + * update check or a boolean if the last notification is in the future. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_INVALID_LASTUPDATETIME_EXTERNAL + * UPDATE_INVALID_LASTUPDATETIME_NOTIFY + * UPDATE_INVALID_LASTUPDATETIME_SUBSEQUENT + * UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL + * UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY + * UPDATE_LAST_NOTIFY_INTERVAL_DAYS_SUBSEQUENT + */ + pingLastUpdateTime: function UT_pingLastUpdateTime(aSuffix) { + const PREF_APP_UPDATE_LASTUPDATETIME = + "app.update.lastUpdateTime.background-update-timer"; + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LASTUPDATETIME)) { + let lastUpdateTimeSeconds = Services.prefs.getIntPref( + PREF_APP_UPDATE_LASTUPDATETIME + ); + if (lastUpdateTimeSeconds) { + let currentTimeSeconds = Math.round(Date.now() / 1000); + if (lastUpdateTimeSeconds > currentTimeSeconds) { + try { + let id = "UPDATE_INVALID_LASTUPDATETIME_" + aSuffix; + // count type histogram + Services.telemetry.getHistogramById(id).add(); + } catch (e) { + console.error(e); + } + } else { + let intervalDays = + (currentTimeSeconds - lastUpdateTimeSeconds) / (60 * 60 * 24); + try { + let id = "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_" + aSuffix; + // exponential type histogram + Services.telemetry.getHistogramById(id).add(intervalDays); + } catch (e) { + console.error(e); + } + } + } + } + }, + + /** + * Submit a telemetry ping for a boolean type histogram that indicates if the + * service is installed and a telemetry ping for a boolean type histogram that + * indicates if the service was at some point installed and is now + * uninstalled. + * + * @param aSuffix + * The histogram id suffix for histogram IDs: + * UPDATE_SERVICE_INSTALLED_EXTERNAL + * UPDATE_SERVICE_INSTALLED_NOTIFY + * UPDATE_SERVICE_INSTALLED_SUBSEQUENT + * UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL + * UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY + * UPDATE_SERVICE_MANUALLY_UNINSTALLED_SUBSEQUENT + * @param aInstalled + * Whether the service is installed. + */ + pingServiceInstallStatus: function UT_PSIS(aSuffix, aInstalled) { + // Report the error but don't throw since it is more important to + // successfully update than to throw. + if (!("@mozilla.org/windows-registry-key;1" in Cc)) { + console.error(Cr.NS_ERROR_NOT_AVAILABLE); + return; + } + + try { + let id = "UPDATE_SERVICE_INSTALLED_" + aSuffix; + // boolean type histogram + Services.telemetry.getHistogramById(id).add(aInstalled); + } catch (e) { + console.error(e); + } + + let attempted = 0; + try { + let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + wrk.open( + wrk.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\MaintenanceService", + wrk.ACCESS_READ | wrk.WOW64_64 + ); + // Was the service at some point installed, but is now uninstalled? + attempted = wrk.readIntValue("Attempted"); + wrk.close(); + } catch (e) { + // Since this will throw if the registry key doesn't exist (e.g. the + // service has never been installed) don't report an error. + } + + try { + let id = "UPDATE_SERVICE_MANUALLY_UNINSTALLED_" + aSuffix; + if (!aInstalled && attempted) { + // count type histogram + Services.telemetry.getHistogramById(id).add(); + } + } catch (e) { + console.error(e); + } + }, + + /** + * Submit a telemetry ping for a count type histogram when the expected value + * does not equal the boolean value of a pref or if the pref isn't present + * when the expected value does not equal default value. This lessens the + * amount of data submitted to telemetry. + * + * @param aID + * The histogram ID to report to. + * @param aPref + * The preference to check. + * @param aDefault + * The default value when the preference isn't present. + * @param aExpected (optional) + * If specified and the value is the same as the value that will be + * added the value won't be added to telemetry. + */ + pingBoolPref: function UT_pingBoolPref(aID, aPref, aDefault, aExpected) { + try { + let val = aDefault; + if (Services.prefs.getPrefType(aPref) != Ci.nsIPrefBranch.PREF_INVALID) { + val = Services.prefs.getBoolPref(aPref); + } + if (val != aExpected) { + // count type histogram + Services.telemetry.getHistogramById(aID).add(); + } + } catch (e) { + console.error(e); + } + }, + + /** + * Submit a telemetry ping for a histogram with the integer value of a + * preference when it is not the expected value or the default value when it + * is not the expected value. This lessens the amount of data submitted to + * telemetry. + * + * @param aID + * The histogram ID to report to. + * @param aPref + * The preference to check. + * @param aDefault + * The default value when the pref is not set. + * @param aExpected (optional) + * If specified and the value is the same as the value that will be + * added the value won't be added to telemetry. + */ + pingIntPref: function UT_pingIntPref(aID, aPref, aDefault, aExpected) { + try { + let val = aDefault; + if (Services.prefs.getPrefType(aPref) != Ci.nsIPrefBranch.PREF_INVALID) { + val = Services.prefs.getIntPref(aPref); + } + if (aExpected === undefined || val != aExpected) { + // enumerated or exponential type histogram + Services.telemetry.getHistogramById(aID).add(val); + } + } catch (e) { + console.error(e); + } + }, + + /** + * Submit a telemetry ping for all histogram types that take a single + * parameter to the telemetry add function and the count type histogram when + * the aExpected parameter is specified. If the aExpected parameter is + * specified and it equals the value specified by the aValue + * parameter the telemetry submission will be skipped. + * + * @param aID + * The histogram ID to report to. + * @param aValue + * The value to add when aExpected is not defined or the value to + * check if it is equal to when aExpected is defined. + * @param aExpected (optional) + * If specified and the value is the same as the value specified by + * aValue parameter the submission will be skipped. + */ + pingGeneric: function UT_pingGeneric(aID, aValue, aExpected) { + try { + if (aExpected === undefined) { + Services.telemetry.getHistogramById(aID).add(aValue); + } else if (aValue != aExpected) { + // count type histogram + Services.telemetry.getHistogramById(aID).add(); + } + } catch (e) { + console.error(e); + } + }, + + /** + * Valid keys for the update.moveresult scalar. + */ + MOVE_RESULT_SUCCESS: "SUCCESS", + MOVE_RESULT_UNKNOWN_FAILURE: "UNKNOWN_FAILURE", + + /** + * Reports the passed result of attempting to move the downloading update + * into the ready update directory. + */ + pingMoveResult: function UT_pingMoveResult(aResult) { + Services.telemetry.keyedScalarAdd("update.move_result", aResult, 1); + }, + + pingSuppressPrompts: function UT_pingSuppressPrompts() { + try { + let val = Services.prefs.getBoolPref("app.update.suppressPrompts", false); + if (val === true) { + Services.telemetry.scalarSet("update.suppress_prompts", true); + } + } catch (e) { + console.error(e); + } + }, + + pingPinPolicy: function UT_pingPinPolicy(updatePin) { + try { + Services.telemetry.scalarSet("update.version_pin", updatePin); + } catch (e) { + console.error(e); + } + }, +}; +Object.freeze(AUSTLMY); diff --git a/toolkit/mozapps/update/common/certificatecheck.cpp b/toolkit/mozapps/update/common/certificatecheck.cpp new file mode 100644 index 0000000000..8c53fa5fd6 --- /dev/null +++ b/toolkit/mozapps/update/common/certificatecheck.cpp @@ -0,0 +1,241 @@ +/* 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/. */ + +#include <stdio.h> +#include <stdlib.h> +#include <windows.h> +#include <softpub.h> +#include <wintrust.h> + +#include "certificatecheck.h" +#include "updatecommon.h" + +static const int ENCODING = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; + +/** + * Checks to see if a file stored at filePath matches the specified info. + * + * @param filePath The PE file path to check + * @param infoToMatch The acceptable information to match + * @return ERROR_SUCCESS if successful, ERROR_NOT_FOUND if the info + * does not match, or the last error otherwise. + */ +DWORD +CheckCertificateForPEFile(LPCWSTR filePath, CertificateCheckInfo& infoToMatch) { + HCERTSTORE certStore = nullptr; + HCRYPTMSG cryptMsg = nullptr; + PCCERT_CONTEXT certContext = nullptr; + PCMSG_SIGNER_INFO signerInfo = nullptr; + DWORD lastError = ERROR_SUCCESS; + + // Get the HCERTSTORE and HCRYPTMSG from the signed file. + DWORD encoding, contentType, formatType; + BOOL result = CryptQueryObject( + CERT_QUERY_OBJECT_FILE, filePath, + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, CERT_QUERY_CONTENT_FLAG_ALL, + 0, &encoding, &contentType, &formatType, &certStore, &cryptMsg, nullptr); + if (!result) { + lastError = GetLastError(); + LOG_WARN(("CryptQueryObject failed. (%lu)", lastError)); + goto cleanup; + } + + // Pass in nullptr to get the needed signer information size. + DWORD signerInfoSize; + result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0, nullptr, + &signerInfoSize); + if (!result) { + lastError = GetLastError(); + LOG_WARN(("CryptMsgGetParam failed. (%lu)", lastError)); + goto cleanup; + } + + // Allocate the needed size for the signer information. + signerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, signerInfoSize); + if (!signerInfo) { + lastError = GetLastError(); + LOG_WARN(("Unable to allocate memory for Signer Info. (%lu)", lastError)); + goto cleanup; + } + + // Get the signer information (PCMSG_SIGNER_INFO). + // In particular we want the issuer and serial number. + result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0, + (PVOID)signerInfo, &signerInfoSize); + if (!result) { + lastError = GetLastError(); + LOG_WARN(("CryptMsgGetParam failed. (%lu)", lastError)); + goto cleanup; + } + + // Search for the signer certificate in the certificate store. + CERT_INFO certInfo; + certInfo.Issuer = signerInfo->Issuer; + certInfo.SerialNumber = signerInfo->SerialNumber; + certContext = + CertFindCertificateInStore(certStore, ENCODING, 0, CERT_FIND_SUBJECT_CERT, + (PVOID)&certInfo, nullptr); + if (!certContext) { + lastError = GetLastError(); + LOG_WARN(("CertFindCertificateInStore failed. (%lu)", lastError)); + goto cleanup; + } + + if (!DoCertificateAttributesMatch(certContext, infoToMatch)) { + lastError = ERROR_NOT_FOUND; + LOG_WARN(("Certificate did not match issuer or name. (%lu)", lastError)); + goto cleanup; + } + +cleanup: + if (signerInfo) { + LocalFree(signerInfo); + } + if (certContext) { + CertFreeCertificateContext(certContext); + } + if (certStore) { + CertCloseStore(certStore, 0); + } + if (cryptMsg) { + CryptMsgClose(cryptMsg); + } + return lastError; +} + +/** + * Checks to see if a file stored at filePath matches the specified info. + * + * @param certContext The certificate context of the file + * @param infoToMatch The acceptable information to match + * @return FALSE if the info does not match or if any error occurs in the check + */ +BOOL DoCertificateAttributesMatch(PCCERT_CONTEXT certContext, + CertificateCheckInfo& infoToMatch) { + DWORD dwData; + LPWSTR szName = nullptr; + + if (infoToMatch.issuer) { + // Pass in nullptr to get the needed size of the issuer buffer. + dwData = CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, + CERT_NAME_ISSUER_FLAG, nullptr, nullptr, 0); + + if (!dwData) { + LOG_WARN(("CertGetNameString failed. (%lu)", GetLastError())); + return FALSE; + } + + // Allocate memory for Issuer name buffer. + szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR)); + if (!szName) { + LOG_WARN(("Unable to allocate memory for issuer name. (%lu)", + GetLastError())); + return FALSE; + } + + // Get Issuer name. + if (!CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, + CERT_NAME_ISSUER_FLAG, nullptr, szName, dwData)) { + LOG_WARN(("CertGetNameString failed. (%lu)", GetLastError())); + LocalFree(szName); + return FALSE; + } + + // If the issuer does not match, return a failure. + if (!infoToMatch.issuer || wcscmp(szName, infoToMatch.issuer)) { + LocalFree(szName); + return FALSE; + } + + LocalFree(szName); + szName = nullptr; + } + + if (infoToMatch.name) { + // Pass in nullptr to get the needed size of the name buffer. + dwData = CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, + nullptr, nullptr, 0); + if (!dwData) { + LOG_WARN(("CertGetNameString failed. (%lu)", GetLastError())); + return FALSE; + } + + // Allocate memory for the name buffer. + szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR)); + if (!szName) { + LOG_WARN(("Unable to allocate memory for subject name. (%lu)", + GetLastError())); + return FALSE; + } + + // Obtain the name. + if (!(CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, + nullptr, szName, dwData))) { + LOG_WARN(("CertGetNameString failed. (%lu)", GetLastError())); + LocalFree(szName); + return FALSE; + } + + // If the issuer does not match, return a failure. + if (!infoToMatch.name || wcscmp(szName, infoToMatch.name)) { + LocalFree(szName); + return FALSE; + } + + // We have a match! + LocalFree(szName); + } + + // If there were any errors we would have aborted by now. + return TRUE; +} + +/** + * Verifies the trust of the specified file path. + * + * @param filePath The file path to check. + * @return ERROR_SUCCESS if successful, or the last error code otherwise. + */ +DWORD +VerifyCertificateTrustForFile(LPCWSTR filePath) { + // Setup the file to check. + WINTRUST_FILE_INFO fileToCheck; + ZeroMemory(&fileToCheck, sizeof(fileToCheck)); + fileToCheck.cbStruct = sizeof(WINTRUST_FILE_INFO); + fileToCheck.pcwszFilePath = filePath; + + // Setup what to check, we want to check it is signed and trusted. + WINTRUST_DATA trustData; + ZeroMemory(&trustData, sizeof(trustData)); + trustData.cbStruct = sizeof(trustData); + trustData.pPolicyCallbackData = nullptr; + trustData.pSIPClientData = nullptr; + trustData.dwUIChoice = WTD_UI_NONE; + trustData.fdwRevocationChecks = WTD_REVOKE_NONE; + trustData.dwUnionChoice = WTD_CHOICE_FILE; + trustData.dwStateAction = 0; + trustData.hWVTStateData = nullptr; + trustData.pwszURLReference = nullptr; + // no UI + trustData.dwUIContext = 0; + trustData.pFile = &fileToCheck; + + GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2; + // Check if the file is signed by something that is trusted. + LONG ret = WinVerifyTrust(nullptr, &policyGUID, &trustData); + if (ERROR_SUCCESS == ret) { + // The hash that represents the subject is trusted and there were no + // verification errors. No publisher nor time stamp chain errors. + LOG(("The file \"%ls\" is signed and the signature was verified.", + filePath)); + return ERROR_SUCCESS; + } + + DWORD lastError = GetLastError(); + LOG_WARN( + ("There was an error validating trust of the certificate for file" + " \"%ls\". Returned: %ld. (%lu)", + filePath, ret, lastError)); + return ret; +} diff --git a/toolkit/mozapps/update/common/certificatecheck.h b/toolkit/mozapps/update/common/certificatecheck.h new file mode 100644 index 0000000000..af9f8456df --- /dev/null +++ b/toolkit/mozapps/update/common/certificatecheck.h @@ -0,0 +1,22 @@ +/* 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/. */ + +#ifndef _CERTIFICATECHECK_H_ +#define _CERTIFICATECHECK_H_ + +#include <windows.h> +#include <wincrypt.h> + +struct CertificateCheckInfo { + LPCWSTR name; + LPCWSTR issuer; +}; + +BOOL DoCertificateAttributesMatch(PCCERT_CONTEXT pCertContext, + CertificateCheckInfo& infoToMatch); +DWORD VerifyCertificateTrustForFile(LPCWSTR filePath); +DWORD CheckCertificateForPEFile(LPCWSTR filePath, + CertificateCheckInfo& infoToMatch); + +#endif diff --git a/toolkit/mozapps/update/common/commonupdatedir.cpp b/toolkit/mozapps/update/common/commonupdatedir.cpp new file mode 100644 index 0000000000..0ba9fcef94 --- /dev/null +++ b/toolkit/mozapps/update/common/commonupdatedir.cpp @@ -0,0 +1,723 @@ +/* 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/. */ + +/* + * This file does not use many of the features Firefox provides such as + * nsAString and nsIFile because code in this file will be included not only + * in Firefox, but also in the Mozilla Maintenance Service, the Mozilla + * Maintenance Service installer, and TestAUSHelper. + */ + +#include <cinttypes> +#include <cwchar> +#include <string> +#include "city.h" +#include "commonupdatedir.h" +#include "updatedefines.h" + +#ifdef XP_WIN +# include <accctrl.h> +# include <aclapi.h> +# include <cstdarg> +# include <errno.h> +# include <objbase.h> +# include <shellapi.h> +# include <shlobj.h> +# include <strsafe.h> +# include <winerror.h> +# include "nsWindowsHelpers.h" +# include "updateutils_win.h" +#endif + +#ifdef XP_WIN +// This is the name of the old update directory +// (i.e. C:\ProgramData\<OLD_ROOT_UPDATE_DIR_NAME>) +# define OLD_ROOT_UPDATE_DIR_NAME "Mozilla" +// This is the name of the current update directory +// (i.e. C:\ProgramData\<ROOT_UPDATE_DIR_NAME>) +// It is really important that we properly set the permissions on this +// directory at creation time. Thus, it is really important that this code be +// the creator of this directory. We had many problems with the old update +// directory having been previously created by old versions of Firefox. To avoid +// this problem in the future, we are including a UUID in the root update +// directory name to attempt to ensure that it will be created by this code and +// won't already exist with the wrong permissions. +# define ROOT_UPDATE_DIR_NAME "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38" +// This describes the directory between the "Mozilla" directory and the install +// path hash (i.e. C:\ProgramData\Mozilla\<UPDATE_PATH_MID_DIR_NAME>\<hash>) +# define UPDATE_PATH_MID_DIR_NAME "updates" + +enum class WhichUpdateDir { + CurrentUpdateDir, + UnmigratedUpdateDir, +}; + +/** + * This is a very simple string class. + * + * This class has some substantial limitations for the sake of simplicity. It + * has no support whatsoever for modifying a string that already has data. There + * is, therefore, no append function and no support for automatically resizing + * strings. + * + * Error handling is also done in a slightly unusual manner. If there is ever + * a failure allocating or assigning to a string, it will do the simplest + * possible recovery: truncate itself to 0-length. + * This coupled with the fact that the length is cached means that an effective + * method of error checking is to attempt assignment and then check the length + * of the result. + */ +class SimpleAutoString { + private: + size_t mLength; + // Unique pointer frees the buffer when the class is deleted or goes out of + // scope. + mozilla::UniquePtr<wchar_t[]> mString; + + /** + * Allocates enough space to store a string of the specified length. + */ + bool AllocLen(size_t len) { + mString = mozilla::MakeUnique<wchar_t[]>(len + 1); + return mString.get() != nullptr; + } + + /** + * Allocates a buffer of the size given. + */ + bool AllocSize(size_t size) { + mString = mozilla::MakeUnique<wchar_t[]>(size); + return mString.get() != nullptr; + } + + public: + SimpleAutoString() : mLength(0) {} + + /* + * Allocates enough space for a string of the given length and formats it as + * an empty string. + */ + bool AllocEmpty(size_t len) { + bool success = AllocLen(len); + Truncate(); + return success; + } + + /** + * These functions can potentially return null if no buffer has yet been + * allocated. After changing a string retrieved with MutableString, the Check + * method should be called to synchronize other members (ex: mLength) with the + * new buffer. + */ + wchar_t* MutableString() { return mString.get(); } + const wchar_t* String() const { return mString.get(); } + + size_t Length() const { return mLength; } + + /** + * This method should be called after manually changing the string's buffer + * via MutableString to synchronize other members (ex: mLength) with the + * new buffer. + * Returns true if the string is now in a valid state. + */ + bool Check() { + mLength = wcslen(mString.get()); + return true; + } + + void SwapBufferWith(mozilla::UniquePtr<wchar_t[]>& other) { + mString.swap(other); + if (mString) { + mLength = wcslen(mString.get()); + } else { + mLength = 0; + } + } + + void Swap(SimpleAutoString& other) { + mString.swap(other.mString); + size_t newLength = other.mLength; + other.mLength = mLength; + mLength = newLength; + } + + /** + * Truncates the string to the length specified. This must not be greater than + * or equal to the size of the string's buffer. + */ + void Truncate(size_t len = 0) { + if (len > mLength) { + return; + } + mLength = len; + if (mString) { + mString.get()[len] = L'\0'; + } + } + + /** + * Assigns a string and ensures that the resulting string is valid and has its + * length set properly. + * + * Note that although other similar functions in this class take length, this + * function takes buffer size instead because it is intended to be follow the + * input convention of sprintf. + * + * Returns the new length, which will be 0 if there was any failure. + * + * This function does no allocation or reallocation. If the buffer is not + * large enough to hold the new string, the call will fail. + */ + size_t AssignSprintf(size_t bufferSize, const wchar_t* format, ...) { + va_list ap; + va_start(ap, format); + size_t returnValue = AssignVsprintf(bufferSize, format, ap); + va_end(ap); + return returnValue; + } + /** + * Same as the above, but takes a va_list like vsprintf does. + */ + size_t AssignVsprintf(size_t bufferSize, const wchar_t* format, va_list ap) { + if (!mString) { + Truncate(); + return 0; + } + + int charsWritten = vswprintf(mString.get(), bufferSize, format, ap); + if (charsWritten < 0 || static_cast<size_t>(charsWritten) >= bufferSize) { + // charsWritten does not include the null terminator. If charsWritten is + // equal to the buffer size, we do not have a null terminator nor do we + // have room for one. + Truncate(); + return 0; + } + mString.get()[charsWritten] = L'\0'; + + mLength = charsWritten; + return mLength; + } + + /** + * Allocates enough space for the string and assigns a value to it with + * sprintf. Takes, as an argument, the maximum length that the string is + * expected to use (which, after adding 1 for the null byte, is the amount of + * space that will be allocated). + * + * Returns the new length, which will be 0 on any failure. + */ + size_t AllocAndAssignSprintf(size_t maxLength, const wchar_t* format, ...) { + if (!AllocLen(maxLength)) { + Truncate(); + return 0; + } + va_list ap; + va_start(ap, format); + size_t charsWritten = AssignVsprintf(maxLength + 1, format, ap); + va_end(ap); + return charsWritten; + } + + /* + * Allocates enough for the formatted text desired. Returns maximum storable + * length of a string in the allocated buffer on success, or 0 on failure. + */ + size_t AllocFromScprintf(const wchar_t* format, ...) { + va_list ap; + va_start(ap, format); + size_t returnValue = AllocFromVscprintf(format, ap); + va_end(ap); + return returnValue; + } + /** + * Same as the above, but takes a va_list like vscprintf does. + */ + size_t AllocFromVscprintf(const wchar_t* format, va_list ap) { + int len = _vscwprintf(format, ap); + if (len < 0) { + Truncate(); + return 0; + } + if (!AllocEmpty(len)) { + // AllocEmpty will Truncate, no need to call it here. + return 0; + } + return len; + } + + /** + * Automatically determines how much space is necessary, allocates that much + * for the string, and assigns the data using swprintf. Returns the resulting + * length of the string, which will be 0 if the function fails. + */ + size_t AutoAllocAndAssignSprintf(const wchar_t* format, ...) { + va_list ap; + va_start(ap, format); + size_t len = AllocFromVscprintf(format, ap); + va_end(ap); + if (len == 0) { + // AllocFromVscprintf will Truncate, no need to call it here. + return 0; + } + + va_start(ap, format); + size_t charsWritten = AssignVsprintf(len + 1, format, ap); + va_end(ap); + + if (len != charsWritten) { + Truncate(); + return 0; + } + return charsWritten; + } + + /** + * The following CopyFrom functions take various types of strings, allocate + * enough space to hold them, and then copy them into that space. + * + * They return an HRESULT that should be interpreted with the SUCCEEDED or + * FAILED macro. + */ + HRESULT CopyFrom(const wchar_t* src) { + mLength = wcslen(src); + if (!AllocLen(mLength)) { + Truncate(); + return E_OUTOFMEMORY; + } + HRESULT hrv = StringCchCopyW(mString.get(), mLength + 1, src); + if (FAILED(hrv)) { + Truncate(); + } + return hrv; + } + HRESULT CopyFrom(const SimpleAutoString& src) { + if (!src.mString) { + Truncate(); + return S_OK; + } + mLength = src.mLength; + if (!AllocLen(mLength)) { + Truncate(); + return E_OUTOFMEMORY; + } + HRESULT hrv = StringCchCopyW(mString.get(), mLength + 1, src.mString.get()); + if (FAILED(hrv)) { + Truncate(); + } + return hrv; + } + HRESULT CopyFrom(const char* src) { + int bufferSize = + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, nullptr, 0); + if (bufferSize == 0) { + Truncate(); + return HRESULT_FROM_WIN32(GetLastError()); + } + if (!AllocSize(bufferSize)) { + Truncate(); + return E_OUTOFMEMORY; + } + int charsWritten = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, + -1, mString.get(), bufferSize); + if (charsWritten == 0) { + Truncate(); + return HRESULT_FROM_WIN32(GetLastError()); + } else if (charsWritten != bufferSize) { + Truncate(); + return E_FAIL; + } + mLength = charsWritten - 1; + return S_OK; + } + + bool StartsWith(const SimpleAutoString& prefix) const { + if (!mString) { + return (prefix.mLength == 0); + } + if (!prefix.mString) { + return true; + } + if (prefix.mLength > mLength) { + return false; + } + return (wcsncmp(mString.get(), prefix.mString.get(), prefix.mLength) == 0); + } +}; + +// Deleter for use with UniquePtr +struct CoTaskMemFreeDeleter { + void operator()(void* aPtr) { ::CoTaskMemFree(aPtr); } +}; + +/** + * A lot of data goes into constructing an ACL and security attributes, and the + * Windows documentation does not make it very clear what can be safely freed + * after these objects are constructed. This struct holds all of the + * construction data in one place so that it can be passed around and freed + * properly. + */ +struct AutoPerms { + SID_IDENTIFIER_AUTHORITY sidIdentifierAuthority; + UniqueSidPtr usersSID; + UniqueSidPtr adminsSID; + UniqueSidPtr systemSID; + EXPLICIT_ACCESS_W ea[3]; + mozilla::UniquePtr<ACL, LocalFreeDeleter> acl; + mozilla::UniquePtr<uint8_t[]> securityDescriptorBuffer; + PSECURITY_DESCRIPTOR securityDescriptor; + SECURITY_ATTRIBUTES securityAttributes; +}; + +static bool GetCachedHash(const char16_t* installPath, HKEY rootKey, + const SimpleAutoString& regPath, + mozilla::UniquePtr<NS_tchar[]>& result); +static HRESULT GetUpdateDirectory(const wchar_t* installPath, + WhichUpdateDir whichDir, + mozilla::UniquePtr<wchar_t[]>& result); +static HRESULT GeneratePermissions(AutoPerms& result); +static HRESULT MakeDir(const SimpleAutoString& path, const AutoPerms& perms); +#endif // XP_WIN + +/** + * Returns a hash of the install path, suitable for uniquely identifying the + * particular Firefox installation that is running. + * + * This function includes a compatibility mode that should NOT be used except by + * GetUserUpdateDirectory. Previous implementations of this function could + * return a value inconsistent with what our installer would generate. When the + * update directory was migrated, this function was re-implemented to return + * values consistent with those generated by the installer. The compatibility + * mode is retained only so that we can properly get the old update directory + * when migrating it. + * + * @param installPath + * The null-terminated path to the installation directory (i.e. the + * directory that contains the binary). Must not be null. The path must + * not include a trailing slash. + * @param result + * The out parameter that will be set to contain the resulting hash. + * The value is wrapped in a UniquePtr to make cleanup easier on the + * caller. + * @return true if successful and false otherwise. + */ +bool GetInstallHash(const char16_t* installPath, + mozilla::UniquePtr<NS_tchar[]>& result) { + MOZ_ASSERT(installPath != nullptr, + "Install path must not be null in GetInstallHash"); + + size_t pathSize = + std::char_traits<char16_t>::length(installPath) * sizeof(*installPath); + uint64_t hash = + CityHash64(reinterpret_cast<const char*>(installPath), pathSize); + + size_t hashStrSize = sizeof(hash) * 2 + 1; // 2 hex digits per byte + null + result = mozilla::MakeUnique<NS_tchar[]>(hashStrSize); + int charsWritten = + NS_tsnprintf(result.get(), hashStrSize, NS_T("%") NS_T(PRIX64), hash); + return !(charsWritten < 1 || + static_cast<size_t>(charsWritten) > hashStrSize - 1); +} + +#ifdef XP_WIN +/** + * Returns true if the registry key was successfully found and read into result. + */ +static bool GetCachedHash(const char16_t* installPath, HKEY rootKey, + const SimpleAutoString& regPath, + mozilla::UniquePtr<NS_tchar[]>& result) { + // Find the size of the string we are reading before we read it so we can + // allocate space. + unsigned long bufferSize; + LSTATUS lrv = RegGetValueW(rootKey, regPath.String(), + reinterpret_cast<const wchar_t*>(installPath), + RRF_RT_REG_SZ, nullptr, nullptr, &bufferSize); + if (lrv != ERROR_SUCCESS) { + return false; + } + result = mozilla::MakeUnique<NS_tchar[]>(bufferSize); + // Now read the actual value from the registry. + lrv = RegGetValueW(rootKey, regPath.String(), + reinterpret_cast<const wchar_t*>(installPath), + RRF_RT_REG_SZ, nullptr, result.get(), &bufferSize); + return (lrv == ERROR_SUCCESS); +} + +/** + * Returns the update directory path. The update directory needs to have + * different permissions from the default, so we don't really want anyone using + * the path without the directory already being created with the correct + * permissions. Therefore, this function also ensures that the base directory + * that needs permissions set already exists. If it does not exist, it is + * created with the needed permissions. + * The desired permissions give Full Control to SYSTEM, Administrators, and + * Users. + * + * @param installPath + * Must be the null-terminated path to the installation directory (i.e. + * the directory that contains the binary). The path must not include a + * trailing slash. + * @param result + * The out parameter that will be set to contain the resulting path. + * The value is wrapped in a UniquePtr to make cleanup easier on the + * caller. + * + * @return An HRESULT that should be tested with SUCCEEDED or FAILED. + */ +HRESULT +GetCommonUpdateDirectory(const wchar_t* installPath, + mozilla::UniquePtr<wchar_t[]>& result) { + return GetUpdateDirectory(installPath, WhichUpdateDir::CurrentUpdateDir, + result); +} + +/** + * This function is identical to the function above except that it gets the + * "old" (pre-migration) update directory. + * + * The other difference is that this function does not create the directory. + */ +HRESULT +GetOldUpdateDirectory(const wchar_t* installPath, + mozilla::UniquePtr<wchar_t[]>& result) { + return GetUpdateDirectory(installPath, WhichUpdateDir::UnmigratedUpdateDir, + result); +} + +/** + * This is a version of the GetCommonUpdateDirectory that can be called from + * Rust. + * The result parameter must be a valid pointer to a buffer of length + * MAX_PATH + 1 + */ +extern "C" HRESULT get_common_update_directory(const wchar_t* installPath, + wchar_t* result) { + mozilla::UniquePtr<wchar_t[]> uniqueResult; + HRESULT hr = GetCommonUpdateDirectory(installPath, uniqueResult); + if (FAILED(hr)) { + return hr; + } + return StringCchCopyW(result, MAX_PATH + 1, uniqueResult.get()); +} + +/** + * This is a helper function that does all of the work for + * GetCommonUpdateDirectory and GetUserUpdateDirectory. + * + * For information on the parameters and return value, see + * GetCommonUpdateDirectory. + */ +static HRESULT GetUpdateDirectory(const wchar_t* installPath, + WhichUpdateDir whichDir, + mozilla::UniquePtr<wchar_t[]>& result) { + MOZ_ASSERT(installPath != nullptr, + "Install path must not be null in GetUpdateDirectory"); + + AutoPerms perms; + HRESULT hrv = GeneratePermissions(perms); + if (FAILED(hrv)) { + return hrv; + } + + PWSTR baseDirParentPath; + hrv = SHGetKnownFolderPath(FOLDERID_ProgramData, KF_FLAG_CREATE, nullptr, + &baseDirParentPath); + // Free baseDirParentPath when it goes out of scope. + mozilla::UniquePtr<wchar_t, CoTaskMemFreeDeleter> baseDirParentPathUnique( + baseDirParentPath); + if (FAILED(hrv)) { + return hrv; + } + + SimpleAutoString baseDir; + if (whichDir == WhichUpdateDir::UnmigratedUpdateDir) { + const wchar_t baseDirLiteral[] = NS_T(OLD_ROOT_UPDATE_DIR_NAME); + hrv = baseDir.CopyFrom(baseDirLiteral); + } else { + const wchar_t baseDirLiteral[] = NS_T(ROOT_UPDATE_DIR_NAME); + hrv = baseDir.CopyFrom(baseDirLiteral); + } + if (FAILED(hrv)) { + return hrv; + } + + // Generate the base path + // (C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38) + SimpleAutoString basePath; + size_t basePathLen = + wcslen(baseDirParentPath) + 1 /* path separator */ + baseDir.Length(); + basePath.AllocAndAssignSprintf(basePathLen, L"%s\\%s", baseDirParentPath, + baseDir.String()); + if (basePath.Length() != basePathLen) { + return E_FAIL; + } + + if (whichDir == WhichUpdateDir::CurrentUpdateDir) { + hrv = MakeDir(basePath, perms); + if (FAILED(hrv)) { + return hrv; + } + } + + // Generate what we are going to call the mid-path + // (C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\updates) + const wchar_t midPathDirName[] = NS_T(UPDATE_PATH_MID_DIR_NAME); + size_t midPathLen = + basePath.Length() + 1 /* path separator */ + wcslen(midPathDirName); + SimpleAutoString midPath; + midPath.AllocAndAssignSprintf(midPathLen, L"%s\\%s", basePath.String(), + midPathDirName); + if (midPath.Length() != midPathLen) { + return E_FAIL; + } + + mozilla::UniquePtr<NS_tchar[]> hash; + + // The Windows installer caches this hash value in the registry + bool gotHash = false; + SimpleAutoString regPath; + regPath.AutoAllocAndAssignSprintf(L"SOFTWARE\\Mozilla\\%S\\TaskBarIDs", + MOZ_APP_BASENAME); + if (regPath.Length() != 0) { + gotHash = GetCachedHash(reinterpret_cast<const char16_t*>(installPath), + HKEY_LOCAL_MACHINE, regPath, hash); + if (!gotHash) { + gotHash = GetCachedHash(reinterpret_cast<const char16_t*>(installPath), + HKEY_CURRENT_USER, regPath, hash); + } + } + // If we couldn't get it out of the registry, we'll just have to regenerate + // it. + if (!gotHash) { + bool success = + GetInstallHash(reinterpret_cast<const char16_t*>(installPath), hash); + if (!success) { + return E_FAIL; + } + } + + size_t updatePathLen = + midPath.Length() + 1 /* path separator */ + wcslen(hash.get()); + SimpleAutoString updatePath; + updatePath.AllocAndAssignSprintf(updatePathLen, L"%s\\%s", midPath.String(), + hash.get()); + if (updatePath.Length() != updatePathLen) { + return E_FAIL; + } + + updatePath.SwapBufferWith(result); + return S_OK; +} + +/** + * Generates the permission set that we want to be applied to the update + * directory and its contents. Returns the permissions data via the result + * outparam. + * + * These are also the permissions that will be used to check that file + * permissions are correct. + */ +static HRESULT GeneratePermissions(AutoPerms& result) { + result.sidIdentifierAuthority = SECURITY_NT_AUTHORITY; + ZeroMemory(&result.ea, sizeof(result.ea)); + + // Make Users group SID and add it to the Explicit Access List. + PSID usersSID = nullptr; + BOOL success = AllocateAndInitializeSid( + &result.sidIdentifierAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_USERS, 0, 0, 0, 0, 0, 0, &usersSID); + result.usersSID.reset(usersSID); + if (!success) { + return HRESULT_FROM_WIN32(GetLastError()); + } + result.ea[0].grfAccessPermissions = FILE_ALL_ACCESS; + result.ea[0].grfAccessMode = SET_ACCESS; + result.ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; + result.ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; + result.ea[0].Trustee.TrusteeType = TRUSTEE_IS_GROUP; + result.ea[0].Trustee.ptstrName = static_cast<LPWSTR>(usersSID); + + // Make Administrators group SID and add it to the Explicit Access List. + PSID adminsSID = nullptr; + success = AllocateAndInitializeSid( + &result.sidIdentifierAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminsSID); + result.adminsSID.reset(adminsSID); + if (!success) { + return HRESULT_FROM_WIN32(GetLastError()); + } + result.ea[1].grfAccessPermissions = FILE_ALL_ACCESS; + result.ea[1].grfAccessMode = SET_ACCESS; + result.ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; + result.ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; + result.ea[1].Trustee.TrusteeType = TRUSTEE_IS_GROUP; + result.ea[1].Trustee.ptstrName = static_cast<LPWSTR>(adminsSID); + + // Make SYSTEM user SID and add it to the Explicit Access List. + PSID systemSID = nullptr; + success = AllocateAndInitializeSid(&result.sidIdentifierAuthority, 1, + SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0, + 0, 0, &systemSID); + result.systemSID.reset(systemSID); + if (!success) { + return HRESULT_FROM_WIN32(GetLastError()); + } + result.ea[2].grfAccessPermissions = FILE_ALL_ACCESS; + result.ea[2].grfAccessMode = SET_ACCESS; + result.ea[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; + result.ea[2].Trustee.TrusteeForm = TRUSTEE_IS_SID; + result.ea[2].Trustee.TrusteeType = TRUSTEE_IS_USER; + result.ea[2].Trustee.ptstrName = static_cast<LPWSTR>(systemSID); + + PACL acl = nullptr; + DWORD drv = SetEntriesInAclW(3, result.ea, nullptr, &acl); + // Put the ACL in a unique pointer so that LocalFree is called when it goes + // out of scope + result.acl.reset(acl); + if (drv != ERROR_SUCCESS) { + return HRESULT_FROM_WIN32(drv); + } + + result.securityDescriptorBuffer = + mozilla::MakeUnique<uint8_t[]>(SECURITY_DESCRIPTOR_MIN_LENGTH); + if (!result.securityDescriptorBuffer) { + return E_OUTOFMEMORY; + } + result.securityDescriptor = reinterpret_cast<PSECURITY_DESCRIPTOR>( + result.securityDescriptorBuffer.get()); + success = InitializeSecurityDescriptor(result.securityDescriptor, + SECURITY_DESCRIPTOR_REVISION); + if (!success) { + return HRESULT_FROM_WIN32(GetLastError()); + } + + success = + SetSecurityDescriptorDacl(result.securityDescriptor, TRUE, acl, FALSE); + if (!success) { + return HRESULT_FROM_WIN32(GetLastError()); + } + + result.securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); + result.securityAttributes.lpSecurityDescriptor = result.securityDescriptor; + result.securityAttributes.bInheritHandle = FALSE; + return S_OK; +} + +/** + * Creates a directory with the permissions specified. If the directory already + * exists, this function will return success. + */ +static HRESULT MakeDir(const SimpleAutoString& path, const AutoPerms& perms) { + BOOL success = CreateDirectoryW( + path.String(), + const_cast<LPSECURITY_ATTRIBUTES>(&perms.securityAttributes)); + if (success) { + return S_OK; + } + DWORD error = GetLastError(); + if (error == ERROR_ALREADY_EXISTS) { + return S_OK; + } + return HRESULT_FROM_WIN32(error); +} +#endif // XP_WIN diff --git a/toolkit/mozapps/update/common/commonupdatedir.h b/toolkit/mozapps/update/common/commonupdatedir.h new file mode 100644 index 0000000000..5d7f88b15e --- /dev/null +++ b/toolkit/mozapps/update/common/commonupdatedir.h @@ -0,0 +1,39 @@ +/* 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/. */ +#ifndef COMMONUPDATEDIR_H +#define COMMONUPDATEDIR_H + +#include "mozilla/UniquePtr.h" + +#ifdef XP_WIN +# include <windows.h> +typedef WCHAR NS_tchar; +#else +typedef char NS_tchar; +#endif + +bool GetInstallHash(const char16_t* installPath, + mozilla::UniquePtr<NS_tchar[]>& result); + +#ifdef XP_WIN +// In addition to getting the update directory, this function also creates it. +// This is to ensure that, when it is created, it is created with the correct +// permissions. The default permissions on the containing directory can cause +// problems, so it is very, very important that we make sure that the +// permissions are set properly. Thus, we won't even give out the path of the +// update directory without ensuring that it was created with the correct +// permissions. +HRESULT GetCommonUpdateDirectory(const wchar_t* installPath, + mozilla::UniquePtr<wchar_t[]>& result); +// Returns the old common update directory. Since this directory was used before +// we made sure to always set the correct permissions, it is possible that the +// permissions on this directory are set such that files can only be modified +// or deleted by the user that created them. This function exists entirely to +// allow us to migrate files out of the old update directory and into the new +// one. +HRESULT GetOldUpdateDirectory(const wchar_t* installPath, + mozilla::UniquePtr<wchar_t[]>& result); +#endif + +#endif diff --git a/toolkit/mozapps/update/common/moz.build b/toolkit/mozapps/update/common/moz.build new file mode 100644 index 0000000000..2c79661c1f --- /dev/null +++ b/toolkit/mozapps/update/common/moz.build @@ -0,0 +1,76 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS += [ + "commonupdatedir.h", + "readstrings.h", + "updatecommon.h", + "updatedefines.h", + "updatererrors.h", +] + +if CONFIG["OS_ARCH"] == "WINNT": + EXPORTS += [ + "pathhash.h", + "uachelper.h", + "updatehelper.cpp", + "updatehelper.h", + "updateutils_win.h", + ] + + if CONFIG["MOZ_MAINTENANCE_SERVICE"]: + EXPORTS += [ + "certificatecheck.h", + "registrycertificates.h", + ] + +Library("updatecommon") + +DEFINES["NS_NO_XPCOM"] = True +USE_STATIC_LIBS = True + +if CONFIG["OS_ARCH"] == "WINNT": + # This forces the creation of updatecommon.lib, which the update agent needs + # in order to link to updatecommon library functions. + NO_EXPAND_LIBS = True + +DisableStlWrapping() + +if CONFIG["OS_ARCH"] == "WINNT": + SOURCES += [ + "pathhash.cpp", + "uachelper.cpp", + "updatehelper.cpp", + "updateutils_win.cpp", + ] + OS_LIBS += [ + "advapi32", + "ole32", + "rpcrt4", + "shell32", + ] + if CONFIG["MOZ_MAINTENANCE_SERVICE"]: + SOURCES += [ + "certificatecheck.cpp", + "registrycertificates.cpp", + ] + OS_LIBS += [ + "crypt32", + "wintrust", + ] + +SOURCES += [ + "/other-licenses/nsis/Contrib/CityHash/cityhash/city.cpp", + "commonupdatedir.cpp", + "readstrings.cpp", + "updatecommon.cpp", +] + +LOCAL_INCLUDES += [ + "/other-licenses/nsis/Contrib/CityHash/cityhash", +] + +DEFINES["MOZ_APP_BASENAME"] = '"%s"' % CONFIG["MOZ_APP_BASENAME"] diff --git a/toolkit/mozapps/update/common/pathhash.cpp b/toolkit/mozapps/update/common/pathhash.cpp new file mode 100644 index 0000000000..e70f69a755 --- /dev/null +++ b/toolkit/mozapps/update/common/pathhash.cpp @@ -0,0 +1,128 @@ +/* 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/. */ + +#include <windows.h> +#include <wincrypt.h> +#include "pathhash.h" + +/** + * Converts a binary sequence into a hex string + * + * @param hash The binary data sequence + * @param hashSize The size of the binary data sequence + * @param hexString A buffer to store the hex string, must be of + * size 2 * @hashSize + */ +static void BinaryDataToHexString(const BYTE* hash, DWORD& hashSize, + LPWSTR hexString) { + WCHAR* p = hexString; + for (DWORD i = 0; i < hashSize; ++i) { + wsprintfW(p, L"%.2x", hash[i]); + p += 2; + } +} + +/** + * Calculates an MD5 hash for the given input binary data + * + * @param data Any sequence of bytes + * @param dataSize The number of bytes inside @data + * @param hash Output buffer to store hash, must be freed by the caller + * @param hashSize The number of bytes in the output buffer + * @return TRUE on success + */ +static BOOL CalculateMD5(const char* data, DWORD dataSize, BYTE** hash, + DWORD& hashSize) { + HCRYPTPROV hProv = 0; + HCRYPTHASH hHash = 0; + + if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL, + CRYPT_VERIFYCONTEXT)) { + if ((DWORD)NTE_BAD_KEYSET != GetLastError()) { + return FALSE; + } + + // Maybe it doesn't exist, try to create it. + if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL, + CRYPT_VERIFYCONTEXT | CRYPT_NEWKEYSET)) { + return FALSE; + } + } + + if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) { + return FALSE; + } + + if (!CryptHashData(hHash, reinterpret_cast<const BYTE*>(data), dataSize, 0)) { + return FALSE; + } + + DWORD dwCount = sizeof(DWORD); + if (!CryptGetHashParam(hHash, HP_HASHSIZE, (BYTE*)&hashSize, &dwCount, 0)) { + return FALSE; + } + + *hash = new BYTE[hashSize]; + ZeroMemory(*hash, hashSize); + if (!CryptGetHashParam(hHash, HP_HASHVAL, *hash, &hashSize, 0)) { + return FALSE; + } + + if (hHash) { + CryptDestroyHash(hHash); + } + + if (hProv) { + CryptReleaseContext(hProv, 0); + } + + return TRUE; +} + +/** + * Converts a file path into a unique registry location for cert storage + * + * @param filePath The input file path to get a registry path from + * @param registryPath A buffer to write the registry path to, must + * be of size in WCHARs MAX_PATH + 1 + * @return TRUE if successful + */ +BOOL CalculateRegistryPathFromFilePath(const LPCWSTR filePath, + LPWSTR registryPath) { + size_t filePathLen = wcslen(filePath); + if (!filePathLen) { + return FALSE; + } + + // If the file path ends in a slash, ignore that character + if (filePath[filePathLen - 1] == L'\\' || filePath[filePathLen - 1] == L'/') { + filePathLen--; + } + + // Copy in the full path into our own buffer. + // Copying in the extra slash is OK because we calculate the hash + // based on the filePathLen which excludes the slash. + // +2 to account for the possibly trailing slash and the null terminator. + WCHAR* lowercasePath = new WCHAR[filePathLen + 2]; + memset(lowercasePath, 0, (filePathLen + 2) * sizeof(WCHAR)); + wcsncpy(lowercasePath, filePath, filePathLen + 1); + _wcslwr(lowercasePath); + + BYTE* hash; + DWORD hashSize = 0; + if (!CalculateMD5(reinterpret_cast<const char*>(lowercasePath), + filePathLen * 2, &hash, hashSize)) { + delete[] lowercasePath; + return FALSE; + } + delete[] lowercasePath; + + LPCWSTR baseRegPath = + L"SOFTWARE\\Mozilla\\" + L"MaintenanceService\\"; + wcsncpy(registryPath, baseRegPath, MAX_PATH); + BinaryDataToHexString(hash, hashSize, registryPath + wcslen(baseRegPath)); + delete[] hash; + return TRUE; +} diff --git a/toolkit/mozapps/update/common/pathhash.h b/toolkit/mozapps/update/common/pathhash.h new file mode 100644 index 0000000000..17f08ae95e --- /dev/null +++ b/toolkit/mozapps/update/common/pathhash.h @@ -0,0 +1,21 @@ +/* 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/. */ + +#ifndef _PATHHASH_H_ +#define _PATHHASH_H_ + +#include <windows.h> + +/** + * Converts a file path into a unique registry location for cert storage + * + * @param filePath The input file path to get a registry path from + * @param registryPath A buffer to write the registry path to, must + * be of size in WCHARs MAX_PATH + 1 + * @return TRUE if successful + */ +BOOL CalculateRegistryPathFromFilePath(const LPCWSTR filePath, + LPWSTR registryPath); + +#endif diff --git a/toolkit/mozapps/update/common/readstrings.cpp b/toolkit/mozapps/update/common/readstrings.cpp new file mode 100644 index 0000000000..17c2d002a1 --- /dev/null +++ b/toolkit/mozapps/update/common/readstrings.cpp @@ -0,0 +1,396 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include <algorithm> +#include <iterator> +#include <string.h> +#include <stdio.h> +#include "readstrings.h" +#include "updatererrors.h" + +#ifdef XP_WIN +# define NS_tfopen _wfopen +# define OPEN_MODE L"rb" +# define NS_tstrlen wcslen +# define NS_tstrcpy wcscpy +#else +# define NS_tfopen fopen +# define OPEN_MODE "r" +# define NS_tstrlen strlen +# define NS_tstrcpy strcpy +#endif + +// stack based FILE wrapper to ensure that fclose is called. +class AutoFILE { + public: + explicit AutoFILE(FILE* fp) : fp_(fp) {} + ~AutoFILE() { + if (fp_) { + fclose(fp_); + } + } + operator FILE*() { return fp_; } + + private: + FILE* fp_; +}; + +class AutoCharArray { + public: + explicit AutoCharArray(size_t len) { ptr_ = new char[len]; } + ~AutoCharArray() { delete[] ptr_; } + operator char*() { return ptr_; } + + private: + char* ptr_; +}; + +static const char kNL[] = "\r\n"; +static const char kEquals[] = "="; +static const char kWhitespace[] = " \t"; +static const char kRBracket[] = "]"; + +static const char* NS_strspnp(const char* delims, const char* str) { + const char* d; + do { + for (d = delims; *d != '\0'; ++d) { + if (*str == *d) { + ++str; + break; + } + } + } while (*d); + + return str; +} + +static char* NS_strtok(const char* delims, char** str) { + if (!*str) { + return nullptr; + } + + char* ret = (char*)NS_strspnp(delims, *str); + + if (!*ret) { + *str = ret; + return nullptr; + } + + char* i = ret; + do { + for (const char* d = delims; *d != '\0'; ++d) { + if (*i == *d) { + *i = '\0'; + *str = ++i; + return ret; + } + } + ++i; + } while (*i); + + *str = nullptr; + return ret; +} + +/** + * Find a key in a keyList containing zero-delimited keys ending with "\0\0". + * Returns a zero-based index of the key in the list, or -1 if the key is not + * found. + */ +static int find_key(const char* keyList, char* key) { + if (!keyList) { + return -1; + } + + int index = 0; + const char* p = keyList; + while (*p) { + if (strcmp(key, p) == 0) { + return index; + } + + p += strlen(p) + 1; + index++; + } + + // The key was not found if we came here + return -1; +} + +/** + * A very basic parser for updater.ini taken mostly from nsINIParser.cpp + * that can be used by standalone apps. + * + * @param path Path to the .ini file to read + * @param keyList List of zero-delimited keys ending with two zero characters + * @param numStrings Number of strings to read into results buffer - must be + * equal to the number of keys + * @param results Array of strings. Array's length must be equal to + * numStrings. Each string will be populated with the value + * corresponding to the key with the same index in keyList. + * @param section Optional name of the section to read; defaults to "Strings" + */ +int ReadStrings(const NS_tchar* path, const char* keyList, + unsigned int numStrings, mozilla::UniquePtr<char[]>* results, + const char* section) { + AutoFILE fp(NS_tfopen(path, OPEN_MODE)); + + if (!fp) { + return READ_ERROR; + } + + /* get file size */ + if (fseek(fp, 0, SEEK_END) != 0) { + return READ_ERROR; + } + + long len = ftell(fp); + if (len <= 0) { + return READ_ERROR; + } + + size_t flen = size_t(len); + AutoCharArray fileContents(flen + 1); + if (!fileContents) { + return READ_STRINGS_MEM_ERROR; + } + + /* read the file in one swoop */ + if (fseek(fp, 0, SEEK_SET) != 0) { + return READ_ERROR; + } + + size_t rd = fread(fileContents, sizeof(char), flen, fp); + if (rd != flen) { + return READ_ERROR; + } + + fileContents[flen] = '\0'; + + char* buffer = fileContents; + bool inStringsSection = false; + + unsigned int read = 0; + + while (char* token = NS_strtok(kNL, &buffer)) { + if (token[0] == '#' || token[0] == ';') { // it's a comment + continue; + } + + token = (char*)NS_strspnp(kWhitespace, token); + if (!*token) { // empty line + continue; + } + + if (token[0] == '[') { // section header! + ++token; + char const* currSection = token; + + char* rb = NS_strtok(kRBracket, &token); + if (!rb || NS_strtok(kWhitespace, &token)) { + // there's either an unclosed [Section or a [Section]Moretext! + // we could frankly decide that this INI file is malformed right + // here and stop, but we won't... keep going, looking for + // a well-formed [section] to continue working with + inStringsSection = false; + } else { + if (section) { + inStringsSection = strcmp(currSection, section) == 0; + } else { + inStringsSection = strcmp(currSection, "Strings") == 0; + } + } + + continue; + } + + if (!inStringsSection) { + // If we haven't found a section header (or we found a malformed + // section header), or this isn't the [Strings] section don't bother + // parsing this line. + continue; + } + + char* key = token; + char* e = NS_strtok(kEquals, &token); + if (!e) { + continue; + } + + int keyIndex = find_key(keyList, key); + if (keyIndex >= 0 && (unsigned int)keyIndex < numStrings) { + size_t valueSize = strlen(token) + 1; + results[keyIndex] = mozilla::MakeUnique<char[]>(valueSize); + + strcpy(results[keyIndex].get(), token); + read++; + } + } + + return (read == numStrings) ? OK : PARSE_ERROR; +} + +// A wrapper function to read strings for the updater. +// Added for compatibility with the original code. +int ReadStrings(const NS_tchar* path, StringTable* results) { + const unsigned int kNumStrings = 2; + const char* kUpdaterKeys = "Title\0Info\0"; + mozilla::UniquePtr<char[]> updater_strings[kNumStrings]; + + int result = ReadStrings(path, kUpdaterKeys, kNumStrings, updater_strings); + + if (result == OK) { + results->title.swap(updater_strings[0]); + results->info.swap(updater_strings[1]); + } + + return result; +} + +IniReader::IniReader(const NS_tchar* iniPath, + const char* section /* = nullptr */) { + if (iniPath) { + mPath = mozilla::MakeUnique<NS_tchar[]>(NS_tstrlen(iniPath) + 1); + NS_tstrcpy(mPath.get(), iniPath); + mMaybeStatusCode = mozilla::Nothing(); + } else { + mMaybeStatusCode = mozilla::Some(READ_STRINGS_MEM_ERROR); + } + if (section) { + mSection = mozilla::MakeUnique<char[]>(strlen(section) + 1); + strcpy(mSection.get(), section); + } else { + mSection.reset(nullptr); + } +} + +bool IniReader::MaybeAddKey(const char* key, size_t& insertionIndex) { + if (!key || strlen(key) == 0 || mMaybeStatusCode.isSome()) { + return false; + } + auto existingKey = std::find_if(mKeys.begin(), mKeys.end(), + [=](mozilla::UniquePtr<char[]>& searchKey) { + return strcmp(key, searchKey.get()) == 0; + }); + if (existingKey != mKeys.end()) { + // Key already in list + insertionIndex = std::distance(mKeys.begin(), existingKey); + return true; + } + + // Key not already in list + insertionIndex = mKeys.size(); + mKeys.emplace_back(mozilla::MakeUnique<char[]>(strlen(key) + 1)); + strcpy(mKeys.back().get(), key); + return true; +} + +void IniReader::AddKey(const char* key, mozilla::UniquePtr<char[]>* outputPtr) { + size_t insertionIndex; + if (!MaybeAddKey(key, insertionIndex)) { + return; + } + + if (!outputPtr) { + return; + } + + mNarrowOutputs.emplace_back(); + mNarrowOutputs.back().keyIndex = insertionIndex; + mNarrowOutputs.back().outputPtr = outputPtr; +} + +#ifdef XP_WIN +void IniReader::AddKey(const char* key, + mozilla::UniquePtr<wchar_t[]>* outputPtr) { + size_t insertionIndex; + if (!MaybeAddKey(key, insertionIndex)) { + return; + } + + if (!outputPtr) { + return; + } + + mWideOutputs.emplace_back(); + mWideOutputs.back().keyIndex = insertionIndex; + mWideOutputs.back().outputPtr = outputPtr; +} + +// Returns true on success, false on failure. +static bool ConvertToWide(const char* toConvert, + mozilla::UniquePtr<wchar_t[]>* result) { + int bufferSize = MultiByteToWideChar(CP_UTF8, 0, toConvert, -1, nullptr, 0); + *result = mozilla::MakeUnique<wchar_t[]>(bufferSize); + int charsWritten = + MultiByteToWideChar(CP_UTF8, 0, toConvert, -1, result->get(), bufferSize); + return charsWritten > 0; +} +#endif + +int IniReader::Read() { + if (mMaybeStatusCode.isSome()) { + return mMaybeStatusCode.value(); + } + + if (mKeys.empty()) { + // If there's nothing to read, just report success and return. + mMaybeStatusCode = mozilla::Some(OK); + return OK; + } + + // First assemble the key list, which will be a character array of + // back-to-back null-terminated strings ending with a double null termination. + size_t keyListSize = 1; // For the final null + for (const auto& key : mKeys) { + keyListSize += strlen(key.get()); + keyListSize += 1; // For the terminating null + } + mozilla::UniquePtr<char[]> keyList = mozilla::MakeUnique<char[]>(keyListSize); + char* keyListPtr = keyList.get(); + for (const auto& key : mKeys) { + strcpy(keyListPtr, key.get()); + // Point keyListPtr directly after the trailing null that strcpy wrote. + keyListPtr += strlen(key.get()) + 1; + } + *keyListPtr = '\0'; + + // Now make the array for the resulting data to be stored in + mozilla::UniquePtr<mozilla::UniquePtr<char[]>[]> results = + mozilla::MakeUnique<mozilla::UniquePtr<char[]>[]>(mKeys.size()); + + // Invoke ReadStrings to read the file and store the data for us + int statusCode = ReadStrings(mPath.get(), keyList.get(), mKeys.size(), + results.get(), mSection.get()); + mMaybeStatusCode = mozilla::Some(statusCode); + + if (statusCode != OK) { + return statusCode; + } + + // Now populate the requested locations with the requested data. + for (const auto output : mNarrowOutputs) { + char* valueBuffer = results[output.keyIndex].get(); + if (valueBuffer) { + *(output.outputPtr) = + mozilla::MakeUnique<char[]>(strlen(valueBuffer) + 1); + strcpy(output.outputPtr->get(), valueBuffer); + } + } + +#ifdef XP_WIN + for (const auto output : mWideOutputs) { + char* valueBuffer = results[output.keyIndex].get(); + if (valueBuffer) { + if (!ConvertToWide(valueBuffer, output.outputPtr)) { + statusCode = STRING_CONVERSION_ERROR; + } + } + } +#endif + + return statusCode; +} diff --git a/toolkit/mozapps/update/common/readstrings.h b/toolkit/mozapps/update/common/readstrings.h new file mode 100644 index 0000000000..9e0ebbefb5 --- /dev/null +++ b/toolkit/mozapps/update/common/readstrings.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef READSTRINGS_H__ +#define READSTRINGS_H__ + +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" + +#include <vector> + +#ifdef XP_WIN +# include <windows.h> +typedef WCHAR NS_tchar; +#else +typedef char NS_tchar; +#endif + +struct StringTable { + mozilla::UniquePtr<char[]> title; + mozilla::UniquePtr<char[]> info; +}; + +/** + * This function reads in localized strings from updater.ini + */ +int ReadStrings(const NS_tchar* path, StringTable* results); + +/** + * This function reads in localized strings corresponding to the keys from a + * given .ini + */ +int ReadStrings(const NS_tchar* path, const char* keyList, + unsigned int numStrings, mozilla::UniquePtr<char[]>* results, + const char* section = nullptr); + +/** + * This class is meant to be a slightly cleaner interface into the ReadStrings + * function. + */ +class IniReader { + public: + // IniReader must be initialized with the path of the INI file and a + // section to read from. If the section is null or not specified, the + // default section name ("Strings") will be used. + explicit IniReader(const NS_tchar* iniPath, const char* section = nullptr); + + // Records a key that ought to be read from the INI file. When + // IniReader::Read() is invoked it will, if successful, store the value + // corresponding to the given key in the UniquePtr given. + // If IniReader::Read() has already been invoked, these functions do nothing. + // The given key must not be the empty string. + void AddKey(const char* key, mozilla::UniquePtr<char[]>* outputPtr); +#ifdef XP_WIN + void AddKey(const char* key, mozilla::UniquePtr<wchar_t[]>* outputPtr); +#endif + bool HasRead() { return mMaybeStatusCode.isSome(); } + // Performs the actual reading and assigns values to the requested locations. + // Returns the same possible values that `ReadStrings` returns. + // If this is called more than once, no action will be taken on subsequent + // calls, and the stored status code will be returned instead. + int Read(); + + private: + bool MaybeAddKey(const char* key, size_t& insertionIndex); + + mozilla::UniquePtr<NS_tchar[]> mPath; + mozilla::UniquePtr<char[]> mSection; + std::vector<mozilla::UniquePtr<char[]>> mKeys; + + template <class T> + struct ValueOutput { + size_t keyIndex; + T* outputPtr; + }; + + // Stores associations between keys and the buffers where their values will + // be stored. + std::vector<ValueOutput<mozilla::UniquePtr<char[]>>> mNarrowOutputs; +#ifdef XP_WIN + std::vector<ValueOutput<mozilla::UniquePtr<wchar_t[]>>> mWideOutputs; +#endif + // If we have attempted to read the INI, this will store the resulting + // status code. + mozilla::Maybe<int> mMaybeStatusCode; +}; + +#endif // READSTRINGS_H__ diff --git a/toolkit/mozapps/update/common/registrycertificates.cpp b/toolkit/mozapps/update/common/registrycertificates.cpp new file mode 100644 index 0000000000..786218130a --- /dev/null +++ b/toolkit/mozapps/update/common/registrycertificates.cpp @@ -0,0 +1,148 @@ +/* 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/. */ + +#include <stdio.h> +#include <stdlib.h> +#include <windows.h> + +#include "registrycertificates.h" +#include "pathhash.h" +#include "updatecommon.h" +#include "updatehelper.h" +#define MAX_KEY_LENGTH 255 + +/** + * Verifies if the file path matches any certificate stored in the registry. + * + * @param filePath The file path of the application to check if allowed. + * @param allowFallbackKeySkip when this is TRUE the fallback registry key will + * be used to skip the certificate check. This is the default since the + * fallback registry key is located under HKEY_LOCAL_MACHINE which can't be + * written to by a low integrity process. + * Note: the maintenance service binary can be used to perform this check for + * testing or troubleshooting. + * @return TRUE if the binary matches any of the allowed certificates. + */ +BOOL DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate, + LPCWSTR filePath, + BOOL allowFallbackKeySkip) { + WCHAR maintenanceServiceKey[MAX_PATH + 1]; + if (!CalculateRegistryPathFromFilePath(basePathForUpdate, + maintenanceServiceKey)) { + return FALSE; + } + + // We use KEY_WOW64_64KEY to always force 64-bit view. + // The user may have both x86 and x64 applications installed + // which each register information. We need a consistent place + // to put those certificate attributes in and hence why we always + // force the non redirected registry under Wow6432Node. + // This flag is ignored on 32bit systems. + HKEY baseKey; + LONG retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, maintenanceServiceKey, 0, + KEY_READ | KEY_WOW64_64KEY, &baseKey); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not open key. (%ld)", retCode)); + // Our tests run with a different apply directory for each test. + // We use this registry key on our test machines to store the + // allowed name/issuers. + retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, TEST_ONLY_FALLBACK_KEY_PATH, 0, + KEY_READ | KEY_WOW64_64KEY, &baseKey); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not open fallback key. (%ld)", retCode)); + return FALSE; + } else if (allowFallbackKeySkip) { + LOG_WARN( + ("Fallback key present, skipping VerifyCertificateTrustForFile " + "check and the certificate attribute registry matching " + "check.")); + RegCloseKey(baseKey); + return TRUE; + } + } + + // Get the number of subkeys. + DWORD subkeyCount = 0; + retCode = RegQueryInfoKeyW(baseKey, nullptr, nullptr, nullptr, &subkeyCount, + nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not query info key. (%ld)", retCode)); + RegCloseKey(baseKey); + return FALSE; + } + + // Enumerate the subkeys, each subkey represents an allowed certificate. + for (DWORD i = 0; i < subkeyCount; i++) { + WCHAR subkeyBuffer[MAX_KEY_LENGTH]; + DWORD subkeyBufferCount = MAX_KEY_LENGTH; + retCode = RegEnumKeyExW(baseKey, i, subkeyBuffer, &subkeyBufferCount, + nullptr, nullptr, nullptr, nullptr); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not enum certs. (%ld)", retCode)); + RegCloseKey(baseKey); + return FALSE; + } + + // Open the subkey for the current certificate + HKEY subKey; + retCode = RegOpenKeyExW(baseKey, subkeyBuffer, 0, + KEY_READ | KEY_WOW64_64KEY, &subKey); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not open subkey. (%ld)", retCode)); + continue; // Try the next subkey + } + + const int MAX_CHAR_COUNT = 256; + DWORD valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR); + WCHAR name[MAX_CHAR_COUNT] = {L'\0'}; + WCHAR issuer[MAX_CHAR_COUNT] = {L'\0'}; + + // Get the name from the registry + retCode = RegQueryValueExW(subKey, L"name", 0, nullptr, (LPBYTE)name, + &valueBufSize); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not obtain name from registry. (%ld)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + // Get the issuer from the registry + valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR); + retCode = RegQueryValueExW(subKey, L"issuer", 0, nullptr, (LPBYTE)issuer, + &valueBufSize); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not obtain issuer from registry. (%ld)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + CertificateCheckInfo allowedCertificate = { + name, + issuer, + }; + + retCode = CheckCertificateForPEFile(filePath, allowedCertificate); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Error on certificate check. (%ld)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + retCode = VerifyCertificateTrustForFile(filePath); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Error on certificate trust check. (%ld)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + RegCloseKey(baseKey); + // Raise the roof, we found a match! + return TRUE; + } + + RegCloseKey(baseKey); + // No certificates match, :'( + return FALSE; +} diff --git a/toolkit/mozapps/update/common/registrycertificates.h b/toolkit/mozapps/update/common/registrycertificates.h new file mode 100644 index 0000000000..9f68d1a8d9 --- /dev/null +++ b/toolkit/mozapps/update/common/registrycertificates.h @@ -0,0 +1,14 @@ +/* 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/. */ + +#ifndef _REGISTRYCERTIFICATES_H_ +#define _REGISTRYCERTIFICATES_H_ + +#include "certificatecheck.h" + +BOOL DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate, + LPCWSTR filePath, + BOOL allowFallbackKeySkip = TRUE); + +#endif diff --git a/toolkit/mozapps/update/common/uachelper.cpp b/toolkit/mozapps/update/common/uachelper.cpp new file mode 100644 index 0000000000..07e9bd53f9 --- /dev/null +++ b/toolkit/mozapps/update/common/uachelper.cpp @@ -0,0 +1,186 @@ +/* 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/. */ + +#include <windows.h> +#include <wtsapi32.h> +#include "uachelper.h" +#include "updatecommon.h" + +// See the MSDN documentation with title: Privilege Constants +// At the time of this writing, this documentation is located at: +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb530716%28v=vs.85%29.aspx +LPCTSTR UACHelper::PrivsToDisable[] = { + SE_ASSIGNPRIMARYTOKEN_NAME, SE_AUDIT_NAME, SE_BACKUP_NAME, + // CreateProcess will succeed but the app will fail to launch on some WinXP + // machines if SE_CHANGE_NOTIFY_NAME is disabled. In particular this + // happens for limited user accounts on those machines. The define is kept + // here as a reminder that it should never be re-added. This permission is + // for directory watching but also from MSDN: "This privilege also causes + // the system to skip all traversal access checks." SE_CHANGE_NOTIFY_NAME, + SE_CREATE_GLOBAL_NAME, SE_CREATE_PAGEFILE_NAME, SE_CREATE_PERMANENT_NAME, + SE_CREATE_SYMBOLIC_LINK_NAME, SE_CREATE_TOKEN_NAME, SE_DEBUG_NAME, + SE_ENABLE_DELEGATION_NAME, SE_IMPERSONATE_NAME, SE_INC_BASE_PRIORITY_NAME, + SE_INCREASE_QUOTA_NAME, SE_INC_WORKING_SET_NAME, SE_LOAD_DRIVER_NAME, + SE_LOCK_MEMORY_NAME, SE_MACHINE_ACCOUNT_NAME, SE_MANAGE_VOLUME_NAME, + SE_PROF_SINGLE_PROCESS_NAME, SE_RELABEL_NAME, SE_REMOTE_SHUTDOWN_NAME, + SE_RESTORE_NAME, SE_SECURITY_NAME, SE_SHUTDOWN_NAME, SE_SYNC_AGENT_NAME, + SE_SYSTEM_ENVIRONMENT_NAME, SE_SYSTEM_PROFILE_NAME, SE_SYSTEMTIME_NAME, + SE_TAKE_OWNERSHIP_NAME, SE_TCB_NAME, SE_TIME_ZONE_NAME, + SE_TRUSTED_CREDMAN_ACCESS_NAME, SE_UNDOCK_NAME, SE_UNSOLICITED_INPUT_NAME}; + +/** + * Opens a user token for the given session ID + * + * @param sessionID The session ID for the token to obtain + * @return A handle to the token to obtain which will be primary if enough + * permissions exist. Caller should close the handle. + */ +HANDLE +UACHelper::OpenUserToken(DWORD sessionID) { + HMODULE module = LoadLibraryW(L"wtsapi32.dll"); + HANDLE token = nullptr; + decltype(WTSQueryUserToken)* wtsQueryUserToken = + (decltype(WTSQueryUserToken)*)GetProcAddress(module, "WTSQueryUserToken"); + if (wtsQueryUserToken) { + wtsQueryUserToken(sessionID, &token); + } + FreeLibrary(module); + return token; +} + +/** + * Opens a linked token for the specified token. + * + * @param token The token to get the linked token from + * @return A linked token or nullptr if one does not exist. + * Caller should close the handle. + */ +HANDLE +UACHelper::OpenLinkedToken(HANDLE token) { + // Magic below... + // UAC creates 2 tokens. One is the restricted token which we have. + // the other is the UAC elevated one. Since we are running as a service + // as the system account we have access to both. + TOKEN_LINKED_TOKEN tlt; + HANDLE hNewLinkedToken = nullptr; + DWORD len; + if (GetTokenInformation(token, (TOKEN_INFORMATION_CLASS)TokenLinkedToken, + &tlt, sizeof(TOKEN_LINKED_TOKEN), &len)) { + token = tlt.LinkedToken; + hNewLinkedToken = token; + } + return hNewLinkedToken; +} + +/** + * Enables or disables a privilege for the specified token. + * + * @param token The token to adjust the privilege on. + * @param priv The privilege to adjust. + * @param enable Whether to enable or disable it + * @return TRUE if the token was adjusted to the specified value. + */ +BOOL UACHelper::SetPrivilege(HANDLE token, LPCTSTR priv, BOOL enable) { + LUID luidOfPriv; + if (!LookupPrivilegeValue(nullptr, priv, &luidOfPriv)) { + return FALSE; + } + + TOKEN_PRIVILEGES tokenPriv; + tokenPriv.PrivilegeCount = 1; + tokenPriv.Privileges[0].Luid = luidOfPriv; + tokenPriv.Privileges[0].Attributes = enable ? SE_PRIVILEGE_ENABLED : 0; + + SetLastError(ERROR_SUCCESS); + if (!AdjustTokenPrivileges(token, false, &tokenPriv, sizeof(tokenPriv), + nullptr, nullptr)) { + return FALSE; + } + + return GetLastError() == ERROR_SUCCESS; +} + +/** + * For each privilege that is specified, an attempt will be made to + * drop the privilege. + * + * @param token The token to adjust the privilege on. + * Pass nullptr for current token. + * @param unneededPrivs An array of unneeded privileges. + * @param count The size of the array + * @return TRUE if there were no errors + */ +BOOL UACHelper::DisableUnneededPrivileges(HANDLE token, LPCTSTR* unneededPrivs, + size_t count) { + HANDLE obtainedToken = nullptr; + if (!token) { + // Note: This handle is a pseudo-handle and need not be closed + HANDLE process = GetCurrentProcess(); + if (!OpenProcessToken(process, TOKEN_ALL_ACCESS_P, &obtainedToken)) { + LOG_WARN( + ("Could not obtain token for current process, no " + "privileges changed. (%lu)", + GetLastError())); + return FALSE; + } + token = obtainedToken; + } + + BOOL result = TRUE; + for (size_t i = 0; i < count; i++) { + if (SetPrivilege(token, unneededPrivs[i], FALSE)) { + LOG(("Disabled unneeded token privilege: %s.", unneededPrivs[i])); + } else { + LOG(("Could not disable token privilege value: %s. (%lu)", + unneededPrivs[i], GetLastError())); + result = FALSE; + } + } + + if (obtainedToken) { + CloseHandle(obtainedToken); + } + return result; +} + +/** + * Disables privileges for the specified token. + * The privileges to disable are in PrivsToDisable. + * In the future there could be new privs and we are not sure if we should + * explicitly disable these or not. + * + * @param token The token to drop the privilege on. + * Pass nullptr for current token. + * @return TRUE if there were no errors + */ +BOOL UACHelper::DisablePrivileges(HANDLE token) { + static const size_t PrivsToDisableSize = + sizeof(UACHelper::PrivsToDisable) / sizeof(UACHelper::PrivsToDisable[0]); + + return DisableUnneededPrivileges(token, UACHelper::PrivsToDisable, + PrivsToDisableSize); +} + +/** + * Check if the current user can elevate. + * + * @return true if the user can elevate. + * false otherwise. + */ +bool UACHelper::CanUserElevate() { + HANDLE token; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + return false; + } + + TOKEN_ELEVATION_TYPE elevationType; + DWORD len; + bool canElevate = + GetTokenInformation(token, TokenElevationType, &elevationType, + sizeof(elevationType), &len) && + (elevationType == TokenElevationTypeLimited); + CloseHandle(token); + + return canElevate; +} diff --git a/toolkit/mozapps/update/common/uachelper.h b/toolkit/mozapps/update/common/uachelper.h new file mode 100644 index 0000000000..c9915981a0 --- /dev/null +++ b/toolkit/mozapps/update/common/uachelper.h @@ -0,0 +1,24 @@ +/* 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/. */ + +#ifndef _UACHELPER_H_ +#define _UACHELPER_H_ + +#include <windows.h> + +class UACHelper { + public: + static HANDLE OpenUserToken(DWORD sessionID); + static HANDLE OpenLinkedToken(HANDLE token); + static BOOL DisablePrivileges(HANDLE token); + static bool CanUserElevate(); + + private: + static BOOL SetPrivilege(HANDLE token, LPCTSTR privs, BOOL enable); + static BOOL DisableUnneededPrivileges(HANDLE token, LPCTSTR* unneededPrivs, + size_t count); + static LPCTSTR PrivsToDisable[]; +}; + +#endif diff --git a/toolkit/mozapps/update/common/updatecommon.cpp b/toolkit/mozapps/update/common/updatecommon.cpp new file mode 100644 index 0000000000..9e00ac5716 --- /dev/null +++ b/toolkit/mozapps/update/common/updatecommon.cpp @@ -0,0 +1,470 @@ +/* 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/. */ + +#if defined(XP_WIN) +# include <windows.h> +# include <winioctl.h> // for FSCTL_GET_REPARSE_POINT +# include <shlobj.h> +# ifndef RRF_SUBKEY_WOW6464KEY +# define RRF_SUBKEY_WOW6464KEY 0x00010000 +# endif +#endif + +#include <stdio.h> +#include <stdarg.h> + +#include "updatecommon.h" +#ifdef XP_WIN +# include "updatehelper.h" +# include "nsWindowsHelpers.h" +# include "mozilla/UniquePtr.h" +# include "mozilla/WinHeaderOnlyUtils.h" + +// This struct isn't in any SDK header, so this definition was copied from: +// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/ns-ntifs-_reparse_data_buffer +typedef struct _REPARSE_DATA_BUFFER { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + union { + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + ULONG Flags; + WCHAR PathBuffer[1]; + } SymbolicLinkReparseBuffer; + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + WCHAR PathBuffer[1]; + } MountPointReparseBuffer; + struct { + UCHAR DataBuffer[1]; + } GenericReparseBuffer; + } DUMMYUNIONNAME; +} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER; +#endif + +UpdateLog::UpdateLog() : logFP(nullptr) {} + +void UpdateLog::Init(NS_tchar* logFilePath) { + if (logFP) { + return; + } + + // When the path is over the length limit disable logging by not opening the + // file and not setting logFP. + int dstFilePathLen = NS_tstrlen(logFilePath); + if (dstFilePathLen > 0 && dstFilePathLen < MAXPATHLEN - 1) { + NS_tstrncpy(mDstFilePath, logFilePath, MAXPATHLEN); +#if defined(XP_WIN) || defined(XP_MACOSX) + logFP = NS_tfopen(mDstFilePath, NS_T("w")); +#else + // On platforms that have an updates directory in the installation directory + // (e.g. platforms other than Windows and Mac) the update log is written to + // a temporary file and then to the update log file. This is needed since + // the installation directory is moved during a replace request. This can be + // removed when the platform's updates directory is located outside of the + // installation directory. + logFP = tmpfile(); +#endif + } +} + +void UpdateLog::Finish() { + if (!logFP) { + return; + } + +#if !defined(XP_WIN) && !defined(XP_MACOSX) + const int blockSize = 1024; + char buffer[blockSize]; + fflush(logFP); + rewind(logFP); + + FILE* updateLogFP = NS_tfopen(mDstFilePath, NS_T("wb+")); + while (!feof(logFP)) { + size_t read = fread(buffer, 1, blockSize, logFP); + if (ferror(logFP)) { + fclose(logFP); + logFP = nullptr; + fclose(updateLogFP); + updateLogFP = nullptr; + return; + } + + size_t written = 0; + + while (written < read) { + size_t chunkWritten = fwrite(buffer, 1, read - written, updateLogFP); + if (chunkWritten <= 0) { + fclose(logFP); + logFP = nullptr; + fclose(updateLogFP); + updateLogFP = nullptr; + return; + } + + written += chunkWritten; + } + } + fclose(updateLogFP); + updateLogFP = nullptr; +#endif + + fclose(logFP); + logFP = nullptr; +} + +void UpdateLog::Flush() { + if (!logFP) { + return; + } + + fflush(logFP); +} + +void UpdateLog::Printf(const char* fmt, ...) { + if (!logFP) { + return; + } + + time_t rawtime = time(nullptr); + struct tm* timeinfo = localtime(&rawtime); + + if (nullptr != timeinfo) { + // attempt to format the time similar to rfc-3339 so that it works with + // sort(1). xxxx-xx-xx xx:xx:xx+xxxx -> 24 chars + 1 NUL + const size_t buffer_size = 25; + char buffer[buffer_size] = {0}; + + if (0 == strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S%z", timeinfo)) { + buffer[0] = '\0'; // reset buffer into a defined state and try posix ts + if (0 > snprintf(buffer, buffer_size, "%d", (int)mktime(timeinfo))) { + buffer[0] = '\0'; // reset and give up + } + } + + fprintf(logFP, "%s: ", buffer); + } + + va_list ap; + va_start(ap, fmt); + vfprintf(logFP, fmt, ap); + va_end(ap); + + fprintf(logFP, "\n"); +#if defined(XP_WIN) && defined(MOZ_DEBUG) + // When the updater crashes on Windows the log file won't be flushed and this + // can make it easier to debug what is going on. + fflush(logFP); +#endif +} + +void UpdateLog::WarnPrintf(const char* fmt, ...) { + if (!logFP) { + return; + } + + va_list ap; + va_start(ap, fmt); + fprintf(logFP, "*** Warning: "); + vfprintf(logFP, fmt, ap); + fprintf(logFP, "***\n"); + va_end(ap); +#if defined(XP_WIN) && defined(MOZ_DEBUG) + // When the updater crashes on Windows the log file won't be flushed and this + // can make it easier to debug what is going on. + fflush(logFP); +#endif +} + +#ifdef XP_WIN +/** + * Determine if a path contains symlinks or junctions to disallowed locations + * + * @param fullPath The full path to check. + * @return true if the path contains invalid links or on errors, + * false if the check passes and the path can be used + */ +bool PathContainsInvalidLinks(wchar_t* const fullPath) { + wchar_t pathCopy[MAXPATHLEN + 1] = L""; + wcsncpy(pathCopy, fullPath, MAXPATHLEN); + wchar_t* remainingPath = nullptr; + wchar_t* nextToken = wcstok_s(pathCopy, L"\\", &remainingPath); + wchar_t* partialPath = nextToken; + + while (nextToken) { + if ((GetFileAttributesW(partialPath) & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + nsAutoHandle h(CreateFileW( + partialPath, 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr)); + if (h == INVALID_HANDLE_VALUE) { + if (GetLastError() == ERROR_FILE_NOT_FOUND) { + // The path can't be an invalid link if it doesn't exist. + return false; + } else { + return true; + } + } + + mozilla::UniquePtr<UINT8[]> byteBuffer = + mozilla::MakeUnique<UINT8[]>(MAXIMUM_REPARSE_DATA_BUFFER_SIZE); + ZeroMemory(byteBuffer.get(), MAXIMUM_REPARSE_DATA_BUFFER_SIZE); + REPARSE_DATA_BUFFER* buffer = (REPARSE_DATA_BUFFER*)byteBuffer.get(); + DWORD bytes = 0; + if (!DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, nullptr, 0, buffer, + MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &bytes, nullptr)) { + return true; + } + + wchar_t* reparseTarget = nullptr; + switch (buffer->ReparseTag) { + case IO_REPARSE_TAG_MOUNT_POINT: + reparseTarget = + buffer->MountPointReparseBuffer.PathBuffer + + (buffer->MountPointReparseBuffer.SubstituteNameOffset / + sizeof(wchar_t)); + if (buffer->MountPointReparseBuffer.SubstituteNameLength < + ARRAYSIZE(L"\\??\\")) { + return false; + } + break; + case IO_REPARSE_TAG_SYMLINK: + reparseTarget = + buffer->SymbolicLinkReparseBuffer.PathBuffer + + (buffer->SymbolicLinkReparseBuffer.SubstituteNameOffset / + sizeof(wchar_t)); + if (buffer->SymbolicLinkReparseBuffer.SubstituteNameLength < + ARRAYSIZE(L"\\??\\")) { + return false; + } + break; + default: + return true; + break; + } + + if (!reparseTarget) { + return false; + } + if (wcsncmp(reparseTarget, L"\\??\\", ARRAYSIZE(L"\\??\\") - 1) != 0) { + return true; + } + } + + nextToken = wcstok_s(nullptr, L"\\", &remainingPath); + PathAppendW(partialPath, nextToken); + } + + return false; +} + +/** + * Determine if a path is located within Program Files, either native or x86 + * + * @param fullPath The full path to check. + * @return true if fullPath begins with either Program Files directory, + * false if it does not or if an error is encountered + */ +bool IsProgramFilesPath(NS_tchar* fullPath) { + // Make sure we don't try to compare against a short path. + DWORD longInstallPathChars = GetLongPathNameW(fullPath, nullptr, 0); + if (longInstallPathChars == 0) { + return false; + } + mozilla::UniquePtr<wchar_t[]> longInstallPath = + mozilla::MakeUnique<wchar_t[]>(longInstallPathChars); + if (!GetLongPathNameW(fullPath, longInstallPath.get(), + longInstallPathChars)) { + return false; + } + + // First check for Program Files (x86). + { + PWSTR programFiles32PathRaw = nullptr; + // FOLDERID_ProgramFilesX86 gets native Program Files directory on a 32-bit + // OS or the (x86) directory on a 64-bit OS regardless of this binary's + // bitness. + if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramFilesX86, 0, nullptr, + &programFiles32PathRaw))) { + // That call should never fail on any supported OS version. + return false; + } + mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter> + programFiles32Path(programFiles32PathRaw); + // We need this path to have a trailing slash so our prefix test doesn't + // match on a different folder which happens to have a name beginning with + // the prefix we're looking for but then also more characters after that. + size_t length = wcslen(programFiles32Path.get()); + if (length == 0) { + return false; + } + if (programFiles32Path.get()[length - 1] == L'\\') { + if (wcsnicmp(longInstallPath.get(), programFiles32Path.get(), length) == + 0) { + return true; + } + } else { + // Allocate space for a copy of the string along with a terminator and one + // extra character for the trailing backslash. + length += 1; + mozilla::UniquePtr<wchar_t[]> programFiles32PathWithSlash = + mozilla::MakeUnique<wchar_t[]>(length + 1); + + NS_tsnprintf(programFiles32PathWithSlash.get(), length + 1, NS_T("%s\\"), + programFiles32Path.get()); + + if (wcsnicmp(longInstallPath.get(), programFiles32PathWithSlash.get(), + length) == 0) { + return true; + } + } + } + + // If we didn't find (x86), check for the native Program Files. + { + // In case we're a 32-bit binary on 64-bit Windows, we now have a problem + // getting the right "native" Program Files path, which is that there is no + // FOLDERID_* value that returns that path. So we always read that one out + // of its canonical registry location instead. If we're on a 32-bit OS, this + // will be the same path that we just checked. First get the buffer size to + // allocate for the path. + DWORD length = 0; + if (RegGetValueW(HKEY_LOCAL_MACHINE, + L"Software\\Microsoft\\Windows\\CurrentVersion", + L"ProgramFilesDir", RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY, + nullptr, nullptr, &length) != ERROR_SUCCESS) { + return false; + } + // RegGetValue returns the length including the terminator, but it's in + // bytes, so convert that to characters. + DWORD lengthChars = (length / sizeof(wchar_t)); + if (lengthChars <= 1) { + return false; + } + mozilla::UniquePtr<wchar_t[]> programFilesNativePath = + mozilla::MakeUnique<wchar_t[]>(lengthChars); + + // Now actually read the value. + if (RegGetValueW( + HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion", + L"ProgramFilesDir", RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY, nullptr, + programFilesNativePath.get(), &length) != ERROR_SUCCESS) { + return false; + } + size_t nativePathStrLen = + wcsnlen_s(programFilesNativePath.get(), lengthChars); + if (nativePathStrLen == 0) { + return false; + } + + // As before, append a backslash if there isn't one already. + if (programFilesNativePath.get()[nativePathStrLen - 1] == L'\\') { + if (wcsnicmp(longInstallPath.get(), programFilesNativePath.get(), + nativePathStrLen) == 0) { + return true; + } + } else { + // Allocate space for a copy of the string along with a terminator and one + // extra character for the trailing backslash. + nativePathStrLen += 1; + mozilla::UniquePtr<wchar_t[]> programFilesNativePathWithSlash = + mozilla::MakeUnique<wchar_t[]>(nativePathStrLen + 1); + + NS_tsnprintf(programFilesNativePathWithSlash.get(), nativePathStrLen + 1, + NS_T("%s\\"), programFilesNativePath.get()); + + if (wcsnicmp(longInstallPath.get(), programFilesNativePathWithSlash.get(), + nativePathStrLen) == 0) { + return true; + } + } + } + + return false; +} +#endif + +/** + * Performs checks of a full path for validity for this application. + * + * @param origFullPath + * The full path to check. + * @return true if the path is valid for this application and false otherwise. + */ +bool IsValidFullPath(NS_tchar* origFullPath) { + // Subtract 1 from MAXPATHLEN for null termination. + if (NS_tstrlen(origFullPath) > MAXPATHLEN - 1) { + // The path is longer than acceptable for this application. + return false; + } + +#ifdef XP_WIN + NS_tchar testPath[MAXPATHLEN] = {NS_T('\0')}; + // GetFullPathNameW will replace / with \ which PathCanonicalizeW requires. + if (GetFullPathNameW(origFullPath, MAXPATHLEN, testPath, nullptr) == 0) { + // Unable to get the full name for the path (e.g. invalid path). + return false; + } + + NS_tchar canonicalPath[MAXPATHLEN] = {NS_T('\0')}; + if (!PathCanonicalizeW(canonicalPath, testPath)) { + // Path could not be canonicalized (e.g. invalid path). + return false; + } + + // Check if the path passed in resolves to a differerent path. + if (NS_tstricmp(origFullPath, canonicalPath) != 0) { + // Case insensitive string comparison between the supplied path and the + // canonical path are not equal. This will prevent directory traversal and + // the use of / in paths since they are converted to \. + return false; + } + + NS_tstrncpy(testPath, origFullPath, MAXPATHLEN); + if (!PathStripToRootW(testPath)) { + // It should always be possible to strip a valid path to its root. + return false; + } + + if (origFullPath[0] == NS_T('\\')) { + // Only allow UNC server share paths. + if (!PathIsUNCServerShareW(testPath)) { + return false; + } + } + + if (PathContainsInvalidLinks(canonicalPath)) { + return false; + } +#else + // Only allow full paths. + if (origFullPath[0] != NS_T('/')) { + return false; + } + + // The path must not traverse directories + if (NS_tstrstr(origFullPath, NS_T("/../")) != nullptr) { + return false; + } + + // The path shall not have a path traversal suffix + const NS_tchar invalidSuffix[] = NS_T("/.."); + size_t pathLen = NS_tstrlen(origFullPath); + size_t invalidSuffixLen = NS_tstrlen(invalidSuffix); + if (invalidSuffixLen <= pathLen && + NS_tstrncmp(origFullPath + pathLen - invalidSuffixLen, invalidSuffix, + invalidSuffixLen) == 0) { + return false; + } +#endif + return true; +} diff --git a/toolkit/mozapps/update/common/updatecommon.h b/toolkit/mozapps/update/common/updatecommon.h new file mode 100644 index 0000000000..e317bdcb1f --- /dev/null +++ b/toolkit/mozapps/update/common/updatecommon.h @@ -0,0 +1,43 @@ +/* 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/. */ + +#ifndef UPDATECOMMON_H +#define UPDATECOMMON_H + +#include "updatedefines.h" +#include <stdio.h> +#include <time.h> +#include "mozilla/Attributes.h" + +class UpdateLog { + public: + static UpdateLog& GetPrimaryLog() { + static UpdateLog primaryLog; + return primaryLog; + } + + void Init(NS_tchar* logFilePath); + void Finish(); + void Flush(); + void Printf(const char* fmt, ...) MOZ_FORMAT_PRINTF(2, 3); + void WarnPrintf(const char* fmt, ...) MOZ_FORMAT_PRINTF(2, 3); + + ~UpdateLog() { Finish(); } + + protected: + UpdateLog(); + FILE* logFP; + NS_tchar mDstFilePath[MAXPATHLEN]; +}; + +bool IsValidFullPath(NS_tchar* fullPath); +bool IsProgramFilesPath(NS_tchar* fullPath); + +#define LOG_WARN(args) UpdateLog::GetPrimaryLog().WarnPrintf args +#define LOG(args) UpdateLog::GetPrimaryLog().Printf args +#define LogInit(FILEPATH_) UpdateLog::GetPrimaryLog().Init(FILEPATH_) +#define LogFinish() UpdateLog::GetPrimaryLog().Finish() +#define LogFlush() UpdateLog::GetPrimaryLog().Flush() + +#endif diff --git a/toolkit/mozapps/update/common/updatedefines.h b/toolkit/mozapps/update/common/updatedefines.h new file mode 100644 index 0000000000..f716e10f66 --- /dev/null +++ b/toolkit/mozapps/update/common/updatedefines.h @@ -0,0 +1,164 @@ +/* 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/. */ + +#ifndef UPDATEDEFINES_H +#define UPDATEDEFINES_H + +#include <stdio.h> +#include <stdarg.h> +#include "readstrings.h" + +#if defined(XP_WIN) +# include <windows.h> +# include <shlwapi.h> +# include <direct.h> +# include <io.h> + +# ifndef F_OK +# define F_OK 00 +# endif +# ifndef W_OK +# define W_OK 02 +# endif +# ifndef R_OK +# define R_OK 04 +# endif +# define S_ISDIR(s) (((s)&_S_IFMT) == _S_IFDIR) +# define S_ISREG(s) (((s)&_S_IFMT) == _S_IFREG) + +# define access _access + +# define putenv _putenv +# if defined(_MSC_VER) && _MSC_VER < 1900 +# define stat _stat +# endif +# define DELETE_DIR L"tobedeleted" +# define CALLBACK_BACKUP_EXT L".moz-callback" + +# define LOG_S "%S" +# define NS_CONCAT(x, y) x##y +// The extra layer of indirection here allows this macro to be passed macros +# define NS_T(str) NS_CONCAT(L, str) +# define NS_SLASH NS_T('\\') +static inline int mywcsprintf(WCHAR* dest, size_t count, const WCHAR* fmt, + ...) { + size_t _count = count - 1; + va_list varargs; + va_start(varargs, fmt); + int result = _vsnwprintf(dest, count - 1, fmt, varargs); + va_end(varargs); + dest[_count] = L'\0'; + return result; +} +# define NS_tsnprintf mywcsprintf +# define NS_taccess _waccess +# define NS_tatoi _wtoi64 +# define NS_tchdir _wchdir +# define NS_tchmod _wchmod +# define NS_tfopen _wfopen +# define NS_tmkdir(path, perms) _wmkdir(path) +# define NS_tpid __int64 +# define NS_tremove _wremove +// _wrename is used to avoid the link tracking service. +# define NS_trename _wrename +# define NS_trmdir _wrmdir +# define NS_tstat _wstat +# define NS_tlstat _wstat // No symlinks on Windows +# define NS_tstat_t _stat +# define NS_tstrcat wcscat +# define NS_tstrcmp wcscmp +# define NS_tstricmp wcsicmp +# define NS_tstrncmp wcsncmp +# define NS_tstrcpy wcscpy +# define NS_tstrncpy wcsncpy +# define NS_tstrlen wcslen +# define NS_tstrchr wcschr +# define NS_tstrrchr wcsrchr +# define NS_tstrstr wcsstr +# include "updateutils_win.h" +# define NS_tDIR DIR +# define NS_tdirent dirent +# define NS_topendir opendir +# define NS_tclosedir closedir +# define NS_treaddir readdir +#else +# include <sys/wait.h> +# include <unistd.h> + +# ifdef HAVE_FTS_H +# include <fts.h> +# else +# include <sys/stat.h> +# endif +# include <dirent.h> + +# ifdef XP_MACOSX +# include <sys/time.h> +# endif + +# define LOG_S "%s" +# define NS_T(str) str +# define NS_SLASH NS_T('/') +# define NS_tsnprintf snprintf +# define NS_taccess access +# define NS_tatoi atoi +# define NS_tchdir chdir +# define NS_tchmod chmod +# define NS_tfopen fopen +# define NS_tmkdir mkdir +# define NS_tpid int +# define NS_tremove remove +# define NS_trename rename +# define NS_trmdir rmdir +# define NS_tstat stat +# define NS_tstat_t stat +# define NS_tlstat lstat +# define NS_tstrcat strcat +# define NS_tstrcmp strcmp +# define NS_tstricmp strcasecmp +# define NS_tstrncmp strncmp +# define NS_tstrcpy strcpy +# define NS_tstrncpy strncpy +# define NS_tstrlen strlen +# define NS_tstrrchr strrchr +# define NS_tstrstr strstr +# define NS_tDIR DIR +# define NS_tdirent dirent +# define NS_topendir opendir +# define NS_tclosedir closedir +# define NS_treaddir readdir +#endif + +#define BACKUP_EXT NS_T(".moz-backup") + +#ifndef MAXPATHLEN +# ifdef PATH_MAX +# define MAXPATHLEN PATH_MAX +# elif defined(MAX_PATH) +# define MAXPATHLEN MAX_PATH +# elif defined(_MAX_PATH) +# define MAXPATHLEN _MAX_PATH +# elif defined(CCHMAXPATH) +# define MAXPATHLEN CCHMAXPATH +# else +# define MAXPATHLEN 1024 +# endif +#endif + +static inline bool NS_tvsnprintf(NS_tchar* dest, size_t count, + const NS_tchar* fmt, ...) { + va_list varargs; + va_start(varargs, fmt); +#if defined(XP_WIN) + int result = _vsnwprintf(dest, count, fmt, varargs); +#else + int result = vsnprintf(dest, count, fmt, varargs); +#endif + va_end(varargs); + // The size_t cast of result is safe because result can only be positive after + // the first check. + return result >= 0 && (size_t)result < count; +} + +#endif diff --git a/toolkit/mozapps/update/common/updatehelper.cpp b/toolkit/mozapps/update/common/updatehelper.cpp new file mode 100644 index 0000000000..b094d9eb75 --- /dev/null +++ b/toolkit/mozapps/update/common/updatehelper.cpp @@ -0,0 +1,763 @@ +/* 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/. */ + +#include <windows.h> + +// Needed for CreateToolhelp32Snapshot +#include <tlhelp32.h> + +#include <stdio.h> +#include <direct.h> +#include "shlobj.h" + +// Needed for PathAppendW +#include <shlwapi.h> + +#include "updatehelper.h" +#include "updateutils_win.h" + +#ifdef MOZ_MAINTENANCE_SERVICE +# include "mozilla/UniquePtr.h" +# include "pathhash.h" +# include "registrycertificates.h" +# include "uachelper.h" + +using mozilla::MakeUnique; +using mozilla::UniquePtr; +#endif + +BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, + LPCWSTR newFileName); + +/** + * Obtains the path of a file in the same directory as the specified file. + * + * @param destinationBuffer A buffer of size MAX_PATH + 1 to store the result. + * @param siblingFilePath The path of another file in the same directory + * @param newFileName The filename of another file in the same directory + * @return TRUE if successful + */ +BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, + LPCWSTR newFileName) { + if (wcslen(siblingFilePath) > MAX_PATH) { + return FALSE; + } + + wcsncpy(destinationBuffer, siblingFilePath, MAX_PATH + 1); + if (!PathRemoveFileSpecW(destinationBuffer)) { + return FALSE; + } + + return PathAppendSafe(destinationBuffer, newFileName); +} + +/** + * Obtains the path of the secure directory used to write the status and log + * files for updates applied with an elevated updater or an updater that is + * launched using the maintenance service. + * + * Example + * Destination buffer value: + * C:\Program Files (x86)\Mozilla Maintenance Service\UpdateLogs + * + * @param outBuf + * A buffer of size MAX_PATH + 1 to store the result. + * @return TRUE if successful + */ +BOOL GetSecureOutputDirectoryPath(LPWSTR outBuf) { + PWSTR progFilesX86; + if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramFilesX86, KF_FLAG_CREATE, + nullptr, &progFilesX86))) { + return FALSE; + } + if (wcslen(progFilesX86) > MAX_PATH) { + CoTaskMemFree(progFilesX86); + return FALSE; + } + wcsncpy(outBuf, progFilesX86, MAX_PATH + 1); + CoTaskMemFree(progFilesX86); + + if (!PathAppendSafe(outBuf, L"Mozilla Maintenance Service")) { + return FALSE; + } + + // Create the Maintenance Service directory in case it doesn't exist. + if (!CreateDirectoryW(outBuf, nullptr) && + GetLastError() != ERROR_ALREADY_EXISTS) { + return FALSE; + } + + if (!PathAppendSafe(outBuf, L"UpdateLogs")) { + return FALSE; + } + + // Create the secure update output directory in case it doesn't exist. + if (!CreateDirectoryW(outBuf, nullptr) && + GetLastError() != ERROR_ALREADY_EXISTS) { + return FALSE; + } + + return TRUE; +} + +/** + * Obtains the name of the update output file using the update patch directory + * path and file extension (must include the '.' separator) passed to this + * function. + * + * Example + * Patch directory path parameter: + * C:\ProgramData\Mozilla\updates\0123456789ABCDEF\updates\0 + * File extension parameter: + * .status + * Destination buffer value: + * 0123456789ABCDEF.status + * + * @param patchDirPath + * The path to the update patch directory. + * @param fileExt + * The file extension for the file including the '.' separator. + * @param outBuf + * A buffer of size MAX_PATH + 1 to store the result. + * @return TRUE if successful + */ +BOOL GetSecureOutputFileName(LPCWSTR patchDirPath, LPCWSTR fileExt, + LPWSTR outBuf) { + size_t fullPathLen = wcslen(patchDirPath); + if (fullPathLen > MAX_PATH) { + return FALSE; + } + + size_t relPathLen = wcslen(PATCH_DIR_PATH); + if (relPathLen > fullPathLen) { + return FALSE; + } + + // The patch directory path must end with updates\0 for updates applied with + // an elevated updater or an updater that is launched using the maintenance + // service. + if (_wcsnicmp(patchDirPath + fullPathLen - relPathLen, PATCH_DIR_PATH, + relPathLen) != 0) { + return FALSE; + } + + wcsncpy(outBuf, patchDirPath, MAX_PATH + 1); + if (!PathRemoveFileSpecW(outBuf)) { + return FALSE; + } + + if (!PathRemoveFileSpecW(outBuf)) { + return FALSE; + } + + PathStripPathW(outBuf); + + size_t outBufLen = wcslen(outBuf); + size_t fileExtLen = wcslen(fileExt); + if (outBufLen + fileExtLen > MAX_PATH) { + return FALSE; + } + + wcsncat(outBuf, fileExt, fileExtLen); + + return TRUE; +} + +/** + * Obtains the full path of the secure update output file using the update patch + * directory path and file extension (must include the '.' separator) passed to + * this function. + * + * Example + * Patch directory path parameter: + * C:\ProgramData\Mozilla\updates\0123456789ABCDEF\updates\0 + * File extension parameter: + * .status + * Destination buffer value: + * C:\Program Files (x86)\Mozilla Maintenance + * Service\UpdateLogs\0123456789ABCDEF.status + * + * @param patchDirPath + * The path to the update patch directory. + * @param fileExt + * The file extension for the file including the '.' separator. + * @param outBuf + * A buffer of size MAX_PATH + 1 to store the result. + * @return TRUE if successful + */ +BOOL GetSecureOutputFilePath(LPCWSTR patchDirPath, LPCWSTR fileExt, + LPWSTR outBuf) { + if (!GetSecureOutputDirectoryPath(outBuf)) { + return FALSE; + } + + WCHAR statusFileName[MAX_PATH + 1] = {L'\0'}; + if (!GetSecureOutputFileName(patchDirPath, fileExt, statusFileName)) { + return FALSE; + } + + return PathAppendSafe(outBuf, statusFileName); +} + +/** + * Writes a UUID to the ID file in the secure output directory. This is used by + * the unelevated updater to determine whether an existing update status file in + * the secure output directory has been updated. + * + * @param patchDirPath + * The path to the update patch directory. + * @return TRUE if successful + */ +BOOL WriteSecureIDFile(LPCWSTR patchDirPath) { + WCHAR uuidString[MAX_PATH + 1] = {L'\0'}; + if (!GetUUIDString(uuidString)) { + return FALSE; + } + + WCHAR idFilePath[MAX_PATH + 1] = {L'\0'}; + if (!GetSecureOutputFilePath(patchDirPath, L".id", idFilePath)) { + return FALSE; + } + + FILE* idFile = _wfopen(idFilePath, L"wb+"); + if (idFile == nullptr) { + return FALSE; + } + + if (fprintf(idFile, "%ls\n", uuidString) == -1) { + fclose(idFile); + return FALSE; + } + + fclose(idFile); + + return TRUE; +} + +/** + * Removes the update status and log files from the secure output directory. + * + * @param patchDirPath + * The path to the update patch directory. + */ +void RemoveSecureOutputFiles(LPCWSTR patchDirPath) { + WCHAR filePath[MAX_PATH + 1] = {L'\0'}; + if (GetSecureOutputFilePath(patchDirPath, L".id", filePath)) { + (void)_wremove(filePath); + } + if (GetSecureOutputFilePath(patchDirPath, L".status", filePath)) { + (void)_wremove(filePath); + } + if (GetSecureOutputFilePath(patchDirPath, L".log", filePath)) { + (void)_wremove(filePath); + } +} + +#ifdef MOZ_MAINTENANCE_SERVICE +/** + * Starts the upgrade process for update of the service if it is + * already installed. + * + * @param installDir the installation directory where + * maintenanceservice_installer.exe is located. + * @return TRUE if successful + */ +BOOL StartServiceUpdate(LPCWSTR installDir) { + // Get a handle to the local computer SCM database + SC_HANDLE manager = OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (!manager) { + return FALSE; + } + + // Open the service + SC_HANDLE svc = OpenServiceW(manager, SVC_NAME, SERVICE_ALL_ACCESS); + if (!svc) { + CloseServiceHandle(manager); + return FALSE; + } + + // If we reach here, then the service is installed, so + // proceed with upgrading it. + + CloseServiceHandle(manager); + + // The service exists and we opened it, get the config bytes needed + DWORD bytesNeeded; + if (!QueryServiceConfigW(svc, nullptr, 0, &bytesNeeded) && + GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + CloseServiceHandle(svc); + return FALSE; + } + + // Get the service config information, in particular we want the binary + // path of the service. + UniquePtr<char[]> serviceConfigBuffer = MakeUnique<char[]>(bytesNeeded); + if (!QueryServiceConfigW( + svc, + reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()), + bytesNeeded, &bytesNeeded)) { + CloseServiceHandle(svc); + return FALSE; + } + + CloseServiceHandle(svc); + + QUERY_SERVICE_CONFIGW& serviceConfig = + *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()); + + PathUnquoteSpacesW(serviceConfig.lpBinaryPathName); + + // Obtain the temp path of the maintenance service binary + WCHAR tmpService[MAX_PATH + 1] = {L'\0'}; + if (!PathGetSiblingFilePath(tmpService, serviceConfig.lpBinaryPathName, + L"maintenanceservice_tmp.exe")) { + return FALSE; + } + + if (wcslen(installDir) > MAX_PATH) { + return FALSE; + } + + // Get the new maintenance service path from the install dir + WCHAR newMaintServicePath[MAX_PATH + 1] = {L'\0'}; + wcsncpy(newMaintServicePath, installDir, MAX_PATH); + PathAppendSafe(newMaintServicePath, L"maintenanceservice.exe"); + + // Copy the temp file in alongside the maintenace service. + // This is a requirement for maintenance service upgrades. + if (!CopyFileW(newMaintServicePath, tmpService, FALSE)) { + return FALSE; + } + + // Check that the copied file's certificate matches the expected name and + // issuer stored in the registry for this installation and that the + // certificate is trusted by the system's certificate store. + if (!DoesBinaryMatchAllowedCertificates(installDir, tmpService)) { + DeleteFileW(tmpService); + return FALSE; + } + + // Start the upgrade comparison process + STARTUPINFOW si = {0}; + si.cb = sizeof(STARTUPINFOW); + // No particular desktop because no UI + si.lpDesktop = const_cast<LPWSTR>(L""); // -Wwritable-strings + PROCESS_INFORMATION pi = {0}; + WCHAR cmdLine[64] = {'\0'}; + wcsncpy(cmdLine, L"dummyparam.exe upgrade", + sizeof(cmdLine) / sizeof(cmdLine[0]) - 1); + BOOL svcUpdateProcessStarted = + CreateProcessW(tmpService, cmdLine, nullptr, nullptr, FALSE, 0, nullptr, + installDir, &si, &pi); + if (svcUpdateProcessStarted) { + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + return svcUpdateProcessStarted; +} + +/** + * Executes a maintenance service command + * + * @param argc The total number of arguments in argv + * @param argv An array of null terminated strings to pass to the service, + * @return ERROR_SUCCESS if the service command was started. + * Less than 16000, a windows system error code from StartServiceW + * More than 20000, 20000 + the last state of the service constant if + * the last state is something other than stopped. + * 17001 if the SCM could not be opened + * 17002 if the service could not be opened + */ +DWORD +StartServiceCommand(int argc, LPCWSTR* argv) { + DWORD lastState = WaitForServiceStop(SVC_NAME, 5); + if (lastState != SERVICE_STOPPED) { + return 20000 + lastState; + } + + // Get a handle to the SCM database. + SC_HANDLE serviceManager = OpenSCManager( + nullptr, nullptr, SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE); + if (!serviceManager) { + return 17001; + } + + // Get a handle to the service. + SC_HANDLE service = OpenServiceW(serviceManager, SVC_NAME, SERVICE_START); + if (!service) { + CloseServiceHandle(serviceManager); + return 17002; + } + + // Wait at most 5 seconds trying to start the service in case of errors + // like ERROR_SERVICE_DATABASE_LOCKED or ERROR_SERVICE_REQUEST_TIMEOUT. + const DWORD maxWaitMS = 5000; + DWORD currentWaitMS = 0; + DWORD lastError = ERROR_SUCCESS; + while (currentWaitMS < maxWaitMS) { + BOOL result = StartServiceW(service, argc, argv); + if (result) { + lastError = ERROR_SUCCESS; + break; + } else { + lastError = GetLastError(); + } + Sleep(100); + currentWaitMS += 100; + } + CloseServiceHandle(service); + CloseServiceHandle(serviceManager); + return lastError; +} + +/** + * Launch a service initiated action for a software update with the + * specified arguments. + * + * @param argc The total number of arguments in argv + * @param argv An array of null terminated strings to pass to the exePath, + * argv[0] must be the path to the updater.exe + * @return ERROR_SUCCESS if successful + */ +DWORD +LaunchServiceSoftwareUpdateCommand(int argc, LPCWSTR* argv) { + // The service command is the same as the updater.exe command line except + // it has 4 extra args: + // 0) The name of the service, automatically added by Windows + // 1) "MozillaMaintenance" (I think this is redundant with 0) + // 2) The command being executed, which is "software-update" + // 3) The path to updater.exe (from argv[0]) + LPCWSTR* updaterServiceArgv = new LPCWSTR[argc + 2]; + updaterServiceArgv[0] = L"MozillaMaintenance"; + updaterServiceArgv[1] = L"software-update"; + + for (int i = 0; i < argc; ++i) { + updaterServiceArgv[i + 2] = argv[i]; + } + + // Execute the service command by starting the service with + // the passed in arguments. + DWORD ret = StartServiceCommand(argc + 2, updaterServiceArgv); + delete[] updaterServiceArgv; + return ret; +} + +/** + * Writes a specific failure code for the update status to a file in the secure + * output directory. The status file's name without the '.' separator and + * extension is the same as the update directory name. + * + * @param patchDirPath + * The path of the update patch directory. + * @param errorCode + * Error code to set + * @return TRUE if successful + */ +BOOL WriteStatusFailure(LPCWSTR patchDirPath, int errorCode) { + WCHAR statusFilePath[MAX_PATH + 1] = {L'\0'}; + if (!GetSecureOutputFilePath(patchDirPath, L".status", statusFilePath)) { + return FALSE; + } + + HANDLE hStatusFile = CreateFileW(statusFilePath, GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, 0, nullptr); + if (hStatusFile == INVALID_HANDLE_VALUE) { + return FALSE; + } + + char failure[32]; + sprintf(failure, "failed: %d", errorCode); + DWORD toWrite = strlen(failure); + DWORD wrote; + BOOL ok = WriteFile(hStatusFile, failure, toWrite, &wrote, nullptr); + CloseHandle(hStatusFile); + + if (!ok || wrote != toWrite) { + return FALSE; + } + + return TRUE; +} + +/** + * Waits for a service to enter a stopped state. + * This function does not stop the service, it just blocks until the service + * is stopped. + * + * @param serviceName The service to wait for. + * @param maxWaitSeconds The maximum number of seconds to wait + * @return state of the service after a timeout or when stopped. + * A value of 255 is returned for an error. Typical values are: + * SERVICE_STOPPED 0x00000001 + * SERVICE_START_PENDING 0x00000002 + * SERVICE_STOP_PENDING 0x00000003 + * SERVICE_RUNNING 0x00000004 + * SERVICE_CONTINUE_PENDING 0x00000005 + * SERVICE_PAUSE_PENDING 0x00000006 + * SERVICE_PAUSED 0x00000007 + * last status not set 0x000000CF + * Could no query status 0x000000DF + * Could not open service, access denied 0x000000EB + * Could not open service, invalid handle 0x000000EC + * Could not open service, invalid name 0x000000ED + * Could not open service, does not exist 0x000000EE + * Could not open service, other error 0x000000EF + * Could not open SCM, access denied 0x000000FD + * Could not open SCM, database does not exist 0x000000FE; + * Could not open SCM, other error 0x000000FF; + * Note: The strange choice of error codes above SERVICE_PAUSED are chosen + * in case Windows comes out with other service stats higher than 7, they + * would likely call it 8 and above. JS code that uses this in TestAUSHelper + * only handles values up to 255 so that's why we don't use GetLastError + * directly. + */ +DWORD +WaitForServiceStop(LPCWSTR serviceName, DWORD maxWaitSeconds) { + // 0x000000CF is defined above to be not set + DWORD lastServiceState = 0x000000CF; + + // Get a handle to the SCM database. + SC_HANDLE serviceManager = OpenSCManager( + nullptr, nullptr, SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE); + if (!serviceManager) { + DWORD lastError = GetLastError(); + switch (lastError) { + case ERROR_ACCESS_DENIED: + return 0x000000FD; + case ERROR_DATABASE_DOES_NOT_EXIST: + return 0x000000FE; + default: + return 0x000000FF; + } + } + + // Get a handle to the service. + SC_HANDLE service = + OpenServiceW(serviceManager, serviceName, SERVICE_QUERY_STATUS); + if (!service) { + DWORD lastError = GetLastError(); + CloseServiceHandle(serviceManager); + switch (lastError) { + case ERROR_ACCESS_DENIED: + return 0x000000EB; + case ERROR_INVALID_HANDLE: + return 0x000000EC; + case ERROR_INVALID_NAME: + return 0x000000ED; + case ERROR_SERVICE_DOES_NOT_EXIST: + return 0x000000EE; + default: + return 0x000000EF; + } + } + + DWORD currentWaitMS = 0; + SERVICE_STATUS_PROCESS ssp; + ssp.dwCurrentState = lastServiceState; + while (currentWaitMS < maxWaitSeconds * 1000) { + DWORD bytesNeeded; + if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssp, + sizeof(SERVICE_STATUS_PROCESS), &bytesNeeded)) { + DWORD lastError = GetLastError(); + switch (lastError) { + case ERROR_INVALID_HANDLE: + ssp.dwCurrentState = 0x000000D9; + break; + case ERROR_ACCESS_DENIED: + ssp.dwCurrentState = 0x000000DA; + break; + case ERROR_INSUFFICIENT_BUFFER: + ssp.dwCurrentState = 0x000000DB; + break; + case ERROR_INVALID_PARAMETER: + ssp.dwCurrentState = 0x000000DC; + break; + case ERROR_INVALID_LEVEL: + ssp.dwCurrentState = 0x000000DD; + break; + case ERROR_SHUTDOWN_IN_PROGRESS: + ssp.dwCurrentState = 0x000000DE; + break; + // These 3 errors can occur when the service is not yet stopped but + // it is stopping. + case ERROR_INVALID_SERVICE_CONTROL: + case ERROR_SERVICE_CANNOT_ACCEPT_CTRL: + case ERROR_SERVICE_NOT_ACTIVE: + currentWaitMS += 50; + Sleep(50); + continue; + default: + ssp.dwCurrentState = 0x000000DF; + } + + // We couldn't query the status so just break out + break; + } + + // The service is already in use. + if (ssp.dwCurrentState == SERVICE_STOPPED) { + break; + } + currentWaitMS += 50; + Sleep(50); + } + + lastServiceState = ssp.dwCurrentState; + CloseServiceHandle(service); + CloseServiceHandle(serviceManager); + return lastServiceState; +} +#endif + +/** + * Determines if there is at least one process running for the specified + * application. A match will be found across any session for any user. + * + * @param process The process to check for existance + * @return ERROR_NOT_FOUND if the process was not found + * ERROR_SUCCESS if the process was found and there were no errors + * Other Win32 system error code for other errors + **/ +DWORD +IsProcessRunning(LPCWSTR filename) { + // Take a snapshot of all processes in the system. + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (INVALID_HANDLE_VALUE == snapshot) { + return GetLastError(); + } + + PROCESSENTRY32W processEntry; + processEntry.dwSize = sizeof(PROCESSENTRY32W); + if (!Process32FirstW(snapshot, &processEntry)) { + DWORD lastError = GetLastError(); + CloseHandle(snapshot); + return lastError; + } + + do { + if (wcsicmp(filename, processEntry.szExeFile) == 0) { + CloseHandle(snapshot); + return ERROR_SUCCESS; + } + } while (Process32NextW(snapshot, &processEntry)); + CloseHandle(snapshot); + return ERROR_NOT_FOUND; +} + +/** + * Waits for the specified application to exit. + * + * @param filename The application to wait for. + * @param maxSeconds The maximum amount of seconds to wait for all + * instances of the application to exit. + * @return ERROR_SUCCESS if no instances of the application exist + * WAIT_TIMEOUT if the process is still running after maxSeconds. + * Any other Win32 system error code. + */ +DWORD +WaitForProcessExit(LPCWSTR filename, DWORD maxSeconds) { + DWORD applicationRunningError = WAIT_TIMEOUT; + for (DWORD i = 0; i < maxSeconds; i++) { + DWORD applicationRunningError = IsProcessRunning(filename); + if (ERROR_NOT_FOUND == applicationRunningError) { + return ERROR_SUCCESS; + } + Sleep(1000); + } + + if (ERROR_SUCCESS == applicationRunningError) { + return WAIT_TIMEOUT; + } + + return applicationRunningError; +} + +#ifdef MOZ_MAINTENANCE_SERVICE +/** + * Determines if the fallback key exists or not + * + * @return TRUE if the fallback key exists and there was no error checking + */ +BOOL DoesFallbackKeyExist() { + HKEY testOnlyFallbackKey; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, TEST_ONLY_FALLBACK_KEY_PATH, 0, + KEY_READ | KEY_WOW64_64KEY, + &testOnlyFallbackKey) != ERROR_SUCCESS) { + return FALSE; + } + + RegCloseKey(testOnlyFallbackKey); + return TRUE; +} + +/** + * Determines if the file system for the specified file handle is local + * @param file path to check the filesystem type for, must be at most MAX_PATH + * @param isLocal out parameter which will hold TRUE if the drive is local + * @return TRUE if the call succeeded + */ +BOOL IsLocalFile(LPCWSTR file, BOOL& isLocal) { + WCHAR rootPath[MAX_PATH + 1] = {L'\0'}; + if (wcslen(file) > MAX_PATH) { + return FALSE; + } + + wcsncpy(rootPath, file, MAX_PATH); + PathStripToRootW(rootPath); + isLocal = GetDriveTypeW(rootPath) == DRIVE_FIXED; + return TRUE; +} + +/** + * Determines the DWORD value of a registry key value + * + * @param key The base key to where the value name exists + * @param valueName The name of the value + * @param retValue Out parameter which will hold the value + * @return TRUE on success + */ +static BOOL GetDWORDValue(HKEY key, LPCWSTR valueName, DWORD& retValue) { + DWORD regDWORDValueSize = sizeof(DWORD); + LONG retCode = + RegQueryValueExW(key, valueName, 0, nullptr, + reinterpret_cast<LPBYTE>(&retValue), ®DWORDValueSize); + return ERROR_SUCCESS == retCode; +} + +/** + * Determines if the the system's elevation type allows + * unprmopted elevation. + * + * @param isUnpromptedElevation Out parameter which specifies if unprompted + * elevation is allowed. + * @return TRUE if the user can actually elevate and the value was obtained + * successfully. + */ +BOOL IsUnpromptedElevation(BOOL& isUnpromptedElevation) { + if (!UACHelper::CanUserElevate()) { + return FALSE; + } + + LPCWSTR UACBaseRegKey = + L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"; + HKEY baseKey; + LONG retCode = + RegOpenKeyExW(HKEY_LOCAL_MACHINE, UACBaseRegKey, 0, KEY_READ, &baseKey); + if (retCode != ERROR_SUCCESS) { + return FALSE; + } + + DWORD consent, secureDesktop; + BOOL success = GetDWORDValue(baseKey, L"ConsentPromptBehaviorAdmin", consent); + success = success && + GetDWORDValue(baseKey, L"PromptOnSecureDesktop", secureDesktop); + + RegCloseKey(baseKey); + if (success) { + isUnpromptedElevation = !consent && !secureDesktop; + } + + return success; +} +#endif diff --git a/toolkit/mozapps/update/common/updatehelper.h b/toolkit/mozapps/update/common/updatehelper.h new file mode 100644 index 0000000000..b346893835 --- /dev/null +++ b/toolkit/mozapps/update/common/updatehelper.h @@ -0,0 +1,39 @@ +/* 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/. */ + +#ifdef MOZ_MAINTENANCE_SERVICE +BOOL StartServiceUpdate(LPCWSTR installDir); +DWORD LaunchServiceSoftwareUpdateCommand(int argc, LPCWSTR* argv); +BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode); +DWORD WaitForServiceStop(LPCWSTR serviceName, DWORD maxWaitSeconds); +BOOL DoesFallbackKeyExist(); +BOOL IsLocalFile(LPCWSTR file, BOOL& isLocal); +DWORD StartServiceCommand(int argc, LPCWSTR* argv); +BOOL IsUnpromptedElevation(BOOL& isUnpromptedElevation); +#endif + +DWORD WaitForProcessExit(LPCWSTR filename, DWORD maxSeconds); +DWORD IsProcessRunning(LPCWSTR filename); +BOOL GetSecureOutputDirectoryPath(LPWSTR outBuf); +BOOL GetSecureOutputFilePath(LPCWSTR patchDirPath, LPCWSTR fileExt, + LPWSTR outBuf); +BOOL WriteSecureIDFile(LPCWSTR patchDirPath); +void RemoveSecureOutputFiles(LPCWSTR patchDirPath); + +#define PATCH_DIR_PATH L"\\updates\\0" + +#ifdef MOZ_MAINTENANCE_SERVICE +# define SVC_NAME L"MozillaMaintenance" + +# define BASE_SERVICE_REG_KEY L"SOFTWARE\\Mozilla\\MaintenanceService" + +// The test only fallback key, as its name implies, is only present on machines +// that will use automated tests. Since automated tests always run from a +// different directory for each test, the presence of this key bypasses the +// "This is a valid installation directory" check. This key also stores +// the allowed name and issuer for cert checks so that the cert check +// code can still be run unchanged. +# define TEST_ONLY_FALLBACK_KEY_PATH \ + BASE_SERVICE_REG_KEY L"\\3932ecacee736d366d6436db0f55bce4" +#endif diff --git a/toolkit/mozapps/update/common/updatererrors.h b/toolkit/mozapps/update/common/updatererrors.h new file mode 100644 index 0000000000..8154b68452 --- /dev/null +++ b/toolkit/mozapps/update/common/updatererrors.h @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef UPDATEERRORS_H +#define UPDATEERRORS_H + +#define OK 0 + +// Error codes that are no longer used should not be used again unless they +// aren't used in client code (e.g. UpdateService.jsm, updates.js, etc.). + +#define MAR_ERROR_EMPTY_ACTION_LIST 1 +#define LOADSOURCE_ERROR_WRONG_SIZE 2 + +// Error codes 3-16 are for general update problems. +#define USAGE_ERROR 3 +#define CRC_ERROR 4 +#define PARSE_ERROR 5 +#define READ_ERROR 6 +#define WRITE_ERROR 7 +// #define UNEXPECTED_ERROR 8 // Replaced with errors 38-42 +#define ELEVATION_CANCELED 9 +#define READ_STRINGS_MEM_ERROR 10 +#define ARCHIVE_READER_MEM_ERROR 11 +#define BSPATCH_MEM_ERROR 12 +#define UPDATER_MEM_ERROR 13 +#define UPDATER_QUOTED_PATH_MEM_ERROR 14 +#define BAD_ACTION_ERROR 15 +#define STRING_CONVERSION_ERROR 16 + +// Error codes 17-23 are related to security tasks for MAR +// signing and MAR protection. +#define CERT_LOAD_ERROR 17 +#define CERT_HANDLING_ERROR 18 +#define CERT_VERIFY_ERROR 19 +#define ARCHIVE_NOT_OPEN 20 +#define COULD_NOT_READ_PRODUCT_INFO_BLOCK_ERROR 21 +#define MAR_CHANNEL_MISMATCH_ERROR 22 +#define VERSION_DOWNGRADE_ERROR 23 + +// Error codes 24-33 and 49-58 are for the Windows maintenance service. +// Note: If more maintenance service error codes are added, the implementations +// of IsServiceSpecificErrorCode in updater.cpp and UpdateService.jsm should be +// updated to account for them. +#define SERVICE_UPDATER_COULD_NOT_BE_STARTED 24 +#define SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS 25 +#define SERVICE_UPDATER_SIGN_ERROR 26 +#define SERVICE_UPDATER_COMPARE_ERROR 27 +#define SERVICE_UPDATER_IDENTITY_ERROR 28 +#define SERVICE_STILL_APPLYING_ON_SUCCESS 29 +#define SERVICE_STILL_APPLYING_ON_FAILURE 30 +#define SERVICE_UPDATER_NOT_FIXED_DRIVE 31 +#define SERVICE_COULD_NOT_LOCK_UPDATER 32 +#define SERVICE_INSTALLDIR_ERROR 33 + +#define NO_INSTALLDIR_ERROR 34 +#define WRITE_ERROR_ACCESS_DENIED 35 +// #define WRITE_ERROR_SHARING_VIOLATION 36 // Replaced with errors 46-48 +#define WRITE_ERROR_CALLBACK_APP 37 +#define UPDATE_SETTINGS_FILE_CHANNEL 38 +#define UNEXPECTED_XZ_ERROR 39 +#define UNEXPECTED_MAR_ERROR 40 +#define UNEXPECTED_BSPATCH_ERROR 41 +#define UNEXPECTED_FILE_OPERATION_ERROR 42 +#define UNEXPECTED_STAGING_ERROR 43 +#define DELETE_ERROR_STAGING_LOCK_FILE 44 +#define DELETE_ERROR_EXPECTED_DIR 46 +#define DELETE_ERROR_EXPECTED_FILE 47 +#define RENAME_ERROR_EXPECTED_FILE 48 + +// Error codes 24-33 and 49-58 are for the Windows maintenance service. +// Note: If more maintenance service error codes are added, the implementations +// of IsServiceSpecificErrorCode in updater.cpp and UpdateService.jsm should be +// updated to account for them. +#define SERVICE_COULD_NOT_COPY_UPDATER 49 +#define SERVICE_STILL_APPLYING_TERMINATED 50 +#define SERVICE_STILL_APPLYING_NO_EXIT_CODE 51 +#define SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR 52 +#define SERVICE_CALC_REG_PATH_ERROR 53 +#define SERVICE_INVALID_APPLYTO_DIR_ERROR 54 +#define SERVICE_INVALID_INSTALL_DIR_PATH_ERROR 55 +#define SERVICE_INVALID_WORKING_DIR_PATH_ERROR 56 +#define SERVICE_INSTALL_DIR_REG_ERROR 57 +#define SERVICE_UPDATE_STATUS_UNCHANGED 58 + +#define WRITE_ERROR_FILE_COPY 61 +#define WRITE_ERROR_DELETE_FILE 62 +#define WRITE_ERROR_OPEN_PATCH_FILE 63 +#define WRITE_ERROR_PATCH_FILE 64 +#define WRITE_ERROR_APPLY_DIR_PATH 65 +#define WRITE_ERROR_CALLBACK_PATH 66 +#define WRITE_ERROR_FILE_ACCESS_DENIED 67 +#define WRITE_ERROR_DIR_ACCESS_DENIED 68 +#define WRITE_ERROR_DELETE_BACKUP 69 +#define WRITE_ERROR_EXTRACT 70 +#define REMOVE_FILE_SPEC_ERROR 71 +#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 UPDATE_STATUS_UNCHANGED 79 + +// Error codes 80 through 99 are reserved for UpdateService.jsm + +// 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 +#define FALLBACKKEY_LAUNCH_ERROR 104 + +#define SILENT_UPDATE_NEEDED_ELEVATION_ERROR 105 +#define WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION 106 + +// Error codes 110 and 111 are reserved for UpdateService.jsm + +#endif // UPDATEERRORS_H diff --git a/toolkit/mozapps/update/common/updateutils_win.cpp b/toolkit/mozapps/update/common/updateutils_win.cpp new file mode 100644 index 0000000000..fc2554e569 --- /dev/null +++ b/toolkit/mozapps/update/common/updateutils_win.cpp @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include "updateutils_win.h" +#include <errno.h> +#include <shlwapi.h> +#include <string.h> + +/** + * Note: The reason that these functions are separated from those in + * updatehelper.h/updatehelper.cpp is that those functions are strictly + * used within the updater, whereas changing functions in updateutils_win + * will have effects reaching beyond application update. + */ + +// This section implements the minimum set of dirent APIs used by updater.cpp on +// Windows. If updater.cpp is modified to use more of this API, we need to +// implement those parts here too. +static dirent gDirEnt; + +DIR::DIR(const WCHAR* path) : findHandle(INVALID_HANDLE_VALUE) { + memset(name, 0, sizeof(name)); + wcsncpy(name, path, sizeof(name) / sizeof(name[0])); + wcsncat(name, L"\\*", sizeof(name) / sizeof(name[0]) - wcslen(name) - 1); +} + +DIR::~DIR() { + if (findHandle != INVALID_HANDLE_VALUE) { + FindClose(findHandle); + } +} + +dirent::dirent() { d_name[0] = L'\0'; } + +DIR* opendir(const WCHAR* path) { return new DIR(path); } + +int closedir(DIR* dir) { + delete dir; + return 0; +} + +dirent* readdir(DIR* dir) { + WIN32_FIND_DATAW data; + if (dir->findHandle != INVALID_HANDLE_VALUE) { + BOOL result = FindNextFileW(dir->findHandle, &data); + if (!result) { + if (GetLastError() != ERROR_NO_MORE_FILES) { + errno = ENOENT; + } + return 0; + } + } else { + // Reading the first directory entry + dir->findHandle = FindFirstFileW(dir->name, &data); + if (dir->findHandle == INVALID_HANDLE_VALUE) { + if (GetLastError() == ERROR_FILE_NOT_FOUND) { + errno = ENOENT; + } else { + errno = EBADF; + } + return 0; + } + } + size_t direntBufferLength = + sizeof(gDirEnt.d_name) / sizeof(gDirEnt.d_name[0]); + wcsncpy(gDirEnt.d_name, data.cFileName, direntBufferLength); + // wcsncpy does not guarantee a null-terminated string if the source string is + // too long. + gDirEnt.d_name[direntBufferLength - 1] = '\0'; + return &gDirEnt; +} + +/** + * Joins a base directory path with a filename. + * + * @param base The base directory path of size MAX_PATH + 1 + * @param extra The filename to append + * @return TRUE if the file name was successful appended to base + */ +BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra) { + if (wcslen(base) + wcslen(extra) >= MAX_PATH) { + return FALSE; + } + + return PathAppendW(base, extra); +} + +/** + * Obtains a uuid as a wide string. + * + * @param outBuf + * A buffer of size MAX_PATH + 1 to store the result. + * @return TRUE if successful + */ +BOOL GetUUIDString(LPWSTR outBuf) { + UUID uuid; + RPC_WSTR uuidString = nullptr; + + // Note: the return value of UuidCreate should always be RPC_S_OK on systems + // after Win2K / Win2003 due to the network hardware address no longer being + // used to create the UUID. + if (UuidCreate(&uuid) != RPC_S_OK) { + return FALSE; + } + if (UuidToStringW(&uuid, &uuidString) != RPC_S_OK) { + return FALSE; + } + if (!uuidString) { + return FALSE; + } + + if (wcslen(reinterpret_cast<LPCWSTR>(uuidString)) > MAX_PATH) { + return FALSE; + } + wcsncpy(outBuf, reinterpret_cast<LPCWSTR>(uuidString), MAX_PATH + 1); + RpcStringFreeW(&uuidString); + + return TRUE; +} + +/** + * Build a temporary file path whose name component is a UUID. + * + * @param basePath The base directory path for the temp file + * @param prefix Optional prefix for the beginning of the file name + * @param tmpPath Output full path, with the base directory and the file + * name. Must already have been allocated with size >= MAX_PATH. + * @return TRUE if tmpPath was successfully filled in, FALSE on errors + */ +BOOL GetUUIDTempFilePath(LPCWSTR basePath, LPCWSTR prefix, LPWSTR tmpPath) { + WCHAR filename[MAX_PATH + 1] = {L"\0"}; + if (prefix) { + if (wcslen(prefix) > MAX_PATH) { + return FALSE; + } + wcsncpy(filename, prefix, MAX_PATH + 1); + } + + WCHAR tmpFileNameString[MAX_PATH + 1] = {L"\0"}; + if (!GetUUIDString(tmpFileNameString)) { + return FALSE; + } + + size_t tmpFileNameStringLen = wcslen(tmpFileNameString); + if (wcslen(filename) + tmpFileNameStringLen > MAX_PATH) { + return FALSE; + } + wcsncat(filename, tmpFileNameString, tmpFileNameStringLen); + + size_t basePathLen = wcslen(basePath); + if (basePathLen > MAX_PATH) { + return FALSE; + } + // Use basePathLen + 1 so wcsncpy will add null termination and if a caller + // doesn't allocate MAX_PATH + 1 for tmpPath this won't fail when there is + // actually enough space allocated. + wcsncpy(tmpPath, basePath, basePathLen + 1); + if (!PathAppendSafe(tmpPath, filename)) { + return FALSE; + } + + return TRUE; +} diff --git a/toolkit/mozapps/update/common/updateutils_win.h b/toolkit/mozapps/update/common/updateutils_win.h new file mode 100644 index 0000000000..9de5914741 --- /dev/null +++ b/toolkit/mozapps/update/common/updateutils_win.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef WINDIRENT_H__ +#define WINDIRENT_H__ + +/** + * Note: The reason that these functions are separated from those in + * updatehelper.h/updatehelper.cpp is that those functions are strictly + * used within the updater, whereas changing functions in updateutils_win + * will have effects reaching beyond application update. + */ + +#ifndef XP_WIN +# error This library should only be used on Windows +#endif + +#include <windows.h> + +struct DIR { + explicit DIR(const WCHAR* path); + ~DIR(); + HANDLE findHandle; + WCHAR name[MAX_PATH + 1]; +}; + +struct dirent { + dirent(); + WCHAR d_name[MAX_PATH + 1]; +}; + +DIR* opendir(const WCHAR* path); +int closedir(DIR* dir); +dirent* readdir(DIR* dir); + +// This is the length of the UUID string including null termination returned by +// GetUUIDString. +#define UUID_LEN 37 + +BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); +BOOL GetUUIDString(LPWSTR outBuf); +BOOL GetUUIDTempFilePath(LPCWSTR basePath, LPCWSTR prefix, LPWSTR tmpPath); + +#endif // WINDIRENT_H__ diff --git a/toolkit/mozapps/update/components.conf b/toolkit/mozapps/update/components.conf new file mode 100644 index 0000000000..0399ee423a --- /dev/null +++ b/toolkit/mozapps/update/components.conf @@ -0,0 +1,38 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}', + 'contract_ids': ['@mozilla.org/updates/update-service;1'], + 'jsm': 'resource://gre/modules/UpdateService.jsm', + 'constructor': 'UpdateService', + 'singleton': True, + }, + { + 'cid': '{093C2356-4843-4C65-8709-D7DBCBBE7DFB}', + 'contract_ids': ['@mozilla.org/updates/update-manager;1'], + 'jsm': 'resource://gre/modules/UpdateService.jsm', + 'constructor': 'UpdateManager', + 'singleton': True, + }, + { + 'cid': '{898CDC9B-E43F-422F-9CC4-2F6291B415A3}', + 'contract_ids': ['@mozilla.org/updates/update-checker;1'], + 'jsm': 'resource://gre/modules/UpdateService.jsm', + 'constructor': 'CheckerService', + 'singleton': True, + }, + + { + 'cid': '{e43b0010-04ba-4da6-b523-1f92580bc150}', + 'contract_ids': ['@mozilla.org/updates/update-service-stub;1'], + 'jsm': 'resource://gre/modules/UpdateServiceStub.jsm', + 'constructor': 'UpdateServiceStub', + 'categories': {'profile-after-change': 'nsUpdateServiceStub'}, + 'singleton': True, + }, +] diff --git a/toolkit/mozapps/update/content/history.js b/toolkit/mozapps/update/content/history.js new file mode 100644 index 0000000000..51de669af3 --- /dev/null +++ b/toolkit/mozapps/update/content/history.js @@ -0,0 +1,96 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +var gUpdateHistory = { + _view: null, + + /** + * Initialize the User Interface + */ + onLoad() { + this._view = document.getElementById("historyItems"); + + var um = Cc["@mozilla.org/updates/update-manager;1"].getService( + Ci.nsIUpdateManager + ); + var uc = um.getUpdateCount(); + if (uc) { + while (this._view.hasChildNodes()) { + this._view.firstChild.remove(); + } + + for (var i = 0; i < uc; ++i) { + var update = um.getUpdateAt(i); + + if (!update || !update.name) { + continue; + } + + // Don't display updates that are downloading since they don't have + // valid statusText for the UI (bug 485493). + if (!update.statusText) { + continue; + } + + var element = document.createXULElement("richlistitem"); + element.className = "update"; + + const topLine = document.createXULElement("hbox"); + const nameLabel = document.createXULElement("label"); + nameLabel.className = "update-name"; + document.l10n.setAttributes(nameLabel, "update-full-build-name", { + name: update.name, + buildID: update.buildID, + }); + topLine.appendChild(nameLabel); + + if (update.detailsURL) { + const detailsLink = document.createXULElement("label", { + is: "text-link", + }); + detailsLink.href = update.detailsURL; + document.l10n.setAttributes(detailsLink, "update-details"); + topLine.appendChild(detailsLink); + } + + const installedOnLabel = document.createXULElement("label"); + installedOnLabel.className = "update-installedOn-label"; + document.l10n.setAttributes(installedOnLabel, "update-installed-on", { + date: this._formatDate(update.installDate), + }); + + const statusLabel = document.createXULElement("label"); + statusLabel.className = "update-status-label"; + document.l10n.setAttributes(statusLabel, "update-status", { + status: update.statusText, + }); + + element.append(topLine, installedOnLabel, statusLabel); + this._view.appendChild(element); + } + } + var cancelbutton = document.getElementById("history").getButton("cancel"); + cancelbutton.focus(); + }, + + /** + * Formats a date into human readable form + * @param seconds + * A date in seconds since 1970 epoch + * @returns A human readable date string + */ + _formatDate(seconds) { + var date = new Date(seconds); + const dtOptions = { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }; + return date.toLocaleString(undefined, dtOptions); + }, +}; diff --git a/toolkit/mozapps/update/content/history.xhtml b/toolkit/mozapps/update/content/history.xhtml new file mode 100644 index 0000000000..9297260f08 --- /dev/null +++ b/toolkit/mozapps/update/content/history.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE window> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://mozapps/skin/update/updates.css"?> + +<window windowtype="Update:History" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + style="min-width: 35em;" + data-l10n-id="close-button-label" + data-l10n-attrs="title" + onload="gUpdateHistory.onLoad();"> +<dialog id="history" + buttons="cancel" + defaultButton="cancel" + data-l10n-id="close-button-label" + data-l10n-attrs="buttonlabelcancel"> + + <linkset> + <html:link rel="localization" href="toolkit/updates/history.ftl"/> + </linkset> + + <script src="chrome://mozapps/content/update/history.js"/> + + <label data-l10n-id="history-intro"></label> + <separator class="thin"/> + <richlistbox id="historyItems" flex="1"> + <label data-l10n-id="no-updates-label"></label> + </richlistbox> + <separator class="thin"/> +</dialog> +</window> diff --git a/toolkit/mozapps/update/content/updateElevation.js b/toolkit/mozapps/update/content/updateElevation.js new file mode 100644 index 0000000000..251a4b5618 --- /dev/null +++ b/toolkit/mozapps/update/content/updateElevation.js @@ -0,0 +1,138 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * + * 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/. */ + +/* This is temporary until bug 1521632 is fixed */ + +"use strict"; + +/* import-globals-from /toolkit/content/contentAreaUtils.js */ + +const gUpdateElevationDialog = { + openUpdateURL(event) { + if (event.button == 0) { + openURL(event.target.getAttribute("url")); + } + }, + getAUSString(key, strings) { + if (strings) { + return this.strings.getFormattedString(key, strings); + } + return this.strings.getString(key); + }, + _setButton(button, string) { + var label = this.getAUSString(string); + if (label.includes("%S")) { + label = label.replace(/%S/, this.brandName); + } + button.label = label; + button.setAttribute("accesskey", this.getAUSString(string + ".accesskey")); + }, + onLoad() { + this.strings = document.getElementById("updateStrings"); + this.brandName = document + .getElementById("brandStrings") + .getString("brandShortName"); + + let um = Cc["@mozilla.org/updates/update-manager;1"].getService( + Ci.nsIUpdateManager + ); + let update = um.readyUpdate; + let updateFinishedName = document.getElementById("updateFinishedName"); + updateFinishedName.value = update.name; + + let link = document.getElementById("detailsLinkLabel"); + if (update.detailsURL) { + link.setAttribute("url", update.detailsURL); + // The details link is stealing focus so it is disabled by default and + // should only be enabled after onPageShow has been called. + link.disabled = false; + } else { + link.hidden = true; + } + + let manualLinkLabel = document.getElementById("manualLinkLabel"); + let manualURL = Services.urlFormatter.formatURLPref( + "app.update.url.manual" + ); + manualLinkLabel.value = manualURL; + manualLinkLabel.setAttribute("url", manualURL); + + let button = document.getElementById("elevateExtra2"); + this._setButton(button, "restartLaterButton"); + button = document.getElementById("elevateExtra1"); + this._setButton(button, "noThanksButton"); + button = document.getElementById("elevateAccept"); + this._setButton(button, "restartNowButton"); + button.focus(); + }, + onRestartLater() { + window.close(); + }, + onNoThanks() { + Services.obs.notifyObservers(null, "update-canceled"); + let um = Cc["@mozilla.org/updates/update-manager;1"].getService( + Ci.nsIUpdateManager + ); + let update = um.readyUpdate; + um.cleanupReadyUpdate(); + // Since the user has clicked "No Thanks", we should not prompt them to update to + // this version again unless they manually select "Check for Updates..." + // which will clear app.update.elevate.never preference. + let aus = Cc["@mozilla.org/updates/update-service;1"].getService( + Ci.nsIApplicationUpdateService + ); + if (aus.elevationRequired && update) { + Services.prefs.setCharPref("app.update.elevate.never", update.appVersion); + } + window.close(); + }, + onRestartNow() { + // disable the "finish" (Restart) and "extra1" (Later) buttons + // because the Software Update wizard is still up at the point, + // and will remain up until we return and we close the + // window with a |window.close()| in wizard.xml + // (it was the firing the "wizardfinish" event that got us here.) + // This prevents the user from switching back + // to the Software Update dialog and clicking "Restart" or "Later" + // when dealing with the "confirm close" prompts. + // See bug #350299 for more details. + document.getElementById("elevateExtra2").disabled = true; + document.getElementById("elevateExtra1").disabled = true; + document.getElementById("elevateAccept").disabled = true; + + // This dialog was shown because elevation was required so there is no need + // to check if elevation is required again. + let um = Cc["@mozilla.org/updates/update-manager;1"].getService( + Ci.nsIUpdateManager + ); + um.elevationOptedIn(); + + // Notify all windows that an application quit has been requested. + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + + // Something aborted the quit process. + if (cancelQuit.data) { + return; + } + + // If already in safe mode restart in safe mode (bug 327119) + if (Services.appinfo.inSafeMode) { + Services.env.set("MOZ_SAFE_MODE_RESTART", "1"); + } + + // Restart the application + Services.startup.quit( + Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart + ); + }, +}; diff --git a/toolkit/mozapps/update/content/updateElevation.xhtml b/toolkit/mozapps/update/content/updateElevation.xhtml new file mode 100644 index 0000000000..c6275c1314 --- /dev/null +++ b/toolkit/mozapps/update/content/updateElevation.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!-- This is temporary until bug 1521632 is fixed --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://mozapps/skin/update/updates.css"?> + +<window windowtype="Update:Elevation" + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="elevation-update-wizard" + data-l10n-attrs="title" + style="width: auto; height: auto" + onload="gUpdateElevationDialog.onLoad();"> +<dialog id="updates" + buttons="extra2,extra1,accept"> + + <script src="chrome://global/content/contentAreaUtils.js"/> + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + <script src="chrome://mozapps/content/update/updateElevation.js"/> + +<linkset> + <html:link rel="localization" href="branding/brand.ftl"/> + <html:link rel="localization" href="toolkit/updates/elevation.ftl"/> +</linkset> + +#if defined(XP_MACOSX) && MOZ_BUILD_APP == browser +#include ../../../../browser/base/content/macWindow.inc.xhtml +#endif + + <stringbundleset id="updateSet"> + <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/> + <stringbundle id="updateStrings" src="chrome://mozapps/locale/update/updates.properties"/> + </stringbundleset> + + <vbox id="elevationBox"> + <hbox class="update-header" flex="1"> + <vbox class="update-header-box-1"> + <vbox class="update-header-box-text"> + <label class="update-header-label" data-l10n-id="elevation-finished-page"/> + </vbox> + </vbox> + </hbox> + <vbox class="update-content" flex="1"> + <label data-l10n-id="elevation-finished-background-page"/> + <separator/> + <hbox align="center"> + <label data-l10n-id="elevation-finished-background"/> + <label id="updateFinishedName" flex="1" crop="right" value=""/> + <label id="detailsLinkLabel" disabled="true" is="text-link" + data-l10n-id="elevation-details-link-label" + onclick="gUpdateElevationDialog.openUpdateURL(event);"/> + </hbox> + <spacer flex="1"/> + <label id="finishedBackgroundMoreElevated" data-l10n-id="elevation-more-elevated"/> + <label data-l10n-id="elevation-error-manual"/> + <hbox> + <label id="manualLinkLabel" is="text-link" value="" + onclick="gUpdateElevationDialog.openUpdateURL(event);"/> + </hbox> + </vbox> + </vbox> + <separator class="groove update-buttons-separator"/> + <hbox id="update-button-box" pack="end"> + <button id="elevateExtra2" dlgtype="extra2" label="" class="dialog-button" + oncommand="gUpdateElevationDialog.onRestartLater();" /> + <button id="elevateExtra1" dlgtype="extra1" label="" class="dialog-button" + oncommand="gUpdateElevationDialog.onNoThanks();" /> + <spacer flex="1"/> + <button id="elevateAccept" dlgtype="accept" label="" class="dialog-button" + oncommand="gUpdateElevationDialog.onRestartNow();" default="true"/> + </hbox> +</dialog> +</window> diff --git a/toolkit/mozapps/update/docs/BackgroundUpdates.rst b/toolkit/mozapps/update/docs/BackgroundUpdates.rst new file mode 100644 index 0000000000..c692f4958a --- /dev/null +++ b/toolkit/mozapps/update/docs/BackgroundUpdates.rst @@ -0,0 +1,224 @@ +================== +Background Updates +================== + +The purpose of the background update system is to perform application updates +during times when Firefox is not running. It was originally implemented in `bug +1689520 <https://bugzilla.mozilla.org/show_bug.cgi?id=1689520>`__. + +The system has three main tasks it needs to handle: + +1. :ref:`Determining whether background updates are possible <background-updates-determining>` + +2. :ref:`Scheduling background tasks <background-updates-scheduling>` + +3. :ref:`Checking for updates <background-updates-checking>` + +Architecturally, the background task is an instance of Firefox running in a +special background mode, not a separate tool. This allows it to leverage +existing functionality in Firefox, including the existing update code, but also +keep acceptable performance characteristics for a background task by controlling +and limiting the parts of Firefox that are loaded. + +Everything in this document applies only to Microsoft Windows systems. In the +future, we would like to extend background update support to macOS (see `bug +1653435 <https://bugzilla.mozilla.org/show_bug.cgi?id=1653435>`__), however +support for Linux and other Unix variants is not planned due to the variation in +OS-level scheduling affordances across distributions/configurations. + +Lifecycle +========= + +When background updates are possible, the background update task will be invoked +every 7 hours (by default). The first invocation initiates an update download +which proceeds after the task exits using Windows BITS. The second invocation +prepares and stages the update. The third invocation installs the update as it +starts up, and then checks for a newer update, possibly initiating another +update download. The cycle then continues. If the user launches Firefox at any +point in this process, it will take over. If the background update task is +invoked while Firefox proper is running, the task exits without doing any work. +In the future, the second invocation will stage and then restart to finish +installing the update, rather than waiting for the third invocation (see `bug +1704855 <https://bugzilla.mozilla.org/show_bug.cgi?id=1704855>`__). + +.. _background-updates-determining: + +Determining whether background updates are possible +=================================================== + +Configuration +------------- + +Updating Firefox, by definition, is an operation that applies to a Firefox +installation. However, Firefox configuration is generally done via preference +values and other files which are stored in a Firefox profile, and in general +profiles do not correspond 1:1 with installations. This raises the question of +how the configuration for something like the background updater should be +managed. We deal with this question in two different ways. + +There are two main preferences specifically relevant to updates. Those +are ``app.update.auto``, which controls whether updates should be +downloaded automatically at all, even if Firefox is running, and +``app.update.background.enabled``, to specifically control whether to +use the background update system. We store these preferences in the +update root directory, which is located in a per-installation location +outside of any profile. Any profile loaded in that installation can +observe and control these settings. + +But there are some other pieces of state which absolutely must come from a +profile, such as the telemetry client ID and logging level settings (see +`BackgroundTasksUtils.jsm <https://searchfox.org/mozilla-central/source/toolkit/components/backgroundtasks/BackgroundTasksUtils.jsm>`__). + +This means that, in addition to our per-installation prefs, we also need +to be able to identify and load a profile. To do that, we leverage `the profile +service <https://searchfox.org/mozilla-central/source/toolkit/profile/nsIToolkitProfileService.idl>`__ +to determine what the default profile for the installation would be if we were +running a normal browser session, and the background updater always uses it. + +Criteria +-------- + +The default profile must satisfy several conditions in order for background +updates to be scheduled. None of these confounding factors are present in fully +default configurations, but some are relatively common. See +`BackgroundUpdate.REASON <https://searchfox.org/mozilla-central/search?q=symbol:BackgroundUpdate%23REASON>`__ +for all the details. + +In order for the background task to be scheduled: + +- The per-installation ``app.update.background.enabled`` pref must be + true + +- The per-installation ``app.update.auto`` pref must be true (the + default) + +- The installation must have been created by an installer executable and not by + manually extracting an archive file + +- The current OS user must be capable of updating the installation based on its + file system permissions, either by having permission to write to application + files directly or by using the Mozilla Maintenance Service (which also + requires that it be installed and enabled, as it is by default) + +- BITS must be enabled via ``app.update.BITS.enabled`` (the default) + +- Firefox proxy server settings must not be configured (the default) + +- ``app.update.langpack.enabled`` must be false, or otherwise there must be no + langpacks installed. Background tasks cannot update addons such as langpacks, + because they are installed into a profile, and langpacks that are not + precisely matched with the version of Firefox that is installed can cause + YSOD failures (see `bug 1647443 <https://bugzilla.mozilla.org/show_bug.cgi?id=1647443>`__), + so background updating in the presence of langpacks is too risky. + +If any per-installation prefs are changed while the default profile is not +running, the background update task will witness the changed prefs during its +next scheduled run, and exit if appropriate. The background task will not be +unscheduled at that point; that is delayed until a browser session is run with +the default profile (it should be possible for the background update task to +unschedule itself, but currently we prefer the simplicity of handling all +scheduling tasks from a single location). + +In the extremely unusual case when prefs belonging to the default profile are +modified outside of Firefox (with a text editor, say), then the +background task will generally pick up those changes with no action needed, +because it will fish the changed settings directly from the profile. + +.. _background-updates-scheduling: + +Scheduling background tasks +=========================== + +We use OS-level scheduling mechanisms to schedule the command ``firefox +--backgroundtask backgroundupdate`` to run on a particular cadence. This cadence +is controlled by the ``app.update.background.interval`` preference, which +defaults to 7 hours. + +On Windows, we use the `Task Scheduler +API <https://docs.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page>`__; +on macOS this will use +`launchd <https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html>`__. +For platform-specific scheduling details, see the +`TaskScheduler.jsm <https://searchfox.org/mozilla-central/source/toolkit/components/taskscheduler/TaskScheduler.jsm>`__ +module. + +These background tasks are scheduled per OS user and run with that user’s +permissions. No additional privileges are requested or needed, regardless of the +user account's status, because we have already verified that either the user has +all the permissions they need or that the Maintenance Service can be used. + +Scheduling is done from within Firefox (or a background task) itself. To +reduce shared state, only the *default* Firefox profile will interact +with the OS-level task scheduling mechanism. + +.. _background-updates-checking: + +Checking for updates +==================== + +After verifying all the preconditions and exiting immediately if any do not +hold, the ``backgroundupdate`` task then verifies that it is the only Firefox +instance running (as determined by a multi-instance lock, see `bug +1553982 <https://bugzilla.mozilla.org/show_bug.cgi?id=1553982>`__), since +otherwise it would be unsafe to continue performing any update work. + +The task then fishes configuration settings from the default profile, namely: + +- A subset of update specific preferences, such as ``app.update.log`` + +- Data reporting preferences, to ensure the task respects the user’s choices + +- The (legacy) Telemetry client ID, so that background update Telemetry + can be correlated with other Firefox Telemetry + +The background task creates a distinct profile for itself to load, because a +profile must be present in order for most of the Firefox code that it relies on +to function. This distinct profile is non-ephemeral, i.e., persistent, but not +visible to users: see `bug 1775132 +<https://bugzilla.mozilla.org/show_bug.cgi?id=1775132>`__ + +After setting up this profile and reading all the configuration we need +into it, the regular +`UpdateService.jsm <https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/UpdateService.jsm>`__ +check process is initiated. To the greatest extent possible, this process is +identical to what happens during any regular browsing session. + +Specific topics +=============== + +User interface +-------------- + +The background update task must not produce any user-visible interface. If it +did, whatever appeared would be \*disembodied\*, unconnected to any usage of +Firefox itself and appearing to a user as a weird, scary popup that came out of +nowhere. To this end, we disable all UI within the updater when invoking +from a background task. See `bug +1696276 <https://bugzilla.mozilla.org/show_bug.cgi?id=1696276>`__. + +This point also means that we cannot prompt for user elevation (on Windows this +would mean a UAC prompt) from within the task, so we have to make very sure that +we will be able to perform an update without needing to elevate. By default on +Windows we are able to do this because of the presence of the Maintenance +Service, but it may be disabled or not installed, so we still have to check. + +Staging +------- + +The background update task will follow the update staging setting in the user’s +default profile. The default setting is to enable staging, so most users will +have it. In the future, background update tasks will recognize when an update +has been staged and try to restart to finalize the staged update (see `bug +1704855 <https://bugzilla.mozilla.org/show_bug.cgi?id=1704855>`__). Background +tasks cannot finalize a staged update in all cases however; for one example, see +`bug 1695797 <https://bugzilla.mozilla.org/show_bug.cgi?id=1695797>`__, where we +ensure that background tasks do not finalize a staged update while other +instances of the application are running. + +Staging is enabled by default because it provides a marked improvement in +startup time for a browsing session. Without staging, browser startup following +retrieving an update would be blocked on extracting the update archive and +patching each individual application file. Staging does all of that in advance, +so that all that needs to be done to complete an update (and therefore all that +needs to be done during the startup path), is to move the already patched (that +is, staged) files into place, a much faster and less resource intensive job. diff --git a/toolkit/mozapps/update/docs/MaintenanceServiceTests.rst b/toolkit/mozapps/update/docs/MaintenanceServiceTests.rst new file mode 100644 index 0000000000..b954b572f8 --- /dev/null +++ b/toolkit/mozapps/update/docs/MaintenanceServiceTests.rst @@ -0,0 +1,103 @@ +Maintenance Service Tests +========================= + +The automated tests for the Mozilla Maintenance Service are a bit tricky. They +are located in ``toolkit/mozapps/update/tests/unit_service_updater/`` and they +allow for automated testing of application update using the Service. + +In automation, everything gets signed and the tests properly check that things +like certificate verification work. If, however, you want to run the tests +locally, the MAR and the updater binary will not be signed. Thus, the +verification of those certificates will fail and the tests will not run +properly. + +We don't want these tests to just always fail if someone runs large amounts of +tests locally. To avoid this, the tests basically just unconditionally pass if +you run them locally and don't take the time to set them up properly. + +If you want them to actually run locally, you will need to set up your +environment properly. + +Setting Up to Run the Tests Locally +----------------------------------- + +In order to run the service tests locally, we have to bypass much of the +certificate verification. Thus, this method may not be helpful if you need to +test that feature in particular. + +Add Fallback Key to Registry +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, you will need to add the fallback key to the registry. Normally, the +Firefox installer writes some certificate information to a registry key in an +installation-specific place. In testing, however, we can't get the permissions +to write this key, nor can we have the test environment predict every possible +installation directory that we might test with. To get around this problem, if +the Service can't find the installation-specific key, it will check a static +fallback location. + +The easiest way to correctly set up the fallback key is to copy the text below +into a ``.reg`` file and then double click it in the file browser to merge it +into the registry. + +.. code:: + + Windows Registry Editor Version 5.00 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4\0] + "issuer"="DigiCert SHA2 Assured ID Code Signing CA" + "name"="Mozilla Corporation" + + [HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4\1] + "issuer"="DigiCert Assured ID Code Signing CA-1" + "name"="Mozilla Corporation" + + [HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4\2] + "issuer"="Mozilla Fake CA" + "name"="Mozilla Fake SPC" + +Build without Certificate Verification +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To disable certificate verification, add this build flag to your ``mozconfig`` +file: + +.. code:: + + ac_add_options --enable-unverified-updates + +You will need to rebuild for this to take effect. + +Copy the Maintenance Service Binary +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This step will assume that you already have the Maintenance Service installed. + +First, move the existing Maintenance Service binary out of the way. It will +initially be located at +``C:\Program Files (x86)\Mozilla Maintenance Service\maintenanceservice.exe``. +An easy way to do this is to append ``.bak`` to its name. You should probably +restore your original Maintenance Service binary when you are done testing. + +Now, copy the Maintenance Service binary that you built into that directory. +It will be located at ``<obj directory>\dist\bin\maintenanceservice.exe``. + +If you make changes to the Maintenance Service and rebuild, you will have to +repeat this step. + +Running the Tests +----------------- + +You should now be ready run a service test: + +.. code:: + + ./mach test toolkit/mozapps/update/tests/unit_service_updater/<test> + +Or run all of them: + +.. code:: + + ./mach test toolkit/mozapps/update/tests/unit_service_updater diff --git a/toolkit/mozapps/update/docs/SettingUpAnUpdateServer.rst b/toolkit/mozapps/update/docs/SettingUpAnUpdateServer.rst new file mode 100644 index 0000000000..cecc81a1d9 --- /dev/null +++ b/toolkit/mozapps/update/docs/SettingUpAnUpdateServer.rst @@ -0,0 +1,218 @@ +Setting Up An Update Server +=========================== + +The goal of this document is to provide instructions for installing a +locally-served Firefox update. + +Obtaining an update MAR +----------------------- + +Updates are served as MAR files. There are two common ways to obtain a +MAR to use: download a prebuilt one, or build one yourself. + +Downloading a MAR +~~~~~~~~~~~~~~~~~ + +Prebuilt Nightly MARs can be found +`here <https://archive.mozilla.org/pub/firefox/nightly/>`__ on +archive.mozilla.org. Be sure that you use the one that matches your +machine's configuration. For example, if you want the Nightly MAR from +2019-09-17 for a 64 bit Windows machine, you probably want the MAR +located at +https://archive.mozilla.org/pub/firefox/nightly/2019/09/2019-09-17-09-36-29-mozilla-central/firefox-71.0a1.en-US.win64.complete.mar. + +Prebuilt MARs for release and beta can be found +`here <https://archive.mozilla.org/pub/firefox/releases/>`__. Beta +builds are those with a ``b`` in the version string. After locating the +desired version, the MARs will be in the ``update`` directory. You want +to use the MAR labelled ``complete``, not a partial MAR. Here is an +example of an appropriate MAR file to use: +https://archive.mozilla.org/pub/firefox/releases/69.0b9/update/win64/en-US/firefox-69.0b9.complete.mar. + +Building a MAR +~~~~~~~~~~~~~~ + +Building a MAR locally is more complicated. Part of the problem is that +MARs are signed by Mozilla and so you cannot really build an "official" +MAR yourself. This is a security measure designed to prevent anyone from +serving malicious updates. If you want to use a locally-built MAR, the +copy of Firefox being updated will need to be built to allow un-signed +MARs. See :ref:`Building Firefox <Firefox Contributors' Quick Reference>` +for more information on building Firefox locally. In order to use a locally +built MAR, you will need to put this line in the mozconfig file in root of the +build directory (create it if it does not exist): + +.. code:: + + ac_add_options --enable-unverified-updates + +Firefox should otherwise be built normally. After building, you may want +to copy the installation of Firefox elsewhere. If you update the +installation without moving it, attempts at further incremental builds +will not work properly, and a clobber will be needed when building next. +To move the installation, first call ``./mach package``, then copy +``<obj dir>/dist/firefox`` elsewhere. The copied directory will be your +install directory. + +If you are running Windows and want the `Mozilla Maintenance +Service <https://support.mozilla.org/en-US/kb/what-mozilla-maintenance-service>`__ +to be used, there are a few additional steps to be taken here. First, +the maintenance service needs to be "installed". Most likely, a +different maintenance service is already installed, probably at +``C:\Program Files (x86)\Mozilla Maintenance Service\maintenanceservice.exe``. +Backup that file to another location and replace it with +``<obj dir>/dist/bin/maintenanceservice.exe``. Don't forget to restore +the backup when you are done. Next, you will need to change the +permissions on the Firefox install directory that you created. Both that +directory and its parent directory should have permissions preventing +the current user from writing to it. + +Now that you have a build of Firefox capable of using a locally-built +MAR, it's time to build the MAR. First, build Firefox the way you want +it to be after updating. If you want it to be the same before and after +updating, this step is unnecessary and you can use the same build that +you used to create the installation. Then run these commands, +substituting ``<obj dir>``, ``<MAR output path>``, ``<version>`` and +``<channel>`` appropriately: + +.. code:: bash + + $ ./mach package + $ touch "<obj dir>/dist/firefox/precomplete" + $ MAR="<obj dir>/dist/host/bin/mar.exe" MOZ_PRODUCT_VERSION=<version> MAR_CHANNEL_ID=<channel> ./tools/update-packaging/make_full_update.sh <MAR output path> "<obj dir>/dist/firefox" + +For macOS you should use these commands: + +.. code:: bash + + $ ./mach package + $ touch "<obj dir>/dist/firefox/Firefox.app/Contents/Resources/precomplete" + $ MAR="<obj dir>/dist/host/bin/mar.exe" MOZ_PRODUCT_VERSION=<version> MAR_CHANNEL_ID=<channel> ./tools/update-packaging/make_full_update.sh <MAR output path> "<obj dir>/dist/firefox/Firefox.app" + +For a local build, ``<channel>`` can be ``default``, and ``<version>`` +can be the value from ``browser/config/version.txt`` (or something +arbitrarily large like ``2000.0a1``). + +.. container:: blockIndicator note + + Note: It can be a bit tricky to get the ``make_full_update.sh`` + script to accept paths with spaces. + +Serving the update +------------------ + +Preparing the update files +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, create the directory that updates will be served from and put the +MAR file in it. Then, create a file within called ``update.xml`` with +these contents, replacing ``<mar name>``, ``<hash>`` and ``<size>`` with +the MAR's filename, its sha512 hash, and its file size in bytes. + +:: + + <?xml version="1.0" encoding="UTF-8"?> + <updates> + <update type="minor" displayVersion="2000.0a1" appVersion="2000.0a1" platformVersion="2000.0a1" buildID="21181002100236"> + <patch type="complete" URL="http://127.0.0.1:8000/<mar name>" hashFunction="sha512" hashValue="<hash>" size="<size>"/> + </update> + </updates> + +If you've downloaded the MAR you're using, you'll find the sha512 value +in a file called SHA512SUMS in the root of the release directory on +archive.mozilla.org for a release or beta build (you'll have to search +it for the file name of your MAR, since it includes the sha512 for every +file that's part of that release), and for a nightly build you'll find a +file with a .checksums extension adjacent to your MAR that contains that +information (for instance, for the MAR file at +https://archive.mozilla.org/pub/firefox/nightly/2019/09/2019-09-17-09-36-29-mozilla-central/firefox-71.0a1.en-US.win64.complete.mar, +the file +https://archive.mozilla.org/pub/firefox/nightly/2019/09/2019-09-17-09-36-29-mozilla-central/firefox-71.0a1.en-US.win64.checksums +contains the sha512 for that file as well as for all the other win64 +files that are part of that nightly release). + +If you've built your own MAR, you can obtain its sha512 checksum by +running the following command, which should work in Linux, macOS, or +Windows in the MozillaBuild environment: + +.. code:: + + shasum --algorithm 512 <filename> + +On Windows, you can get the exact file size in bytes for your MAR by +right clicking on it in the file explorer and selecting Properties. +You'll find the correct size in bytes at the end of the line that begins +"Size", **not** the one that begins "Size on disk". Be sure to remove +the commas when you paste this number into the XML file. + +On macOS, you can get the exact size of your MAR by running the command: + +.. code:: + + stat -f%z <filename> + +Or on Linux, the same command would be: + +.. code:: + + stat --format "%s" <filename> + +Starting your update server +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now, start an update server to serve the update files on port 8000. An +easy way to do this is with Python. Remember to navigate to the correct +directory before starting the server. This is the Python2 command: + +.. code:: bash + + $ python -m SimpleHTTPServer 8000 + +or, this is the Python3 command: + +.. code:: bash + + $ python3 -m http.server 8000 + +.. container:: blockIndicator note + + If you aren't sure that you started the server correctly, try using a + web browser to navigate to ``http://127.0.0.1:8000/update.xml`` and + make sure that you get the XML file you created earlier. + +Installing the update +--------------------- + +You may want to start by deleting any pending updates to ensure that no +previously found updates interfere with installing the desired update. +You can use this command with Firefox's browser console to determine the +update directory: + +.. code:: + + ChromeUtils.importESModule("resource://gre/modules/FileUtils.sys.mjs").FileUtils.getDir("UpdRootD", [], false).path + +Once you have determined the update directory, close Firefox, browse to +the directory and remove the subdirectory called ``updates``. + +| Next, you need to change the update URL to point to the local XML + file. This can be done most reliably with an enterprise policy. The + policy file location depends on the operating system you are using. +| Windows/Linux: ``<install dir>/distribution/policies.json`` +| macOS: ``<install dir>/Contents/Resources/distribution/policies.json`` +| Create the ``distribution`` directory, if necessary, and put this in + ``policies.json``: + +:: + + { + "policies": { + "AppUpdateURL": "http://127.0.0.1:8000/update.xml" + } + } + +Now you are ready to update! Launch Firefox out of its installation +directory and navigate to the Update section ``about:preferences``. You +should see it downloading the update to the update directory. Since the +transfer is entirely local this should finish quickly, and a "Restart to +Update" button should appear. Click it to restart and apply the update. diff --git a/toolkit/mozapps/update/docs/index.rst b/toolkit/mozapps/update/docs/index.rst new file mode 100644 index 0000000000..d6cc9f5e64 --- /dev/null +++ b/toolkit/mozapps/update/docs/index.rst @@ -0,0 +1,10 @@ +================== +Application Update +================== + +.. toctree:: + :maxdepth: 1 + + BackgroundUpdates + MaintenanceServiceTests + SettingUpAnUpdateServer diff --git a/toolkit/mozapps/update/jar.mn b/toolkit/mozapps/update/jar.mn new file mode 100644 index 0000000000..0ff9cdae61 --- /dev/null +++ b/toolkit/mozapps/update/jar.mn @@ -0,0 +1,10 @@ +# 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/. + +toolkit.jar: +% content mozapps %content/mozapps/ + content/mozapps/update/history.xhtml (content/history.xhtml) + content/mozapps/update/history.js (content/history.js) + content/mozapps/update/updateElevation.js (content/updateElevation.js) +* content/mozapps/update/updateElevation.xhtml (content/updateElevation.xhtml) diff --git a/toolkit/mozapps/update/metrics.yaml b/toolkit/mozapps/update/metrics.yaml new file mode 100644 index 0000000000..2a57dbce8b --- /dev/null +++ b/toolkit/mozapps/update/metrics.yaml @@ -0,0 +1,407 @@ +# 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/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - "Toolkit :: Application Update" + +background_update: + client_id: + type: uuid + description: > + The legacy Telemetry client ID of this installation's default profile. + + The default profile is as determined by the Profile Service, namely + `nsIToolkitProfileService.defaultProfile`. The majority of users have + only one Firefox installation and only one profile, so the default profile + is their regular browsing profile. + + It is possible for a Firefox installation to not have a default profile, + but in such cases the background update task will abort before sending any + telemetry; therefore, the legacy Telemetry client ID should always be + present. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1794053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1794053 + data_sensitivity: + - highly_sensitive + - technical + notification_emails: + - install-update@mozilla.com + expires: never + no_lint: + - BASELINE_PING + send_in_pings: + - background-update + - metrics + - events + - baseline + + targeting_exists: + type: boolean + description: > + True if the default profile had a targeting snapshot serialized to disk, + and there was no exception thrown reading it. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + targeting_exception: + type: boolean + description: > + True if the default profile had a targeting snapshot serialized to disk, + but an exception was thrown reading it. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + targeting_version: + type: quantity + unit: version number + description: > + If the default profile had a targeting snapshot serialized to disk, the + `version` of the snapshot. + + This version number does not have a physical unit: it's only useful to + compare between versions. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + targeting_env_firefox_version: + type: quantity + unit: version number + description: > + The `environment.firefoxVersion` of the default profile's serialized + targeting snapshot. At the time of writing, this version is an integer + representing the Firefox major version, e.g., `109`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_sensitivity: + - interaction + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + targeting_env_current_date: + type: datetime + time_unit: day + description: > + The `environment.currentDate` of the default profile's serialized + targeting snapshot. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_sensitivity: + - interaction + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + targeting_env_profile_age: + type: datetime + time_unit: day + description: > + The `environment.profileAgeCreated` of the default profile's serialized + targeting snapshot. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1795467 + data_sensitivity: + - interaction + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + final_state: + type: string + description: > + String description of the final state the update state machine reached. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + states: + type: string_list + description: > + Ordered list of string descriptions of the states that the update state + machine reached. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + reasons: + type: string_list + description: > + List of reasons that the background update task did not run. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + exit_code_success: + type: boolean + description: > + True if the exit code/status of the background update task is 0, which + means success. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + exit_code_exception: + type: boolean + description: > + True if the exit code/status of the background update task is 3, which + means an exception was thrown. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + +update: + service_enabled: + type: boolean + description: > + Preference "app.update.service.enabled": whether the Mozilla Maintenance + Service is enabled. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + auto_download: + type: boolean + description: > + Per-installation preference "app.update.auto": whether to fetch and + install updates without user intervention. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + background_update: + type: boolean + description: > + Per-installation preference "app.update.background.enabled": whether to + fetch and install updates in the background when Firefox is not running. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + enabled: + type: boolean + description: > + True when policies are disabled or when the "DisableAppUpdate" is not in + effect. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + channel: + type: string + description: > + The update channel. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + can_usually_apply_updates: + type: boolean + description: > + Whether or not the Update Service can usually download and install + updates. + See `canUsuallyApplyUpdates` in + https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/nsIUpdateService.idl. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + can_usually_check_for_updates: + type: boolean + description: > + Whether or not the Update Service can usually check for updates. + See `canUsuallyCheckForUpdates` in + https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/nsIUpdateService.idl. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + can_usually_stage_updates: + type: boolean + description: > + Whether the Update Service is usually able to stage updates. + See `canUsuallyStageUpdates` in + https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/nsIUpdateService.idl. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update + + can_usually_use_bits: + type: boolean + description: > + On Windows, whether the Update Service can usually use BITS. + See `canUsuallyUseBits` in + https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/nsIUpdateService.idl. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - background-update diff --git a/toolkit/mozapps/update/moz.build b/toolkit/mozapps/update/moz.build new file mode 100644 index 0000000000..05fa8c3a37 --- /dev/null +++ b/toolkit/mozapps/update/moz.build @@ -0,0 +1,58 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +SPHINX_TREES["docs"] = "docs" + +XPIDL_MODULE = "update" + +DIRS += [ + "updater", +] + +XPIDL_SOURCES += [ + "nsIUpdateService.idl", +] + +TEST_DIRS += ["tests"] +MARIONETTE_UNIT_MANIFESTS += ["tests/marionette/marionette.ini"] + +EXTRA_COMPONENTS += [ + "nsUpdateService.manifest", +] + +EXTRA_JS_MODULES += [ + "AppUpdater.jsm", + "UpdateListener.jsm", + "UpdateService.jsm", + "UpdateServiceStub.jsm", + "UpdateTelemetry.jsm", +] + +# This is Firefox-only for now simply because the `backgroundupdate` uses +# `AppUpdater.jsm`, which is Firefox-only. But there's nothing truly specific +# to Firefox here: that module could be generalized to toolkit/, or the +# functionality rewritten to consume App Update Service directly. +if ( + CONFIG["MOZ_BUILD_APP"] == "browser" + and CONFIG["MOZ_BACKGROUNDTASKS"] + and CONFIG["MOZ_UPDATE_AGENT"] +): + EXTRA_JS_MODULES += [ + "BackgroundUpdate.jsm", + ] + + EXTRA_JS_MODULES.backgroundtasks += [ + "BackgroundTask_backgroundupdate.sys.mjs", + ] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +JAR_MANIFESTS += ["jar.mn"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Application Update") diff --git a/toolkit/mozapps/update/nsIUpdateService.idl b/toolkit/mozapps/update/nsIUpdateService.idl new file mode 100644 index 0000000000..9c20bcf4ed --- /dev/null +++ b/toolkit/mozapps/update/nsIUpdateService.idl @@ -0,0 +1,752 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsIRequest; +interface nsIRequestObserver; +interface nsISimpleEnumerator; +interface nsIFile; + +webidl Element; +webidl Document; + +/** + * An interface that describes an object representing a patch file that can + * be downloaded and applied to a version of this application so that it + * can be updated. + */ +[scriptable, uuid(dc8fb8a9-3a53-4031-9469-2a5197ea30e7)] +interface nsIUpdatePatch : nsISupports +{ + /** + * The type of this patch: + * "partial" A binary difference between two application versions + * "complete" A complete patch containing all of the replacement files + * to update to the new version + */ + readonly attribute AString type; + + /** + * The URL this patch was being downloaded from + */ + readonly attribute AString URL; + + /** + * The final URL this patch was being downloaded from + */ + attribute AString finalURL; + + /** + * The size of this file, in bytes. + */ + readonly attribute unsigned long size; + + /** + * The state of this patch + */ + attribute AString state; + + /** + * A numeric error code that conveys additional information about the state of + * a failed update. If the update is not in the "failed" state the value is + * zero. The possible values are located in common/updatererrors.h and values between + * 80 and 99 are in nsUpdateService.js. + */ + attribute long errorCode; + + /** + * true if this patch is currently selected as the patch to be downloaded and + * installed for this update transaction, false if another patch from this + * update has been selected. + */ + attribute boolean selected; + + /** + * Serializes this patch object into a DOM Element + * @param updates + * The document to serialize into + * @returns The DOM Element created by the serialization process + */ + Element serialize(in Document updates); +}; + +/** + * An interface that describes an object representing an available update to + * the current application - this update may have several available patches + * from which one must be selected to download and install, for example we + * might select a binary difference patch first and attempt to apply that, + * then if the application process fails fall back to downloading a complete + * file-replace patch. This object also contains information about the update + * that the front end and other application services can use to learn more + * about what is going on. + */ +[scriptable, uuid(e094c045-f4ff-41fd-92da-cd2effd2c7c9)] +interface nsIUpdate : nsISupports +{ + /** + * The type of update: + * "major" A major new version of the Application + * "minor" A minor update to the Application (e.g. security update) + */ + readonly attribute AString type; + + /** + * The name of the update, or "<Application Name> <Update Version>" + */ + readonly attribute AString name; + + /** + * The string to display in the user interface for the version. If you want + * a real version number use appVersion. + */ + readonly attribute AString displayVersion; + + /** + * The Application version of this update. + */ + readonly attribute AString appVersion; + + /** + * The Application version prior to the application being updated. + */ + readonly attribute AString previousAppVersion; + + /** + * The Build ID of this update. Used to determine a particular build, down + * to the hour, minute and second of its creation. This allows the system + * to differentiate between several nightly builds with the same |version| + * for example. + */ + readonly attribute AString buildID; + + /** + * The URL to a page which offers details about the content of this + * update. Ideally, this page is not the release notes but some other page + * that summarizes the differences between this update and the previous, + * which also links to the release notes. + */ + readonly attribute AString detailsURL; + + /** + * The URL to the Update Service that supplied this update. + */ + readonly attribute AString serviceURL; + + /** + * The channel used to retrieve this update from the Update Service. + */ + readonly attribute AString channel; + + /** + * Whether the update is no longer supported on this system. + */ + readonly attribute boolean unsupported; + + /** + * Allows overriding the default amount of time in seconds before prompting the + * user to apply an update. If not specified, the value of + * app.update.promptWaitTime will be used. + */ + attribute long long promptWaitTime; + + /** + * Whether or not the update being downloaded is a complete replacement of + * the user's existing installation or a patch representing the difference + * between the new version and the previous version. + */ + attribute boolean isCompleteUpdate; + + /** + * When the update was installed. + */ + attribute long long installDate; + + /** + * A message associated with this update, if any. + */ + attribute AString statusText; + + /** + * The currently selected patch for this update. + */ + readonly attribute nsIUpdatePatch selectedPatch; + + /** + * The state of the selected patch: + * "downloading" The update is being downloaded. + * "pending" The update is ready to be applied. + * "pending-service" The update is ready to be applied with the service. + * "pending-elevate" The update is ready to be applied but requires elevation. + * "applying" The update is being applied. + * "applied" The update is ready to be switched to. + * "applied-os" The update is OS update and to be installed. + * "applied-service" The update is ready to be switched to with the service. + * "succeeded" The update was successfully applied. + * "download-failed" The update failed to be downloaded. + * "failed" The update failed to be applied. + */ + attribute AString state; + + /** + * A numeric error code that conveys additional information about the state of + * a failed update. If the update is not in the "failed" state the value is + * zero. The possible values are located in common/updatererrors.h and values between + * 80 and 99 are in nsUpdateService.js. + */ + attribute long errorCode; + + /** + * Whether an elevation failure has been encountered for this update. + */ + attribute boolean elevationFailure; + + /** + * The number of patches supplied by this update. + */ + readonly attribute unsigned long patchCount; + + /** + * Retrieves a patch. + * @param index + * The index of the patch to retrieve. + * @returns The nsIUpdatePatch at the specified index. + */ + nsIUpdatePatch getPatchAt(in unsigned long index); + + /** + * Serializes this update object into a DOM Element + * @param updates + * The document to serialize into + * @returns The DOM Element created by the serialization process + */ + Element serialize(in Document updates); +}; + +/** + * An interface describing the result of an update check. + */ +[scriptable, uuid(bff08110-e79f-4a9f-a56c-348170f9208a)] +interface nsIUpdateCheckResult : nsISupports +{ + /** + * True if update checks are allowed. otherwise false. + */ + readonly attribute boolean checksAllowed; + + /** + * True if the update check succeeded, otherwise false. Guaranteed to be false + * if checksAllowed is false. + */ + readonly attribute boolean succeeded; + + /** + * The XMLHttpRequest handling the update check. Depending on exactly how the + * check failed, it's possible for this to be null. + */ + readonly attribute jsval request; + + /** + * If `!checksAllowed`, this will always be an empty array. + * + * If `succeeded`, this will be an array of nsIUpdate objects listing + * available updates. The length will be 0 if there are no available updates. + * + * If `checksAllowed && !succeeded`, this will be an array containing exactly + * one nsIUpdate object. Most of the attributes will have no useful value + * since we did not successfully retrieve an update, but `errorCode` and + * `statusText` will be set to values that describe the error encountered when + * checking for updates. + */ + readonly attribute Array<nsIUpdate> updates; +}; + +/** + * An interface describing an update check that may still be in-progress or may + * be completed. + */ +[scriptable, uuid(2620aa24-27aa-463a-b6d2-0734695c1f7a)] +interface nsIUpdateCheck : nsISupports +{ + /** + * An id that represents a particular update check. Can be passed to + * nsIUpdateChecker::stopCheck. + * + * Ids are guaranteed to be truthy (non-zero) and non-repeating. This is + * just for caller convenience so that (a) it's not an error to cancel a check + * that already completed and (b) they can easily check `if (idVar)` to see if + * they stored an id. + */ + readonly attribute long id; + + /** + * A promise that resolves to the results of the update check, which will be + * of type nsIUpdateCheckResult. + */ + readonly attribute Promise result; +}; + +/** + * An interface describing an object that knows how to check for updates. It can + * perform multiple update checks simultaneously or consolidate multiple check + * requests into a single web request, depending on whether the parameters + * specified for update checking match. + */ +[scriptable, uuid(877ace25-8bc5-452a-8586-9c1cf2871994)] +interface nsIUpdateChecker : nsISupports +{ + /** + * Enumerated constants. See the `checkType` parameter of `checkForUpdates` + * for details. + */ + const long BACKGROUND_CHECK = 1; + const long FOREGROUND_CHECK = 2; + + /** + * Checks for available updates. + * @param checkType + * Must be either BACKGROUND_CHECK or FOREGROUND_CHECK. If + * FOREGROUND_CHECK is specified, the normal + * nsIApplicationUpdateService.canCheckForUpdates check will be + * overridden and the "force" parameter will be included in the + * update URL. + * + * Regarding the "force" parameter: + * Sometimes the update server throttles updates, arbitrarily + * refraining from returning the newest version to some clients. The + * force parameter overrides this behavior and tells it to + * unconditionally return the newest available version. + * + * It's worth noting that the update server technically supports + * forcing the decision in the other direction too, preventing + * the newest version from being returned, but this interface doesn't + * actually support setting the force parameter this way. If the + * force parameter is used, it always forces getting the newest + * version. + * @returns An nsIUpdateCheck object that describes the update check and + * provides a Promise that resolves to the update check results. + */ + nsIUpdateCheck checkForUpdates(in long checkType); + + /** + * Gets the update URL. + * @param checkType + * Must be either BACKGROUND_CHECK or FOREGROUND_CHECK. See the + * checkType parameter of nsIUpdateChecker.checkForUpdates for more + * details. + * @returns A Promise that resolves to the URL to be used to check for + * updates, as a string. This URL should resolve to an XML describing + * the updates that are available to the current Firefox + * installation. + */ + Promise getUpdateURL(in long checkType); + + /** + * Ends a pending update check. Has no effect if the id is invalid or the + * check corresponding to the id has already completed. + * + * Note that because `nsIUpdateChecker` potentially combines multiple update + * checks, it is not guaranteed that this will actually cause the update + * request to be aborted. It also doesn't guarantee that + * `nsIUpdateCheck.result` will resolve when this is called. This merely marks + * the check id as cancelled and only if there are no other check ids waiting + * on the request does it abort it. + * + * @param id + * The id of a check to stop (accessible via nsIUpdateCheck). + */ + void stopCheck(in long id); + + /** + * Ends all pending update checks. + */ + void stopAllChecks(); +}; + +/** + * An interface describing a global application service that handles performing + * background update checks and provides utilities for selecting and + * downloading update patches. + */ +[scriptable, uuid(1107d207-a263-403a-b268-05772ec10757)] +interface nsIApplicationUpdateService : nsISupports +{ + /** + * Checks for available updates in the background using the listener provided + * by the application update service for background checks. + * @returns true if the update check was started, false if not. Note that the + * check starting does not necessarily mean that the check will + * succeed or that an update will be downloaded. + */ + bool checkForBackgroundUpdates(); + + /** + * Selects the best update to install from a list of available updates. + * @param updates + * An array of updates that are available + */ + nsIUpdate selectUpdate(in Array<nsIUpdate> updates); + + /** + * Adds a listener that receives progress and state information about the + * update that is currently being downloaded, e.g. to update a user + * interface. Registered listeners will be called for all downloads and all + * updates during a browser session; they are not automatically removed + * following the first (successful or failed) download. + * @param listener + * An object implementing nsIRequestObserver and optionally + * nsIProgressEventSink that is to be notified of state and + * progress information as the update is downloaded. + */ + void addDownloadListener(in nsIRequestObserver listener); + + /** + * Removes a listener that is receiving progress and state information + * about the update that is currently being downloaded. + * @param listener + * The listener object to remove. + */ + void removeDownloadListener(in nsIRequestObserver listener); + + /** + * Starts downloading the update passed. Once the update is downloaded, it + * will automatically be prepared for installation. + * + * @param update + * The update to download. + * @returns A promise that resolves to `true` if an update download was + * started, otherwise `false. + */ + Promise downloadUpdate(in nsIUpdate update); + + /** + * This is the function called internally by the Application Update Service + * when an update check is complete. Though this can be used to potentially + * start an update download, `downloadUpdate` should used for that. + * This is mostly exposed in the interface in order to make it accessible for + * testing. + */ + Promise onCheckComplete(in nsIUpdateCheckResult result); + + /** + * Stop the active update download process. This is the equivalent of + * calling nsIRequest::Cancel on the download's nsIRequest. When downloading + * with nsIIncrementalDownload, this will leave the partial download in place. + * When downloading with BITS, any partial download progress will be removed. + * + * @returns A Promise that resolves once the download has been stopped. + */ + Promise stopDownload(); + + /** + * There are a few things that can disable the Firefox updater at runtime + * such as Enterprise Policies. If this attribute is set to true, update + * should not be performed and most update interfaces will return errors. + */ + readonly attribute boolean disabled; + + /** + * Whether or not the Update Service can usually check for updates. This is a + * function of whether or not application update is disabled by the + * application and the platform the application is running on. + */ + readonly attribute boolean canUsuallyCheckForUpdates; + + /** + * Whether or not the Update Service can check for updates right now. This is + * a function of whether or not application update is disabled by the + * application, the platform the application is running on, and transient + * factors such as whether other instances are running. + */ + readonly attribute boolean canCheckForUpdates; + + /** + * Whether or not the installation requires elevation. Currently only + * implemented on OSX, returns false on other platforms. + */ + readonly attribute boolean elevationRequired; + + /** + * Whether or not the Update Service can usually download and install updates. + * On Windows, this is a function of whether or not the maintenance service + * is installed and enabled. On other systems, and as a fallback on Windows, + * this depends on whether the current user has write access to the install + * directory. + */ + readonly attribute boolean canUsuallyApplyUpdates; + + /** + * Whether or not the Update Service can download and install updates right now. + * On Windows, this is a function of whether or not the maintenance service + * is installed and enabled. On other systems, and as a fallback on Windows, + * this depends on whether the current user has write access to the install + * directory. On all systems, this includes transient factors such as whether + * other instances are running. + */ + readonly attribute boolean canApplyUpdates; + + /** + * Whether or not a different instance is handling updates of this + * installation. This currently only ever returns true on Windows + * when 2 instances of an application are open. Only one of the instances + * will actually handle updates for the installation. + */ + readonly attribute boolean isOtherInstanceHandlingUpdates; + + /** + * Whether the Update Service is usually able to stage updates. + */ + readonly attribute boolean canUsuallyStageUpdates; + + /** + * Whether the Update Service is able to stage updates right now. On all + * systems, this includes transient factors such as whether other instances + * are running. + */ + readonly attribute boolean canStageUpdates; + + /** + * On Windows, whether the Update Service can usually use BITS. + */ + readonly attribute boolean canUsuallyUseBits; + + /** + * On Windows, whether the Update Service can use BITS right now. This + * includes transient factors such as whether other instances are running. + */ + readonly attribute boolean canUseBits; + + /** + * Indicates whether or not the enterprise policy that allows only manual + * updating is active. One of the features of this policy is not being + * notified of updates; you are intended to need to manually tell Firefox + * that you want to update each time that you want to do so. + * + * This policy has some implications for the way that update checks work. We + * don't want to do background update checks. Without being able to notify + * the user, there's not really anything to do if we find one. However, we + * will allow "automatic" update checks when loading the update interfaces + * in about:preferences, the About Dialog, etc. When those interfaces are + * open, we do have a way of telling the user about an update without + * bothering them with a doorhanger. + */ + readonly attribute boolean manualUpdateOnly; + + /** + * This can be set to true to prevent updates being processed beyond starting + * an update download. This should only be used when we are being run as a + * background task. + * This exists to prevent a particularly fast update download from beginning + * to stage while the background task is shutting down. + */ + attribute boolean onlyDownloadUpdatesThisSession; + + /** + * Enumerated constants describing the update states that the updater can be + * in. + * Note that update checking is not part of the states that this interface + * can recognize and report. This is for two reasons: 1) there are multiple + * kinds of update checks (ex: foreground and background) and it would make + * the current update state more complicated if we wanted to track both, and + * 2) there isn't really any reason to concern ourselves with current update + * checks because nsIUpdateChecker will seamlessly combine multiple identical + * update checks into a single request without the caller having to worry + * about its internal state. + */ + // An update download hasn't started yet, or we failed at some point in the + // update process and aborted. + const long STATE_IDLE = 1; + // An update is currently being downloaded. + // This state begins once nsIApplicationUpdateService.downloadUpdate resolves + // to `true`. + // Note that we may be downloading an update but not be in this state because + // we can download a second update while we have an update ready. But there + // isn't much reason for currentState to track the second update. We know that + // it will always be in the downloading state until it finishes downloading + // and becomes the ready update. See STATE_UPDATE_SWAP for more details. + const long STATE_DOWNLOADING = 2; + // An update is currently being staged. Note that we do not always stage + // updates. + const long STATE_STAGING = 4; + // An update is pending. If the browser restarts now, it will be installed. + // Note that although "pending" is one of the potential update statuses, this + // does not correspond to exactly that status. + const long STATE_PENDING = 5; + // We had an update pending. Then we downloaded another update. Now we are + // in the process of removing the old update and swapping the new update into + // its place. + // Note that when the state initially changes to `STATE_SWAP`, the new update + // will be in `nsIUpdateManager.downloadingUpdate` but it will be moved into + // `nsIUpdateManager.readyUpdate` before moving to the next state. + const long STATE_SWAP = 6; + + /** + * Gets a string describing the state (mostly intended to be make console + * logs easier to read). + */ + AString getStateName(in long state); + + /** + * The current state of the application updater. Returns one of the enumerated + * constants, above. + * + * The expected flow looks like this: + * STATE_IDLE -> STATE_DOWNLOADING -> STATE_STAGING -> STATE_PENDING + * If a failure is encountered at some time, we go back to STATE_IDLE. + * If staging is not enabled, STATE_STAGING will be skipped. + * + * We may download additional updates after we reach STATE_PENDING. If we do, + * the state will remain at STATE_PENDING while we download the new update. If + * we restart during that time, the pending update will be installed and the + * partially downloaded update will be discarded. If a download completes + * successfully, there will be a brief period where STATE_PENDING is no longer + * correct, because the Update Service is in the process of removing the old + * update and replacing it with the new update. So if we restart during that + * period, the update will not be correctly installed. Thus, we switch away + * from STATE_PENDING to STATE_SWAP during that time. Assuming that the swap + * is successful, the state will then switch back STATE_STAGING (assuming that + * staging is enabled), then to STATE_PENDING. So the full expected state flow + * looks more like this: + * STATE_IDLE -> STATE_DOWNLOADING -> STATE_STAGING -> STATE_PENDING -> + * STATE_SWAP -> STATE_STAGING -> STATE_PENDING -> + * STATE_SWAP -> STATE_STAGING -> STATE_PENDING -> ... + * (Omitting STATE_STAGING if staging is not enabled). + */ + readonly attribute long currentState; + + /** + * A Promise that resolves immediately after `currentState` changes. + */ + readonly attribute Promise stateTransition; +}; + +/** + * An interface describing a component which handles the job of processing + * an update after it's been downloaded. + */ +[scriptable, uuid(74439497-d796-4915-8cef-3dfe43027e4d)] +interface nsIUpdateProcessor : nsISupports +{ + /** + * Stages an update while the application is running. + */ + void processUpdate(); + + /** + * The installer writes an installation-specific registry key if the + * Maintenance Service can be used for this installation. This function checks + * for that key's existence (it does not read or verify the key's contents). + * + * This function should only be called on Windows. + * + * @returns true if the registry key exists, false if it does not. + * @throws NS_ERROR_NOT_AVAILABLE + * If registry access fails. + * @throws NS_ERROR_NOT_IMPLEMENTED + * If this is called on a non-Windows platform. + */ + bool getServiceRegKeyExists(); +}; + +/** + * Upon creation, which should happen early during startup, the sync manager + * creates/opens and locks a file. All other running instances of the same + * installation of the app also open the same lock, so we can use it to + * determine whether any other instance is running. If so, we'll temporarily + * hold off on performing update tasks until there are no other instances or + * until a timeout expires, whichever comes first. That way we can avoid + * updating behind the back of copies that are still running, so we don't force + * all running instances to restart (see bug 1366808, where an error was added + * informing the user of the need to restart any running instances that have + * been updated). + */ +[scriptable, uuid(cf4c4487-66d9-4e18-a2e9-39002245332f)] +interface nsIUpdateSyncManager : nsISupports +{ + /** + * Returns whether another instance of this application is running. + * @returns true if another instance has the lock open, false if not + */ + bool isOtherInstanceRunning(); + + /** + * Should only be used for testing. + * + * Closes and reopens the lock file, possibly under a different name if a + * parameter is given (or the path hash has changed, which should only happen + * if a test is forcing it). + */ + void resetLock([optional] in nsIFile anAppFile); +}; + +/** + * An interface describing a global application service that maintains a list + * of updates previously performed as well as the current active update. + */ +[scriptable, uuid(0f1098e9-a447-4af9-b030-6f8f35c85f89)] +interface nsIUpdateManager : nsISupports +{ + /** + * Gets the update at the specified index + * @param index + * The index within the updates array + * @returns The nsIUpdate object at the specified index + */ + nsIUpdate getUpdateAt(in long index); + + /** + * Gets the total number of updates in the history list. + */ + long getUpdateCount(); + + /** + * The update that has been downloaded, or null if there isn't one. + */ + attribute nsIUpdate readyUpdate; + + /** + * The update that is currently downloading, or null if there isn't one. + * An update is no longer considered to be downloading once onStopRequest is + * called. This means that both onStopRequest handlers for download listeners + * and observers of the "update-downloaded" topic should expect the update + * that was just downloaded to be stored in readyUpdate, not + * downloadingUpdate. + */ + attribute nsIUpdate downloadingUpdate; + + /** + * Adds the specified update to the update history. The update history is + * limited to 10 items, so this may also remove the last item from the + * history. + */ + void addUpdateToHistory(in nsIUpdate update); + + /** + * Saves all updates to disk. + */ + void saveUpdates(); + + /** + * Refresh the update status based on the information in update.status. + * + * @returns A Promise that resolves after the update status is refreshed. + */ + Promise refreshUpdateStatus(); + + /** + * The user agreed to proceed with an elevated update and we are now + * permitted to show an elevation prompt. + */ + void elevationOptedIn(); + + /** + * These functions both clean up and remove an active update without applying + * it. The first function does this for the update that is currently being + * downloaded. The second function does this for the update that has already + * been downloaded. + */ + void cleanupDownloadingUpdate(); + void cleanupReadyUpdate(); +}; diff --git a/toolkit/mozapps/update/nsUpdateService.manifest b/toolkit/mozapps/update/nsUpdateService.manifest new file mode 100644 index 0000000000..a8d2534b1e --- /dev/null +++ b/toolkit/mozapps/update/nsUpdateService.manifest @@ -0,0 +1 @@ +category update-timer nsUpdateService @mozilla.org/updates/update-service;1,getService,background-update-timer,app.update.interval,43200,86400 diff --git a/toolkit/mozapps/update/pings.yaml b/toolkit/mozapps/update/pings.yaml new file mode 100644 index 0000000000..542f8916d8 --- /dev/null +++ b/toolkit/mozapps/update/pings.yaml @@ -0,0 +1,35 @@ +# 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/. + +--- +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 + +background-update: + description: | + This ping measures the technical health of the background update system. + Said system downloads and processes updates when Firefox is not running. It + is expected that this ping will be analyzed by humans to gain confidence in + the implementation as the staged rollout of the system proceeds to the + release channel, before settling into an automated analysis to detect spikes + in background update failure rates. This ping will also help to + characterize the update-related settings of our user population. + + Right now the background update system, and therefore this ping, is + restricted to Windows. + + This ping is submitted only by the background update task. It should be + submitted once per background update task invocation. The expected schedule + is every 7 hours, controlled by the pref `app.update.background.interval`, + and subject to scheduling decisions made by the OS. + include_client_id: true + send_if_empty: false + reasons: + backgroundupdate_task: | + The ping was sent as part of the normal background update task execution. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1703318#c17 + notification_emails: + - install-update@mozilla.com diff --git a/toolkit/mozapps/update/tests/Makefile.in b/toolkit/mozapps/update/tests/Makefile.in new file mode 100644 index 0000000000..b07afbb0a9 --- /dev/null +++ b/toolkit/mozapps/update/tests/Makefile.in @@ -0,0 +1,13 @@ +# 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/. + +ifndef MOZ_WINCONSOLE +ifdef MOZ_DEBUG +MOZ_WINCONSOLE = 1 +else +MOZ_WINCONSOLE = 0 +endif +endif + +include $(topsrcdir)/config/rules.mk diff --git a/toolkit/mozapps/update/tests/TestAUSHelper.cpp b/toolkit/mozapps/update/tests/TestAUSHelper.cpp new file mode 100644 index 0000000000..d1b32e3caf --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSHelper.cpp @@ -0,0 +1,462 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "updatedefines.h" + +#ifdef XP_WIN +# include "commonupdatedir.h" +# include "updatehelper.h" +# include "certificatecheck.h" +# define NS_main wmain +# define NS_tgetcwd _wgetcwd +# define NS_ttoi _wtoi +#else +# define NS_main main +# define NS_tgetcwd getcwd +# define NS_ttoi atoi +#endif + +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <sys/stat.h> + +static void WriteMsg(const NS_tchar* path, const char* status) { + FILE* outFP = NS_tfopen(path, NS_T("wb")); + if (!outFP) { + return; + } + + fprintf(outFP, "%s\n", status); + fclose(outFP); + outFP = nullptr; +} + +static bool CheckMsg(const NS_tchar* path, const char* expected) { + FILE* inFP = NS_tfopen(path, NS_T("rb")); + if (!inFP) { + return false; + } + + struct stat ms; + if (fstat(fileno(inFP), &ms)) { + fclose(inFP); + inFP = nullptr; + return false; + } + + char* mbuf = (char*)malloc(ms.st_size + 1); + if (!mbuf) { + fclose(inFP); + inFP = nullptr; + return false; + } + + size_t r = ms.st_size; + char* rb = mbuf; + size_t c = fread(rb, sizeof(char), 50, inFP); + r -= c; + if (c == 0 && r) { + free(mbuf); + fclose(inFP); + inFP = nullptr; + return false; + } + mbuf[ms.st_size] = '\0'; + rb = mbuf; + + bool isMatch = strcmp(rb, expected) == 0; + free(mbuf); + fclose(inFP); + inFP = nullptr; + return isMatch; +} + +int NS_main(int argc, NS_tchar** argv) { + if (argc == 2) { + if (!NS_tstrcmp(argv[1], NS_T("post-update-async")) || + !NS_tstrcmp(argv[1], NS_T("post-update-sync"))) { + NS_tchar exePath[MAXPATHLEN]; +#ifdef XP_WIN + if (!::GetModuleFileNameW(0, exePath, MAXPATHLEN)) { + return 1; + } +#else + if (!NS_tvsnprintf(exePath, sizeof(exePath) / sizeof(exePath[0]), + NS_T("%s"), argv[0])) { + return 1; + } +#endif + NS_tchar runFilePath[MAXPATHLEN]; + if (!NS_tvsnprintf(runFilePath, + sizeof(runFilePath) / sizeof(runFilePath[0]), + NS_T("%s.running"), exePath)) { + return 1; + } +#ifdef XP_WIN + if (!NS_taccess(runFilePath, F_OK)) { + // This makes it possible to check if the post update process was + // launched twice which happens when the service performs an update. + NS_tchar runFilePathBak[MAXPATHLEN]; + if (!NS_tvsnprintf(runFilePathBak, + sizeof(runFilePathBak) / sizeof(runFilePathBak[0]), + NS_T("%s.bak"), runFilePath)) { + return 1; + } + MoveFileExW(runFilePath, runFilePathBak, MOVEFILE_REPLACE_EXISTING); + } +#endif + WriteMsg(runFilePath, "running"); + + if (!NS_tstrcmp(argv[1], NS_T("post-update-sync"))) { +#ifdef XP_WIN + Sleep(2000); +#else + sleep(2); +#endif + } + + NS_tchar logFilePath[MAXPATHLEN]; + if (!NS_tvsnprintf(logFilePath, + sizeof(logFilePath) / sizeof(logFilePath[0]), + NS_T("%s.log"), exePath)) { + return 1; + } + WriteMsg(logFilePath, "post-update"); + return 0; + } + } + + if (argc < 3) { + fprintf( + stderr, + "\n" + "Application Update Service Test Helper\n" + "\n" + "Usage: WORKINGDIR INFILE OUTFILE -s SECONDS [FILETOLOCK]\n" + " or: WORKINGDIR LOGFILE [ARG2 ARG3...]\n" + " or: signature-check filepath\n" + " or: setup-symlink dir1 dir2 file symlink\n" + " or: remove-symlink dir1 dir2 file symlink\n" + " or: check-symlink symlink\n" + " or: check-umask existing-umask\n" + " or: post-update\n" + " or: create-update-dir\n" + "\n" + " WORKINGDIR \tThe relative path to the working directory to use.\n" + " INFILE \tThe relative path from the working directory for the " + "file to\n" + " \tread actions to perform such as finish.\n" + " OUTFILE \tThe relative path from the working directory for the " + "file to\n" + " \twrite status information.\n" + " SECONDS \tThe number of seconds to sleep.\n" + " FILETOLOCK \tThe relative path from the working directory to an " + "existing\n" + " \tfile to open exlusively.\n" + " \tOnly available on Windows platforms and silently " + "ignored on\n" + " \tother platforms.\n" + " LOGFILE \tThe relative path from the working directory to log " + "the\n" + " \tcommand line arguments.\n" + " ARG2 ARG3...\tArguments to write to the LOGFILE after the preceding " + "command\n" + " \tline arguments.\n" + "\n" + "Note: All paths must be relative.\n" + "\n"); + return 1; + } + + if (!NS_tstrcmp(argv[1], NS_T("check-signature"))) { +#if defined(XP_WIN) && defined(MOZ_MAINTENANCE_SERVICE) + if (ERROR_SUCCESS == VerifyCertificateTrustForFile(argv[2])) { + return 0; + } else { + return 1; + } +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("setup-symlink"))) { +#ifdef XP_UNIX + NS_tchar path[MAXPATHLEN]; + if (!NS_tvsnprintf(path, sizeof(path) / sizeof(path[0]), NS_T("%s/%s"), + NS_T("/tmp"), argv[2])) { + return 1; + } + if (mkdir(path, 0755)) { + return 1; + } + if (!NS_tvsnprintf(path, sizeof(path) / sizeof(path[0]), NS_T("%s/%s/%s"), + NS_T("/tmp"), argv[2], argv[3])) { + return 1; + } + if (mkdir(path, 0755)) { + return 1; + } + if (!NS_tvsnprintf(path, sizeof(path) / sizeof(path[0]), + NS_T("%s/%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3], + argv[4])) { + return 1; + } + FILE* file = NS_tfopen(path, NS_T("w")); + if (file) { + fputs(NS_T("test"), file); + fclose(file); + } + if (symlink(path, argv[5]) != 0) { + return 1; + } + if (!NS_tvsnprintf(path, sizeof(path) / sizeof(path[0]), NS_T("%s/%s"), + NS_T("/tmp"), argv[2])) { + return 1; + } + if (argc > 6 && !NS_tstrcmp(argv[6], NS_T("change-perm"))) { + if (chmod(path, 0644)) { + return 1; + } + } + return 0; +#else + // Not implemented on non-Unix platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("remove-symlink"))) { +#ifdef XP_UNIX + // The following can be called at the start of a test in case these symlinks + // need to be removed if they already exist and at the end of a test to + // remove the symlinks created by the test so ignore file doesn't exist + // errors. + NS_tchar path[MAXPATHLEN]; + if (!NS_tvsnprintf(path, sizeof(path) / sizeof(path[0]), NS_T("%s/%s"), + NS_T("/tmp"), argv[2])) { + return 1; + } + if (chmod(path, 0755) && errno != ENOENT) { + return 1; + } + if (!NS_tvsnprintf(path, sizeof(path) / sizeof(path[0]), + NS_T("%s/%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3], + argv[4])) { + return 1; + } + if (unlink(path) && errno != ENOENT) { + return 1; + } + if (!NS_tvsnprintf(path, sizeof(path) / sizeof(path[0]), NS_T("%s/%s/%s"), + NS_T("/tmp"), argv[2], argv[3])) { + return 1; + } + if (rmdir(path) && errno != ENOENT) { + return 1; + } + if (!NS_tvsnprintf(path, sizeof(path) / sizeof(path[0]), NS_T("%s/%s"), + NS_T("/tmp"), argv[2])) { + return 1; + } + if (rmdir(path) && errno != ENOENT) { + return 1; + } + return 0; +#else + // Not implemented on non-Unix platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("check-symlink"))) { +#ifdef XP_UNIX + struct stat ss; + if (lstat(argv[2], &ss)) { + return 1; + } + return S_ISLNK(ss.st_mode) ? 0 : 1; +#else + // Not implemented on non-Unix platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("check-umask"))) { +#ifdef XP_UNIX + // Discover the current value of the umask. There is no way to read the + // umask without changing it. The system call is specified as unable to + // fail. + uint32_t umask = ::umask(0777); + ::umask(umask); + + NS_tchar logFilePath[MAXPATHLEN]; + if (!NS_tvsnprintf(logFilePath, + sizeof(logFilePath) / sizeof(logFilePath[0]), NS_T("%s"), + argv[2])) { + return 1; + } + + FILE* logFP = NS_tfopen(logFilePath, NS_T("wb")); + if (!logFP) { + return 1; + } + fprintf(logFP, "check-umask\numask-%d\n", umask); + + fclose(logFP); + logFP = nullptr; + + return 0; +#else + // Not implemented on non-Unix platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("wait-for-service-stop"))) { +#if defined(XP_WIN) && defined(MOZ_MAINTENANCE_SERVICE) + const int maxWaitSeconds = NS_ttoi(argv[3]); + LPCWSTR serviceName = argv[2]; + DWORD serviceState = WaitForServiceStop(serviceName, maxWaitSeconds); + if (SERVICE_STOPPED == serviceState) { + return 0; + } else { + return serviceState; + } +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("wait-for-application-exit"))) { +#ifdef XP_WIN + const int maxWaitSeconds = NS_ttoi(argv[3]); + LPCWSTR application = argv[2]; + DWORD ret = WaitForProcessExit(application, maxWaitSeconds); + if (ERROR_SUCCESS == ret) { + return 0; + } else if (WAIT_TIMEOUT == ret) { + return 1; + } else { + return 2; + } +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("launch-service"))) { +#if defined(XP_WIN) && defined(MOZ_MAINTENANCE_SERVICE) + DWORD ret = + LaunchServiceSoftwareUpdateCommand(argc - 2, (LPCWSTR*)argv + 2); + if (ret != ERROR_SUCCESS) { + // 192 is used to avoid reusing a possible return value from the call to + // WaitForServiceStop + return 0x000000C0; + } + // Wait a maximum of 120 seconds. + DWORD lastState = WaitForServiceStop(SVC_NAME, 120); + if (SERVICE_STOPPED == lastState) { + return 0; + } + return lastState; +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (!NS_tstrcmp(argv[1], NS_T("create-update-dir"))) { +#ifdef XP_WIN + mozilla::UniquePtr<wchar_t[]> updateDir; + HRESULT result = GetCommonUpdateDirectory(argv[2], updateDir); + return SUCCEEDED(result) ? 0 : 1; +#else + // Not implemented on non-Windows platforms + return 1; +#endif + } + + if (NS_tchdir(argv[1]) != 0) { + return 1; + } + + // File in use test helper section + if (!NS_tstrcmp(argv[4], NS_T("-s"))) { + // Note: glibc's getcwd() allocates the buffer dynamically using malloc(3) + // if buf (the 1st param) is NULL so free cwd when it is no longer needed. + NS_tchar* cwd = NS_tgetcwd(nullptr, 0); + NS_tchar inFilePath[MAXPATHLEN]; + if (!NS_tvsnprintf(inFilePath, sizeof(inFilePath) / sizeof(inFilePath[0]), + NS_T("%s/%s"), cwd, argv[2])) { + return 1; + } + NS_tchar outFilePath[MAXPATHLEN]; + if (!NS_tvsnprintf(outFilePath, + sizeof(outFilePath) / sizeof(outFilePath[0]), + NS_T("%s/%s"), cwd, argv[3])) { + return 1; + } + free(cwd); + + int seconds = NS_ttoi(argv[5]); +#ifdef XP_WIN + HANDLE hFile = INVALID_HANDLE_VALUE; + if (argc == 7) { + hFile = CreateFileW(argv[6], DELETE | GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, 0, nullptr); + if (hFile == INVALID_HANDLE_VALUE) { + WriteMsg(outFilePath, "error_locking"); + return 1; + } + } + + WriteMsg(outFilePath, "sleeping"); + int i = 0; + while (!CheckMsg(inFilePath, "finish\n") && i++ <= seconds) { + Sleep(1000); + } + + if (argc == 7) { + CloseHandle(hFile); + } +#else + WriteMsg(outFilePath, "sleeping"); + int i = 0; + while (!CheckMsg(inFilePath, "finish\n") && i++ <= seconds) { + sleep(1); + } +#endif + WriteMsg(outFilePath, "finished"); + return 0; + } + + { + // Command line argument test helper section + NS_tchar logFilePath[MAXPATHLEN]; + if (!NS_tvsnprintf(logFilePath, + sizeof(logFilePath) / sizeof(logFilePath[0]), NS_T("%s"), + argv[2])) { + return 1; + } + + FILE* logFP = NS_tfopen(logFilePath, NS_T("wb")); + if (!logFP) { + return 1; + } + for (int i = 1; i < argc; ++i) { + fprintf(logFP, LOG_S "\n", argv[i]); + } + + fclose(logFP); + logFP = nullptr; + } + + return 0; +} diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings.cpp b/toolkit/mozapps/update/tests/TestAUSReadStrings.cpp new file mode 100644 index 0000000000..d4c090772b --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSReadStrings.cpp @@ -0,0 +1,210 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/** + * This binary tests the updater's ReadStrings ini parser and should run in a + * directory with a Unicode character to test bug 473417. + */ +#ifdef XP_WIN +# include <windows.h> +# define NS_main wmain +# define PATH_SEPARATOR_CHAR L'\\' +// On Windows, argv[0] can also have forward slashes instead +# define ALT_PATH_SEPARATOR_CHAR L'/' +#else +# define NS_main main +# define PATH_SEPARATOR_CHAR '/' +#endif + +#include <stdio.h> +#include <stdarg.h> +#include <string.h> + +#include "updater/progressui.h" +#include "common/readstrings.h" +#include "common/updatererrors.h" +#include "common/updatedefines.h" +#include "mozilla/ArrayUtils.h" + +#ifndef MAXPATHLEN +# ifdef PATH_MAX +# define MAXPATHLEN PATH_MAX +# elif defined(MAX_PATH) +# define MAXPATHLEN MAX_PATH +# elif defined(_MAX_PATH) +# define MAXPATHLEN _MAX_PATH +# elif defined(CCHMAXPATH) +# define MAXPATHLEN CCHMAXPATH +# else +# define MAXPATHLEN 1024 +# endif +#endif + +#define TEST_NAME "Updater ReadStrings" + +using namespace mozilla; + +static int gFailCount = 0; + +/** + * Prints the given failure message and arguments using printf, prepending + * "TEST-UNEXPECTED-FAIL " for the benefit of the test harness and + * appending "\n" to eliminate having to type it at each call site. + */ +void fail(const char* msg, ...) { + va_list ap; + + printf("TEST-UNEXPECTED-FAIL | "); + + va_start(ap, msg); + vprintf(msg, ap); + va_end(ap); + + putchar('\n'); + ++gFailCount; +} + +int NS_main(int argc, NS_tchar** argv) { + printf("Running TestAUSReadStrings tests\n"); + + int rv = 0; + int retval; + NS_tchar inifile[MAXPATHLEN]; + StringTable testStrings; + + NS_tchar* slash = NS_tstrrchr(argv[0], PATH_SEPARATOR_CHAR); +#ifdef ALT_PATH_SEPARATOR_CHAR + NS_tchar* altslash = NS_tstrrchr(argv[0], ALT_PATH_SEPARATOR_CHAR); + slash = (slash > altslash) ? slash : altslash; +#endif // ALT_PATH_SEPARATOR_CHAR + + if (!slash) { + fail("%s | unable to find platform specific path separator (check 1)", + TEST_NAME); + return 20; + } + + *(++slash) = '\0'; + // Test success when the ini file exists with both Title and Info in the + // Strings section and the values for Title and Info. + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings1.ini"), + argv[0]); + retval = ReadStrings(inifile, &testStrings); + if (retval == OK) { + if (strcmp(testStrings.title.get(), + "Title Test - \xD0\x98\xD1\x81\xD0\xBF\xD1\x8B" + "\xD1\x82\xD0\xB0\xD0\xBD\xD0\xB8\xD0\xB5 " + "\xCE\x94\xCE\xBF\xCE\xBA\xCE\xB9\xCE\xBC\xCE\xAE " + "\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88 " + "\xE6\xB8\xAC\xE8\xA9\xA6 " + "\xE6\xB5\x8B\xE8\xAF\x95") != 0) { + rv = 21; + fail("%s | Title ini value incorrect (check 3)", TEST_NAME); + } + + if (strcmp(testStrings.info.get(), + "Info Test - \xD0\x98\xD1\x81\xD0\xBF\xD1\x8B" + "\xD1\x82\xD0\xB0\xD0\xBD\xD0\xB8\xD0\xB5 " + "\xCE\x94\xCE\xBF\xCE\xBA\xCE\xB9\xCE\xBC\xCE\xAE " + "\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88 " + "\xE6\xB8\xAC\xE8\xA9\xA6 " + "\xE6\xB5\x8B\xE8\xAF\x95\xE2\x80\xA6") != 0) { + rv = 22; + fail("%s | Info ini value incorrect (check 4)", TEST_NAME); + } + } else { + fail("%s | ReadStrings returned %i (check 2)", TEST_NAME, retval); + rv = 23; + } + + // Test failure when the ini file exists without Title and with Info in the + // Strings section. + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings2.ini"), + argv[0]); + retval = ReadStrings(inifile, &testStrings); + if (retval != PARSE_ERROR) { + rv = 24; + fail("%s | ReadStrings returned %i (check 5)", TEST_NAME, retval); + } + + // Test failure when the ini file exists with Title and without Info in the + // Strings section. + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings3.ini"), + argv[0]); + retval = ReadStrings(inifile, &testStrings); + if (retval != PARSE_ERROR) { + rv = 25; + fail("%s | ReadStrings returned %i (check 6)", TEST_NAME, retval); + } + + // Test failure when the ini file doesn't exist + NS_tsnprintf(inifile, ArrayLength(inifile), + NS_T("%sTestAUSReadStringsBogus.ini"), argv[0]); + retval = ReadStrings(inifile, &testStrings); + if (retval != READ_ERROR) { + rv = 26; + fail("%s | ini file doesn't exist (check 7)", TEST_NAME); + } + + // Test reading a non-default section name + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings3.ini"), + argv[0]); + retval = + ReadStrings(inifile, "Title\0", 1, &testStrings.title, "BogusSection2"); + if (retval == OK) { + if (strcmp(testStrings.title.get(), "Bogus Title") != 0) { + rv = 27; + fail("%s | Title ini value incorrect (check 9)", TEST_NAME); + } + } else { + fail("%s | ReadStrings returned %i (check 8)", TEST_NAME, retval); + rv = 28; + } + + // Test reading an exceedingly long string + NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings4.ini"), + argv[0]); + retval = ReadStrings(inifile, "LongValue\0", 1, &testStrings.title); + const char* expectedValue = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam id " + "ipsum condimentum, faucibus ante porta, vehicula metus. Nunc nec luctus " + "lorem. Nunc mattis viverra nisl, eu ornare dui feugiat id. Aenean " + "commodo ligula porttitor elit aliquam, ut luctus nunc aliquam. In eu " + "eros at nunc pulvinar porta. Praesent porta felis vitae massa " + "sollicitudin, a vestibulum dolor rutrum. Aenean finibus, felis ac " + "dictum hendrerit, ligula arcu semper enim, rhoncus consequat arcu orci " + "nec est. Sed auctor hendrerit rhoncus. Maecenas dignissim lorem et " + "tellus maximus, sit amet pretium urna imperdiet. Duis ut libero " + "volutpat, rhoncus mi non, placerat lacus. Nunc id tortor in quam " + "lacinia luctus. Nam eu maximus ipsum, eu bibendum enim. Ut iaculis " + "maximus ipsum in condimentum. Aliquam tellus nulla, congue quis pretium " + "a, posuere quis ligula. Donec vel quam ipsum. Pellentesque congue urna " + "eget porttitor pulvinar. Proin non risus lacus. Vestibulum molestie et " + "ligula sit amet pellentesque. Phasellus luctus auctor lorem, vel " + "dapibus ante iaculis sed. Cras ligula ex, vehicula a dui vel, posuere " + "fermentum elit. Vestibulum et nisi at libero maximus interdum a non ex. " + "Ut ut leo in metus convallis porta a et libero. Pellentesque fringilla " + "dolor sit amet eleifend fermentum. Quisque blandit dolor facilisis " + "purus vulputate sodales eget ac arcu. Nulla pulvinar feugiat accumsan. " + "Phasellus auctor nisl eget diam auctor, sit amet imperdiet mauris " + "condimentum. In a risus ut felis lobortis facilisis."; + if (retval == OK) { + if (strcmp(testStrings.title.get(), expectedValue) != 0) { + rv = 29; + fail("%s | LongValue ini value incorrect (check 10)", TEST_NAME); + } + } else { + fail("%s | ReadStrings returned %i (check 11)", TEST_NAME, retval); + rv = 30; + } + + if (rv == 0) { + printf("TEST-PASS | %s | all checks passed\n", TEST_NAME); + } else { + fail("%s | %i out of 9 checks failed", TEST_NAME, gFailCount); + } + + return rv; +} diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings1.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings1.ini new file mode 100644 index 0000000000..5ab13c185d --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSReadStrings1.ini @@ -0,0 +1,47 @@ +; This file is in the UTF-8 encoding + +[BogusSection1] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment + +[Strings] + +Bogus1=Bogus1 + +; Comment + +Title=Title Test - Испытание Δοκιμή テスト 測試 测试 + +; Comment + +Bogus2=Bogus2 + +; Comment + +Info=Info Test - Испытание Δοκιμή テスト 測試 测试… + +; Comment + +Bogus3=Bogus3 + +; Comment + +[BogusSection2] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings2.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings2.ini new file mode 100644 index 0000000000..8291a7c94c --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSReadStrings2.ini @@ -0,0 +1,39 @@ +; This file is in the UTF-8 encoding + +[BogusSection1] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment + +[Strings] + +Bogus1=Bogus1 + +; Comment + +Info=Info + +; Comment + +Bogus2=Bogus2 + +; Comment + +[BogusSection2] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings3.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings3.ini new file mode 100644 index 0000000000..a64d1232e2 --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSReadStrings3.ini @@ -0,0 +1,39 @@ +; This file is in the UTF-8 encoding + +[BogusSection1] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment + +[Strings] + +Bogus1=Bogus1 + +; Comment + +Title=Title + +; Comment + +Bogus2=Bogus2 + +; Comment + +[BogusSection2] + +; Comment + +Title=Bogus Title + +; Comment + +Info=Bogus Info + +; Comment diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings4.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings4.ini new file mode 100644 index 0000000000..b930455e8b --- /dev/null +++ b/toolkit/mozapps/update/tests/TestAUSReadStrings4.ini @@ -0,0 +1,5 @@ +; This file is in the UTF-8 encoding + +[Strings] + +LongValue=Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam id ipsum condimentum, faucibus ante porta, vehicula metus. Nunc nec luctus lorem. Nunc mattis viverra nisl, eu ornare dui feugiat id. Aenean commodo ligula porttitor elit aliquam, ut luctus nunc aliquam. In eu eros at nunc pulvinar porta. Praesent porta felis vitae massa sollicitudin, a vestibulum dolor rutrum. Aenean finibus, felis ac dictum hendrerit, ligula arcu semper enim, rhoncus consequat arcu orci nec est. Sed auctor hendrerit rhoncus. Maecenas dignissim lorem et tellus maximus, sit amet pretium urna imperdiet. Duis ut libero volutpat, rhoncus mi non, placerat lacus. Nunc id tortor in quam lacinia luctus. Nam eu maximus ipsum, eu bibendum enim. Ut iaculis maximus ipsum in condimentum. Aliquam tellus nulla, congue quis pretium a, posuere quis ligula. Donec vel quam ipsum. Pellentesque congue urna eget porttitor pulvinar. Proin non risus lacus. Vestibulum molestie et ligula sit amet pellentesque. Phasellus luctus auctor lorem, vel dapibus ante iaculis sed. Cras ligula ex, vehicula a dui vel, posuere fermentum elit. Vestibulum et nisi at libero maximus interdum a non ex. Ut ut leo in metus convallis porta a et libero. Pellentesque fringilla dolor sit amet eleifend fermentum. Quisque blandit dolor facilisis purus vulputate sodales eget ac arcu. Nulla pulvinar feugiat accumsan. Phasellus auctor nisl eget diam auctor, sit amet imperdiet mauris condimentum. In a risus ut felis lobortis facilisis. diff --git a/toolkit/mozapps/update/tests/browser/browser.bits.ini b/toolkit/mozapps/update/tests/browser/browser.bits.ini new file mode 100644 index 0000000000..a9b812680c --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser.bits.ini @@ -0,0 +1,81 @@ +[DEFAULT] +skip-if = + os != 'win' + msix # Updater is disabled in MSIX builds +reason = BITS is only available on Windows. +dupe-manifest = +tags = appupdate bits +head = head.js +support-files = + ../data/shared.js + ../data/sharedUpdateXML.js + ../data/app_update.sjs + downloadPage.html + testConstants.js + +prefs = + app.update.BITS.enabled=true + app.update.langpack.enabled=true + +# BITS Download Tests +##################### + +# About Dialog Application Update Tests +[browser_aboutDialog_bc_downloading.js] +[browser_aboutDialog_bc_downloading_staging.js] +[browser_aboutDialog_bc_downloading_notify.js] +[browser_aboutDialog_bc_downloaded.js] +[browser_aboutDialog_bc_downloaded_staging.js] +[browser_aboutDialog_bc_downloaded_staged.js] +[browser_aboutDialog_bc_downloaded_stagingFailure.js] +[browser_aboutDialog_fc_downloadAuto.js] +[browser_aboutDialog_fc_downloadAuto_staging.js] +[browser_aboutDialog_fc_downloadOptIn.js] +[browser_aboutDialog_fc_downloadOptIn_staging.js] +[browser_aboutDialog_fc_patch_completeBadSize.js] +[browser_aboutDialog_fc_patch_partialBadSize.js] +[browser_aboutDialog_fc_patch_partialBadSize_complete.js] +[browser_aboutDialog_fc_patch_partialBadSize_completeBadSize.js] +[browser_aboutDialog_bc_multiUpdate.js] + +# about:preferences Application Update Tests +[browser_aboutPrefs_bc_downloading.js] +[browser_aboutPrefs_bc_downloading_staging.js] +[browser_aboutPrefs_bc_downloaded.js] +[browser_aboutPrefs_bc_downloaded_staging.js] +[browser_aboutPrefs_bc_downloaded_stagingFailure.js] +[browser_aboutPrefs_bc_downloaded_staged.js] +[browser_aboutPrefs_fc_downloadAuto.js] +[browser_aboutPrefs_fc_downloadAuto_staging.js] +[browser_aboutPrefs_fc_downloadOptIn.js] +[browser_aboutPrefs_fc_downloadOptIn_staging.js] +[browser_aboutPrefs_fc_patch_completeBadSize.js] +[browser_aboutPrefs_fc_patch_partialBadSize.js] +[browser_aboutPrefs_fc_patch_partialBadSize_complete.js] +[browser_aboutPrefs_fc_patch_partialBadSize_completeBadSize.js] +[browser_aboutPrefs_bc_multiUpdate.js] + +# Doorhanger Application Update Tests +[browser_doorhanger_bc_downloaded.js] +[browser_doorhanger_bc_downloaded_staged.js] +[browser_doorhanger_bc_downloadAutoFailures.js] +[browser_doorhanger_bc_downloadAutoFailures_bgWin.js] +[browser_doorhanger_bc_downloadOptIn.js] +[browser_doorhanger_bc_downloadOptIn_bgWin.js] +[browser_doorhanger_bc_downloadOptIn_staging.js] +[browser_doorhanger_bc_patch_completeBadSize.js] +[browser_doorhanger_bc_patch_partialBadSize.js] +[browser_doorhanger_bc_patch_partialBadSize_complete.js] +[browser_doorhanger_bc_patch_partialBadSize_completeBadSize.js] +[browser_doorhanger_sp_patch_completeApplyFailure.js] +[browser_doorhanger_sp_patch_partialApplyFailure.js] +[browser_doorhanger_sp_patch_partialApplyFailure_complete.js] +[browser_doorhanger_sp_patch_partialApplyFailure_complete_staging.js] +[browser_doorhanger_sp_patch_partialApplyFailure_completeBadSize.js] +[browser_doorhanger_bc_downloaded_disableBITS.js] +[browser_doorhanger_bc_multiUpdate.js] +[browser_doorhanger_bc_multiUpdate_promptWaitTime.js] + +# Telemetry Update Ping Tests +[browser_telemetry_updatePing_downloaded_ready.js] +[browser_telemetry_updatePing_staged_ready.js] diff --git a/toolkit/mozapps/update/tests/browser/browser.ini b/toolkit/mozapps/update/tests/browser/browser.ini new file mode 100644 index 0000000000..07b297471a --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser.ini @@ -0,0 +1,119 @@ +[DEFAULT] +dupe-manifest = +tags = appupdate internal +head = head.js +support-files = + ../data/shared.js + ../data/sharedUpdateXML.js + ../data/app_update.sjs + downloadPage.html + testConstants.js +prefs = + app.update.BITS.enabled=false + app.update.langpack.enabled=true +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds + +# About Dialog Application Update Tests +[browser_aboutDialog_bc_downloading.js] +[browser_aboutDialog_bc_downloading_staging.js] +[browser_aboutDialog_bc_downloading_notify.js] +[browser_aboutDialog_bc_downloaded.js] +[browser_aboutDialog_bc_downloaded_staging.js] +[browser_aboutDialog_bc_downloaded_stagingFailure.js] +[browser_aboutDialog_bc_downloaded_staged.js] +[browser_aboutDialog_fc_apply_blocked.js] +[browser_aboutDialog_fc_downloadAuto.js] +skip-if = tsan # Bug 1683730 +[browser_aboutDialog_fc_downloadAuto_staging.js] +[browser_aboutDialog_fc_downloadOptIn.js] +[browser_aboutDialog_fc_downloadOptIn_staging.js] +[browser_aboutDialog_fc_patch_completeBadSize.js] +[browser_aboutDialog_fc_patch_partialBadSize.js] +[browser_aboutDialog_fc_patch_partialBadSize_complete.js] +[browser_aboutDialog_fc_patch_partialBadSize_completeBadSize.js] +[browser_aboutDialog_fc_check_cantApply.js] +skip-if = os != 'win' +reason = test must be able to prevent file deletion. +[browser_aboutDialog_fc_network_failure.js] +[browser_aboutDialog_fc_network_offline.js] +[browser_aboutDialog_fc_check_malformedXML.js] +[browser_aboutDialog_fc_check_noUpdate.js] +[browser_aboutDialog_fc_check_otherInstance.js] +skip-if = os != 'win' +reason = Windows only feature. +[browser_aboutDialog_fc_check_unsupported.js] +[browser_aboutDialog_bc_multiUpdate.js] +[browser_aboutDialog_internalError.js] +[browser_aboutDialog_AppUpdater_stop_checking.js] +[browser_aboutDialog_AppUpdater_stop_no_update.js] +[browser_aboutDialog_AppUpdater_stop_download_and_install.js] +[browser_aboutDialog_AppUpdater_stop_downloading.js] +[browser_aboutDialog_AppUpdater_stop_download_failed.js] +[browser_aboutDialog_AppUpdater_stop_staging.js] +[browser_aboutDialog_AppUpdater_stop_ready_for_restart.js] +[browser_aboutDialog_AppUpdater_stop_internal_error.js] +[browser_aboutDialog_AppUpdater_stop_swap.js] + +# about:preferences Application Update Tests +[browser_aboutPrefs_fc_apply_blocked.js] +[browser_aboutPrefs_bc_downloading.js] +[browser_aboutPrefs_bc_downloading_staging.js] +[browser_aboutPrefs_bc_downloaded.js] +[browser_aboutPrefs_bc_downloaded_staging.js] +[browser_aboutPrefs_bc_downloaded_stagingFailure.js] +[browser_aboutPrefs_bc_downloaded_staged.js] +[browser_aboutPrefs_fc_downloadAuto.js] +[browser_aboutPrefs_fc_downloadAuto_staging.js] +[browser_aboutPrefs_fc_downloadOptIn.js] +[browser_aboutPrefs_fc_downloadOptIn_staging.js] +[browser_aboutPrefs_fc_patch_completeBadSize.js] +[browser_aboutPrefs_fc_patch_partialBadSize.js] +[browser_aboutPrefs_fc_patch_partialBadSize_complete.js] +[browser_aboutPrefs_fc_patch_partialBadSize_completeBadSize.js] +[browser_aboutPrefs_fc_check_cantApply.js] +skip-if = os != 'win' +reason = test must be able to prevent file deletion. +[browser_aboutPrefs_fc_network_failure.js] +[browser_aboutPrefs_fc_network_offline.js] +[browser_aboutPrefs_fc_check_malformedXML.js] +[browser_aboutPrefs_fc_check_noUpdate.js] +[browser_aboutPrefs_fc_check_otherInstance.js] +skip-if = os != 'win' +reason = Windows only feature. +[browser_aboutPrefs_fc_check_unsupported.js] +[browser_aboutPrefs_settings.js] +[browser_aboutPrefs_bc_multiUpdate.js] +[browser_aboutPrefs_backgroundUpdateSetting.js] +[browser_aboutPrefs_internalError.js] + +# Doorhanger Application Update Tests +[browser_doorhanger_bc_check_cantApply.js] +skip-if = os != 'win' +reason = test must be able to prevent file deletion. +[browser_doorhanger_bc_check_malformedXML.js] +[browser_doorhanger_bc_check_unsupported.js] +[browser_doorhanger_bc_downloaded.js] +[browser_doorhanger_bc_downloaded_staged.js] +[browser_doorhanger_bc_downloadAutoFailures.js] +[browser_doorhanger_bc_downloadAutoFailures_bgWin.js] +[browser_doorhanger_bc_downloadOptIn.js] +[browser_doorhanger_bc_downloadOptIn_bgWin.js] +[browser_doorhanger_bc_downloadOptIn_staging.js] +[browser_doorhanger_bc_patch_completeBadSize.js] +[browser_doorhanger_bc_patch_partialBadSize.js] +[browser_doorhanger_bc_patch_partialBadSize_complete.js] +[browser_doorhanger_bc_patch_partialBadSize_completeBadSize.js] +[browser_doorhanger_sp_patch_completeApplyFailure.js] +[browser_doorhanger_sp_patch_partialApplyFailure.js] +[browser_doorhanger_sp_patch_partialApplyFailure_complete.js] +[browser_doorhanger_sp_patch_partialApplyFailure_complete_staging.js] +[browser_doorhanger_sp_patch_partialApplyFailure_completeBadSize.js] +[browser_doorhanger_bc_multiUpdate.js] +[browser_doorhanger_bc_multiUpdate_promptWaitTime.js] + +# Elevation Dialog Tests +[browser_elevationDialog.js] + +# Telemetry Update Ping Tests +[browser_telemetry_updatePing_downloaded_ready.js] +[browser_telemetry_updatePing_staged_ready.js] diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_checking.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_checking.js new file mode 100644 index 0000000000..370f658656 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_checking.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that while a download is in-progress, calling `AppUpdater.stop()` while +// in the checking state causes the interface to return to the `NEVER_CHECKED` +// state. +// This is less a test of the About dialog than of AppUpdater, but it's easier +// to test it via the About dialog just because there is already a testing +// framework for the About dialog. +add_task(async function aboutDialog_AppUpdater_stop_checking() { + let params = { queryString: "&noUpdates=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + // Omit the continue file to keep us in the checking state. + }, + aboutDialog => { + aboutDialog.gAppUpdater._appUpdater.stop(); + }, + { + panelId: "checkForUpdates", + }, + ]); + + // Ideally this would go in a cleanup function. But this needs to happen + // before any other cleanup functions and for some reason cleanup functions + // do not always seem to execute in reverse registration order. + dump("Cleanup: Waiting for checking to finish.\n"); + await continueFileHandler(CONTINUE_CHECK); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_download_and_install.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_download_and_install.js new file mode 100644 index 0000000000..74e179043d --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_download_and_install.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that while a download is in-progress, calling `AppUpdater.stop()` while +// in the "download and install" state causes the interface to return to the +// `NEVER_CHECKED` state. +// This is less a test of the About dialog than of AppUpdater, but it's easier +// to test it via the About dialog just because there is already a testing +// framework for the About dialog. +add_task(async function aboutDialog_AppUpdater_stop_download_and_install() { + await UpdateUtils.setAppUpdateAutoEnabled(false); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + noContinue: true, + }, + aboutDialog => { + aboutDialog.gAppUpdater._appUpdater.stop(); + }, + { + panelId: "checkForUpdates", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_download_failed.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_download_failed.js new file mode 100644 index 0000000000..8900765f81 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_download_failed.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that while a download is in-progress, calling `AppUpdater.stop()` while +// in the "downloadFailed" state doesn't cause a shift to any other state, such +// as internal error. +// This is less a test of the About dialog than of AppUpdater, but it's easier +// to test it via the About dialog just because there is already a testing +// framework for the About dialog. +add_task(async function aboutDialog_AppUpdater_stop_download_failed() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "complete", bitsResult: gBadSizeResult }; + downloadInfo[1] = { patchType: "complete", internalResult: gBadSizeResult }; + } else { + downloadInfo[0] = { patchType: "complete", internalResult: gBadSizeResult }; + } + + let params = { queryString: "&completePatchOnly=1&invalidCompleteSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "downloadFailed", + }, + aboutDialog => { + aboutDialog.gAppUpdater._appUpdater.stop(); + }, + { + panelId: "downloadFailed", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_downloading.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_downloading.js new file mode 100644 index 0000000000..d0de7e03b9 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_downloading.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that while a download is in-progress, calling `AppUpdater.stop()` during +// the downloading state causes the interface to return to the `NEVER_CHECKED` +// state. +// This is less a test of the About dialog than of AppUpdater, but it's easier +// to test it via the About dialog just because there is already a testing +// framework for the About dialog. +add_task(async function aboutDialog_AppUpdater_stop_downloading() { + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&useSlowDownloadMar=1&invalidCompleteSize=1", + backgroundUpdate: true, + waitForUpdateState: STATE_DOWNLOADING, + }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + // Omit continue file to keep the UI in the downloading state. + }, + aboutDialog => { + aboutDialog.gAppUpdater._appUpdater.stop(); + }, + { + panelId: "checkForUpdates", + // The update will still be in the downloading state even though + // AppUpdater has stopped because stopping AppUpdater doesn't stop the + // Application Update Service from continuing with the update. + checkActiveUpdate: { state: STATE_DOWNLOADING }, + expectedStateOverride: Ci.nsIApplicationUpdateService.STATE_DOWNLOADING, + }, + ]); + + // Ideally this would go in a cleanup function. But this needs to happen + // before any other cleanup functions and for some reason cleanup functions + // do not always seem to execute in reverse registration order. + dump("Cleanup: Waiting for downloading to finish.\n"); + await continueFileHandler(CONTINUE_DOWNLOAD); + if (gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_DOWNLOADING) { + await gAUS.stateTransition; + } +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_internal_error.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_internal_error.js new file mode 100644 index 0000000000..70fbce97b2 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_internal_error.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + AppUpdater: "resource://gre/modules/AppUpdater.jsm", + sinon: "resource://testing-common/Sinon.jsm", +}); + +add_setup(function setup_internalErrorTest() { + const sandbox = sinon.createSandbox(); + sandbox.stub(AppUpdater.prototype, "aus").get(() => { + throw new Error("intentional test error"); + }); + sandbox.stub(AppUpdater.prototype, "checker").get(() => { + throw new Error("intentional test error"); + }); + sandbox.stub(AppUpdater.prototype, "um").get(() => { + throw new Error("intentional test error"); + }); + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +// Test that while a download is in-progress, calling `AppUpdater.stop()` while +// in the "internal error" state doesn't cause a shift to any other state. +// This is less a test of the About dialog than of AppUpdater, but it's easier +// to test it via the About dialog just because there is already a testing +// framework for the About dialog. +add_task(async function aboutDialog_AppUpdater_stop_internal_error() { + let params = {}; + await runAboutDialogUpdateTest(params, [ + { + panelId: "internalError", + }, + aboutDialog => { + aboutDialog.gAppUpdater._appUpdater.stop(); + }, + { + panelId: "internalError", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_no_update.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_no_update.js new file mode 100644 index 0000000000..65a52ccc87 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_no_update.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that while a download is in-progress, calling `AppUpdater.stop()` while +// in the "noUpdatesFound" state doesn't cause a shift to any other state, such +// as internal error. +// This is less a test of the About dialog than of AppUpdater, but it's easier +// to test it via the About dialog just because there is already a testing +// framework for the About dialog. +add_task(async function aboutDialog_AppUpdater_stop_no_update() { + let params = { queryString: "&noUpdates=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + continueFile: CONTINUE_CHECK, + }, + { + panelId: "noUpdatesFound", + }, + aboutDialog => { + aboutDialog.gAppUpdater._appUpdater.stop(); + }, + { + panelId: "noUpdatesFound", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_ready_for_restart.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_ready_for_restart.js new file mode 100644 index 0000000000..8c9d1f788f --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_ready_for_restart.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that while a download is in-progress, calling `AppUpdater.stop()` while +// in the "ready for restart" state doesn't cause a shift to any other state, +// such as internal error. +// This is less a test of the About dialog than of AppUpdater, but it's easier +// to test it via the About dialog just because there is already a testing +// framework for the About dialog. +add_task(async function aboutDialog_AppUpdater_stop_ready_for_restart() { + let params = { backgroundUpdate: true, waitForUpdateState: STATE_PENDING }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + }, + aboutDialog => { + aboutDialog.gAppUpdater._appUpdater.stop(); + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_staging.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_staging.js new file mode 100644 index 0000000000..dd822e6391 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_staging.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that while a download is in-progress, calling `AppUpdater.stop()` while +// in the staging state causes the interface to return to the `NEVER_CHECKED` +// state. +// This is less a test of the About dialog than of AppUpdater, but it's easier +// to test it via the About dialog just because there is already a testing +// framework for the About dialog. +add_task(async function aboutDialog_AppUpdater_stop_staging() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&useSlowDownloadMar=1&invalidCompleteSize=1", + backgroundUpdate: true, + waitForUpdateState: STATE_DOWNLOADING, + }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + // Don't pass a continue file in order to leave us in the staging state. + }, + aboutDialog => { + aboutDialog.gAppUpdater._appUpdater.stop(); + }, + { + panelId: "checkForUpdates", + // The update will still be in the staging state even though AppUpdater + // has stopped because stopping AppUpdater doesn't stop the Application + // Update Service from continuing with the update. + checkActiveUpdate: { state: STATE_PENDING }, + expectedStateOverride: Ci.nsIApplicationUpdateService.STATE_STAGING, + }, + ]); + + // Ideally this would go in a cleanup function. But this needs to happen + // before any other cleanup functions and for some reason cleanup functions + // do not always seem to execute in reverse registration order. + dump("Cleanup: Waiting for staging to finish.\n"); + await continueFileHandler(CONTINUE_STAGING); + if (gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING) { + await gAUS.stateTransition; + } +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_swap.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_swap.js new file mode 100644 index 0000000000..02441dea53 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_swap.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FIRST_UPDATE_VERSION = "999998.0"; +const SECOND_UPDATE_VERSION = "999999.0"; + +function prepareToDownloadVersion(version) { + setUpdateURL( + URL_HTTP_UPDATE_SJS + + `?detailsURL=${gDetailsURL}&promptWaitTime=0&appVersion=${version}` + ); +} + +add_task(async function aboutDialog_backgroundCheck_multiUpdate() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let params = { + version: FIRST_UPDATE_VERSION, + backgroundUpdate: true, + waitForUpdateState: STATE_PENDING, + }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + () => { + prepareToDownloadVersion(SECOND_UPDATE_VERSION); + gAUS.checkForBackgroundUpdates(); + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + // Don't pass a continue file in order to leave us in the staging state. + }, + aboutDialog => { + aboutDialog.gAppUpdater._appUpdater.stop(); + }, + { + panelId: "checkForUpdates", + checkActiveUpdate: { state: STATE_PENDING }, + expectedStateOverride: Ci.nsIApplicationUpdateService.STATE_STAGING, + }, + ]); + + // Ideally this would go in a cleanup function. But this needs to happen + // before any other cleanup functions and for some reason cleanup functions + // do not always seem to execute in reverse registration order. + dump("Cleanup: Waiting for staging to finish.\n"); + await continueFileHandler(CONTINUE_STAGING); + if (gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING) { + await gAUS.stateTransition; + } +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded.js new file mode 100644 index 0000000000..0955750c93 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog background check for updates +// with the update downloaded when the About Dialog is opened. +add_task(async function aboutDialog_backgroundCheck_downloaded() { + let params = { backgroundUpdate: true, waitForUpdateState: STATE_PENDING }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staged.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staged.js new file mode 100644 index 0000000000..2d7bd64d05 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staged.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog background check for updates +// with the update downloaded and staged when the About Dialog is opened. +add_task(async function aboutDialog_backgroundCheck_downloaded_staged() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&invalidCompleteSize=1", + backgroundUpdate: true, + continueFile: CONTINUE_STAGING, + waitForUpdateState: STATE_APPLIED, + }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staging.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staging.js new file mode 100644 index 0000000000..b60a8f128d --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staging.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog background check for updates +// with the update downloaded and the About Dialog opened during staging. +add_task(async function aboutDialog_backgroundCheck_downloaded_staging() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let lankpackCall = mockLangpackInstall(); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&invalidCompleteSize=1", + backgroundUpdate: true, + waitForUpdateState: STATE_PENDING, + }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + async aboutDialog => { + // Once the state is applied but langpacks aren't complete the about + // dialog should still be showing applying. + TestUtils.waitForCondition(() => { + return readStatusFile() == STATE_APPLIED; + }); + + is( + aboutDialog.gAppUpdater.selectedPanel.id, + "applying", + "UI should still show as applying." + ); + + let { appVersion, resolve } = await lankpackCall; + is( + appVersion, + Services.appinfo.version, + "Should see the right app version." + ); + resolve(); + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_stagingFailure.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_stagingFailure.js new file mode 100644 index 0000000000..c05b2daa74 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_stagingFailure.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog background check for updates +// with the update downloaded and staging has failed when the About Dialog is +// opened. +add_task( + async function aboutDialog_backgroundCheck_downloaded_stagingFailure() { + Services.env.set("MOZ_TEST_STAGING_ERROR", "1"); + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&completePatchOnly=1", + backgroundUpdate: true, + waitForUpdateState: STATE_PENDING, + }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); + } +); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading.js new file mode 100644 index 0000000000..6c2a7486a9 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog background check for updates +// with the About Dialog opened during downloading. +add_task(async function aboutDialog_backgroundCheck_downloading() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD, false]], + }); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + let lankpackCall = mockLangpackInstall(); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&useSlowDownloadMar=1&invalidCompleteSize=1", + backgroundUpdate: true, + waitForUpdateState: STATE_DOWNLOADING, + }; + await runAboutDialogUpdateTest(params, [ + async function aboutDialog_downloading() { + is( + PanelUI.notificationPanel.state, + "closed", + "The window's doorhanger is closed." + ); + ok( + !PanelUI.menuButton.hasAttribute("badge-status"), + "The window does not have a badge." + ); + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + async aboutDialog => { + // Once the state is pending but langpacks aren't complete the about + // dialog should still be showing downloading. + TestUtils.waitForCondition(() => { + return readStatusFile() == STATE_PENDING; + }); + + is( + aboutDialog.gAppUpdater.selectedPanel.id, + "downloading", + "UI should still show as downloading." + ); + + let { appVersion, resolve } = await lankpackCall; + is( + appVersion, + Services.appinfo.version, + "Should see the right app version." + ); + resolve(); + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading_notify.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading_notify.js new file mode 100644 index 0000000000..cf067efe7d --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading_notify.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog background check for updates with the +// "notify during download" feature turned on. +add_task(async function aboutDialog_backgroundCheck_downloading_notify() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD, true]], + }); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&useSlowDownloadMar=1&invalidCompleteSize=1", + backgroundUpdate: true, + waitForUpdateState: STATE_DOWNLOADING, + }; + await runAboutDialogUpdateTest(params, [ + async function aboutDialog_downloading_notification() { + await TestUtils.waitForCondition( + () => PanelUI.menuButton.hasAttribute("badge-status"), + "Waiting for update badge", + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test. + logTestInfo(e); + }); + is( + PanelUI.notificationPanel.state, + "closed", + "The window's doorhanger is closed." + ); + ok( + PanelUI.menuButton.hasAttribute("badge-status"), + "The window has a badge." + ); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-downloading", + "The downloading badge is showing for the background window" + ); + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading_staging.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading_staging.js new file mode 100644 index 0000000000..3f6b476e1b --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading_staging.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog background check for updates +// with the About Dialog opened during downloading and stages the update. +add_task(async function aboutDialog_backgroundCheck_downloading_staging() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + let lankpackCall = mockLangpackInstall(); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&useSlowDownloadMar=1&invalidCompleteSize=1", + backgroundUpdate: true, + waitForUpdateState: STATE_DOWNLOADING, + }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + async aboutDialog => { + // Once the state is applied but langpacks aren't complete the about + // dialog should still be showing applying. + TestUtils.waitForCondition(() => { + return readStatusFile() == STATE_APPLIED; + }); + + is( + aboutDialog.gAppUpdater.selectedPanel.id, + "applying", + "UI should still show as applying." + ); + + let { appVersion, resolve } = await lankpackCall; + is( + appVersion, + Services.appinfo.version, + "Should see the right app version." + ); + resolve(); + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_multiUpdate.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_multiUpdate.js new file mode 100644 index 0000000000..b6c893ea15 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_multiUpdate.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FIRST_UPDATE_VERSION = "999998.0"; +const SECOND_UPDATE_VERSION = "999999.0"; + +function prepareToDownloadVersion(version) { + setUpdateURL( + URL_HTTP_UPDATE_SJS + + `?detailsURL=${gDetailsURL}&promptWaitTime=0&appVersion=${version}` + ); +} + +add_task(async function aboutDialog_backgroundCheck_multiUpdate() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let params = { + version: FIRST_UPDATE_VERSION, + backgroundUpdate: true, + waitForUpdateState: STATE_PENDING, + }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + () => { + prepareToDownloadVersion(SECOND_UPDATE_VERSION); + gAUS.checkForBackgroundUpdates(); + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_apply_blocked.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_apply_blocked.js new file mode 100644 index 0000000000..59171cf781 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_apply_blocked.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromptTestUtils } = ChromeUtils.import( + "resource://testing-common/PromptTestUtils.jsm" +); + +const BUILDER_URL = "https://example.com/document-builder.sjs?html="; +const PAGE_MARKUP = ` +<html> +<head> + <script> + window.onbeforeunload = function() { + return true; + }; + </script> +</head> +<body>TEST PAGE</body> +</html> +`; +const TEST_URL = BUILDER_URL + encodeURI(PAGE_MARKUP); + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); +}); + +// Test for About Dialog foreground check for updates +// and apply but restart is blocked by a page. +add_task(async function aboutDialog_foregroundCheck_apply_blocked() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + let aboutDialog; + let handlePromise = (async () => { + let dialog = await PromptTestUtils.waitForPrompt(window, { + modalType: Ci.nsIPrompt.MODAL_TYPE_CONTENT, + promptType: "confirmEx", + }); + Assert.equal( + aboutDialog.gAppUpdater.selectedPanel.id, + "restarting", + "The restarting panel should be displayed" + ); + + await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 }); + })(); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1&promptWaitTime=0" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + async function getAboutDialogHandle(dialog) { + aboutDialog = dialog; + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + forceApply: true, + }, + async function ensureDialogHasBeenCanceled() { + await handlePromise; + }, + // A final check to ensure that we are back in the apply state. + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); + + BrowserTestUtils.removeTab(tab, { skipPermitUnload: true }); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_cantApply.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_cantApply.js new file mode 100644 index 0000000000..cd659ef74b --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_cantApply.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// without the ability to apply updates. +add_task(async function aboutDialog_foregroundCheck_cantApply() { + lockWriteTestFile(); + + let params = {}; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "manualUpdate", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_malformedXML.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_malformedXML.js new file mode 100644 index 0000000000..529a4c7a63 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_malformedXML.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with a malformed update XML file. +add_task(async function aboutDialog_foregroundCheck_malformedXML() { + let params = { queryString: "&xmlMalformed=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "checkingFailed", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_noUpdate.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_noUpdate.js new file mode 100644 index 0000000000..2bd23cddf0 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_noUpdate.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with no update available. +add_task(async function aboutDialog_foregroundCheck_noUpdate() { + let params = { queryString: "&noUpdates=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "noUpdatesFound", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_otherInstance.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_otherInstance.js new file mode 100644 index 0000000000..fa1110effd --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_otherInstance.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with another application instance handling updates. +add_task(async function aboutDialog_foregroundCheck_otherInstance() { + setOtherInstanceHandlingUpdates(); + + let params = {}; + await runAboutDialogUpdateTest(params, [ + { + panelId: "otherInstanceHandlingUpdates", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_unsupported.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_unsupported.js new file mode 100644 index 0000000000..22e3425967 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_unsupported.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with an unsupported update. +add_task(async function aboutDialog_foregroundCheck_unsupported() { + let params = { queryString: "&unsupported=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "unsupportedSystem", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadAuto.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadAuto.js new file mode 100644 index 0000000000..a80c9deac8 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadAuto.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with an automatic download. +add_task(async function aboutDialog_foregroundCheck_downloadAuto() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1&promptWaitTime=0" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + async function aboutDialog_restart_notification() { + await TestUtils.waitForCondition( + () => PanelUI.menuButton.hasAttribute("badge-status"), + "Waiting for update badge", + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test. + logTestInfo(e); + }); + is( + PanelUI.notificationPanel.state, + "closed", + "The window's doorhanger is closed." + ); + ok( + PanelUI.menuButton.hasAttribute("badge-status"), + "The window has a badge." + ); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-restart", + "The restart badge is showing for the background window" + ); + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadAuto_staging.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadAuto_staging.js new file mode 100644 index 0000000000..e212b0c611 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadAuto_staging.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with an automatic download and update staging. +add_task(async function aboutDialog_foregroundCheck_downloadAuto_staging() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn.js new file mode 100644 index 0000000000..5a2ff513ad --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with a manual download. +add_task(async function aboutDialog_foregroundCheck_downloadOptIn() { + await UpdateUtils.setAppUpdateAutoEnabled(false); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + checkActiveUpdate: null, + continueFile: null, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn_staging.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn_staging.js new file mode 100644 index 0000000000..3ed331ec64 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn_staging.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with a manual download and update staging. +add_task(async function aboutDialog_foregroundCheck_downloadOptIn_staging() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + await UpdateUtils.setAppUpdateAutoEnabled(false); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + checkActiveUpdate: null, + continueFile: null, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_network_failure.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_network_failure.js new file mode 100644 index 0000000000..8e25b96d7f --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_network_failure.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates which fails, because it is +// impossible to connect to the update server +add_task(async function aboutDialog_foregroundCheck_network_failure() { + let params = { + baseURL: "https://localhost:7777", + }; + + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingFailed", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_network_offline.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_network_offline.js new file mode 100644 index 0000000000..9f81573e23 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_network_offline.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates which fails, because the +// browser is in offline mode and `localhost` cannot be resolved. +add_task(async function aboutDialog_foregroundCheck_network_offline() { + info("[OFFLINE] setting Services.io.offline (do not forget to reset it!)"); + // avoid that real network connectivity changes influence the test execution + Services.io.manageOfflineStatus = false; + Services.io.offline = true; + registerCleanupFunction(() => { + info("[ONLINE] Resetting Services.io.offline"); + Services.io.offline = false; + Services.io.manageOfflineStatus = true; + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.disable-localhost-when-offline", true], + ["network.dns.offline-localhost", false], + ], + }); + + await runAboutDialogUpdateTest({}, [ + { + panelId: "checkingFailed", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_completeBadSize.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_completeBadSize.js new file mode 100644 index 0000000000..fdc7d3407e --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_completeBadSize.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with a complete bad size patch. +add_task(async function aboutDialog_foregroundCheck_completeBadSize() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "complete", bitsResult: gBadSizeResult }; + downloadInfo[1] = { patchType: "complete", internalResult: gBadSizeResult }; + } else { + downloadInfo[0] = { patchType: "complete", internalResult: gBadSizeResult }; + } + + let params = { queryString: "&completePatchOnly=1&invalidCompleteSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "downloadFailed", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize.js new file mode 100644 index 0000000000..411fb25969 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with a partial bad size patch. +add_task(async function aboutDialog_foregroundCheck_partialBadSize() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: gBadSizeResult }; + downloadInfo[1] = { patchType: "partial", internalResult: gBadSizeResult }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: gBadSizeResult }; + } + + let params = { queryString: "&partialPatchOnly=1&invalidPartialSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "downloadFailed", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize_complete.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize_complete.js new file mode 100644 index 0000000000..396be1e930 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize_complete.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with a partial bad size patch and a complete patch. +add_task(async function aboutDialog_foregroundCheck_partialBadSize_complete() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: gBadSizeResult }; + downloadInfo[1] = { patchType: "partial", internalResult: gBadSizeResult }; + downloadInfo[2] = { patchType: "complete", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: gBadSizeResult }; + downloadInfo[1] = { patchType: "complete", internalResult: "0" }; + } + + let params = { queryString: "&invalidPartialSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize_completeBadSize.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize_completeBadSize.js new file mode 100644 index 0000000000..fb10250a49 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize_completeBadSize.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for About Dialog foreground check for updates +// with a partial bad size patch and a complete bad size patch. +add_task( + async function aboutDialog_foregroundCheck_partialBadSize_completeBadSize() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: gBadSizeResult }; + downloadInfo[1] = { + patchType: "partial", + internalResult: gBadSizeResult, + }; + downloadInfo[2] = { patchType: "complete", bitsResult: gBadSizeResult }; + downloadInfo[3] = { + patchType: "complete", + internalResult: gBadSizeResult, + }; + } else { + downloadInfo[0] = { + patchType: "partial", + internalResult: gBadSizeResult, + }; + downloadInfo[1] = { + patchType: "complete", + internalResult: gBadSizeResult, + }; + } + + let params = { queryString: "&invalidPartialSize=1&invalidCompleteSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "downloadFailed", + checkActiveUpdate: null, + continueFile: null, + }, + ]); + } +); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutDialog_internalError.js b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_internalError.js new file mode 100644 index 0000000000..fa59650afb --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutDialog_internalError.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + AppUpdater: "resource://gre/modules/AppUpdater.jsm", + sinon: "resource://testing-common/Sinon.jsm", +}); + +add_setup(function setup_internalErrorTest() { + const sandbox = sinon.createSandbox(); + sandbox.stub(AppUpdater.prototype, "aus").get(() => { + throw new Error("intentional test error"); + }); + sandbox.stub(AppUpdater.prototype, "checker").get(() => { + throw new Error("intentional test error"); + }); + sandbox.stub(AppUpdater.prototype, "um").get(() => { + throw new Error("intentional test error"); + }); + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +// Test for the About dialog's internal error handling. +add_task(async function aboutDialog_internalError() { + let params = {}; + await runAboutDialogUpdateTest(params, [ + { + panelId: "internalError", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_backgroundUpdateSetting.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_backgroundUpdateSetting.js new file mode 100644 index 0000000000..cd57efe985 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_backgroundUpdateSetting.js @@ -0,0 +1,175 @@ +/* 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"; + +/** + * This file tests the background update UI in about:preferences. + */ + +ChromeUtils.defineESModuleGetters(this, { + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", +}); + +const BACKGROUND_UPDATE_PREF = "app.update.background.enabled"; + +add_task(async function testBackgroundUpdateSettingUI() { + if (!AppConstants.MOZ_UPDATE_AGENT) { + // The element that we are testing in about:preferences is #ifdef'ed out of + // the file if MOZ_UPDATE_AGENT isn't defined. So there is nothing to + // test in that case. + logTestInfo( + ` +=============================================================================== +WARNING! This test involves background update, but background tasks are + disabled. This test will unconditionally pass since the feature it + wants to test isn't available. +=============================================================================== +` + ); + // Some of our testing environments do not consider a test to have passed if + // it didn't make any assertions. + ok(true, "Unconditionally passing test"); + return; + } + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences" + ); + + const originalBackgroundUpdateVal = await UpdateUtils.readUpdateConfigSetting( + BACKGROUND_UPDATE_PREF + ); + const originalUpdateAutoVal = await UpdateUtils.getAppUpdateAutoEnabled(); + registerCleanupFunction(async () => { + await BrowserTestUtils.removeTab(tab); + await UpdateUtils.writeUpdateConfigSetting( + BACKGROUND_UPDATE_PREF, + originalBackgroundUpdateVal + ); + await UpdateUtils.setAppUpdateAutoEnabled(originalUpdateAutoVal); + }); + + // If auto update is disabled, the control for background update should be + // disabled, since we cannot update in the background if we can't update + // automatically. + await UpdateUtils.setAppUpdateAutoEnabled(false); + await SpecialPowers.spawn( + tab.linkedBrowser, + [UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED], + async perInstallationPrefsSupported => { + let backgroundUpdateCheckbox = content.document.getElementById( + "backgroundUpdate" + ); + is( + backgroundUpdateCheckbox.hidden, + !perInstallationPrefsSupported, + `The background update UI should ${ + perInstallationPrefsSupported ? "not" : "" + } be hidden when and perInstallationPrefsSupported is ` + + `${perInstallationPrefsSupported}` + ); + if (perInstallationPrefsSupported) { + is( + backgroundUpdateCheckbox.disabled, + true, + `The background update UI should be disabled when auto update is ` + + `disabled` + ); + } + } + ); + + if (!UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED) { + // The remaining tests only make sense on platforms where per-installation + // prefs are supported and the UI will ever actually be displayed + return; + } + + await UpdateUtils.setAppUpdateAutoEnabled(true); + await UpdateUtils.writeUpdateConfigSetting(BACKGROUND_UPDATE_PREF, true); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let backgroundUpdateCheckbox = content.document.getElementById( + "backgroundUpdate" + ); + is( + backgroundUpdateCheckbox.disabled, + false, + `The background update UI should not be disabled when auto update is ` + + `enabled` + ); + + is( + backgroundUpdateCheckbox.checked, + true, + "After enabling background update, the checkbox should be checked" + ); + + // Note that this action results in asynchronous activity. Normally when + // we change the update config, we await on the function to wait for the + // value to be written to the disk. We can't easily await on the UI state + // though. Luckily, we don't have to because reads/writes of the config file + // are serialized. So when we verify the written value by awaiting on + // readUpdateConfigSetting(), that will also wait for the value to be + // written to disk and for this UI to react to that. + backgroundUpdateCheckbox.click(); + }); + + is( + await UpdateUtils.readUpdateConfigSetting(BACKGROUND_UPDATE_PREF), + false, + "Toggling the checkbox should have changed the setting value to false" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let backgroundUpdateCheckbox = content.document.getElementById( + "backgroundUpdate" + ); + is( + backgroundUpdateCheckbox.checked, + false, + "After toggling the checked checkbox, it should be unchecked." + ); + + // Like the last call like this one, this initiates asynchronous behavior. + backgroundUpdateCheckbox.click(); + }); + + is( + await UpdateUtils.readUpdateConfigSetting(BACKGROUND_UPDATE_PREF), + true, + "Toggling the checkbox should have changed the setting value to true" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + is( + content.document.getElementById("backgroundUpdate").checked, + true, + "After toggling the unchecked checkbox, it should be checked" + ); + }); + + // Test that the UI reacts to observed setting changes properly. + await UpdateUtils.writeUpdateConfigSetting(BACKGROUND_UPDATE_PREF, false); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + is( + content.document.getElementById("backgroundUpdate").checked, + false, + "Externally disabling background update should uncheck the checkbox" + ); + }); + + await UpdateUtils.writeUpdateConfigSetting(BACKGROUND_UPDATE_PREF, true); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + is( + content.document.getElementById("backgroundUpdate").checked, + true, + "Externally enabling background update should check the checkbox" + ); + }); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded.js new file mode 100644 index 0000000000..6df4ee7ff5 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences background check for updates +// with the update downloaded when about:preferences is opened. +add_task(async function aboutPrefs_backgroundCheck_downloaded() { + let params = { backgroundUpdate: true, waitForUpdateState: STATE_PENDING }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_staged.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_staged.js new file mode 100644 index 0000000000..749a8f0b07 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_staged.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences background check for updates +// with the update downloaded and staged when about:preferences is opened. +add_task(async function aboutPrefs_backgroundCheck_downloaded_staged() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&invalidCompleteSize=1", + backgroundUpdate: true, + continueFile: CONTINUE_STAGING, + waitForUpdateState: STATE_APPLIED, + }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_staging.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_staging.js new file mode 100644 index 0000000000..73804cdf7a --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_staging.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences background check for updates +// with the update downloaded and about:preferences opened during staging. +add_task(async function aboutPrefs_backgroundCheck_downloaded_staged() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let lankpackCall = mockLangpackInstall(); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&invalidCompleteSize=1", + backgroundUpdate: true, + waitForUpdateState: STATE_PENDING, + }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + async tab => { + // Once the state is pending but langpacks aren't complete the about + // dialog should still be showing downloading. + TestUtils.waitForCondition(() => { + return readStatusFile() == STATE_APPLIED; + }); + + let updateDeckId = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + return content.gAppUpdater.selectedPanel.id; + } + ); + + is(updateDeckId, "applying", "UI should still show as applying."); + + let { appVersion, resolve } = await lankpackCall; + is( + appVersion, + Services.appinfo.version, + "Should see the right app version." + ); + resolve(); + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_stagingFailure.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_stagingFailure.js new file mode 100644 index 0000000000..9755fe167b --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_stagingFailure.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences background check for updates +// with the update downloaded and staging has failed when the about:preferences +// is opened. +add_task(async function aboutPrefs_backgroundCheck_downloaded_stagingFailure() { + Services.env.set("MOZ_TEST_STAGING_ERROR", "1"); + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&completePatchOnly=1", + backgroundUpdate: true, + waitForUpdateState: STATE_PENDING, + }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloading.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloading.js new file mode 100644 index 0000000000..d9034296b0 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloading.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences background check for updates +// with about:preferences opened during downloading. +add_task(async function aboutPrefs_backgroundCheck_downloading() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + let lankpackCall = mockLangpackInstall(); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&useSlowDownloadMar=1&invalidCompleteSize=1", + backgroundUpdate: true, + waitForUpdateState: STATE_DOWNLOADING, + }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + async tab => { + // Once the state is pending but langpacks aren't complete the about + // dialog should still be showing downloading. + TestUtils.waitForCondition(() => { + return readStatusFile() == STATE_PENDING; + }); + + let updateDeckId = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + return content.gAppUpdater.selectedPanel.id; + } + ); + + is(updateDeckId, "downloading", "UI should still show as downloading."); + + let { appVersion, resolve } = await lankpackCall; + is( + appVersion, + Services.appinfo.version, + "Should see the right app version." + ); + resolve(); + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloading_staging.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloading_staging.js new file mode 100644 index 0000000000..8085ec4c2c --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloading_staging.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences background check for updates +// with about:preferences opened during downloading and stages the update. +add_task(async function aboutPrefs_backgroundCheck_downloading_staging() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + let lankpackCall = mockLangpackInstall(); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { + queryString: "&useSlowDownloadMar=1&invalidCompleteSize=1", + backgroundUpdate: true, + waitForUpdateState: STATE_DOWNLOADING, + }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + async tab => { + // Once the state is pending but langpacks aren't complete the about + // dialog should still be showing downloading. + TestUtils.waitForCondition(() => { + return readStatusFile() == STATE_APPLIED; + }); + + let updateDeckId = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + return content.gAppUpdater.selectedPanel.id; + } + ); + + is(updateDeckId, "applying", "UI should still show as applying."); + + let { appVersion, resolve } = await lankpackCall; + is( + appVersion, + Services.appinfo.version, + "Should see the right app version." + ); + resolve(); + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_multiUpdate.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_multiUpdate.js new file mode 100644 index 0000000000..9ccf593db5 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_multiUpdate.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FIRST_UPDATE_VERSION = "999998.0"; +const SECOND_UPDATE_VERSION = "999999.0"; + +function prepareToDownloadVersion(version) { + setUpdateURL( + URL_HTTP_UPDATE_SJS + + `?detailsURL=${gDetailsURL}&promptWaitTime=0&appVersion=${version}` + ); +} + +add_task(async function aboutPrefs_backgroundCheck_multiUpdate() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let params = { + version: FIRST_UPDATE_VERSION, + backgroundUpdate: true, + waitForUpdateState: STATE_PENDING, + }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + () => { + prepareToDownloadVersion(SECOND_UPDATE_VERSION); + gAUS.checkForBackgroundUpdates(); + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_apply_blocked.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_apply_blocked.js new file mode 100644 index 0000000000..ccf3c06b95 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_apply_blocked.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromptTestUtils } = ChromeUtils.import( + "resource://testing-common/PromptTestUtils.jsm" +); + +const BUILDER_URL = "https://example.com/document-builder.sjs?html="; +const PAGE_MARKUP = ` +<html> +<head> + <script> + window.onbeforeunload = function() { + return true; + }; + </script> +</head> +<body>TEST PAGE</body> +</html> +`; +const TEST_URL = BUILDER_URL + encodeURI(PAGE_MARKUP); + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); +}); + +// Test for About Dialog foreground check for updates +// and apply but restart is blocked by a page. +add_task(async function aboutDialog_foregroundCheck_apply_blocked() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + let prefsTab; + let handlePromise = (async () => { + let dialog = await PromptTestUtils.waitForPrompt(window, { + modalType: Ci.nsIPrompt.MODAL_TYPE_CONTENT, + promptType: "confirmEx", + }); + await SpecialPowers.spawn(prefsTab.linkedBrowser, [], async () => { + Assert.equal( + content.gAppUpdater.selectedPanel.id, + "restarting", + "The restarting panel should be displayed" + ); + }); + + await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 }); + })(); + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1&promptWaitTime=0" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + async function getPrefsTab(tab) { + prefsTab = tab; + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + forceApply: true, + }, + async function ensureDialogHasBeenCanceled() { + await handlePromise; + }, + // A final check to ensure that we are back in the apply state. + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); + + BrowserTestUtils.removeTab(tab, { skipPermitUnload: true }); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_cantApply.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_cantApply.js new file mode 100644 index 0000000000..aac9b33134 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_cantApply.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// without the ability to apply updates. +add_task(async function aboutPrefs_foregroundCheck_cantApply() { + lockWriteTestFile(); + + let params = {}; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "manualUpdate", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_malformedXML.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_malformedXML.js new file mode 100644 index 0000000000..6d99a58e86 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_malformedXML.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with a malformed update XML file. +add_task(async function aboutPrefs_foregroundCheck_malformedXML() { + let params = { queryString: "&xmlMalformed=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "checkingFailed", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_noUpdate.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_noUpdate.js new file mode 100644 index 0000000000..a8f6c072f7 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_noUpdate.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with no update available. +add_task(async function aboutPrefs_foregroundCheck_noUpdate() { + let params = { queryString: "&noUpdates=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "noUpdatesFound", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_otherInstance.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_otherInstance.js new file mode 100644 index 0000000000..271c8f2837 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_otherInstance.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with another application instance handling updates. +add_task(async function aboutPrefs_foregroundCheck_otherInstance() { + setOtherInstanceHandlingUpdates(); + + let params = {}; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "otherInstanceHandlingUpdates", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_unsupported.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_unsupported.js new file mode 100644 index 0000000000..e18ce31bc4 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_unsupported.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with an unsupported update. +add_task(async function aboutPrefs_foregroundCheck_unsupported() { + let params = { queryString: "&unsupported=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "unsupportedSystem", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadAuto.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadAuto.js new file mode 100644 index 0000000000..bd5fd18289 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadAuto.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with an automatic download. +add_task(async function aboutPrefs_foregroundCheck_downloadAuto() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadAuto_staging.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadAuto_staging.js new file mode 100644 index 0000000000..1d9d082edd --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadAuto_staging.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with an automatic download and update staging. +add_task(async function aboutPrefs_foregroundCheck_downloadAuto_staging() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadOptIn.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadOptIn.js new file mode 100644 index 0000000000..115e875b74 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadOptIn.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with a manual download. +add_task(async function aboutPrefs_foregroundCheck_downloadOptIn() { + await UpdateUtils.setAppUpdateAutoEnabled(false); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + checkActiveUpdate: null, + continueFile: null, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadOptIn_staging.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadOptIn_staging.js new file mode 100644 index 0000000000..cff5d74701 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadOptIn_staging.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with a manual download and update staging. +add_task(async function aboutPrefs_foregroundCheck_downloadOptIn_staging() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + await UpdateUtils.setAppUpdateAutoEnabled(false); + + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; + } + + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + checkActiveUpdate: null, + continueFile: null, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "applying", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: CONTINUE_STAGING, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_network_failure.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_network_failure.js new file mode 100644 index 0000000000..fbf91c0e54 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_network_failure.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates which fails, +// because it is impossible to connect to the update server. +add_task(async function aboutPrefs_foregroundCheck_network_failure() { + let params = { + baseURL: "https://localhost:7777", + }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingFailed", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_network_offline.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_network_offline.js new file mode 100644 index 0000000000..7b5b6899b3 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_network_offline.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates which fails because +// the browser is in offline mode and `localhost` cannot be resolved. +add_task(async function aboutPrefs_foregroundCheck_network_offline() { + info("[OFFLINE] Setting Services.io.offline (do not forget to reset it!)"); + // avoid that real network connectivity changes influence the test execution + Services.io.manageOfflineStatus = false; + Services.io.offline = true; + registerCleanupFunction(() => { + info("[ONLINE] Resetting Services.io.offline"); + Services.io.offline = false; + Services.io.manageOfflineStatus = true; + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.disable-localhost-when-offline", true], + ["network.dns.offline-localhost", false], + ], + }); + + await runAboutPrefsUpdateTest({}, [ + { + panelId: "checkingFailed", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_completeBadSize.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_completeBadSize.js new file mode 100644 index 0000000000..a36d3d9807 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_completeBadSize.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with a complete bad size patch. +add_task(async function aboutPrefs_foregroundCheck_completeBadSize() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "complete", bitsResult: gBadSizeResult }; + downloadInfo[1] = { patchType: "complete", internalResult: gBadSizeResult }; + } else { + downloadInfo[0] = { patchType: "complete", internalResult: gBadSizeResult }; + } + + let params = { queryString: "&completePatchOnly=1&invalidCompleteSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "downloadFailed", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize.js new file mode 100644 index 0000000000..060acd405a --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with a partial bad size patch. +add_task(async function aboutPrefs_foregroundCheck_partialBadSize() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: gBadSizeResult }; + downloadInfo[1] = { patchType: "partial", internalResult: gBadSizeResult }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: gBadSizeResult }; + } + + let params = { queryString: "&partialPatchOnly=1&invalidPartialSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "downloadFailed", + checkActiveUpdate: null, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize_complete.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize_complete.js new file mode 100644 index 0000000000..c6e5b5b20b --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize_complete.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with a partial bad size patch and a complete patch. +add_task(async function aboutPrefs_foregroundCheck_partialBadSize_complete() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: gBadSizeResult }; + downloadInfo[1] = { patchType: "partial", internalResult: gBadSizeResult }; + downloadInfo[2] = { patchType: "complete", bitsResult: "0" }; + } else { + downloadInfo[0] = { patchType: "partial", internalResult: gBadSizeResult }; + downloadInfo[1] = { patchType: "complete", internalResult: "0" }; + } + + let params = { queryString: "&invalidPartialSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize_completeBadSize.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize_completeBadSize.js new file mode 100644 index 0000000000..62abdc3af5 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize_completeBadSize.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for about:preferences foreground check for updates +// with a partial bad size patch and a complete bad size patch. +add_task( + async function aboutPrefs_foregroundCheck_partialBadSize_completeBadSize() { + let downloadInfo = []; + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED)) { + downloadInfo[0] = { patchType: "partial", bitsResult: gBadSizeResult }; + downloadInfo[1] = { + patchType: "partial", + internalResult: gBadSizeResult, + }; + downloadInfo[2] = { patchType: "complete", bitsResult: gBadSizeResult }; + downloadInfo[3] = { + patchType: "complete", + internalResult: gBadSizeResult, + }; + } else { + downloadInfo[0] = { + patchType: "partial", + internalResult: gBadSizeResult, + }; + downloadInfo[1] = { + patchType: "complete", + internalResult: gBadSizeResult, + }; + } + + let params = { queryString: "&invalidPartialSize=1&invalidCompleteSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "downloadFailed", + checkActiveUpdate: null, + continueFile: null, + }, + ]); + } +); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_internalError.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_internalError.js new file mode 100644 index 0000000000..f22b60ae5a --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_internalError.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + AppUpdater: "resource://gre/modules/AppUpdater.jsm", + sinon: "resource://testing-common/Sinon.jsm", +}); + +add_setup(function setup_internalErrorTest() { + const sandbox = sinon.createSandbox(); + sandbox.stub(AppUpdater.prototype, "aus").get(() => { + throw new Error("intentional test error"); + }); + sandbox.stub(AppUpdater.prototype, "checker").get(() => { + throw new Error("intentional test error"); + }); + sandbox.stub(AppUpdater.prototype, "um").get(() => { + throw new Error("intentional test error"); + }); + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +// Test for about:preferences internal error handling. +add_task(async function aboutPrefs_internalError() { + let params = {}; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "internalError", + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_settings.js b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_settings.js new file mode 100644 index 0000000000..c249d2d73a --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_aboutPrefs_settings.js @@ -0,0 +1,151 @@ +/* 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/. + */ + +// Changes, then verifies the value of app.update.auto via the about:preferences +// UI. Requires a tab with about:preferences open to be passed in. +async function changeAndVerifyPref(tab, newConfigValue) { + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ newConfigValue }], + async function({ newConfigValue }) { + let radioId = newConfigValue ? "autoDesktop" : "manualDesktop"; + let radioElement = content.document.getElementById(radioId); + let updateRadioGroup = radioElement.radioGroup; + let promise = ContentTaskUtils.waitForEvent( + updateRadioGroup, + "ProcessedUpdatePrefChange" + ); + radioElement.click(); + await promise; + + is( + updateRadioGroup.value, + `${newConfigValue}`, + "Update preference should match expected" + ); + is( + updateRadioGroup.disabled, + false, + "Update preferences should no longer be disabled" + ); + } + ); + + let configValueRead = await UpdateUtils.getAppUpdateAutoEnabled(); + is( + configValueRead, + newConfigValue, + "Value returned should have matched the expected value" + ); +} + +async function changeAndVerifyUpdateWrites({ + tab, + newConfigValue, + discardUpdate, + expectPrompt, + expectRemainingUpdate, +}) { + // A value of 1 will keep the update and a value of 0 will discard the update + // when the prompt service is called when the value of app.update.auto is + // changed to false. + let confirmExReply = discardUpdate ? 0 : 1; + let didPrompt = false; + let promptService = { + QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]), + confirmEx(...args) { + promptService._confirmExArgs = args; + didPrompt = true; + return confirmExReply; + }, + }; + Services.prompt = promptService; + await changeAndVerifyPref(tab, newConfigValue); + is( + didPrompt, + expectPrompt, + `We should ${expectPrompt ? "" : "not "}be prompted` + ); + is( + !!gUpdateManager.readyUpdate, + expectRemainingUpdate, + `There should ${expectRemainingUpdate ? "" : "not "}be a ready update` + ); +} + +add_task(async function testUpdateAutoPrefUI() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences" + ); + + // Hack: make the test run faster: + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + content.gMainPane._minUpdatePrefDisableTime = 10; + }); + + info("Enable automatic updates and check that works."); + await changeAndVerifyPref(tab, true); + ok( + !gUpdateManager.downloadingUpdate, + "There should not be a downloading update" + ); + ok(!gUpdateManager.readyUpdate, "There should not be a ready update"); + + info("Disable automatic updates and check that works."); + await changeAndVerifyPref(tab, false); + ok( + !gUpdateManager.downloadingUpdate, + "There should not be a downloading update" + ); + ok(!gUpdateManager.readyUpdate, "There should not be a ready update"); + + let patchProps = { state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + let updateProps = { checkInterval: "1" }; + let updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_PENDING); + reloadUpdateManagerData(); + ok(!!gUpdateManager.readyUpdate, "There should be a ready update"); + + let { prompt } = Services; + registerCleanupFunction(() => { + Services.prompt = prompt; + }); + + // Setting the value to false will call the prompt service and when we + // don't discard the update there should still be an active update afterwards. + await changeAndVerifyUpdateWrites({ + tab, + newConfigValue: false, + discardUpdate: false, + expectPrompt: true, + expectRemainingUpdate: true, + }); + + // Setting the value to true should not call the prompt service so there + // should still be an active update, even if we indicate we can discard + // the update in a hypothetical prompt. + await changeAndVerifyUpdateWrites({ + tab, + newConfigValue: true, + discardUpdate: true, + expectPrompt: false, + expectRemainingUpdate: true, + }); + + // Setting the value to false will call the prompt service, and we do + // discard the update, so there should not be an active update. + await changeAndVerifyUpdateWrites({ + tab, + newConfigValue: false, + discardUpdate: true, + expectPrompt: true, + expectRemainingUpdate: false, + }); + + await BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_cantApply.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_cantApply.js new file mode 100644 index 0000000000..90f4c385cc --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_cantApply.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_check_cantApply() { + lockWriteTestFile(); + + let params = { checkAttempts: 1, queryString: "&promptWaitTime=0" }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { whatsNew: gDetailsURL, manual: URL_MANUAL_UPDATE }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_malformedXML.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_malformedXML.js new file mode 100644 index 0000000000..d83bc70b6f --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_malformedXML.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_check_malformedXML() { + const maxBackgroundErrors = 10; + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors]], + }); + + let params = { + checkAttempts: maxBackgroundErrors, + queryString: "&xmlMalformed=1", + }; + await runDoorhangerUpdateTest(params, [ + { + // If the update check fails 10 consecutive attempts then the manual + // update doorhanger. + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { whatsNew: gDetailsURL, manual: URL_MANUAL_UPDATE }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_unsupported.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_unsupported.js new file mode 100644 index 0000000000..02aaab1064 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_unsupported.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for doorhanger background check for updates +// with an unsupported update. +add_task(async function doorhanger_bc_check_unsupported() { + let params = { checkAttempts: 1, queryString: "&unsupported=1" }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-unsupported", + button: "button", + pageURLs: { manual: gDetailsURL }, + }, + async function doorhanger_unsupported_persist() { + await TestUtils.waitForCondition( + () => PanelUI.menuButton.hasAttribute("badge-status"), + "Waiting for update badge", + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test. + logTestInfo(e); + }); + is( + PanelUI.notificationPanel.state, + "closed", + "The window's doorhanger is closed." + ); + ok( + PanelUI.menuButton.hasAttribute("badge-status"), + "The window has a badge." + ); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-unsupported", + "The correct badge is showing for the background window" + ); + + // Test persistence of the badge when the client has restarted by + // resetting the UpdateListener. + UpdateListener.reset(); + is( + PanelUI.notificationPanel.state, + "closed", + "The window's doorhanger is closed." + ); + ok( + !PanelUI.menuButton.hasAttribute("badge-status"), + "The window does not have a badge." + ); + UpdateListener.maybeShowUnsupportedNotification(); + is( + PanelUI.notificationPanel.state, + "closed", + "The window's doorhanger is closed." + ); + ok( + PanelUI.menuButton.hasAttribute("badge-status"), + "The window has a badge." + ); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-unsupported", + "The correct badge is showing for the background window." + ); + }, + ]); + + params = { + checkAttempts: 1, + queryString: "&invalidCompleteSize=1&promptWaitTime=0", + }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_PENDING }, + }, + async function doorhanger_unsupported_removed() { + // Test that finding an update removes the app.update.unsupported.url + // preference. + let unsupportedURL = Services.prefs.getCharPref( + PREF_APP_UPDATE_UNSUPPORTED_URL, + null + ); + ok( + !unsupportedURL, + "The " + PREF_APP_UPDATE_UNSUPPORTED_URL + " preference was removed." + ); + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadAutoFailures.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadAutoFailures.js new file mode 100644 index 0000000000..3678a440d2 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadAutoFailures.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_downloadAutoFailures() { + const maxBackgroundErrors = 5; + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors]], + }); + + let params = { checkAttempts: 1, queryString: "&badURL=1" }; + await runDoorhangerUpdateTest(params, [ + { + // If the update download fails maxBackgroundErrors download attempts then + // show the update available prompt. + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + // If the update process is unable to install the update show the manual + // update doorhanger. + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { manual: URL_MANUAL_UPDATE }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadAutoFailures_bgWin.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadAutoFailures_bgWin.js new file mode 100644 index 0000000000..0683e0b532 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadAutoFailures_bgWin.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_downloadAutoFailures_bgWin() { + function getBackgroundWindowHandler(destroyWindow) { + return async function() { + await TestUtils.waitForCondition( + () => PanelUI.menuButton.hasAttribute("badge-status"), + "Background window has a badge.", + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test. + logTestInfo(e); + }); + ok( + PanelUI.menuButton.hasAttribute("badge-status"), + "PanelUI.menuButton should have a 'badge-status' attribute" + ); + is( + PanelUI.notificationPanel.state, + "closed", + "The doorhanger is not showing for the background window" + ); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-available", + "The badge is showing for the background window" + ); + + let buttonEl = getNotificationButton( + extraWindow, + "update-available", + "button" + ); + buttonEl.click(); + + if (destroyWindow) { + // The next popup may be shown during closeWindow or promiseFocus + // calls. + let waitForPopupShown = new Promise(resolve => { + window.addEventListener( + "popupshown", + () => { + executeSoon(resolve); + }, + { once: true } + ); + }); + await BrowserTestUtils.closeWindow(extraWindow); + await SimpleTest.promiseFocus(window); + await waitForPopupShown; + } + }; + } + + const maxBackgroundErrors = 5; + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors]], + }); + + let extraWindow = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(extraWindow); + + let params = { checkAttempts: 1, queryString: "&badURL=1", popupShown: true }; + await runDoorhangerUpdateTest(params, [ + getBackgroundWindowHandler(false), + getBackgroundWindowHandler(true), + { + // If the update process is unable to install the update show the manual + // update doorhanger. + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { manual: URL_MANUAL_UPDATE }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn.js new file mode 100644 index 0000000000..2237161863 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_downloadOptIn() { + await UpdateUtils.setAppUpdateAutoEnabled(false); + + let params = { + checkAttempts: 1, + queryString: "&invalidCompleteSize=1&promptWaitTime=0", + }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_PENDING }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_bgWin.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_bgWin.js new file mode 100644 index 0000000000..0975166bd2 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_bgWin.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_downloadOptIn_bgWin() { + function getBackgroundWindowHandler() { + return async function() { + await TestUtils.waitForCondition( + () => PanelUI.menuButton.hasAttribute("badge-status"), + "Background window has a badge.", + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test. + logTestInfo(e); + }); + ok( + PanelUI.menuButton.hasAttribute("badge-status"), + "PanelUI.menuButton should have a 'badge-status' attribute" + ); + is( + PanelUI.notificationPanel.state, + "closed", + "The doorhanger is not showing for the background window" + ); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-available", + "The badge is showing for the background window" + ); + let popupShownPromise = BrowserTestUtils.waitForEvent( + PanelUI.notificationPanel, + "popupshown" + ); + await BrowserTestUtils.closeWindow(extraWindow); + await SimpleTest.promiseFocus(window); + await popupShownPromise; + + let buttonEl = getNotificationButton( + window, + "update-available", + "button" + ); + buttonEl.click(); + }; + } + + await UpdateUtils.setAppUpdateAutoEnabled(false); + + let extraWindow = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(extraWindow); + + let params = { checkAttempts: 1, queryString: "&promptWaitTime=0" }; + await runDoorhangerUpdateTest(params, [ + getBackgroundWindowHandler(), + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_PENDING }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_staging.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_staging.js new file mode 100644 index 0000000000..7ba2d67964 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_staging.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_downloadOptIn_staging() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Tests the app.update.promptWaitTime pref + [PREF_APP_UPDATE_PROMPTWAITTIME, 0], + [PREF_APP_UPDATE_STAGING_ENABLED, true], + ], + }); + await UpdateUtils.setAppUpdateAutoEnabled(false); + + let params = { checkAttempts: 1, queryString: "&invalidCompleteSize=1" }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_APPLIED }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded.js new file mode 100644 index 0000000000..e29dad26fa --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_downloaded() { + let params = { + checkAttempts: 1, + queryString: "&invalidCompleteSize=1&promptWaitTime=0", + }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_PENDING }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_disableBITS.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_disableBITS.js new file mode 100644 index 0000000000..5f89c95322 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_disableBITS.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_downloaded_disableBITS() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_BITS_ENABLED, true]], + }); + + let params = { + checkAttempts: 1, + queryString: "&promptWaitTime=0&disableBITS=true", + }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_PENDING }, + }, + ]); + + let patch = getPatchOfType( + "partial", + gUpdateManager.readyUpdate + ).QueryInterface(Ci.nsIWritablePropertyBag); + ok( + !patch.getProperty("bitsId"), + "The selected patch should not have a bitsId property" + ); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_staged.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_staged.js new file mode 100644 index 0000000000..50416608f2 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_staged.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_downloaded_staged() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let params = { + checkAttempts: 1, + queryString: "&invalidCompleteSize=1&promptWaitTime=0", + }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_APPLIED }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate.js new file mode 100644 index 0000000000..07e7bf51fa --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test downloads 2 updates sequentially, and ensures that the doorhanger + * and badge do what they are supposed to do: + * First thing after the first download, the doorhanger should be displayed. + * Then download the next update. + * While that update stages, the badge should be hidden to prevent restarting + * to update while the update is staging. + * Once the staging completes, the badge should return. The doorhanger should + * not be shown at this time, because it has already been shown this + * session. + */ + +const FIRST_UPDATE_VERSION = "999998.0"; +const SECOND_UPDATE_VERSION = "999999.0"; + +function prepareToDownloadVersion(version) { + setUpdateURL( + URL_HTTP_UPDATE_SJS + + `?detailsURL=${gDetailsURL}&promptWaitTime=0&appVersion=${version}` + ); +} + +add_task(async function doorhanger_bc_multiUpdate() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let params = { + checkAttempts: 1, + queryString: "&promptWaitTime=0", + version: FIRST_UPDATE_VERSION, + slowStaging: true, + }; + await runDoorhangerUpdateTest(params, [ + () => { + return continueFileHandler(CONTINUE_STAGING); + }, + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_APPLIED }, + }, + async () => { + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-restart", + "Should have restart badge" + ); + + prepareToDownloadVersion(SECOND_UPDATE_VERSION); + let updateSwapped = waitForEvent("update-swap"); + gAUS.checkForBackgroundUpdates(); + await updateSwapped; + // The badge should be hidden while we swap from one update to the other + // to prevent restarting to update while staging is occurring. But since + // it will be waiting on the same event we are waiting on, wait an + // additional tick to let the other update-swap listeners run. + await TestUtils.waitForTick(); + + is( + PanelUI.menuButton.getAttribute("badge-status"), + "", + "Should not have restart badge during staging" + ); + + await continueFileHandler(CONTINUE_STAGING); + + try { + await TestUtils.waitForCondition( + () => + PanelUI.menuButton.getAttribute("badge-status") == "update-restart", + "Waiting for update restart badge to return after staging" + ); + } catch (ex) {} + + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-restart", + "Restart badge should be restored after staging completes" + ); + is( + PanelUI.notificationPanel.state, + "closed", + "Should not open a second doorhanger" + ); + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate_promptWaitTime.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate_promptWaitTime.js new file mode 100644 index 0000000000..00c61bcbb4 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate_promptWaitTime.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test downloads 2 updates sequentially, and ensures that the doorhanger + * and badge do what they are supposed to do. However, the first update has a + * long promptWaitTime, and the second has a short one and the badge wait time + * is set to 0. This should result in this behavior: + * First thing after the first download, the badge should be displayed, but + * not the doorhanger. + * Then download the next update. + * While that update stages, the badge should be hidden to prevent restarting + * to update while the update is staging. + * Once the staging completes, the doorhanger should be shown. Despite the + * long promptWaitTime of the initial update, this patch's short wait time + * means that the doorhanger should be shown soon rather than in a long + * time. + */ + +const FIRST_UPDATE_VERSION = "999998.0"; +const SECOND_UPDATE_VERSION = "999999.0"; +const LONG_PROMPT_WAIT_TIME_SEC = 10 * 60 * 60; // 10 hours + +function prepareToDownloadVersion(version, promptWaitTime) { + setUpdateURL( + URL_HTTP_UPDATE_SJS + + `?detailsURL=${gDetailsURL}&promptWaitTime=${promptWaitTime}&appVersion=${version}` + ); +} + +add_task(async function doorhanger_bc_multiUpdate() { + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_APP_UPDATE_STAGING_ENABLED, true], + [PREF_APP_UPDATE_BADGEWAITTIME, 0], + ], + }); + + let params = { + checkAttempts: 1, + queryString: `&promptWaitTime=${LONG_PROMPT_WAIT_TIME_SEC}`, + version: FIRST_UPDATE_VERSION, + slowStaging: true, + }; + await runDoorhangerUpdateTest(params, [ + async () => { + await continueFileHandler(CONTINUE_STAGING); + + try { + await TestUtils.waitForCondition( + () => + PanelUI.menuButton.getAttribute("badge-status") == "update-restart", + "Waiting for update restart badge to return after staging" + ); + } catch (ex) {} + + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-restart", + "Should have restart badge" + ); + + prepareToDownloadVersion(SECOND_UPDATE_VERSION, 0); + let updateSwapped = waitForEvent("update-swap"); + gAUS.checkForBackgroundUpdates(); + await updateSwapped; + // The badge should be hidden while we swap from one update to the other + // to prevent restarting to update while staging is occurring. But since + // it will be waiting on the same event we are waiting on, wait an + // additional tick to let the other update-swap listeners run. + await TestUtils.waitForTick(); + + is( + PanelUI.menuButton.getAttribute("badge-status"), + "", + "Should not have restart badge during staging" + ); + + await continueFileHandler(CONTINUE_STAGING); + }, + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_APPLIED }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_completeBadSize.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_completeBadSize.js new file mode 100644 index 0000000000..3c29d6b4b0 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_completeBadSize.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_patch_completeBadSize() { + let params = { + checkAttempts: 1, + queryString: "&completePatchOnly=1&invalidCompleteSize=1", + }; + await runDoorhangerUpdateTest(params, [ + { + // If the update download fails maxBackgroundErrors download attempts then + // show the update available prompt. + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + // If the update process is unable to install the update show the manual + // update doorhanger. + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { manual: URL_MANUAL_UPDATE }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize.js new file mode 100644 index 0000000000..68854b3f26 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_patch_partialBadSize() { + let params = { + checkAttempts: 1, + queryString: "&partialPatchOnly=1&invalidPartialSize=1", + }; + await runDoorhangerUpdateTest(params, [ + { + // If the update download fails maxBackgroundErrors download attempts then + // show the update available prompt. + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + // If the update process is unable to install the update show the manual + // update doorhanger. + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { manual: URL_MANUAL_UPDATE }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_complete.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_complete.js new file mode 100644 index 0000000000..f3c9b1f51f --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_complete.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_patch_partialBadSize_complete() { + let params = { + checkAttempts: 1, + queryString: "&invalidPartialSize=1&promptWaitTime=0", + }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_PENDING }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_completeBadSize.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_completeBadSize.js new file mode 100644 index 0000000000..a18e2f6444 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_completeBadSize.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_bc_patch_partialBadSize_completeBadSize() { + let params = { + checkAttempts: 1, + queryString: "&invalidPartialSize=1&invalidCompleteSize=1", + }; + await runDoorhangerUpdateTest(params, [ + { + // If the update download fails maxBackgroundErrors download attempts then + // show the update available prompt. + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + notificationId: "update-available", + button: "button", + checkActiveUpdate: null, + }, + { + // If the update process is unable to install the update show the manual + // update doorhanger. + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { manual: URL_MANUAL_UPDATE }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_completeApplyFailure.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_completeApplyFailure.js new file mode 100644 index 0000000000..5c7c937d81 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_completeApplyFailure.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_sp_patch_completeApplyFailure() { + let patchProps = { state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + let updateProps = { checkInterval: "1" }; + let updates = getLocalUpdateString(updateProps, patches); + + let params = { updates }; + await runDoorhangerUpdateTest(params, [ + { + // If the update process is unable to install the update show the manual + // update doorhanger. + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { manual: URL_MANUAL_UPDATE }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure.js new file mode 100644 index 0000000000..45434c8361 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_sp_patch_partialApplyFailure() { + let patchProps = { type: "partial", state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + let updateProps = { isCompleteUpdate: "false", checkInterval: "1" }; + let updates = getLocalUpdateString(updateProps, patches); + + let params = { updates }; + await runDoorhangerUpdateTest(params, [ + { + // If there is only an invalid patch show the manual update doorhanger. + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { manual: URL_MANUAL_UPDATE }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete.js new file mode 100644 index 0000000000..bf533dab04 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function doorhanger_sp_patch_partialApplyFailure_complete() { + let patchProps = { type: "partial", state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + patchProps = { selected: "false" }; + patches += getLocalPatchString(patchProps); + let updateProps = { isCompleteUpdate: "false", promptWaitTime: "0" }; + let updates = getLocalUpdateString(updateProps, patches); + + let params = { updates }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_PENDING }, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_completeBadSize.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_completeBadSize.js new file mode 100644 index 0000000000..df17bc1220 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_completeBadSize.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function doorhanger_sp_patch_partialApplyFailure_completeBadSize() { + // Because of the way the test is simulating failure it has to pretend it has + // already retried. + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS, 0]], + }); + + let patchProps = { type: "partial", state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + patchProps = { size: "1234", selected: "false" }; + patches += getLocalPatchString(patchProps); + let updateProps = { isCompleteUpdate: "false" }; + let updates = getLocalUpdateString(updateProps, patches); + + let params = { updates }; + await runDoorhangerUpdateTest(params, [ + { + // If there is only an invalid patch show the manual update doorhanger. + notificationId: "update-manual", + button: "button", + checkActiveUpdate: null, + pageURLs: { manual: URL_MANUAL_UPDATE }, + }, + ]); + } +); diff --git a/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete_staging.js b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete_staging.js new file mode 100644 index 0000000000..a99c04b0be --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete_staging.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function doorhanger_sp_patch_partialApplyFailure_complete_staging() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let patchProps = { type: "partial", state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + patchProps = { selected: "false" }; + patches += getLocalPatchString(patchProps); + let updateProps = { isCompleteUpdate: "false", promptWaitTime: "0" }; + let updates = getLocalUpdateString(updateProps, patches); + + let params = { updates }; + await runDoorhangerUpdateTest(params, [ + { + notificationId: "update-restart", + button: "secondaryButton", + checkActiveUpdate: { state: STATE_APPLIED }, + }, + ]); + } +); diff --git a/toolkit/mozapps/update/tests/browser/browser_elevationDialog.js b/toolkit/mozapps/update/tests/browser/browser_elevationDialog.js new file mode 100644 index 0000000000..6aa32a7fc9 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_elevationDialog.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function elevation_dialog() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_DISABLEDFORTESTING, false]], + }); + + // Create a mock of nsIAppStartup's quit method so clicking the restart button + // won't restart the application. + let { startup } = Services; + let appStartup = { + QueryInterface: ChromeUtils.generateQI(["nsIAppStartup"]), + quit(mode) { + if (elevationDialog) { + elevationDialog.close(); + elevationDialog = null; + } + }, + }; + Services.startup = appStartup; + registerCleanupFunction(() => { + Services.startup = startup; + }); + + registerCleanupFunction(async () => { + let win = Services.wm.getMostRecentWindow("Update:Elevation"); + if (win) { + win.close(); + await TestUtils.waitForCondition( + () => !Services.wm.getMostRecentWindow("Update:Elevation"), + "The Update Elevation dialog should have closed" + ); + } + }); + + // Test clicking the "Restart Later" button + let elevationDialog = await waitForElevationDialog(); + await TestUtils.waitForTick(); + elevationDialog.document.getElementById("elevateExtra2").click(); + await TestUtils.waitForCondition( + () => !Services.wm.getMostRecentWindow("Update:Elevation"), + "The Update Elevation dialog should have closed" + ); + ok(!!gUpdateManager.readyUpdate, "There should be a ready update"); + is( + gUpdateManager.readyUpdate.state, + STATE_PENDING_ELEVATE, + "The ready update state should equal " + STATE_PENDING_ELEVATE + ); + is( + readStatusFile(), + STATE_PENDING_ELEVATE, + "The status file state should equal " + STATE_PENDING_ELEVATE + ); + + // Test clicking the "No Thanks" button + elevationDialog = await waitForElevationDialog(); + await TestUtils.waitForTick(); + elevationDialog.document.getElementById("elevateExtra1").click(); + await TestUtils.waitForCondition( + () => !Services.wm.getMostRecentWindow("Update:Elevation"), + "The Update Elevation dialog should have closed" + ); + ok(!gUpdateManager.readyUpdate, "There should not be a ready update"); + is( + readStatusFile(), + STATE_NONE, + "The status file state should equal " + STATE_NONE + ); + + // Test clicking the "Restart <brandShortName>" button + elevationDialog = await waitForElevationDialog(); + await TestUtils.waitForTick(); + elevationDialog.document.getElementById("elevateAccept").click(); + await TestUtils.waitForCondition( + () => !Services.wm.getMostRecentWindow("Update:Elevation"), + "The Update Elevation dialog should have closed" + ); + ok(!!gUpdateManager.readyUpdate, "There should be a ready update"); + is( + gUpdateManager.readyUpdate.state, + STATE_PENDING_ELEVATE, + "The active update state should equal " + STATE_PENDING_ELEVATE + ); + is( + readStatusFile(), + STATE_PENDING, + "The status file state should equal " + STATE_PENDING + ); +}); + +/** + * Waits for the Update Elevation Dialog to load. + * + * @return A promise that returns the domWindow for the Update Elevation Dialog + * and resolves when the Update Elevation Dialog loads. + */ +function waitForElevationDialog() { + return new Promise(resolve => { + var listener = { + onOpenWindow: aXULWindow => { + debugDump("Update Elevation dialog shown..."); + Services.wm.removeListener(listener); + + async function elevationDialogOnLoad() { + domwindow.removeEventListener("load", elevationDialogOnLoad, true); + let chromeURI = + "chrome://mozapps/content/update/updateElevation.xhtml"; + is( + domwindow.document.location.href, + chromeURI, + "Update Elevation appeared" + ); + resolve(domwindow); + } + + var domwindow = aXULWindow.docShell.domWindow; + domwindow.addEventListener("load", elevationDialogOnLoad, true); + }, + onCloseWindow: aXULWindow => {}, + }; + + Services.wm.addListener(listener); + // Add the active-update.xml and update.status files used for these tests, + // reload the update manager, and then simulate startup so the Update + // Elevation Dialog is opened. + let patchProps = { state: STATE_PENDING_ELEVATE }; + let patches = getLocalPatchString(patchProps); + let updateProps = { checkInterval: "1" }; + let updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_PENDING_ELEVATE); + reloadUpdateManagerData(); + testPostUpdateProcessing(); + }); +} diff --git a/toolkit/mozapps/update/tests/browser/browser_telemetry_updatePing_downloaded_ready.js b/toolkit/mozapps/update/tests/browser/browser_telemetry_updatePing_downloaded_ready.js new file mode 100644 index 0000000000..eb84e113d1 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_telemetry_updatePing_downloaded_ready.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryArchiveTesting } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryArchiveTesting.sys.mjs" +); + +/** + * Test that UpdatePing telemetry with a payload reason of ready is sent for a + * staged update. + * + * Please note that this is really a Telemetry test, not an + * "update UI" test like the rest of the tests in this directory. + * This test does not live in toolkit/components/telemetry/tests to prevent + * duplicating the code for all the test dependencies. Unfortunately, due + * to a limitation in the build system, we were not able to simply reference + * the dependencies as "support-files" in the test manifest. + */ +add_task(async function telemetry_updatePing_ready() { + let archiveChecker = new TelemetryArchiveTesting.Checker(); + await archiveChecker.promiseInit(); + + let updateParams = ""; + await runTelemetryUpdateTest(updateParams, "update-downloaded"); + + // We cannot control when the ping will be generated/archived after we trigger + // an update, so let's make sure to have one before moving on with validation. + let updatePing; + await TestUtils.waitForCondition( + async function() { + // Check that the ping made it into the Telemetry archive. + // The test data is defined in ../data/sharedUpdateXML.js + updatePing = await archiveChecker.promiseFindPing("update", [ + [["payload", "reason"], "ready"], + [["payload", "targetBuildId"], "20080811053724"], + ]); + return !!updatePing; + }, + "Make sure the ping is generated before trying to validate it.", + 500, + 100 + ); + + ok(updatePing, "The 'update' ping must be correctly sent."); + + // We don't know the exact value for the other fields, so just check + // that they're available. + for (let f of ["targetVersion", "targetChannel", "targetDisplayVersion"]) { + ok( + f in updatePing.payload, + `${f} must be available in the update ping payload.` + ); + ok( + typeof updatePing.payload[f] == "string", + `${f} must have the correct format.` + ); + } + + // Also make sure that the ping contains both a client id and an + // environment section. + ok("clientId" in updatePing, "The update ping must report a client id."); + ok( + "environment" in updatePing, + "The update ping must report the environment." + ); +}); diff --git a/toolkit/mozapps/update/tests/browser/browser_telemetry_updatePing_staged_ready.js b/toolkit/mozapps/update/tests/browser/browser_telemetry_updatePing_staged_ready.js new file mode 100644 index 0000000000..84b39851d4 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/browser_telemetry_updatePing_staged_ready.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TelemetryArchiveTesting } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryArchiveTesting.sys.mjs" +); + +/** + * Test that UpdatePing telemetry with a payload reason of ready is sent for a + * staged update. + * + * Please note that this is really a Telemetry test, not an + * "update UI" test like the rest of the tests in this directory. + * This test does not live in toolkit/components/telemetry/tests to prevent + * duplicating the code for all the test dependencies. Unfortunately, due + * to a limitation in the build system, we were not able to simply reference + * the dependencies as "support-files" in the test manifest. + */ +add_task(async function telemetry_updatePing_ready() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + let archiveChecker = new TelemetryArchiveTesting.Checker(); + await archiveChecker.promiseInit(); + + let updateParams = ""; + await runTelemetryUpdateTest(updateParams, "update-staged"); + + // We cannot control when the ping will be generated/archived after we trigger + // an update, so let's make sure to have one before moving on with validation. + let updatePing; + await TestUtils.waitForCondition( + async function() { + // Check that the ping made it into the Telemetry archive. + // The test data is defined in ../data/sharedUpdateXML.js + updatePing = await archiveChecker.promiseFindPing("update", [ + [["payload", "reason"], "ready"], + [["payload", "targetBuildId"], "20080811053724"], + ]); + return !!updatePing; + }, + "Make sure the ping is generated before trying to validate it.", + 500, + 100 + ); + + ok(updatePing, "The 'update' ping must be correctly sent."); + + // We don't know the exact value for the other fields, so just check + // that they're available. + for (let f of ["targetVersion", "targetChannel", "targetDisplayVersion"]) { + ok( + f in updatePing.payload, + `${f} must be available in the update ping payload.` + ); + ok( + typeof updatePing.payload[f] == "string", + `${f} must have the correct format.` + ); + } + + // Also make sure that the ping contains both a client id and an + // environment section. + ok("clientId" in updatePing, "The update ping must report a client id."); + ok( + "environment" in updatePing, + "The update ping must report the environment." + ); +}); diff --git a/toolkit/mozapps/update/tests/browser/downloadPage.html b/toolkit/mozapps/update/tests/browser/downloadPage.html new file mode 100644 index 0000000000..4810e2e0d6 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/downloadPage.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> + <title>Download page</title> + <meta charset="utf-8"> +</head> +<body> +<!-- just use simple.mar since we have it available and it will result in a download dialog --> +<a id="download-link" href="http://example.com/browser/browser/base/content/test/appUpdate/simple.mar" data-link-type="download"> + Download +</a> +</body> +</html> diff --git a/toolkit/mozapps/update/tests/browser/head.js b/toolkit/mozapps/update/tests/browser/head.js new file mode 100644 index 0000000000..46409e4397 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/head.js @@ -0,0 +1,1289 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", +}); +ChromeUtils.defineModuleGetter( + this, + "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "UpdateListener", + "resource://gre/modules/UpdateListener.jsm" +); +const { XPIInstall } = ChromeUtils.import( + "resource://gre/modules/addons/XPIInstall.jsm" +); + +const BIN_SUFFIX = AppConstants.platform == "win" ? ".exe" : ""; +const FILE_UPDATER_BIN = + "updater" + (AppConstants.platform == "macosx" ? ".app" : BIN_SUFFIX); +const FILE_UPDATER_BIN_BAK = FILE_UPDATER_BIN + ".bak"; + +const LOG_FUNCTION = info; + +const MAX_UPDATE_COPY_ATTEMPTS = 10; + +const DATA_URI_SPEC = + "chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/"; +/* import-globals-from testConstants.js */ +Services.scriptloader.loadSubScript(DATA_URI_SPEC + "testConstants.js", this); + +var gURLData = URL_HOST + "/" + REL_PATH_DATA; +const URL_MANUAL_UPDATE = gURLData + "downloadPage.html"; + +const gBadSizeResult = Cr.NS_ERROR_UNEXPECTED.toString(); + +/* import-globals-from ../data/shared.js */ +Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this); + +let gOriginalUpdateAutoValue = null; + +// Some elements append a trailing /. After the chrome tests are removed this +// code can be changed so URL_HOST already has a trailing /. +const gDetailsURL = URL_HOST + "/"; + +// Set to true to log additional information for debugging. To log additional +// information for individual tests set gDebugTest to false here and to true +// globally in the test. +gDebugTest = false; + +// This is to accommodate the TV task which runs the tests with --verify. +requestLongerTimeout(10); + +/** + * Common tasks to perform for all tests before each one has started. + */ +add_setup(async function setupTestCommon() { + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_APP_UPDATE_BADGEWAITTIME, 1800], + [PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0], + [PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS, 2], + [PREF_APP_UPDATE_LOG, gDebugTest], + [PREF_APP_UPDATE_PROMPTWAITTIME, 3600], + [PREF_APP_UPDATE_SERVICE_ENABLED, false], + ], + }); + + // We need to keep the update sync manager from thinking two instances are + // running because of the mochitest parent instance, which means we need to + // override the directory service with a fake executable path and then reset + // the lock. But leaving the directory service overridden causes problems for + // these tests, so we need to restore the real service immediately after. + // To form the path, we'll use the real executable path with a token appended + // (the path needs to be absolute, but not to point to a real file). + // This block is loosely copied from adjustGeneralPaths() in another update + // test file, xpcshellUtilsAUS.js, but this is a much more limited version; + // it's been copied here both because the full function is overkill and also + // because making it general enough to run in both xpcshell and mochitest + // would have been unreasonably difficult. + let exePath = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile); + let dirProvider = { + getFile: function AGP_DP_getFile(aProp, aPersistent) { + // Set the value of persistent to false so when this directory provider is + // unregistered it will revert back to the original provider. + aPersistent.value = false; + switch (aProp) { + case XRE_EXECUTABLE_FILE: + exePath.append("browser-test"); + return exePath; + } + return null; + }, + QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), + }; + let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService); + ds.QueryInterface(Ci.nsIProperties).undefine(XRE_EXECUTABLE_FILE); + ds.registerProvider(dirProvider); + + let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + syncManager.resetLock(); + + ds.unregisterProvider(dirProvider); + + setUpdateTimerPrefs(); + reloadUpdateManagerData(true); + removeUpdateFiles(true); + UpdateListener.reset(); + AppMenuNotifications.removeNotification(/.*/); + // Most app update mochitest-browser-chrome tests expect auto update to be + // enabled. Those that don't will explicitly change this. + await setAppUpdateAutoEnabledHelper(true); +}); + +/** + * Common tasks to perform for all tests after each one has finished. + */ +registerCleanupFunction(async () => { + AppMenuNotifications.removeNotification(/.*/); + Services.env.set("MOZ_TEST_SKIP_UPDATE_STAGE", ""); + Services.env.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", ""); + Services.env.set("MOZ_TEST_STAGING_ERROR", ""); + UpdateListener.reset(); + AppMenuNotifications.removeNotification(/.*/); + reloadUpdateManagerData(true); + // Pass false when the log files are needed for troubleshooting the tests. + removeUpdateFiles(true); + // Always try to restore the original updater files. If none of the updater + // backup files are present then this is just a no-op. + await finishTestRestoreUpdaterBackup(); + // Reset the update lock once again so that we know the lock we're + // interested in here will be closed properly (normally that happens during + // XPCOM shutdown, but that isn't consistent during tests). + let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + syncManager.resetLock(); +}); + +/** + * Overrides the add-ons manager language pack staging with a mocked version. + * The returned promise resolves when language pack staging begins returning an + * object with the new appVersion and platformVersion and functions to resolve + * or reject the install. + */ +function mockLangpackInstall() { + let original = XPIInstall.stageLangpacksForAppUpdate; + registerCleanupFunction(() => { + XPIInstall.stageLangpacksForAppUpdate = original; + }); + + let stagingCall = PromiseUtils.defer(); + XPIInstall.stageLangpacksForAppUpdate = (appVersion, platformVersion) => { + let result = PromiseUtils.defer(); + stagingCall.resolve({ + appVersion, + platformVersion, + resolve: result.resolve, + reject: result.reject, + }); + + return result.promise; + }; + + return stagingCall.promise; +} + +/** + * Creates and locks the app update write test file so it is possible to test + * when the user doesn't have write access to update. Since this is only + * possible on Windows the function throws when it is called on other platforms. + * This uses registerCleanupFunction to remove the lock and the file when the + * test completes. + * + * @throws If the function is called on a platform other than Windows. + */ +function lockWriteTestFile() { + if (AppConstants.platform != "win") { + throw new Error("Windows only test function called"); + } + let file = getUpdateDirFile(FILE_UPDATE_TEST).QueryInterface( + Ci.nsILocalFileWin + ); + // Remove the file if it exists just in case. + if (file.exists()) { + file.readOnly = false; + file.remove(false); + } + file.create(file.NORMAL_FILE_TYPE, 0o444); + file.readOnly = true; + registerCleanupFunction(() => { + file.readOnly = false; + file.remove(false); + }); +} + +/** + * Closes the update mutex handle in nsUpdateService.js if it exists and then + * creates a new update mutex handle so the update code thinks there is another + * instance of the application handling updates. + * + * @throws If the function is called on a platform other than Windows. + */ +function setOtherInstanceHandlingUpdates() { + if (AppConstants.platform != "win") { + throw new Error("Windows only test function called"); + } + gAUS.observe(null, "test-close-handle-update-mutex", ""); + let handle = createMutex(getPerInstallationMutexName()); + registerCleanupFunction(() => { + closeHandle(handle); + }); +} + +/** + * Gets the update version info for the update url parameters to send to + * app_update.sjs. + * + * @param aAppVersion (optional) + * The application version for the update snippet. If not specified the + * current application version will be used. + * @return The url parameters for the application and platform version to send + * to app_update.sjs. + */ +function getVersionParams(aAppVersion) { + let appInfo = Services.appinfo; + return "&appVersion=" + (aAppVersion ? aAppVersion : appInfo.version); +} + +/** + * Prevent nsIUpdateTimerManager from notifying nsIApplicationUpdateService + * to check for updates by setting the app update last update time to the + * current time minus one minute in seconds and the interval time to 12 hours + * in seconds. + */ +function setUpdateTimerPrefs() { + let now = Math.round(Date.now() / 1000) - 60; + Services.prefs.setIntPref(PREF_APP_UPDATE_LASTUPDATETIME, now); + Services.prefs.setIntPref(PREF_APP_UPDATE_INTERVAL, 43200); +} + +/* + * Sets the value of the App Auto Update setting and sets it back to the + * original value at the start of the test when the test finishes. + * + * @param enabled + * The value to set App Auto Update to. + */ +async function setAppUpdateAutoEnabledHelper(enabled) { + if (gOriginalUpdateAutoValue == null) { + gOriginalUpdateAutoValue = await UpdateUtils.getAppUpdateAutoEnabled(); + registerCleanupFunction(async () => { + await UpdateUtils.setAppUpdateAutoEnabled(gOriginalUpdateAutoValue); + }); + } + await UpdateUtils.setAppUpdateAutoEnabled(enabled); +} + +/** + * Gets the specified button for the notification. + * + * @param win + * The window to get the notification button for. + * @param notificationId + * The ID of the notification to get the button for. + * @param button + * The anonid of the button to get. + * @return The button element. + */ +function getNotificationButton(win, notificationId, button) { + let notification = win.document.getElementById( + `appMenu-${notificationId}-notification` + ); + ok(!notification.hidden, `${notificationId} notification is showing`); + return notification[button]; +} + +/** + * For staging tests the test updater must be used and this restores the backed + * up real updater if it exists and tries again on failure since Windows debug + * builds at times leave the file in use. After success moveRealUpdater is + * called to continue the setup of the test updater. + */ +function setupTestUpdater() { + return (async function() { + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED)) { + try { + restoreUpdaterBackup(); + } catch (e) { + logTestInfo( + "Attempt to restore the backed up updater failed... " + + "will try again, Exception: " + + e + ); + await TestUtils.waitForTick(); + await setupTestUpdater(); + return; + } + await moveRealUpdater(); + } + })(); +} + +/** + * Backs up the real updater and tries again on failure since Windows debug + * builds at times leave the file in use. After success it will call + * copyTestUpdater to continue the setup of the test updater. + */ +function moveRealUpdater() { + return (async function() { + try { + // Move away the real updater + let greBinDir = getGREBinDir(); + let updater = greBinDir.clone(); + updater.append(FILE_UPDATER_BIN); + updater.moveTo(greBinDir, FILE_UPDATER_BIN_BAK); + + let greDir = getGREDir(); + let updateSettingsIni = greDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); + if (updateSettingsIni.exists()) { + updateSettingsIni.moveTo(greDir, FILE_UPDATE_SETTINGS_INI_BAK); + } + + let precomplete = greDir.clone(); + precomplete.append(FILE_PRECOMPLETE); + if (precomplete.exists()) { + precomplete.moveTo(greDir, FILE_PRECOMPLETE_BAK); + } + } catch (e) { + logTestInfo( + "Attempt to move the real updater out of the way failed... " + + "will try again, Exception: " + + e + ); + await TestUtils.waitForTick(); + await moveRealUpdater(); + return; + } + + await copyTestUpdater(); + })(); +} + +/** + * Copies the test updater and tries again on failure since Windows debug builds + * at times leave the file in use. + */ +function copyTestUpdater(attempt = 0) { + return (async function() { + try { + // Copy the test updater + let greBinDir = getGREBinDir(); + let testUpdaterDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + let relPath = REL_PATH_DATA; + let pathParts = relPath.split("/"); + for (let i = 0; i < pathParts.length; ++i) { + testUpdaterDir.append(pathParts[i]); + } + + let testUpdater = testUpdaterDir.clone(); + testUpdater.append(FILE_UPDATER_BIN); + testUpdater.copyToFollowingLinks(greBinDir, FILE_UPDATER_BIN); + + let greDir = getGREDir(); + let updateSettingsIni = greDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); + writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS); + + let precomplete = greDir.clone(); + precomplete.append(FILE_PRECOMPLETE); + writeFile(precomplete, PRECOMPLETE_CONTENTS); + } catch (e) { + if (attempt < MAX_UPDATE_COPY_ATTEMPTS) { + logTestInfo( + "Attempt to copy the test updater failed... " + + "will try again, Exception: " + + e + ); + await TestUtils.waitForTick(); + await copyTestUpdater(attempt++); + } + } + })(); +} + +/** + * Restores the updater and updater related file that if there a backup exists. + * This is called in setupTestUpdater before the backup of the real updater is + * done in case the previous test failed to restore the file when a test has + * finished. This is also called in finishTestRestoreUpdaterBackup to restore + * the files when a test finishes. + */ +function restoreUpdaterBackup() { + let greBinDir = getGREBinDir(); + let updater = greBinDir.clone(); + let updaterBackup = greBinDir.clone(); + updater.append(FILE_UPDATER_BIN); + updaterBackup.append(FILE_UPDATER_BIN_BAK); + if (updaterBackup.exists()) { + if (updater.exists()) { + updater.remove(true); + } + updaterBackup.moveTo(greBinDir, FILE_UPDATER_BIN); + } + + let greDir = getGREDir(); + let updateSettingsIniBackup = greDir.clone(); + updateSettingsIniBackup.append(FILE_UPDATE_SETTINGS_INI_BAK); + if (updateSettingsIniBackup.exists()) { + let updateSettingsIni = greDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); + if (updateSettingsIni.exists()) { + updateSettingsIni.remove(false); + } + updateSettingsIniBackup.moveTo(greDir, FILE_UPDATE_SETTINGS_INI); + } + + let precomplete = greDir.clone(); + let precompleteBackup = greDir.clone(); + precomplete.append(FILE_PRECOMPLETE); + precompleteBackup.append(FILE_PRECOMPLETE_BAK); + if (precompleteBackup.exists()) { + if (precomplete.exists()) { + precomplete.remove(false); + } + precompleteBackup.moveTo(greDir, FILE_PRECOMPLETE); + } else if (precomplete.exists()) { + if (readFile(precomplete) == PRECOMPLETE_CONTENTS) { + precomplete.remove(false); + } + } +} + +/** + * When a test finishes this will repeatedly attempt to restore the real updater + * and the other files for the updater if a backup of the file exists. + */ +function finishTestRestoreUpdaterBackup() { + return (async function() { + try { + // Windows debug builds keep the updater file in use for a short period of + // time after the updater process exits. + restoreUpdaterBackup(); + } catch (e) { + logTestInfo( + "Attempt to restore the backed up updater failed... " + + "will try again, Exception: " + + e + ); + + await TestUtils.waitForTick(); + await finishTestRestoreUpdaterBackup(); + } + })(); +} + +/** + * Waits for the About Dialog to load. + * + * @return A promise that returns the domWindow for the About Dialog and + * resolves when the About Dialog loads. + */ +function waitForAboutDialog() { + return new Promise(resolve => { + var listener = { + onOpenWindow: aXULWindow => { + debugDump("About dialog shown..."); + Services.wm.removeListener(listener); + + async function aboutDialogOnLoad() { + domwindow.removeEventListener("load", aboutDialogOnLoad, true); + let chromeURI = "chrome://browser/content/aboutDialog.xhtml"; + is( + domwindow.document.location.href, + chromeURI, + "About dialog appeared" + ); + resolve(domwindow); + } + + var domwindow = aXULWindow.docShell.domWindow; + domwindow.addEventListener("load", aboutDialogOnLoad, true); + }, + onCloseWindow: aXULWindow => {}, + }; + + Services.wm.addListener(listener); + openAboutDialog(); + }); +} + +/** + * Return the first UpdatePatch with the given type. + * + * @param type + * The type of the patch ("complete" or "partial") + * @param update + * The nsIUpdate to select a patch from. + * @return A nsIUpdatePatch object matching the type specified + */ +function getPatchOfType(type, update) { + if (update) { + for (let i = 0; i < update.patchCount; ++i) { + let patch = update.getPatchAt(i); + if (patch && patch.type == type) { + return patch; + } + } + } + return null; +} + +/** + * Runs a Doorhanger update test. This will set various common prefs for + * updating and runs the provided list of steps. + * + * @param params + * An object containing parameters used to run the test. + * @param steps + * An array of test steps to perform. A step will either be an object + * containing expected conditions and actions or a function to call. + * @return A promise which will resolve once all of the steps have been run. + */ +function runDoorhangerUpdateTest(params, steps) { + function processDoorhangerStep(step) { + if (typeof step == "function") { + return step(); + } + + const { + notificationId, + button, + checkActiveUpdate, + pageURLs, + expectedStateOverride, + } = step; + return (async function() { + if (!params.popupShown && !PanelUI.isNotificationPanelOpen) { + await BrowserTestUtils.waitForEvent( + PanelUI.notificationPanel, + "popupshown" + ); + } + const shownNotificationId = AppMenuNotifications.activeNotification.id; + is( + shownNotificationId, + notificationId, + "The right notification showed up." + ); + + let expectedState = Ci.nsIApplicationUpdateService.STATE_IDLE; + if (expectedStateOverride) { + expectedState = expectedStateOverride; + } else if (notificationId == "update-restart") { + expectedState = Ci.nsIApplicationUpdateService.STATE_PENDING; + } + let actualState = gAUS.currentState; + is( + actualState, + expectedState, + `The current update state should be ` + + `"${gAUS.getStateName(expectedState)}". Actual: ` + + `"${gAUS.getStateName(actualState)}"` + ); + + if (checkActiveUpdate) { + let activeUpdate = + checkActiveUpdate.state == STATE_DOWNLOADING + ? gUpdateManager.downloadingUpdate + : gUpdateManager.readyUpdate; + ok(!!activeUpdate, "There should be an active update"); + is( + activeUpdate.state, + checkActiveUpdate.state, + `The active update state should equal ${checkActiveUpdate.state}` + ); + } else { + ok( + !gUpdateManager.downloadingUpdate, + "There should not be a downloading update" + ); + ok(!gUpdateManager.readyUpdate, "There should not be a ready update"); + } + + let buttonEl = getNotificationButton(window, notificationId, button); + buttonEl.click(); + + if (pageURLs && pageURLs.manual !== undefined) { + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + is( + gBrowser.selectedBrowser.currentURI.spec, + pageURLs.manual, + `The page's url should equal ${pageURLs.manual}` + ); + gBrowser.removeTab(gBrowser.selectedTab); + } + })(); + } + + return (async function() { + if (params.slowStaging) { + Services.env.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1"); + } else { + Services.env.set("MOZ_TEST_SKIP_UPDATE_STAGE", "1"); + } + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_APP_UPDATE_DISABLEDFORTESTING, false], + [PREF_APP_UPDATE_URL_DETAILS, gDetailsURL], + [PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE], + ], + }); + + await setupTestUpdater(); + + let baseURL = URL_HTTP_UPDATE_SJS; + if (params.baseURL) { + baseURL = params.baseURL; + } + let queryString = params.queryString ? params.queryString : ""; + let updateURL = + baseURL + + "?detailsURL=" + + gDetailsURL + + queryString + + getVersionParams(params.version); + setUpdateURL(updateURL); + if (params.checkAttempts) { + // Perform a background check doorhanger test. + executeSoon(() => { + (async function() { + gAUS.checkForBackgroundUpdates(); + for (var i = 0; i < params.checkAttempts - 1; i++) { + await waitForEvent("update-error", "check-attempt-failed"); + gAUS.checkForBackgroundUpdates(); + } + })(); + }); + } else { + // Perform a startup processing doorhanger test. + writeStatusFile(STATE_FAILED_CRC_ERROR); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(params.updates), true); + reloadUpdateManagerData(); + testPostUpdateProcessing(); + } + + for (let step of steps) { + await processDoorhangerStep(step); + } + })(); +} + +/** + * Runs an About Dialog update test. This will set various common prefs for + * updating and runs the provided list of steps. + * + * @param params + * An object containing parameters used to run the test. + * @param steps + * An array of test steps to perform. A step will either be an object + * containing expected conditions and actions or a function to call. + * @return A promise which will resolve once all of the steps have been run. + */ +function runAboutDialogUpdateTest(params, steps) { + let aboutDialog; + function processAboutDialogStep(step) { + if (typeof step == "function") { + return step(aboutDialog); + } + + const { + panelId, + checkActiveUpdate, + continueFile, + downloadInfo, + forceApply, + noContinue, + expectedStateOverride, + } = step; + return (async function() { + await TestUtils.waitForCondition( + () => + aboutDialog.gAppUpdater && + aboutDialog.gAppUpdater.selectedPanel?.id == panelId, + "Waiting for the expected panel ID: " + panelId, + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test so the panel + // ID and the expected panel ID is printed in the log. + logTestInfo(e); + }); + let { selectedPanel } = aboutDialog.gAppUpdater; + is(selectedPanel.id, panelId, "The panel ID should equal " + panelId); + ok( + BrowserTestUtils.is_visible(selectedPanel), + "The panel should be visible" + ); + + if ( + panelId == "downloading" && + gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_IDLE + ) { + // Now that `AUS.downloadUpdate` is async, we start showing the + // downloading panel while `AUS.downloadUpdate` is still resolving. + // But the below checks assume that this resolution has already + // happened. So we need to wait for things to actually resolve. + debugDump("Waiting for downloading state to actually start"); + await gAUS.stateTransition; + + // Check that the checks that we made above are still valid. + selectedPanel = aboutDialog.gAppUpdater.selectedPanel; + is(selectedPanel.id, panelId, "The panel ID should equal " + panelId); + ok( + BrowserTestUtils.is_visible(selectedPanel), + "The panel should be visible" + ); + } + + let expectedState = Ci.nsIApplicationUpdateService.STATE_IDLE; + if (expectedStateOverride) { + expectedState = expectedStateOverride; + } else if (panelId == "apply") { + expectedState = Ci.nsIApplicationUpdateService.STATE_PENDING; + } else if (panelId == "downloading") { + expectedState = Ci.nsIApplicationUpdateService.STATE_DOWNLOADING; + } else if (panelId == "applying") { + expectedState = Ci.nsIApplicationUpdateService.STATE_STAGING; + } + let actualState = gAUS.currentState; + is( + actualState, + expectedState, + `The current update state should be ` + + `"${gAUS.getStateName(expectedState)}". Actual: ` + + `"${gAUS.getStateName(actualState)}"` + ); + + if (checkActiveUpdate) { + let activeUpdate = + checkActiveUpdate.state == STATE_DOWNLOADING + ? gUpdateManager.downloadingUpdate + : gUpdateManager.readyUpdate; + ok(!!activeUpdate, "There should be an active update"); + is( + activeUpdate.state, + checkActiveUpdate.state, + "The active update state should equal " + checkActiveUpdate.state + ); + } else { + ok( + !gUpdateManager.downloadingUpdate, + "There should not be a downloading update" + ); + ok(!gUpdateManager.readyUpdate, "There should not be a ready update"); + } + + // Some tests just want to stop at the downloading state. These won't + // include a continue file in that state. + if (panelId == "downloading" && continueFile) { + for (let i = 0; i < downloadInfo.length; ++i) { + let data = downloadInfo[i]; + await continueFileHandler(continueFile); + let patch = getPatchOfType( + data.patchType, + gUpdateManager.downloadingUpdate + ); + // The update is removed early when the last download fails so check + // that there is a patch before proceeding. + let isLastPatch = i == downloadInfo.length - 1; + if (!isLastPatch || patch) { + let resultName = data.bitsResult ? "bitsResult" : "internalResult"; + patch.QueryInterface(Ci.nsIWritablePropertyBag); + await TestUtils.waitForCondition( + () => patch.getProperty(resultName) == data[resultName], + "Waiting for expected patch property " + + resultName + + " value: " + + data[resultName], + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test so the + // property value and the expected property value is printed in + // the log. + logTestInfo(e); + }); + is( + "" + patch.getProperty(resultName), + data[resultName], + "The patch property " + + resultName + + " value should equal " + + data[resultName] + ); + + // Check the download status text. It should be something like, + // "1.4 of 1.4 KB". + let expectedText = DownloadUtils.getTransferTotal( + data[resultName] == gBadSizeResult ? 0 : patch.size, + patch.size + ); + Assert.ok( + expectedText, + "Sanity check: Expected download status text should be non-empty" + ); + Assert.equal( + aboutDialog.document.getElementById("downloadStatus").textContent, + expectedText, + "Download status text should be correct" + ); + } + } + } else if (continueFile) { + await continueFileHandler(continueFile); + } + + let linkPanels = ["downloadFailed", "manualUpdate", "unsupportedSystem"]; + if (linkPanels.includes(panelId)) { + // The unsupportedSystem panel uses the update's detailsURL and the + // downloadFailed and manualUpdate panels use the app.update.url.manual + // preference. + let link = selectedPanel.querySelector("label.text-link"); + is( + link.href, + gDetailsURL, + `The panel's link href should equal ${gDetailsURL}` + ); + } + + // Automatically click the download button unless `noContinue` was passed. + let buttonPanels = ["downloadAndInstall", "apply"]; + if (buttonPanels.includes(panelId) && !noContinue) { + let buttonEl = selectedPanel.querySelector("button"); + await TestUtils.waitForCondition( + () => aboutDialog.document.activeElement == buttonEl, + "The button should receive focus" + ); + ok(!buttonEl.disabled, "The button should be enabled"); + // Don't click the button on the apply panel since this will restart the + // application. + if (panelId != "apply" || forceApply) { + buttonEl.click(); + } + } + })(); + } + + return (async function() { + Services.env.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1"); + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_APP_UPDATE_DISABLEDFORTESTING, false], + [PREF_APP_UPDATE_URL_MANUAL, gDetailsURL], + ], + }); + + await setupTestUpdater(); + + let baseURL = URL_HTTP_UPDATE_SJS; + if (params.baseURL) { + baseURL = params.baseURL; + } + let queryString = params.queryString ? params.queryString : ""; + let updateURL = + baseURL + + "?detailsURL=" + + gDetailsURL + + queryString + + getVersionParams(params.version); + if (params.backgroundUpdate) { + setUpdateURL(updateURL); + gAUS.checkForBackgroundUpdates(); + if (params.continueFile) { + await continueFileHandler(params.continueFile); + } + if (params.waitForUpdateState) { + let whichUpdate = + params.waitForUpdateState == STATE_DOWNLOADING + ? "downloadingUpdate" + : "readyUpdate"; + await TestUtils.waitForCondition( + () => + gUpdateManager[whichUpdate] && + gUpdateManager[whichUpdate].state == params.waitForUpdateState, + "Waiting for update state: " + params.waitForUpdateState, + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test so the panel + // ID and the expected panel ID is printed in the log. + logTestInfo(e); + }); + // Display the UI after the update state equals the expected value. + is( + gUpdateManager[whichUpdate].state, + params.waitForUpdateState, + "The update state value should equal " + params.waitForUpdateState + ); + } + } else { + updateURL += "&slowUpdateCheck=1&useSlowDownloadMar=1"; + setUpdateURL(updateURL); + } + + aboutDialog = await waitForAboutDialog(); + registerCleanupFunction(() => { + aboutDialog.close(); + }); + + for (let step of steps) { + await processAboutDialogStep(step); + } + })(); +} + +/** + * Runs an about:preferences update test. This will set various common prefs for + * updating and runs the provided list of steps. + * + * @param params + * An object containing parameters used to run the test. + * @param steps + * An array of test steps to perform. A step will either be an object + * containing expected conditions and actions or a function to call. + * @return A promise which will resolve once all of the steps have been run. + */ +function runAboutPrefsUpdateTest(params, steps) { + let tab; + function processAboutPrefsStep(step) { + if (typeof step == "function") { + return step(tab); + } + + const { + panelId, + checkActiveUpdate, + continueFile, + downloadInfo, + forceApply, + expectedStateOverride, + } = step; + return (async function() { + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ panelId }], + async ({ panelId }) => { + await ContentTaskUtils.waitForCondition( + () => content.gAppUpdater.selectedPanel?.id == panelId, + "Waiting for the expected panel ID: " + panelId, + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test so the panel + // ID and the expected panel ID is printed in the log. Use info here + // instead of logTestInfo since logTestInfo isn't available in the + // content task. + info(e); + }); + is( + content.gAppUpdater.selectedPanel.id, + panelId, + "The panel ID should equal " + panelId + ); + } + ); + + if ( + panelId == "downloading" && + gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_IDLE + ) { + // Now that `AUS.downloadUpdate` is async, we start showing the + // downloading panel while `AUS.downloadUpdate` is still resolving. + // But the below checks assume that this resolution has already + // happened. So we need to wait for things to actually resolve. + debugDump("Waiting for downloading state to actually start"); + await gAUS.stateTransition; + + // Check that the checks that we made above are still valid. + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ panelId }], + ({ panelId }) => { + is( + content.gAppUpdater.selectedPanel.id, + panelId, + "The panel ID should equal " + panelId + ); + } + ); + } + + let expectedState = Ci.nsIApplicationUpdateService.STATE_IDLE; + if (expectedStateOverride) { + expectedState = expectedStateOverride; + } else if (panelId == "apply") { + expectedState = Ci.nsIApplicationUpdateService.STATE_PENDING; + } else if (panelId == "downloading") { + expectedState = Ci.nsIApplicationUpdateService.STATE_DOWNLOADING; + } else if (panelId == "applying") { + expectedState = Ci.nsIApplicationUpdateService.STATE_STAGING; + } + let actualState = gAUS.currentState; + is( + actualState, + expectedState, + `The current update state should be ` + + `"${gAUS.getStateName(expectedState)}". Actual: ` + + `"${gAUS.getStateName(actualState)}"` + ); + + if (checkActiveUpdate) { + let activeUpdate = + checkActiveUpdate.state == STATE_DOWNLOADING + ? gUpdateManager.downloadingUpdate + : gUpdateManager.readyUpdate; + ok(!!activeUpdate, "There should be an active update"); + is( + activeUpdate.state, + checkActiveUpdate.state, + "The active update state should equal " + checkActiveUpdate.state + ); + } else { + ok( + !gUpdateManager.downloadingUpdate, + "There should not be a downloading update" + ); + ok(!gUpdateManager.readyUpdate, "There should not be a ready update"); + } + + if (panelId == "downloading") { + if (!downloadInfo) { + logTestInfo("no downloadinfo, possible error?"); + } + for (let i = 0; i < downloadInfo.length; ++i) { + let data = downloadInfo[i]; + // The About Dialog tests always specify a continue file. + await continueFileHandler(continueFile); + let patch = getPatchOfType( + data.patchType, + gUpdateManager.downloadingUpdate + ); + // The update is removed early when the last download fails so check + // that there is a patch before proceeding. + let isLastPatch = i == downloadInfo.length - 1; + if (!isLastPatch || patch) { + let resultName = data.bitsResult ? "bitsResult" : "internalResult"; + patch.QueryInterface(Ci.nsIWritablePropertyBag); + await TestUtils.waitForCondition( + () => patch.getProperty(resultName) == data[resultName], + "Waiting for expected patch property " + + resultName + + " value: " + + data[resultName], + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test so the + // property value and the expected property value is printed in + // the log. + logTestInfo(e); + }); + is( + "" + patch.getProperty(resultName), + data[resultName], + "The patch property " + + resultName + + " value should equal " + + data[resultName] + ); + + // Check the download status text. It should be something like, + // "Downloading update — 1.4 of 1.4 KB". We check only the second + // part to make sure that the downloaded size is updated correctly. + let actualText = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => content.document.getElementById("downloading").textContent + ); + let expectedSuffix = DownloadUtils.getTransferTotal( + data[resultName] == gBadSizeResult ? 0 : patch.size, + patch.size + ); + Assert.ok( + expectedSuffix, + "Sanity check: Expected download status text should be non-empty" + ); + Assert.ok( + actualText.endsWith(expectedSuffix), + "Download status text should end as expected: " + + JSON.stringify({ actualText, expectedSuffix }) + ); + } + } + } else if (continueFile) { + await continueFileHandler(continueFile); + } + + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ panelId, gDetailsURL, forceApply }], + async ({ panelId, gDetailsURL, forceApply }) => { + let linkPanels = [ + "downloadFailed", + "manualUpdate", + "unsupportedSystem", + ]; + if (linkPanels.includes(panelId)) { + let { selectedPanel } = content.gAppUpdater; + // The unsupportedSystem panel uses the update's detailsURL and the + // downloadFailed and manualUpdate panels use the app.update.url.manual + // preference. + let selector = "label.text-link"; + // The downloadFailed panel in about:preferences uses an anchor + // instead of a label for the link. + if (selectedPanel.id == "downloadFailed") { + selector = "a.text-link"; + } + let link = selectedPanel.querySelector(selector); + is( + link.href, + gDetailsURL, + `The panel's link href should equal ${gDetailsURL}` + ); + } + + let buttonPanels = ["downloadAndInstall", "apply"]; + if (buttonPanels.includes(panelId)) { + let { selectedPanel } = content.gAppUpdater; + let buttonEl = selectedPanel.querySelector("button"); + // Note: The about:preferences doesn't focus the button like the + // About Dialog does. + ok(!buttonEl.disabled, "The button should be enabled"); + // Don't click the button on the apply panel since this will restart + // the application. + if (selectedPanel.id != "apply" || forceApply) { + buttonEl.click(); + } + } + } + ); + })(); + } + + return (async function() { + Services.env.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1"); + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_APP_UPDATE_DISABLEDFORTESTING, false], + [PREF_APP_UPDATE_URL_MANUAL, gDetailsURL], + ], + }); + + await setupTestUpdater(); + + let baseURL = URL_HTTP_UPDATE_SJS; + if (params.baseURL) { + baseURL = params.baseURL; + } + let queryString = params.queryString ? params.queryString : ""; + let updateURL = + baseURL + + "?detailsURL=" + + gDetailsURL + + queryString + + getVersionParams(params.version); + if (params.backgroundUpdate) { + setUpdateURL(updateURL); + gAUS.checkForBackgroundUpdates(); + if (params.continueFile) { + await continueFileHandler(params.continueFile); + } + if (params.waitForUpdateState) { + // Wait until the update state equals the expected value before + // displaying the UI. + let whichUpdate = + params.waitForUpdateState == STATE_DOWNLOADING + ? "downloadingUpdate" + : "readyUpdate"; + await TestUtils.waitForCondition( + () => + gUpdateManager[whichUpdate] && + gUpdateManager[whichUpdate].state == params.waitForUpdateState, + "Waiting for update state: " + params.waitForUpdateState, + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test so the panel + // ID and the expected panel ID is printed in the log. + logTestInfo(e); + }); + is( + gUpdateManager[whichUpdate].state, + params.waitForUpdateState, + "The update state value should equal " + params.waitForUpdateState + ); + } + } else { + updateURL += "&slowUpdateCheck=1&useSlowDownloadMar=1"; + setUpdateURL(updateURL); + } + + tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences" + ); + registerCleanupFunction(async () => { + await BrowserTestUtils.removeTab(tab); + }); + + // Scroll the UI into view so it is easier to troubleshoot tests. + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + content.document.getElementById("updatesCategory").scrollIntoView(); + }); + + for (let step of steps) { + await processAboutPrefsStep(step); + } + })(); +} + +/** + * Removes the modified update-settings.ini file so the updater will fail to + * stage an update. + */ +function removeUpdateSettingsIni() { + if (Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED)) { + let greDir = getGREDir(); + let updateSettingsIniBak = greDir.clone(); + updateSettingsIniBak.append(FILE_UPDATE_SETTINGS_INI_BAK); + if (updateSettingsIniBak.exists()) { + let updateSettingsIni = greDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); + updateSettingsIni.remove(false); + } + } +} + +/** + * Runs a telemetry update test. This will set various common prefs for + * updating, checks for an update, and waits for the specified observer + * notification. + * + * @param updateParams + * Params which will be sent to app_update.sjs. + * @param event + * The observer notification to wait for before proceeding. + * @param stageFailure (optional) + * Whether to force a staging failure by removing the modified + * update-settings.ini file. + * @return A promise which will resolve after the . + */ +function runTelemetryUpdateTest(updateParams, event, stageFailure = false) { + return (async function() { + Services.telemetry.clearScalars(); + Services.env.set("MOZ_TEST_SKIP_UPDATE_STAGE", "1"); + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_DISABLEDFORTESTING, false]], + }); + + await setupTestUpdater(); + + if (stageFailure) { + removeUpdateSettingsIni(); + } + + let updateURL = + URL_HTTP_UPDATE_SJS + + "?detailsURL=" + + gDetailsURL + + updateParams + + getVersionParams(); + setUpdateURL(updateURL); + gAUS.checkForBackgroundUpdates(); + await waitForEvent(event); + })(); +} diff --git a/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser.ini b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser.ini new file mode 100644 index 0000000000..949ad255c9 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser.ini @@ -0,0 +1,18 @@ +[DEFAULT] +head = head.js +prefs = + app.update.BITS.enabled=false + browser.policies.alternatePath='<test-root>/toolkit/mozapps/update/tests/browser/manual_app_update_only/config_manual_app_update_only.json' +support-files = + !/toolkit/mozapps/update/tests/browser/head.js + config_manual_app_update_only.json + ../../data/shared.js + ../../data/app_update.sjs + ../testConstants.js +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds + +[browser_aboutPrefs_fc_autoUpdateTrue.js] +[browser_aboutPrefs_fc_autoUpdateFalse.js] +[browser_aboutDialog_fc_autoUpdateTrue.js] +[browser_aboutDialog_fc_autoUpdateFalse.js] +[browser_noBackgroundUpdate.js] diff --git a/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutDialog_fc_autoUpdateFalse.js b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutDialog_fc_autoUpdateFalse.js new file mode 100644 index 0000000000..169e66033a --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutDialog_fc_autoUpdateFalse.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_manual_app_update_policy() { + await setAppUpdateAutoEnabledHelper(false); + + is( + Services.policies.isAllowed("autoAppUpdateChecking"), + false, + "autoAppUpdateChecking should be disabled by policy" + ); + is(gAUS.manualUpdateOnly, true, "gAUS.manualUpdateOnly should be true"); + + let downloadInfo = [{ patchType: "partial", internalResult: "0" }]; + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + checkActiveUpdate: null, + continueFile: null, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutDialog_fc_autoUpdateTrue.js b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutDialog_fc_autoUpdateTrue.js new file mode 100644 index 0000000000..0a59f59d71 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutDialog_fc_autoUpdateTrue.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_manual_app_update_policy() { + await setAppUpdateAutoEnabledHelper(true); + + is( + Services.policies.isAllowed("autoAppUpdateChecking"), + false, + "autoAppUpdateChecking should be disabled by policy" + ); + is(gAUS.manualUpdateOnly, true, "gAUS.manualUpdateOnly should be true"); + + let downloadInfo = [{ patchType: "partial", internalResult: "0" }]; + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutDialogUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + checkActiveUpdate: null, + continueFile: null, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutPrefs_fc_autoUpdateFalse.js b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutPrefs_fc_autoUpdateFalse.js new file mode 100644 index 0000000000..97da951189 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutPrefs_fc_autoUpdateFalse.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_manual_app_update_policy() { + await setAppUpdateAutoEnabledHelper(false); + + is( + Services.policies.isAllowed("autoAppUpdateChecking"), + false, + "autoAppUpdateChecking should be disabled by policy" + ); + is(gAUS.manualUpdateOnly, true, "gAUS.manualUpdateOnly should be true"); + + let downloadInfo = [{ patchType: "partial", internalResult: "0" }]; + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + checkActiveUpdate: null, + continueFile: null, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + async tab => { + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let setting = content.document.getElementById( + "updateSettingsContainer" + ); + is( + setting.hidden, + true, + "Update choices should be disabled when manualUpdateOnly" + ); + }); + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutPrefs_fc_autoUpdateTrue.js b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutPrefs_fc_autoUpdateTrue.js new file mode 100644 index 0000000000..f716357b0f --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutPrefs_fc_autoUpdateTrue.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_manual_app_update_policy() { + await setAppUpdateAutoEnabledHelper(true); + + is( + Services.policies.isAllowed("autoAppUpdateChecking"), + false, + "autoAppUpdateChecking should be disabled by policy" + ); + is(gAUS.manualUpdateOnly, true, "gAUS.manualUpdateOnly should be true"); + + let downloadInfo = [{ patchType: "partial", internalResult: "0" }]; + // Since the partial should be successful specify an invalid size for the + // complete update. + let params = { queryString: "&invalidCompleteSize=1" }; + await runAboutPrefsUpdateTest(params, [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + checkActiveUpdate: null, + continueFile: null, + }, + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, + async tab => { + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let setting = content.document.getElementById( + "updateSettingsContainer" + ); + is( + setting.hidden, + true, + "Update choices should be disabled when manualUpdateOnly" + ); + }); + }, + ]); +}); diff --git a/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_noBackgroundUpdate.js b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_noBackgroundUpdate.js new file mode 100644 index 0000000000..38b27e31ad --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_noBackgroundUpdate.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_manual_app_update_policy() { + // Unfortunately, we can't really test the other background update entry + // point, gAUS.notify, because it doesn't return anything and it would be + // a bit overkill to edit the nsITimerCallback interface just for this test. + // But the two entry points just immediately call the same function, so this + // should probably be alright. + is( + gAUS.checkForBackgroundUpdates(), + false, + "gAUS.checkForBackgroundUpdates() should not proceed with update check" + ); +}); diff --git a/toolkit/mozapps/update/tests/browser/manual_app_update_only/config_manual_app_update_only.json b/toolkit/mozapps/update/tests/browser/manual_app_update_only/config_manual_app_update_only.json new file mode 100644 index 0000000000..4e7c785bc1 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/manual_app_update_only/config_manual_app_update_only.json @@ -0,0 +1,5 @@ +{ + "policies": { + "ManualAppUpdateOnly": true + } +} diff --git a/toolkit/mozapps/update/tests/browser/manual_app_update_only/head.js b/toolkit/mozapps/update/tests/browser/manual_app_update_only/head.js new file mode 100644 index 0000000000..a1967394d6 --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/manual_app_update_only/head.js @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/head.js", + this +); diff --git a/toolkit/mozapps/update/tests/browser/testConstants.js b/toolkit/mozapps/update/tests/browser/testConstants.js new file mode 100644 index 0000000000..915391054b --- /dev/null +++ b/toolkit/mozapps/update/tests/browser/testConstants.js @@ -0,0 +1,7 @@ +const REL_PATH_DATA = "browser/toolkit/mozapps/update/tests/browser/"; +const URL_HOST = "http://127.0.0.1:8888"; +const URL_PATH_UPDATE_XML = "/" + REL_PATH_DATA + "app_update.sjs"; +const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML; +const CONTINUE_CHECK = "continueCheck"; +const CONTINUE_DOWNLOAD = "continueDownload"; +const CONTINUE_STAGING = "continueStaging"; diff --git a/toolkit/mozapps/update/tests/data/app_update.sjs b/toolkit/mozapps/update/tests/data/app_update.sjs new file mode 100644 index 0000000000..610778fe8b --- /dev/null +++ b/toolkit/mozapps/update/tests/data/app_update.sjs @@ -0,0 +1,251 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +/** + * Server side http server script for application update tests. + */ + +// Definitions from test and other files used by the tests +/* global getState */ + +function getTestDataFile(aFilename) { + let file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + let pathParts = REL_PATH_DATA.split("/"); + for (let i = 0; i < pathParts.length; ++i) { + file.append(pathParts[i]); + } + if (aFilename) { + file.append(aFilename); + } + return file; +} + +function loadHelperScript(aScriptFile) { + let scriptSpec = Services.io.newFileURI(aScriptFile).spec; + Services.scriptloader.loadSubScript(scriptSpec, this); +} + +var scriptFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); +scriptFile.initWithPath(getState("__LOCATION__")); +scriptFile = scriptFile.parent; +/* import-globals-from ../browser/testConstants.js */ +scriptFile.append("testConstants.js"); +loadHelperScript(scriptFile); + +/* import-globals-from sharedUpdateXML.js */ +scriptFile = getTestDataFile("sharedUpdateXML.js"); +loadHelperScript(scriptFile); + +const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR; +const BAD_SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + "not_here.mar"; + +// A value of 10 caused the tests to intermittently fail on Mac OS X so be +// careful when changing this value. +const SLOW_RESPONSE_INTERVAL = 100; +const MAX_SLOW_RESPONSE_RETRIES = 200; +var gSlowDownloadTimer; +var gSlowCheckTimer; + +function handleRequest(aRequest, aResponse) { + let params = {}; + if (aRequest.queryString) { + params = parseQueryString(aRequest.queryString); + } + + let statusCode = params.statusCode ? parseInt(params.statusCode) : 200; + let statusReason = params.statusReason ? params.statusReason : "OK"; + aResponse.setStatusLine(aRequest.httpVersion, statusCode, statusReason); + aResponse.setHeader("Cache-Control", "no-cache", false); + + // When a mar download is started by the update service it can finish + // downloading before the ui has loaded. By specifying a serviceURL for the + // update patch that points to this file and has a slowDownloadMar param the + // mar will be downloaded asynchronously which will allow the ui to load + // before the download completes. + if (params.slowDownloadMar) { + aResponse.processAsync(); + aResponse.setHeader("Content-Type", "binary/octet-stream"); + aResponse.setHeader("Content-Length", SIZE_SIMPLE_MAR); + + // BITS will first make a HEAD request followed by a GET request. + if (aRequest.method == "HEAD") { + aResponse.finish(); + return; + } + + let retries = 0; + gSlowDownloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gSlowDownloadTimer.initWithCallback( + function(aTimer) { + let continueFile = getTestDataFile(CONTINUE_DOWNLOAD); + retries++; + if (continueFile.exists() || retries == MAX_SLOW_RESPONSE_RETRIES) { + try { + // If the continue file is in use try again the next time the timer + // fires unless the retries has reached the value defined by + // MAX_SLOW_RESPONSE_RETRIES in which case let the test remove the + // continue file. + if (retries < MAX_SLOW_RESPONSE_RETRIES) { + continueFile.remove(false); + } + gSlowDownloadTimer.cancel(); + aResponse.write(readFileBytes(getTestDataFile(FILE_SIMPLE_MAR))); + aResponse.finish(); + } catch (e) {} + } + }, + SLOW_RESPONSE_INTERVAL, + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + return; + } + + if (params.uiURL) { + aResponse.write( + '<html><head><meta http-equiv="content-type" content=' + + '"text/html; charset=utf-8"></head><body>' + + params.uiURL + + "<br><br>this is a test mar that will not " + + "affect your build.</body></html>" + ); + return; + } + + if (params.xmlMalformed) { + respond(aResponse, params, "xml error"); + return; + } + + if (params.noUpdates) { + respond(aResponse, params, getRemoteUpdatesXMLString("")); + return; + } + + if (params.unsupported) { + let detailsURL = params.detailsURL ? params.detailsURL : URL_HOST; + let unsupportedXML = getRemoteUpdatesXMLString( + ' <update type="major" ' + + 'unsupported="true" ' + + 'detailsURL="' + + detailsURL + + '"></update>\n' + ); + respond(aResponse, params, unsupportedXML); + return; + } + + let size; + let patches = ""; + let url = ""; + if (params.useSlowDownloadMar) { + url = URL_HTTP_UPDATE_SJS + "?slowDownloadMar=1"; + } else { + url = params.badURL ? BAD_SERVICE_URL : SERVICE_URL; + } + if (!params.partialPatchOnly) { + size = SIZE_SIMPLE_MAR + (params.invalidCompleteSize ? "1" : ""); + let patchProps = { type: "complete", url, size }; + patches += getRemotePatchString(patchProps); + } + + if (!params.completePatchOnly) { + size = SIZE_SIMPLE_MAR + (params.invalidPartialSize ? "1" : ""); + let patchProps = { type: "partial", url, size }; + patches += getRemotePatchString(patchProps); + } + + let updateProps = {}; + if (params.type) { + updateProps.type = params.type; + } + + if (params.name) { + updateProps.name = params.name; + } + + if (params.appVersion) { + updateProps.appVersion = params.appVersion; + } + + if (params.displayVersion) { + updateProps.displayVersion = params.displayVersion; + } + + if (params.buildID) { + updateProps.buildID = params.buildID; + } + + if (params.promptWaitTime) { + updateProps.promptWaitTime = params.promptWaitTime; + } + + if (params.disableBITS) { + updateProps.disableBITS = params.disableBITS; + } + + let updates = getRemoteUpdateString(updateProps, patches); + let xml = getRemoteUpdatesXMLString(updates); + respond(aResponse, params, xml); +} + +function respond(aResponse, aParams, aResponseString) { + if (aParams.slowUpdateCheck) { + let retries = 0; + aResponse.processAsync(); + gSlowCheckTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gSlowCheckTimer.initWithCallback( + function(aTimer) { + retries++; + let continueFile = getTestDataFile(CONTINUE_CHECK); + if (continueFile.exists() || retries == MAX_SLOW_RESPONSE_RETRIES) { + try { + // If the continue file is in use try again the next time the timer + // fires unless the retries has reached the value defined by + // MAX_SLOW_RESPONSE_RETRIES in which case let the test remove the + // continue file. + if (retries < MAX_SLOW_RESPONSE_RETRIES) { + continueFile.remove(false); + } + gSlowCheckTimer.cancel(); + aResponse.write(aResponseString); + aResponse.finish(); + } catch (e) {} + } + }, + SLOW_RESPONSE_INTERVAL, + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + } else { + aResponse.write(aResponseString); + } +} + +/** + * Helper function to create a JS object representing the url parameters from + * the request's queryString. + * + * @param aQueryString + * The request's query string. + * @return A JS object representing the url parameters from the request's + * queryString. + */ +function parseQueryString(aQueryString) { + let paramArray = aQueryString.split("&"); + let regex = /^([^=]+)=(.*)$/; + let params = {}; + for (let i = 0, sz = paramArray.length; i < sz; i++) { + let match = regex.exec(paramArray[i]); + if (!match) { + throw Components.Exception( + "Bad parameter in queryString! '" + paramArray[i] + "'", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); + } + + return params; +} diff --git a/toolkit/mozapps/update/tests/data/complete.exe b/toolkit/mozapps/update/tests/data/complete.exe Binary files differnew file mode 100644 index 0000000000..da9cdf0cc0 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete.exe diff --git a/toolkit/mozapps/update/tests/data/complete.mar b/toolkit/mozapps/update/tests/data/complete.mar Binary files differnew file mode 100644 index 0000000000..375fd7bd08 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete.mar diff --git a/toolkit/mozapps/update/tests/data/complete.png b/toolkit/mozapps/update/tests/data/complete.png Binary files differnew file mode 100644 index 0000000000..2990a539ff --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete.png diff --git a/toolkit/mozapps/update/tests/data/complete_log_success_mac b/toolkit/mozapps/update/tests/data/complete_log_success_mac new file mode 100644 index 0000000000..4f992a1374 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_log_success_mac @@ -0,0 +1,332 @@ +UPDATE TYPE complete +PREPARE REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0 +PREPARE REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png +PREPARE REMOVEFILE Contents/Resources/removed-files +PREPARE REMOVEFILE Contents/Resources/precomplete +PREPARE REMOVEFILE Contents/Resources/2/20/20text0 +PREPARE REMOVEFILE Contents/Resources/2/20/20png0.png +PREPARE REMOVEFILE Contents/Resources/0/0exe0.exe +PREPARE REMOVEFILE Contents/Resources/0/00/00text0 +PREPARE REMOVEFILE Contents/MacOS/exe0.exe +PREPARE REMOVEDIR Contents/Resources/searchplugins/ +PREPARE REMOVEDIR Contents/Resources/defaults/pref/ +PREPARE REMOVEDIR Contents/Resources/defaults/ +PREPARE REMOVEDIR Contents/Resources/2/20/ +PREPARE REMOVEDIR Contents/Resources/2/ +PREPARE REMOVEDIR Contents/Resources/0/00/ +PREPARE REMOVEDIR Contents/Resources/0/ +PREPARE REMOVEDIR Contents/Resources/ +PREPARE REMOVEDIR Contents/MacOS/ +PREPARE REMOVEDIR Contents/ +PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0 +PREPARE ADD Contents/Resources/searchplugins/searchpluginspng1.png +PREPARE ADD Contents/Resources/searchplugins/searchpluginspng0.png +PREPARE ADD Contents/Resources/removed-files +PREPARE ADD Contents/Resources/precomplete +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +PREPARE ADD Contents/Resources/1/10/10text0 +PREPARE ADD Contents/Resources/0/0exe0.exe +PREPARE ADD Contents/Resources/0/00/00text1 +PREPARE ADD Contents/Resources/0/00/00text0 +PREPARE ADD Contents/Resources/0/00/00png0.png +PREPARE ADD Contents/MacOS/exe0.exe +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/98/ +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/970/ +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/971/ +PREPARE REMOVEDIR Contents/Resources/9/97/ +PREPARE REMOVEFILE Contents/Resources/9/96/96text0 +PREPARE REMOVEFILE Contents/Resources/9/96/96text1 +PREPARE REMOVEDIR Contents/Resources/9/96/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/93/ +PREPARE REMOVEDIR Contents/Resources/9/92/ +PREPARE REMOVEDIR Contents/Resources/9/91/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/88/ +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/870/ +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/871/ +PREPARE REMOVEDIR Contents/Resources/8/87/ +PREPARE REMOVEFILE Contents/Resources/8/86/86text0 +PREPARE REMOVEFILE Contents/Resources/8/86/86text1 +PREPARE REMOVEDIR Contents/Resources/8/86/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/83/ +PREPARE REMOVEDIR Contents/Resources/8/82/ +PREPARE REMOVEDIR Contents/Resources/8/81/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/70/ +PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/71/ +PREPARE REMOVEFILE Contents/Resources/7/7text0 +PREPARE REMOVEFILE Contents/Resources/7/7text1 +PREPARE REMOVEDIR Contents/Resources/7/ +PREPARE REMOVEDIR Contents/Resources/6/ +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5test.exe +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEDIR Contents/Resources/5/ +PREPARE REMOVEFILE Contents/Resources/4/4text1 +PREPARE REMOVEFILE Contents/Resources/4/4text0 +PREPARE REMOVEDIR Contents/Resources/4/ +PREPARE REMOVEFILE Contents/Resources/3/3text1 +PREPARE REMOVEFILE Contents/Resources/3/3text0 +EXECUTE REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0 +EXECUTE REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png +EXECUTE REMOVEFILE Contents/Resources/removed-files +EXECUTE REMOVEFILE Contents/Resources/precomplete +EXECUTE REMOVEFILE Contents/Resources/2/20/20text0 +EXECUTE REMOVEFILE Contents/Resources/2/20/20png0.png +EXECUTE REMOVEFILE Contents/Resources/0/0exe0.exe +EXECUTE REMOVEFILE Contents/Resources/0/00/00text0 +EXECUTE REMOVEFILE Contents/MacOS/exe0.exe +EXECUTE REMOVEDIR Contents/Resources/searchplugins/ +EXECUTE REMOVEDIR Contents/Resources/defaults/pref/ +EXECUTE REMOVEDIR Contents/Resources/defaults/ +EXECUTE REMOVEDIR Contents/Resources/2/20/ +EXECUTE REMOVEDIR Contents/Resources/2/ +EXECUTE REMOVEDIR Contents/Resources/0/00/ +EXECUTE REMOVEDIR Contents/Resources/0/ +EXECUTE REMOVEDIR Contents/Resources/ +EXECUTE REMOVEDIR Contents/MacOS/ +EXECUTE REMOVEDIR Contents/ +EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0 +EXECUTE ADD Contents/Resources/searchplugins/searchpluginspng1.png +EXECUTE ADD Contents/Resources/searchplugins/searchpluginspng0.png +EXECUTE ADD Contents/Resources/removed-files +EXECUTE ADD Contents/Resources/precomplete +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +EXECUTE ADD Contents/Resources/1/10/10text0 +EXECUTE ADD Contents/Resources/0/0exe0.exe +EXECUTE ADD Contents/Resources/0/00/00text1 +EXECUTE ADD Contents/Resources/0/00/00text0 +EXECUTE ADD Contents/Resources/0/00/00png0.png +EXECUTE ADD Contents/MacOS/exe0.exe +EXECUTE REMOVEDIR Contents/Resources/9/99/ +EXECUTE REMOVEDIR Contents/Resources/9/99/ +EXECUTE REMOVEDIR Contents/Resources/9/98/ +EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +EXECUTE REMOVEDIR Contents/Resources/9/97/970/ +EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +EXECUTE REMOVEDIR Contents/Resources/9/97/971/ +EXECUTE REMOVEDIR Contents/Resources/9/97/ +EXECUTE REMOVEFILE Contents/Resources/9/96/96text0 +EXECUTE REMOVEFILE Contents/Resources/9/96/96text1 +EXECUTE REMOVEDIR Contents/Resources/9/96/ +EXECUTE REMOVEDIR Contents/Resources/9/95/ +EXECUTE REMOVEDIR Contents/Resources/9/95/ +EXECUTE REMOVEDIR Contents/Resources/9/94/ +EXECUTE REMOVEDIR Contents/Resources/9/94/ +EXECUTE REMOVEDIR Contents/Resources/9/93/ +EXECUTE REMOVEDIR Contents/Resources/9/92/ +EXECUTE REMOVEDIR Contents/Resources/9/91/ +EXECUTE REMOVEDIR Contents/Resources/9/90/ +EXECUTE REMOVEDIR Contents/Resources/9/90/ +EXECUTE REMOVEDIR Contents/Resources/8/89/ +EXECUTE REMOVEDIR Contents/Resources/8/89/ +EXECUTE REMOVEDIR Contents/Resources/8/88/ +EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +EXECUTE REMOVEDIR Contents/Resources/8/87/870/ +EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +EXECUTE REMOVEDIR Contents/Resources/8/87/871/ +EXECUTE REMOVEDIR Contents/Resources/8/87/ +EXECUTE REMOVEFILE Contents/Resources/8/86/86text0 +EXECUTE REMOVEFILE Contents/Resources/8/86/86text1 +EXECUTE REMOVEDIR Contents/Resources/8/86/ +EXECUTE REMOVEDIR Contents/Resources/8/85/ +EXECUTE REMOVEDIR Contents/Resources/8/85/ +EXECUTE REMOVEDIR Contents/Resources/8/84/ +EXECUTE REMOVEDIR Contents/Resources/8/84/ +EXECUTE REMOVEDIR Contents/Resources/8/83/ +EXECUTE REMOVEDIR Contents/Resources/8/82/ +EXECUTE REMOVEDIR Contents/Resources/8/81/ +EXECUTE REMOVEDIR Contents/Resources/8/80/ +EXECUTE REMOVEDIR Contents/Resources/8/80/ +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtest.exe +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext0 +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext1 +EXECUTE REMOVEDIR Contents/Resources/7/70/ +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtest.exe +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext0 +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext1 +EXECUTE REMOVEDIR Contents/Resources/7/71/ +EXECUTE REMOVEFILE Contents/Resources/7/7text0 +EXECUTE REMOVEFILE Contents/Resources/7/7text1 +EXECUTE REMOVEDIR Contents/Resources/7/ +EXECUTE REMOVEDIR Contents/Resources/6/ +EXECUTE REMOVEFILE Contents/Resources/5/5text1 +EXECUTE REMOVEFILE Contents/Resources/5/5text0 +EXECUTE REMOVEFILE Contents/Resources/5/5test.exe +EXECUTE REMOVEFILE Contents/Resources/5/5text0 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEFILE Contents/Resources/5/5text1 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEDIR Contents/Resources/5/ +EXECUTE REMOVEFILE Contents/Resources/4/4text1 +EXECUTE REMOVEFILE Contents/Resources/4/4text0 +EXECUTE REMOVEDIR Contents/Resources/4/ +EXECUTE REMOVEFILE Contents/Resources/3/3text1 +EXECUTE REMOVEFILE Contents/Resources/3/3text0 +FINISH REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0 +FINISH REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png +FINISH REMOVEFILE Contents/Resources/removed-files +FINISH REMOVEFILE Contents/Resources/precomplete +FINISH REMOVEFILE Contents/Resources/2/20/20text0 +FINISH REMOVEFILE Contents/Resources/2/20/20png0.png +FINISH REMOVEFILE Contents/Resources/0/0exe0.exe +FINISH REMOVEFILE Contents/Resources/0/00/00text0 +FINISH REMOVEFILE Contents/MacOS/exe0.exe +FINISH REMOVEDIR Contents/Resources/searchplugins/ +removing directory: Contents/Resources/searchplugins/, rv: 0 +FINISH REMOVEDIR Contents/Resources/defaults/pref/ +removing directory: Contents/Resources/defaults/pref/, rv: 0 +FINISH REMOVEDIR Contents/Resources/defaults/ +removing directory: Contents/Resources/defaults/, rv: 0 +FINISH REMOVEDIR Contents/Resources/2/20/ +FINISH REMOVEDIR Contents/Resources/2/ +FINISH REMOVEDIR Contents/Resources/0/00/ +removing directory: Contents/Resources/0/00/, rv: 0 +FINISH REMOVEDIR Contents/Resources/0/ +removing directory: Contents/Resources/0/, rv: 0 +FINISH REMOVEDIR Contents/Resources/ +removing directory: Contents/Resources/, rv: 0 +FINISH REMOVEDIR Contents/MacOS/ +removing directory: Contents/MacOS/, rv: 0 +FINISH REMOVEDIR Contents/ +removing directory: Contents/, rv: 0 +FINISH ADD Contents/Resources/searchplugins/searchpluginstext0 +FINISH ADD Contents/Resources/searchplugins/searchpluginspng1.png +FINISH ADD Contents/Resources/searchplugins/searchpluginspng0.png +FINISH ADD Contents/Resources/removed-files +FINISH ADD Contents/Resources/precomplete +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +FINISH ADD Contents/Resources/1/10/10text0 +FINISH ADD Contents/Resources/0/0exe0.exe +FINISH ADD Contents/Resources/0/00/00text1 +FINISH ADD Contents/Resources/0/00/00text0 +FINISH ADD Contents/Resources/0/00/00png0.png +FINISH ADD Contents/MacOS/exe0.exe +FINISH REMOVEDIR Contents/Resources/9/99/ +FINISH REMOVEDIR Contents/Resources/9/99/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/98/ +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0 +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1 +FINISH REMOVEDIR Contents/Resources/9/97/970/ +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0 +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1 +FINISH REMOVEDIR Contents/Resources/9/97/971/ +FINISH REMOVEDIR Contents/Resources/9/97/ +FINISH REMOVEFILE Contents/Resources/9/96/96text0 +FINISH REMOVEFILE Contents/Resources/9/96/96text1 +FINISH REMOVEDIR Contents/Resources/9/96/ +FINISH REMOVEDIR Contents/Resources/9/95/ +FINISH REMOVEDIR Contents/Resources/9/95/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/94/ +FINISH REMOVEDIR Contents/Resources/9/94/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/93/ +FINISH REMOVEDIR Contents/Resources/9/92/ +removing directory: Contents/Resources/9/92/, rv: 0 +FINISH REMOVEDIR Contents/Resources/9/91/ +removing directory: Contents/Resources/9/91/, rv: 0 +FINISH REMOVEDIR Contents/Resources/9/90/ +FINISH REMOVEDIR Contents/Resources/9/90/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/89/ +FINISH REMOVEDIR Contents/Resources/8/89/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/88/ +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0 +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1 +FINISH REMOVEDIR Contents/Resources/8/87/870/ +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0 +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1 +FINISH REMOVEDIR Contents/Resources/8/87/871/ +FINISH REMOVEDIR Contents/Resources/8/87/ +FINISH REMOVEFILE Contents/Resources/8/86/86text0 +FINISH REMOVEFILE Contents/Resources/8/86/86text1 +FINISH REMOVEDIR Contents/Resources/8/86/ +FINISH REMOVEDIR Contents/Resources/8/85/ +FINISH REMOVEDIR Contents/Resources/8/85/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/84/ +FINISH REMOVEDIR Contents/Resources/8/84/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/83/ +FINISH REMOVEDIR Contents/Resources/8/82/ +removing directory: Contents/Resources/8/82/, rv: 0 +FINISH REMOVEDIR Contents/Resources/8/81/ +removing directory: Contents/Resources/8/81/, rv: 0 +FINISH REMOVEDIR Contents/Resources/8/80/ +FINISH REMOVEDIR Contents/Resources/8/80/ +directory no longer exists; skipping +FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe +FINISH REMOVEFILE Contents/Resources/7/70/7xtext0 +FINISH REMOVEFILE Contents/Resources/7/70/7xtext1 +FINISH REMOVEDIR Contents/Resources/7/70/ +FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe +FINISH REMOVEFILE Contents/Resources/7/71/7xtext0 +FINISH REMOVEFILE Contents/Resources/7/71/7xtext1 +FINISH REMOVEDIR Contents/Resources/7/71/ +FINISH REMOVEFILE Contents/Resources/7/7text0 +FINISH REMOVEFILE Contents/Resources/7/7text1 +FINISH REMOVEDIR Contents/Resources/7/ +FINISH REMOVEDIR Contents/Resources/6/ +FINISH REMOVEFILE Contents/Resources/5/5text1 +FINISH REMOVEFILE Contents/Resources/5/5text0 +FINISH REMOVEFILE Contents/Resources/5/5test.exe +FINISH REMOVEDIR Contents/Resources/5/ +FINISH REMOVEFILE Contents/Resources/4/4text1 +FINISH REMOVEFILE Contents/Resources/4/4text0 +FINISH REMOVEDIR Contents/Resources/4/ +FINISH REMOVEFILE Contents/Resources/3/3text1 +FINISH REMOVEFILE Contents/Resources/3/3text0 +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/complete_log_success_win b/toolkit/mozapps/update/tests/data/complete_log_success_win new file mode 100644 index 0000000000..c5a03dc9d6 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_log_success_win @@ -0,0 +1,320 @@ +UPDATE TYPE complete +PREPARE REMOVEFILE searchplugins/searchpluginstext0 +PREPARE REMOVEFILE searchplugins/searchpluginspng0.png +PREPARE REMOVEFILE removed-files +PREPARE REMOVEFILE precomplete +PREPARE REMOVEFILE exe0.exe +PREPARE REMOVEFILE 2/20/20text0 +PREPARE REMOVEFILE 2/20/20png0.png +PREPARE REMOVEFILE 0/0exe0.exe +PREPARE REMOVEFILE 0/00/00text0 +PREPARE REMOVEDIR searchplugins/ +PREPARE REMOVEDIR defaults/pref/ +PREPARE REMOVEDIR defaults/ +PREPARE REMOVEDIR 2/20/ +PREPARE REMOVEDIR 2/ +PREPARE REMOVEDIR 0/00/ +PREPARE REMOVEDIR 0/ +PREPARE ADD searchplugins/searchpluginstext0 +PREPARE ADD searchplugins/searchpluginspng1.png +PREPARE ADD searchplugins/searchpluginspng0.png +PREPARE ADD removed-files +PREPARE ADD precomplete +PREPARE ADD exe0.exe +PREPARE ADD distribution/extensions/extensions1/extensions1text0 +PREPARE ADD distribution/extensions/extensions1/extensions1png1.png +PREPARE ADD distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD distribution/extensions/extensions0/extensions0text0 +PREPARE ADD distribution/extensions/extensions0/extensions0png1.png +PREPARE ADD distribution/extensions/extensions0/extensions0png0.png +PREPARE ADD 1/10/10text0 +PREPARE ADD 0/0exe0.exe +PREPARE ADD 0/00/00text1 +PREPARE ADD 0/00/00text0 +PREPARE ADD 0/00/00png0.png +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/98/ +PREPARE REMOVEFILE 9/97/970/97xtext0 +PREPARE REMOVEFILE 9/97/970/97xtext1 +PREPARE REMOVEDIR 9/97/970/ +PREPARE REMOVEFILE 9/97/971/97xtext0 +PREPARE REMOVEFILE 9/97/971/97xtext1 +PREPARE REMOVEDIR 9/97/971/ +PREPARE REMOVEDIR 9/97/ +PREPARE REMOVEFILE 9/96/96text0 +PREPARE REMOVEFILE 9/96/96text1 +PREPARE REMOVEDIR 9/96/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/93/ +PREPARE REMOVEDIR 9/92/ +PREPARE REMOVEDIR 9/91/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/88/ +PREPARE REMOVEFILE 8/87/870/87xtext0 +PREPARE REMOVEFILE 8/87/870/87xtext1 +PREPARE REMOVEDIR 8/87/870/ +PREPARE REMOVEFILE 8/87/871/87xtext0 +PREPARE REMOVEFILE 8/87/871/87xtext1 +PREPARE REMOVEDIR 8/87/871/ +PREPARE REMOVEDIR 8/87/ +PREPARE REMOVEFILE 8/86/86text0 +PREPARE REMOVEFILE 8/86/86text1 +PREPARE REMOVEDIR 8/86/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/83/ +PREPARE REMOVEDIR 8/82/ +PREPARE REMOVEDIR 8/81/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEFILE 7/70/7xtest.exe +PREPARE REMOVEFILE 7/70/7xtext0 +PREPARE REMOVEFILE 7/70/7xtext1 +PREPARE REMOVEDIR 7/70/ +PREPARE REMOVEFILE 7/71/7xtest.exe +PREPARE REMOVEFILE 7/71/7xtext0 +PREPARE REMOVEFILE 7/71/7xtext1 +PREPARE REMOVEDIR 7/71/ +PREPARE REMOVEFILE 7/7text0 +PREPARE REMOVEFILE 7/7text1 +PREPARE REMOVEDIR 7/ +PREPARE REMOVEDIR 6/ +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5test.exe +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEDIR 5/ +PREPARE REMOVEFILE 4/4text1 +PREPARE REMOVEFILE 4/4text0 +PREPARE REMOVEDIR 4/ +PREPARE REMOVEFILE 3/3text1 +PREPARE REMOVEFILE 3/3text0 +EXECUTE REMOVEFILE searchplugins/searchpluginstext0 +EXECUTE REMOVEFILE searchplugins/searchpluginspng0.png +EXECUTE REMOVEFILE removed-files +EXECUTE REMOVEFILE precomplete +EXECUTE REMOVEFILE exe0.exe +EXECUTE REMOVEFILE 2/20/20text0 +EXECUTE REMOVEFILE 2/20/20png0.png +EXECUTE REMOVEFILE 0/0exe0.exe +EXECUTE REMOVEFILE 0/00/00text0 +EXECUTE REMOVEDIR searchplugins/ +EXECUTE REMOVEDIR defaults/pref/ +EXECUTE REMOVEDIR defaults/ +EXECUTE REMOVEDIR 2/20/ +EXECUTE REMOVEDIR 2/ +EXECUTE REMOVEDIR 0/00/ +EXECUTE REMOVEDIR 0/ +EXECUTE ADD searchplugins/searchpluginstext0 +EXECUTE ADD searchplugins/searchpluginspng1.png +EXECUTE ADD searchplugins/searchpluginspng0.png +EXECUTE ADD removed-files +EXECUTE ADD precomplete +EXECUTE ADD exe0.exe +EXECUTE ADD distribution/extensions/extensions1/extensions1text0 +EXECUTE ADD distribution/extensions/extensions1/extensions1png1.png +EXECUTE ADD distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD distribution/extensions/extensions0/extensions0text0 +EXECUTE ADD distribution/extensions/extensions0/extensions0png1.png +EXECUTE ADD distribution/extensions/extensions0/extensions0png0.png +EXECUTE ADD 1/10/10text0 +EXECUTE ADD 0/0exe0.exe +EXECUTE ADD 0/00/00text1 +EXECUTE ADD 0/00/00text0 +EXECUTE ADD 0/00/00png0.png +EXECUTE REMOVEDIR 9/99/ +EXECUTE REMOVEDIR 9/99/ +EXECUTE REMOVEDIR 9/98/ +EXECUTE REMOVEFILE 9/97/970/97xtext0 +EXECUTE REMOVEFILE 9/97/970/97xtext1 +EXECUTE REMOVEDIR 9/97/970/ +EXECUTE REMOVEFILE 9/97/971/97xtext0 +EXECUTE REMOVEFILE 9/97/971/97xtext1 +EXECUTE REMOVEDIR 9/97/971/ +EXECUTE REMOVEDIR 9/97/ +EXECUTE REMOVEFILE 9/96/96text0 +EXECUTE REMOVEFILE 9/96/96text1 +EXECUTE REMOVEDIR 9/96/ +EXECUTE REMOVEDIR 9/95/ +EXECUTE REMOVEDIR 9/95/ +EXECUTE REMOVEDIR 9/94/ +EXECUTE REMOVEDIR 9/94/ +EXECUTE REMOVEDIR 9/93/ +EXECUTE REMOVEDIR 9/92/ +EXECUTE REMOVEDIR 9/91/ +EXECUTE REMOVEDIR 9/90/ +EXECUTE REMOVEDIR 9/90/ +EXECUTE REMOVEDIR 8/89/ +EXECUTE REMOVEDIR 8/89/ +EXECUTE REMOVEDIR 8/88/ +EXECUTE REMOVEFILE 8/87/870/87xtext0 +EXECUTE REMOVEFILE 8/87/870/87xtext1 +EXECUTE REMOVEDIR 8/87/870/ +EXECUTE REMOVEFILE 8/87/871/87xtext0 +EXECUTE REMOVEFILE 8/87/871/87xtext1 +EXECUTE REMOVEDIR 8/87/871/ +EXECUTE REMOVEDIR 8/87/ +EXECUTE REMOVEFILE 8/86/86text0 +EXECUTE REMOVEFILE 8/86/86text1 +EXECUTE REMOVEDIR 8/86/ +EXECUTE REMOVEDIR 8/85/ +EXECUTE REMOVEDIR 8/85/ +EXECUTE REMOVEDIR 8/84/ +EXECUTE REMOVEDIR 8/84/ +EXECUTE REMOVEDIR 8/83/ +EXECUTE REMOVEDIR 8/82/ +EXECUTE REMOVEDIR 8/81/ +EXECUTE REMOVEDIR 8/80/ +EXECUTE REMOVEDIR 8/80/ +EXECUTE REMOVEFILE 7/70/7xtest.exe +EXECUTE REMOVEFILE 7/70/7xtext0 +EXECUTE REMOVEFILE 7/70/7xtext1 +EXECUTE REMOVEDIR 7/70/ +EXECUTE REMOVEFILE 7/71/7xtest.exe +EXECUTE REMOVEFILE 7/71/7xtext0 +EXECUTE REMOVEFILE 7/71/7xtext1 +EXECUTE REMOVEDIR 7/71/ +EXECUTE REMOVEFILE 7/7text0 +EXECUTE REMOVEFILE 7/7text1 +EXECUTE REMOVEDIR 7/ +EXECUTE REMOVEDIR 6/ +EXECUTE REMOVEFILE 5/5text1 +EXECUTE REMOVEFILE 5/5text0 +EXECUTE REMOVEFILE 5/5test.exe +EXECUTE REMOVEFILE 5/5text0 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEFILE 5/5text1 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEDIR 5/ +EXECUTE REMOVEFILE 4/4text1 +EXECUTE REMOVEFILE 4/4text0 +EXECUTE REMOVEDIR 4/ +EXECUTE REMOVEFILE 3/3text1 +EXECUTE REMOVEFILE 3/3text0 +FINISH REMOVEFILE searchplugins/searchpluginstext0 +FINISH REMOVEFILE searchplugins/searchpluginspng0.png +FINISH REMOVEFILE removed-files +FINISH REMOVEFILE precomplete +FINISH REMOVEFILE exe0.exe +FINISH REMOVEFILE 2/20/20text0 +FINISH REMOVEFILE 2/20/20png0.png +FINISH REMOVEFILE 0/0exe0.exe +FINISH REMOVEFILE 0/00/00text0 +FINISH REMOVEDIR searchplugins/ +removing directory: searchplugins/, rv: 0 +FINISH REMOVEDIR defaults/pref/ +removing directory: defaults/pref/, rv: 0 +FINISH REMOVEDIR defaults/ +removing directory: defaults/, rv: 0 +FINISH REMOVEDIR 2/20/ +FINISH REMOVEDIR 2/ +FINISH REMOVEDIR 0/00/ +removing directory: 0/00/, rv: 0 +FINISH REMOVEDIR 0/ +removing directory: 0/, rv: 0 +FINISH ADD searchplugins/searchpluginstext0 +FINISH ADD searchplugins/searchpluginspng1.png +FINISH ADD searchplugins/searchpluginspng0.png +FINISH ADD removed-files +FINISH ADD precomplete +FINISH ADD exe0.exe +FINISH ADD distribution/extensions/extensions1/extensions1text0 +FINISH ADD distribution/extensions/extensions1/extensions1png1.png +FINISH ADD distribution/extensions/extensions1/extensions1png0.png +FINISH ADD distribution/extensions/extensions0/extensions0text0 +FINISH ADD distribution/extensions/extensions0/extensions0png1.png +FINISH ADD distribution/extensions/extensions0/extensions0png0.png +FINISH ADD 1/10/10text0 +FINISH ADD 0/0exe0.exe +FINISH ADD 0/00/00text1 +FINISH ADD 0/00/00text0 +FINISH ADD 0/00/00png0.png +FINISH REMOVEDIR 9/99/ +FINISH REMOVEDIR 9/99/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/98/ +FINISH REMOVEFILE 9/97/970/97xtext0 +FINISH REMOVEFILE 9/97/970/97xtext1 +FINISH REMOVEDIR 9/97/970/ +FINISH REMOVEFILE 9/97/971/97xtext0 +FINISH REMOVEFILE 9/97/971/97xtext1 +FINISH REMOVEDIR 9/97/971/ +FINISH REMOVEDIR 9/97/ +FINISH REMOVEFILE 9/96/96text0 +FINISH REMOVEFILE 9/96/96text1 +FINISH REMOVEDIR 9/96/ +FINISH REMOVEDIR 9/95/ +FINISH REMOVEDIR 9/95/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/94/ +FINISH REMOVEDIR 9/94/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/93/ +FINISH REMOVEDIR 9/92/ +removing directory: 9/92/, rv: 0 +FINISH REMOVEDIR 9/91/ +removing directory: 9/91/, rv: 0 +FINISH REMOVEDIR 9/90/ +FINISH REMOVEDIR 9/90/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/89/ +FINISH REMOVEDIR 8/89/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/88/ +FINISH REMOVEFILE 8/87/870/87xtext0 +FINISH REMOVEFILE 8/87/870/87xtext1 +FINISH REMOVEDIR 8/87/870/ +FINISH REMOVEFILE 8/87/871/87xtext0 +FINISH REMOVEFILE 8/87/871/87xtext1 +FINISH REMOVEDIR 8/87/871/ +FINISH REMOVEDIR 8/87/ +FINISH REMOVEFILE 8/86/86text0 +FINISH REMOVEFILE 8/86/86text1 +FINISH REMOVEDIR 8/86/ +FINISH REMOVEDIR 8/85/ +FINISH REMOVEDIR 8/85/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/84/ +FINISH REMOVEDIR 8/84/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/83/ +FINISH REMOVEDIR 8/82/ +removing directory: 8/82/, rv: 0 +FINISH REMOVEDIR 8/81/ +removing directory: 8/81/, rv: 0 +FINISH REMOVEDIR 8/80/ +FINISH REMOVEDIR 8/80/ +directory no longer exists; skipping +FINISH REMOVEFILE 7/70/7xtest.exe +FINISH REMOVEFILE 7/70/7xtext0 +FINISH REMOVEFILE 7/70/7xtext1 +FINISH REMOVEDIR 7/70/ +FINISH REMOVEFILE 7/71/7xtest.exe +FINISH REMOVEFILE 7/71/7xtext0 +FINISH REMOVEFILE 7/71/7xtext1 +FINISH REMOVEDIR 7/71/ +FINISH REMOVEFILE 7/7text0 +FINISH REMOVEFILE 7/7text1 +FINISH REMOVEDIR 7/ +FINISH REMOVEDIR 6/ +FINISH REMOVEFILE 5/5text1 +FINISH REMOVEFILE 5/5text0 +FINISH REMOVEFILE 5/5test.exe +FINISH REMOVEDIR 5/ +FINISH REMOVEFILE 4/4text1 +FINISH REMOVEFILE 4/4text0 +FINISH REMOVEDIR 4/ +FINISH REMOVEFILE 3/3text1 +FINISH REMOVEFILE 3/3text0 +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/complete_mac.mar b/toolkit/mozapps/update/tests/data/complete_mac.mar Binary files differnew file mode 100644 index 0000000000..c54088610a --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_mac.mar diff --git a/toolkit/mozapps/update/tests/data/complete_precomplete b/toolkit/mozapps/update/tests/data/complete_precomplete new file mode 100644 index 0000000000..ae7a0013ff --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_precomplete @@ -0,0 +1,18 @@ +remove "searchplugins/searchpluginstext0" +remove "searchplugins/searchpluginspng1.png" +remove "searchplugins/searchpluginspng0.png" +remove "removed-files" +remove "precomplete" +remove "exe0.exe" +remove "1/10/10text0" +remove "0/0exe0.exe" +remove "0/00/00text1" +remove "0/00/00text0" +remove "0/00/00png0.png" +rmdir "searchplugins/" +rmdir "defaults/pref/" +rmdir "defaults/" +rmdir "1/10/" +rmdir "1/" +rmdir "0/00/" +rmdir "0/" diff --git a/toolkit/mozapps/update/tests/data/complete_precomplete_mac b/toolkit/mozapps/update/tests/data/complete_precomplete_mac new file mode 100644 index 0000000000..8d81a36d66 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_precomplete_mac @@ -0,0 +1,21 @@ +remove "Contents/Resources/searchplugins/searchpluginstext0" +remove "Contents/Resources/searchplugins/searchpluginspng1.png" +remove "Contents/Resources/searchplugins/searchpluginspng0.png" +remove "Contents/Resources/removed-files" +remove "Contents/Resources/precomplete" +remove "Contents/Resources/1/10/10text0" +remove "Contents/Resources/0/0exe0.exe" +remove "Contents/Resources/0/00/00text1" +remove "Contents/Resources/0/00/00text0" +remove "Contents/Resources/0/00/00png0.png" +remove "Contents/MacOS/exe0.exe" +rmdir "Contents/Resources/searchplugins/" +rmdir "Contents/Resources/defaults/pref/" +rmdir "Contents/Resources/defaults/" +rmdir "Contents/Resources/1/10/" +rmdir "Contents/Resources/1/" +rmdir "Contents/Resources/0/00/" +rmdir "Contents/Resources/0/" +rmdir "Contents/Resources/" +rmdir "Contents/MacOS/" +rmdir "Contents/" diff --git a/toolkit/mozapps/update/tests/data/complete_removed-files b/toolkit/mozapps/update/tests/data/complete_removed-files new file mode 100644 index 0000000000..e45c43c1f8 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_removed-files @@ -0,0 +1,41 @@ +text0 +text1 +3/3text0 +3/3text1 +4/exe0.exe +4/4text0 +4/4text1 +4/ +5/5text0 +5/5text1 +5/* +6/ +7/* +8/80/ +8/81/ +8/82/ +8/83/ +8/84/ +8/85/* +8/86/* +8/87/* +8/88/* +8/89/* +8/80/ +8/84/* +8/85/* +8/89/ +9/90/ +9/91/ +9/92/ +9/93/ +9/94/ +9/95/* +9/96/* +9/97/* +9/98/* +9/99/* +9/90/ +9/94/* +9/95/* +9/99/ diff --git a/toolkit/mozapps/update/tests/data/complete_removed-files_mac b/toolkit/mozapps/update/tests/data/complete_removed-files_mac new file mode 100644 index 0000000000..955dc5b340 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_removed-files_mac @@ -0,0 +1,41 @@ +Contents/Resources/text0 +Contents/Resources/text1 +Contents/Resources/3/3text0 +Contents/Resources/3/3text1 +Contents/Resources/4/exe0.exe +Contents/Resources/4/4text0 +Contents/Resources/4/4text1 +Contents/Resources/4/ +Contents/Resources/5/5text0 +Contents/Resources/5/5text1 +Contents/Resources/5/* +Contents/Resources/6/ +Contents/Resources/7/* +Contents/Resources/8/80/ +Contents/Resources/8/81/ +Contents/Resources/8/82/ +Contents/Resources/8/83/ +Contents/Resources/8/84/ +Contents/Resources/8/85/* +Contents/Resources/8/86/* +Contents/Resources/8/87/* +Contents/Resources/8/88/* +Contents/Resources/8/89/* +Contents/Resources/8/80/ +Contents/Resources/8/84/* +Contents/Resources/8/85/* +Contents/Resources/8/89/ +Contents/Resources/9/90/ +Contents/Resources/9/91/ +Contents/Resources/9/92/ +Contents/Resources/9/93/ +Contents/Resources/9/94/ +Contents/Resources/9/95/* +Contents/Resources/9/96/* +Contents/Resources/9/97/* +Contents/Resources/9/98/* +Contents/Resources/9/99/* +Contents/Resources/9/90/ +Contents/Resources/9/94/* +Contents/Resources/9/95/* +Contents/Resources/9/99/ diff --git a/toolkit/mozapps/update/tests/data/complete_update_manifest b/toolkit/mozapps/update/tests/data/complete_update_manifest new file mode 100644 index 0000000000..383a324f63 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/complete_update_manifest @@ -0,0 +1,59 @@ +type "complete" +add "precomplete" +add "searchplugins/searchpluginstext0" +add "searchplugins/searchpluginspng1.png" +add "searchplugins/searchpluginspng0.png" +add "removed-files" +add-if "extensions/extensions1" "extensions/extensions1/extensions1text0" +add-if "extensions/extensions1" "extensions/extensions1/extensions1png1.png" +add-if "extensions/extensions1" "extensions/extensions1/extensions1png0.png" +add-if "extensions/extensions0" "extensions/extensions0/extensions0text0" +add-if "extensions/extensions0" "extensions/extensions0/extensions0png1.png" +add-if "extensions/extensions0" "extensions/extensions0/extensions0png0.png" +add "exe0.exe" +add "1/10/10text0" +add "0/0exe0.exe" +add "0/00/00text1" +add "0/00/00text0" +add "0/00/00png0.png" +remove "text1" +remove "text0" +rmrfdir "9/99/" +rmdir "9/99/" +rmrfdir "9/98/" +rmrfdir "9/97/" +rmrfdir "9/96/" +rmrfdir "9/95/" +rmrfdir "9/95/" +rmrfdir "9/94/" +rmdir "9/94/" +rmdir "9/93/" +rmdir "9/92/" +rmdir "9/91/" +rmdir "9/90/" +rmdir "9/90/" +rmrfdir "8/89/" +rmdir "8/89/" +rmrfdir "8/88/" +rmrfdir "8/87/" +rmrfdir "8/86/" +rmrfdir "8/85/" +rmrfdir "8/85/" +rmrfdir "8/84/" +rmdir "8/84/" +rmdir "8/83/" +rmdir "8/82/" +rmdir "8/81/" +rmdir "8/80/" +rmdir "8/80/" +rmrfdir "7/" +rmdir "6/" +remove "5/5text1" +remove "5/5text0" +rmrfdir "5/" +remove "4/exe0.exe" +remove "4/4text1" +remove "4/4text0" +rmdir "4/" +remove "3/3text1" +remove "3/3text0" diff --git a/toolkit/mozapps/update/tests/data/old_version.mar b/toolkit/mozapps/update/tests/data/old_version.mar Binary files differnew file mode 100644 index 0000000000..b48f1d5fa4 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/old_version.mar diff --git a/toolkit/mozapps/update/tests/data/partial.exe b/toolkit/mozapps/update/tests/data/partial.exe Binary files differnew file mode 100644 index 0000000000..3949fd2a0e --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial.exe diff --git a/toolkit/mozapps/update/tests/data/partial.mar b/toolkit/mozapps/update/tests/data/partial.mar Binary files differnew file mode 100644 index 0000000000..b6b04bbdbf --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial.mar diff --git a/toolkit/mozapps/update/tests/data/partial.png b/toolkit/mozapps/update/tests/data/partial.png Binary files differnew file mode 100644 index 0000000000..9246f586c7 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial.png diff --git a/toolkit/mozapps/update/tests/data/partial_log_failure_mac b/toolkit/mozapps/update/tests/data/partial_log_failure_mac new file mode 100644 index 0000000000..3b2933ebd2 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_log_failure_mac @@ -0,0 +1,192 @@ +UPDATE TYPE partial +PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0 +PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng1.png +PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng0.png +PREPARE ADD Contents/Resources/precomplete +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +PREPARE PATCH Contents/Resources/0/0exe0.exe +PREPARE ADD Contents/Resources/0/00/00text0 +PREPARE PATCH Contents/Resources/0/00/00png0.png +PREPARE PATCH Contents/MacOS/exe0.exe +PREPARE ADD Contents/Resources/2/20/20text0 +PREPARE ADD Contents/Resources/2/20/20png0.png +PREPARE ADD Contents/Resources/0/00/00text2 +PREPARE REMOVEFILE Contents/Resources/1/10/10text0 +PREPARE REMOVEFILE Contents/Resources/0/00/00text1 +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/98/ +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/970/ +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/971/ +PREPARE REMOVEDIR Contents/Resources/9/97/ +PREPARE REMOVEFILE Contents/Resources/9/96/96text0 +PREPARE REMOVEFILE Contents/Resources/9/96/96text1 +PREPARE REMOVEDIR Contents/Resources/9/96/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/93/ +PREPARE REMOVEDIR Contents/Resources/9/92/ +PREPARE REMOVEDIR Contents/Resources/9/91/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/88/ +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/870/ +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/871/ +PREPARE REMOVEDIR Contents/Resources/8/87/ +PREPARE REMOVEFILE Contents/Resources/8/86/86text0 +PREPARE REMOVEFILE Contents/Resources/8/86/86text1 +PREPARE REMOVEDIR Contents/Resources/8/86/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/83/ +PREPARE REMOVEDIR Contents/Resources/8/82/ +PREPARE REMOVEDIR Contents/Resources/8/81/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/70/ +PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/71/ +PREPARE REMOVEFILE Contents/Resources/7/7text0 +PREPARE REMOVEFILE Contents/Resources/7/7text1 +PREPARE REMOVEDIR Contents/Resources/7/ +PREPARE REMOVEDIR Contents/Resources/6/ +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5test.exe +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEDIR Contents/Resources/5/ +PREPARE REMOVEFILE Contents/Resources/4/4text1 +PREPARE REMOVEFILE Contents/Resources/4/4text0 +PREPARE REMOVEDIR Contents/Resources/4/ +PREPARE REMOVEFILE Contents/Resources/3/3text1 +PREPARE REMOVEFILE Contents/Resources/3/3text0 +PREPARE REMOVEDIR Contents/Resources/1/10/ +PREPARE REMOVEDIR Contents/Resources/1/ +EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0 +EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng1.png +EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng0.png +EXECUTE ADD Contents/Resources/precomplete +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +EXECUTE PATCH Contents/Resources/0/0exe0.exe +LoadSourceFile: destination file size 776 does not match expected size 79872 +LoadSourceFile failed +### execution failed +FINISH ADD Contents/Resources/searchplugins/searchpluginstext0 +FINISH PATCH Contents/Resources/searchplugins/searchpluginspng1.png +FINISH PATCH Contents/Resources/searchplugins/searchpluginspng0.png +FINISH ADD Contents/Resources/precomplete +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +backup_restore: backup file doesn't exist: Contents/Resources/distribution/extensions/extensions1/extensions1text0.moz-backup +FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +FINISH PATCH Contents/Resources/0/0exe0.exe +backup_restore: backup file doesn't exist: Contents/Resources/0/0exe0.exe.moz-backup +FINISH ADD Contents/Resources/0/00/00text0 +backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text0.moz-backup +FINISH PATCH Contents/Resources/0/00/00png0.png +backup_restore: backup file doesn't exist: Contents/Resources/0/00/00png0.png.moz-backup +FINISH PATCH Contents/MacOS/exe0.exe +backup_restore: backup file doesn't exist: Contents/MacOS/exe0.exe.moz-backup +FINISH ADD Contents/Resources/2/20/20text0 +backup_restore: backup file doesn't exist: Contents/Resources/2/20/20text0.moz-backup +FINISH ADD Contents/Resources/2/20/20png0.png +backup_restore: backup file doesn't exist: Contents/Resources/2/20/20png0.png.moz-backup +FINISH ADD Contents/Resources/0/00/00text2 +backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text2.moz-backup +FINISH REMOVEFILE Contents/Resources/1/10/10text0 +backup_restore: backup file doesn't exist: Contents/Resources/1/10/10text0.moz-backup +FINISH REMOVEFILE Contents/Resources/0/00/00text1 +backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text1.moz-backup +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/9/97/970/97xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/9/97/970/97xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/9/97/971/97xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/9/97/971/97xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/9/96/96text0 +backup_restore: backup file doesn't exist: Contents/Resources/9/96/96text0.moz-backup +FINISH REMOVEFILE Contents/Resources/9/96/96text1 +backup_restore: backup file doesn't exist: Contents/Resources/9/96/96text1.moz-backup +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/8/87/870/87xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/8/87/870/87xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/8/87/871/87xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/8/87/871/87xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/8/86/86text0 +backup_restore: backup file doesn't exist: Contents/Resources/8/86/86text0.moz-backup +FINISH REMOVEFILE Contents/Resources/8/86/86text1 +backup_restore: backup file doesn't exist: Contents/Resources/8/86/86text1.moz-backup +FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe +backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtest.exe.moz-backup +FINISH REMOVEFILE Contents/Resources/7/70/7xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/7/70/7xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe +backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtest.exe.moz-backup +FINISH REMOVEFILE Contents/Resources/7/71/7xtext0 +backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtext0.moz-backup +FINISH REMOVEFILE Contents/Resources/7/71/7xtext1 +backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtext1.moz-backup +FINISH REMOVEFILE Contents/Resources/7/7text0 +backup_restore: backup file doesn't exist: Contents/Resources/7/7text0.moz-backup +FINISH REMOVEFILE Contents/Resources/7/7text1 +backup_restore: backup file doesn't exist: Contents/Resources/7/7text1.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5text1 +backup_restore: backup file doesn't exist: Contents/Resources/5/5text1.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5text0 +backup_restore: backup file doesn't exist: Contents/Resources/5/5text0.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5test.exe +backup_restore: backup file doesn't exist: Contents/Resources/5/5test.exe.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5text0 +backup_restore: backup file doesn't exist: Contents/Resources/5/5text0.moz-backup +FINISH REMOVEFILE Contents/Resources/5/5text1 +backup_restore: backup file doesn't exist: Contents/Resources/5/5text1.moz-backup +FINISH REMOVEFILE Contents/Resources/4/4text1 +backup_restore: backup file doesn't exist: Contents/Resources/4/4text1.moz-backup +FINISH REMOVEFILE Contents/Resources/4/4text0 +backup_restore: backup file doesn't exist: Contents/Resources/4/4text0.moz-backup +FINISH REMOVEFILE Contents/Resources/3/3text1 +backup_restore: backup file doesn't exist: Contents/Resources/3/3text1.moz-backup +FINISH REMOVEFILE Contents/Resources/3/3text0 +backup_restore: backup file doesn't exist: Contents/Resources/3/3text0.moz-backup +failed: 2 +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/partial_log_failure_win b/toolkit/mozapps/update/tests/data/partial_log_failure_win new file mode 100644 index 0000000000..e3d683dc19 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_log_failure_win @@ -0,0 +1,192 @@ +UPDATE TYPE partial +PREPARE ADD searchplugins/searchpluginstext0 +PREPARE PATCH searchplugins/searchpluginspng1.png +PREPARE PATCH searchplugins/searchpluginspng0.png +PREPARE ADD precomplete +PREPARE PATCH exe0.exe +PREPARE ADD distribution/extensions/extensions1/extensions1text0 +PREPARE PATCH distribution/extensions/extensions1/extensions1png1.png +PREPARE PATCH distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD distribution/extensions/extensions0/extensions0text0 +PREPARE PATCH distribution/extensions/extensions0/extensions0png1.png +PREPARE PATCH distribution/extensions/extensions0/extensions0png0.png +PREPARE PATCH 0/0exe0.exe +PREPARE ADD 0/00/00text0 +PREPARE PATCH 0/00/00png0.png +PREPARE ADD 2/20/20text0 +PREPARE ADD 2/20/20png0.png +PREPARE ADD 0/00/00text2 +PREPARE REMOVEFILE 1/10/10text0 +PREPARE REMOVEFILE 0/00/00text1 +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/98/ +PREPARE REMOVEFILE 9/97/970/97xtext0 +PREPARE REMOVEFILE 9/97/970/97xtext1 +PREPARE REMOVEDIR 9/97/970/ +PREPARE REMOVEFILE 9/97/971/97xtext0 +PREPARE REMOVEFILE 9/97/971/97xtext1 +PREPARE REMOVEDIR 9/97/971/ +PREPARE REMOVEDIR 9/97/ +PREPARE REMOVEFILE 9/96/96text0 +PREPARE REMOVEFILE 9/96/96text1 +PREPARE REMOVEDIR 9/96/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/93/ +PREPARE REMOVEDIR 9/92/ +PREPARE REMOVEDIR 9/91/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/88/ +PREPARE REMOVEFILE 8/87/870/87xtext0 +PREPARE REMOVEFILE 8/87/870/87xtext1 +PREPARE REMOVEDIR 8/87/870/ +PREPARE REMOVEFILE 8/87/871/87xtext0 +PREPARE REMOVEFILE 8/87/871/87xtext1 +PREPARE REMOVEDIR 8/87/871/ +PREPARE REMOVEDIR 8/87/ +PREPARE REMOVEFILE 8/86/86text0 +PREPARE REMOVEFILE 8/86/86text1 +PREPARE REMOVEDIR 8/86/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/83/ +PREPARE REMOVEDIR 8/82/ +PREPARE REMOVEDIR 8/81/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEFILE 7/70/7xtest.exe +PREPARE REMOVEFILE 7/70/7xtext0 +PREPARE REMOVEFILE 7/70/7xtext1 +PREPARE REMOVEDIR 7/70/ +PREPARE REMOVEFILE 7/71/7xtest.exe +PREPARE REMOVEFILE 7/71/7xtext0 +PREPARE REMOVEFILE 7/71/7xtext1 +PREPARE REMOVEDIR 7/71/ +PREPARE REMOVEFILE 7/7text0 +PREPARE REMOVEFILE 7/7text1 +PREPARE REMOVEDIR 7/ +PREPARE REMOVEDIR 6/ +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5test.exe +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEDIR 5/ +PREPARE REMOVEFILE 4/4text1 +PREPARE REMOVEFILE 4/4text0 +PREPARE REMOVEDIR 4/ +PREPARE REMOVEFILE 3/3text1 +PREPARE REMOVEFILE 3/3text0 +PREPARE REMOVEDIR 1/10/ +PREPARE REMOVEDIR 1/ +EXECUTE ADD searchplugins/searchpluginstext0 +EXECUTE PATCH searchplugins/searchpluginspng1.png +EXECUTE PATCH searchplugins/searchpluginspng0.png +EXECUTE ADD precomplete +EXECUTE PATCH exe0.exe +EXECUTE ADD distribution/extensions/extensions1/extensions1text0 +EXECUTE PATCH distribution/extensions/extensions1/extensions1png1.png +EXECUTE PATCH distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD distribution/extensions/extensions0/extensions0text0 +EXECUTE PATCH distribution/extensions/extensions0/extensions0png1.png +EXECUTE PATCH distribution/extensions/extensions0/extensions0png0.png +EXECUTE PATCH 0/0exe0.exe +LoadSourceFile: destination file size 776 does not match expected size 79872 +LoadSourceFile failed +### execution failed +FINISH ADD searchplugins/searchpluginstext0 +FINISH PATCH searchplugins/searchpluginspng1.png +FINISH PATCH searchplugins/searchpluginspng0.png +FINISH ADD precomplete +FINISH PATCH exe0.exe +FINISH ADD distribution/extensions/extensions1/extensions1text0 +backup_restore: backup file doesn't exist: distribution/extensions/extensions1/extensions1text0.moz-backup +FINISH PATCH distribution/extensions/extensions1/extensions1png1.png +FINISH PATCH distribution/extensions/extensions1/extensions1png0.png +FINISH ADD distribution/extensions/extensions0/extensions0text0 +FINISH PATCH distribution/extensions/extensions0/extensions0png1.png +FINISH PATCH distribution/extensions/extensions0/extensions0png0.png +FINISH PATCH 0/0exe0.exe +backup_restore: backup file doesn't exist: 0/0exe0.exe.moz-backup +FINISH ADD 0/00/00text0 +backup_restore: backup file doesn't exist: 0/00/00text0.moz-backup +FINISH PATCH 0/00/00png0.png +backup_restore: backup file doesn't exist: 0/00/00png0.png.moz-backup +FINISH ADD 2/20/20text0 +backup_restore: backup file doesn't exist: 2/20/20text0.moz-backup +FINISH ADD 2/20/20png0.png +backup_restore: backup file doesn't exist: 2/20/20png0.png.moz-backup +FINISH ADD 0/00/00text2 +backup_restore: backup file doesn't exist: 0/00/00text2.moz-backup +FINISH REMOVEFILE 1/10/10text0 +backup_restore: backup file doesn't exist: 1/10/10text0.moz-backup +FINISH REMOVEFILE 0/00/00text1 +backup_restore: backup file doesn't exist: 0/00/00text1.moz-backup +FINISH REMOVEFILE 9/97/970/97xtext0 +backup_restore: backup file doesn't exist: 9/97/970/97xtext0.moz-backup +FINISH REMOVEFILE 9/97/970/97xtext1 +backup_restore: backup file doesn't exist: 9/97/970/97xtext1.moz-backup +FINISH REMOVEFILE 9/97/971/97xtext0 +backup_restore: backup file doesn't exist: 9/97/971/97xtext0.moz-backup +FINISH REMOVEFILE 9/97/971/97xtext1 +backup_restore: backup file doesn't exist: 9/97/971/97xtext1.moz-backup +FINISH REMOVEFILE 9/96/96text0 +backup_restore: backup file doesn't exist: 9/96/96text0.moz-backup +FINISH REMOVEFILE 9/96/96text1 +backup_restore: backup file doesn't exist: 9/96/96text1.moz-backup +FINISH REMOVEFILE 8/87/870/87xtext0 +backup_restore: backup file doesn't exist: 8/87/870/87xtext0.moz-backup +FINISH REMOVEFILE 8/87/870/87xtext1 +backup_restore: backup file doesn't exist: 8/87/870/87xtext1.moz-backup +FINISH REMOVEFILE 8/87/871/87xtext0 +backup_restore: backup file doesn't exist: 8/87/871/87xtext0.moz-backup +FINISH REMOVEFILE 8/87/871/87xtext1 +backup_restore: backup file doesn't exist: 8/87/871/87xtext1.moz-backup +FINISH REMOVEFILE 8/86/86text0 +backup_restore: backup file doesn't exist: 8/86/86text0.moz-backup +FINISH REMOVEFILE 8/86/86text1 +backup_restore: backup file doesn't exist: 8/86/86text1.moz-backup +FINISH REMOVEFILE 7/70/7xtest.exe +backup_restore: backup file doesn't exist: 7/70/7xtest.exe.moz-backup +FINISH REMOVEFILE 7/70/7xtext0 +backup_restore: backup file doesn't exist: 7/70/7xtext0.moz-backup +FINISH REMOVEFILE 7/70/7xtext1 +backup_restore: backup file doesn't exist: 7/70/7xtext1.moz-backup +FINISH REMOVEFILE 7/71/7xtest.exe +backup_restore: backup file doesn't exist: 7/71/7xtest.exe.moz-backup +FINISH REMOVEFILE 7/71/7xtext0 +backup_restore: backup file doesn't exist: 7/71/7xtext0.moz-backup +FINISH REMOVEFILE 7/71/7xtext1 +backup_restore: backup file doesn't exist: 7/71/7xtext1.moz-backup +FINISH REMOVEFILE 7/7text0 +backup_restore: backup file doesn't exist: 7/7text0.moz-backup +FINISH REMOVEFILE 7/7text1 +backup_restore: backup file doesn't exist: 7/7text1.moz-backup +FINISH REMOVEFILE 5/5text1 +backup_restore: backup file doesn't exist: 5/5text1.moz-backup +FINISH REMOVEFILE 5/5text0 +backup_restore: backup file doesn't exist: 5/5text0.moz-backup +FINISH REMOVEFILE 5/5test.exe +backup_restore: backup file doesn't exist: 5/5test.exe.moz-backup +FINISH REMOVEFILE 5/5text0 +backup_restore: backup file doesn't exist: 5/5text0.moz-backup +FINISH REMOVEFILE 5/5text1 +backup_restore: backup file doesn't exist: 5/5text1.moz-backup +FINISH REMOVEFILE 4/4text1 +backup_restore: backup file doesn't exist: 4/4text1.moz-backup +FINISH REMOVEFILE 4/4text0 +backup_restore: backup file doesn't exist: 4/4text0.moz-backup +FINISH REMOVEFILE 3/3text1 +backup_restore: backup file doesn't exist: 3/3text1.moz-backup +FINISH REMOVEFILE 3/3text0 +backup_restore: backup file doesn't exist: 3/3text0.moz-backup +failed: 2 +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/partial_log_success_mac b/toolkit/mozapps/update/tests/data/partial_log_success_mac new file mode 100644 index 0000000000..fb5272ad2c --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_log_success_mac @@ -0,0 +1,279 @@ +UPDATE TYPE partial +PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0 +PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng1.png +PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng0.png +PREPARE ADD Contents/Resources/precomplete +PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +PREPARE PATCH Contents/Resources/0/0exe0.exe +PREPARE ADD Contents/Resources/0/00/00text0 +PREPARE PATCH Contents/Resources/0/00/00png0.png +PREPARE PATCH Contents/MacOS/exe0.exe +PREPARE ADD Contents/Resources/2/20/20text0 +PREPARE ADD Contents/Resources/2/20/20png0.png +PREPARE ADD Contents/Resources/0/00/00text2 +PREPARE REMOVEFILE Contents/Resources/1/10/10text0 +PREPARE REMOVEFILE Contents/Resources/0/00/00text1 +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/99/ +PREPARE REMOVEDIR Contents/Resources/9/98/ +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/970/ +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +PREPARE REMOVEDIR Contents/Resources/9/97/971/ +PREPARE REMOVEDIR Contents/Resources/9/97/ +PREPARE REMOVEFILE Contents/Resources/9/96/96text0 +PREPARE REMOVEFILE Contents/Resources/9/96/96text1 +PREPARE REMOVEDIR Contents/Resources/9/96/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/95/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/94/ +PREPARE REMOVEDIR Contents/Resources/9/93/ +PREPARE REMOVEDIR Contents/Resources/9/92/ +PREPARE REMOVEDIR Contents/Resources/9/91/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/9/90/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/89/ +PREPARE REMOVEDIR Contents/Resources/8/88/ +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/870/ +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +PREPARE REMOVEDIR Contents/Resources/8/87/871/ +PREPARE REMOVEDIR Contents/Resources/8/87/ +PREPARE REMOVEFILE Contents/Resources/8/86/86text0 +PREPARE REMOVEFILE Contents/Resources/8/86/86text1 +PREPARE REMOVEDIR Contents/Resources/8/86/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/85/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/84/ +PREPARE REMOVEDIR Contents/Resources/8/83/ +PREPARE REMOVEDIR Contents/Resources/8/82/ +PREPARE REMOVEDIR Contents/Resources/8/81/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEDIR Contents/Resources/8/80/ +PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/70/ +PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0 +PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1 +PREPARE REMOVEDIR Contents/Resources/7/71/ +PREPARE REMOVEFILE Contents/Resources/7/7text0 +PREPARE REMOVEFILE Contents/Resources/7/7text1 +PREPARE REMOVEDIR Contents/Resources/7/ +PREPARE REMOVEDIR Contents/Resources/6/ +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5test.exe +PREPARE REMOVEFILE Contents/Resources/5/5text0 +PREPARE REMOVEFILE Contents/Resources/5/5text1 +PREPARE REMOVEDIR Contents/Resources/5/ +PREPARE REMOVEFILE Contents/Resources/4/4text1 +PREPARE REMOVEFILE Contents/Resources/4/4text0 +PREPARE REMOVEDIR Contents/Resources/4/ +PREPARE REMOVEFILE Contents/Resources/3/3text1 +PREPARE REMOVEFILE Contents/Resources/3/3text0 +PREPARE REMOVEDIR Contents/Resources/1/10/ +PREPARE REMOVEDIR Contents/Resources/1/ +EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0 +EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng1.png +EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng0.png +EXECUTE ADD Contents/Resources/precomplete +EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +EXECUTE PATCH Contents/Resources/0/0exe0.exe +EXECUTE ADD Contents/Resources/0/00/00text0 +EXECUTE PATCH Contents/Resources/0/00/00png0.png +EXECUTE PATCH Contents/MacOS/exe0.exe +EXECUTE ADD Contents/Resources/2/20/20text0 +EXECUTE ADD Contents/Resources/2/20/20png0.png +EXECUTE ADD Contents/Resources/0/00/00text2 +EXECUTE REMOVEFILE Contents/Resources/1/10/10text0 +EXECUTE REMOVEFILE Contents/Resources/0/00/00text1 +EXECUTE REMOVEDIR Contents/Resources/9/99/ +EXECUTE REMOVEDIR Contents/Resources/9/99/ +EXECUTE REMOVEDIR Contents/Resources/9/98/ +EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext0 +EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext1 +EXECUTE REMOVEDIR Contents/Resources/9/97/970/ +EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext0 +EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext1 +EXECUTE REMOVEDIR Contents/Resources/9/97/971/ +EXECUTE REMOVEDIR Contents/Resources/9/97/ +EXECUTE REMOVEFILE Contents/Resources/9/96/96text0 +EXECUTE REMOVEFILE Contents/Resources/9/96/96text1 +EXECUTE REMOVEDIR Contents/Resources/9/96/ +EXECUTE REMOVEDIR Contents/Resources/9/95/ +EXECUTE REMOVEDIR Contents/Resources/9/95/ +EXECUTE REMOVEDIR Contents/Resources/9/94/ +EXECUTE REMOVEDIR Contents/Resources/9/94/ +EXECUTE REMOVEDIR Contents/Resources/9/93/ +EXECUTE REMOVEDIR Contents/Resources/9/92/ +EXECUTE REMOVEDIR Contents/Resources/9/91/ +EXECUTE REMOVEDIR Contents/Resources/9/90/ +EXECUTE REMOVEDIR Contents/Resources/9/90/ +EXECUTE REMOVEDIR Contents/Resources/8/89/ +EXECUTE REMOVEDIR Contents/Resources/8/89/ +EXECUTE REMOVEDIR Contents/Resources/8/88/ +EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext0 +EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext1 +EXECUTE REMOVEDIR Contents/Resources/8/87/870/ +EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext0 +EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext1 +EXECUTE REMOVEDIR Contents/Resources/8/87/871/ +EXECUTE REMOVEDIR Contents/Resources/8/87/ +EXECUTE REMOVEFILE Contents/Resources/8/86/86text0 +EXECUTE REMOVEFILE Contents/Resources/8/86/86text1 +EXECUTE REMOVEDIR Contents/Resources/8/86/ +EXECUTE REMOVEDIR Contents/Resources/8/85/ +EXECUTE REMOVEDIR Contents/Resources/8/85/ +EXECUTE REMOVEDIR Contents/Resources/8/84/ +EXECUTE REMOVEDIR Contents/Resources/8/84/ +EXECUTE REMOVEDIR Contents/Resources/8/83/ +EXECUTE REMOVEDIR Contents/Resources/8/82/ +EXECUTE REMOVEDIR Contents/Resources/8/81/ +EXECUTE REMOVEDIR Contents/Resources/8/80/ +EXECUTE REMOVEDIR Contents/Resources/8/80/ +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtest.exe +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext0 +EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext1 +EXECUTE REMOVEDIR Contents/Resources/7/70/ +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtest.exe +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext0 +EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext1 +EXECUTE REMOVEDIR Contents/Resources/7/71/ +EXECUTE REMOVEFILE Contents/Resources/7/7text0 +EXECUTE REMOVEFILE Contents/Resources/7/7text1 +EXECUTE REMOVEDIR Contents/Resources/7/ +EXECUTE REMOVEDIR Contents/Resources/6/ +EXECUTE REMOVEFILE Contents/Resources/5/5text1 +EXECUTE REMOVEFILE Contents/Resources/5/5text0 +EXECUTE REMOVEFILE Contents/Resources/5/5test.exe +EXECUTE REMOVEFILE Contents/Resources/5/5text0 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEFILE Contents/Resources/5/5text1 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEDIR Contents/Resources/5/ +EXECUTE REMOVEFILE Contents/Resources/4/4text1 +EXECUTE REMOVEFILE Contents/Resources/4/4text0 +EXECUTE REMOVEDIR Contents/Resources/4/ +EXECUTE REMOVEFILE Contents/Resources/3/3text1 +EXECUTE REMOVEFILE Contents/Resources/3/3text0 +EXECUTE REMOVEDIR Contents/Resources/1/10/ +EXECUTE REMOVEDIR Contents/Resources/1/ +FINISH ADD Contents/Resources/searchplugins/searchpluginstext0 +FINISH PATCH Contents/Resources/searchplugins/searchpluginspng1.png +FINISH PATCH Contents/Resources/searchplugins/searchpluginspng0.png +FINISH ADD Contents/Resources/precomplete +FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0 +FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png +FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png +FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0 +FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png +FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png +FINISH PATCH Contents/Resources/0/0exe0.exe +FINISH ADD Contents/Resources/0/00/00text0 +FINISH PATCH Contents/Resources/0/00/00png0.png +FINISH PATCH Contents/MacOS/exe0.exe +FINISH ADD Contents/Resources/2/20/20text0 +FINISH ADD Contents/Resources/2/20/20png0.png +FINISH ADD Contents/Resources/0/00/00text2 +FINISH REMOVEFILE Contents/Resources/1/10/10text0 +FINISH REMOVEFILE Contents/Resources/0/00/00text1 +FINISH REMOVEDIR Contents/Resources/9/99/ +FINISH REMOVEDIR Contents/Resources/9/99/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/98/ +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0 +FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1 +FINISH REMOVEDIR Contents/Resources/9/97/970/ +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0 +FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1 +FINISH REMOVEDIR Contents/Resources/9/97/971/ +FINISH REMOVEDIR Contents/Resources/9/97/ +FINISH REMOVEFILE Contents/Resources/9/96/96text0 +FINISH REMOVEFILE Contents/Resources/9/96/96text1 +FINISH REMOVEDIR Contents/Resources/9/96/ +FINISH REMOVEDIR Contents/Resources/9/95/ +FINISH REMOVEDIR Contents/Resources/9/95/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/94/ +FINISH REMOVEDIR Contents/Resources/9/94/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/9/93/ +FINISH REMOVEDIR Contents/Resources/9/92/ +removing directory: Contents/Resources/9/92/, rv: 0 +FINISH REMOVEDIR Contents/Resources/9/91/ +removing directory: Contents/Resources/9/91/, rv: 0 +FINISH REMOVEDIR Contents/Resources/9/90/ +FINISH REMOVEDIR Contents/Resources/9/90/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/89/ +FINISH REMOVEDIR Contents/Resources/8/89/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/88/ +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0 +FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1 +FINISH REMOVEDIR Contents/Resources/8/87/870/ +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0 +FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1 +FINISH REMOVEDIR Contents/Resources/8/87/871/ +FINISH REMOVEDIR Contents/Resources/8/87/ +FINISH REMOVEFILE Contents/Resources/8/86/86text0 +FINISH REMOVEFILE Contents/Resources/8/86/86text1 +FINISH REMOVEDIR Contents/Resources/8/86/ +FINISH REMOVEDIR Contents/Resources/8/85/ +FINISH REMOVEDIR Contents/Resources/8/85/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/84/ +FINISH REMOVEDIR Contents/Resources/8/84/ +directory no longer exists; skipping +FINISH REMOVEDIR Contents/Resources/8/83/ +FINISH REMOVEDIR Contents/Resources/8/82/ +removing directory: Contents/Resources/8/82/, rv: 0 +FINISH REMOVEDIR Contents/Resources/8/81/ +removing directory: Contents/Resources/8/81/, rv: 0 +FINISH REMOVEDIR Contents/Resources/8/80/ +FINISH REMOVEDIR Contents/Resources/8/80/ +directory no longer exists; skipping +FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe +FINISH REMOVEFILE Contents/Resources/7/70/7xtext0 +FINISH REMOVEFILE Contents/Resources/7/70/7xtext1 +FINISH REMOVEDIR Contents/Resources/7/70/ +FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe +FINISH REMOVEFILE Contents/Resources/7/71/7xtext0 +FINISH REMOVEFILE Contents/Resources/7/71/7xtext1 +FINISH REMOVEDIR Contents/Resources/7/71/ +FINISH REMOVEFILE Contents/Resources/7/7text0 +FINISH REMOVEFILE Contents/Resources/7/7text1 +FINISH REMOVEDIR Contents/Resources/7/ +FINISH REMOVEDIR Contents/Resources/6/ +FINISH REMOVEFILE Contents/Resources/5/5text1 +FINISH REMOVEFILE Contents/Resources/5/5text0 +FINISH REMOVEFILE Contents/Resources/5/5test.exe +FINISH REMOVEDIR Contents/Resources/5/ +FINISH REMOVEFILE Contents/Resources/4/4text1 +FINISH REMOVEFILE Contents/Resources/4/4text0 +FINISH REMOVEDIR Contents/Resources/4/ +FINISH REMOVEFILE Contents/Resources/3/3text1 +FINISH REMOVEFILE Contents/Resources/3/3text0 +FINISH REMOVEDIR Contents/Resources/1/10/ +FINISH REMOVEDIR Contents/Resources/1/ +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/partial_log_success_win b/toolkit/mozapps/update/tests/data/partial_log_success_win new file mode 100644 index 0000000000..1f5c4b3b49 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_log_success_win @@ -0,0 +1,279 @@ +UPDATE TYPE partial +PREPARE ADD searchplugins/searchpluginstext0 +PREPARE PATCH searchplugins/searchpluginspng1.png +PREPARE PATCH searchplugins/searchpluginspng0.png +PREPARE ADD precomplete +PREPARE PATCH exe0.exe +PREPARE ADD distribution/extensions/extensions1/extensions1text0 +PREPARE PATCH distribution/extensions/extensions1/extensions1png1.png +PREPARE PATCH distribution/extensions/extensions1/extensions1png0.png +PREPARE ADD distribution/extensions/extensions0/extensions0text0 +PREPARE PATCH distribution/extensions/extensions0/extensions0png1.png +PREPARE PATCH distribution/extensions/extensions0/extensions0png0.png +PREPARE PATCH 0/0exe0.exe +PREPARE ADD 0/00/00text0 +PREPARE PATCH 0/00/00png0.png +PREPARE ADD 2/20/20text0 +PREPARE ADD 2/20/20png0.png +PREPARE ADD 0/00/00text2 +PREPARE REMOVEFILE 1/10/10text0 +PREPARE REMOVEFILE 0/00/00text1 +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/99/ +PREPARE REMOVEDIR 9/98/ +PREPARE REMOVEFILE 9/97/970/97xtext0 +PREPARE REMOVEFILE 9/97/970/97xtext1 +PREPARE REMOVEDIR 9/97/970/ +PREPARE REMOVEFILE 9/97/971/97xtext0 +PREPARE REMOVEFILE 9/97/971/97xtext1 +PREPARE REMOVEDIR 9/97/971/ +PREPARE REMOVEDIR 9/97/ +PREPARE REMOVEFILE 9/96/96text0 +PREPARE REMOVEFILE 9/96/96text1 +PREPARE REMOVEDIR 9/96/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/95/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/94/ +PREPARE REMOVEDIR 9/93/ +PREPARE REMOVEDIR 9/92/ +PREPARE REMOVEDIR 9/91/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 9/90/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/89/ +PREPARE REMOVEDIR 8/88/ +PREPARE REMOVEFILE 8/87/870/87xtext0 +PREPARE REMOVEFILE 8/87/870/87xtext1 +PREPARE REMOVEDIR 8/87/870/ +PREPARE REMOVEFILE 8/87/871/87xtext0 +PREPARE REMOVEFILE 8/87/871/87xtext1 +PREPARE REMOVEDIR 8/87/871/ +PREPARE REMOVEDIR 8/87/ +PREPARE REMOVEFILE 8/86/86text0 +PREPARE REMOVEFILE 8/86/86text1 +PREPARE REMOVEDIR 8/86/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/85/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/84/ +PREPARE REMOVEDIR 8/83/ +PREPARE REMOVEDIR 8/82/ +PREPARE REMOVEDIR 8/81/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEDIR 8/80/ +PREPARE REMOVEFILE 7/70/7xtest.exe +PREPARE REMOVEFILE 7/70/7xtext0 +PREPARE REMOVEFILE 7/70/7xtext1 +PREPARE REMOVEDIR 7/70/ +PREPARE REMOVEFILE 7/71/7xtest.exe +PREPARE REMOVEFILE 7/71/7xtext0 +PREPARE REMOVEFILE 7/71/7xtext1 +PREPARE REMOVEDIR 7/71/ +PREPARE REMOVEFILE 7/7text0 +PREPARE REMOVEFILE 7/7text1 +PREPARE REMOVEDIR 7/ +PREPARE REMOVEDIR 6/ +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5test.exe +PREPARE REMOVEFILE 5/5text0 +PREPARE REMOVEFILE 5/5text1 +PREPARE REMOVEDIR 5/ +PREPARE REMOVEFILE 4/4text1 +PREPARE REMOVEFILE 4/4text0 +PREPARE REMOVEDIR 4/ +PREPARE REMOVEFILE 3/3text1 +PREPARE REMOVEFILE 3/3text0 +PREPARE REMOVEDIR 1/10/ +PREPARE REMOVEDIR 1/ +EXECUTE ADD searchplugins/searchpluginstext0 +EXECUTE PATCH searchplugins/searchpluginspng1.png +EXECUTE PATCH searchplugins/searchpluginspng0.png +EXECUTE ADD precomplete +EXECUTE PATCH exe0.exe +EXECUTE ADD distribution/extensions/extensions1/extensions1text0 +EXECUTE PATCH distribution/extensions/extensions1/extensions1png1.png +EXECUTE PATCH distribution/extensions/extensions1/extensions1png0.png +EXECUTE ADD distribution/extensions/extensions0/extensions0text0 +EXECUTE PATCH distribution/extensions/extensions0/extensions0png1.png +EXECUTE PATCH distribution/extensions/extensions0/extensions0png0.png +EXECUTE PATCH 0/0exe0.exe +EXECUTE ADD 0/00/00text0 +EXECUTE PATCH 0/00/00png0.png +EXECUTE ADD 2/20/20text0 +EXECUTE ADD 2/20/20png0.png +EXECUTE ADD 0/00/00text2 +EXECUTE REMOVEFILE 1/10/10text0 +EXECUTE REMOVEFILE 0/00/00text1 +EXECUTE REMOVEDIR 9/99/ +EXECUTE REMOVEDIR 9/99/ +EXECUTE REMOVEDIR 9/98/ +EXECUTE REMOVEFILE 9/97/970/97xtext0 +EXECUTE REMOVEFILE 9/97/970/97xtext1 +EXECUTE REMOVEDIR 9/97/970/ +EXECUTE REMOVEFILE 9/97/971/97xtext0 +EXECUTE REMOVEFILE 9/97/971/97xtext1 +EXECUTE REMOVEDIR 9/97/971/ +EXECUTE REMOVEDIR 9/97/ +EXECUTE REMOVEFILE 9/96/96text0 +EXECUTE REMOVEFILE 9/96/96text1 +EXECUTE REMOVEDIR 9/96/ +EXECUTE REMOVEDIR 9/95/ +EXECUTE REMOVEDIR 9/95/ +EXECUTE REMOVEDIR 9/94/ +EXECUTE REMOVEDIR 9/94/ +EXECUTE REMOVEDIR 9/93/ +EXECUTE REMOVEDIR 9/92/ +EXECUTE REMOVEDIR 9/91/ +EXECUTE REMOVEDIR 9/90/ +EXECUTE REMOVEDIR 9/90/ +EXECUTE REMOVEDIR 8/89/ +EXECUTE REMOVEDIR 8/89/ +EXECUTE REMOVEDIR 8/88/ +EXECUTE REMOVEFILE 8/87/870/87xtext0 +EXECUTE REMOVEFILE 8/87/870/87xtext1 +EXECUTE REMOVEDIR 8/87/870/ +EXECUTE REMOVEFILE 8/87/871/87xtext0 +EXECUTE REMOVEFILE 8/87/871/87xtext1 +EXECUTE REMOVEDIR 8/87/871/ +EXECUTE REMOVEDIR 8/87/ +EXECUTE REMOVEFILE 8/86/86text0 +EXECUTE REMOVEFILE 8/86/86text1 +EXECUTE REMOVEDIR 8/86/ +EXECUTE REMOVEDIR 8/85/ +EXECUTE REMOVEDIR 8/85/ +EXECUTE REMOVEDIR 8/84/ +EXECUTE REMOVEDIR 8/84/ +EXECUTE REMOVEDIR 8/83/ +EXECUTE REMOVEDIR 8/82/ +EXECUTE REMOVEDIR 8/81/ +EXECUTE REMOVEDIR 8/80/ +EXECUTE REMOVEDIR 8/80/ +EXECUTE REMOVEFILE 7/70/7xtest.exe +EXECUTE REMOVEFILE 7/70/7xtext0 +EXECUTE REMOVEFILE 7/70/7xtext1 +EXECUTE REMOVEDIR 7/70/ +EXECUTE REMOVEFILE 7/71/7xtest.exe +EXECUTE REMOVEFILE 7/71/7xtext0 +EXECUTE REMOVEFILE 7/71/7xtext1 +EXECUTE REMOVEDIR 7/71/ +EXECUTE REMOVEFILE 7/7text0 +EXECUTE REMOVEFILE 7/7text1 +EXECUTE REMOVEDIR 7/ +EXECUTE REMOVEDIR 6/ +EXECUTE REMOVEFILE 5/5text1 +EXECUTE REMOVEFILE 5/5text0 +EXECUTE REMOVEFILE 5/5test.exe +EXECUTE REMOVEFILE 5/5text0 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEFILE 5/5text1 +file cannot be removed because it does not exist; skipping +EXECUTE REMOVEDIR 5/ +EXECUTE REMOVEFILE 4/4text1 +EXECUTE REMOVEFILE 4/4text0 +EXECUTE REMOVEDIR 4/ +EXECUTE REMOVEFILE 3/3text1 +EXECUTE REMOVEFILE 3/3text0 +EXECUTE REMOVEDIR 1/10/ +EXECUTE REMOVEDIR 1/ +FINISH ADD searchplugins/searchpluginstext0 +FINISH PATCH searchplugins/searchpluginspng1.png +FINISH PATCH searchplugins/searchpluginspng0.png +FINISH ADD precomplete +FINISH PATCH exe0.exe +FINISH ADD distribution/extensions/extensions1/extensions1text0 +FINISH PATCH distribution/extensions/extensions1/extensions1png1.png +FINISH PATCH distribution/extensions/extensions1/extensions1png0.png +FINISH ADD distribution/extensions/extensions0/extensions0text0 +FINISH PATCH distribution/extensions/extensions0/extensions0png1.png +FINISH PATCH distribution/extensions/extensions0/extensions0png0.png +FINISH PATCH 0/0exe0.exe +FINISH ADD 0/00/00text0 +FINISH PATCH 0/00/00png0.png +FINISH ADD 2/20/20text0 +FINISH ADD 2/20/20png0.png +FINISH ADD 0/00/00text2 +FINISH REMOVEFILE 1/10/10text0 +FINISH REMOVEFILE 0/00/00text1 +FINISH REMOVEDIR 9/99/ +FINISH REMOVEDIR 9/99/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/98/ +FINISH REMOVEFILE 9/97/970/97xtext0 +FINISH REMOVEFILE 9/97/970/97xtext1 +FINISH REMOVEDIR 9/97/970/ +FINISH REMOVEFILE 9/97/971/97xtext0 +FINISH REMOVEFILE 9/97/971/97xtext1 +FINISH REMOVEDIR 9/97/971/ +FINISH REMOVEDIR 9/97/ +FINISH REMOVEFILE 9/96/96text0 +FINISH REMOVEFILE 9/96/96text1 +FINISH REMOVEDIR 9/96/ +FINISH REMOVEDIR 9/95/ +FINISH REMOVEDIR 9/95/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/94/ +FINISH REMOVEDIR 9/94/ +directory no longer exists; skipping +FINISH REMOVEDIR 9/93/ +FINISH REMOVEDIR 9/92/ +removing directory: 9/92/, rv: 0 +FINISH REMOVEDIR 9/91/ +removing directory: 9/91/, rv: 0 +FINISH REMOVEDIR 9/90/ +FINISH REMOVEDIR 9/90/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/89/ +FINISH REMOVEDIR 8/89/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/88/ +FINISH REMOVEFILE 8/87/870/87xtext0 +FINISH REMOVEFILE 8/87/870/87xtext1 +FINISH REMOVEDIR 8/87/870/ +FINISH REMOVEFILE 8/87/871/87xtext0 +FINISH REMOVEFILE 8/87/871/87xtext1 +FINISH REMOVEDIR 8/87/871/ +FINISH REMOVEDIR 8/87/ +FINISH REMOVEFILE 8/86/86text0 +FINISH REMOVEFILE 8/86/86text1 +FINISH REMOVEDIR 8/86/ +FINISH REMOVEDIR 8/85/ +FINISH REMOVEDIR 8/85/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/84/ +FINISH REMOVEDIR 8/84/ +directory no longer exists; skipping +FINISH REMOVEDIR 8/83/ +FINISH REMOVEDIR 8/82/ +removing directory: 8/82/, rv: 0 +FINISH REMOVEDIR 8/81/ +removing directory: 8/81/, rv: 0 +FINISH REMOVEDIR 8/80/ +FINISH REMOVEDIR 8/80/ +directory no longer exists; skipping +FINISH REMOVEFILE 7/70/7xtest.exe +FINISH REMOVEFILE 7/70/7xtext0 +FINISH REMOVEFILE 7/70/7xtext1 +FINISH REMOVEDIR 7/70/ +FINISH REMOVEFILE 7/71/7xtest.exe +FINISH REMOVEFILE 7/71/7xtext0 +FINISH REMOVEFILE 7/71/7xtext1 +FINISH REMOVEDIR 7/71/ +FINISH REMOVEFILE 7/7text0 +FINISH REMOVEFILE 7/7text1 +FINISH REMOVEDIR 7/ +FINISH REMOVEDIR 6/ +FINISH REMOVEFILE 5/5text1 +FINISH REMOVEFILE 5/5text0 +FINISH REMOVEFILE 5/5test.exe +FINISH REMOVEDIR 5/ +FINISH REMOVEFILE 4/4text1 +FINISH REMOVEFILE 4/4text0 +FINISH REMOVEDIR 4/ +FINISH REMOVEFILE 3/3text1 +FINISH REMOVEFILE 3/3text0 +FINISH REMOVEDIR 1/10/ +FINISH REMOVEDIR 1/ +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/partial_mac.mar b/toolkit/mozapps/update/tests/data/partial_mac.mar Binary files differnew file mode 100644 index 0000000000..bcc04b9939 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_mac.mar diff --git a/toolkit/mozapps/update/tests/data/partial_precomplete b/toolkit/mozapps/update/tests/data/partial_precomplete new file mode 100644 index 0000000000..3ec201463a --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_precomplete @@ -0,0 +1,19 @@ +remove "searchplugins/searchpluginstext0" +remove "searchplugins/searchpluginspng1.png" +remove "searchplugins/searchpluginspng0.png" +remove "removed-files" +remove "precomplete" +remove "exe0.exe" +remove "2/20/20text0" +remove "2/20/20png0.png" +remove "0/0exe0.exe" +remove "0/00/00text2" +remove "0/00/00text0" +remove "0/00/00png0.png" +rmdir "searchplugins/" +rmdir "defaults/pref/" +rmdir "defaults/" +rmdir "2/20/" +rmdir "2/" +rmdir "0/00/" +rmdir "0/" diff --git a/toolkit/mozapps/update/tests/data/partial_precomplete_mac b/toolkit/mozapps/update/tests/data/partial_precomplete_mac new file mode 100644 index 0000000000..c65b6e4e38 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_precomplete_mac @@ -0,0 +1,22 @@ +remove "Contents/Resources/searchplugins/searchpluginstext0" +remove "Contents/Resources/searchplugins/searchpluginspng1.png" +remove "Contents/Resources/searchplugins/searchpluginspng0.png" +remove "Contents/Resources/removed-files" +remove "Contents/Resources/precomplete" +remove "Contents/Resources/2/20/20text0" +remove "Contents/Resources/2/20/20png0.png" +remove "Contents/Resources/0/0exe0.exe" +remove "Contents/Resources/0/00/00text2" +remove "Contents/Resources/0/00/00text0" +remove "Contents/Resources/0/00/00png0.png" +remove "Contents/MacOS/exe0.exe" +rmdir "Contents/Resources/searchplugins/" +rmdir "Contents/Resources/defaults/pref/" +rmdir "Contents/Resources/defaults/" +rmdir "Contents/Resources/2/20/" +rmdir "Contents/Resources/2/" +rmdir "Contents/Resources/0/00/" +rmdir "Contents/Resources/0/" +rmdir "Contents/Resources/" +rmdir "Contents/MacOS/" +rmdir "Contents/" diff --git a/toolkit/mozapps/update/tests/data/partial_removed-files b/toolkit/mozapps/update/tests/data/partial_removed-files new file mode 100644 index 0000000000..881311b82c --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_removed-files @@ -0,0 +1,41 @@ +a/b/text0 +a/b/text1 +a/b/3/3text0 +a/b/3/3text1 +a/b/4/4exe0.exe +a/b/4/4text0 +a/b/4/4text1 +a/b/4/ +a/b/5/5text0 +a/b/5/5text1 +a/b/5/* +a/b/6/ +a/b/7/* +a/b/8/80/ +a/b/8/81/ +a/b/8/82/ +a/b/8/83/ +a/b/8/84/ +a/b/8/85/* +a/b/8/86/* +a/b/8/87/* +a/b/8/88/* +a/b/8/89/* +a/b/8/80/ +a/b/8/84/* +a/b/8/85/* +a/b/8/89/ +a/b/9/90/ +a/b/9/91/ +a/b/9/92/ +a/b/9/93/ +a/b/9/94/ +a/b/9/95/* +a/b/9/96/* +a/b/9/97/* +a/b/9/98/* +a/b/9/99/* +a/b/9/90/ +a/b/9/94/* +a/b/9/95/* +a/b/9/99/ diff --git a/toolkit/mozapps/update/tests/data/partial_removed-files_mac b/toolkit/mozapps/update/tests/data/partial_removed-files_mac new file mode 100644 index 0000000000..955dc5b340 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_removed-files_mac @@ -0,0 +1,41 @@ +Contents/Resources/text0 +Contents/Resources/text1 +Contents/Resources/3/3text0 +Contents/Resources/3/3text1 +Contents/Resources/4/exe0.exe +Contents/Resources/4/4text0 +Contents/Resources/4/4text1 +Contents/Resources/4/ +Contents/Resources/5/5text0 +Contents/Resources/5/5text1 +Contents/Resources/5/* +Contents/Resources/6/ +Contents/Resources/7/* +Contents/Resources/8/80/ +Contents/Resources/8/81/ +Contents/Resources/8/82/ +Contents/Resources/8/83/ +Contents/Resources/8/84/ +Contents/Resources/8/85/* +Contents/Resources/8/86/* +Contents/Resources/8/87/* +Contents/Resources/8/88/* +Contents/Resources/8/89/* +Contents/Resources/8/80/ +Contents/Resources/8/84/* +Contents/Resources/8/85/* +Contents/Resources/8/89/ +Contents/Resources/9/90/ +Contents/Resources/9/91/ +Contents/Resources/9/92/ +Contents/Resources/9/93/ +Contents/Resources/9/94/ +Contents/Resources/9/95/* +Contents/Resources/9/96/* +Contents/Resources/9/97/* +Contents/Resources/9/98/* +Contents/Resources/9/99/* +Contents/Resources/9/90/ +Contents/Resources/9/94/* +Contents/Resources/9/95/* +Contents/Resources/9/99/ diff --git a/toolkit/mozapps/update/tests/data/partial_update_manifest b/toolkit/mozapps/update/tests/data/partial_update_manifest new file mode 100644 index 0000000000..8d4e60ed25 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/partial_update_manifest @@ -0,0 +1,63 @@ +type "partial" +add "precomplete" +add "a/b/searchplugins/searchpluginstext0" +patch-if "a/b/searchplugins/searchpluginspng1.png" "a/b/searchplugins/searchpluginspng1.png.patch" "a/b/searchplugins/searchpluginspng1.png" +patch-if "a/b/searchplugins/searchpluginspng0.png" "a/b/searchplugins/searchpluginspng0.png.patch" "a/b/searchplugins/searchpluginspng0.png" +add-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1text0" +patch-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1png1.png.patch" "a/b/extensions/extensions1/extensions1png1.png" +patch-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1png0.png.patch" "a/b/extensions/extensions1/extensions1png0.png" +add-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0text0" +patch-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0png1.png.patch" "a/b/extensions/extensions0/extensions0png1.png" +patch-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0png0.png.patch" "a/b/extensions/extensions0/extensions0png0.png" +patch "a/b/exe0.exe.patch" "a/b/exe0.exe" +patch "a/b/0/0exe0.exe.patch" "a/b/0/0exe0.exe" +add "a/b/0/00/00text0" +patch "a/b/0/00/00png0.png.patch" "a/b/0/00/00png0.png" +add "a/b/2/20/20text0" +add "a/b/2/20/20png0.png" +add "a/b/0/00/00text2" +remove "a/b/1/10/10text0" +remove "a/b/0/00/00text1" +remove "a/b/text1" +remove "a/b/text0" +rmrfdir "a/b/9/99/" +rmdir "a/b/9/99/" +rmrfdir "a/b/9/98/" +rmrfdir "a/b/9/97/" +rmrfdir "a/b/9/96/" +rmrfdir "a/b/9/95/" +rmrfdir "a/b/9/95/" +rmrfdir "a/b/9/94/" +rmdir "a/b/9/94/" +rmdir "a/b/9/93/" +rmdir "a/b/9/92/" +rmdir "a/b/9/91/" +rmdir "a/b/9/90/" +rmdir "a/b/9/90/" +rmrfdir "a/b/8/89/" +rmdir "a/b/8/89/" +rmrfdir "a/b/8/88/" +rmrfdir "a/b/8/87/" +rmrfdir "a/b/8/86/" +rmrfdir "a/b/8/85/" +rmrfdir "a/b/8/85/" +rmrfdir "a/b/8/84/" +rmdir "a/b/8/84/" +rmdir "a/b/8/83/" +rmdir "a/b/8/82/" +rmdir "a/b/8/81/" +rmdir "a/b/8/80/" +rmdir "a/b/8/80/" +rmrfdir "a/b/7/" +rmdir "a/b/6/" +remove "a/b/5/5text1" +remove "a/b/5/5text0" +rmrfdir "a/b/5/" +remove "a/b/4/4text1" +remove "a/b/4/4text0" +remove "a/b/4/4exe0.exe" +rmdir "a/b/4/" +remove "a/b/3/3text1" +remove "a/b/3/3text0" +rmdir "a/b/1/10/" +rmdir "a/b/1/" diff --git a/toolkit/mozapps/update/tests/data/replace_log_success b/toolkit/mozapps/update/tests/data/replace_log_success new file mode 100644 index 0000000000..323f1db41e --- /dev/null +++ b/toolkit/mozapps/update/tests/data/replace_log_success @@ -0,0 +1,6 @@ +Performing a replace request +rename_file: proceeding to rename the directory +rename_file: proceeding to rename the directory +Now, remove the tmpDir +succeeded +calling QuitProgressUI diff --git a/toolkit/mozapps/update/tests/data/shared.js b/toolkit/mozapps/update/tests/data/shared.js new file mode 100644 index 0000000000..4826000ae2 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/shared.js @@ -0,0 +1,932 @@ +/* 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/. */ + +/* Shared code for xpcshell, mochitests-chrome, and mochitest-browser-chrome. */ + +// Definitions needed to run eslint on this file. +/* global AppConstants, DATA_URI_SPEC, LOG_FUNCTION */ +/* global Services, URL_HOST, TestUtils */ + +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineModuleGetter( + this, + "ctypes", + "resource://gre/modules/ctypes.jsm" +); +ChromeUtils.defineESModuleGetters(this, { + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", +}); + +const PREF_APP_UPDATE_AUTO = "app.update.auto"; +const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors"; +const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors"; +const PREF_APP_UPDATE_BADGEWAITTIME = "app.update.badgeWaitTime"; +const PREF_APP_UPDATE_BITS_ENABLED = "app.update.BITS.enabled"; +const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations"; +const PREF_APP_UPDATE_CHANNEL = "app.update.channel"; +const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts"; +const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS = "app.update.download.attempts"; +const PREF_APP_UPDATE_DISABLEDFORTESTING = "app.update.disabledForTesting"; +const PREF_APP_UPDATE_INTERVAL = "app.update.interval"; +const PREF_APP_UPDATE_LASTUPDATETIME = + "app.update.lastUpdateTime.background-update-timer"; +const PREF_APP_UPDATE_LOG = "app.update.log"; +const PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD = "app.update.notifyDuringDownload"; +const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime"; +const PREF_APP_UPDATE_RETRYTIMEOUT = "app.update.socket.retryTimeout"; +const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled"; +const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors"; +const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled"; +const PREF_APP_UPDATE_UNSUPPORTED_URL = "app.update.unsupported.url"; +const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details"; +const PREF_APP_UPDATE_URL_MANUAL = "app.update.url.manual"; +const PREF_APP_UPDATE_LANGPACK_ENABLED = "app.update.langpack.enabled"; + +const PREFBRANCH_APP_PARTNER = "app.partner."; +const PREF_DISTRIBUTION_ID = "distribution.id"; +const PREF_DISTRIBUTION_VERSION = "distribution.version"; + +const CONFIG_APP_UPDATE_AUTO = "app.update.auto"; + +const NS_APP_PROFILE_DIR_STARTUP = "ProfDS"; +const NS_APP_USER_PROFILE_50_DIR = "ProfD"; +const NS_GRE_BIN_DIR = "GreBinD"; +const NS_GRE_DIR = "GreD"; +const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD"; +const XRE_EXECUTABLE_FILE = "XREExeF"; +const XRE_OLD_UPDATE_ROOT_DIR = "OldUpdRootD"; +const XRE_UPDATE_ROOT_DIR = "UpdRootD"; + +const DIR_PATCH = "0"; +const DIR_TOBEDELETED = "tobedeleted"; +const DIR_UPDATES = "updates"; +const DIR_UPDATED = + AppConstants.platform == "macosx" ? "Updated.app" : "updated"; +const DIR_DOWNLOADING = "downloading"; + +const FILE_ACTIVE_UPDATE_XML = "active-update.xml"; +const FILE_ACTIVE_UPDATE_XML_TMP = "active-update.xml.tmp"; +const FILE_APPLICATION_INI = "application.ini"; +const FILE_BACKUP_UPDATE_CONFIG_JSON = "backup-update-config.json"; +const FILE_BACKUP_UPDATE_LOG = "backup-update.log"; +const FILE_BT_RESULT = "bt.result"; +const FILE_LAST_UPDATE_LOG = "last-update.log"; +const FILE_PRECOMPLETE = "precomplete"; +const FILE_PRECOMPLETE_BAK = "precomplete.bak"; +const FILE_UPDATE_CONFIG_JSON = "update-config.json"; +const FILE_UPDATE_LOG = "update.log"; +const FILE_UPDATE_MAR = "update.mar"; +const FILE_UPDATE_SETTINGS_INI = "update-settings.ini"; +const FILE_UPDATE_SETTINGS_INI_BAK = "update-settings.ini.bak"; +const FILE_UPDATE_STATUS = "update.status"; +const FILE_UPDATE_TEST = "update.test"; +const FILE_UPDATE_VERSION = "update.version"; +const FILE_UPDATER_INI = "updater.ini"; +const FILE_UPDATES_XML = "updates.xml"; +const FILE_UPDATES_XML_TMP = "updates.xml.tmp"; + +const UPDATE_SETTINGS_CONTENTS = + "[Settings]\nACCEPTED_MAR_CHANNEL_IDS=xpcshell-test\n"; +const PRECOMPLETE_CONTENTS = 'rmdir "nonexistent_dir/"\n'; + +const PR_RDWR = 0x04; +const PR_CREATE_FILE = 0x08; +const PR_TRUNCATE = 0x20; + +var gChannel; +var gDebugTest = false; + +/* import-globals-from sharedUpdateXML.js */ +Services.scriptloader.loadSubScript(DATA_URI_SPEC + "sharedUpdateXML.js", this); + +const PERMS_FILE = FileUtils.PERMS_FILE; +const PERMS_DIRECTORY = FileUtils.PERMS_DIRECTORY; + +const MODE_WRONLY = FileUtils.MODE_WRONLY; +const MODE_CREATE = FileUtils.MODE_CREATE; +const MODE_APPEND = FileUtils.MODE_APPEND; +const MODE_TRUNCATE = FileUtils.MODE_TRUNCATE; + +const URI_UPDATES_PROPERTIES = + "chrome://mozapps/locale/update/updates.properties"; +const gUpdateBundle = Services.strings.createBundle(URI_UPDATES_PROPERTIES); + +XPCOMUtils.defineLazyGetter(this, "gAUS", function test_gAUS() { + return Cc["@mozilla.org/updates/update-service;1"] + .getService(Ci.nsIApplicationUpdateService) + .QueryInterface(Ci.nsITimerCallback) + .QueryInterface(Ci.nsIObserver); +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gUpdateManager", + "@mozilla.org/updates/update-manager;1", + "nsIUpdateManager" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gUpdateChecker", + "@mozilla.org/updates/update-checker;1", + "nsIUpdateChecker" +); + +XPCOMUtils.defineLazyGetter(this, "gDefaultPrefBranch", function test_gDPB() { + return Services.prefs.getDefaultBranch(null); +}); + +XPCOMUtils.defineLazyGetter(this, "gPrefRoot", function test_gPR() { + return Services.prefs.getBranch(null); +}); + +/** + * Waits for the specified topic and (optionally) status. + * + * @param topic + * String representing the topic to wait for. + * @param status (optional) + * A string representing the status on said topic to wait for. + * @return A promise which will resolve the first time an event occurs on the + * specified topic, and (optionally) with the specified status. + */ +function waitForEvent(topic, status = null) { + return new Promise(resolve => + Services.obs.addObserver( + { + observe(subject, innerTopic, innerStatus) { + if (!status || status == innerStatus) { + Services.obs.removeObserver(this, topic); + resolve(innerStatus); + } + }, + }, + topic + ) + ); +} + +/* Triggers post-update processing */ +function testPostUpdateProcessing() { + gAUS.observe(null, "test-post-update-processing", ""); +} + +/* Initializes the update service stub */ +function initUpdateServiceStub() { + Cc["@mozilla.org/updates/update-service-stub;1"].createInstance( + Ci.nsISupports + ); +} + +/** + * Reloads the update xml files. + * + * @param skipFiles (optional) + * If true, the update xml files will not be read and the metadata will + * be reset. If false (the default), the update xml files will be read + * to populate the update metadata. + */ +function reloadUpdateManagerData(skipFiles = false) { + let observeData = skipFiles ? "skip-files" : ""; + gUpdateManager + .QueryInterface(Ci.nsIObserver) + .observe(null, "um-reload-update-data", observeData); +} + +const observer = { + observe(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed" && aData == PREF_APP_UPDATE_CHANNEL) { + let channel = gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL); + if (channel != gChannel) { + debugDump("Changing channel from " + channel + " to " + gChannel); + gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel); + } + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), +}; + +/** + * Sets the app.update.channel preference. + * + * @param aChannel + * The update channel. + */ +function setUpdateChannel(aChannel) { + gChannel = aChannel; + debugDump( + "setting default pref " + PREF_APP_UPDATE_CHANNEL + " to " + gChannel + ); + gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel); + gPrefRoot.addObserver(PREF_APP_UPDATE_CHANNEL, observer); +} + +/** + * Sets the effective update url. + * + * @param aURL + * The update url. If not specified 'URL_HOST + "/update.xml"' will be + * used. + */ +function setUpdateURL(aURL) { + let url = aURL ? aURL : URL_HOST + "/update.xml"; + debugDump("setting update URL to " + url); + + // The Update URL is stored in appinfo. We can replace this process's appinfo + // directly, but that will affect only this process. Luckily, the update URL + // is only ever read from the update process. This means that replacing + // Services.appinfo is sufficient and we don't need to worry about registering + // a replacement factory or anything like that. + let origAppInfo = Services.appinfo; + registerCleanupFunction(() => { + Services.appinfo = origAppInfo; + }); + + // Override the appinfo object with an object that exposes all of the same + // properties overriding just the updateURL. + let mockAppInfo = Object.create(origAppInfo, { + updateURL: { + configurable: true, + enumerable: true, + writable: false, + value: url, + }, + }); + + Services.appinfo = mockAppInfo; +} + +/** + * Writes the updates specified to either the active-update.xml or the + * updates.xml. + * + * @param aContent + * The updates represented as a string to write to the XML file. + * @param isActiveUpdate + * If true this will write to the active-update.xml otherwise it will + * write to the updates.xml file. + */ +function writeUpdatesToXMLFile(aContent, aIsActiveUpdate) { + let file = getUpdateDirFile( + aIsActiveUpdate ? FILE_ACTIVE_UPDATE_XML : FILE_UPDATES_XML + ); + writeFile(file, aContent); +} + +/** + * Writes the current update operation/state to a file in the patch + * directory, indicating to the patching system that operations need + * to be performed. + * + * @param aStatus + * The status value to write. + */ +function writeStatusFile(aStatus) { + let file = getUpdateDirFile(FILE_UPDATE_STATUS); + writeFile(file, aStatus + "\n"); +} + +/** + * Writes the current update version to a file in the patch directory, + * indicating to the patching system the version of the update. + * + * @param aVersion + * The version value to write. + */ +function writeVersionFile(aVersion) { + let file = getUpdateDirFile(FILE_UPDATE_VERSION); + writeFile(file, aVersion + "\n"); +} + +/** + * Writes text to a file. This will replace existing text if the file exists + * and create the file if it doesn't exist. + * + * @param aFile + * The file to write to. Will be created if it doesn't exist. + * @param aText + * The text to write to the file. If there is existing text it will be + * replaced. + */ +function writeFile(aFile, aText) { + let fos = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + if (!aFile.exists()) { + aFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + } + fos.init(aFile, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, PERMS_FILE, 0); + fos.write(aText, aText.length); + fos.close(); +} + +/** + * Reads the current update operation/state in the status file in the patch + * directory including the error code if it is present. + * + * @return The status value. + */ +function readStatusFile() { + let file = getUpdateDirFile(FILE_UPDATE_STATUS); + if (!file.exists()) { + debugDump("update status file does not exists! Path: " + file.path); + return STATE_NONE; + } + return readFile(file).split("\n")[0]; +} + +/** + * Reads the current update operation/state in the status file in the patch + * directory without the error code if it is present. + * + * @return The state value. + */ +function readStatusState() { + return readStatusFile().split(": ")[0]; +} + +/** + * Reads the current update operation/state in the status file in the patch + * directory with the error code. + * + * @return The state value. + */ +function readStatusFailedCode() { + return readStatusFile().split(": ")[1]; +} + +/** + * Returns whether or not applying the current update resulted in an error + * verifying binary transparency information. + * + * @return true if there was an error result and false otherwise + */ +function updateHasBinaryTransparencyErrorResult() { + let file = getUpdateDirFile(FILE_BT_RESULT); + return file.exists(); +} + +/** + * Reads text from a file and returns the string. + * + * @param aFile + * The file to read from. + * @return The string of text read from the file. + */ +function readFile(aFile) { + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + if (!aFile.exists()) { + return null; + } + // Specifying -1 for ioFlags will open the file with the default of PR_RDONLY. + // Specifying -1 for perm will open the file with the default of 0. + fis.init(aFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF); + let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(fis); + let text = sis.read(sis.available()); + sis.close(); + return text; +} + +/* Returns human readable status text from the updates.properties bundle */ +function getStatusText(aErrCode) { + return getString("check_error-" + aErrCode); +} + +/* Returns a string from the updates.properties bundle */ +function getString(aName) { + try { + return gUpdateBundle.GetStringFromName(aName); + } catch (e) {} + return null; +} + +/** + * Gets the file extension for an nsIFile. + * + * @param aFile + * The file to get the file extension for. + * @return The file extension. + */ +function getFileExtension(aFile) { + return Services.io.newFileURI(aFile).QueryInterface(Ci.nsIURL).fileExtension; +} + +/** + * Gets the specified update file or directory. + * + * @param aLogLeafName + * The leafName of the file or directory to get. + * @param aWhichDir + * Since we started having a separate patch directory and downloading + * directory, there are now files with the same name that can be in + * either directory. This argument is optional and defaults to the + * patch directory for historical reasons. But if it is specified as + * DIR_DOWNLOADING, this function will provide the version of the file + * in the downloading directory. For files that aren't in the patch + * directory or the downloading directory, this value is ignored. + * @return nsIFile for the file or directory. + */ +function getUpdateDirFile(aLeafName, aWhichDir = null) { + let file = Services.dirsvc.get(XRE_UPDATE_ROOT_DIR, Ci.nsIFile); + switch (aLeafName) { + case undefined: + return file; + case DIR_UPDATES: + case FILE_ACTIVE_UPDATE_XML: + case FILE_ACTIVE_UPDATE_XML_TMP: + case FILE_UPDATE_CONFIG_JSON: + case FILE_BACKUP_UPDATE_CONFIG_JSON: + case FILE_UPDATE_TEST: + case FILE_UPDATES_XML: + case FILE_UPDATES_XML_TMP: + file.append(aLeafName); + return file; + case DIR_PATCH: + case DIR_DOWNLOADING: + case FILE_BACKUP_UPDATE_LOG: + case FILE_LAST_UPDATE_LOG: + file.append(DIR_UPDATES); + file.append(aLeafName); + return file; + case FILE_BT_RESULT: + case FILE_UPDATE_LOG: + case FILE_UPDATE_MAR: + case FILE_UPDATE_STATUS: + case FILE_UPDATE_VERSION: + case FILE_UPDATER_INI: + file.append(DIR_UPDATES); + if (aWhichDir == DIR_DOWNLOADING) { + file.append(DIR_DOWNLOADING); + } else { + file.append(DIR_PATCH); + } + file.append(aLeafName); + return file; + } + + throw new Error( + "The leafName specified is not handled by this function, " + + "leafName: " + + aLeafName + ); +} + +/** + * Helper function for getting the nsIFile for a file in the directory where the + * update will be staged. + * + * The files for the update are located two directories below the stage + * directory since Mac OS X sets the last modified time for the root directory + * to the current time and if the update changes any files in the root directory + * then it wouldn't be possible to test (bug 600098). + * + * @param aRelPath (optional) + * The relative path to the file or directory to get from the root of + * the stage directory. If not specified the stage directory will be + * returned. + * @return The nsIFile for the file in the directory where the update will be + * staged. + */ +function getStageDirFile(aRelPath) { + let file; + if (AppConstants.platform == "macosx") { + file = getUpdateDirFile(DIR_PATCH); + } else { + file = getGREBinDir(); + } + file.append(DIR_UPDATED); + if (aRelPath) { + let pathParts = aRelPath.split("/"); + for (let i = 0; i < pathParts.length; i++) { + if (pathParts[i]) { + file.append(pathParts[i]); + } + } + } + return file; +} + +/** + * Removes the update files that typically need to be removed by tests without + * removing the directories since removing the directories has caused issues + * when running tests with --verify and recursively removes the stage directory. + * + * @param aRemoveLogFiles + * When true the update log files will also be removed. This allows + * for the inspection of the log files while troubleshooting tests. + */ +function removeUpdateFiles(aRemoveLogFiles) { + let files = [ + [FILE_ACTIVE_UPDATE_XML], + [FILE_UPDATES_XML], + [FILE_BT_RESULT], + [FILE_UPDATE_STATUS], + [FILE_UPDATE_VERSION], + [FILE_UPDATE_MAR], + [FILE_UPDATE_MAR, DIR_DOWNLOADING], + [FILE_UPDATER_INI], + ]; + + if (aRemoveLogFiles) { + files = files.concat([ + [FILE_BACKUP_UPDATE_LOG], + [FILE_LAST_UPDATE_LOG], + [FILE_UPDATE_LOG], + ]); + } + + for (let i = 0; i < files.length; i++) { + let file = getUpdateDirFile.apply(null, files[i]); + try { + if (file.exists()) { + file.remove(false); + } + } catch (e) { + logTestInfo( + "Unable to remove file. Path: " + file.path + ", Exception: " + e + ); + } + } + + let stageDir = getStageDirFile(); + if (stageDir.exists()) { + try { + removeDirRecursive(stageDir); + } catch (e) { + logTestInfo( + "Unable to remove directory. Path: " + + stageDir.path + + ", Exception: " + + e + ); + } + } +} + +/** + * Deletes a directory and its children. First it tries nsIFile::Remove(true). + * If that fails it will fall back to recursing, setting the appropriate + * permissions, and deleting the current entry. + * + * @param aDir + * nsIFile for the directory to be deleted. + */ +function removeDirRecursive(aDir) { + if (!aDir.exists()) { + return; + } + + if (!aDir.isDirectory()) { + throw new Error("Only a directory can be passed to this funtion!"); + } + + try { + debugDump("attempting to remove directory. Path: " + aDir.path); + aDir.remove(true); + return; + } catch (e) { + logTestInfo("non-fatal error removing directory. Exception: " + e); + } + + let dirEntries = aDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + let entry = dirEntries.nextFile; + + if (entry.isDirectory()) { + removeDirRecursive(entry); + } else { + entry.permissions = PERMS_FILE; + try { + debugDump("attempting to remove file. Path: " + entry.path); + entry.remove(false); + } catch (e) { + logTestInfo("error removing file. Exception: " + e); + throw e; + } + } + } + + aDir.permissions = PERMS_DIRECTORY; + try { + debugDump("attempting to remove directory. Path: " + aDir.path); + aDir.remove(true); + } catch (e) { + logTestInfo("error removing directory. Exception: " + e); + throw e; + } +} + +/** + * Returns the directory for the currently running process. This is used to + * clean up after the tests and to locate the active-update.xml and updates.xml + * files. + * + * @return nsIFile for the current process directory. + */ +function getCurrentProcessDir() { + return Services.dirsvc.get(NS_XPCOM_CURRENT_PROCESS_DIR, Ci.nsIFile); +} + +/** + * Returns the Gecko Runtime Engine directory where files other than executable + * binaries are located. On Mac OS X this will be <bundle>/Contents/Resources/ + * and the installation directory on all other platforms. + * + * @return nsIFile for the Gecko Runtime Engine directory. + */ +function getGREDir() { + return Services.dirsvc.get(NS_GRE_DIR, Ci.nsIFile); +} + +/** + * Returns the Gecko Runtime Engine Binary directory where the executable + * binaries are located such as the updater binary (Windows and Linux) or + * updater package (Mac OS X). On Mac OS X this will be + * <bundle>/Contents/MacOS/ and the installation directory on all other + * platforms. + * + * @return nsIFile for the Gecko Runtime Engine Binary directory. + */ +function getGREBinDir() { + return Services.dirsvc.get(NS_GRE_BIN_DIR, Ci.nsIFile); +} + +/** + * Gets the unique mutex name for the installation. + * + * @return Global mutex path. + * @throws If the function is called on a platform other than Windows. + */ +function getPerInstallationMutexName() { + if (AppConstants.platform != "win") { + throw new Error("Windows only function called by a different platform!"); + } + + let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + hasher.init(hasher.SHA1); + + let exeFile = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile); + let converter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let data = converter.convertToByteArray(exeFile.path.toLowerCase()); + + hasher.update(data, data.length); + return "Global\\MozillaUpdateMutex-" + hasher.finish(true); +} + +/** + * Closes a Win32 handle. + * + * @param aHandle + * The handle to close. + * @throws If the function is called on a platform other than Windows. + */ +function closeHandle(aHandle) { + if (AppConstants.platform != "win") { + throw new Error("Windows only function called by a different platform!"); + } + + let lib = ctypes.open("kernel32.dll"); + let CloseHandle = lib.declare( + "CloseHandle", + ctypes.winapi_abi, + ctypes.int32_t /* success */, + ctypes.void_t.ptr + ); /* handle */ + CloseHandle(aHandle); + lib.close(); +} + +/** + * Creates a mutex. + * + * @param aName + * The name for the mutex. + * @return The Win32 handle to the mutex. + * @throws If the function is called on a platform other than Windows. + */ +function createMutex(aName) { + if (AppConstants.platform != "win") { + throw new Error("Windows only function called by a different platform!"); + } + + const INITIAL_OWN = 1; + const ERROR_ALREADY_EXISTS = 0xb7; + let lib = ctypes.open("kernel32.dll"); + let CreateMutexW = lib.declare( + "CreateMutexW", + ctypes.winapi_abi, + ctypes.void_t.ptr /* return handle */, + ctypes.void_t.ptr /* security attributes */, + ctypes.int32_t /* initial owner */, + ctypes.char16_t.ptr + ); /* name */ + + let handle = CreateMutexW(null, INITIAL_OWN, aName); + lib.close(); + let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS; + if (handle && !handle.isNull() && alreadyExists) { + closeHandle(handle); + handle = null; + } + + if (handle && handle.isNull()) { + handle = null; + } + + return handle; +} + +/** + * Synchronously writes the value of the app.update.auto setting to the update + * configuration file on Windows or to a user preference on other platforms. + * When the value passed to this function is null or undefined it will remove + * the configuration file on Windows or the user preference on other platforms. + * + * @param aEnabled + * Possible values are true, false, null, and undefined. When true or + * false this value will be written for app.update.auto in the update + * configuration file on Windows or to the user preference on other + * platforms. When null or undefined the update configuration file will + * be removed on Windows or the user preference will be removed on other + * platforms. + */ +function setAppUpdateAutoSync(aEnabled) { + if (AppConstants.platform == "win") { + let file = getUpdateDirFile(FILE_UPDATE_CONFIG_JSON); + if (aEnabled === undefined || aEnabled === null) { + if (file.exists()) { + file.remove(false); + } + } else { + writeFile( + file, + '{"' + CONFIG_APP_UPDATE_AUTO + '":' + aEnabled.toString() + "}" + ); + } + } else if (aEnabled === undefined || aEnabled === null) { + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_AUTO)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_AUTO); + } + } else { + Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO, aEnabled); + } +} + +/** + * Logs TEST-INFO messages. + * + * @param aText + * The text to log. + * @param aCaller (optional) + * An optional Components.stack.caller. If not specified + * Components.stack.caller will be used. + */ +function logTestInfo(aText, aCaller) { + let caller = aCaller ? aCaller : Components.stack.caller; + let now = new Date(); + let hh = now.getHours(); + let mm = now.getMinutes(); + let ss = now.getSeconds(); + let ms = now.getMilliseconds(); + let time = + (hh < 10 ? "0" + hh : hh) + + ":" + + (mm < 10 ? "0" + mm : mm) + + ":" + + (ss < 10 ? "0" + ss : ss) + + ":"; + if (ms < 10) { + time += "00"; + } else if (ms < 100) { + time += "0"; + } + time += ms; + let msg = + time + + " | TEST-INFO | " + + caller.filename + + " | [" + + caller.name + + " : " + + caller.lineNumber + + "] " + + aText; + LOG_FUNCTION(msg); +} + +/** + * Logs TEST-INFO messages when gDebugTest evaluates to true. + * + * @param aText + * The text to log. + * @param aCaller (optional) + * An optional Components.stack.caller. If not specified + * Components.stack.caller will be used. + */ +function debugDump(aText, aCaller) { + if (gDebugTest) { + let caller = aCaller ? aCaller : Components.stack.caller; + logTestInfo(aText, caller); + } +} + +/** + * Creates the continue file used to signal that update staging or the mock http + * server should continue. The delay this creates allows the tests to verify the + * user interfaces before they auto advance to other phases of an update. The + * continue file for staging will be deleted by the test updater and the + * continue file for the update check and update download requests will be + * deleted by the test http server handler implemented in app_update.sjs. The + * test returns a promise so the test can wait on the deletion of the continue + * file when necessary. If the continue file still exists at the end of a test + * it will be removed to prevent it from affecting tests that run after the test + * that created it. + * + * @param leafName + * The leafName of the file to create. This should be one of the + * folowing constants that are defined in testConstants.js: + * CONTINUE_CHECK + * CONTINUE_DOWNLOAD + * CONTINUE_STAGING + * @return Promise + * Resolves when the file is deleted or if the file is not deleted when + * the check for the file's existence times out. If the file isn't + * deleted before the check for the file's existence times out it will + * be deleted when the test ends so it doesn't affect tests that run + * after the test that created the continue file. + * @throws If the file already exists. + */ +async function continueFileHandler(leafName) { + // The total time to wait with 300 retries and the default interval of 100 is + // approximately 30 seconds. + let interval = 100; + let retries = 300; + let continueFile; + if (leafName == CONTINUE_STAGING) { + // The total time to wait with 600 retries and an interval of 200 is + // approximately 120 seconds. + interval = 200; + retries = 600; + continueFile = getGREBinDir(); + if (AppConstants.platform == "macosx") { + continueFile = continueFile.parent.parent; + } + continueFile.append(leafName); + } else { + continueFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + let continuePath = REL_PATH_DATA + leafName; + let continuePathParts = continuePath.split("/"); + for (let i = 0; i < continuePathParts.length; ++i) { + continueFile.append(continuePathParts[i]); + } + } + if (continueFile.exists()) { + logTestInfo( + "The continue file should not exist, path: " + continueFile.path + ); + continueFile.remove(false); + } + debugDump("Creating continue file, path: " + continueFile.path); + continueFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + // If for whatever reason the continue file hasn't been removed when a test + // has finished remove it during cleanup so it doesn't affect tests that run + // after the test that created it. + registerCleanupFunction(() => { + if (continueFile.exists()) { + logTestInfo( + "Removing continue file during test cleanup, path: " + continueFile.path + ); + continueFile.remove(false); + } + }); + return TestUtils.waitForCondition( + () => !continueFile.exists(), + "Waiting for file to be deleted, path: " + continueFile.path, + interval, + retries + ).catch(e => { + logTestInfo( + "Continue file was not removed after checking " + + retries + + " times, path: " + + continueFile.path + ); + }); +} diff --git a/toolkit/mozapps/update/tests/data/sharedUpdateXML.js b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js new file mode 100644 index 0000000000..acff4aec3f --- /dev/null +++ b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js @@ -0,0 +1,417 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Shared code for xpcshell, mochitests-chrome, mochitest-browser-chrome, and + * SJS server-side scripts for the test http server. + */ + +/** + * Helper functions for creating xml strings used by application update tests. + */ + +/* import-globals-from ../browser/testConstants.js */ + +/* global Services, UpdateUtils, gURLData */ + +const FILE_SIMPLE_MAR = "simple.mar"; +const SIZE_SIMPLE_MAR = "1419"; + +const STATE_NONE = "null"; +const STATE_DOWNLOADING = "downloading"; +const STATE_PENDING = "pending"; +const STATE_PENDING_SVC = "pending-service"; +const STATE_PENDING_ELEVATE = "pending-elevate"; +const STATE_APPLYING = "applying"; +const STATE_APPLIED = "applied"; +const STATE_APPLIED_SVC = "applied-service"; +const STATE_SUCCEEDED = "succeeded"; +const STATE_DOWNLOAD_FAILED = "download-failed"; +const STATE_FAILED = "failed"; + +const LOADSOURCE_ERROR_WRONG_SIZE = 2; +const CRC_ERROR = 4; +const READ_ERROR = 6; +const WRITE_ERROR = 7; +const MAR_CHANNEL_MISMATCH_ERROR = 22; +const VERSION_DOWNGRADE_ERROR = 23; +const UPDATE_SETTINGS_FILE_CHANNEL = 38; +const SERVICE_COULD_NOT_COPY_UPDATER = 49; +const SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR = 52; +const SERVICE_INVALID_APPLYTO_DIR_ERROR = 54; +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; + +// Error codes 80 through 99 are reserved for nsUpdateService.js and are not +// defined in common/updatererrors.h +const ERR_OLDER_VERSION_OR_SAME_BUILD = 90; +const ERR_UPDATE_STATE_NONE = 91; +const ERR_CHANNEL_CHANGE = 92; + +const WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION = 106; + +const STATE_FAILED_DELIMETER = ": "; + +const STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE = + STATE_FAILED + STATE_FAILED_DELIMETER + LOADSOURCE_ERROR_WRONG_SIZE; +const STATE_FAILED_CRC_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + 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_UPDATE_SETTINGS_FILE_CHANNEL = + STATE_FAILED + STATE_FAILED_DELIMETER + UPDATE_SETTINGS_FILE_CHANNEL; +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; +const STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR = + STATE_FAILED + + STATE_FAILED_DELIMETER + + SERVICE_INVALID_WORKING_DIR_PATH_ERROR; +const STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_STAGED_ERROR; +const STATE_FAILED_INVALID_APPLYTO_DIR_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_ERROR; +const STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_INSTALL_DIR_PATH_ERROR; +const STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_WORKING_DIR_PATH_ERROR; +const STATE_FAILED_INVALID_CALLBACK_PATH_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_CALLBACK_PATH_ERROR; +const STATE_FAILED_INVALID_CALLBACK_DIR_ERROR = + STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_CALLBACK_DIR_ERROR; +const STATE_FAILED_WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION = + STATE_FAILED + + STATE_FAILED_DELIMETER + + WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION; + +const DEFAULT_UPDATE_VERSION = "999999.0"; + +/** + * Constructs a string representing a remote update xml file. + * + * @param aUpdates + * The string representing the update elements. + * @return The string representing a remote update xml file. + */ +function getRemoteUpdatesXMLString(aUpdates) { + return '<?xml version="1.0"?><updates>' + aUpdates + "</updates>"; +} + +/** + * Constructs a string representing an update element for a remote update xml + * file. See getUpdateString for parameter information not provided below. + * + * @param aUpdateProps + * An object containing non default test values for an nsIUpdate. + * See updateProps names below for possible object names. + * @param aPatches + * String representing the application update patches. + * @return The string representing an update element for an update xml file. + */ +function getRemoteUpdateString(aUpdateProps, aPatches) { + const updateProps = { + appVersion: DEFAULT_UPDATE_VERSION, + buildID: "20080811053724", + custom1: null, + custom2: null, + detailsURL: URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS", + displayVersion: null, + name: "App Update Test", + promptWaitTime: null, + type: "major", + }; + + for (let name in aUpdateProps) { + updateProps[name] = aUpdateProps[name]; + } + + // To test that text nodes are handled properly the string returned contains + // spaces and newlines. + return getUpdateString(updateProps) + ">\n " + aPatches + "\n</update>\n"; +} + +/** + * Constructs a string representing a patch element for a remote update xml + * file. See getPatchString for parameter information not provided below. + * + * @param aPatchProps (optional) + * An object containing non default test values for an nsIUpdatePatch. + * See patchProps below for possible object names. + * @return The string representing a patch element for a remote update xml file. + */ +function getRemotePatchString(aPatchProps) { + const patchProps = { + type: "complete", + _url: null, + get url() { + if (this._url) { + return this._url; + } + if (gURLData) { + return gURLData + FILE_SIMPLE_MAR; + } + return null; + }, + set url(val) { + this._url = val; + }, + custom1: null, + custom2: null, + size: SIZE_SIMPLE_MAR, + }; + + for (let name in aPatchProps) { + patchProps[name] = aPatchProps[name]; + } + + return getPatchString(patchProps) + "/>"; +} + +/** + * Constructs a string representing a local update xml file. + * + * @param aUpdates + * The string representing the update elements. + * @return The string representing a local update xml file. + */ +function getLocalUpdatesXMLString(aUpdates) { + if (!aUpdates || aUpdates == "") { + return '<updates xmlns="http://www.mozilla.org/2005/app-update"/>'; + } + return ( + '<updates xmlns="http://www.mozilla.org/2005/app-update">' + + aUpdates + + "</updates>" + ); +} + +/** + * Constructs a string representing an update element for a local update xml + * file. See getUpdateString for parameter information not provided below. + * + * @param aUpdateProps + * An object containing non default test values for an nsIUpdate. + * See updateProps names below for possible object names. + * @param aPatches + * String representing the application update patches. + * @return The string representing an update element for an update xml file. + */ +function getLocalUpdateString(aUpdateProps, aPatches) { + const updateProps = { + _appVersion: null, + get appVersion() { + if (this._appVersion) { + return this._appVersion; + } + if (Services && Services.appinfo && Services.appinfo.version) { + return Services.appinfo.version; + } + return DEFAULT_UPDATE_VERSION; + }, + set appVersion(val) { + this._appVersion = val; + }, + buildID: "20080811053724", + channel: UpdateUtils ? UpdateUtils.getUpdateChannel() : "default", + custom1: null, + custom2: null, + detailsURL: URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS", + displayVersion: null, + foregroundDownload: "true", + installDate: "1238441400314", + isCompleteUpdate: "true", + name: "App Update Test", + previousAppVersion: null, + promptWaitTime: null, + serviceURL: "http://test_service/", + statusText: "Install Pending", + type: "major", + }; + + for (let name in aUpdateProps) { + updateProps[name] = aUpdateProps[name]; + } + + let checkInterval = updateProps.checkInterval + ? 'checkInterval="' + updateProps.checkInterval + '" ' + : ""; + let channel = 'channel="' + updateProps.channel + '" '; + let isCompleteUpdate = + 'isCompleteUpdate="' + updateProps.isCompleteUpdate + '" '; + let foregroundDownload = updateProps.foregroundDownload + ? 'foregroundDownload="' + updateProps.foregroundDownload + '" ' + : ""; + let installDate = 'installDate="' + updateProps.installDate + '" '; + let previousAppVersion = updateProps.previousAppVersion + ? 'previousAppVersion="' + updateProps.previousAppVersion + '" ' + : ""; + let statusText = updateProps.statusText + ? 'statusText="' + updateProps.statusText + '" ' + : ""; + let serviceURL = 'serviceURL="' + updateProps.serviceURL + '">'; + + return ( + getUpdateString(updateProps) + + " " + + checkInterval + + channel + + isCompleteUpdate + + foregroundDownload + + installDate + + previousAppVersion + + statusText + + serviceURL + + aPatches + + "</update>" + ); +} + +/** + * Constructs a string representing a patch element for a local update xml file. + * See getPatchString for parameter information not provided below. + * + * @param aPatchProps (optional) + * An object containing non default test values for an nsIUpdatePatch. + * See patchProps below for possible object names. + * @return The string representing a patch element for a local update xml file. + */ +function getLocalPatchString(aPatchProps) { + const patchProps = { + type: "complete", + url: gURLData + FILE_SIMPLE_MAR, + size: SIZE_SIMPLE_MAR, + custom1: null, + custom2: null, + selected: "true", + state: STATE_SUCCEEDED, + }; + + for (let name in aPatchProps) { + patchProps[name] = aPatchProps[name]; + } + + let selected = 'selected="' + patchProps.selected + '" '; + let state = 'state="' + patchProps.state + '"/>'; + return getPatchString(patchProps) + " " + selected + state; +} + +/** + * Constructs a string representing an update element for a remote update xml + * file. + * + * @param aUpdateProps (optional) + * An object containing non default test values for an nsIUpdate. + * See the aUpdateProps property names below for possible object names. + * @return The string representing an update element for an update xml file. + */ +function getUpdateString(aUpdateProps) { + let type = 'type="' + aUpdateProps.type + '" '; + let name = 'name="' + aUpdateProps.name + '" '; + let displayVersion = aUpdateProps.displayVersion + ? 'displayVersion="' + aUpdateProps.displayVersion + '" ' + : ""; + let appVersion = 'appVersion="' + aUpdateProps.appVersion + '" '; + // Not specifying a detailsURL will cause a leak due to bug 470244 + let detailsURL = 'detailsURL="' + aUpdateProps.detailsURL + '" '; + let promptWaitTime = aUpdateProps.promptWaitTime + ? 'promptWaitTime="' + aUpdateProps.promptWaitTime + '" ' + : ""; + let disableBITS = aUpdateProps.disableBITS + ? 'disableBITS="' + aUpdateProps.disableBITS + '" ' + : ""; + let disableBackgroundUpdates = aUpdateProps.disableBackgroundUpdates + ? 'disableBackgroundUpdates="' + + aUpdateProps.disableBackgroundUpdates + + '" ' + : ""; + let custom1 = aUpdateProps.custom1 ? aUpdateProps.custom1 + " " : ""; + let custom2 = aUpdateProps.custom2 ? aUpdateProps.custom2 + " " : ""; + let buildID = 'buildID="' + aUpdateProps.buildID + '"'; + + return ( + "<update " + + type + + name + + displayVersion + + appVersion + + detailsURL + + promptWaitTime + + disableBITS + + disableBackgroundUpdates + + custom1 + + custom2 + + buildID + ); +} + +/** + * Constructs a string representing a patch element for an update xml file. + * + * @param aPatchProps (optional) + * An object containing non default test values for an nsIUpdatePatch. + * See the patchProps property names below for possible object names. + * @return The string representing a patch element for an update xml file. + */ +function getPatchString(aPatchProps) { + let type = 'type="' + aPatchProps.type + '" '; + let url = 'URL="' + aPatchProps.url + '" '; + let size = 'size="' + aPatchProps.size + '"'; + let custom1 = aPatchProps.custom1 ? aPatchProps.custom1 + " " : ""; + let custom2 = aPatchProps.custom2 ? aPatchProps.custom2 + " " : ""; + return "<patch " + type + url + custom1 + custom2 + size; +} + +/** + * Reads the binary contents of a file and returns it as a string. + * + * @param aFile + * The file to read from. + * @return The contents of the file as a string. + */ +function readFileBytes(aFile) { + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + // Specifying -1 for ioFlags will open the file with the default of PR_RDONLY. + // Specifying -1 for perm will open the file with the default of 0. + fis.init(aFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bis.setInputStream(fis); + let data = []; + let count = fis.available(); + while (count > 0) { + let bytes = bis.readByteArray(Math.min(65535, count)); + data.push(String.fromCharCode.apply(null, bytes)); + count -= bytes.length; + if (!bytes.length) { + throw new Error("Nothing read from input stream!"); + } + } + data = data.join(""); + fis.close(); + return data.toString(); +} diff --git a/toolkit/mozapps/update/tests/data/simple.mar b/toolkit/mozapps/update/tests/data/simple.mar Binary files differnew file mode 100644 index 0000000000..fd635b46bd --- /dev/null +++ b/toolkit/mozapps/update/tests/data/simple.mar diff --git a/toolkit/mozapps/update/tests/data/syncManagerTestChild.js b/toolkit/mozapps/update/tests/data/syncManagerTestChild.js new file mode 100644 index 0000000000..1f52554e7c --- /dev/null +++ b/toolkit/mozapps/update/tests/data/syncManagerTestChild.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This is the script that runs in the child xpcshell process for the test +// unit_aus_update/updateSyncManager.js. +// The main thing this script does is override the child's directory service +// so that it ends up with the same fake binary path that the parent test runner +// has opened its update lock with. +// This requires that we have already been passed a constant on our command +// line which contains the relevant fake binary path, which is called: +/* global customExePath */ + +print("child process is running"); + +// This function is copied from xpcshellUtilsAUS.js so that we can have our +// xpcshell subprocess call it without having to load that whole file, because +// it turns out that needs a bunch of infrastructure that normally the testing +// framework would provide, and that also requires a bunch of setup, and it's +// just not worth all that. This is a cut down version that only includes the +// directory provider functionality that the subprocess really needs. +function adjustGeneralPaths() { + let dirProvider = { + getFile: function AGP_DP_getFile(aProp, aPersistent) { + // Set the value of persistent to false so when this directory provider is + // unregistered it will revert back to the original provider. + aPersistent.value = false; + // The sync manager only needs XREExeF, so that's all we provide. + if (aProp == "XREExeF") { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(customExePath); + return file; + } + return null; + }, + QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), + }; + let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService); + ds.QueryInterface(Ci.nsIProperties).undefine("XREExeF"); + ds.registerProvider(dirProvider); + + // Now that we've overridden the directory provider, the name of the update + // lock needs to be changed to match the overridden path. + let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + syncManager.resetLock(); +} + +adjustGeneralPaths(); + +// Wait a few seconds for the parent to do what it needs to do, then exit. +print("child process should now have the lock; will exit in 5 seconds"); +simulateNoScriptActivity(5); +print("child process exiting now"); diff --git a/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js b/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js new file mode 100644 index 0000000000..90880d96d9 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js @@ -0,0 +1,29 @@ +/* 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/. */ + +#filter substitution + +/* Preprocessed constants used by xpcshell tests */ + +// MOZ_APP_VENDOR is optional. +#ifdef MOZ_APP_VENDOR +const MOZ_APP_VENDOR = "@MOZ_APP_VENDOR@"; +#else +const MOZ_APP_VENDOR = ""; +#endif + +// MOZ_APP_BASENAME is not optional for tests. +const MOZ_APP_BASENAME = "@MOZ_APP_BASENAME@"; + +#ifdef MOZ_VERIFY_MAR_SIGNATURE +const MOZ_VERIFY_MAR_SIGNATURE = true; +#else +const MOZ_VERIFY_MAR_SIGNATURE = false; +#endif + +#ifdef DISABLE_UPDATER_AUTHENTICODE_CHECK + const IS_AUTHENTICODE_CHECK_ENABLED = false; +#else + const IS_AUTHENTICODE_CHECK_ENABLED = true; +#endif diff --git a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js new file mode 100644 index 0000000000..510494de94 --- /dev/null +++ b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js @@ -0,0 +1,4811 @@ +/* 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/. */ + +/** + * Test log warnings that happen before the test has started + * "Couldn't get the user appdata directory. Crash events may not be produced." + * in nsExceptionHandler.cpp (possibly bug 619104) + * + * Test log warnings that happen after the test has finished + * "OOPDeinit() without successful OOPInit()" in nsExceptionHandler.cpp + * (bug 619104) + * "XPCOM objects created/destroyed from static ctor/dtor" in nsTraceRefcnt.cpp + * (possibly bug 457479) + * + * Other warnings printed to the test logs + * "site security information will not be persisted" in + * nsSiteSecurityService.cpp and the error in nsSystemInfo.cpp preceding this + * error are due to not having a profile when running some of the xpcshell + * tests. Since most xpcshell tests also log these errors these tests don't + * call do_get_profile unless necessary for the test. + * "!mMainThread" in nsThreadManager.cpp are due to using timers and it might be + * possible to fix some or all of these in the test itself. + * "NS_FAILED(rv)" in nsThreadUtils.cpp are due to using timers and it might be + * possible to fix some or all of these in the test itself. + */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs", + updateAppInfo: "resource://testing-common/AppInfo.sys.mjs", +}); + +const Cm = Components.manager; + +/* global MOZ_APP_VENDOR, MOZ_APP_BASENAME */ +/* global MOZ_VERIFY_MAR_SIGNATURE, IS_AUTHENTICODE_CHECK_ENABLED */ +load("../data/xpcshellConstantsPP.js"); + +const DIR_MACOS = AppConstants.platform == "macosx" ? "Contents/MacOS/" : ""; +const DIR_RESOURCES = + AppConstants.platform == "macosx" ? "Contents/Resources/" : ""; +const TEST_FILE_SUFFIX = AppConstants.platform == "macosx" ? "_mac" : ""; +const FILE_COMPLETE_MAR = "complete" + TEST_FILE_SUFFIX + ".mar"; +const FILE_PARTIAL_MAR = "partial" + TEST_FILE_SUFFIX + ".mar"; +const FILE_COMPLETE_PRECOMPLETE = "complete_precomplete" + TEST_FILE_SUFFIX; +const FILE_PARTIAL_PRECOMPLETE = "partial_precomplete" + TEST_FILE_SUFFIX; +const FILE_COMPLETE_REMOVEDFILES = "complete_removed-files" + TEST_FILE_SUFFIX; +const FILE_PARTIAL_REMOVEDFILES = "partial_removed-files" + TEST_FILE_SUFFIX; +const FILE_UPDATE_IN_PROGRESS_LOCK = "updated.update_in_progress.lock"; +const COMPARE_LOG_SUFFIX = "_" + mozinfo.os; +const LOG_COMPLETE_SUCCESS = "complete_log_success" + COMPARE_LOG_SUFFIX; +const LOG_PARTIAL_SUCCESS = "partial_log_success" + COMPARE_LOG_SUFFIX; +const LOG_PARTIAL_FAILURE = "partial_log_failure" + COMPARE_LOG_SUFFIX; +const LOG_REPLACE_SUCCESS = "replace_log_success"; + +// xpcshell tests need this preference set to true for Cu.isInAutomation to be +// true. +const PREF_IS_IN_AUTOMATION = + "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer"; + +const USE_EXECV = AppConstants.platform == "linux"; + +const URL_HOST = "http://localhost"; + +const APP_INFO_NAME = "XPCShell"; +const APP_INFO_VENDOR = "Mozilla"; + +const APP_BIN_SUFFIX = + AppConstants.platform == "linux" ? "-bin" : mozinfo.bin_suffix; +const FILE_APP_BIN = AppConstants.MOZ_APP_NAME + APP_BIN_SUFFIX; +const FILE_COMPLETE_EXE = "complete.exe"; +const FILE_HELPER_BIN = "TestAUSHelper" + mozinfo.bin_suffix; +const FILE_MAINTENANCE_SERVICE_BIN = "maintenanceservice.exe"; +const FILE_MAINTENANCE_SERVICE_INSTALLER_BIN = + "maintenanceservice_installer.exe"; +const FILE_OLD_VERSION_MAR = "old_version.mar"; +const FILE_PARTIAL_EXE = "partial.exe"; +const FILE_UPDATER_BIN = "updater" + mozinfo.bin_suffix; + +const PERFORMING_STAGED_UPDATE = "Performing a staged update"; +const CALL_QUIT = "calling QuitProgressUI"; +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_BGTASK_EXCLUSIVE = + "failed to exclusively open executable file from background task: "; + +const LOG_SVC_SUCCESSFUL_LAUNCH = "Process was started... waiting on result."; +const LOG_SVC_UNSUCCESSFUL_LAUNCH = + "The install directory path is not valid for this application."; + +// 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"; + +// Time in seconds the helper application should sleep before exiting. The +// helper can also be made to exit by writing |finish| to its input file. +const HELPER_SLEEP_TIMEOUT = 180; + +// How many of do_timeout calls using FILE_IN_USE_TIMEOUT_MS to wait before the +// test is aborted. +const FILE_IN_USE_TIMEOUT_MS = 1000; + +const PIPE_TO_NULL = + AppConstants.platform == "win" ? ">nul" : "> /dev/null 2>&1"; + +const LOG_FUNCTION = info; + +const gHTTPHandlerPath = "updates.xml"; + +var gIsServiceTest; +var gTestID; + +// This default value will be overridden when using the http server. +var gURLData = URL_HOST + "/"; +var gTestserver; +var gUpdateCheckCount = 0; + +var gIncrementalDownloadErrorType; + +var gResponseBody; + +var gProcess; +var gAppTimer; +var gHandle; + +var gGREDirOrig; +var gGREBinDirOrig; + +var gPIDPersistProcess; + +// Variables are used instead of contants so tests can override these values if +// necessary. +var gCallbackBinFile = "callback_app" + mozinfo.bin_suffix; +var gCallbackArgs = ["./", "callback.log", "Test Arg 2", "Test Arg 3"]; +var gPostUpdateBinFile = "postup_app" + mozinfo.bin_suffix; + +var gTimeoutRuns = 0; + +// Environment related globals +var gShouldResetEnv = undefined; +var gAddedEnvXRENoWindowsCrashDialog = false; +var gEnvXPCOMDebugBreak; +var gEnvXPCOMMemLeakLog; +var gEnvForceServiceFallback = false; + +const URL_HTTP_UPDATE_SJS = "http://test_details/"; +const DATA_URI_SPEC = Services.io.newFileURI(do_get_file("", false)).spec; + +/* import-globals-from shared.js */ +load("shared.js"); + +// Set to true to log additional information for debugging. To log additional +// information for individual tests set gDebugTest to false here and to true in +// the test's onload function. +gDebugTest = true; + +// Setting gDebugTestLog to true will create log files for the tests in +// <objdir>/_tests/xpcshell/toolkit/mozapps/update/tests/<testdir>/ except for +// the service tests since they run sequentially. This can help when debugging +// failures for the tests that intermittently fail when they run in parallel. +// Never set gDebugTestLog to true except when running tests locally. +var gDebugTestLog = false; +// An empty array for gTestsToLog will log most of the output of all of the +// update tests except for the service tests. To only log specific tests add the +// test file name without the file extension to the array below. +var gTestsToLog = []; +var gRealDump; +var gFOS; + +var gTestFiles = []; +var gTestDirs = []; + +// Common files for both successful and failed updates. +var gTestFilesCommon = [ + { + description: "Should never change", + fileName: FILE_UPDATE_SETTINGS_INI, + relPathDir: DIR_RESOURCES, + originalContents: UPDATE_SETTINGS_CONTENTS, + compareContents: UPDATE_SETTINGS_CONTENTS, + originalFile: null, + compareFile: null, + originalPerms: 0o767, + comparePerms: 0o767, + }, + { + description: "Should never change", + fileName: "channel-prefs.js", + relPathDir: DIR_RESOURCES + "defaults/pref/", + originalContents: "ShouldNotBeReplaced\n", + compareContents: "ShouldNotBeReplaced\n", + originalFile: null, + compareFile: null, + originalPerms: 0o767, + comparePerms: 0o767, + }, +]; + +// Files for a complete successful update. This can be used for a complete +// failed update by calling setTestFilesAndDirsForFailure. +var gTestFilesCompleteSuccess = [ + { + description: "Added by update.manifest (add)", + fileName: "precomplete", + relPathDir: DIR_RESOURCES, + originalContents: null, + compareContents: null, + originalFile: FILE_PARTIAL_PRECOMPLETE, + compareFile: FILE_COMPLETE_PRECOMPLETE, + originalPerms: 0o666, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "searchpluginstext0", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: 0o775, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "searchpluginspng1.png", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: null, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "searchpluginspng0.png", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: null, + compareContents: null, + originalFile: "partial.png", + compareFile: "complete.png", + originalPerms: 0o666, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "removed-files", + relPathDir: DIR_RESOURCES, + originalContents: null, + compareContents: null, + originalFile: FILE_PARTIAL_REMOVEDFILES, + compareFile: FILE_COMPLETE_REMOVEDFILES, + originalPerms: 0o666, + comparePerms: 0o644, + }, + { + description: + "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions1text0", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644, + }, + { + description: + "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions1png1.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: null, + originalFile: "partial.png", + compareFile: "complete.png", + originalPerms: 0o666, + comparePerms: 0o644, + }, + { + description: + "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions1png0.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: null, + comparePerms: 0o644, + }, + { + description: + "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions0text0", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644, + }, + { + description: + "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions0png1.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: null, + comparePerms: 0o644, + }, + { + description: + "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions0png0.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: null, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "exe0.exe", + relPathDir: DIR_MACOS, + originalContents: null, + compareContents: null, + originalFile: FILE_HELPER_BIN, + compareFile: FILE_COMPLETE_EXE, + originalPerms: 0o777, + comparePerms: 0o755, + }, + { + description: "Added by update.manifest (add)", + fileName: "10text0", + relPathDir: DIR_RESOURCES + "1/10/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: 0o767, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "0exe0.exe", + relPathDir: DIR_RESOURCES + "0/", + originalContents: null, + compareContents: null, + originalFile: FILE_HELPER_BIN, + compareFile: FILE_COMPLETE_EXE, + originalPerms: 0o777, + comparePerms: 0o755, + }, + { + description: "Added by update.manifest (add)", + fileName: "00text1", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: 0o677, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "00text0", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: "ToBeReplacedWithFromComplete\n", + compareContents: "FromComplete\n", + originalFile: null, + compareFile: null, + originalPerms: 0o775, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "00png0.png", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "complete.png", + originalPerms: 0o776, + comparePerms: 0o644, + }, + { + description: "Removed by precomplete (remove)", + fileName: "20text0", + relPathDir: DIR_RESOURCES + "2/20/", + originalContents: "ToBeDeleted\n", + compareContents: null, + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: null, + }, + { + description: "Removed by precomplete (remove)", + fileName: "20png0.png", + relPathDir: DIR_RESOURCES + "2/20/", + originalContents: "ToBeDeleted\n", + compareContents: null, + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: null, + }, +]; + +// Concatenate the common files to the end of the array. +gTestFilesCompleteSuccess = gTestFilesCompleteSuccess.concat(gTestFilesCommon); + +// Files for a partial successful update. This can be used for a partial failed +// update by calling setTestFilesAndDirsForFailure. +var gTestFilesPartialSuccess = [ + { + description: "Added by update.manifest (add)", + fileName: "precomplete", + relPathDir: DIR_RESOURCES, + originalContents: null, + compareContents: null, + originalFile: FILE_COMPLETE_PRECOMPLETE, + compareFile: FILE_PARTIAL_PRECOMPLETE, + originalPerms: 0o666, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "searchpluginstext0", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: "ToBeReplacedWithFromPartial\n", + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: 0o775, + comparePerms: 0o644, + }, + { + description: "Patched by update.manifest if the file exists (patch-if)", + fileName: "searchpluginspng1.png", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666, + }, + { + description: "Patched by update.manifest if the file exists (patch-if)", + fileName: "searchpluginspng0.png", + relPathDir: DIR_RESOURCES + "searchplugins/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666, + }, + { + description: + "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions1text0", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644, + }, + { + description: + "Patched by update.manifest if the parent directory exists (patch-if)", + fileName: "extensions1png1.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666, + }, + { + description: + "Patched by update.manifest if the parent directory exists (patch-if)", + fileName: "extensions1png0.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666, + }, + { + description: + "Added by update.manifest if the parent directory exists (add-if)", + fileName: "extensions0text0", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: "ToBeReplacedWithFromPartial\n", + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: 0o644, + comparePerms: 0o644, + }, + { + description: + "Patched by update.manifest if the parent directory exists (patch-if)", + fileName: "extensions0png1.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o644, + comparePerms: 0o644, + }, + { + description: + "Patched by update.manifest if the parent directory exists (patch-if)", + fileName: "extensions0png0.png", + relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o644, + comparePerms: 0o644, + }, + { + description: "Patched by update.manifest (patch)", + fileName: "exe0.exe", + relPathDir: DIR_MACOS, + originalContents: null, + compareContents: null, + originalFile: FILE_COMPLETE_EXE, + compareFile: FILE_PARTIAL_EXE, + originalPerms: 0o755, + comparePerms: 0o755, + }, + { + description: "Patched by update.manifest (patch)", + fileName: "0exe0.exe", + relPathDir: DIR_RESOURCES + "0/", + originalContents: null, + compareContents: null, + originalFile: FILE_COMPLETE_EXE, + compareFile: FILE_PARTIAL_EXE, + originalPerms: 0o755, + comparePerms: 0o755, + }, + { + description: "Added by update.manifest (add)", + fileName: "00text0", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: "ToBeReplacedWithFromPartial\n", + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: 0o644, + comparePerms: 0o644, + }, + { + description: "Patched by update.manifest (patch)", + fileName: "00png0.png", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: null, + compareContents: null, + originalFile: "complete.png", + compareFile: "partial.png", + originalPerms: 0o666, + comparePerms: 0o666, + }, + { + description: "Added by update.manifest (add)", + fileName: "20text0", + relPathDir: DIR_RESOURCES + "2/20/", + originalContents: null, + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "20png0.png", + relPathDir: DIR_RESOURCES + "2/20/", + originalContents: null, + compareContents: null, + originalFile: null, + compareFile: "partial.png", + originalPerms: null, + comparePerms: 0o644, + }, + { + description: "Added by update.manifest (add)", + fileName: "00text2", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: null, + compareContents: "FromPartial\n", + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: 0o644, + }, + { + description: "Removed by update.manifest (remove)", + fileName: "10text0", + relPathDir: DIR_RESOURCES + "1/10/", + originalContents: "ToBeDeleted\n", + compareContents: null, + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: null, + }, + { + description: "Removed by update.manifest (remove)", + fileName: "00text1", + relPathDir: DIR_RESOURCES + "0/00/", + originalContents: "ToBeDeleted\n", + compareContents: null, + originalFile: null, + compareFile: null, + originalPerms: null, + comparePerms: null, + }, +]; + +// Concatenate the common files to the end of the array. +gTestFilesPartialSuccess = gTestFilesPartialSuccess.concat(gTestFilesCommon); + +var gTestDirsCommon = [ + { + relPathDir: DIR_RESOURCES + "3/", + dirRemoved: false, + files: ["3text0", "3text1"], + filesRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "4/", + dirRemoved: true, + files: ["4text0", "4text1"], + filesRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "5/", + dirRemoved: true, + files: ["5test.exe", "5text0", "5text1"], + filesRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "6/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "7/", + dirRemoved: true, + files: ["7text0", "7text1"], + subDirs: ["70/", "71/"], + subDirFiles: ["7xtest.exe", "7xtext0", "7xtext1"], + }, + { + relPathDir: DIR_RESOURCES + "8/", + dirRemoved: false, + }, + { + relPathDir: DIR_RESOURCES + "8/80/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "8/81/", + dirRemoved: false, + files: ["81text0", "81text1"], + }, + { + relPathDir: DIR_RESOURCES + "8/82/", + dirRemoved: false, + subDirs: ["820/", "821/"], + }, + { + relPathDir: DIR_RESOURCES + "8/83/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "8/84/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "8/85/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "8/86/", + dirRemoved: true, + files: ["86text0", "86text1"], + }, + { + relPathDir: DIR_RESOURCES + "8/87/", + dirRemoved: true, + subDirs: ["870/", "871/"], + subDirFiles: ["87xtext0", "87xtext1"], + }, + { + relPathDir: DIR_RESOURCES + "8/88/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "8/89/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "9/90/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "9/91/", + dirRemoved: false, + files: ["91text0", "91text1"], + }, + { + relPathDir: DIR_RESOURCES + "9/92/", + dirRemoved: false, + subDirs: ["920/", "921/"], + }, + { + relPathDir: DIR_RESOURCES + "9/93/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "9/94/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "9/95/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "9/96/", + dirRemoved: true, + files: ["96text0", "96text1"], + }, + { + relPathDir: DIR_RESOURCES + "9/97/", + dirRemoved: true, + subDirs: ["970/", "971/"], + subDirFiles: ["97xtext0", "97xtext1"], + }, + { + relPathDir: DIR_RESOURCES + "9/98/", + dirRemoved: true, + }, + { + relPathDir: DIR_RESOURCES + "9/99/", + dirRemoved: true, + }, + { + description: + "Silences 'WARNING: Failed to resolve XUL App Dir.' in debug builds", + relPathDir: DIR_RESOURCES + "browser", + dirRemoved: false, + }, +]; + +// Directories for a complete successful update. This array can be used for a +// complete failed update by calling setTestFilesAndDirsForFailure. +var gTestDirsCompleteSuccess = [ + { + description: "Removed by precomplete (rmdir)", + relPathDir: DIR_RESOURCES + "2/20/", + dirRemoved: true, + }, + { + description: "Removed by precomplete (rmdir)", + relPathDir: DIR_RESOURCES + "2/", + dirRemoved: true, + }, +]; + +// Concatenate the common files to the beginning of the array. +gTestDirsCompleteSuccess = gTestDirsCommon.concat(gTestDirsCompleteSuccess); + +// Directories for a partial successful update. This array can be used for a +// partial failed update by calling setTestFilesAndDirsForFailure. +var gTestDirsPartialSuccess = [ + { + description: "Removed by update.manifest (rmdir)", + relPathDir: DIR_RESOURCES + "1/10/", + dirRemoved: true, + }, + { + description: "Removed by update.manifest (rmdir)", + relPathDir: DIR_RESOURCES + "1/", + dirRemoved: true, + }, +]; + +// Concatenate the common files to the beginning of the array. +gTestDirsPartialSuccess = gTestDirsCommon.concat(gTestDirsPartialSuccess); + +/** + * Helper function for setting up the test environment. + * + * @param aAppUpdateAutoEnabled + * See setAppUpdateAutoSync in shared.js for details. + * @param aAllowBits + * If true, allow update downloads via the Windows BITS service. + * If false, this download mechanism will not be used. + */ +function setupTestCommon(aAppUpdateAutoEnabled = false, aAllowBits = false) { + debugDump("start - general test setup"); + + Assert.strictEqual( + gTestID, + undefined, + "gTestID should be 'undefined' (setupTestCommon should " + + "only be called once)" + ); + + let caller = Components.stack.caller; + gTestID = caller.filename + .toString() + .split("/") + .pop() + .split(".")[0]; + + if (gDebugTestLog && !gIsServiceTest) { + if (!gTestsToLog.length || gTestsToLog.includes(gTestID)) { + let logFile = do_get_file(gTestID + ".log", true); + if (!logFile.exists()) { + logFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + } + gFOS = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + gFOS.init(logFile, MODE_WRONLY | MODE_APPEND, PERMS_FILE, 0); + + gRealDump = dump; + dump = dumpOverride; + } + } + + createAppInfo("xpcshell@tests.mozilla.org", APP_INFO_NAME, "1.0", "2.0"); + + if (gIsServiceTest && !shouldRunServiceTest()) { + return false; + } + + do_test_pending(); + + setDefaultPrefs(); + + gGREDirOrig = getGREDir(); + gGREBinDirOrig = getGREBinDir(); + + let applyDir = getApplyDirFile().parent; + + // Try to remove the directory used to apply updates and the updates directory + // on platforms other than Windows. This is non-fatal for the test since if + // this fails a different directory will be used. + if (applyDir.exists()) { + debugDump("attempting to remove directory. Path: " + applyDir.path); + try { + removeDirRecursive(applyDir); + } catch (e) { + logTestInfo( + "non-fatal error removing directory. Path: " + + applyDir.path + + ", Exception: " + + e + ); + // When the application doesn't exit properly it can cause the test to + // fail again on the second run with an NS_ERROR_FILE_ACCESS_DENIED error + // along with no useful information in the test log. To prevent this use + // a different directory for the test when it isn't possible to remove the + // existing test directory (bug 1294196). + gTestID += "_new"; + logTestInfo( + "using a new directory for the test by changing gTestID " + + "since there is an existing test directory that can't be " + + "removed, gTestID: " + + gTestID + ); + } + } + + if (AppConstants.platform == "win") { + Services.prefs.setBoolPref( + PREF_APP_UPDATE_SERVICE_ENABLED, + !!gIsServiceTest + ); + } + + if (gIsServiceTest) { + let exts = ["id", "log", "status"]; + for (let i = 0; i < exts.length; ++i) { + let file = getSecureOutputFile(exts[i]); + if (file.exists()) { + try { + file.remove(false); + } catch (e) {} + } + } + } + + adjustGeneralPaths(); + createWorldWritableAppUpdateDir(); + + // Logged once here instead of in the mock directory provider to lessen test + // log spam. + debugDump("Updates Directory (UpdRootD) Path: " + getMockUpdRootD().path); + + // This prevents a warning about not being able to find the greprefs.js file + // from being logged. + let grePrefsFile = getGREDir(); + if (!grePrefsFile.exists()) { + grePrefsFile.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } + grePrefsFile.append("greprefs.js"); + if (!grePrefsFile.exists()) { + grePrefsFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + } + + // The name of the update lock needs to be changed to match the path + // overridden in adjustGeneralPaths() above. Wait until now to reset + // because the GRE dir now exists, which may cause the "install + // path" to be normalized differently now that it can be resolved. + debugDump("resetting update lock"); + let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + syncManager.resetLock(); + + // Remove the updates directory on Windows and Mac OS X which is located + // outside of the application directory after the call to adjustGeneralPaths + // has set it up. Since the test hasn't ran yet and the directory shouldn't + // exist this is non-fatal for the test. + if (AppConstants.platform == "win" || AppConstants.platform == "macosx") { + let updatesDir = getMockUpdRootD(); + if (updatesDir.exists()) { + debugDump("attempting to remove directory. Path: " + updatesDir.path); + try { + removeDirRecursive(updatesDir); + } catch (e) { + logTestInfo( + "non-fatal error removing directory. Path: " + + updatesDir.path + + ", Exception: " + + e + ); + } + } + } + + setAppUpdateAutoSync(aAppUpdateAutoEnabled); + Services.prefs.setBoolPref(PREF_APP_UPDATE_BITS_ENABLED, aAllowBits); + + debugDump("finish - general test setup"); + return true; +} + +/** + * Nulls out the most commonly used global vars used by tests to prevent leaks + * as needed and attempts to restore the system to its original state. + */ +function cleanupTestCommon() { + debugDump("start - general test cleanup"); + + if (gChannel) { + gPrefRoot.removeObserver(PREF_APP_UPDATE_CHANNEL, observer); + } + + gTestserver = null; + + if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") { + // This will delete the launch script if it exists. + getLaunchScript(); + } + + if (gIsServiceTest) { + let exts = ["id", "log", "status"]; + for (let i = 0; i < exts.length; ++i) { + let file = getSecureOutputFile(exts[i]); + if (file.exists()) { + try { + file.remove(false); + } catch (e) {} + } + } + } + + if (AppConstants.platform == "win" && MOZ_APP_BASENAME) { + let appDir = getApplyDirFile(); + let vendor = MOZ_APP_VENDOR ? MOZ_APP_VENDOR : "Mozilla"; + const REG_PATH = + "SOFTWARE\\" + vendor + "\\" + MOZ_APP_BASENAME + "\\TaskBarIDs"; + let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + key.open( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_ALL + ); + if (key.hasValue(appDir.path)) { + key.removeValue(appDir.path); + } + } catch (e) {} + try { + key.open( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_ALL + ); + if (key.hasValue(appDir.path)) { + key.removeValue(appDir.path); + } + } catch (e) {} + } + + // The updates directory is located outside of the application directory and + // needs to be removed on Windows and Mac OS X. + if (AppConstants.platform == "win" || AppConstants.platform == "macosx") { + let updatesDir = getMockUpdRootD(); + // Try to remove the directory used to apply updates. Since the test has + // already finished this is non-fatal for the test. + if (updatesDir.exists()) { + debugDump("attempting to remove directory. Path: " + updatesDir.path); + try { + removeDirRecursive(updatesDir); + } catch (e) { + logTestInfo( + "non-fatal error removing directory. Path: " + + updatesDir.path + + ", Exception: " + + e + ); + } + if (AppConstants.platform == "macosx") { + let updatesRootDir = gUpdatesRootDir.clone(); + while (updatesRootDir.path != updatesDir.path) { + if (updatesDir.exists()) { + debugDump( + "attempting to remove directory. Path: " + updatesDir.path + ); + try { + // Try to remove the directory without the recursive flag set + // since the top level directory has already had its contents + // removed and the parent directory might still be used by a + // different test. + updatesDir.remove(false); + } catch (e) { + logTestInfo( + "non-fatal error removing directory. Path: " + + updatesDir.path + + ", Exception: " + + e + ); + if (e == Cr.NS_ERROR_FILE_DIR_NOT_EMPTY) { + break; + } + } + } + updatesDir = updatesDir.parent; + } + } + } + } + + let applyDir = getApplyDirFile().parent; + + // Try to remove the directory used to apply updates. Since the test has + // already finished this is non-fatal for the test. + if (applyDir.exists()) { + debugDump("attempting to remove directory. Path: " + applyDir.path); + try { + removeDirRecursive(applyDir); + } catch (e) { + logTestInfo( + "non-fatal error removing directory. Path: " + + applyDir.path + + ", Exception: " + + e + ); + } + } + + resetEnvironment(); + Services.prefs.clearUserPref(PREF_APP_UPDATE_BITS_ENABLED); + + debugDump("finish - general test cleanup"); + + if (gRealDump) { + dump = gRealDump; + gRealDump = null; + } + + if (gFOS) { + gFOS.close(); + } +} + +/** + * Helper function to store the log output of calls to dump in a variable so the + * values can be written to a file for a parallel run of a test and printed to + * the log file when the test runs synchronously. + */ +function dumpOverride(aText) { + gFOS.write(aText, aText.length); + gRealDump(aText); +} + +/** + * Helper function that calls do_test_finished that tracks whether a parallel + * run of a test passed when it runs synchronously so the log output can be + * inspected. + */ +function doTestFinish() { + if (gDebugTest) { + // This prevents do_print errors from being printed by the xpcshell test + // harness due to nsUpdateService.js logging to the console when the + // app.update.log preference is true. + Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, false); + gAUS.observe(null, "nsPref:changed", PREF_APP_UPDATE_LOG); + } + + reloadUpdateManagerData(true); + + // Call app update's observe method passing quit-application to test that the + // shutdown of app update runs without throwing or leaking. The observer + // method is used directly instead of calling notifyObservers so components + // outside of the scope of this test don't assert and thereby cause app update + // tests to fail. + gAUS.observe(null, "quit-application", ""); + + executeSoon(do_test_finished); +} + +/** + * Sets the most commonly used preferences used by tests + */ +function setDefaultPrefs() { + Services.prefs.setBoolPref(PREF_IS_IN_AUTOMATION, true); + Services.prefs.setBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false); + if (gDebugTest) { + // Enable Update logging + Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, true); + } else { + // Some apps set this preference to true by default + Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, false); + } +} + +/** + * Helper function for updater binary tests that sets the appropriate values + * to check for update failures. + */ +function setTestFilesAndDirsForFailure() { + gTestFiles.forEach(function STFADFF_Files(aTestFile) { + aTestFile.compareContents = aTestFile.originalContents; + aTestFile.compareFile = aTestFile.originalFile; + aTestFile.comparePerms = aTestFile.originalPerms; + }); + + gTestDirs.forEach(function STFADFF_Dirs(aTestDir) { + aTestDir.dirRemoved = false; + if (aTestDir.filesRemoved) { + aTestDir.filesRemoved = false; + } + }); +} + +/** + * Helper function for updater binary tests that prevents the distribution + * directory files from being created. + */ +function preventDistributionFiles() { + gTestFiles = gTestFiles.filter(function(aTestFile) { + return !aTestFile.relPathDir.includes("distribution/"); + }); + + gTestDirs = gTestDirs.filter(function(aTestDir) { + return !aTestDir.relPathDir.includes("distribution/"); + }); +} + +/** + * On Mac OS X this sets the last modified time for the app bundle directory to + * a date in the past to test that the last modified time is updated when an + * update has been successfully applied (bug 600098). + */ +function setAppBundleModTime() { + if (AppConstants.platform != "macosx") { + return; + } + let now = Date.now(); + let yesterday = now - 1000 * 60 * 60 * 24; + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; +} + +/** + * On Mac OS X this checks that the last modified time for the app bundle + * directory has been updated when an update has been successfully applied + * (bug 600098). + */ +function checkAppBundleModTime() { + if (AppConstants.platform != "macosx") { + return; + } + // All we care about is that the last modified time has changed so that Mac OS + // X Launch Services invalidates its cache so the test allows up to one minute + // difference in the last modified time. + const MAC_MAX_TIME_DIFFERENCE = 60000; + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + Assert.ok( + timeDiff < MAC_MAX_TIME_DIFFERENCE, + "the last modified time on the apply to directory should " + + "change after a successful update" + ); +} + +/** + * Performs Update Manager checks to verify that the update metadata is correct + * and that it is the same after the update xml files are reloaded. + * + * @param aStatusFileState + * The expected state of the status file. + * @param aHasActiveUpdate + * Should there be an active update. + * @param aUpdateStatusState + * The expected update's status state. + * @param aUpdateErrCode + * The expected update's error code. + * @param aUpdateCount + * The update history's update count. + */ +function checkUpdateManager( + aStatusFileState, + aHasActiveUpdate, + aUpdateStatusState, + aUpdateErrCode, + aUpdateCount +) { + let activeUpdate = + aUpdateStatusState == STATE_DOWNLOADING + ? gUpdateManager.downloadingUpdate + : gUpdateManager.readyUpdate; + Assert.equal( + readStatusState(), + aStatusFileState, + "the status file state" + MSG_SHOULD_EQUAL + ); + let msgTags = [" after startup ", " after a file reload "]; + for (let i = 0; i < msgTags.length; ++i) { + logTestInfo( + "checking Update Manager updates" + msgTags[i] + "is performed" + ); + if (aHasActiveUpdate) { + Assert.ok( + !!activeUpdate, + msgTags[i] + "the active update should be defined" + ); + } else { + Assert.ok( + !activeUpdate, + msgTags[i] + "the active update should not be defined" + ); + } + Assert.equal( + gUpdateManager.getUpdateCount(), + aUpdateCount, + msgTags[i] + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL + ); + if (aUpdateCount > 0) { + let update = gUpdateManager.getUpdateAt(0); + Assert.equal( + update.state, + aUpdateStatusState, + msgTags[i] + "the first update state" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.errorCode, + aUpdateErrCode, + msgTags[i] + "the first update errorCode" + MSG_SHOULD_EQUAL + ); + } + if (i != msgTags.length - 1) { + reloadUpdateManagerData(); + } + } +} + +/** + * Waits until the update files exist or not based on the parameters specified + * when calling this function or the default values if the parameters are not + * specified. This is necessary due to the update xml files being written + * asynchronously by nsIUpdateManager. + * + * @param aActiveUpdateExists (optional) + * Whether the active-update.xml file should exist (default is false). + * @param aUpdatesExists (optional) + * Whether the updates.xml file should exist (default is true). + */ +async function waitForUpdateXMLFiles( + aActiveUpdateExists = false, + aUpdatesExists = true +) { + function areFilesStabilized() { + let file = getUpdateDirFile(FILE_ACTIVE_UPDATE_XML_TMP); + if (file.exists()) { + debugDump("file exists, Path: " + file.path); + return false; + } + file = getUpdateDirFile(FILE_UPDATES_XML_TMP); + if (file.exists()) { + debugDump("file exists, Path: " + file.path); + return false; + } + file = getUpdateDirFile(FILE_ACTIVE_UPDATE_XML); + if (file.exists() != aActiveUpdateExists) { + debugDump( + "file exists should equal: " + + aActiveUpdateExists + + ", Path: " + + file.path + ); + return false; + } + file = getUpdateDirFile(FILE_UPDATES_XML); + if (file.exists() != aUpdatesExists) { + debugDump( + "file exists should equal: " + + aActiveUpdateExists + + ", Path: " + + file.path + ); + return false; + } + return true; + } + + await TestUtils.waitForCondition( + () => areFilesStabilized(), + "Waiting for update xml files to stabilize" + ); +} + +/** + * On Mac OS X and Windows this checks if the post update '.running' file exists + * to determine if the post update binary was launched. + * + * @param aShouldExist + * Whether the post update '.running' file should exist. + */ +function checkPostUpdateRunningFile(aShouldExist) { + if (AppConstants.platform == "linux") { + return; + } + let postUpdateRunningFile = getPostUpdateFile(".running"); + if (aShouldExist) { + Assert.ok( + postUpdateRunningFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(postUpdateRunningFile.path) + ); + } else { + Assert.ok( + !postUpdateRunningFile.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(postUpdateRunningFile.path) + ); + } +} + +/** + * Initializes the most commonly used settings and creates an instance of the + * update service stub. + */ +function standardInit() { + // Initialize the update service stub component + initUpdateServiceStub(); +} + +/** + * Helper function for getting the application version from the application.ini + * file. This will look in both the GRE and the application directories for the + * application.ini file. + * + * @return The version string from the application.ini file. + */ +function getAppVersion() { + // Read the application.ini and use its application version. + let iniFile = gGREDirOrig.clone(); + iniFile.append(FILE_APPLICATION_INI); + if (!iniFile.exists()) { + iniFile = gGREBinDirOrig.clone(); + iniFile.append(FILE_APPLICATION_INI); + } + Assert.ok(iniFile.exists(), MSG_SHOULD_EXIST + getMsgPath(iniFile.path)); + let iniParser = Cc["@mozilla.org/xpcom/ini-parser-factory;1"] + .getService(Ci.nsIINIParserFactory) + .createINIParser(iniFile); + return iniParser.getString("App", "Version"); +} + +/** + * Helper function for getting the path to the directory where the + * application binary is located (e.g. <test_file_leafname>/dir.app/). + * + * Note: The dir.app subdirectory under <test_file_leafname> is needed for + * platforms other than Mac OS X so the tests can run in parallel due to + * update staging creating a lock file named moz_update_in_progress.lock in + * the parent directory of the installation directory. + * Note: For service tests with IS_AUTHENTICODE_CHECK_ENABLED we use an absolute + * path inside Program Files because the service itself will refuse to + * update an installation not located in Program Files. + * + * @return The path to the directory where application binary is located. + */ +function getApplyDirPath() { + if (gIsServiceTest && IS_AUTHENTICODE_CHECK_ENABLED) { + let dir = getMaintSvcDir(); + dir.append(gTestID); + dir.append("dir.app"); + return dir.path; + } + return gTestID + "/dir.app/"; +} + +/** + * Helper function for getting the nsIFile for a file in the directory where the + * update will be applied. + * + * The files for the update are located two directories below the apply to + * directory since Mac OS X sets the last modified time for the root directory + * to the current time and if the update changes any files in the root directory + * then it wouldn't be possible to test (bug 600098). + * + * @param aRelPath (optional) + * The relative path to the file or directory to get from the root of + * the test's directory. If not specified the test's directory will be + * returned. + * @return The nsIFile for the file in the directory where the update will be + * applied. + */ +function getApplyDirFile(aRelPath) { + // do_get_file only supports relative paths, but under these conditions we + // need to use an absolute path in Program Files instead. + if (gIsServiceTest && IS_AUTHENTICODE_CHECK_ENABLED) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(getApplyDirPath()); + if (aRelPath) { + if (aRelPath == "..") { + file = file.parent; + } else { + aRelPath = aRelPath.replace(/\//g, "\\"); + file.appendRelativePath(aRelPath); + } + } + return file; + } + let relpath = getApplyDirPath() + (aRelPath ? aRelPath : ""); + return do_get_file(relpath, true); +} + +/** + * Helper function for getting the relative path to the directory where the + * test data files are located. + * + * @return The relative path to the directory where the test data files are + * located. + */ +function getTestDirPath() { + return "../data/"; +} + +/** + * Helper function for getting the nsIFile for a file in the test data + * directory. + * + * @param aRelPath (optional) + * The relative path to the file or directory to get from the root of + * the test's data directory. If not specified the test's data + * directory will be returned. + * @param aAllowNonExists (optional) + * Whether or not to throw an error if the path exists. + * If not specified, then false is used. + * @return The nsIFile for the file in the test data directory. + * @throws If the file or directory does not exist. + */ +function getTestDirFile(aRelPath, aAllowNonExists) { + let relpath = getTestDirPath() + (aRelPath ? aRelPath : ""); + return do_get_file(relpath, !!aAllowNonExists); +} + +/** + * Helper function for getting the nsIFile for the maintenance service + * directory on Windows. + * + * @return The nsIFile for the maintenance service directory. + * @throws If called from a platform other than Windows. + */ +function getMaintSvcDir() { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + const CSIDL_PROGRAM_FILES = 0x26; + const CSIDL_PROGRAM_FILESX86 = 0x2a; + // This will return an empty string on our Win XP build systems. + let maintSvcDir = getSpecialFolderDir(CSIDL_PROGRAM_FILESX86); + if (maintSvcDir) { + maintSvcDir.append("Mozilla Maintenance Service"); + debugDump( + "using CSIDL_PROGRAM_FILESX86 - maintenance service install " + + "directory path: " + + maintSvcDir.path + ); + } + if (!maintSvcDir || !maintSvcDir.exists()) { + maintSvcDir = getSpecialFolderDir(CSIDL_PROGRAM_FILES); + if (maintSvcDir) { + maintSvcDir.append("Mozilla Maintenance Service"); + debugDump( + "using CSIDL_PROGRAM_FILES - maintenance service install " + + "directory path: " + + maintSvcDir.path + ); + } + } + if (!maintSvcDir) { + do_throw("Unable to find the maintenance service install directory"); + } + + return maintSvcDir; +} + +/** + * Reads the current update operation/state in the status file in the secure + * update log directory. + * + * @return The status value. + */ +function readSecureStatusFile() { + let file = getSecureOutputFile("status"); + if (!file.exists()) { + debugDump("update status file does not exist, path: " + file.path); + return STATE_NONE; + } + return readFile(file).split("\n")[0]; +} + +/** + * Get an nsIFile for a file in the secure update log directory. The file name + * is always the value of gTestID and the file extension is specified by the + * aFileExt parameter. + * + * @param aFileExt + * The file extension. + * @return The nsIFile of the secure update file. + */ +function getSecureOutputFile(aFileExt) { + let file = getMaintSvcDir(); + file.append("UpdateLogs"); + file.append(gTestID + "." + aFileExt); + return file; +} + +/** + * Get the nsIFile for a Windows special folder determined by the CSIDL + * passed. + * + * @param aCSIDL + * The CSIDL for the Windows special folder. + * @return The nsIFile for the Windows special folder. + * @throws If called from a platform other than Windows. + */ +function getSpecialFolderDir(aCSIDL) { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + let lib = ctypes.open("shell32"); + let SHGetSpecialFolderPath = lib.declare( + "SHGetSpecialFolderPathW", + ctypes.winapi_abi, + ctypes.bool /* bool(return) */, + ctypes.int32_t /* HWND hwndOwner */, + ctypes.char16_t.ptr /* LPTSTR lpszPath */, + ctypes.int32_t /* int csidl */, + ctypes.bool /* BOOL fCreate */ + ); + + let aryPath = ctypes.char16_t.array()(260); + let rv = SHGetSpecialFolderPath(0, aryPath, aCSIDL, false); + if (!rv) { + do_throw( + "SHGetSpecialFolderPath failed to retrieve " + + aCSIDL + + " with Win32 error " + + ctypes.winLastError + ); + } + lib.close(); + + let path = aryPath.readString(); // Convert the c-string to js-string + if (!path) { + return null; + } + debugDump("SHGetSpecialFolderPath returned path: " + path); + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath(path); + return dir; +} + +XPCOMUtils.defineLazyGetter(this, "gInstallDirPathHash", function test_gIDPH() { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + if (!MOZ_APP_BASENAME) { + return null; + } + + let vendor = MOZ_APP_VENDOR ? MOZ_APP_VENDOR : "Mozilla"; + let appDir = getApplyDirFile(); + + const REG_PATH = + "SOFTWARE\\" + vendor + "\\" + MOZ_APP_BASENAME + "\\TaskBarIDs"; + let regKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + regKey.open( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_ALL + ); + regKey.writeStringValue(appDir.path, gTestID); + return gTestID; + } catch (e) {} + + try { + regKey.create( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_ALL + ); + regKey.writeStringValue(appDir.path, gTestID); + return gTestID; + } catch (e) { + logTestInfo( + "failed to create registry value. Registry Path: " + + REG_PATH + + ", Value Name: " + + appDir.path + + ", Value Data: " + + gTestID + + ", Exception " + + e + ); + do_throw( + "Unable to write HKLM or HKCU TaskBarIDs registry value, key path: " + + REG_PATH + ); + } + return null; +}); + +XPCOMUtils.defineLazyGetter(this, "gLocalAppDataDir", function test_gLADD() { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + const CSIDL_LOCAL_APPDATA = 0x1c; + return getSpecialFolderDir(CSIDL_LOCAL_APPDATA); +}); + +XPCOMUtils.defineLazyGetter(this, "gCommonAppDataDir", function test_gCDD() { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + const CSIDL_COMMON_APPDATA = 0x0023; + return getSpecialFolderDir(CSIDL_COMMON_APPDATA); +}); + +XPCOMUtils.defineLazyGetter(this, "gProgFilesDir", function test_gPFD() { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + const CSIDL_PROGRAM_FILES = 0x26; + return getSpecialFolderDir(CSIDL_PROGRAM_FILES); +}); + +/** + * Helper function for getting the update root directory used by the tests. This + * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir + * in nsXREDirProvider.cpp so an application will be able to find the update + * when running a test that launches the application. + * + * The aGetOldLocation argument performs the same function that the argument + * with the same name in nsXREDirProvider::GetUpdateRootDir performs. If true, + * the old (pre-migration) update directory is returned. + */ +function getMockUpdRootD(aGetOldLocation = false) { + if (AppConstants.platform == "win") { + return getMockUpdRootDWin(aGetOldLocation); + } + + if (AppConstants.platform == "macosx") { + return getMockUpdRootDMac(); + } + + return getApplyDirFile(DIR_MACOS); +} + +/** + * Helper function for getting the update root directory used by the tests. This + * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir + * in nsXREDirProvider.cpp so an application will be able to find the update + * when running a test that launches the application. + * + * @throws If called from a platform other than Windows. + */ +function getMockUpdRootDWin(aGetOldLocation) { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + let relPathUpdates = ""; + let dataDirectory = gCommonAppDataDir.clone(); + if (aGetOldLocation) { + relPathUpdates += "Mozilla"; + } else { + relPathUpdates += "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38"; + } + + relPathUpdates += "\\" + DIR_UPDATES + "\\" + gInstallDirPathHash; + let updatesDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + updatesDir.initWithPath(dataDirectory.path + "\\" + relPathUpdates); + return updatesDir; +} + +function createWorldWritableAppUpdateDir() { + // This function is only necessary in Windows + if (AppConstants.platform == "win") { + let installDir = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile) + .parent; + let exitValue = runTestHelperSync(["create-update-dir", installDir.path]); + Assert.equal(exitValue, 0, "The helper process exit value should be 0"); + } +} + +XPCOMUtils.defineLazyGetter(this, "gUpdatesRootDir", function test_gURD() { + if (AppConstants.platform != "macosx") { + do_throw("Mac OS X only function called by a different platform!"); + } + + let dir = Services.dirsvc.get("ULibDir", Ci.nsIFile); + dir.append("Caches"); + if (MOZ_APP_VENDOR || MOZ_APP_BASENAME) { + dir.append(MOZ_APP_VENDOR ? MOZ_APP_VENDOR : MOZ_APP_BASENAME); + } else { + dir.append("Mozilla"); + } + dir.append(DIR_UPDATES); + return dir; +}); + +/** + * Helper function for getting the update root directory used by the tests. This + * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir + * in nsXREDirProvider.cpp so an application will be able to find the update + * when running a test that launches the application. + */ +function getMockUpdRootDMac() { + if (AppConstants.platform != "macosx") { + do_throw("Mac OS X only function called by a different platform!"); + } + + let appDir = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).parent + .parent.parent; + let appDirPath = appDir.path; + appDirPath = appDirPath.substr(0, appDirPath.length - 4); + + let pathUpdates = gUpdatesRootDir.path + appDirPath; + let updatesDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + updatesDir.initWithPath(pathUpdates); + return updatesDir; +} + +/** + * Creates an update in progress lock file in the specified directory on + * Windows. + * + * @param aDir + * The nsIFile for the directory where the lock file should be created. + * @throws If called from a platform other than Windows. + */ +function createUpdateInProgressLockFile(aDir) { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + let file = aDir.clone(); + file.append(FILE_UPDATE_IN_PROGRESS_LOCK); + file.create(file.NORMAL_FILE_TYPE, 0o444); + file.QueryInterface(Ci.nsILocalFileWin); + file.readOnly = true; + Assert.ok(file.exists(), MSG_SHOULD_EXIST + getMsgPath(file.path)); + Assert.ok(!file.isWritable(), "the lock file should not be writeable"); +} + +/** + * Removes an update in progress lock file in the specified directory on + * Windows. + * + * @param aDir + * The nsIFile for the directory where the lock file is located. + * @throws If called from a platform other than Windows. + */ +function removeUpdateInProgressLockFile(aDir) { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + let file = aDir.clone(); + file.append(FILE_UPDATE_IN_PROGRESS_LOCK); + file.QueryInterface(Ci.nsILocalFileWin); + file.readOnly = false; + file.remove(false); + Assert.ok(!file.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(file.path)); +} + +/** + * Copies the test updater to the GRE binary directory and returns the nsIFile + * for the copied test updater. + * + * @return nsIFIle for the copied test updater. + */ +function copyTestUpdaterToBinDir() { + let updaterLeafName = + AppConstants.platform == "macosx" ? "updater.app" : FILE_UPDATER_BIN; + let testUpdater = getTestDirFile(updaterLeafName); + let updater = getGREBinDir(); + updater.append(updaterLeafName); + if (!updater.exists()) { + testUpdater.copyToFollowingLinks(updater.parent, updaterLeafName); + } + if (AppConstants.platform == "macosx") { + updater.append("Contents"); + updater.append("MacOS"); + updater.append("org.mozilla.updater"); + } + return updater; +} + +/** + * Logs the contents of an update log and for maintenance service tests this + * will log the contents of the latest maintenanceservice.log. + * + * @param aLogLeafName + * The leaf name of the update log. + */ +function logUpdateLog(aLogLeafName) { + let updateLog = getUpdateDirFile(aLogLeafName); + if (updateLog.exists()) { + // xpcshell tests won't display the entire contents so log each line. + let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n"); + updateLogContents = removeTimeStamps(updateLogContents); + updateLogContents = replaceLogPaths(updateLogContents); + let aryLogContents = updateLogContents.split("\n"); + logTestInfo("contents of " + updateLog.path + ":"); + aryLogContents.forEach(function LU_ULC_FE(aLine) { + logTestInfo(aLine); + }); + } else { + logTestInfo("update log doesn't exist, path: " + updateLog.path); + } + + if (gIsServiceTest) { + let secureStatus = readSecureStatusFile(); + logTestInfo("secure update status: " + secureStatus); + + updateLog = getSecureOutputFile("log"); + if (updateLog.exists()) { + // xpcshell tests won't display the entire contents so log each line. + let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n"); + updateLogContents = removeTimeStamps(updateLogContents); + updateLogContents = replaceLogPaths(updateLogContents); + let aryLogContents = updateLogContents.split("\n"); + logTestInfo("contents of " + updateLog.path + ":"); + aryLogContents.forEach(function LU_SULC_FE(aLine) { + logTestInfo(aLine); + }); + } else { + logTestInfo("secure update log doesn't exist, path: " + updateLog.path); + } + + let serviceLog = getMaintSvcDir(); + serviceLog.append("logs"); + serviceLog.append("maintenanceservice.log"); + if (serviceLog.exists()) { + // xpcshell tests won't display the entire contents so log each line. + let serviceLogContents = readFileBytes(serviceLog).replace(/\r\n/g, "\n"); + serviceLogContents = replaceLogPaths(serviceLogContents); + let aryLogContents = serviceLogContents.split("\n"); + logTestInfo("contents of " + serviceLog.path + ":"); + aryLogContents.forEach(function LU_MSLC_FE(aLine) { + logTestInfo(aLine); + }); + } else { + logTestInfo( + "maintenance service log doesn't exist, path: " + serviceLog.path + ); + } + } +} + +/** + * Gets the maintenance service log contents. + */ +function readServiceLogFile() { + let file = getMaintSvcDir(); + file.append("logs"); + file.append("maintenanceservice.log"); + return readFile(file); +} + +/** + * Launches the updater binary to apply an update for updater tests. + * + * @param aExpectedStatus + * The expected value of update.status when the update finishes. For + * service tests passing STATE_PENDING or STATE_APPLIED will change the + * value to STATE_PENDING_SVC and STATE_APPLIED_SVC respectively. + * @param aSwitchApp + * If true the update should switch the application with an updated + * staged application and if false the update should be applied to the + * installed application. + * @param aExpectedExitValue + * The expected exit value from the updater binary for non-service + * tests. + * @param aCheckSvcLog + * Whether the service log should be checked for service tests. + * @param aPatchDirPath (optional) + * When specified the patch directory path to use for invalid argument + * tests otherwise the normal path will be used. + * @param aInstallDirPath (optional) + * When specified the install directory path to use for invalid + * argument tests otherwise the normal path will be used. + * @param aApplyToDirPath (optional) + * When specified the apply to / working directory path to use for + * invalid argument tests otherwise the normal path will be used. + * @param aCallbackPath (optional) + * When specified the callback path to use for invalid argument tests + * otherwise the normal path will be used. + */ +function runUpdate( + aExpectedStatus, + aSwitchApp, + aExpectedExitValue, + aCheckSvcLog, + aPatchDirPath, + aInstallDirPath, + aApplyToDirPath, + aCallbackPath +) { + let isInvalidArgTest = + !!aPatchDirPath || + !!aInstallDirPath || + !!aApplyToDirPath || + !!aCallbackPath; + + let svcOriginalLog; + if (gIsServiceTest) { + copyFileToTestAppDir(FILE_MAINTENANCE_SERVICE_BIN, false); + copyFileToTestAppDir(FILE_MAINTENANCE_SERVICE_INSTALLER_BIN, false); + if (aCheckSvcLog) { + svcOriginalLog = readServiceLogFile(); + } + } + + let pid = 0; + if (gPIDPersistProcess) { + pid = gPIDPersistProcess.pid; + Services.env.set("MOZ_TEST_SHORTER_WAIT_PID", "1"); + } + + let updateBin = copyTestUpdaterToBinDir(); + Assert.ok(updateBin.exists(), MSG_SHOULD_EXIST + getMsgPath(updateBin.path)); + + let updatesDirPath = aPatchDirPath || getUpdateDirFile(DIR_PATCH).path; + let installDirPath = aInstallDirPath || getApplyDirFile().path; + let applyToDirPath = aApplyToDirPath || getApplyDirFile().path; + let stageDirPath = aApplyToDirPath || getStageDirFile().path; + + let callbackApp = getApplyDirFile(DIR_RESOURCES + gCallbackBinFile); + Assert.ok( + callbackApp.exists(), + MSG_SHOULD_EXIST + ", path: " + callbackApp.path + ); + callbackApp.permissions = PERMS_DIRECTORY; + + setAppBundleModTime(); + + let args = [updatesDirPath, installDirPath]; + if (aSwitchApp) { + args[2] = stageDirPath; + args[3] = pid + "/replace"; + } else { + args[2] = applyToDirPath; + args[3] = pid; + } + + let launchBin = gIsServiceTest && isInvalidArgTest ? callbackApp : updateBin; + + if (!isInvalidArgTest) { + args = args.concat([callbackApp.parent.path, callbackApp.path]); + args = args.concat(gCallbackArgs); + } else if (gIsServiceTest) { + args = ["launch-service", updateBin.path].concat(args); + } else if (aCallbackPath) { + args = args.concat([callbackApp.parent.path, aCallbackPath]); + } + + debugDump("launching the program: " + launchBin.path + " " + args.join(" ")); + + if (aSwitchApp && !isInvalidArgTest) { + // We want to set the env vars again + gShouldResetEnv = undefined; + } + + setEnvironment(); + + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(launchBin); + process.run(true, args, args.length); + + resetEnvironment(); + + if (gPIDPersistProcess) { + Services.env.set("MOZ_TEST_SHORTER_WAIT_PID", ""); + } + + let status = readStatusFile(); + if ( + (!gIsServiceTest && process.exitValue != aExpectedExitValue) || + (status != aExpectedStatus && !gIsServiceTest && !isInvalidArgTest) + ) { + if (process.exitValue != aExpectedExitValue) { + logTestInfo( + "updater exited with unexpected value! Got: " + + process.exitValue + + ", Expected: " + + aExpectedExitValue + ); + } + if (status != aExpectedStatus) { + logTestInfo( + "update status is not the expected status! Got: " + + status + + ", Expected: " + + aExpectedStatus + ); + } + logUpdateLog(FILE_LAST_UPDATE_LOG); + } + + if (gIsServiceTest && isInvalidArgTest) { + let secureStatus = readSecureStatusFile(); + if (secureStatus != STATE_NONE) { + status = secureStatus; + } + } + + if (!gIsServiceTest) { + Assert.equal( + process.exitValue, + aExpectedExitValue, + "the process exit value" + MSG_SHOULD_EQUAL + ); + } + + if (status != aExpectedStatus) { + logUpdateLog(FILE_UPDATE_LOG); + } + Assert.equal(status, aExpectedStatus, "the update status" + MSG_SHOULD_EQUAL); + + Assert.ok( + !updateHasBinaryTransparencyErrorResult(), + "binary transparency is not being processed for now" + ); + + if (gIsServiceTest && aCheckSvcLog) { + let contents = readServiceLogFile(); + Assert.notEqual( + contents, + svcOriginalLog, + "the contents of the maintenanceservice.log should not " + + "be the same as the original contents" + ); + if (gEnvForceServiceFallback) { + // If we are forcing the service to fail and fall back to update without + // the service, the service log should reflect that we failed in that way. + Assert.ok( + contents.includes(LOG_SVC_UNSUCCESSFUL_LAUNCH), + "the contents of the maintenanceservice.log should " + + "contain the unsuccessful launch string" + ); + } else if (!isInvalidArgTest) { + Assert.notEqual( + contents.indexOf(LOG_SVC_SUCCESSFUL_LAUNCH), + -1, + "the contents of the maintenanceservice.log should " + + "contain the successful launch string" + ); + } + } +} + +/** + * Launches the helper binary synchronously with the specified arguments for + * updater tests. + * + * @param aArgs + * The arguments to pass to the helper binary. + * @return the process exit value returned by the helper binary. + */ +function runTestHelperSync(aArgs) { + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(helperBin); + debugDump("Running " + helperBin.path + " " + aArgs.join(" ")); + process.run(true, aArgs, aArgs.length); + return process.exitValue; +} + +/** + * Creates a symlink for updater tests. + */ +function createSymlink() { + let args = [ + "setup-symlink", + "moz-foo", + "moz-bar", + "target", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link", + ]; + let exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, "the helper process exit value should be 0"); + let file = getApplyDirFile(DIR_RESOURCES + "link"); + Assert.ok(file.exists(), MSG_SHOULD_EXIST + ", path: " + file.path); + file.permissions = 0o666; + args = [ + "setup-symlink", + "moz-foo2", + "moz-bar2", + "target2", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link2", + "change-perm", + ]; + exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, "the helper process exit value should be 0"); +} + +/** + * Removes a symlink for updater tests. + */ +function removeSymlink() { + let args = [ + "remove-symlink", + "moz-foo", + "moz-bar", + "target", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link", + ]; + let exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, "the helper process exit value should be 0"); + args = [ + "remove-symlink", + "moz-foo2", + "moz-bar2", + "target2", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link2", + ]; + exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, "the helper process exit value should be 0"); +} + +/** + * Checks a symlink for updater tests. + */ +function checkSymlink() { + let args = [ + "check-symlink", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link", + ]; + let exitValue = runTestHelperSync(args); + Assert.equal(exitValue, 0, "the helper process exit value should be 0"); +} + +/** + * Sets the active update and related information for updater tests. + */ +function setupActiveUpdate() { + let pendingState = gIsServiceTest ? STATE_PENDING_SVC : STATE_PENDING; + let patchProps = { state: pendingState }; + let patches = getLocalPatchString(patchProps); + let updates = getLocalUpdateString({}, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeVersionFile(DEFAULT_UPDATE_VERSION); + writeStatusFile(pendingState); + reloadUpdateManagerData(); + Assert.ok(!!gUpdateManager.readyUpdate, "the ready update should be defined"); +} + +/** + * Stages an update using nsIUpdateProcessor:processUpdate for updater tests. + * + * @param aStateAfterStage + * The expected update state after the update has been staged. + * @param aCheckSvcLog + * Whether the service log should be checked for service tests. + * @param aUpdateRemoved (optional) + * Whether the update is removed after staging. This can happen when + * a staging failure occurs. + */ +async function stageUpdate( + aStateAfterStage, + aCheckSvcLog, + aUpdateRemoved = false +) { + debugDump("start - attempting to stage update"); + + let svcLogOriginalContents; + if (gIsServiceTest && aCheckSvcLog) { + svcLogOriginalContents = readServiceLogFile(); + } + + setAppBundleModTime(); + setEnvironment(); + try { + // Stage the update. + Cc["@mozilla.org/updates/update-processor;1"] + .createInstance(Ci.nsIUpdateProcessor) + .processUpdate(); + } catch (e) { + Assert.ok( + false, + "error thrown while calling processUpdate, Exception: " + e + ); + } + await waitForEvent("update-staged", aStateAfterStage); + resetEnvironment(); + + if (AppConstants.platform == "win") { + if (gIsServiceTest) { + waitForServiceStop(false); + } else { + let updater = getApplyDirFile(FILE_UPDATER_BIN); + await TestUtils.waitForCondition( + () => !isFileInUse(updater), + "Waiting for the file tp not be in use, Path: " + updater.path + ); + } + } + + if (!aUpdateRemoved) { + Assert.equal( + readStatusState(), + aStateAfterStage, + "the status file state" + MSG_SHOULD_EQUAL + ); + + Assert.equal( + gUpdateManager.readyUpdate.state, + aStateAfterStage, + "the update state" + MSG_SHOULD_EQUAL + ); + } + + let log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + let stageDir = getStageDirFile(); + if ( + aStateAfterStage == STATE_APPLIED || + aStateAfterStage == STATE_APPLIED_SVC + ) { + Assert.ok(stageDir.exists(), MSG_SHOULD_EXIST + getMsgPath(stageDir.path)); + } else { + Assert.ok( + !stageDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(stageDir.path) + ); + } + + if (gIsServiceTest && aCheckSvcLog) { + let contents = readServiceLogFile(); + Assert.notEqual( + contents, + svcLogOriginalContents, + "the contents of the maintenanceservice.log should not " + + "be the same as the original contents" + ); + Assert.notEqual( + contents.indexOf(LOG_SVC_SUCCESSFUL_LAUNCH), + -1, + "the contents of the maintenanceservice.log should " + + "contain the successful launch string" + ); + } + + debugDump("finish - attempting to stage update"); +} + +/** + * Helper function to check whether the maintenance service updater tests should + * run. See bug 711660 for more details. + * + * @return true if the test should run and false if it shouldn't. + * @throws If called from a platform other than Windows. + */ +function shouldRunServiceTest() { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + let binDir = getGREBinDir(); + let updaterBin = binDir.clone(); + updaterBin.append(FILE_UPDATER_BIN); + Assert.ok( + updaterBin.exists(), + MSG_SHOULD_EXIST + ", leafName: " + updaterBin.leafName + ); + + let updaterBinPath = updaterBin.path; + if (/ /.test(updaterBinPath)) { + updaterBinPath = '"' + updaterBinPath + '"'; + } + + let isBinSigned = isBinarySigned(updaterBinPath); + + const REG_PATH = + "SOFTWARE\\Mozilla\\MaintenanceService\\" + + "3932ecacee736d366d6436db0f55bce4"; + let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + key.open( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + REG_PATH, + Ci.nsIWindowsRegKey.ACCESS_READ | key.WOW64_64 + ); + } catch (e) { + // The build system could sign the files and not have the test registry key + // in which case we should fail the test if the updater binary is signed so + // the build system can be fixed by adding the registry key. + if (IS_AUTHENTICODE_CHECK_ENABLED) { + Assert.ok( + !isBinSigned, + "the updater.exe binary should not be signed when the test " + + "registry key doesn't exist (if it is, build system " + + "configuration bug?)" + ); + } + + logTestInfo( + "this test can only run on the buildbot build system at this time" + ); + return false; + } + + // Check to make sure the service is installed + let args = ["wait-for-service-stop", "MozillaMaintenance", "10"]; + let exitValue = runTestHelperSync(args); + Assert.notEqual( + exitValue, + 0xee, + "the maintenance service should be " + + "installed (if not, build system configuration bug?)" + ); + + if (IS_AUTHENTICODE_CHECK_ENABLED) { + // The test registry key exists and IS_AUTHENTICODE_CHECK_ENABLED is true + // so the binaries should be signed. To run the test locally + // DISABLE_UPDATER_AUTHENTICODE_CHECK can be defined. + Assert.ok( + isBinSigned, + "the updater.exe binary should be signed (if not, build system " + + "configuration bug?)" + ); + } + + // In case the machine is running an old maintenance service or if it + // is not installed, and permissions exist to install it. Then install + // the newer bin that we have since all of the other checks passed. + return attemptServiceInstall(); +} + +/** + * Helper function to check whether the a binary is signed. + * + * @param aBinPath + * The path to the file to check if it is signed. + * @return true if the file is signed and false if it isn't. + */ +function isBinarySigned(aBinPath) { + let args = ["check-signature", aBinPath]; + let exitValue = runTestHelperSync(args); + if (exitValue != 0) { + logTestInfo( + "binary is not signed. " + + FILE_HELPER_BIN + + " returned " + + exitValue + + " for file " + + aBinPath + ); + return false; + } + return true; +} + +/** + * Helper function for setting up the application files required to launch the + * application for the updater tests by either copying or creating symlinks to + * the files. + * + * @param options.requiresOmnijar when true, copy or symlink omnijars as well. + * This may be required to launch the updated application and have non-trivial + * functionality available. + */ +function setupAppFiles({ requiresOmnijar = false } = {}) { + debugDump( + "start - copying or creating symlinks to application files " + + "for the test" + ); + + let destDir = getApplyDirFile(); + if (!destDir.exists()) { + try { + destDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } catch (e) { + logTestInfo( + "unable to create directory! Path: " + + destDir.path + + ", Exception: " + + e + ); + do_throw(e); + } + } + + // Required files for the application or the test that aren't listed in the + // dependentlibs.list file. + let appFiles = [ + { relPath: FILE_APP_BIN, inGreDir: false }, + { relPath: FILE_APPLICATION_INI, inGreDir: true }, + { relPath: "dependentlibs.list", inGreDir: true }, + ]; + + if (requiresOmnijar) { + appFiles.push({ relPath: AppConstants.OMNIJAR_NAME, inGreDir: true }); + + if (AppConstants.MOZ_BUILD_APP == "browser") { + // Only Firefox uses an app-specific omnijar. + appFiles.push({ + relPath: "browser/" + AppConstants.OMNIJAR_NAME, + inGreDir: true, + }); + } + } + + // On Linux the updater.png must also be copied and libsoftokn3.so must be + // symlinked or copied. + if (AppConstants.platform == "linux") { + appFiles.push( + { relPath: "icons/updater.png", inGreDir: true }, + { relPath: "libsoftokn3.so", inGreDir: true } + ); + } + + // Read the dependent libs file leafnames from the dependentlibs.list file + // into the array. + let deplibsFile = gGREDirOrig.clone(); + deplibsFile.append("dependentlibs.list"); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fis.init(deplibsFile, 0x01, 0o444, Ci.nsIFileInputStream.CLOSE_ON_EOF); + fis.QueryInterface(Ci.nsILineInputStream); + + let hasMore; + let line = {}; + do { + hasMore = fis.readLine(line); + appFiles.push({ relPath: line.value, inGreDir: false }); + } while (hasMore); + + fis.close(); + + appFiles.forEach(function CMAF_FLN_FE(aAppFile) { + copyFileToTestAppDir(aAppFile.relPath, aAppFile.inGreDir); + }); + + copyTestUpdaterToBinDir(); + + debugDump( + "finish - copying or creating symlinks to application files " + + "for the test" + ); +} + +/** + * Copies the specified files from the dist/bin directory into the test's + * application directory. + * + * @param aFileRelPath + * The relative path to the source and the destination of the file to + * copy. + * @param aInGreDir + * Whether the file is located in the GRE directory which is + * <bundle>/Contents/Resources on Mac OS X and is the installation + * directory on all other platforms. If false the file must be in the + * GRE Binary directory which is <bundle>/Contents/MacOS on Mac OS X + * and is the installation directory on on all other platforms. + */ +function copyFileToTestAppDir(aFileRelPath, aInGreDir) { + // gGREDirOrig and gGREBinDirOrig must always be cloned when changing its + // properties + let srcFile = aInGreDir ? gGREDirOrig.clone() : gGREBinDirOrig.clone(); + let destFile = aInGreDir ? getGREDir() : getGREBinDir(); + let fileRelPath = aFileRelPath; + let pathParts = fileRelPath.split("/"); + for (let i = 0; i < pathParts.length; i++) { + if (pathParts[i]) { + srcFile.append(pathParts[i]); + destFile.append(pathParts[i]); + } + } + + if (AppConstants.platform == "macosx" && !srcFile.exists()) { + debugDump( + "unable to copy file since it doesn't exist! Checking if " + + fileRelPath + + ".app exists. Path: " + + srcFile.path + ); + // gGREDirOrig and gGREBinDirOrig must always be cloned when changing its + // properties + srcFile = aInGreDir ? gGREDirOrig.clone() : gGREBinDirOrig.clone(); + destFile = aInGreDir ? getGREDir() : getGREBinDir(); + for (let i = 0; i < pathParts.length; i++) { + if (pathParts[i]) { + srcFile.append( + pathParts[i] + (pathParts.length - 1 == i ? ".app" : "") + ); + destFile.append( + pathParts[i] + (pathParts.length - 1 == i ? ".app" : "") + ); + } + } + fileRelPath = fileRelPath + ".app"; + } + Assert.ok( + srcFile.exists(), + MSG_SHOULD_EXIST + ", leafName: " + srcFile.leafName + ); + + // Symlink libraries. Note that the XUL library on Mac OS X doesn't have a + // file extension and shouldSymlink will always be false on Windows. + let shouldSymlink = + pathParts[pathParts.length - 1] == "XUL" || + fileRelPath.substr(fileRelPath.length - 3) == ".so" || + fileRelPath.substr(fileRelPath.length - 6) == ".dylib"; + if (!shouldSymlink) { + if (!destFile.exists()) { + try { + srcFile.copyToFollowingLinks(destFile.parent, destFile.leafName); + } catch (e) { + // Just in case it is partially copied + if (destFile.exists()) { + try { + destFile.remove(true); + } catch (ex) { + logTestInfo( + "unable to remove file that failed to copy! Path: " + + destFile.path + + ", Exception: " + + ex + ); + } + } + do_throw( + "Unable to copy file! Path: " + srcFile.path + ", Exception: " + e + ); + } + } + } else { + try { + if (destFile.exists()) { + destFile.remove(false); + } + let ln = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + ln.initWithPath("/bin/ln"); + let process = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + process.init(ln); + let args = ["-s", srcFile.path, destFile.path]; + process.run(true, args, args.length); + Assert.ok( + destFile.isSymlink(), + destFile.leafName + " should be a symlink" + ); + } catch (e) { + do_throw( + "Unable to create symlink for file! Path: " + + srcFile.path + + ", Exception: " + + e + ); + } + } +} + +/** + * Attempts to upgrade the maintenance service if permissions are allowed. + * This is useful for XP where we have permission to upgrade in case an + * older service installer exists. Also if the user manually installed into + * a unprivileged location. + * + * @return true if the installed service is from this build. If the installed + * service is not from this build the test will fail instead of + * returning false. + * @throws If called from a platform other than Windows. + */ +function attemptServiceInstall() { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + let maintSvcDir = getMaintSvcDir(); + Assert.ok( + maintSvcDir.exists(), + MSG_SHOULD_EXIST + ", leafName: " + maintSvcDir.leafName + ); + let oldMaintSvcBin = maintSvcDir.clone(); + oldMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN); + Assert.ok( + oldMaintSvcBin.exists(), + MSG_SHOULD_EXIST + ", leafName: " + oldMaintSvcBin.leafName + ); + let buildMaintSvcBin = getGREBinDir(); + buildMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN); + if (readFileBytes(oldMaintSvcBin) == readFileBytes(buildMaintSvcBin)) { + debugDump( + "installed maintenance service binary is the same as the " + + "build's maintenance service binary" + ); + return true; + } + let backupMaintSvcBin = maintSvcDir.clone(); + backupMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN + ".backup"); + try { + if (backupMaintSvcBin.exists()) { + backupMaintSvcBin.remove(false); + } + oldMaintSvcBin.moveTo( + maintSvcDir, + FILE_MAINTENANCE_SERVICE_BIN + ".backup" + ); + buildMaintSvcBin.copyTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN); + backupMaintSvcBin.remove(false); + } catch (e) { + // Restore the original file in case the moveTo was successful. + if (backupMaintSvcBin.exists()) { + oldMaintSvcBin = maintSvcDir.clone(); + oldMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN); + if (!oldMaintSvcBin.exists()) { + backupMaintSvcBin.moveTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN); + } + } + Assert.ok( + false, + "should be able copy the test maintenance service to " + + "the maintenance service directory (if not, build system " + + "configuration bug?), path: " + + maintSvcDir.path + ); + } + + return true; +} + +/** + * Waits for the applications that are launched by the maintenance service to + * stop. + * + * @throws If called from a platform other than Windows. + */ +function waitServiceApps() { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + // maintenanceservice_installer.exe is started async during updates. + waitForApplicationStop("maintenanceservice_installer.exe"); + // maintenanceservice_tmp.exe is started async from the service installer. + waitForApplicationStop("maintenanceservice_tmp.exe"); + // In case the SCM thinks the service is stopped, but process still exists. + waitForApplicationStop("maintenanceservice.exe"); +} + +/** + * Waits for the maintenance service to stop. + * + * @throws If called from a platform other than Windows. + */ +function waitForServiceStop(aFailTest) { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + waitServiceApps(); + debugDump("waiting for the maintenance service to stop if necessary"); + // Use the helper bin to ensure the service is stopped. If not stopped, then + // wait for the service to stop (at most 120 seconds). + let args = ["wait-for-service-stop", "MozillaMaintenance", "120"]; + let exitValue = runTestHelperSync(args); + Assert.notEqual(exitValue, 0xee, "the maintenance service should exist"); + if (exitValue != 0) { + if (aFailTest) { + Assert.ok( + false, + "the maintenance service should stop, process exit " + + "value: " + + exitValue + ); + } + logTestInfo( + "maintenance service did not stop which may cause test " + + "failures later, process exit value: " + + exitValue + ); + } else { + debugDump("service stopped"); + } + waitServiceApps(); +} + +/** + * Waits for the specified application to stop. + * + * @param aApplication + * The application binary name to wait until it has stopped. + * @throws If called from a platform other than Windows. + */ +function waitForApplicationStop(aApplication) { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + debugDump("waiting for " + aApplication + " to stop if necessary"); + // Use the helper bin to ensure the application is stopped. If not stopped, + // then wait for it to stop (at most 120 seconds). + let args = ["wait-for-application-exit", aApplication, "120"]; + let exitValue = runTestHelperSync(args); + Assert.equal( + exitValue, + 0, + "the process should have stopped, process name: " + aApplication + ); +} + +/** + * Gets the platform specific shell binary that is launched using nsIProcess and + * in turn launches a binary used for the test (e.g. application, updater, + * etc.). A shell is used so debug console output can be redirected to a file so + * it doesn't end up in the test log. + * + * @return nsIFile for the shell binary to launch using nsIProcess. + */ +function getLaunchBin() { + let launchBin; + if (AppConstants.platform == "win") { + launchBin = Services.dirsvc.get("WinD", Ci.nsIFile); + launchBin.append("System32"); + launchBin.append("cmd.exe"); + } else { + launchBin = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + launchBin.initWithPath("/bin/sh"); + } + Assert.ok(launchBin.exists(), MSG_SHOULD_EXIST + getMsgPath(launchBin.path)); + + return launchBin; +} + +/** + * Locks a Windows directory. + * + * @param aDirPath + * The test file object that describes the file to make in use. + * @throws If called from a platform other than Windows. + */ +function lockDirectory(aDirPath) { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + debugDump("start - locking installation directory"); + const LPCWSTR = ctypes.char16_t.ptr; + const DWORD = ctypes.uint32_t; + const LPVOID = ctypes.voidptr_t; + const GENERIC_READ = 0x80000000; + const FILE_SHARE_READ = 1; + const FILE_SHARE_WRITE = 2; + const OPEN_EXISTING = 3; + const FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + const INVALID_HANDLE_VALUE = LPVOID(0xffffffff); + let kernel32 = ctypes.open("kernel32"); + let CreateFile = kernel32.declare( + "CreateFileW", + ctypes.winapi_abi, + LPVOID, + LPCWSTR, + DWORD, + DWORD, + LPVOID, + DWORD, + DWORD, + LPVOID + ); + gHandle = CreateFile( + aDirPath, + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + LPVOID(0), + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + LPVOID(0) + ); + Assert.notEqual( + gHandle.toString(), + INVALID_HANDLE_VALUE.toString(), + "the handle should not equal INVALID_HANDLE_VALUE" + ); + kernel32.close(); + debugDump("finish - locking installation directory"); +} + +/** + * Launches the test helper binary to make it in use for updater tests. + * + * @param aRelPath + * The relative path in the apply to directory for the helper binary. + * @param aCopyTestHelper + * Whether to copy the test helper binary to the relative path in the + * apply to directory. + */ +async function runHelperFileInUse(aRelPath, aCopyTestHelper) { + debugDump("aRelPath: " + aRelPath); + // Launch an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let fileInUseBin = getApplyDirFile(aRelPath); + if (aCopyTestHelper) { + if (fileInUseBin.exists()) { + fileInUseBin.remove(false); + } + helperBin.copyTo(fileInUseBin.parent, fileInUseBin.leafName); + } + fileInUseBin.permissions = PERMS_DIRECTORY; + let args = [ + getApplyDirPath() + DIR_RESOURCES, + "input", + "output", + "-s", + HELPER_SLEEP_TIMEOUT, + ]; + let fileInUseProcess = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + await waitForHelperSleep(); +} + +/** + * Launches the test helper binary to provide a pid that is in use for updater + * tests. + * + * @param aRelPath + * The relative path in the apply to directory for the helper binary. + * @param aCopyTestHelper + * Whether to copy the test helper binary to the relative path in the + * apply to directory. + */ +async function runHelperPIDPersists(aRelPath, aCopyTestHelper) { + debugDump("aRelPath: " + aRelPath); + // Launch an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let pidPersistsBin = getApplyDirFile(aRelPath); + if (aCopyTestHelper) { + if (pidPersistsBin.exists()) { + pidPersistsBin.remove(false); + } + helperBin.copyTo(pidPersistsBin.parent, pidPersistsBin.leafName); + } + pidPersistsBin.permissions = PERMS_DIRECTORY; + let args = [ + getApplyDirPath() + DIR_RESOURCES, + "input", + "output", + "-s", + HELPER_SLEEP_TIMEOUT, + ]; + gPIDPersistProcess = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + gPIDPersistProcess.init(pidPersistsBin); + gPIDPersistProcess.run(false, args, args.length); + + await waitForHelperSleep(); + await TestUtils.waitForCondition( + () => !!gPIDPersistProcess.pid, + "Waiting for the process pid" + ); +} + +/** + * Launches the test helper binary and locks a file specified on the command + * line for updater tests. + * + * @param aTestFile + * The test file object that describes the file to lock. + */ +async function runHelperLockFile(aTestFile) { + // Exclusively lock an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let helperDestDir = getApplyDirFile(DIR_RESOURCES); + helperBin.copyTo(helperDestDir, FILE_HELPER_BIN); + helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN); + // Strip off the first two directories so the path has to be from the helper's + // working directory. + let lockFileRelPath = aTestFile.relPathDir.split("/"); + if (AppConstants.platform == "macosx") { + lockFileRelPath = lockFileRelPath.slice(2); + } + lockFileRelPath = lockFileRelPath.join("/") + "/" + aTestFile.fileName; + let args = [ + getApplyDirPath() + DIR_RESOURCES, + "input", + "output", + "-s", + HELPER_SLEEP_TIMEOUT, + lockFileRelPath, + ]; + let helperProcess = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + helperProcess.init(helperBin); + helperProcess.run(false, args, args.length); + + await waitForHelperSleep(); +} + +/** + * Helper function that waits until the helper has completed its operations. + */ +async function waitForHelperSleep() { + // Give the lock file process time to lock the file before updating otherwise + // this test can fail intermittently on Windows debug builds. + let file = getApplyDirFile(DIR_RESOURCES + "output"); + await TestUtils.waitForCondition( + () => file.exists(), + "Waiting for file to exist, path: " + file.path + ); + + let expectedContents = "sleeping\n"; + await TestUtils.waitForCondition( + () => readFile(file) == expectedContents, + "Waiting for expected file contents: " + expectedContents + ); + + await TestUtils.waitForCondition(() => { + try { + file.remove(false); + } catch (e) { + debugDump( + "failed to remove file. Path: " + file.path + ", Exception: " + e + ); + } + return !file.exists(); + }, "Waiting for file to be removed, Path: " + file.path); +} + +/** + * Helper function to tell the helper to finish and exit its sleep state. + */ +async function waitForHelperExit() { + let file = getApplyDirFile(DIR_RESOURCES + "input"); + writeFile(file, "finish\n"); + + // Give the lock file process time to lock the file before updating otherwise + // this test can fail intermittently on Windows debug builds. + file = getApplyDirFile(DIR_RESOURCES + "output"); + await TestUtils.waitForCondition( + () => file.exists(), + "Waiting for file to exist, Path: " + file.path + ); + + let expectedContents = "finished\n"; + await TestUtils.waitForCondition( + () => readFile(file) == expectedContents, + "Waiting for expected file contents: " + expectedContents + ); + + // Give the lock file process time to unlock the file before deleting the + // input and output files. + await TestUtils.waitForCondition(() => { + try { + file.remove(false); + } catch (e) { + debugDump( + "failed to remove file. Path: " + file.path + ", Exception: " + e + ); + } + return !file.exists(); + }, "Waiting for file to be removed, Path: " + file.path); + + file = getApplyDirFile(DIR_RESOURCES + "input"); + await TestUtils.waitForCondition(() => { + try { + file.remove(false); + } catch (e) { + debugDump( + "failed to remove file. Path: " + file.path + ", Exception: " + e + ); + } + return !file.exists(); + }, "Waiting for file to be removed, Path: " + file.path); +} + +/** + * Helper function for updater binary tests that creates the files and + * directories used by the test. + * + * @param aMarFile + * The mar file for the update test. + * @param aPostUpdateAsync + * When null the updater.ini is not created otherwise this parameter + * is passed to createUpdaterINI. + * @param aPostUpdateExeRelPathPrefix + * When aPostUpdateAsync null this value is ignored otherwise it is + * passed to createUpdaterINI. + * @param aSetupActiveUpdate + * Whether to setup the active update. + * + * @param options.requiresOmnijar + * When true, copy or symlink omnijars as well. This may be required + * to launch the updated application and have non-trivial functionality + * available. + */ +async function setupUpdaterTest( + aMarFile, + aPostUpdateAsync, + aPostUpdateExeRelPathPrefix = "", + aSetupActiveUpdate = true, + { requiresOmnijar = false } = {} +) { + debugDump("start - updater test setup"); + let updatesPatchDir = getUpdateDirFile(DIR_PATCH); + if (!updatesPatchDir.exists()) { + updatesPatchDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } + // Copy the mar that will be applied + let mar = getTestDirFile(aMarFile); + mar.copyToFollowingLinks(updatesPatchDir, FILE_UPDATE_MAR); + + let helperBin = getTestDirFile(FILE_HELPER_BIN); + helperBin.permissions = PERMS_DIRECTORY; + let afterApplyBinDir = getApplyDirFile(DIR_RESOURCES); + helperBin.copyToFollowingLinks(afterApplyBinDir, gCallbackBinFile); + helperBin.copyToFollowingLinks(afterApplyBinDir, gPostUpdateBinFile); + + gTestFiles.forEach(function SUT_TF_FE(aTestFile) { + debugDump("start - setup test file: " + aTestFile.fileName); + if (aTestFile.originalFile || aTestFile.originalContents) { + let testDir = getApplyDirFile(aTestFile.relPathDir); + // Somehow these create calls are failing with FILE_ALREADY_EXISTS even + // after checking .exists() first, so we just catch the exception. + try { + testDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } catch (e) { + if (e.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) { + throw e; + } + } + + let testFile; + if (aTestFile.originalFile) { + testFile = getTestDirFile(aTestFile.originalFile); + testFile.copyToFollowingLinks(testDir, aTestFile.fileName); + testFile = getApplyDirFile(aTestFile.relPathDir + aTestFile.fileName); + Assert.ok( + testFile.exists(), + MSG_SHOULD_EXIST + ", path: " + testFile.path + ); + } else { + testFile = getApplyDirFile(aTestFile.relPathDir + aTestFile.fileName); + writeFile(testFile, aTestFile.originalContents); + } + + // Skip these tests on Windows since chmod doesn't really set permissions + // on Windows. + if (AppConstants.platform != "win" && aTestFile.originalPerms) { + testFile.permissions = aTestFile.originalPerms; + // Store the actual permissions on the file for reference later after + // setting the permissions. + if (!aTestFile.comparePerms) { + aTestFile.comparePerms = testFile.permissions; + } + } + } + debugDump("finish - setup test file: " + aTestFile.fileName); + }); + + // Add the test directory that will be updated for a successful update or left + // in the initial state for a failed update. + gTestDirs.forEach(function SUT_TD_FE(aTestDir) { + debugDump("start - setup test directory: " + aTestDir.relPathDir); + let testDir = getApplyDirFile(aTestDir.relPathDir); + // Somehow these create calls are failing with FILE_ALREADY_EXISTS even + // after checking .exists() first, so we just catch the exception. + try { + testDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } catch (e) { + if (e.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) { + throw e; + } + } + + if (aTestDir.files) { + aTestDir.files.forEach(function SUT_TD_F_FE(aTestFile) { + let testFile = getApplyDirFile(aTestDir.relPathDir + aTestFile); + if (!testFile.exists()) { + testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + } + }); + } + + if (aTestDir.subDirs) { + aTestDir.subDirs.forEach(function SUT_TD_SD_FE(aSubDir) { + let testSubDir = getApplyDirFile(aTestDir.relPathDir + aSubDir); + if (!testSubDir.exists()) { + testSubDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY); + } + + if (aTestDir.subDirFiles) { + aTestDir.subDirFiles.forEach(function SUT_TD_SDF_FE(aTestFile) { + let testFile = getApplyDirFile( + aTestDir.relPathDir + aSubDir + aTestFile + ); + if (!testFile.exists()) { + testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + } + }); + } + }); + } + debugDump("finish - setup test directory: " + aTestDir.relPathDir); + }); + + if (aSetupActiveUpdate) { + setupActiveUpdate(); + } + + if (aPostUpdateAsync !== null) { + createUpdaterINI(aPostUpdateAsync, aPostUpdateExeRelPathPrefix); + } + + await TestUtils.waitForCondition(() => { + try { + setupAppFiles({ requiresOmnijar }); + return true; + } catch (e) { + logTestInfo("exception when calling setupAppFiles, Exception: " + e); + } + return false; + }, "Waiting to setup app files"); + + debugDump("finish - updater test setup"); +} + +/** + * Helper function for updater binary tests that creates the updater.ini + * file. + * + * @param aIsExeAsync + * True or undefined if the post update process should be async. If + * undefined ExeAsync will not be added to the updater.ini file in + * order to test the default launch behavior which is async. + * @param aExeRelPathPrefix + * A string to prefix the ExeRelPath values in the updater.ini. + */ +function createUpdaterINI(aIsExeAsync, aExeRelPathPrefix) { + let exeArg = "ExeArg=post-update-async\n"; + let exeAsync = ""; + if (aIsExeAsync !== undefined) { + if (aIsExeAsync) { + exeAsync = "ExeAsync=true\n"; + } else { + exeArg = "ExeArg=post-update-sync\n"; + exeAsync = "ExeAsync=false\n"; + } + } + + if (AppConstants.platform == "win" && aExeRelPathPrefix) { + aExeRelPathPrefix = aExeRelPathPrefix.replace("/", "\\"); + } + + let exeRelPathMac = + "ExeRelPath=" + + aExeRelPathPrefix + + DIR_RESOURCES + + gPostUpdateBinFile + + "\n"; + let exeRelPathWin = + "ExeRelPath=" + aExeRelPathPrefix + gPostUpdateBinFile + "\n"; + let updaterIniContents = + "[Strings]\n" + + "Title=Update Test\n" + + "Info=Running update test " + + gTestID + + "\n\n" + + "[PostUpdateMac]\n" + + exeRelPathMac + + exeArg + + exeAsync + + "\n" + + "[PostUpdateWin]\n" + + exeRelPathWin + + exeArg + + exeAsync; + let updaterIni = getApplyDirFile(DIR_RESOURCES + FILE_UPDATER_INI); + writeFile(updaterIni, updaterIniContents); +} + +/** + * Gets the message log path used for assert checks to lessen the length printed + * to the log file. + * + * @param aPath + * The path to shorten for the log file. + * @return the message including the shortened path for the log file. + */ +function getMsgPath(aPath) { + return ", path: " + replaceLogPaths(aPath); +} + +/** + * Helper function that replaces the common part of paths in the update log's + * contents with <test_dir_path> for paths to the the test directory and + * <update_dir_path> for paths to the update directory. This is needed since + * Assert.equal will truncate what it prints to the xpcshell log file. + * + * @param aLogContents + * The update log file's contents. + * @return the log contents with the paths replaced. + */ +function replaceLogPaths(aLogContents) { + let logContents = aLogContents; + // Remove the majority of the path up to the test directory. This is needed + // since Assert.equal won't print long strings to the test logs. + let testDirPath = getApplyDirFile().parent.path; + if (AppConstants.platform == "win") { + // Replace \\ with \\\\ so the regexp works. + testDirPath = testDirPath.replace(/\\/g, "\\\\"); + } + logContents = logContents.replace( + new RegExp(testDirPath, "g"), + "<test_dir_path>/" + gTestID + ); + let updatesDirPath = getMockUpdRootD().path; + if (AppConstants.platform == "win") { + // Replace \\ with \\\\ so the regexp works. + updatesDirPath = updatesDirPath.replace(/\\/g, "\\\\"); + } + logContents = logContents.replace( + new RegExp(updatesDirPath, "g"), + "<update_dir_path>/" + gTestID + ); + if (AppConstants.platform == "win") { + // Replace \ with / + logContents = logContents.replace(/\\/g, "/"); + } + return logContents; +} + +/** + * Helper function that removes the timestamps in the update log + * + * @param aLogContents + * The update log file's contents. + * @return the log contents without timestamps + */ +function removeTimeStamps(aLogContents) { + return aLogContents.replace( + /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{4}: /gm, + "" + ); +} + +/** + * Helper function for updater binary tests for verifying the contents of the + * update log after a successful update. + * + * @param aCompareLogFile + * The log file to compare the update log with. + * @param aStaged + * If the update log file is for a staged update. + * @param aReplace + * If the update log file is for a replace update. + * @param aExcludeDistDir + * Removes lines containing the distribution directory from the log + * file to compare the update log with. + */ +function checkUpdateLogContents( + aCompareLogFile, + aStaged = false, + aReplace = false, + aExcludeDistDir = false +) { + if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") { + // The order that files are returned when enumerating the file system on + // Linux and Mac is not deterministic so skip checking the logs. + return; + } + + let updateLog = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + let updateLogContents = readFileBytes(updateLog); + + // Remove leading timestamps + updateLogContents = removeTimeStamps(updateLogContents); + + // The channel-prefs.js is defined in gTestFilesCommon which will always be + // located to the end of gTestFiles when it is present. + if ( + gTestFiles.length > 1 && + gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" && + !gTestFiles[gTestFiles.length - 1].originalContents + ) { + updateLogContents = updateLogContents.replace(/.*defaults\/.*/g, ""); + } + + if ( + gTestFiles.length > 2 && + gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI && + !gTestFiles[gTestFiles.length - 2].originalContents + ) { + updateLogContents = updateLogContents.replace( + /.*update-settings.ini.*/g, + "" + ); + } + + // Skip the source/destination lines since they contain absolute paths. + // These could be changed to relative paths using <test_dir_path> and + // <update_dir_path> + updateLogContents = updateLogContents.replace(/PATCH DIRECTORY.*/g, ""); + updateLogContents = updateLogContents.replace( + /INSTALLATION DIRECTORY.*/g, + "" + ); + updateLogContents = updateLogContents.replace(/WORKING DIRECTORY.*/g, ""); + // Skip lines that log failed attempts to open the callback executable. + updateLogContents = updateLogContents.replace( + /NS_main: callback app file .*/g, + "" + ); + // Remove carriage returns. + updateLogContents = updateLogContents.replace(/\r/g, ""); + + if (AppConstants.platform == "win") { + // The FindFile results when enumerating the filesystem on Windows is not + // determistic so the results matching the following need to be fixed. + let re = new RegExp( + // eslint-disable-next-line no-control-regex + "([^\n]* 7/7text1[^\n]*)\n([^\n]* 7/7text0[^\n]*)\n", + "g" + ); + updateLogContents = updateLogContents.replace(re, "$2\n$1\n"); + } + + if (aReplace) { + // Remove the lines which contain absolute paths + updateLogContents = updateLogContents.replace(/^Begin moving.*$/gm, ""); + updateLogContents = updateLogContents.replace( + /^ensure_remove: failed to remove file: .*$/gm, + "" + ); + updateLogContents = updateLogContents.replace( + /^ensure_remove_recursive: unable to remove directory: .*$/gm, + "" + ); + updateLogContents = updateLogContents.replace( + /^Removing tmpDir failed, err: -1$/gm, + "" + ); + updateLogContents = updateLogContents.replace( + /^remove_recursive_on_reboot: .*$/gm, + "" + ); + // Replace requests will retry renaming the installation directory 10 times + // when there are files still in use. The following will remove the + // additional entries from the log file when this happens so the log check + // passes. + let re = new RegExp( + ERR_RENAME_FILE + + "[^\n]*\n" + + "PerformReplaceRequest: destDir rename[^\n]*\n" + + "rename_file: proceeding to rename the directory\n", + "g" + ); + updateLogContents = updateLogContents.replace(re, ""); + } + + // Replace error codes since they are different on each platform. + updateLogContents = updateLogContents.replace(/, err:.*\n/g, "\n"); + // Replace to make the log parsing happy. + updateLogContents = updateLogContents.replace(/non-fatal error /g, ""); + // Remove consecutive newlines + updateLogContents = updateLogContents.replace(/\n+/g, "\n"); + // Remove leading and trailing newlines + updateLogContents = updateLogContents.replace(/^\n|\n$/g, ""); + // Replace the log paths with <test_dir_path> and <update_dir_path> + updateLogContents = replaceLogPaths(updateLogContents); + + let compareLogContents = ""; + if (aCompareLogFile) { + compareLogContents = readFileBytes(getTestDirFile(aCompareLogFile)); + } + + if (aStaged) { + compareLogContents = PERFORMING_STAGED_UPDATE + "\n" + compareLogContents; + } + + // Remove leading timestamps + compareLogContents = removeTimeStamps(compareLogContents); + + // The channel-prefs.js is defined in gTestFilesCommon which will always be + // located to the end of gTestFiles. + if ( + gTestFiles.length > 1 && + gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" && + !gTestFiles[gTestFiles.length - 1].originalContents + ) { + compareLogContents = compareLogContents.replace(/.*defaults\/.*/g, ""); + } + + if ( + gTestFiles.length > 2 && + gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI && + !gTestFiles[gTestFiles.length - 2].originalContents + ) { + compareLogContents = compareLogContents.replace( + /.*update-settings.ini.*/g, + "" + ); + } + + if (aExcludeDistDir) { + compareLogContents = compareLogContents.replace(/.*distribution\/.*/g, ""); + } + + // Remove leading and trailing newlines + compareLogContents = compareLogContents.replace(/\n+/g, "\n"); + // Remove leading and trailing newlines + compareLogContents = compareLogContents.replace(/^\n|\n$/g, ""); + + // Don't write the contents of the file to the log to reduce log spam + // unless there is a failure. + if (compareLogContents == updateLogContents) { + Assert.ok(true, "the update log contents" + MSG_SHOULD_EQUAL); + } else { + logTestInfo("the update log contents are not correct"); + logUpdateLog(FILE_LAST_UPDATE_LOG); + let aryLog = updateLogContents.split("\n"); + let aryCompare = compareLogContents.split("\n"); + // Pushing an empty string to both arrays makes it so either array's length + // can be used in the for loop below without going out of bounds. + aryLog.push(""); + aryCompare.push(""); + // xpcshell tests won't display the entire contents so log the first + // incorrect line. + for (let i = 0; i < aryLog.length; ++i) { + if (aryLog[i] != aryCompare[i]) { + logTestInfo( + "the first incorrect line is line #" + + i + + " and the " + + "value is: '" + + aryLog[i] + + "'" + ); + Assert.equal( + aryLog[i], + aryCompare[i], + "the update log contents" + MSG_SHOULD_EQUAL + ); + } + } + // This should never happen! + do_throw("Unable to find incorrect update log contents!"); + } +} + +/** + * Helper function to check if the update log contains a string. + * + * @param aCheckString + * The string to check if the update log contains. + */ +function checkUpdateLogContains(aCheckString) { + let updateLog = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n"); + updateLogContents = removeTimeStamps(updateLogContents); + updateLogContents = replaceLogPaths(updateLogContents); + Assert.notEqual( + updateLogContents.indexOf(aCheckString), + -1, + "the update log '" + + updateLog + + "' 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. + * @param aStageDirExists + * If true the staging directory will be tested for existence and if + * false the staging directory will be tested for non-existence. + * @param aToBeDeletedDirExists + * On Windows, if true the tobedeleted directory will be tested for + * existence and if false the tobedeleted directory will be tested for + * non-existence. On all othere platforms it will be tested for + * non-existence. + */ +function checkFilesAfterUpdateSuccess( + aGetFileFunc, + aStageDirExists = false, + aToBeDeletedDirExists = false +) { + debugDump("testing contents of files after a successful update"); + gTestFiles.forEach(function CFAUS_TF_FE(aTestFile) { + let testFile = aGetFileFunc( + aTestFile.relPathDir + aTestFile.fileName, + true + ); + debugDump("testing file: " + testFile.path); + if (aTestFile.compareFile || aTestFile.compareContents) { + Assert.ok( + testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path) + ); + + // Skip these tests on Windows since chmod doesn't really set permissions + // on Windows. + if (AppConstants.platform != "win" && aTestFile.comparePerms) { + // Check if the permssions as set in the complete mar file are correct. + Assert.equal( + "0o" + (testFile.permissions & 0xfff).toString(8), + "0o" + (aTestFile.comparePerms & 0xfff).toString(8), + "the file permissions" + MSG_SHOULD_EQUAL + ); + } + + let fileContents1 = readFileBytes(testFile); + let fileContents2 = aTestFile.compareFile + ? readFileBytes(getTestDirFile(aTestFile.compareFile)) + : aTestFile.compareContents; + // Don't write the contents of the file to the log to reduce log spam + // unless there is a failure. + if (fileContents1 == fileContents2) { + Assert.ok(true, "the file contents" + MSG_SHOULD_EQUAL); + } else { + Assert.equal( + fileContents1, + fileContents2, + "the file contents" + MSG_SHOULD_EQUAL + ); + } + } else { + Assert.ok( + !testFile.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path) + ); + } + }); + + debugDump( + "testing operations specified in removed-files were performed " + + "after a successful update" + ); + gTestDirs.forEach(function CFAUS_TD_FE(aTestDir) { + let testDir = aGetFileFunc(aTestDir.relPathDir, true); + debugDump("testing directory: " + testDir.path); + if (aTestDir.dirRemoved) { + Assert.ok( + !testDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(testDir.path) + ); + } else { + Assert.ok(testDir.exists(), MSG_SHOULD_EXIST + getMsgPath(testDir.path)); + + if (aTestDir.files) { + aTestDir.files.forEach(function CFAUS_TD_F_FE(aTestFile) { + let testFile = aGetFileFunc(aTestDir.relPathDir + aTestFile, true); + if (aTestDir.filesRemoved) { + Assert.ok( + !testFile.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path) + ); + } else { + Assert.ok( + testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path) + ); + } + }); + } + + if (aTestDir.subDirs) { + aTestDir.subDirs.forEach(function CFAUS_TD_SD_FE(aSubDir) { + let testSubDir = aGetFileFunc(aTestDir.relPathDir + aSubDir, true); + Assert.ok( + testSubDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(testSubDir.path) + ); + if (aTestDir.subDirFiles) { + aTestDir.subDirFiles.forEach(function CFAUS_TD_SDF_FE(aTestFile) { + let testFile = aGetFileFunc( + aTestDir.relPathDir + aSubDir + aTestFile, + true + ); + Assert.ok( + testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path) + ); + }); + } + }); + } + } + }); + + checkFilesAfterUpdateCommon(aStageDirExists, aToBeDeletedDirExists); +} + +/** + * Helper function for updater binary tests for verifying the state of files and + * directories after a failed update. + * + * @param aGetFileFunc + * The function used to get the files in the directory to be checked. + * @param aStageDirExists + * If true the staging directory will be tested for existence and if + * false the staging directory will be tested for non-existence. + * @param aToBeDeletedDirExists + * On Windows, if true the tobedeleted directory will be tested for + * existence and if false the tobedeleted directory will be tested for + * non-existence. On all othere platforms it will be tested for + * non-existence. + */ +function checkFilesAfterUpdateFailure( + aGetFileFunc, + aStageDirExists = false, + aToBeDeletedDirExists = false +) { + debugDump("testing contents of files after a failed update"); + gTestFiles.forEach(function CFAUF_TF_FE(aTestFile) { + let testFile = aGetFileFunc( + aTestFile.relPathDir + aTestFile.fileName, + true + ); + debugDump("testing file: " + testFile.path); + if (aTestFile.compareFile || aTestFile.compareContents) { + Assert.ok( + testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path) + ); + + // Skip these tests on Windows since chmod doesn't really set permissions + // on Windows. + if (AppConstants.platform != "win" && aTestFile.comparePerms) { + // Check the original permssions are retained on the file. + Assert.equal( + testFile.permissions & 0xfff, + aTestFile.comparePerms & 0xfff, + "the file permissions" + MSG_SHOULD_EQUAL + ); + } + + let fileContents1 = readFileBytes(testFile); + let fileContents2 = aTestFile.compareFile + ? readFileBytes(getTestDirFile(aTestFile.compareFile)) + : aTestFile.compareContents; + // Don't write the contents of the file to the log to reduce log spam + // unless there is a failure. + if (fileContents1 == fileContents2) { + Assert.ok(true, "the file contents" + MSG_SHOULD_EQUAL); + } else { + Assert.equal( + fileContents1, + fileContents2, + "the file contents" + MSG_SHOULD_EQUAL + ); + } + } else { + Assert.ok( + !testFile.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path) + ); + } + }); + + debugDump( + "testing operations specified in removed-files were not " + + "performed after a failed update" + ); + gTestDirs.forEach(function CFAUF_TD_FE(aTestDir) { + let testDir = aGetFileFunc(aTestDir.relPathDir, true); + Assert.ok(testDir.exists(), MSG_SHOULD_EXIST + getMsgPath(testDir.path)); + + if (aTestDir.files) { + aTestDir.files.forEach(function CFAUS_TD_F_FE(aTestFile) { + let testFile = aGetFileFunc(aTestDir.relPathDir + aTestFile, true); + Assert.ok( + testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path) + ); + }); + } + + if (aTestDir.subDirs) { + aTestDir.subDirs.forEach(function CFAUS_TD_SD_FE(aSubDir) { + let testSubDir = aGetFileFunc(aTestDir.relPathDir + aSubDir, true); + Assert.ok( + testSubDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(testSubDir.path) + ); + if (aTestDir.subDirFiles) { + aTestDir.subDirFiles.forEach(function CFAUS_TD_SDF_FE(aTestFile) { + let testFile = aGetFileFunc( + aTestDir.relPathDir + aSubDir + aTestFile, + true + ); + Assert.ok( + testFile.exists(), + MSG_SHOULD_EXIST + getMsgPath(testFile.path) + ); + }); + } + }); + } + }); + + checkFilesAfterUpdateCommon(aStageDirExists, aToBeDeletedDirExists); +} + +/** + * Helper function for updater binary tests for verifying the state of common + * files and directories after a successful or failed update. + * + * @param aStageDirExists + * If true the staging directory will be tested for existence and if + * false the staging directory will be tested for non-existence. + * @param aToBeDeletedDirExists + * On Windows, if true the tobedeleted directory will be tested for + * existence and if false the tobedeleted directory will be tested for + * non-existence. On all othere platforms it will be tested for + * non-existence. + */ +function checkFilesAfterUpdateCommon(aStageDirExists, aToBeDeletedDirExists) { + debugDump("testing extra directories"); + let stageDir = getStageDirFile(); + if (aStageDirExists) { + Assert.ok(stageDir.exists(), MSG_SHOULD_EXIST + getMsgPath(stageDir.path)); + } else { + Assert.ok( + !stageDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(stageDir.path) + ); + } + + let toBeDeletedDirExists = + AppConstants.platform == "win" ? aToBeDeletedDirExists : false; + let toBeDeletedDir = getApplyDirFile(DIR_TOBEDELETED); + if (toBeDeletedDirExists) { + Assert.ok( + toBeDeletedDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(toBeDeletedDir.path) + ); + } else { + Assert.ok( + !toBeDeletedDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(toBeDeletedDir.path) + ); + } + + let updatingDir = getApplyDirFile("updating"); + Assert.ok( + !updatingDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(updatingDir.path) + ); + + if (stageDir.exists()) { + updatingDir = stageDir.clone(); + updatingDir.append("updating"); + Assert.ok( + !updatingDir.exists(), + MSG_SHOULD_NOT_EXIST + getMsgPath(updatingDir.path) + ); + } + + debugDump( + "testing backup files should not be left behind in the " + + "application directory" + ); + let applyToDir = getApplyDirFile(); + checkFilesInDirRecursive(applyToDir, checkForBackupFiles); + + if (stageDir.exists()) { + debugDump( + "testing backup files should not be left behind in the " + + "staging directory" + ); + checkFilesInDirRecursive(stageDir, checkForBackupFiles); + } +} + +/** + * Helper function for updater binary tests for verifying the contents of the + * updater callback application log which should contain the arguments passed to + * the callback application. + * + * @param appLaunchLog (optional) + * The application log nsIFile to verify. Defaults to the second + * parameter passed to the callback executable (in the apply directory). + */ +function checkCallbackLog( + appLaunchLog = getApplyDirFile(DIR_RESOURCES + gCallbackArgs[1]) +) { + if (!appLaunchLog.exists()) { + // Uses do_timeout instead of do_execute_soon to lessen log spew. + do_timeout(FILE_IN_USE_TIMEOUT_MS, checkCallbackLog); + return; + } + + let expectedLogContents = gCallbackArgs.join("\n") + "\n"; + let logContents = readFile(appLaunchLog); + // It is possible for the log file contents check to occur before the log file + // contents are completely written so wait until the contents are the expected + // value. If the contents are never the expected value then the test will + // fail by timing out after gTimeoutRuns is greater than MAX_TIMEOUT_RUNS or + // the test harness times out the test. + const MAX_TIMEOUT_RUNS = 20000; + if (logContents != expectedLogContents) { + gTimeoutRuns++; + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + logTestInfo("callback log contents are not correct"); + // This file doesn't contain full paths so there is no need to call + // replaceLogPaths. + let aryLog = logContents.split("\n"); + let aryCompare = expectedLogContents.split("\n"); + // Pushing an empty string to both arrays makes it so either array's length + // can be used in the for loop below without going out of bounds. + aryLog.push(""); + aryCompare.push(""); + // xpcshell tests won't display the entire contents so log the incorrect + // line. + for (let i = 0; i < aryLog.length; ++i) { + if (aryLog[i] != aryCompare[i]) { + logTestInfo( + "the first incorrect line in the callback log is: " + aryLog[i] + ); + Assert.equal( + aryLog[i], + aryCompare[i], + "the callback log contents" + MSG_SHOULD_EQUAL + ); + } + } + // This should never happen! + do_throw("Unable to find incorrect callback log contents!"); + } + // Uses do_timeout instead of do_execute_soon to lessen log spew. + do_timeout(FILE_IN_USE_TIMEOUT_MS, checkCallbackLog); + return; + } + Assert.ok(true, "the callback log contents" + MSG_SHOULD_EQUAL); + + waitForFilesInUse(); +} + +/** + * Helper function for updater binary tests for getting the log and running + * files created by the test helper binary file when called with the post-update + * command line argument. + * + * @param aSuffix + * The string to append to the post update test helper binary path. + */ +function getPostUpdateFile(aSuffix) { + return getApplyDirFile(DIR_RESOURCES + gPostUpdateBinFile + aSuffix); +} + +/** + * Checks the contents of the updater post update binary log. When completed + * checkPostUpdateAppLogFinished will be called. + */ +async function checkPostUpdateAppLog() { + // Only Mac OS X and Windows support post update. + if (AppConstants.platform == "macosx" || AppConstants.platform == "win") { + let file = getPostUpdateFile(".log"); + await TestUtils.waitForCondition( + () => file.exists(), + "Waiting for file to exist, path: " + file.path + ); + + let expectedContents = "post-update\n"; + await TestUtils.waitForCondition( + () => readFile(file) == expectedContents, + "Waiting for expected file contents: " + expectedContents + ); + } +} + +/** + * Helper function to check if a file is in use on Windows by making a copy of + * a file and attempting to delete the original file. If the deletion is + * successful the copy of the original file is renamed to the original file's + * name and if the deletion is not successful the copy of the original file is + * deleted. + * + * @param aFile + * An nsIFile for the file to be checked if it is in use. + * @return true if the file can't be deleted and false otherwise. + * @throws If called from a platform other than Windows. + */ +function isFileInUse(aFile) { + if (AppConstants.platform != "win") { + do_throw("Windows only function called by a different platform!"); + } + + if (!aFile.exists()) { + debugDump("file does not exist, path: " + aFile.path); + return false; + } + + let fileBak = aFile.parent; + fileBak.append(aFile.leafName + ".bak"); + try { + if (fileBak.exists()) { + fileBak.remove(false); + } + aFile.copyTo(aFile.parent, fileBak.leafName); + aFile.remove(false); + fileBak.moveTo(aFile.parent, aFile.leafName); + debugDump("file is not in use, path: " + aFile.path); + return false; + } catch (e) { + debugDump("file in use, path: " + aFile.path + ", Exception: " + e); + try { + if (fileBak.exists()) { + fileBak.remove(false); + } + } catch (ex) { + logTestInfo( + "unable to remove backup file, path: " + + fileBak.path + + ", Exception: " + + ex + ); + } + } + return true; +} + +/** + * Waits until files that are in use that break tests are no longer in use and + * then calls doTestFinish to end the test. + */ +function waitForFilesInUse() { + if (AppConstants.platform == "win") { + let fileNames = [ + FILE_APP_BIN, + FILE_UPDATER_BIN, + FILE_MAINTENANCE_SERVICE_INSTALLER_BIN, + ]; + for (let i = 0; i < fileNames.length; ++i) { + let file = getApplyDirFile(fileNames[i]); + if (isFileInUse(file)) { + do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForFilesInUse); + return; + } + } + } + + debugDump("calling doTestFinish"); + doTestFinish(); +} + +/** + * Helper function for updater binary tests for verifying there are no update + * backup files left behind after an update. + * + * @param aFile + * An nsIFile to check if it has moz-backup for its extension. + */ +function checkForBackupFiles(aFile) { + Assert.notEqual( + getFileExtension(aFile), + "moz-backup", + "the file's extension should not equal moz-backup" + getMsgPath(aFile.path) + ); +} + +/** + * Helper function for updater binary tests for recursively enumerating a + * directory and calling a callback function with the file as a parameter for + * each file found. + * + * @param aDir + * A nsIFile for the directory to be deleted + * @param aCallback + * A callback function that will be called with the file as a + * parameter for each file found. + */ +function checkFilesInDirRecursive(aDir, aCallback) { + if (!aDir.exists()) { + do_throw("Directory must exist!"); + } + + let dirEntries = aDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + let entry = dirEntries.nextFile; + + if (entry.exists()) { + if (entry.isDirectory()) { + checkFilesInDirRecursive(entry, aCallback); + } else { + aCallback(entry); + } + } + } +} + +/** + * Waits for an update check request to complete and asserts that the results + * are as-expected. + * + * @param aSuccess + * Whether the update check succeeds or not. If aSuccess is true then + * the check should succeed and if aSuccess is false then the check + * should fail. + * @param aExpectedValues + * An object with common values to check. + * @return A promise which will resolve with the nsIUpdateCheckResult object + * once the update check is complete. + */ +async function waitForUpdateCheck(aSuccess, aExpectedValues = {}) { + let check = gUpdateChecker.checkForUpdates(gUpdateChecker.FOREGROUND_CHECK); + let result = await check.result; + Assert.ok(result.checksAllowed, "We should be able to check for updates"); + Assert.equal( + result.succeeded, + aSuccess, + "the update check should " + (aSuccess ? "succeed" : "error") + ); + if (aExpectedValues.updateCount) { + Assert.equal( + aExpectedValues.updateCount, + result.updates.length, + "the update count" + MSG_SHOULD_EQUAL + ); + } + if (aExpectedValues.url) { + Assert.equal( + aExpectedValues.url, + result.request.channel.originalURI.spec, + "the url" + MSG_SHOULD_EQUAL + ); + } + return result; +} + +/** + * Downloads an update and waits for the download onStopRequest. + * + * @param aUpdates + * An array of updates to select from to download an update. + * @param aUpdateCount + * The number of updates in the aUpdates array. + * @param aExpectedStatus + * The download onStopRequest expected status. + * @return A promise which will resolve the first time the update download + * onStopRequest occurs and returns the arguments from onStopRequest. + */ +async function waitForUpdateDownload(aUpdates, aExpectedStatus) { + let bestUpdate = gAUS.selectUpdate(aUpdates); + let success = await gAUS.downloadUpdate(bestUpdate, false); + if (!success) { + do_throw("nsIApplicationUpdateService:downloadUpdate returned " + success); + } + return new Promise(resolve => + gAUS.addDownloadListener({ + onStartRequest: aRequest => {}, + onProgress: (aRequest, aContext, aProgress, aMaxProgress) => {}, + onStatus: (aRequest, aStatus, aStatusText) => {}, + onStopRequest(request, status) { + gAUS.removeDownloadListener(this); + Assert.equal( + aExpectedStatus, + status, + "the download status" + MSG_SHOULD_EQUAL + ); + resolve(request, status); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsIProgressEventSink", + ]), + }) + ); +} + +/** + * Helper for starting the http server used by the tests + */ +function start_httpserver() { + let dir = getTestDirFile(); + debugDump("http server directory path: " + dir.path); + + if (!dir.isDirectory()) { + do_throw( + "A file instead of a directory was specified for HttpServer " + + "registerDirectory! Path: " + + dir.path + ); + } + + let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + gTestserver = new HttpServer(); + gTestserver.registerDirectory("/", dir); + gTestserver.registerPathHandler("/" + gHTTPHandlerPath, pathHandler); + gTestserver.start(-1); + let testserverPort = gTestserver.identity.primaryPort; + // eslint-disable-next-line no-global-assign + gURLData = URL_HOST + ":" + testserverPort + "/"; + debugDump("http server port = " + testserverPort); +} + +/** + * Custom path handler for the http server + * + * @param aMetadata + * The http metadata for the request. + * @param aResponse + * The http response for the request. + */ +function pathHandler(aMetadata, aResponse) { + gUpdateCheckCount += 1; + aResponse.setHeader("Content-Type", "text/xml", false); + aResponse.setStatusLine(aMetadata.httpVersion, 200, "OK"); + aResponse.bodyOutputStream.write(gResponseBody, gResponseBody.length); +} + +/** + * Helper for stopping the http server used by the tests + * + * @param aCallback + * The callback to call after stopping the http server. + */ +function stop_httpserver(aCallback) { + Assert.ok(!!aCallback, "the aCallback parameter should be defined"); + gTestserver.stop(aCallback); +} + +/** + * Creates an nsIXULAppInfo + * + * @param aID + * The ID of the test application + * @param aName + * A name for the test application + * @param aVersion + * The version of the application + * @param aPlatformVersion + * The gecko version of the application + */ +function createAppInfo(aID, aName, aVersion, aPlatformVersion) { + updateAppInfo({ + vendor: APP_INFO_VENDOR, + name: aName, + ID: aID, + version: aVersion, + appBuildID: "2007010101", + platformVersion: aPlatformVersion, + platformBuildID: "2007010101", + inSafeMode: false, + logConsoleErrors: true, + OS: "XPCShell", + XPCOMABI: "noarch-spidermonkey", + }); +} + +/** + * Returns the platform specific arguments used by nsIProcess when launching + * the application. + * + * @param aExtraArgs (optional) + * An array of extra arguments to append to the default arguments. + * @return an array of arguments to be passed to nsIProcess. + * + * Note: a shell is necessary to pipe the application's console output which + * would otherwise pollute the xpcshell log. + * + * Command line arguments used when launching the application: + * -no-remote prevents shell integration from being affected by an existing + * application process. + * -test-process-updates makes the application exit after being relaunched by + * the updater. + * the platform specific string defined by PIPE_TO_NULL to output both stdout + * and stderr to null. This is needed to prevent output from the application + * from ending up in the xpchsell log. + */ +function getProcessArgs(aExtraArgs) { + if (!aExtraArgs) { + aExtraArgs = []; + } + + let appBin = getApplyDirFile(DIR_MACOS + FILE_APP_BIN); + Assert.ok(appBin.exists(), MSG_SHOULD_EXIST + ", path: " + appBin.path); + let appBinPath = appBin.path; + + // The profile must be specified for the tests that launch the application to + // run locally when the profiles.ini and installs.ini files already exist. + // We can't use getApplyDirFile to find the profile path because on Windows + // for service tests that would place the profile inside Program Files, and + // this test script has permission to write in Program Files, but the + // application may drop those permissions. So for Windows service tests we + // override that path with the per-test temp directory that xpcshell provides, + // which should be user writable. + let profileDir = appBin.parent.parent; + if (gIsServiceTest && IS_AUTHENTICODE_CHECK_ENABLED) { + profileDir = do_get_tempdir(); + } + profileDir.append("profile"); + let profilePath = profileDir.path; + + let args; + if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") { + let launchScript = getLaunchScript(); + // Precreate the script with executable permissions + launchScript.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_DIRECTORY); + + let scriptContents = "#! /bin/sh\n"; + scriptContents += "export XRE_PROFILE_PATH=" + profilePath + "\n"; + scriptContents += + appBinPath + + " -no-remote -test-process-updates " + + aExtraArgs.join(" ") + + " " + + PIPE_TO_NULL; + writeFile(launchScript, scriptContents); + debugDump( + "created " + launchScript.path + " containing:\n" + scriptContents + ); + args = [launchScript.path]; + } else { + args = [ + "/D", + "/Q", + "/C", + appBinPath, + "-profile", + profilePath, + "-no-remote", + "-test-process-updates", + "-wait-for-browser", + ] + .concat(aExtraArgs) + .concat([PIPE_TO_NULL]); + } + return args; +} + +/** + * Gets a file path for the application to dump its arguments into. This is used + * to verify that a callback application is launched. + * + * @return the file for the application to dump its arguments into. + */ +function getAppArgsLogPath() { + let appArgsLog = do_get_file("/" + gTestID + "_app_args_log", true); + if (appArgsLog.exists()) { + appArgsLog.remove(false); + } + let appArgsLogPath = appArgsLog.path; + if (/ /.test(appArgsLogPath)) { + appArgsLogPath = '"' + appArgsLogPath + '"'; + } + return appArgsLogPath; +} + +/** + * Gets the nsIFile reference for the shell script to launch the application. If + * the file exists it will be removed by this function. + * + * @return the nsIFile for the shell script to launch the application. + */ +function getLaunchScript() { + let launchScript = do_get_file("/" + gTestID + "_launch.sh", true); + if (launchScript.exists()) { + launchScript.remove(false); + } + return launchScript; +} + +/** + * Makes GreD, XREExeF, and UpdRootD point to unique file system locations so + * xpcshell tests can run in parallel and to keep the environment clean. + */ +function adjustGeneralPaths() { + let dirProvider = { + getFile: function AGP_DP_getFile(aProp, aPersistent) { + // Set the value of persistent to false so when this directory provider is + // unregistered it will revert back to the original provider. + aPersistent.value = false; + switch (aProp) { + case NS_GRE_DIR: + return getApplyDirFile(DIR_RESOURCES); + case NS_GRE_BIN_DIR: + return getApplyDirFile(DIR_MACOS); + case XRE_EXECUTABLE_FILE: + return getApplyDirFile(DIR_MACOS + FILE_APP_BIN); + case XRE_UPDATE_ROOT_DIR: + return getMockUpdRootD(); + case XRE_OLD_UPDATE_ROOT_DIR: + return getMockUpdRootD(true); + } + return null; + }, + QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), + }; + let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService); + ds.QueryInterface(Ci.nsIProperties).undefine(NS_GRE_DIR); + ds.QueryInterface(Ci.nsIProperties).undefine(NS_GRE_BIN_DIR); + ds.QueryInterface(Ci.nsIProperties).undefine(XRE_EXECUTABLE_FILE); + ds.registerProvider(dirProvider); + registerCleanupFunction(function AGP_cleanup() { + debugDump("start - unregistering directory provider"); + + if (gAppTimer) { + debugDump("start - cancel app timer"); + gAppTimer.cancel(); + gAppTimer = null; + debugDump("finish - cancel app timer"); + } + + if (gProcess && gProcess.isRunning) { + debugDump("start - kill process"); + try { + gProcess.kill(); + } catch (e) { + debugDump("kill process failed, Exception: " + e); + } + gProcess = null; + debugDump("finish - kill process"); + } + + if (gPIDPersistProcess && gPIDPersistProcess.isRunning) { + debugDump("start - kill pid persist process"); + try { + gPIDPersistProcess.kill(); + } catch (e) { + debugDump("kill pid persist process failed, Exception: " + e); + } + gPIDPersistProcess = null; + debugDump("finish - kill pid persist process"); + } + + if (gHandle) { + try { + debugDump("start - closing handle"); + let kernel32 = ctypes.open("kernel32"); + let CloseHandle = kernel32.declare( + "CloseHandle", + ctypes.winapi_abi, + ctypes.bool /* return*/, + ctypes.voidptr_t /* handle*/ + ); + if (!CloseHandle(gHandle)) { + debugDump("call to CloseHandle failed"); + } + kernel32.close(); + gHandle = null; + debugDump("finish - closing handle"); + } catch (e) { + debugDump("call to CloseHandle failed, Exception: " + e); + } + } + + ds.unregisterProvider(dirProvider); + cleanupTestCommon(); + + // Now that our provider is unregistered, reset the lock a second time so + // that we know the lock we're interested in gets released (xpcshell + // doesn't always run a proper XPCOM shutdown sequence, which is where that + // would normally be happening). + let syncManager = Cc[ + "@mozilla.org/updates/update-sync-manager;1" + ].getService(Ci.nsIUpdateSyncManager); + syncManager.resetLock(); + + debugDump("finish - unregistering directory provider"); + }); +} + +/** + * The timer callback to kill the process if it takes too long. + */ +const gAppTimerCallback = { + notify: function TC_notify(aTimer) { + gAppTimer = null; + if (gProcess.isRunning) { + logTestInfo("attempting to kill process"); + gProcess.kill(); + } + Assert.ok(false, "launch application timer expired"); + }, + QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]), +}; + +/** + * Launches an application to apply an update. + * + * @param aExpectedStatus + * The expected value of update.status when the update finishes. + */ +async function runUpdateUsingApp(aExpectedStatus) { + debugDump("start - launching application to apply update"); + + // The maximum number of milliseconds the process that is launched can run + // before the test will try to kill it. + const APP_TIMER_TIMEOUT = 120000; + let launchBin = getLaunchBin(); + let args = getProcessArgs(); + debugDump("launching " + launchBin.path + " " + args.join(" ")); + + gProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + gProcess.init(launchBin); + + gAppTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gAppTimer.initWithCallback( + gAppTimerCallback, + APP_TIMER_TIMEOUT, + Ci.nsITimer.TYPE_ONE_SHOT + ); + + setEnvironment(); + + debugDump("launching application"); + gProcess.run(true, args, args.length); + debugDump("launched application exited"); + + resetEnvironment(); + + if (AppConstants.platform == "win") { + waitForApplicationStop(FILE_UPDATER_BIN); + } + + let file = getUpdateDirFile(FILE_UPDATE_STATUS); + await TestUtils.waitForCondition( + () => file.exists(), + "Waiting for file to exist, path: " + file.path + ); + + await TestUtils.waitForCondition( + () => readStatusFile() == aExpectedStatus, + "Waiting for expected status file contents: " + aExpectedStatus + ).catch(e => { + // Instead of throwing let the check below fail the test so the status + // file's contents are logged. + logTestInfo(e); + }); + Assert.equal( + readStatusFile(), + aExpectedStatus, + "the status file state" + MSG_SHOULD_EQUAL + ); + + // Don't check for an update log when the code in nsUpdateDriver.cpp skips + // updating. + if ( + aExpectedStatus != STATE_PENDING && + aExpectedStatus != STATE_PENDING_SVC && + aExpectedStatus != STATE_APPLIED && + aExpectedStatus != STATE_APPLIED_SVC + ) { + // Don't proceed until the update log has been created. + file = getUpdateDirFile(FILE_UPDATE_LOG); + await TestUtils.waitForCondition( + () => file.exists(), + "Waiting for file to exist, path: " + file.path + ); + } + + debugDump("finish - launching application to apply update"); +} + +/* This Mock incremental downloader is used to verify that connection interrupts + * work correctly in updater code. The implementation of the mock incremental + * downloader is very simple, it simply copies the file to the destination + * location. + */ +function initMockIncrementalDownload() { + const INC_CONTRACT_ID = "@mozilla.org/network/incremental-download;1"; + let incrementalDownloadCID = MockRegistrar.register( + INC_CONTRACT_ID, + IncrementalDownload + ); + registerCleanupFunction(() => { + MockRegistrar.unregister(incrementalDownloadCID); + }); +} + +function IncrementalDownload() { + this.wrappedJSObject = this; +} + +IncrementalDownload.prototype = { + /* nsIIncrementalDownload */ + init(uri, file, chunkSize, intervalInSeconds) { + this._destination = file; + this._URI = uri; + this._finalURI = uri; + }, + + start(observer, ctxt) { + // Do the actual operation async to give a chance for observers to add + // themselves. + Services.tm.dispatchToMainThread(() => { + this._observer = observer.QueryInterface(Ci.nsIRequestObserver); + this._ctxt = ctxt; + this._observer.onStartRequest(this); + let mar = getTestDirFile(FILE_SIMPLE_MAR); + mar.copyTo(this._destination.parent, this._destination.leafName); + let status = Cr.NS_OK; + switch (gIncrementalDownloadErrorType++) { + case 0: + status = Cr.NS_ERROR_NET_RESET; + break; + case 1: + status = Cr.NS_ERROR_CONNECTION_REFUSED; + break; + case 2: + status = Cr.NS_ERROR_NET_RESET; + break; + case 3: + status = Cr.NS_OK; + break; + case 4: + status = Cr.NS_ERROR_OFFLINE; + // After we report offline, we want to eventually show offline + // status being changed to online. + Services.tm.dispatchToMainThread(function() { + Services.obs.notifyObservers( + gAUS, + "network:offline-status-changed", + "online" + ); + }); + break; + } + this._observer.onStopRequest(this, status); + }); + }, + + get URI() { + return this._URI; + }, + + get currentSize() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + get destination() { + return this._destination; + }, + + get finalURI() { + return this._finalURI; + }, + + get totalSize() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + /* nsIRequest */ + cancel(aStatus) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + suspend() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + isPending() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + _loadFlags: 0, + get loadFlags() { + return this._loadFlags; + }, + set loadFlags(val) { + this._loadFlags = val; + }, + + _loadGroup: null, + get loadGroup() { + return this._loadGroup; + }, + set loadGroup(val) { + this._loadGroup = val; + }, + + _name: "", + get name() { + return this._name; + }, + + _status: 0, + get status() { + return this._status; + }, + QueryInterface: ChromeUtils.generateQI(["nsIIncrementalDownload"]), +}; + +/** + * Sets the environment that will be used by the application process when it is + * launched. + */ +function setEnvironment() { + if (AppConstants.platform == "win") { + // The tests use nsIProcess to launch the updater and it is simpler to just + // set an environment variable and have the test updater set the current + // working directory than it is to set the current working directory in the + // test itself. + Services.env.set("CURWORKDIRPATH", getApplyDirFile().path); + } + + // Prevent setting the environment more than once. + if (gShouldResetEnv !== undefined) { + return; + } + + gShouldResetEnv = true; + + if ( + AppConstants.platform == "win" && + !Services.env.exists("XRE_NO_WINDOWS_CRASH_DIALOG") + ) { + gAddedEnvXRENoWindowsCrashDialog = true; + debugDump( + "setting the XRE_NO_WINDOWS_CRASH_DIALOG environment " + + "variable to 1... previously it didn't exist" + ); + Services.env.set("XRE_NO_WINDOWS_CRASH_DIALOG", "1"); + } + + if (Services.env.exists("XPCOM_MEM_LEAK_LOG")) { + gEnvXPCOMMemLeakLog = Services.env.get("XPCOM_MEM_LEAK_LOG"); + debugDump( + "removing the XPCOM_MEM_LEAK_LOG environment variable... " + + "previous value " + + gEnvXPCOMMemLeakLog + ); + Services.env.set("XPCOM_MEM_LEAK_LOG", ""); + } + + if (Services.env.exists("XPCOM_DEBUG_BREAK")) { + gEnvXPCOMDebugBreak = Services.env.get("XPCOM_DEBUG_BREAK"); + debugDump( + "setting the XPCOM_DEBUG_BREAK environment variable to " + + "warn... previous value " + + gEnvXPCOMDebugBreak + ); + } else { + debugDump( + "setting the XPCOM_DEBUG_BREAK environment variable to " + + "warn... previously it didn't exist" + ); + } + + Services.env.set("XPCOM_DEBUG_BREAK", "warn"); + + if (gEnvForceServiceFallback) { + // This env variable forces the updater to use the service in an invalid + // way, so that it has to fall back to updating without the service. + debugDump("setting MOZ_FORCE_SERVICE_FALLBACK environment variable to 1"); + Services.env.set("MOZ_FORCE_SERVICE_FALLBACK", "1"); + } else if (gIsServiceTest) { + debugDump("setting MOZ_NO_SERVICE_FALLBACK environment variable to 1"); + Services.env.set("MOZ_NO_SERVICE_FALLBACK", "1"); + } +} + +/** + * Sets the environment back to the original values after launching the + * application. + */ +function resetEnvironment() { + // Prevent resetting the environment more than once. + if (gShouldResetEnv !== true) { + return; + } + + gShouldResetEnv = false; + + if (gEnvXPCOMMemLeakLog) { + debugDump( + "setting the XPCOM_MEM_LEAK_LOG environment variable back to " + + gEnvXPCOMMemLeakLog + ); + Services.env.set("XPCOM_MEM_LEAK_LOG", gEnvXPCOMMemLeakLog); + } + + if (gEnvXPCOMDebugBreak) { + debugDump( + "setting the XPCOM_DEBUG_BREAK environment variable back to " + + gEnvXPCOMDebugBreak + ); + Services.env.set("XPCOM_DEBUG_BREAK", gEnvXPCOMDebugBreak); + } else if (Services.env.exists("XPCOM_DEBUG_BREAK")) { + debugDump("clearing the XPCOM_DEBUG_BREAK environment variable"); + Services.env.set("XPCOM_DEBUG_BREAK", ""); + } + + if (AppConstants.platform == "win" && gAddedEnvXRENoWindowsCrashDialog) { + debugDump("removing the XRE_NO_WINDOWS_CRASH_DIALOG environment variable"); + Services.env.set("XRE_NO_WINDOWS_CRASH_DIALOG", ""); + } + + if (gEnvForceServiceFallback) { + debugDump("removing MOZ_FORCE_SERVICE_FALLBACK environment variable"); + Services.env.set("MOZ_FORCE_SERVICE_FALLBACK", ""); + } else if (gIsServiceTest) { + debugDump("removing MOZ_NO_SERVICE_FALLBACK environment variable"); + Services.env.set("MOZ_NO_SERVICE_FALLBACK", ""); + } +} diff --git a/toolkit/mozapps/update/tests/diff_base_service.bash b/toolkit/mozapps/update/tests/diff_base_service.bash new file mode 100644 index 0000000000..bf2338666d --- /dev/null +++ b/toolkit/mozapps/update/tests/diff_base_service.bash @@ -0,0 +1,116 @@ +#!/usr/bin/env bash + +# Compare the common files in unit_base_updater and unit_service_updater. + +while read LINE1 && read LINE2 && read BLANK; do + diff -U 3 "unit_base_updater/$LINE1" "unit_service_updater/$LINE2" +done <<END +invalidArgInstallDirPathTooLongFailure.js +invalidArgInstallDirPathTooLongFailureSvc.js + +invalidArgInstallDirPathTraversalFailure.js +invalidArgInstallDirPathTraversalFailureSvc.js + +invalidArgInstallWorkingDirPathNotSameFailure_win.js +invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js + +invalidArgPatchDirPathTraversalFailure.js +invalidArgPatchDirPathTraversalFailureSvc.js + +invalidArgStageDirNotInInstallDirFailure_win.js +invalidArgStageDirNotInInstallDirFailureSvc_win.js + +invalidArgWorkingDirPathLocalUNCFailure_win.js +invalidArgWorkingDirPathLocalUNCFailureSvc_win.js + +invalidArgWorkingDirPathRelativeFailure.js +invalidArgWorkingDirPathRelativeFailureSvc.js + +marAppApplyDirLockedStageFailure_win.js +marAppApplyDirLockedStageFailureSvc_win.js + +marAppApplyUpdateAppBinInUseStageSuccess_win.js +marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js + +marAppApplyUpdateStageSuccess.js +marAppApplyUpdateStageSuccessSvc.js + +marAppApplyUpdateSuccess.js +marAppApplyUpdateSuccessSvc.js + +marAppInUseBackgroundTaskFailure_win.js +marAppInUseBackgroundTaskFailureSvc_win.js + +marAppInUseStageFailureComplete_win.js +marAppInUseStageFailureCompleteSvc_win.js + +marAppInUseSuccessComplete.js +marAppInUseSuccessCompleteSvc.js + +marCallbackAppStageSuccessComplete_win.js +marCallbackAppStageSuccessCompleteSvc_win.js + +marCallbackAppStageSuccessPartial_win.js +marCallbackAppStageSuccessPartialSvc_win.js + +marCallbackAppSuccessComplete_win.js +marCallbackAppSuccessCompleteSvc_win.js + +marCallbackAppSuccessPartial_win.js +marCallbackAppSuccessPartialSvc_win.js + +marFailurePartial.js +marFailurePartialSvc.js + +marFileInUseStageFailureComplete_win.js +marFileInUseStageFailureCompleteSvc_win.js + +marFileInUseStageFailurePartial_win.js +marFileInUseStageFailurePartialSvc_win.js + +marFileInUseSuccessComplete_win.js +marFileInUseSuccessCompleteSvc_win.js + +marFileInUseSuccessPartial_win.js +marFileInUseSuccessPartialSvc_win.js + +marFileLockedFailureComplete_win.js +marFileLockedFailureCompleteSvc_win.js + +marFileLockedFailurePartial_win.js +marFileLockedFailurePartialSvc_win.js + +marFileLockedStageFailureComplete_win.js +marFileLockedStageFailureCompleteSvc_win.js + +marFileLockedStageFailurePartial_win.js +marFileLockedStageFailurePartialSvc_win.js + +marRMRFDirFileInUseStageFailureComplete_win.js +marRMRFDirFileInUseStageFailureCompleteSvc_win.js + +marRMRFDirFileInUseStageFailurePartial_win.js +marRMRFDirFileInUseStageFailurePartialSvc_win.js + +marRMRFDirFileInUseSuccessComplete_win.js +marRMRFDirFileInUseSuccessCompleteSvc_win.js + +marRMRFDirFileInUseSuccessPartial_win.js +marRMRFDirFileInUseSuccessPartialSvc_win.js + +marStageFailurePartial.js +marStageFailurePartialSvc.js + +marStageSuccessComplete.js +marStageSuccessCompleteSvc.js + +marStageSuccessPartial.js +marStageSuccessPartialSvc.js + +marSuccessComplete.js +marSuccessCompleteSvc.js + +marSuccessPartial.js +marSuccessPartialSvc.js + +END diff --git a/toolkit/mozapps/update/tests/marionette/marionette.ini b/toolkit/mozapps/update/tests/marionette/marionette.ini new file mode 100644 index 0000000000..4bb7f3a640 --- /dev/null +++ b/toolkit/mozapps/update/tests/marionette/marionette.ini @@ -0,0 +1,3 @@ +[test_no_window_update_restart.py] +run-if = os == "mac" +reason = Test of a mac only feature diff --git a/toolkit/mozapps/update/tests/marionette/test_no_window_update_restart.py b/toolkit/mozapps/update/tests/marionette/test_no_window_update_restart.py new file mode 100644 index 0000000000..245efc774d --- /dev/null +++ b/toolkit/mozapps/update/tests/marionette/test_no_window_update_restart.py @@ -0,0 +1,255 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +# This test is not written in a very modular way because update tests are generally not done with +# Marionette, so this is sort of a one-off. We can't successfully just load all the actual setup +# code that the normal update tests use, so this test basically just copies the bits that it needs. +# The reason that this is a Marionette test at all is that, even if we stub out the quit/restart +# call, we need no windows to be open to test the relevant functionality, but xpcshell doesn't do +# windows at all and mochitest has a test runner window that Firefox recognizes, but mustn't close +# during testing. + +from marionette_driver import Wait, errors +from marionette_harness import MarionetteTestCase + + +class TestNoWindowUpdateRestart(MarionetteTestCase): + def setUp(self): + super(TestNoWindowUpdateRestart, self).setUp() + + self.marionette.delete_session() + self.marionette.start_session({"moz:windowless": True}) + # See Bug 1777956 + window = self.marionette.window_handles[0] + self.marionette.switch_to_window(window) + + # Every part of this test ought to run in the chrome context. + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + self.setUpBrowser() + self.origDisabledForTesting = self.marionette.get_pref( + "app.update.disabledForTesting" + ) + self.resetUpdate() + + def setUpBrowser(self): + self.origAppUpdateAuto = self.marionette.execute_async_script( + """ + let [resolve] = arguments; + + (async () => { + Services.prefs.setIntPref("app.update.download.attempts", 0); + Services.prefs.setIntPref("app.update.download.maxAttempts", 0); + Services.prefs.setBoolPref("app.update.staging.enabled", false); + Services.prefs.setBoolPref("app.update.noWindowAutoRestart.enabled", true); + Services.prefs.setIntPref("app.update.noWindowAutoRestart.delayMs", 1000); + Services.prefs.clearUserPref("testing.no_window_update_restart.silent_restart_env"); + + let { UpdateUtils } = ChromeUtils.importESModule( + "resource://gre/modules/UpdateUtils.sys.mjs" + ); + let origAppUpdateAuto = await UpdateUtils.getAppUpdateAutoEnabled(); + await UpdateUtils.setAppUpdateAutoEnabled(true); + + // Prevent the update sync manager from thinking there are two instances running + let exePath = Services.dirsvc.get("XREExeF", Ci.nsIFile); + let dirProvider = { + getFile: function AGP_DP_getFile(aProp, aPersistent) { + aPersistent.value = false; + switch (aProp) { + case "XREExeF": + exePath.append("browser-test"); + return exePath; + } + return null; + }, + QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), + }; + let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService); + ds.QueryInterface(Ci.nsIProperties).undefine("XREExeF"); + ds.registerProvider(dirProvider); + let gSyncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + gSyncManager.resetLock(); + ds.unregisterProvider(dirProvider); + + return origAppUpdateAuto; + })().then(resolve); + """ + ) + + def tearDown(self): + self.tearDownBrowser() + self.resetUpdate() + + # Reset context to the default. + self.marionette.set_context(self.marionette.CONTEXT_CONTENT) + + super(TestNoWindowUpdateRestart, self).tearDown() + + def tearDownBrowser(self): + self.marionette.execute_async_script( + """ + let [origAppUpdateAuto, origDisabledForTesting, resolve] = arguments; + (async () => { + Services.prefs.setBoolPref("app.update.disabledForTesting", origDisabledForTesting); + Services.prefs.clearUserPref("app.update.download.attempts"); + Services.prefs.clearUserPref("app.update.download.maxAttempts"); + Services.prefs.clearUserPref("app.update.staging.enabled"); + Services.prefs.clearUserPref("app.update.noWindowAutoRestart.enabled"); + Services.prefs.clearUserPref("app.update.noWindowAutoRestart.delayMs"); + Services.prefs.clearUserPref("testing.no_window_update_restart.silent_restart_env"); + + let { UpdateUtils } = ChromeUtils.importESModule( + "resource://gre/modules/UpdateUtils.sys.mjs" + ); + await UpdateUtils.setAppUpdateAutoEnabled(origAppUpdateAuto); + })().then(resolve); + """, + script_args=(self.origAppUpdateAuto, self.origDisabledForTesting), + ) + + def test_update_on_last_window_close(self): + # By preparing an update and closing all windows, we activate the No + # Window Update Restart feature (see Bug 1720742) which causes Firefox + # to restart to install updates. + self.marionette.restart( + callback=self.prepare_update_and_close_all_windows, in_app=True + ) + + # Firefox should come back without any windows (i.e. silently). + with self.assertRaises(errors.TimeoutException): + wait = Wait( + self.marionette, + ignored_exceptions=errors.NoSuchWindowException, + timeout=5, + ) + wait.until(lambda _: self.marionette.window_handles) + + # Reset the browser and active WebDriver session + self.marionette.restart(in_app=True) + self.marionette.delete_session() + self.marionette.start_session() + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + quit_flags_correct = self.marionette.get_pref( + "testing.no_window_update_restart.silent_restart_env" + ) + self.assertTrue(quit_flags_correct) + + # Normally, the update status file would have been removed at this point by Post Update + # Processing. But restarting resets app.update.disabledForTesting, which causes that to be + # skipped, allowing us to look at the update status file directly. + update_status_path = self.marionette.execute_script( + """ + let statusFile = FileUtils.getDir("UpdRootD", ["updates", "0"], true); + statusFile.append("update.status"); + return statusFile.path; + """ + ) + with open(update_status_path, "r") as f: + # If Firefox was built with "--enable-unverified-updates" (or presumably if we tested + # with an actual, signed update), the update should succeed. Otherwise, it will fail + # with CERT_VERIFY_ERROR (error code 19). Unfortunately, there is no good way to tell + # which of those situations we are in. Luckily, it doesn't matter, because we aren't + # trying to test whether the update applied successfully, just whether the + # "No Window Update Restart" feature works. + self.assertIn(f.read().strip(), ["succeeded", "failed: 19"]) + + def resetUpdate(self): + self.marionette.execute_script( + """ + let UM = Cc["@mozilla.org/updates/update-manager;1"].getService(Ci.nsIUpdateManager); + UM.QueryInterface(Ci.nsIObserver).observe(null, "um-reload-update-data", "skip-files"); + + let { UpdateListener } = ChromeUtils.import("resource://gre/modules/UpdateListener.jsm"); + UpdateListener.reset(); + + let { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" + ); + AppMenuNotifications.removeNotification(/.*/); + + // Remove old update files so that they don't interfere with tests. + let rootUpdateDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile); + let updateDir = rootUpdateDir.clone(); + updateDir.append("updates"); + let patchDir = updateDir.clone(); + patchDir.append("0"); + + let filesToRemove = []; + let addFileToRemove = (dir, filename) => { + let file = dir.clone(); + file.append(filename); + filesToRemove.push(file); + }; + + addFileToRemove(rootUpdateDir, "active-update.xml"); + addFileToRemove(rootUpdateDir, "updates.xml"); + addFileToRemove(patchDir, "bt.result"); + addFileToRemove(patchDir, "update.status"); + addFileToRemove(patchDir, "update.version"); + addFileToRemove(patchDir, "update.mar"); + addFileToRemove(patchDir, "updater.ini"); + addFileToRemove(updateDir, "backup-update.log"); + addFileToRemove(updateDir, "last-update.log"); + addFileToRemove(patchDir, "update.log"); + + for (const file of filesToRemove) { + try { + if (file.exists()) { + file.remove(false); + } + } catch (e) { + console.warn("Unable to remove file. Path: '" + file.path + "', Exception: " + e); + } + } + """ + ) + + def prepare_update_and_close_all_windows(self): + self.marionette.execute_async_script( + """ + let [updateURLString, resolve] = arguments; + + (async () => { + let updateDownloadedPromise = new Promise(innerResolve => { + Services.obs.addObserver(function callback() { + Services.obs.removeObserver(callback, "update-downloaded"); + innerResolve(); + }, "update-downloaded"); + }); + + // Set the update URL to the one that was passed in. + let mockAppInfo = Object.create(Services.appinfo, { + updateURL: { + configurable: true, + enumerable: true, + writable: false, + value: updateURLString, + }, + }); + Services.appinfo = mockAppInfo; + + // We aren't going to flip this until after the URL is set because the test fails + // if we hit the real update server. + Services.prefs.setBoolPref("app.update.disabledForTesting", false); + + let aus = Cc["@mozilla.org/updates/update-service;1"] + .getService(Ci.nsIApplicationUpdateService); + aus.checkForBackgroundUpdates(); + + await updateDownloadedPromise; + + Services.obs.addObserver((aSubject, aTopic, aData) => { + let silent_restart = Services.env.get("MOZ_APP_SILENT_START") == 1 && Services.env.get("MOZ_APP_RESTART") == 1; + Services.prefs.setBoolPref("testing.no_window_update_restart.silent_restart_env", silent_restart); + }, "quit-application-granted"); + + for (const win of Services.wm.getEnumerator("navigator:browser")) { + win.close(); + } + })().then(resolve); + """, + script_args=(self.marionette.absolute_url("update.xml"),), + ) diff --git a/toolkit/mozapps/update/tests/moz.build b/toolkit/mozapps/update/tests/moz.build new file mode 100644 index 0000000000..d0adf99c84 --- /dev/null +++ b/toolkit/mozapps/update/tests/moz.build @@ -0,0 +1,118 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +FINAL_TARGET = "_tests/xpcshell/toolkit/mozapps/update/tests/data" + +if not CONFIG["MOZ_SUITE"]: + BROWSER_CHROME_MANIFESTS += [ + "browser/browser.ini", + "browser/manual_app_update_only/browser.ini", + ] + if CONFIG["MOZ_BITS_DOWNLOAD"]: + BROWSER_CHROME_MANIFESTS += ["browser/browser.bits.ini"] + +XPCSHELL_TESTS_MANIFESTS += [ + "unit_aus_update/xpcshell.ini", + "unit_base_updater/xpcshell.ini", +] + +if CONFIG["MOZ_MAINTENANCE_SERVICE"]: + XPCSHELL_TESTS_MANIFESTS += ["unit_service_updater/xpcshell.ini"] + +if CONFIG["MOZ_BUILD_APP"] == "browser" and CONFIG["MOZ_UPDATE_AGENT"]: + XPCSHELL_TESTS_MANIFESTS += ["unit_background_update/xpcshell.ini"] + +SimplePrograms( + [ + "TestAUSHelper", + "TestAUSReadStrings", + ] +) + +LOCAL_INCLUDES += [ + "/toolkit/mozapps/update", + "/toolkit/mozapps/update/common", +] + +if CONFIG["OS_ARCH"] == "WINNT": + OS_LIBS += [ + "shlwapi", + "user32", + "uuid", + ] + +USE_LIBS += [ + "updatecommon", +] + +for var in ("MOZ_APP_VENDOR", "MOZ_APP_BASENAME"): + DEFINES[var] = CONFIG[var] + +DEFINES["NS_NO_XPCOM"] = True + +DisableStlWrapping() + +if CONFIG["MOZ_MAINTENANCE_SERVICE"]: + DEFINES["MOZ_MAINTENANCE_SERVICE"] = CONFIG["MOZ_MAINTENANCE_SERVICE"] + +if CONFIG["DISABLE_UPDATER_AUTHENTICODE_CHECK"]: + DEFINES["DISABLE_UPDATER_AUTHENTICODE_CHECK"] = True + +if CONFIG["CC_TYPE"] == "clang-cl": + WIN32_EXE_LDFLAGS += ["-ENTRY:wmainCRTStartup"] + +if CONFIG["OS_ARCH"] == "WINNT": + DEFINES["UNICODE"] = True + DEFINES["_UNICODE"] = True + USE_STATIC_LIBS = True + if CONFIG["CC_TYPE"] in ("clang", "gcc"): + WIN32_EXE_LDFLAGS += ["-municode"] + +TEST_HARNESS_FILES.testing.mochitest.browser.toolkit.mozapps.update.tests.browser += [ + "data/simple.mar", +] + +FINAL_TARGET_FILES += [ + "data/complete.exe", + "data/complete.mar", + "data/complete.png", + "data/complete_log_success_mac", + "data/complete_log_success_win", + "data/complete_mac.mar", + "data/complete_precomplete", + "data/complete_precomplete_mac", + "data/complete_removed-files", + "data/complete_removed-files_mac", + "data/complete_update_manifest", + "data/old_version.mar", + "data/partial.exe", + "data/partial.mar", + "data/partial.png", + "data/partial_log_failure_mac", + "data/partial_log_failure_win", + "data/partial_log_success_mac", + "data/partial_log_success_win", + "data/partial_mac.mar", + "data/partial_precomplete", + "data/partial_precomplete_mac", + "data/partial_removed-files", + "data/partial_removed-files_mac", + "data/partial_update_manifest", + "data/replace_log_success", + "data/simple.mar", + "data/syncManagerTestChild.js", + "TestAUSReadStrings1.ini", + "TestAUSReadStrings2.ini", + "TestAUSReadStrings3.ini", + "TestAUSReadStrings4.ini", +] + +FINAL_TARGET_PP_FILES += [ + "data/xpcshellConstantsPP.js", +] + +with Files("browser/browser_telemetry_updatePing_*_ready.js"): + BUG_COMPONENT = ("Toolkit", "Telemetry") diff --git a/toolkit/mozapps/update/tests/unit_aus_update/ausReadStrings.js b/toolkit/mozapps/update/tests/unit_aus_update/ausReadStrings.js new file mode 100644 index 0000000000..ed331f5ebc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/ausReadStrings.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const BIN_DIR = + AppConstants.platform == "win" ? "test_bug473417-ó" : "test_bug473417"; +const BIN_EXE = "TestAUSReadStrings" + mozinfo.bin_suffix; +const tempdir = do_get_tempdir(); + +function run_test() { + let workdir = tempdir.clone(); + workdir.append(BIN_DIR); + + let paths = [ + BIN_EXE, + "TestAUSReadStrings1.ini", + "TestAUSReadStrings2.ini", + "TestAUSReadStrings3.ini", + "TestAUSReadStrings4.ini", + ]; + for (let i = 0; i < paths.length; i++) { + let file = do_get_file("../data/" + paths[i]); + file.copyTo(workdir, null); + } + + let readStrings = workdir.clone(); + readStrings.append(BIN_EXE); + + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(readStrings); + process.run(true, [], 0); + Assert.equal(process.exitValue, 0); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/backgroundUpdateTaskInternalUpdater.js b/toolkit/mozapps/update/tests/unit_aus_update/backgroundUpdateTaskInternalUpdater.js new file mode 100644 index 0000000000..329b8edbea --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/backgroundUpdateTaskInternalUpdater.js @@ -0,0 +1,85 @@ +/* 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"; + +/** + * This test ensures that we don't resume an update download with the internal + * downloader when we are running background updates. Normally, the background + * update task won't even run if we can't use BITS. But it is possible for us to + * fall back from BITS to the internal downloader. Background update should + * prevent this fallback and just abort. + * + * But interactive Firefox allows that fallback. And once the internal + * download has started, the background update task must leave that download + * untouched and allow it to finish. + */ + +var TEST_MAR_CONTENTS = "Arbitrary MAR contents"; + +add_task(async function setup() { + setupTestCommon(); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + setUpdateChannel("test_channel"); + + // Pretend that this is a background task. + const bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + bts.overrideBackgroundTaskNameForTesting("test-task"); + + // No need for cleanup needed for changing update files. These will be cleaned + // up by removeUpdateFiles. + const downloadingMarFile = getUpdateDirFile(FILE_UPDATE_MAR, DIR_DOWNLOADING); + await IOUtils.writeUTF8(downloadingMarFile.path, TEST_MAR_CONTENTS); + + writeStatusFile(STATE_DOWNLOADING); + + let patchProps = { + state: STATE_DOWNLOADING, + bitsResult: Cr.NS_ERROR_FAILURE, + }; + let patches = getLocalPatchString(patchProps); + let updateProps = { appVersion: "1.0" }; + let updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); +}); + +add_task(async function backgroundUpdate() { + let patches = getRemotePatchString({}); + let updateString = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updateString); + + let { updates } = await waitForUpdateCheck(true); + let bestUpdate = gAUS.selectUpdate(updates); + let success = await gAUS.downloadUpdate(bestUpdate, false); + Assert.equal( + success, + false, + "We should not attempt to download an update in the background when an " + + "internal update download is already in progress." + ); + Assert.equal( + readStatusFile(), + STATE_DOWNLOADING, + "Background update during an internally downloading update should not " + + "change update status" + ); + const downloadingMarFile = getUpdateDirFile(FILE_UPDATE_MAR, DIR_DOWNLOADING); + Assert.ok( + await IOUtils.exists(downloadingMarFile.path), + "Downloading MAR should still exist" + ); + Assert.equal( + await IOUtils.readUTF8(downloadingMarFile.path), + TEST_MAR_CONTENTS, + "Downloading MAR should not have been modified" + ); +}); + +add_task(async function finish() { + stop_httpserver(doTestFinish); +}); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js b/toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js new file mode 100644 index 0000000000..b3dcb72d14 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js @@ -0,0 +1,62 @@ +/* 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/. + */ + +function run_test() { + setupTestCommon(); + + // Verify write access to the custom app dir + debugDump("testing write access to the application directory"); + let testFile = getCurrentProcessDir(); + testFile.append("update_write_access_test"); + testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + Assert.ok(testFile.exists(), MSG_SHOULD_EXIST); + testFile.remove(false); + Assert.ok(!testFile.exists(), MSG_SHOULD_NOT_EXIST); + + if (AppConstants.platform == "win") { + // Create a mutex to prevent being able to check for or apply updates. + debugDump("attempting to create mutex"); + let handle = createMutex(getPerInstallationMutexName()); + Assert.ok(!!handle, "the update mutex should have been created"); + + // Check if available updates cannot be checked for when there is a mutex + // for this installation. + Assert.ok( + !gAUS.canCheckForUpdates, + "should not be able to check for " + + "updates when there is an update mutex" + ); + + // Check if updates cannot be applied when there is a mutex for this + // installation. + Assert.ok( + !gAUS.canApplyUpdates, + "should not be able to apply updates when there is an update mutex" + ); + + debugDump("destroying mutex"); + closeHandle(handle); + } + + // Check if available updates can be checked for + Assert.ok(gAUS.canCheckForUpdates, "should be able to check for updates"); + // Check if updates can be applied + Assert.ok(gAUS.canApplyUpdates, "should be able to apply updates"); + + if (AppConstants.platform == "win") { + // Attempt to create a mutex when application update has already created one + // with the same name. + debugDump("attempting to create mutex"); + let handle = createMutex(getPerInstallationMutexName()); + + Assert.ok( + !handle, + "should not be able to create the update mutex when " + + "the application has created the update mutex" + ); + } + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js new file mode 100644 index 0000000000..33df63e7e7 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js @@ -0,0 +1,60 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + + debugDump( + "testing removal of an active update for a channel that is not " + + "valid due to switching channels (Bug 486275)." + ); + + let patchProps = { state: STATE_DOWNLOADING }; + let patches = getLocalPatchString(patchProps); + let updateProps = { appVersion: "1.0" }; + let updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_DOWNLOADING); + + setUpdateChannel("original_channel"); + + standardInit(); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "there should not be a downloading update" + ); + Assert.ok(!gUpdateManager.readyUpdate, "there should not be a ready update"); + Assert.equal( + gUpdateManager.getUpdateCount(), + 1, + "the update manager update count" + MSG_SHOULD_EQUAL + ); + let update = gUpdateManager.getUpdateAt(0); + Assert.equal( + update.state, + STATE_FAILED, + "the first update state" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.errorCode, + ERR_CHANNEL_CHANGE, + "the first update errorCode" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.statusText, + getString("statusFailed"), + "the first update statusText " + MSG_SHOULD_EQUAL + ); + await waitForUpdateXMLFiles(); + + let dir = getUpdateDirFile(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + let statusFile = getUpdateDirFile(FILE_UPDATE_STATUS); + Assert.ok(!statusFile.exists(), MSG_SHOULD_NOT_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js new file mode 100644 index 0000000000..3c9f7d1c2e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js @@ -0,0 +1,58 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + + debugDump( + "testing cleanup of an update download in progress for an " + + "older version of the application on startup (Bug 485624)" + ); + + let patchProps = { state: STATE_DOWNLOADING }; + let patches = getLocalPatchString(patchProps); + let updateProps = { appVersion: "0.9" }; + let updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_DOWNLOADING); + + standardInit(); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "there should not be a downloading update" + ); + Assert.ok(!gUpdateManager.readyUpdate, "there should not be a ready update"); + Assert.equal( + gUpdateManager.getUpdateCount(), + 1, + "the update manager update count" + MSG_SHOULD_EQUAL + ); + let update = gUpdateManager.getUpdateAt(0); + Assert.equal( + update.state, + STATE_FAILED, + "the first update state" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.errorCode, + ERR_OLDER_VERSION_OR_SAME_BUILD, + "the first update errorCode" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.statusText, + getString("statusFailed"), + "the first update statusText " + MSG_SHOULD_EQUAL + ); + await waitForUpdateXMLFiles(); + + let dir = getUpdateDirFile(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + let statusFile = getUpdateDirFile(FILE_UPDATE_STATUS); + Assert.ok(!statusFile.exists(), MSG_SHOULD_NOT_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js new file mode 100644 index 0000000000..0eb1b6c22e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js @@ -0,0 +1,59 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + + debugDump( + "testing removal of an update download in progress for the " + + "same version of the application with the same application " + + "build id on startup (Bug 536547)" + ); + + let patchProps = { state: STATE_DOWNLOADING }; + let patches = getLocalPatchString(patchProps); + let updateProps = { appVersion: "1.0", buildID: "2007010101" }; + let updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_DOWNLOADING); + + standardInit(); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "there should not be a downloading update" + ); + Assert.ok(!gUpdateManager.readyUpdate, "there should not be a ready update"); + Assert.equal( + gUpdateManager.getUpdateCount(), + 1, + "the update manager update count" + MSG_SHOULD_EQUAL + ); + let update = gUpdateManager.getUpdateAt(0); + Assert.equal( + update.state, + STATE_FAILED, + "the first update state" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.errorCode, + ERR_OLDER_VERSION_OR_SAME_BUILD, + "the first update errorCode" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.statusText, + getString("statusFailed"), + "the first update statusText " + MSG_SHOULD_EQUAL + ); + await waitForUpdateXMLFiles(); + + let dir = getUpdateDirFile(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + let statusFile = getUpdateDirFile(FILE_UPDATE_STATUS); + Assert.ok(!statusFile.exists(), MSG_SHOULD_NOT_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js new file mode 100644 index 0000000000..8468ca453c --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function run_test() { + setupTestCommon(); + + debugDump( + "testing update cleanup when reading the status file returns " + + "STATUS_NONE and the update xml has an update with " + + "STATE_DOWNLOADING (Bug 539717)." + ); + + let patchProps = { state: STATE_DOWNLOADING }; + let patches = getLocalPatchString(patchProps); + let updates = getLocalUpdateString({}, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_NONE); + + standardInit(); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "there should not be a downloading update" + ); + Assert.ok(!gUpdateManager.readyUpdate, "there should not be a ready update"); + Assert.equal( + gUpdateManager.getUpdateCount(), + 1, + "the update manager update count" + MSG_SHOULD_EQUAL + ); + let update = gUpdateManager.getUpdateAt(0); + Assert.equal( + update.state, + STATE_FAILED, + "the first update state" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.errorCode, + ERR_UPDATE_STATE_NONE, + "the first update errorCode" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.statusText, + getString("statusFailed"), + "the first update statusText " + MSG_SHOULD_EQUAL + ); + await waitForUpdateXMLFiles(); + + let dir = getUpdateDirFile(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + let statusFile = getUpdateDirFile(FILE_UPDATE_STATUS); + Assert.ok(!statusFile.exists(), MSG_SHOULD_NOT_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js new file mode 100644 index 0000000000..274f029150 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function run_test() { + setupTestCommon(); + + debugDump( + "testing update cleanup when reading the status file returns " + + "STATUS_NONE, the version file is for a newer version, and the " + + "update xml has an update with STATE_PENDING (Bug 601701)." + ); + + let patchProps = { state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + let updates = getLocalUpdateString({}, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeVersionFile("99.9"); + + // Check that there are no active updates first so the updates directory is + // cleaned up by the UpdateManager before the remaining tests. + Assert.ok( + !gUpdateManager.downloadingUpdate, + "there should not be a downloading update" + ); + Assert.ok(!gUpdateManager.readyUpdate, "there should not be a ready update"); + Assert.equal( + gUpdateManager.getUpdateCount(), + 1, + "the update manager update count" + MSG_SHOULD_EQUAL + ); + let update = gUpdateManager.getUpdateAt(0); + Assert.equal( + update.state, + STATE_FAILED, + "the first update state" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.errorCode, + ERR_UPDATE_STATE_NONE, + "the first update errorCode" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.statusText, + getString("statusFailed"), + "the first update statusText " + MSG_SHOULD_EQUAL + ); + await waitForUpdateXMLFiles(); + + let dir = getUpdateDirFile(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + let versionFile = getUpdateDirFile(FILE_UPDATE_VERSION); + Assert.ok(!versionFile.exists(), MSG_SHOULD_NOT_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js new file mode 100644 index 0000000000..3a36693dca --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js @@ -0,0 +1,61 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + + debugDump("testing that the update.log is moved after a successful update"); + + Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, 5); + + let patchProps = { state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + let updates = getLocalUpdateString({}, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_SUCCEEDED); + + let log = getUpdateDirFile(FILE_UPDATE_LOG); + writeFile(log, "Last Update Log"); + + standardInit(); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "there should not be a downloading update" + ); + Assert.ok(!gUpdateManager.readyUpdate, "there should not be a ready update"); + Assert.equal( + gUpdateManager.getUpdateCount(), + 1, + "the update manager update count" + MSG_SHOULD_EQUAL + ); + await waitForUpdateXMLFiles(); + + let cancelations = Services.prefs.getIntPref(PREF_APP_UPDATE_CANCELATIONS, 0); + Assert.equal( + cancelations, + 0, + "the " + PREF_APP_UPDATE_CANCELATIONS + " preference " + MSG_SHOULD_EQUAL + ); + + log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST); + Assert.equal( + readFile(log), + "Last Update Log", + "the last update log contents" + MSG_SHOULD_EQUAL + ); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST); + + let dir = getUpdateDirFile(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js new file mode 100644 index 0000000000..9caceccc99 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js @@ -0,0 +1,63 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + + debugDump("testing update logs are first in first out deleted"); + + let patchProps = { state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + let updates = getLocalUpdateString({}, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_SUCCEEDED); + + let log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + writeFile(log, "Backup Update Log"); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + writeFile(log, "To Be Deleted Backup Update Log"); + + log = getUpdateDirFile(FILE_UPDATE_LOG); + writeFile(log, "Last Update Log"); + + standardInit(); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "there should not be a downloading update" + ); + Assert.ok(!gUpdateManager.readyUpdate, "there should not be a ready update"); + Assert.equal( + gUpdateManager.getUpdateCount(), + 1, + "the update manager update count" + MSG_SHOULD_EQUAL + ); + await waitForUpdateXMLFiles(); + + log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST); + Assert.equal( + readFile(log), + "Last Update Log", + "the last update log contents" + MSG_SHOULD_EQUAL + ); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST); + Assert.equal( + readFile(log), + "Backup Update Log", + "the backup update log contents" + MSG_SHOULD_EQUAL + ); + + let dir = getUpdateDirFile(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/disableBackgroundUpdatesBackgroundTask.js b/toolkit/mozapps/update/tests/unit_aus_update/disableBackgroundUpdatesBackgroundTask.js new file mode 100644 index 0000000000..4dcf559563 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/disableBackgroundUpdatesBackgroundTask.js @@ -0,0 +1,48 @@ +/* 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"; + +/** + * This test verifies that when Balrog advertises that an update should not + * be downloaded in the background, it is not. + */ + +function setup() { + setupTestCommon(); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + setUpdateChannel("test_channel"); + + // Pretend that this is a background task. + const bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + bts.overrideBackgroundTaskNameForTesting("test-task"); +} +setup(); + +add_task(async function disableBackgroundUpdatesBackgroundTask() { + let patches = getRemotePatchString({}); + let updateString = getRemoteUpdateString( + { disableBackgroundUpdates: "true" }, + patches + ); + gResponseBody = getRemoteUpdatesXMLString(updateString); + + let { updates } = await waitForUpdateCheck(true); + let bestUpdate = gAUS.selectUpdate(updates); + let success = await gAUS.downloadUpdate(bestUpdate, false); + Assert.equal( + success, + false, + "Update should not download when disableBackgroundUpdates is specified " + + "and we are in background task mode." + ); +}); + +add_task(async function finish() { + stop_httpserver(doTestFinish); +}); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/disableBackgroundUpdatesNonBackgroundTask.js b/toolkit/mozapps/update/tests/unit_aus_update/disableBackgroundUpdatesNonBackgroundTask.js new file mode 100644 index 0000000000..2f4eec25ff --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/disableBackgroundUpdatesNonBackgroundTask.js @@ -0,0 +1,41 @@ +/* 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"; + +/** + * This test verifies that when Balrog advertises that an update should not + * be downloaded in the background, but we are not running in the background, + * the advertisement does not have any effect. + */ + +function setup() { + setupTestCommon(); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + setUpdateChannel("test_channel"); +} +setup(); + +add_task(async function disableBackgroundUpdatesBackgroundTask() { + let patches = getRemotePatchString({}); + let updateString = getRemoteUpdateString( + { disableBackgroundUpdates: "true" }, + patches + ); + gResponseBody = getRemoteUpdatesXMLString(updateString); + + let { updates } = await waitForUpdateCheck(true); + + initMockIncrementalDownload(); + gIncrementalDownloadErrorType = 3; + + // This will assert that the download completes successfully. + await waitForUpdateDownload(updates, Cr.NS_OK); +}); + +add_task(async function finish() { + stop_httpserver(doTestFinish); +}); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedNoRecovery.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedNoRecovery.js new file mode 100644 index 0000000000..15dd39ce0a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedNoRecovery.js @@ -0,0 +1,23 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + debugDump("testing mar download with interrupted recovery count exceeded"); + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + initMockIncrementalDownload(); + gIncrementalDownloadErrorType = 0; + Services.prefs.setIntPref(PREF_APP_UPDATE_SOCKET_MAXERRORS, 2); + Services.prefs.setIntPref(PREF_APP_UPDATE_RETRYTIMEOUT, 0); + let patches = getRemotePatchString({}); + let updates = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 1 }).then(async aArgs => { + await waitForUpdateDownload(aArgs.updates, Cr.NS_ERROR_NET_RESET); + }); + stop_httpserver(doTestFinish); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedOffline.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedOffline.js new file mode 100644 index 0000000000..2c54e058d2 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedOffline.js @@ -0,0 +1,21 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + debugDump("testing mar download when offline"); + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + initMockIncrementalDownload(); + gIncrementalDownloadErrorType = 4; + let patches = getRemotePatchString({}); + let updates = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 1 }).then(async aArgs => { + await waitForUpdateDownload(aArgs.updates, Cr.NS_OK); + }); + stop_httpserver(doTestFinish); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js new file mode 100644 index 0000000000..1ed40f9a2a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js @@ -0,0 +1,26 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + debugDump("testing mar mar download interrupted recovery"); + // This test assumes speculative connections enabled. + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + }); + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + initMockIncrementalDownload(); + gIncrementalDownloadErrorType = 0; + let patches = getRemotePatchString({}); + let updates = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 1 }).then(async aArgs => { + await waitForUpdateDownload(aArgs.updates, Cr.NS_OK); + }); + stop_httpserver(doTestFinish); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js new file mode 100644 index 0000000000..8a386513f3 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js @@ -0,0 +1,38 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + + debugDump( + "testing resuming an update download in progress for the same " + + "version of the application on startup (Bug 485624)" + ); + + let patchProps = { state: STATE_DOWNLOADING }; + let patches = getLocalPatchString(patchProps); + let updateProps = { appVersion: "1.0" }; + let updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_DOWNLOADING); + + standardInit(); + + Assert.equal( + gUpdateManager.getUpdateCount(), + 0, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + gUpdateManager.downloadingUpdate.state, + STATE_DOWNLOADING, + "the update manager activeUpdate state attribute" + MSG_SHOULD_EQUAL + ); + + // Cancel the download early to prevent it writing the update xml files during + // shutdown. + await gAUS.stopDownload(); + executeSoon(doTestFinish); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/ensureExperimentToRolloutTransitionPerformed.js b/toolkit/mozapps/update/tests/unit_aus_update/ensureExperimentToRolloutTransitionPerformed.js new file mode 100644 index 0000000000..c91add9f3f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/ensureExperimentToRolloutTransitionPerformed.js @@ -0,0 +1,112 @@ +/* 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/. + */ + +XPCOMUtils.defineLazyModuleGetters(this, { + BackgroundUpdate: "resource://gre/modules/BackgroundUpdate.jsm", +}); + +const transitionPerformedPref = "app.update.background.rolledout"; +const backgroundUpdateEnabledPref = "app.update.background.enabled"; +const defaultPrefValue = + UpdateUtils.PER_INSTALLATION_PREFS[backgroundUpdateEnabledPref].defaultValue; + +async function testTransition(options) { + Services.prefs.clearUserPref(transitionPerformedPref); + await UpdateUtils.writeUpdateConfigSetting( + backgroundUpdateEnabledPref, + options.initialDefaultValue, + { setDefaultOnly: true } + ); + await UpdateUtils.writeUpdateConfigSetting( + backgroundUpdateEnabledPref, + options.initialUserValue + ); + BackgroundUpdate.ensureExperimentToRolloutTransitionPerformed(); + Assert.equal( + await UpdateUtils.readUpdateConfigSetting(backgroundUpdateEnabledPref), + options.expectedPostTransitionValue, + "Post transition option value does not match the expected value" + ); + + // Make sure that we only do the transition once. + + // If we change the default value, then change the user value to the same + // thing, we will end up with only a default value and no saved user value. + // This allows us to ensure that we read the default value back out, if it is + // changed. + await UpdateUtils.writeUpdateConfigSetting( + backgroundUpdateEnabledPref, + !defaultPrefValue, + { setDefaultOnly: true } + ); + await UpdateUtils.writeUpdateConfigSetting( + backgroundUpdateEnabledPref, + !defaultPrefValue + ); + BackgroundUpdate.ensureExperimentToRolloutTransitionPerformed(); + Assert.equal( + await UpdateUtils.readUpdateConfigSetting(backgroundUpdateEnabledPref), + !defaultPrefValue, + "Transition should not change the pref value if it already ran" + ); +} + +async function run_test() { + setupTestCommon(null); + standardInit(); + // The setup functions we use for update testing typically allow for update. + // But we are just testing preferences here. We don't want anything to + // actually attempt to update. Also, because we are messing with the pref + // system itself in this test, we want to make sure to use a pref outside of + // that system to disable update. + Services.prefs.setBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, true); + + const originalBackgroundUpdateEnabled = await UpdateUtils.readUpdateConfigSetting( + backgroundUpdateEnabledPref + ); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref(transitionPerformedPref); + await UpdateUtils.writeUpdateConfigSetting( + backgroundUpdateEnabledPref, + originalBackgroundUpdateEnabled + ); + await UpdateUtils.writeUpdateConfigSetting( + backgroundUpdateEnabledPref, + defaultPrefValue, + { setDefaultOnly: true } + ); + }); + + await testTransition({ + initialDefaultValue: true, + initialUserValue: true, + expectedPostTransitionValue: true, + }); + + // Make sure we don't interfere with a user's choice to turn the feature off. + await testTransition({ + initialDefaultValue: true, + initialUserValue: false, + expectedPostTransitionValue: false, + }); + + // In this case, there effectively is no user value since the user value + // equals the default value. So the effective value should change after + // the transition switches the default. + await testTransition({ + initialDefaultValue: false, + initialUserValue: false, + expectedPostTransitionValue: true, + }); + + await testTransition({ + initialDefaultValue: false, + initialUserValue: true, + expectedPostTransitionValue: true, + }); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/head_update.js b/toolkit/mozapps/update/tests/unit_aus_update/head_update.js new file mode 100644 index 0000000000..3cfde06015 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/head_update.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const IS_SERVICE_TEST = false; + +/* import-globals-from ../data/xpcshellUtilsAUS.js */ +load("xpcshellUtilsAUS.js"); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/languagePackUpdates.js b/toolkit/mozapps/update/tests/unit_aus_update/languagePackUpdates.js new file mode 100644 index 0000000000..7dc6253c82 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/languagePackUpdates.js @@ -0,0 +1,291 @@ +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); +const { getAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +const { XPIInstall } = ChromeUtils.import( + "resource://gre/modules/addons/XPIInstall.jsm" +); +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +AddonTestUtils.init(this); +setupTestCommon(); +AddonTestUtils.appInfo = getAppInfo(); +start_httpserver(); +setUpdateURL(gURLData + gHTTPHandlerPath); +setUpdateChannel("test_channel"); +Services.prefs.setBoolPref(PREF_APP_UPDATE_LANGPACK_ENABLED, true); + +/** + * Checks for updates and waits for the update to download. + */ +async function downloadUpdate() { + let patches = getRemotePatchString({}); + let updateString = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updateString); + + let { updates } = await waitForUpdateCheck(true); + + initMockIncrementalDownload(); + gIncrementalDownloadErrorType = 3; + + await waitForUpdateDownload(updates, Cr.NS_OK); +} + +/** + * Returns a promise that will resolve when the add-ons manager attempts to + * stage langpack updates. The returned object contains the appVersion and + * platformVersion parameters as well as resolve and reject functions to + * complete the mocked langpack update. + */ +function mockLangpackUpdate() { + let stagingCall = PromiseUtils.defer(); + XPIInstall.stageLangpacksForAppUpdate = (appVersion, platformVersion) => { + let result = PromiseUtils.defer(); + stagingCall.resolve({ + appVersion, + platformVersion, + resolve: result.resolve, + reject: result.reject, + }); + + return result.promise; + }; + + return stagingCall.promise; +} + +add_setup(async function() { + // Thunderbird doesn't have one or more of the probes used in this test. + // Ensure the data is collected anyway. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function testLangpackUpdateSuccess() { + let histogram = TelemetryTestUtils.getAndClearHistogram( + "UPDATE_LANGPACK_OVERTIME" + ); + + let updateDownloadNotified = false; + let notified = waitForEvent("update-downloaded").then( + () => (updateDownloadNotified = true) + ); + + let stagingCall = mockLangpackUpdate(); + + await downloadUpdate(); + + // We have to wait for UpdateService's onStopRequest to run far enough that + // the notification will have been sent if the language pack update completed. + await TestUtils.waitForCondition(() => readStatusFile() == "pending"); + + Assert.ok( + !updateDownloadNotified, + "Should not have seen the notification yet." + ); + + let { appVersion, platformVersion, resolve } = await stagingCall; + Assert.equal( + appVersion, + DEFAULT_UPDATE_VERSION, + "Should see the right app version" + ); + Assert.equal( + platformVersion, + DEFAULT_UPDATE_VERSION, + "Should see the right platform version" + ); + + resolve(); + + await notified; + + // Because we resolved the lang pack call after the download completed a value + // should have been recorded in telemetry. + let snapshot = histogram.snapshot(); + Assert.ok( + !Object.values(snapshot.values).every(val => val == 0), + "Should have recorded a time" + ); + + // Reload the update manager so that we can download the same update again + reloadUpdateManagerData(true); +}); + +add_task(async function testLangpackUpdateFails() { + let updateDownloadNotified = false; + let notified = waitForEvent("update-downloaded").then( + () => (updateDownloadNotified = true) + ); + + let stagingCall = mockLangpackUpdate(); + + await downloadUpdate(); + + // We have to wait for UpdateService's onStopRequest to run far enough that + // the notification will have been sent if the language pack update completed. + await TestUtils.waitForCondition(() => readStatusFile() == "pending"); + + Assert.ok( + !updateDownloadNotified, + "Should not have seen the notification yet." + ); + + let { appVersion, platformVersion, reject } = await stagingCall; + Assert.equal( + appVersion, + DEFAULT_UPDATE_VERSION, + "Should see the right app version" + ); + Assert.equal( + platformVersion, + DEFAULT_UPDATE_VERSION, + "Should see the right platform version" + ); + + reject(); + + await notified; + + // Reload the update manager so that we can download the same update again + reloadUpdateManagerData(true); +}); + +add_task(async function testLangpackStaged() { + let updateStagedNotified = false; + let notified = waitForEvent("update-staged").then( + () => (updateStagedNotified = true) + ); + + let stagingCall = mockLangpackUpdate(); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true); + copyTestUpdaterToBinDir(); + + let greDir = getGREDir(); + let updateSettingsIni = greDir.clone(); + updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI); + writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS); + + await downloadUpdate(); + + // We have to wait for the update to be applied and then check that the + // notification hasn't been sent. + await TestUtils.waitForCondition(() => readStatusFile() == "applied"); + + Assert.ok( + !updateStagedNotified, + "Should not have seen the notification yet." + ); + + let { appVersion, platformVersion, resolve } = await stagingCall; + Assert.equal( + appVersion, + DEFAULT_UPDATE_VERSION, + "Should see the right app version" + ); + Assert.equal( + platformVersion, + DEFAULT_UPDATE_VERSION, + "Should see the right platform version" + ); + + resolve(); + + await notified; + + // Reload the update manager so that we can download the same update again + reloadUpdateManagerData(true); +}); + +add_task(async function testRedownload() { + // When the download of a partial mar fails the same downloader is re-used to + // download the complete mar. We should only call the add-ons manager to stage + // language packs once in this case. + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false); + let histogram = TelemetryTestUtils.getAndClearHistogram( + "UPDATE_LANGPACK_OVERTIME" + ); + + let partialPatch = getRemotePatchString({ + type: "partial", + url: gURLData + "missing.mar", + size: 28, + }); + let completePatch = getRemotePatchString({}); + let updateString = getRemoteUpdateString({}, partialPatch + completePatch); + gResponseBody = getRemoteUpdatesXMLString(updateString); + + let { updates } = await waitForUpdateCheck(true); + + initMockIncrementalDownload(); + gIncrementalDownloadErrorType = 3; + + let stageCount = 0; + XPIInstall.stageLangpacksForAppUpdate = () => { + stageCount++; + return Promise.resolve(); + }; + + let downloadCount = 0; + let listener = { + onStartRequest: aRequest => {}, + onProgress: (aRequest, aContext, aProgress, aMaxProgress) => {}, + onStatus: (aRequest, aStatus, aStatusText) => {}, + onStopRequest: (request, status) => { + Assert.equal( + status, + downloadCount ? 0 : Cr.NS_ERROR_CORRUPTED_CONTENT, + "Should have seen the right status." + ); + downloadCount++; + + // Keep the same status. + gIncrementalDownloadErrorType = 3; + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsIProgressEventSink", + ]), + }; + gAUS.addDownloadListener(listener); + + let bestUpdate = gAUS.selectUpdate(updates); + await gAUS.downloadUpdate(bestUpdate, false); + + await waitForEvent("update-downloaded"); + + gAUS.removeDownloadListener(listener); + + Assert.equal(downloadCount, 2, "Should have seen two downloads"); + Assert.equal(stageCount, 1, "Should have only tried to stage langpacks once"); + + // Because we resolved the lang pack call before the download completed a value + // should not have been recorded in telemetry. + let snapshot = histogram.snapshot(); + Assert.ok( + Object.values(snapshot.values).every(val => val == 0), + "Should have recorded a time" + ); + + // Reload the update manager so that we can download the same update again + reloadUpdateManagerData(true); +}); + +add_task(async function finish() { + stop_httpserver(doTestFinish); +}); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/multiUpdate.js b/toolkit/mozapps/update/tests/unit_aus_update/multiUpdate.js new file mode 100644 index 0000000000..73a262e527 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/multiUpdate.js @@ -0,0 +1,396 @@ +/* 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/. + */ + +/** + * This tests the multiple update downloads per Firefox session feature. + * + * This test does some unusual things, compared to the other files in this + * directory. We want to start the updates with aus.checkForBackgroundUpdates() + * to ensure that we test the whole flow. Other tests start update with things + * like aus.downloadUpdate(), but that bypasses some of the exact checks that we + * are trying to test as part of the multiupdate flow. + * + * In order to accomplish all this, we will be using app_update.sjs to serve + * updates XMLs and MARs. Outside of this test, this is really only done + * by browser-chrome mochitests (in ../browser). So we have to do some weird + * things to make it work properly in an xpcshell test. Things like + * defining URL_HTTP_UPDATE_SJS in testConstants.js so that it can be read by + * app_update.sjs in order to provide the correct download URL for MARs, but + * not reading that file here, because URL_HTTP_UPDATE_SJS is already defined + * (as something else) in xpcshellUtilsAUS.js. + */ + +let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// These are from testConstants.js, which cannot be loaded by this file, because +// some values are already defined at this point. However, we need these some +// other values to be defined because continueFileHandler in shared.js expects +// them to be. +const REL_PATH_DATA = ""; +// This should be URL_HOST, but that conflicts with an existing constant. +const APP_UPDATE_SJS_HOST = "http://127.0.0.1:8888"; +const URL_PATH_UPDATE_XML = "/" + REL_PATH_DATA + "app_update.sjs"; +// This should be URL_HTTP_UPDATE_SJS, but that conflicts with an existing +// constant. +const APP_UPDATE_SJS_URL = APP_UPDATE_SJS_HOST + URL_PATH_UPDATE_XML; +const CONTINUE_CHECK = "continueCheck"; +const CONTINUE_DOWNLOAD = "continueDownload"; +const CONTINUE_STAGING = "continueStaging"; + +const FIRST_UPDATE_VERSION = "999998.0"; +const SECOND_UPDATE_VERSION = "999999.0"; + +/** + * Downloads an update via aus.checkForBackgroundUpdates() + * Function returns only after the update has been downloaded. + * + * The provided callback will be invoked once during the update download, + * specifically when onStartRequest is fired. + * + * If automatic update downloads are turned off (appUpdateAuto is false), then + * we listen for the update-available notification and then roughly simulate + * accepting the prompt by calling: + * AppUpdateService.downloadUpdate(update, true); + * This is what is normally called when the user accepts the update-available + * prompt. + */ +async function downloadUpdate(appUpdateAuto, onDownloadStartCallback) { + let downloadFinishedPromise = waitForEvent("update-downloaded"); + let updateAvailablePromise; + if (!appUpdateAuto) { + updateAvailablePromise = new Promise(resolve => { + let observer = (subject, topic, status) => { + Services.obs.removeObserver(observer, "update-available"); + subject.QueryInterface(Ci.nsIUpdate); + resolve({ update: subject, status }); + }; + Services.obs.addObserver(observer, "update-available"); + }); + } + let waitToStartPromise = new Promise(resolve => { + let listener = { + onStartRequest: aRequest => { + gAUS.removeDownloadListener(listener); + onDownloadStartCallback(); + resolve(); + }, + onProgress: (aRequest, aContext, aProgress, aMaxProgress) => {}, + onStatus: (aRequest, aStatus, aStatusText) => {}, + onStopRequest(request, status) {}, + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsIProgressEventSink", + ]), + }; + gAUS.addDownloadListener(listener); + }); + + let updateCheckStarted = gAUS.checkForBackgroundUpdates(); + Assert.ok(updateCheckStarted, "Update check should have started"); + + if (!appUpdateAuto) { + let { update, status } = await updateAvailablePromise; + Assert.equal( + status, + "show-prompt", + "Should attempt to show the update-available prompt" + ); + // Simulate accepting the update-available prompt + await gAUS.downloadUpdate(update, true); + } + + await continueFileHandler(CONTINUE_DOWNLOAD); + await waitToStartPromise; + await downloadFinishedPromise; + // Wait an extra tick after the download has finished. If we try to check for + // another update exactly when "update-downloaded" fires, + // Downloader:onStopRequest won't have finished yet, which it normally would + // have. + await TestUtils.waitForTick(); +} + +/** + * This is like downloadUpdate. The difference is that downloadUpdate assumes + * that an update actually will be downloaded. This function instead verifies + * that we the update wasn't downloaded. + * + * downloadUpdate(), above, uses aus.checkForBackgroundUpdates() to download + * updates to verify that background updates actually will check for subsequent + * updates, but this function will use some slightly different mechanisms. We + * can call aus.downloadUpdate() and check its return value to see if it started + * a download. But this doesn't properly check that the update-available + * notification isn't shown. So we will do an additional check where we follow + * the normal flow a bit more closely by forwarding the results that we got from + * checkForUpdates() to aus.onCheckComplete() and make sure that the update + * prompt isn't shown. + */ +async function testUpdateDoesNotDownload() { + let check = gUpdateChecker.checkForUpdates(gUpdateChecker.BACKGROUND_CHECK); + let result = await check.result; + Assert.ok(result.checksAllowed, "Should be able to check for updates"); + Assert.ok(result.succeeded, "Update check should have succeeded"); + + Assert.equal( + result.updates.length, + 1, + "Should have gotten 1 update in update check" + ); + let update = result.updates[0]; + + let downloadStarted = await gAUS.downloadUpdate(update, true); + Assert.equal( + downloadStarted, + false, + "Expected that we would not start downloading an update" + ); + + let updateAvailableObserved = false; + let observer = (subject, topic, status) => { + updateAvailableObserved = true; + }; + Services.obs.addObserver(observer, "update-available"); + await gAUS.onCheckComplete(result); + Services.obs.removeObserver(observer, "update-available"); + Assert.equal( + updateAvailableObserved, + false, + "update-available notification should not fire if we aren't going to " + + "download the update." + ); +} + +function testUpdateCheckDoesNotStart() { + let updateCheckStarted = gAUS.checkForBackgroundUpdates(); + Assert.equal( + updateCheckStarted, + false, + "Update check should not have started" + ); +} + +function prepareToDownloadVersion(version, onlyCompleteMar = false) { + let updateUrl = `${APP_UPDATE_SJS_URL}?useSlowDownloadMar=1&appVersion=${version}`; + if (onlyCompleteMar) { + updateUrl += "&completePatchOnly=1"; + } + setUpdateURL(updateUrl); +} + +function startUpdateServer() { + let httpServer = new HttpServer(); + httpServer.registerContentType("sjs", "sjs"); + httpServer.registerDirectory("/", do_get_cwd()); + httpServer.start(8888); + registerCleanupFunction(async function cleanup_httpServer() { + await new Promise(resolve => { + httpServer.stop(resolve); + }); + }); +} + +async function multi_update_test(appUpdateAuto) { + await UpdateUtils.setAppUpdateAutoEnabled(appUpdateAuto); + + prepareToDownloadVersion(FIRST_UPDATE_VERSION); + + await downloadUpdate(appUpdateAuto, () => { + Assert.ok( + !gUpdateManager.readyUpdate, + "There should not be a ready update yet" + ); + Assert.ok( + !!gUpdateManager.downloadingUpdate, + "First update download should be in downloadingUpdate" + ); + Assert.equal( + gUpdateManager.downloadingUpdate.state, + STATE_DOWNLOADING, + "downloadingUpdate should be downloading" + ); + Assert.equal( + readStatusFile(), + STATE_DOWNLOADING, + "Updater state should be downloading" + ); + }); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "First update download should no longer be in downloadingUpdate" + ); + Assert.ok( + !!gUpdateManager.readyUpdate, + "First update download should be in readyUpdate" + ); + Assert.equal( + gUpdateManager.readyUpdate.state, + STATE_PENDING, + "readyUpdate should be pending" + ); + Assert.equal( + gUpdateManager.readyUpdate.appVersion, + FIRST_UPDATE_VERSION, + "readyUpdate version should be match the version of the first update" + ); + Assert.equal( + readStatusFile(), + STATE_PENDING, + "Updater state should be pending" + ); + + let existingUpdate = gUpdateManager.readyUpdate; + await testUpdateDoesNotDownload(); + + Assert.equal( + gUpdateManager.readyUpdate, + existingUpdate, + "readyUpdate should not have changed when no newer update is available" + ); + Assert.equal( + gUpdateManager.readyUpdate.state, + STATE_PENDING, + "readyUpdate should still be pending" + ); + Assert.equal( + gUpdateManager.readyUpdate.appVersion, + FIRST_UPDATE_VERSION, + "readyUpdate version should be match the version of the first update" + ); + Assert.equal( + readStatusFile(), + STATE_PENDING, + "Updater state should still be pending" + ); + + // With only a complete update available, we should not download the newer + // update when we already have an update ready. + prepareToDownloadVersion(SECOND_UPDATE_VERSION, true); + await testUpdateDoesNotDownload(); + + Assert.equal( + gUpdateManager.readyUpdate, + existingUpdate, + "readyUpdate should not have changed when no newer partial update is available" + ); + Assert.equal( + gUpdateManager.readyUpdate.state, + STATE_PENDING, + "readyUpdate should still be pending" + ); + Assert.equal( + gUpdateManager.readyUpdate.appVersion, + FIRST_UPDATE_VERSION, + "readyUpdate version should be match the version of the first update" + ); + Assert.equal( + readStatusFile(), + STATE_PENDING, + "Updater state should still be pending" + ); + + prepareToDownloadVersion(SECOND_UPDATE_VERSION); + + await downloadUpdate(appUpdateAuto, () => { + Assert.ok( + !!gUpdateManager.downloadingUpdate, + "Second update download should be in downloadingUpdate" + ); + Assert.equal( + gUpdateManager.downloadingUpdate.state, + STATE_DOWNLOADING, + "downloadingUpdate should be downloading" + ); + Assert.ok( + !!gUpdateManager.readyUpdate, + "First update download should still be in readyUpdate" + ); + Assert.equal( + gUpdateManager.readyUpdate.state, + STATE_PENDING, + "readyUpdate should still be pending" + ); + Assert.equal( + gUpdateManager.readyUpdate.appVersion, + FIRST_UPDATE_VERSION, + "readyUpdate version should be match the version of the first update" + ); + Assert.equal( + readStatusFile(), + STATE_PENDING, + "Updater state should match the readyUpdate's state" + ); + }); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "Second update download should no longer be in downloadingUpdate" + ); + Assert.ok( + !!gUpdateManager.readyUpdate, + "Second update download should be in readyUpdate" + ); + Assert.equal( + gUpdateManager.readyUpdate.state, + STATE_PENDING, + "readyUpdate should be pending" + ); + Assert.equal( + gUpdateManager.readyUpdate.appVersion, + SECOND_UPDATE_VERSION, + "readyUpdate version should be match the version of the second update" + ); + Assert.equal( + readStatusFile(), + STATE_PENDING, + "Updater state should be pending" + ); + + // Reset the updater to its initial state to test that the complete/partial + // MAR behavior is correct + reloadUpdateManagerData(true); + + // Second parameter forces a complete MAR download. + prepareToDownloadVersion(FIRST_UPDATE_VERSION, true); + + await downloadUpdate(appUpdateAuto, () => { + Assert.equal( + gUpdateManager.downloadingUpdate.selectedPatch.type, + "complete", + "First update download should be a complete patch" + ); + }); + + Assert.equal( + gUpdateManager.readyUpdate.selectedPatch.type, + "complete", + "First update download should be a complete patch" + ); + + // Even a newer partial update should not be downloaded at this point. + prepareToDownloadVersion(SECOND_UPDATE_VERSION); + testUpdateCheckDoesNotStart(); +} + +add_task(async function all_multi_update_tests() { + setupTestCommon(true); + startUpdateServer(); + + Services.prefs.setBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false); + Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false); + + let origAppUpdateAutoVal = await UpdateUtils.getAppUpdateAutoEnabled(); + registerCleanupFunction(async () => { + await UpdateUtils.setAppUpdateAutoEnabled(origAppUpdateAutoVal); + }); + + await multi_update_test(true); + + // Reset the update system so we can start again from scratch. + reloadUpdateManagerData(true); + + await multi_update_test(false); + + doTestFinish(); +}); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/onlyDownloadUpdatesThisSession.js b/toolkit/mozapps/update/tests/unit_aus_update/onlyDownloadUpdatesThisSession.js new file mode 100644 index 0000000000..ab08ac854f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/onlyDownloadUpdatesThisSession.js @@ -0,0 +1,69 @@ +/* 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"; + +function setup() { + setupTestCommon(); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + setUpdateChannel("test_channel"); +} +setup(); + +/** + * Checks for updates and makes sure that the update process does not proceed + * beyond the downloading stage. + */ +async function downloadUpdate() { + let patches = getRemotePatchString({}); + let updateString = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updateString); + + let { updates } = await waitForUpdateCheck(true); + + initMockIncrementalDownload(); + gIncrementalDownloadErrorType = 3; + + let downloadRestrictionHitPromise = new Promise(resolve => { + let downloadRestrictionHitListener = (subject, topic) => { + Services.obs.removeObserver(downloadRestrictionHitListener, topic); + resolve(); + }; + Services.obs.addObserver( + downloadRestrictionHitListener, + "update-download-restriction-hit" + ); + }); + + let bestUpdate = gAUS.selectUpdate(updates); + let success = await gAUS.downloadUpdate(bestUpdate, false); + Assert.ok(success, "Update download should have started"); + return downloadRestrictionHitPromise; +} + +add_task(async function onlyDownloadUpdatesThisSession() { + gAUS.onlyDownloadUpdatesThisSession = true; + + await downloadUpdate(); + + Assert.ok( + !gUpdateManager.readyUpdate, + "There should not be a ready update. The update should still be downloading" + ); + Assert.ok( + !!gUpdateManager.downloadingUpdate, + "A downloading update should exist" + ); + Assert.equal( + gUpdateManager.downloadingUpdate.state, + STATE_DOWNLOADING, + "The downloading update should still be in the downloading state" + ); +}); + +add_task(async function finish() { + stop_httpserver(doTestFinish); +}); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/perInstallationPrefs.js b/toolkit/mozapps/update/tests/unit_aus_update/perInstallationPrefs.js new file mode 100644 index 0000000000..4a4a8ff2ae --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/perInstallationPrefs.js @@ -0,0 +1,238 @@ +/* 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/. + */ + +let gPolicyFunctionResult; + +async function testSetup() { + // The setup functions we use for update testing typically allow for update. + // But we are just testing preferences here. We don't want anything to + // actually attempt to update. Also, because we are messing with the pref + // system itself in this test, we want to make sure to use a pref outside of + // that system to disable update. + Services.prefs.setBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, true); + + // We run these tests whether per-installation prefs are supported or not, + // because this API needs to work in both cases. + logTestInfo( + "PER_INSTALLATION_PREFS_SUPPORTED = " + + UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED.toString() + ); + + // Save the original state so we can restore it after the test. + const originalPerInstallationPrefs = UpdateUtils.PER_INSTALLATION_PREFS; + let configFile = getUpdateDirFile(FILE_UPDATE_CONFIG_JSON); + try { + configFile.moveTo(null, FILE_BACKUP_UPDATE_CONFIG_JSON); + } catch (e) {} + + // Currently, features exist for per-installation prefs that are not used by + // any actual pref. We added them because we intend to use these features in + // the future. Thus, we will override the normally defined per-installation + // prefs and provide our own, for the purposes of testing. + UpdateUtils.PER_INSTALLATION_PREFS = { + "test.pref.boolean": { + type: UpdateUtils.PER_INSTALLATION_PREF_TYPE_BOOL, + defaultValue: true, + observerTopic: "test-pref-change-observer-boolean", + }, + "test.pref.integer": { + type: UpdateUtils.PER_INSTALLATION_PREF_TYPE_INT, + defaultValue: 1234, + observerTopic: "test-pref-change-observer-integer", + }, + "test.pref.string": { + type: UpdateUtils.PER_INSTALLATION_PREF_TYPE_ASCII_STRING, + defaultValue: "<default>", + observerTopic: "test-pref-change-observer-string", + }, + "test.pref.policy": { + type: UpdateUtils.PER_INSTALLATION_PREF_TYPE_INT, + defaultValue: 1234, + observerTopic: "test-pref-change-observer-policy", + policyFn: () => gPolicyFunctionResult, + }, + }; + // We need to re-initialize the pref system with these new prefs + UpdateUtils.initPerInstallPrefs(); + + registerCleanupFunction(() => { + if (UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED) { + let testConfigFile = getUpdateDirFile(FILE_UPDATE_CONFIG_JSON); + let backupConfigFile = getUpdateDirFile(FILE_BACKUP_UPDATE_CONFIG_JSON); + try { + testConfigFile.remove(false); + backupConfigFile.moveTo(null, FILE_UPDATE_CONFIG_JSON); + } catch (ex) {} + } else { + for (const prefName in UpdateUtils.PER_INSTALLATION_PREFS) { + Services.prefs.clearUserPref(prefName); + } + } + + UpdateUtils.PER_INSTALLATION_PREFS = originalPerInstallationPrefs; + UpdateUtils.initPerInstallPrefs(); + }); +} + +let gObserverSeenCount = 0; +let gExpectedObserverData; +function observerCallback(subject, topic, data) { + gObserverSeenCount += 1; + Assert.equal( + data, + gExpectedObserverData, + `Expected observer to have data: "${gExpectedObserverData}". ` + + `It actually has data: "${data}"` + ); +} + +async function changeAndVerifyPref( + prefName, + expectedInitialValue, + newValue, + setterShouldThrow = false +) { + let initialValue = await UpdateUtils.readUpdateConfigSetting(prefName); + Assert.strictEqual( + initialValue, + expectedInitialValue, + `Expected pref '${prefName}' to have an initial value of ` + + `${JSON.stringify(expectedInitialValue)}. Its actual initial value is ` + + `${JSON.stringify(initialValue)}` + ); + + let expectedObserverCount = 1; + if (initialValue == newValue || setterShouldThrow) { + expectedObserverCount = 0; + } + + let observerTopic = + UpdateUtils.PER_INSTALLATION_PREFS[prefName].observerTopic; + gObserverSeenCount = 0; + gExpectedObserverData = newValue.toString(); + Services.obs.addObserver(observerCallback, observerTopic); + + let returned; + let exceptionThrown; + try { + returned = await UpdateUtils.writeUpdateConfigSetting(prefName, newValue); + } catch (e) { + exceptionThrown = e; + } + if (setterShouldThrow) { + Assert.ok(!!exceptionThrown, "Expected an exception to be thrown"); + } else { + Assert.ok( + !exceptionThrown, + `Unexpected exception thrown by writeUpdateConfigSetting: ` + + `${exceptionThrown}` + ); + } + + if (!exceptionThrown) { + Assert.strictEqual( + returned, + newValue, + `Expected writeUpdateConfigSetting to return ` + + `${JSON.stringify(newValue)}. It actually returned ` + + `${JSON.stringify(returned)}` + ); + } + + let readValue = await UpdateUtils.readUpdateConfigSetting(prefName); + let expectedReadValue = exceptionThrown ? expectedInitialValue : newValue; + Assert.strictEqual( + readValue, + expectedReadValue, + `Expected pref '${prefName}' to be ${JSON.stringify(expectedReadValue)}.` + + ` It was actually ${JSON.stringify(readValue)}.` + ); + + Assert.equal( + gObserverSeenCount, + expectedObserverCount, + `Expected to see observer fire ${expectedObserverCount} times. It ` + + `actually fired ${gObserverSeenCount} times.` + ); + Services.obs.removeObserver(observerCallback, observerTopic); +} + +async function run_test() { + setupTestCommon(null); + standardInit(); + await testSetup(); + + logTestInfo("Testing boolean pref and its observer"); + let pref = "test.pref.boolean"; + let defaultValue = UpdateUtils.PER_INSTALLATION_PREFS[pref].defaultValue; + await changeAndVerifyPref(pref, defaultValue, defaultValue); + await changeAndVerifyPref(pref, defaultValue, !defaultValue); + await changeAndVerifyPref(pref, !defaultValue, !defaultValue); + await changeAndVerifyPref(pref, !defaultValue, defaultValue); + await changeAndVerifyPref(pref, defaultValue, defaultValue); + await changeAndVerifyPref(pref, defaultValue, "true", true); + await changeAndVerifyPref(pref, defaultValue, 1, true); + + logTestInfo("Testing string pref and its observer"); + pref = "test.pref.string"; + defaultValue = UpdateUtils.PER_INSTALLATION_PREFS[pref].defaultValue; + await changeAndVerifyPref(pref, defaultValue, defaultValue); + await changeAndVerifyPref(pref, defaultValue, defaultValue + "1"); + await changeAndVerifyPref(pref, defaultValue + "1", ""); + await changeAndVerifyPref(pref, "", 1, true); + await changeAndVerifyPref(pref, "", true, true); + + logTestInfo("Testing integer pref and its observer"); + pref = "test.pref.integer"; + defaultValue = UpdateUtils.PER_INSTALLATION_PREFS[pref].defaultValue; + await changeAndVerifyPref(pref, defaultValue, defaultValue); + await changeAndVerifyPref(pref, defaultValue, defaultValue + 1); + await changeAndVerifyPref(pref, defaultValue + 1, 0); + await changeAndVerifyPref(pref, 0, "1", true); + await changeAndVerifyPref(pref, 0, true, true); + + // Testing that the default pref branch works the same way that the default + // branch works for our per-profile prefs. + logTestInfo("Testing default branch behavior"); + pref = "test.pref.integer"; + let originalDefault = UpdateUtils.PER_INSTALLATION_PREFS[pref].defaultValue; + // Make sure the value is the default value, then change the default value + // and check that the effective value changes. + await UpdateUtils.writeUpdateConfigSetting(pref, originalDefault, { + setDefaultOnly: true, + }); + await UpdateUtils.writeUpdateConfigSetting(pref, originalDefault); + await UpdateUtils.writeUpdateConfigSetting(pref, originalDefault + 1, { + setDefaultOnly: true, + }); + Assert.strictEqual( + await UpdateUtils.readUpdateConfigSetting(pref), + originalDefault + 1, + `Expected that changing the default of a pref with no user value should ` + + `change the effective value` + ); + // Now make the user value different from the default value and ensure that + // changing the default value does not affect the effective value + await UpdateUtils.writeUpdateConfigSetting(pref, originalDefault + 10); + await UpdateUtils.writeUpdateConfigSetting(pref, originalDefault + 20, { + setDefaultOnly: true, + }); + Assert.strictEqual( + await UpdateUtils.readUpdateConfigSetting(pref), + originalDefault + 10, + `Expected that changing the default of a pref with a user value should ` + + `NOT change the effective value` + ); + + logTestInfo("Testing policy behavior"); + pref = "test.pref.policy"; + defaultValue = UpdateUtils.PER_INSTALLATION_PREFS[pref].defaultValue; + gPolicyFunctionResult = null; + await changeAndVerifyPref(pref, defaultValue, defaultValue + 1); + gPolicyFunctionResult = defaultValue + 10; + await changeAndVerifyPref(pref, gPolicyFunctionResult, 0, true); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js b/toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js new file mode 100644 index 0000000000..94081a01a5 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js @@ -0,0 +1,327 @@ +/* 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/. + */ + +async function run_test() { + setupTestCommon(); + debugDump("testing remote update xml attributes"); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + setUpdateChannel("test_channel"); + + debugDump("testing update xml not available"); + await waitForUpdateCheck(false).then(aArgs => { + Assert.equal( + aArgs.updates[0].errorCode, + 1500, + "the update errorCode" + MSG_SHOULD_EQUAL + ); + }); + + debugDump( + "testing one update available, the update's property values and " + + "the property values of the update's patches" + ); + let patchProps = { + type: "complete", + url: "http://complete/", + size: "9856459", + }; + let patches = getRemotePatchString(patchProps); + patchProps = { type: "partial", url: "http://partial/", size: "1316138" }; + patches += getRemotePatchString(patchProps); + let updateProps = { + type: "minor", + name: "Minor Test", + displayVersion: "version 2.1a1pre", + appVersion: "2.1a1pre", + buildID: "20080811053724", + detailsURL: "http://details/", + promptWaitTime: "345600", + custom1: 'custom1_attr="custom1 value"', + custom2: 'custom2_attr="custom2 value"', + }; + let updates = getRemoteUpdateString(updateProps, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 1 }).then(aArgs => { + // XXXrstrong - not specifying a detailsURL will cause a leak due to + // bug 470244 and until this is fixed this will not test the value for + // detailsURL when it isn't specified in the update xml. + + let bestUpdate = gAUS + .selectUpdate(aArgs.updates) + .QueryInterface(Ci.nsIWritablePropertyBag); + Assert.equal( + bestUpdate.type, + "minor", + "the update type attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.name, + "Minor Test", + "the update name attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.displayVersion, + "version 2.1a1pre", + "the update displayVersion attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.appVersion, + "2.1a1pre", + "the update appVersion attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.buildID, + "20080811053724", + "the update buildID attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.detailsURL, + "http://details/", + "the update detailsURL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.promptWaitTime, + "345600", + "the update promptWaitTime attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.serviceURL, + gURLData + gHTTPHandlerPath + "?force=1", + "the update serviceURL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.channel, + "test_channel", + "the update channel attribute" + MSG_SHOULD_EQUAL + ); + Assert.ok( + !bestUpdate.isCompleteUpdate, + "the update isCompleteUpdate attribute" + MSG_SHOULD_EQUAL + ); + Assert.ok( + !bestUpdate.isSecurityUpdate, + "the update isSecurityUpdate attribute" + MSG_SHOULD_EQUAL + ); + // Check that installDate is within 10 seconds of the current date. + Assert.ok( + Date.now() - bestUpdate.installDate < 10000, + "the update installDate attribute should be within 10 seconds " + + "of the current time" + ); + Assert.ok( + !bestUpdate.statusText, + "the update statusText attribute" + MSG_SHOULD_EQUAL + ); + // nsIUpdate:state returns an empty string when no action has been performed + // on an available update + Assert.equal( + bestUpdate.state, + "", + "the update state attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.errorCode, + 0, + "the update errorCode attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.patchCount, + 2, + "the update patchCount attribute" + MSG_SHOULD_EQUAL + ); + // XXX TODO - test nsIUpdate:serialize + + Assert.equal( + bestUpdate.getProperty("custom1_attr"), + "custom1 value", + "the update custom1_attr property" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.getProperty("custom2_attr"), + "custom2 value", + "the update custom2_attr property" + MSG_SHOULD_EQUAL + ); + + let patch = bestUpdate.getPatchAt(0); + Assert.equal( + patch.type, + "complete", + "the update patch type attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.URL, + "http://complete/", + "the update patch URL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.size, + 9856459, + "the update patch size attribute" + MSG_SHOULD_EQUAL + ); + // The value for patch.state can be the string 'null' as a valid value. This + // is confusing if it returns null which is an invalid value since the test + // failure output will show a failure for null == null. To lessen the + // confusion first check that the typeof for patch.state is string. + Assert.equal( + typeof patch.state, + "string", + "the update patch state typeof value should equal |string|" + ); + Assert.equal( + patch.state, + STATE_NONE, + "the update patch state attribute" + MSG_SHOULD_EQUAL + ); + Assert.ok( + !patch.selected, + "the update patch selected attribute" + MSG_SHOULD_EQUAL + ); + // XXX TODO - test nsIUpdatePatch:serialize + + patch = bestUpdate.getPatchAt(1); + Assert.equal( + patch.type, + "partial", + "the update patch type attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.URL, + "http://partial/", + "the update patch URL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.size, + 1316138, + "the update patch size attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.state, + STATE_NONE, + "the update patch state attribute" + MSG_SHOULD_EQUAL + ); + Assert.ok( + !patch.selected, + "the update patch selected attribute" + MSG_SHOULD_EQUAL + ); + // XXX TODO - test nsIUpdatePatch:serialize + }); + + debugDump( + "testing an empty update xml that returns a root node name of " + + "parsererror" + ); + gResponseBody = "<parsererror/>"; + await waitForUpdateCheck(false).then(aArgs => { + Assert.equal( + aArgs.updates[0].errorCode, + 1200, + "the update errorCode" + MSG_SHOULD_EQUAL + ); + }); + + debugDump("testing no updates available"); + gResponseBody = getRemoteUpdatesXMLString(""); + await waitForUpdateCheck(true, { updateCount: 0 }); + + debugDump("testing one update available with two patches"); + patches = getRemotePatchString({}); + patchProps = { type: "partial" }; + patches += getRemotePatchString(patchProps); + updates = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 1 }); + + debugDump("testing three updates available each with two patches"); + patches = getRemotePatchString({}); + patchProps = { type: "partial" }; + patches += getRemotePatchString(patchProps); + updates = getRemoteUpdateString({}, patches); + updates += updates + updates; + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 3 }); + + debugDump( + "testing one update with complete and partial patches with size " + + "0 specified in the update xml" + ); + patchProps = { size: "0" }; + patches = getRemotePatchString(patchProps); + patchProps = { type: "partial", size: "0" }; + patches += getRemotePatchString(patchProps); + updates = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true).then(aArgs => { + Assert.equal( + aArgs.updates.length, + 0, + "the update count" + MSG_SHOULD_EQUAL + ); + }); + + debugDump( + "testing one update with complete patch with size 0 specified in " + + "the update xml" + ); + patchProps = { size: "0" }; + patches = getRemotePatchString(patchProps); + updates = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 0 }); + + debugDump( + "testing one update with partial patch with size 0 specified in " + + "the update xml" + ); + patchProps = { type: "partial", size: "0" }; + patches = getRemotePatchString(patchProps); + updates = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 0 }); + + debugDump( + "testing that updates for older versions of the application " + + "aren't selected" + ); + patches = getRemotePatchString({}); + patchProps = { type: "partial" }; + patches += getRemotePatchString(patchProps); + updateProps = { appVersion: "1.0pre" }; + updates = getRemoteUpdateString(updateProps, patches); + updateProps = { appVersion: "1.0a" }; + updates += getRemoteUpdateString(updateProps, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 2 }).then(aArgs => { + let bestUpdate = gAUS.selectUpdate(aArgs.updates); + Assert.ok(!bestUpdate, "there shouldn't be an update available"); + }); + + debugDump( + "testing that updates for the current version of the application " + + "are selected" + ); + patches = getRemotePatchString({}); + patchProps = { type: "partial" }; + patches += getRemotePatchString(patchProps); + updateProps = { appVersion: "1.0" }; + updates = getRemoteUpdateString(updateProps, patches); + gResponseBody = getRemoteUpdatesXMLString(updates); + await waitForUpdateCheck(true, { updateCount: 1 }).then(aArgs => { + let bestUpdate = gAUS.selectUpdate(aArgs.updates); + Assert.ok(!!bestUpdate, "there should be one update available"); + Assert.equal( + bestUpdate.appVersion, + "1.0", + "the update appVersion attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + bestUpdate.displayVersion, + "1.0", + "the update displayVersion attribute" + MSG_SHOULD_EQUAL + ); + }); + + stop_httpserver(doTestFinish); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/testConstants.js b/toolkit/mozapps/update/tests/unit_aus_update/testConstants.js new file mode 100644 index 0000000000..ace6134a38 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/testConstants.js @@ -0,0 +1,8 @@ +/* eslint-disable no-unused-vars */ +const REL_PATH_DATA = ""; +const URL_HOST = "http://127.0.0.1:8888"; +const URL_PATH_UPDATE_XML = "/" + REL_PATH_DATA + "app_update.sjs"; +const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML; +const CONTINUE_CHECK = "continueCheck"; +const CONTINUE_DOWNLOAD = "continueDownload"; +const CONTINUE_STAGING = "continueStaging"; diff --git a/toolkit/mozapps/update/tests/unit_aus_update/updateAutoPrefMigrate.js b/toolkit/mozapps/update/tests/unit_aus_update/updateAutoPrefMigrate.js new file mode 100644 index 0000000000..8f62b9458b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/updateAutoPrefMigrate.js @@ -0,0 +1,74 @@ +/* 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/. + */ + +async function verifyPref(expectedValue) { + let configValue = await UpdateUtils.getAppUpdateAutoEnabled(); + Assert.equal( + configValue, + expectedValue, + "Value returned by getAppUpdateAutoEnabled should have " + + "matched the expected value" + ); +} + +async function run_test() { + setupTestCommon(null); + standardInit(); + + let configFile = getUpdateDirFile(FILE_UPDATE_CONFIG_JSON); + + // Test that, if there is no value to migrate, the default value is set. + Services.prefs.setBoolPref("app.update.auto.migrated", false); + Services.prefs.clearUserPref("app.update.auto"); + Assert.ok(!configFile.exists(), "Config file should not exist yet"); + await verifyPref( + UpdateUtils.PER_INSTALLATION_PREFS["app.update.auto"].defaultValue + ); + + debugDump("about to remove config file"); + configFile.remove(false); + + // Test migration of a |false| value + Services.prefs.setBoolPref("app.update.auto.migrated", false); + Services.prefs.setBoolPref("app.update.auto", false); + Assert.ok(!configFile.exists(), "Config file should have been removed"); + await verifyPref(false); + + // Test that migration doesn't happen twice + Services.prefs.setBoolPref("app.update.auto", true); + await verifyPref(false); + + // If the file is deleted after migration, the default value should be + // returned, regardless of the pref value. + debugDump("about to remove config file"); + configFile.remove(false); + Assert.ok(!configFile.exists(), "Config file should have been removed"); + let configValue = await UpdateUtils.getAppUpdateAutoEnabled(); + Assert.equal( + configValue, + true, + "getAppUpdateAutoEnabled should have returned the default value (true)" + ); + + // Setting a new value should cause the value to get written out again + await UpdateUtils.setAppUpdateAutoEnabled(false); + await verifyPref(false); + + // Test migration of a |true| value + Services.prefs.setBoolPref("app.update.auto.migrated", false); + Services.prefs.setBoolPref("app.update.auto", true); + configFile.remove(false); + Assert.ok( + !configFile.exists(), + "App update config file should have been removed" + ); + await verifyPref(true); + + // Test that setting app.update.auto without migrating also works + await UpdateUtils.setAppUpdateAutoEnabled(false); + await verifyPref(false); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/updateCheckCombine.js b/toolkit/mozapps/update/tests/unit_aus_update/updateCheckCombine.js new file mode 100644 index 0000000000..1b2c9ef78c --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/updateCheckCombine.js @@ -0,0 +1,38 @@ +/* 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"; + +/** + * This test checks that multiple foreground update checks are combined into + * a single web request. + */ + +add_task(async function setup() { + setupTestCommon(); + start_httpserver(); + setUpdateURL(gURLData + gHTTPHandlerPath); + setUpdateChannel("test_channel"); + + let patches = getRemotePatchString({}); + let updateString = getRemoteUpdateString({}, patches); + gResponseBody = getRemoteUpdatesXMLString(updateString); +}); + +add_task(async function testUpdateCheckCombine() { + gUpdateCheckCount = 0; + let check1 = gUpdateChecker.checkForUpdates(gUpdateChecker.FOREGROUND_CHECK); + let check2 = gUpdateChecker.checkForUpdates(gUpdateChecker.FOREGROUND_CHECK); + + let result1 = await check1.result; + let result2 = await check2.result; + Assert.ok(result1.succeeded, "Check 1 should have succeeded"); + Assert.ok(result2.succeeded, "Check 2 should have succeeded"); + Assert.equal(gUpdateCheckCount, 1, "Should only have made a single request"); +}); + +add_task(async function finish() { + stop_httpserver(doTestFinish); +}); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/updateDirectoryMigrate.js b/toolkit/mozapps/update/tests/unit_aus_update/updateDirectoryMigrate.js new file mode 100644 index 0000000000..6c566d76fc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/updateDirectoryMigrate.js @@ -0,0 +1,246 @@ +/* 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/. + */ + +/** + * Gets the root directory for the old (unmigrated) updates directory. + * + * @return nsIFile for the updates root directory. + */ +function getOldUpdatesRootDir() { + return Services.dirsvc.get(XRE_OLD_UPDATE_ROOT_DIR, Ci.nsIFile); +} + +/** + * Gets the old (unmigrated) updates directory. + * + * @return nsIFile for the updates directory. + */ +function getOldUpdatesDir() { + let dir = getOldUpdatesRootDir(); + dir.append(DIR_UPDATES); + return dir; +} + +/** + * Gets the directory for update patches in the old (unmigrated) updates + * directory. + * + * @return nsIFile for the updates directory. + */ +function getOldUpdatesPatchDir() { + let dir = getOldUpdatesDir(); + dir.append(DIR_PATCH); + return dir; +} + +/** + * Returns either the active or regular update database XML file in the old + * (unmigrated) updates directory + * + * @param isActiveUpdate + * If true this will return the active-update.xml otherwise it will + * return the updates.xml file. + */ +function getOldUpdatesXMLFile(aIsActiveUpdate) { + let file = getOldUpdatesRootDir(); + file.append(aIsActiveUpdate ? FILE_ACTIVE_UPDATE_XML : FILE_UPDATES_XML); + return file; +} + +/** + * Writes the updates specified to either the active-update.xml or the + * updates.xml in the old (unmigrated) update directory + * + * @param aContent + * The updates represented as a string to write to the XML file. + * @param isActiveUpdate + * If true this will write to the active-update.xml otherwise it will + * write to the updates.xml file. + */ +function writeUpdatesToOldXMLFile(aContent, aIsActiveUpdate) { + writeFile(getOldUpdatesXMLFile(aIsActiveUpdate), aContent); +} + +/** + * Writes the given update operation/state to a file in the old (unmigrated) + * patch directory, indicating to the patching system what operations need + * to be performed. + * + * @param aStatus + * The status value to write. + */ +function writeOldStatusFile(aStatus) { + let file = getOldUpdatesPatchDir(); + file.append(FILE_UPDATE_STATUS); + writeFile(file, aStatus + "\n"); +} + +/** + * Writes the given data to the config file in the old (unmigrated) + * patch directory. + * + * @param aData + * The config data to write. + */ +function writeOldConfigFile(aData) { + let file = getOldUpdatesRootDir(); + file.append(FILE_UPDATE_CONFIG_JSON); + writeFile(file, aData); +} + +/** + * Gets the specified update log from the old (unmigrated) update directory + * + * @param aLogLeafName + * The leaf name of the log to get. + * @return nsIFile for the update log. + */ +function getOldUpdateLog(aLogLeafName) { + let updateLog = getOldUpdatesDir(); + if (aLogLeafName == FILE_UPDATE_LOG) { + updateLog.append(DIR_PATCH); + } + updateLog.append(aLogLeafName); + return updateLog; +} + +async function run_test() { + setupTestCommon(null); + + debugDump( + "testing that the update directory is migrated after a successful update" + ); + + Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, 5); + + let patchProps = { state: STATE_PENDING }; + let patches = getLocalPatchString(patchProps); + let updates = getLocalUpdateString({}, patches); + writeUpdatesToOldXMLFile(getLocalUpdatesXMLString(updates), true); + writeOldStatusFile(STATE_SUCCEEDED); + writeOldConfigFile('{"app.update.auto":false}'); + + let log = getOldUpdateLog(FILE_UPDATE_LOG); + writeFile(log, "Last Update Log"); + + let oldUninstallPingFile = getOldUpdatesRootDir(); + const hash = oldUninstallPingFile.leafName; + const uninstallPingFilename = `uninstall_ping_${hash}_98537294-d37b-4b8b-a4e9-ab417a5d7a87.json`; + oldUninstallPingFile = oldUninstallPingFile.parent.parent; + oldUninstallPingFile.append(uninstallPingFilename); + const uninstallPingContents = "arbitrary uninstall ping file contents"; + writeFile(oldUninstallPingFile, uninstallPingContents); + + let oldBackgroundUpdateLog1File = getOldUpdatesRootDir(); + const oldBackgroundUpdateLog1Filename = "backgroundupdate.moz_log"; + oldBackgroundUpdateLog1File.append(oldBackgroundUpdateLog1Filename); + const oldBackgroundUpdateLog1Contents = "arbitrary log 1 contents"; + writeFile(oldBackgroundUpdateLog1File, oldBackgroundUpdateLog1Contents); + + let oldBackgroundUpdateLog2File = getOldUpdatesRootDir(); + const oldBackgroundUpdateLog2Filename = "backgroundupdate.child-1.moz_log"; + oldBackgroundUpdateLog2File.append(oldBackgroundUpdateLog2Filename); + const oldBackgroundUpdateLog2Contents = "arbitrary log 2 contents"; + writeFile(oldBackgroundUpdateLog2File, oldBackgroundUpdateLog2Contents); + + const pendingPingRelativePath = + "backgroundupdate\\datareporting\\glean\\pending_pings\\" + + "01234567-89ab-cdef-fedc-0123456789ab"; + let oldPendingPingFile = getOldUpdatesRootDir(); + oldPendingPingFile.appendRelativePath(pendingPingRelativePath); + const pendingPingContents = "arbitrary pending ping file contents"; + writeFile(oldPendingPingFile, pendingPingContents); + + standardInit(); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "there should not be a downloading update" + ); + Assert.ok(!gUpdateManager.readyUpdate, "there should not be a ready update"); + Assert.equal( + gUpdateManager.getUpdateCount(), + 1, + "the update manager update count" + MSG_SHOULD_EQUAL + ); + await waitForUpdateXMLFiles(); + + let cancelations = Services.prefs.getIntPref(PREF_APP_UPDATE_CANCELATIONS, 0); + Assert.equal( + cancelations, + 0, + "the " + PREF_APP_UPDATE_CANCELATIONS + " preference " + MSG_SHOULD_EQUAL + ); + + let oldDir = getOldUpdatesRootDir(); + let newDir = getUpdateDirFile(); + if (oldDir.path != newDir.path) { + Assert.ok( + !oldDir.exists(), + "Old update directory should have been deleted after migration" + ); + } + + log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST); + Assert.equal( + readFile(log), + "Last Update Log", + "the last update log contents" + MSG_SHOULD_EQUAL + ); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST); + + let dir = getUpdateDirFile(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST); + + Assert.equal( + await UpdateUtils.getAppUpdateAutoEnabled(), + false, + "Automatic update download setting should have been migrated." + ); + + let newUninstallPing = newDir.parent.parent; + newUninstallPing.append(uninstallPingFilename); + Assert.ok(newUninstallPing.exists(), MSG_SHOULD_EXIST); + Assert.equal( + readFile(newUninstallPing), + uninstallPingContents, + "the uninstall ping contents" + MSG_SHOULD_EQUAL + ); + + let newBackgroundUpdateLog1File = newDir.clone(); + newBackgroundUpdateLog1File.append(oldBackgroundUpdateLog1Filename); + Assert.ok(newBackgroundUpdateLog1File.exists(), MSG_SHOULD_EXIST); + Assert.equal( + readFile(newBackgroundUpdateLog1File), + oldBackgroundUpdateLog1Contents, + "background log file 1 contents" + MSG_SHOULD_EQUAL + ); + + let newBackgroundUpdateLog2File = newDir.clone(); + newBackgroundUpdateLog2File.append(oldBackgroundUpdateLog2Filename); + Assert.ok(newBackgroundUpdateLog2File.exists(), MSG_SHOULD_EXIST); + Assert.equal( + readFile(newBackgroundUpdateLog2File), + oldBackgroundUpdateLog2Contents, + "background log file 2 contents" + MSG_SHOULD_EQUAL + ); + + let newPendingPing = newDir.clone(); + newPendingPing.appendRelativePath(pendingPingRelativePath); + Assert.ok(newPendingPing.exists(), MSG_SHOULD_EXIST); + Assert.equal( + readFile(newPendingPing), + pendingPingContents, + "the pending ping contents" + MSG_SHOULD_EQUAL + ); + + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js b/toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js new file mode 100644 index 0000000000..8da56aae95 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js @@ -0,0 +1,593 @@ +/* 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/. + */ + +function run_test() { + setupTestCommon(); + + debugDump( + "testing addition of a successful update to " + + FILE_UPDATES_XML + + " and verification of update properties including the format " + + "prior to bug 530872" + ); + + setUpdateChannel("test_channel"); + + let patchProps = { + type: "partial", + url: "http://partial/", + size: "86", + selected: "true", + state: STATE_PENDING, + custom1: 'custom1_attr="custom1 patch value"', + custom2: 'custom2_attr="custom2 patch value"', + }; + let patches = getLocalPatchString(patchProps); + let updateProps = { + type: "major", + name: "New", + displayVersion: "version 4", + appVersion: "4.0", + buildID: "20070811053724", + detailsURL: "http://details1/", + serviceURL: "http://service1/", + installDate: "1238441300314", + statusText: "test status text", + isCompleteUpdate: "false", + channel: "test_channel", + foregroundDownload: "true", + promptWaitTime: "345600", + previousAppVersion: "3.0", + custom1: 'custom1_attr="custom1 value"', + custom2: 'custom2_attr="custom2 value"', + }; + let updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeStatusFile(STATE_SUCCEEDED); + + patchProps = { + type: "complete", + url: "http://complete/", + size: "75", + selected: "true", + state: STATE_FAILED, + custom1: 'custom3_attr="custom3 patch value"', + custom2: 'custom4_attr="custom4 patch value"', + }; + patches = getLocalPatchString(patchProps); + updateProps = { + type: "minor", + name: "Existing", + appVersion: "3.0", + detailsURL: "http://details2/", + serviceURL: "http://service2/", + statusText: getString("patchApplyFailure"), + isCompleteUpdate: "true", + channel: "test_channel", + foregroundDownload: "false", + promptWaitTime: "691200", + custom1: 'custom3_attr="custom3 value"', + custom2: 'custom4_attr="custom4 value"', + }; + updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), false); + + standardInit(); + + Assert.ok( + !gUpdateManager.downloadingUpdate, + "there should not be a downloading update" + ); + Assert.ok(!gUpdateManager.readyUpdate, "there should not be a ready update"); + Assert.equal( + gUpdateManager.getUpdateCount(), + 2, + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL + ); + + debugDump("checking the first update properties"); + let update = gUpdateManager + .getUpdateAt(0) + .QueryInterface(Ci.nsIWritablePropertyBag); + Assert.equal( + update.state, + STATE_SUCCEEDED, + "the update state attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.type, + "major", + "the update type attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.name, + "New", + "the update name attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.displayVersion, + "version 4", + "the update displayVersion attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.appVersion, + "4.0", + "the update appVersion attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.buildID, + "20070811053724", + "the update buildID attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.detailsURL, + "http://details1/", + "the update detailsURL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.serviceURL, + "http://service1/", + "the update serviceURL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.installDate, + "1238441300314", + "the update installDate attribute" + MSG_SHOULD_EQUAL + ); + // statusText is updated + Assert.equal( + update.statusText, + getString("installSuccess"), + "the update statusText attribute" + MSG_SHOULD_EQUAL + ); + Assert.ok( + !update.isCompleteUpdate, + "the update isCompleteUpdate attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.channel, + "test_channel", + "the update channel attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.promptWaitTime, + "345600", + "the update promptWaitTime attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.previousAppVersion, + "3.0", + "the update previousAppVersion attribute" + MSG_SHOULD_EQUAL + ); + // Custom attributes + Assert.equal( + update.getProperty("custom1_attr"), + "custom1 value", + "the update custom1_attr property" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.getProperty("custom2_attr"), + "custom2 value", + "the update custom2_attr property" + MSG_SHOULD_EQUAL + ); + // nsIPropertyBag enumerator + debugDump("checking the first update enumerator"); + Assert.ok( + update.enumerator instanceof Ci.nsISimpleEnumerator, + "update enumerator should be an instance of nsISimpleEnumerator" + ); + let results = Array.from(update.enumerator); + Assert.equal( + results.length, + 3, + "the length of the array created from the update enumerator" + + MSG_SHOULD_EQUAL + ); + Assert.ok( + results.every(prop => prop instanceof Ci.nsIProperty), + "the objects in the array created from the update enumerator " + + "should all be an instance of nsIProperty" + ); + Assert.equal( + results[0].name, + "custom1_attr", + "the first property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[0].value, + "custom1 value", + "the first property value" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[1].name, + "custom2_attr", + "the second property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[1].value, + "custom2 value", + "the second property value" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[2].name, + "foregroundDownload", + "the second property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[2].value, + "true", + "the third property value" + MSG_SHOULD_EQUAL + ); + + debugDump("checking the first update patch properties"); + let patch = update.selectedPatch.QueryInterface(Ci.nsIWritablePropertyBag); + Assert.equal( + patch.type, + "partial", + "the update patch type attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.URL, + "http://partial/", + "the update patch URL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.size, + "86", + "the update patch size attribute" + MSG_SHOULD_EQUAL + ); + Assert.ok( + !!patch.selected, + "the update patch selected attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.state, + STATE_SUCCEEDED, + "the update patch state attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.getProperty("custom1_attr"), + "custom1 patch value", + "the update patch custom1_attr property" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.getProperty("custom2_attr"), + "custom2 patch value", + "the update patch custom2_attr property" + MSG_SHOULD_EQUAL + ); + // nsIPropertyBag enumerator + debugDump("checking the first update patch enumerator"); + Assert.ok( + patch.enumerator instanceof Ci.nsISimpleEnumerator, + "patch enumerator should be an instance of nsISimpleEnumerator" + ); + results = Array.from(patch.enumerator); + Assert.equal( + results.length, + 2, + "the length of the array created from the patch enumerator" + + MSG_SHOULD_EQUAL + ); + Assert.ok( + results.every(prop => prop instanceof Ci.nsIProperty), + "the objects in the array created from the patch enumerator " + + "should all be an instance of nsIProperty" + ); + Assert.equal( + results[0].name, + "custom1_attr", + "the first property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[0].value, + "custom1 patch value", + "the first property value" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[1].name, + "custom2_attr", + "the second property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[1].value, + "custom2 patch value", + "the second property value" + MSG_SHOULD_EQUAL + ); + + debugDump("checking the second update properties"); + update = gUpdateManager + .getUpdateAt(1) + .QueryInterface(Ci.nsIWritablePropertyBag); + Assert.equal( + update.state, + STATE_FAILED, + "the update state attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.name, + "Existing", + "the update name attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.type, + "minor", + "the update type attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.displayVersion, + "3.0", + "the update displayVersion attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.appVersion, + "3.0", + "the update appVersion attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.detailsURL, + "http://details2/", + "the update detailsURL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.serviceURL, + "http://service2/", + "the update serviceURL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.installDate, + "1238441400314", + "the update installDate attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.statusText, + getString("patchApplyFailure"), + "the update statusText attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.buildID, + "20080811053724", + "the update buildID attribute" + MSG_SHOULD_EQUAL + ); + Assert.ok( + !!update.isCompleteUpdate, + "the update isCompleteUpdate attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.channel, + "test_channel", + "the update channel attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.promptWaitTime, + "691200", + "the update promptWaitTime attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.previousAppVersion, + "1.0", + "the update previousAppVersion attribute" + MSG_SHOULD_EQUAL + ); + // Custom attributes + Assert.equal( + update.getProperty("custom3_attr"), + "custom3 value", + "the update custom3_attr property" + MSG_SHOULD_EQUAL + ); + Assert.equal( + update.getProperty("custom4_attr"), + "custom4 value", + "the update custom4_attr property" + MSG_SHOULD_EQUAL + ); + // nsIPropertyBag enumerator + debugDump("checking the second update enumerator"); + Assert.ok( + update.enumerator instanceof Ci.nsISimpleEnumerator, + "update enumerator should be an instance of nsISimpleEnumerator" + ); + results = Array.from(update.enumerator); + Assert.equal( + results.length, + 3, + "the length of the array created from the update enumerator" + + MSG_SHOULD_EQUAL + ); + Assert.ok( + results.every(prop => prop instanceof Ci.nsIProperty), + "the objects in the array created from the update enumerator " + + "should all be an instance of nsIProperty" + ); + Assert.equal( + results[0].name, + "custom3_attr", + "the first property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[0].value, + "custom3 value", + "the first property value" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[1].name, + "custom4_attr", + "the second property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[1].value, + "custom4 value", + "the second property value" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[2].name, + "foregroundDownload", + "the third property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[2].value, + "false", + "the third property value" + MSG_SHOULD_EQUAL + ); + + debugDump("checking the second update patch properties"); + patch = update.selectedPatch.QueryInterface(Ci.nsIWritablePropertyBag); + Assert.equal( + patch.type, + "complete", + "the update patch type attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.URL, + "http://complete/", + "the update patch URL attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.size, + "75", + "the update patch size attribute" + MSG_SHOULD_EQUAL + ); + Assert.ok( + !!patch.selected, + "the update patch selected attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.state, + STATE_FAILED, + "the update patch state attribute" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.getProperty("custom3_attr"), + "custom3 patch value", + "the update patch custom3_attr property" + MSG_SHOULD_EQUAL + ); + Assert.equal( + patch.getProperty("custom4_attr"), + "custom4 patch value", + "the update patch custom4_attr property" + MSG_SHOULD_EQUAL + ); + // nsIPropertyBag enumerator + debugDump("checking the second update patch enumerator"); + Assert.ok( + patch.enumerator instanceof Ci.nsISimpleEnumerator, + "patch enumerator should be an instance of nsISimpleEnumerator" + ); + results = Array.from(patch.enumerator); + Assert.equal( + results.length, + 2, + "the length of the array created from the patch enumerator" + + MSG_SHOULD_EQUAL + ); + Assert.ok( + results.every(prop => prop instanceof Ci.nsIProperty), + "the objects in the array created from the patch enumerator " + + "should all be an instance of nsIProperty" + ); + Assert.equal( + results[0].name, + "custom3_attr", + "the first property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[0].value, + "custom3 patch value", + "the first property value" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[1].name, + "custom4_attr", + "the second property name" + MSG_SHOULD_EQUAL + ); + Assert.equal( + results[1].value, + "custom4 patch value", + "the second property value" + MSG_SHOULD_EQUAL + ); + + let attrNames = [ + "appVersion", + "buildID", + "channel", + "detailsURL", + "displayVersion", + "elevationFailure", + "errorCode", + "installDate", + "isCompleteUpdate", + "name", + "previousAppVersion", + "promptWaitTime", + "serviceURL", + "state", + "statusText", + "type", + "unsupported", + ]; + checkIllegalProperties(update, attrNames); + + attrNames = [ + "errorCode", + "finalURL", + "selected", + "size", + "state", + "type", + "URL", + ]; + checkIllegalProperties(patch, attrNames); + + executeSoon(doTestFinish); +} + +function checkIllegalProperties(object, propertyNames) { + let objectName = + object instanceof Ci.nsIUpdate ? "nsIUpdate" : "nsIUpdatePatch"; + propertyNames.forEach(function(name) { + // Check that calling getProperty, setProperty, and deleteProperty on an + // nsIUpdate attribute throws NS_ERROR_ILLEGAL_VALUE + let result = 0; + try { + object.getProperty(name); + } catch (e) { + result = e.result; + } + Assert.equal( + result, + Cr.NS_ERROR_ILLEGAL_VALUE, + "calling getProperty using an " + + objectName + + " attribute " + + "name should throw NS_ERROR_ILLEGAL_VALUE" + ); + + result = 0; + try { + object.setProperty(name, "value"); + } catch (e) { + result = e.result; + } + Assert.equal( + result, + Cr.NS_ERROR_ILLEGAL_VALUE, + "calling setProperty using an " + + objectName + + " attribute " + + "name should throw NS_ERROR_ILLEGAL_VALUE" + ); + + result = 0; + try { + object.deleteProperty(name); + } catch (e) { + result = e.result; + } + Assert.equal( + result, + Cr.NS_ERROR_ILLEGAL_VALUE, + "calling deleteProperty using an " + + objectName + + " attribute " + + "name should throw NS_ERROR_ILLEGAL_VALUE" + ); + }); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/updateSyncManager.js b/toolkit/mozapps/update/tests/unit_aus_update/updateSyncManager.js new file mode 100644 index 0000000000..a0f3fe080f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/updateSyncManager.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This test verifies that the update sync manager is working correctly by +// a) making sure we're the only one that's opened it to begin with, and then +// b) starting a second copy of the same binary and making sure we can tell we +// are no longer the only one that's opened it. + +const { Subprocess } = ChromeUtils.importESModule( + "resource://gre/modules/Subprocess.sys.mjs" +); + +// Save off the real GRE directory and binary path before we register our +// mock directory service which overrides them both. +const thisBinary = Services.dirsvc.get("XREExeF", Ci.nsIFile); +const greDir = Services.dirsvc.get("GreD", Ci.nsIFile); + +add_task(async function() { + setupTestCommon(); + + // First check that we believe we exclusively hold the lock. + let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + Assert.ok( + !syncManager.isOtherInstanceRunning(), + "no other instance is running yet" + ); + + // Now start a second copy of this xpcshell binary so that something else + // takes the same lock. First we'll define its command line. + // Most of the child's code is in a separate script file, so all the command + // line has to do is set up a few required path strings we need to pass + // through to the child, and then include the script file. + const args = [ + "-g", + greDir.path, + "-e", + ` + const customGreDirPath = "${getApplyDirFile( + DIR_RESOURCES + ).path.replaceAll("\\", "\\\\")}"; + const customGreBinDirPath = "${getApplyDirFile(DIR_MACOS).path.replaceAll( + "\\", + "\\\\" + )}"; + const customExePath = "${getApplyDirFile( + DIR_MACOS + FILE_APP_BIN + ).path.replaceAll("\\", "\\\\")}"; + const customUpdDirPath = "${getMockUpdRootD().path.replaceAll( + "\\", + "\\\\" + )}"; + const customOldUpdDirPath = "${getMockUpdRootD(true).path.replaceAll( + "\\", + "\\\\" + )}"; + `, + "-f", + getTestDirFile("syncManagerTestChild.js").path, + ]; + + // Run the second copy two times, to show the lock is usable after having + // been closed. + for (let runs = 0; runs < 2; runs++) { + // Now we can actually invoke the process. + debugDump( + `launching child process at ${thisBinary.path} with args ${args}` + ); + Subprocess.call({ + command: thisBinary.path, + arguments: args, + stderr: "stdout", + }); + + // It will take the new xpcshell a little time to start up, but we should see + // the effect on the lock within at most a few seconds. + await TestUtils.waitForCondition( + () => syncManager.isOtherInstanceRunning(), + "waiting for child process to take the lock" + ).catch(e => { + // Rather than throwing out of waitForCondition(), catch and log the failure + // manually so that we get output that's a bit more readable. + Assert.ok( + syncManager.isOtherInstanceRunning(), + "child process has the lock" + ); + }); + + // The lock should have been closed when the process exited, but we'll allow + // a little time for the OS to clean up the handle. + await TestUtils.waitForCondition( + () => !syncManager.isOtherInstanceRunning(), + "waiting for child process to release the lock" + ).catch(e => { + Assert.ok( + !syncManager.isOtherInstanceRunning(), + "child process has released the lock" + ); + }); + } + + doTestFinish(); +}); diff --git a/toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js b/toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js new file mode 100644 index 0000000000..6fd1cf8a73 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js @@ -0,0 +1,26 @@ +/* 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/. + */ + +/* Application Update URL Construction Tests */ + +/** + * Tests for the majority of values are located in + * toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js + * toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js + */ + +async function run_test() { + const URL_PREFIX = URL_HOST + "/"; + setupTestCommon(); + let url = URL_PREFIX; + debugDump("testing url force param is present: " + url); + setUpdateURL(url); + await waitForUpdateCheck(false, { url: url + "?force=1" }); + url = URL_PREFIX + "?testparam=1"; + debugDump("testing url force param when there is already a param: " + url); + setUpdateURL(url); + await waitForUpdateCheck(false, { url: url + "&force=1" }); + doTestFinish(); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/verifyChannelPrefsFile.js b/toolkit/mozapps/update/tests/unit_aus_update/verifyChannelPrefsFile.js new file mode 100644 index 0000000000..4cca77c73d --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/verifyChannelPrefsFile.js @@ -0,0 +1,38 @@ +/* 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/. + */ + +/** + * This test exists solely to ensure that channel-prefs.js is not changed. + * If it does get changed, it will cause a variation of Bug 1431342. + * To summarize, our updater doesn't update that file. But, on macOS, it is + * still used to compute the application's signature. This means that if Firefox + * updates and that file has been changed, the signature no will no longer + * validate. + */ + +const expectedChannelPrefsContents = `/* 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/. */ +// +// This pref is in its own file for complex reasons. See the comment in +// browser/app/Makefile.in, bug 756325, and bug 1431342 for details. Do not add +// other prefs to this file. + +pref("app.update.channel", "${UpdateUtils.UpdateChannel}"); +`; + +async function run_test() { + let channelPrefsFile = Services.dirsvc.get("GreD", Ci.nsIFile); + channelPrefsFile.append("defaults"); + channelPrefsFile.append("pref"); + channelPrefsFile.append("channel-prefs.js"); + + const contents = await IOUtils.readUTF8(channelPrefsFile.path); + Assert.equal( + contents, + expectedChannelPrefsContents, + "Channel Prefs file should should not change" + ); +} diff --git a/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini b/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini new file mode 100644 index 0000000000..bb0a575d72 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini @@ -0,0 +1,76 @@ +# 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/. + +[DEFAULT] +tags = appupdate +head = head_update.js +skip-if = + (os == 'win' && (ccov || msix)) # Our updater is disabled in MSIX builds +prefs = + app.update.staging.enabled=false +support-files = + ../data/shared.js + ../data/sharedUpdateXML.js + ../data/xpcshellUtilsAUS.js + ../data/app_update.sjs + testConstants.js + ../data/simple.mar + +[ausReadStrings.js] +[canCheckForAndCanApplyUpdates.js] +[urlConstruction.js] +skip-if = + socketprocess_networking # Bug 1759035 +[updateManagerXML.js] +[remoteUpdateXML.js] +skip-if = + socketprocess_networking # Bug 1759035 +[cleanupDownloadingForOlderAppVersion.js] +[cleanupDownloadingForDifferentChannel.js] +[cleanupDownloadingForSameVersionAndBuildID.js] +[cleanupDownloadingIncorrectStatus.js] +[cleanupPendingVersionFileIncorrectStatus.js] +[cleanupSuccessLogMove.js] +[cleanupSuccessLogsFIFO.js] +[downloadInterruptedOffline.js] +skip-if = + socketprocess_networking # Bug 1759035 +[downloadInterruptedNoRecovery.js] +skip-if = + socketprocess_networking # Bug 1759035 +[downloadInterruptedRecovery.js] +skip-if = + socketprocess_networking # Bug 1759035 +[downloadResumeForSameAppVersion.js] +[languagePackUpdates.js] +skip-if = + socketprocess_networking # Bug 1759035 +[updateSyncManager.js] +[updateAutoPrefMigrate.js] +skip-if = os != 'win' +reason = Update pref migration is currently Windows only +[updateDirectoryMigrate.js] +skip-if = os != 'win' +reason = Update directory migration is currently Windows only +[multiUpdate.js] +skip-if = + socketprocess_networking # Bug 1759035 +[perInstallationPrefs.js] +[onlyDownloadUpdatesThisSession.js] +skip-if = + socketprocess_networking # Bug 1759035 +[disableBackgroundUpdatesBackgroundTask.js] +skip-if = + socketprocess_networking # Bug 1759035 +[disableBackgroundUpdatesNonBackgroundTask.js] +skip-if = + socketprocess_networking # Bug 1759035 +[ensureExperimentToRolloutTransitionPerformed.js] +run-if = os == 'win' && appname == 'firefox' +reason = Feature is Firefox-specific and Windows-specific. +[verifyChannelPrefsFile.js] +run-if = appname == 'firefox' +reason = File being verified is Firefox-specific. +[backgroundUpdateTaskInternalUpdater.js] +[updateCheckCombine.js] diff --git a/toolkit/mozapps/update/tests/unit_background_update/head.js b/toolkit/mozapps/update/tests/unit_background_update/head.js new file mode 100644 index 0000000000..089fe32977 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_background_update/head.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from ../data/xpcshellUtilsAUS.js */ +load("xpcshellUtilsAUS.js"); +gIsServiceTest = false; + +const { BackgroundTasksTestUtils } = ChromeUtils.import( + "resource://testing-common/BackgroundTasksTestUtils.jsm" +); +BackgroundTasksTestUtils.init(this); +const do_backgroundtask = BackgroundTasksTestUtils.do_backgroundtask.bind( + BackgroundTasksTestUtils +); +const setupProfileService = BackgroundTasksTestUtils.setupProfileService.bind( + BackgroundTasksTestUtils +); diff --git a/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_exitcodes.js b/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_exitcodes.js new file mode 100644 index 0000000000..7ce3f27d66 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_exitcodes.js @@ -0,0 +1,80 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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"; + +// This test exercises functionality and also ensures the exit codes, +// which are a public API, do not change over time. +const { EXIT_CODE } = ChromeUtils.import( + "resource://gre/modules/BackgroundUpdate.jsm" +).BackgroundUpdate; + +setupProfileService(); + +// Ensure launched background tasks don't see this xpcshell as a concurrent +// instance. +let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager +); +let lockFile = do_get_profile(); +lockFile.append("customExePath"); +lockFile.append("customExe"); +syncManager.resetLock(lockFile); + +add_task(async function test_default_profile_does_not_exist() { + // Pretend there's no default profile. + let exitCode = await do_backgroundtask("backgroundupdate", { + extraEnv: { + MOZ_BACKGROUNDTASKS_NO_DEFAULT_PROFILE: "1", + }, + }); + Assert.equal(EXIT_CODE.DEFAULT_PROFILE_DOES_NOT_EXIST, exitCode); + Assert.equal(11, exitCode); +}); + +add_task(async function test_default_profile_cannot_be_locked() { + // Now, lock the default profile. + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let file = do_get_profile(); + file.append("profile_cannot_be_locked"); + + let profile = profileService.createUniqueProfile( + file, + "test_default_profile" + ); + let lock = profile.lock({}); + + try { + let exitCode = await do_backgroundtask("backgroundupdate", { + extraEnv: { + MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH: lock.directory.path, + }, + }); + Assert.equal(EXIT_CODE.DEFAULT_PROFILE_CANNOT_BE_LOCKED, exitCode); + Assert.equal(12, exitCode); + } finally { + lock.unlock(); + } +}); + +add_task(async function test_default_profile_cannot_be_read() { + // Finally, provide an empty default profile, one without prefs. + let file = do_get_profile(); + file.append("profile_cannot_be_read"); + + await IOUtils.makeDirectory(file.path); + + let exitCode = await do_backgroundtask("backgroundupdate", { + extraEnv: { + MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH: file.path, + }, + }); + Assert.equal(EXIT_CODE.DEFAULT_PROFILE_CANNOT_BE_READ, exitCode); + Assert.equal(13, exitCode); +}); diff --git a/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_glean.js b/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_glean.js new file mode 100644 index 0000000000..4a7d1502a5 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_glean.js @@ -0,0 +1,222 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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 { ASRouterTargeting } = ChromeUtils.import( + "resource://activity-stream/lib/ASRouterTargeting.jsm" +); + +const { BackgroundUpdate } = ChromeUtils.import( + "resource://gre/modules/BackgroundUpdate.jsm" +); + +const { maybeSubmitBackgroundUpdatePing } = ChromeUtils.import( + "resource://gre/modules/backgroundtasks/BackgroundTask_backgroundupdate.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "UpdateService", + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService" +); + +add_setup(function test_setup() { + // FOG needs a profile directory to put its data in. + do_get_profile(); + + // We need to initialize it once, otherwise operations will be stuck in the pre-init queue. + Services.fog.initializeFOG(); + + setupProfileService(); +}); + +add_task(async function test_record_update_environment() { + await BackgroundUpdate.recordUpdateEnvironment(); + + let pingSubmitted = false; + let appUpdateAutoEnabled = await UpdateUtils.getAppUpdateAutoEnabled(); + let backgroundUpdateEnabled = await UpdateUtils.readUpdateConfigSetting( + "app.update.background.enabled" + ); + GleanPings.backgroundUpdate.testBeforeNextSubmit(reason => { + Assert.equal(reason, "backgroundupdate_task"); + + pingSubmitted = true; + Assert.equal( + Services.prefs.getBoolPref("app.update.service.enabled", false), + Glean.update.serviceEnabled.testGetValue() + ); + + Assert.equal( + appUpdateAutoEnabled, + Glean.update.autoDownload.testGetValue() + ); + + Assert.equal( + backgroundUpdateEnabled, + Glean.update.backgroundUpdate.testGetValue() + ); + + Assert.equal( + UpdateUtils.UpdateChannel, + Glean.update.channel.testGetValue() + ); + Assert.equal( + !Services.policies || Services.policies.isAllowed("appUpdate"), + Glean.update.enabled.testGetValue() + ); + + Assert.equal( + UpdateService.canUsuallyApplyUpdates, + Glean.update.canUsuallyApplyUpdates.testGetValue() + ); + Assert.equal( + UpdateService.canUsuallyCheckForUpdates, + Glean.update.canUsuallyCheckForUpdates.testGetValue() + ); + Assert.equal( + UpdateService.canUsuallyStageUpdates, + Glean.update.canUsuallyStageUpdates.testGetValue() + ); + Assert.equal( + UpdateService.canUsuallyUseBits, + Glean.update.canUsuallyUseBits.testGetValue() + ); + }); + + // There's nothing async in this function atm, but it's annotated async, so.. + await maybeSubmitBackgroundUpdatePing(); + + ok(pingSubmitted, "'background-update' ping was submitted"); +}); + +async function do_readTargeting(content, beforeNextSubmitCallback) { + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let file = do_get_profile(); + file.append("profile_cannot_be_locked"); + + let profile = profileService.createUniqueProfile( + file, + "test_default_profile" + ); + + let targetingSnapshot = profile.rootDir.clone(); + targetingSnapshot.append("targeting.snapshot.json"); + + if (content) { + await IOUtils.writeUTF8(targetingSnapshot.path, content); + } + + let lock = profile.lock({}); + + Services.fog.testResetFOG(); + try { + await BackgroundUpdate.readFirefoxMessagingSystemTargetingSnapshot(lock); + } finally { + lock.unlock(); + } + + let pingSubmitted = false; + GleanPings.backgroundUpdate.testBeforeNextSubmit(reason => { + pingSubmitted = true; + return beforeNextSubmitCallback(reason); + }); + + // There's nothing async in this function atm, but it's annotated async, so.. + await maybeSubmitBackgroundUpdatePing(); + + ok(pingSubmitted, "'background-update' ping was submitted"); +} + +// Missing targeting is anticipated. +add_task(async function test_targeting_missing() { + await do_readTargeting(null, reason => { + Assert.equal(false, Glean.backgroundUpdate.targetingExists.testGetValue()); + + Assert.equal( + false, + Glean.backgroundUpdate.targetingException.testGetValue() + ); + }); +}); + +// Malformed JSON yields an exception. +add_task(async function test_targeting_exception() { + await do_readTargeting("{", reason => { + Assert.equal(false, Glean.backgroundUpdate.targetingExists.testGetValue()); + + Assert.equal( + true, + Glean.backgroundUpdate.targetingException.testGetValue() + ); + }); +}); + +// Well formed targeting values are reflected into the Glean telemetry. +add_task(async function test_targeting_exists() { + // We can't take a full environment snapshot under `xpcshell`; these are just + // the items we need. + let target = { + currentDate: ASRouterTargeting.Environment.currentDate, + profileAgeCreated: ASRouterTargeting.Environment.profileAgeCreated, + firefoxVersion: ASRouterTargeting.Environment.firefoxVersion, + }; + let targetSnapshot = await ASRouterTargeting.getEnvironmentSnapshot(target); + + await do_readTargeting(JSON.stringify(targetSnapshot), reason => { + Assert.equal(true, Glean.backgroundUpdate.targetingExists.testGetValue()); + + Assert.equal( + false, + Glean.backgroundUpdate.targetingException.testGetValue() + ); + + // `environment.firefoxVersion` is a positive integer. + Assert.ok( + Glean.backgroundUpdate.targetingEnvFirefoxVersion.testGetValue() > 0 + ); + + Assert.equal( + targetSnapshot.environment.firefoxVersion, + Glean.backgroundUpdate.targetingEnvFirefoxVersion.testGetValue() + ); + + let profileAge = Glean.backgroundUpdate.targetingEnvProfileAge.testGetValue(); + + Assert.ok(profileAge instanceof Date); + Assert.ok(0 < profileAge.getTime()); + Assert.ok(profileAge.getTime() < Date.now()); + + // `environment.profileAgeCreated` is an integer, milliseconds since the + // Unix epoch. + let targetProfileAge = new Date( + targetSnapshot.environment.profileAgeCreated + ); + // Our `time_unit: day` has Glean round to the nearest day *in the local + // timezone*, so we must do the same. + targetProfileAge.setHours(0, 0, 0, 0); + + Assert.equal(targetProfileAge.toISOString(), profileAge.toISOString()); + + let currentDate = Glean.backgroundUpdate.targetingEnvCurrentDate.testGetValue(); + + Assert.ok(0 < currentDate.getTime()); + Assert.ok(currentDate.getTime() < Date.now()); + + // `environment.currentDate` is in ISO string format. + let targetCurrentDate = new Date(targetSnapshot.environment.currentDate); + // Our `time_unit: day` has Glean round to the nearest day *in the local + // timezone*, so we must do the same. + targetCurrentDate.setHours(0, 0, 0, 0); + + Assert.equal(targetCurrentDate.toISOString(), currentDate.toISOString()); + }); +}); diff --git a/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_reason_schedule.js b/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_reason_schedule.js new file mode 100644 index 0000000000..61b1f57252 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_reason_schedule.js @@ -0,0 +1,116 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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 { BackgroundUpdate } = ChromeUtils.import( + "resource://gre/modules/BackgroundUpdate.jsm" +); +let reasons = () => BackgroundUpdate._reasonsToNotScheduleUpdates(); +let REASON = BackgroundUpdate.REASON; + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); +const { ExtensionTestUtils } = ChromeUtils.import( + "resource://testing-common/ExtensionXPCShellUtils.jsm" +); + +setupProfileService(); + +// Setup that allows to install a langpack. +ExtensionTestUtils.init(this); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +add_task( + { + skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS, + }, + async function test_reasons_schedule_langpacks() { + await AddonTestUtils.promiseStartupManager(); + + Services.prefs.setBoolPref("app.update.langpack.enabled", true); + + let result = await reasons(); + Assert.ok( + !result.includes(REASON.LANGPACK_INSTALLED), + "Reasons does not include LANGPACK_INSTALLED" + ); + + // Install a langpack. + let langpack = { + "manifest.json": { + name: "test Language Pack", + version: "1.0", + manifest_version: 2, + browser_specific_settings: { + gecko: { + id: "@test-langpack", + strict_min_version: "42.0", + strict_max_version: "42.0", + }, + }, + langpack_id: "fr", + languages: { + fr: { + chrome_resources: { + global: "chrome/fr/locale/fr/global/", + }, + version: "20171001190118", + }, + }, + sources: { + browser: { + base_path: "browser/", + }, + }, + }, + }; + + await Promise.all([ + TestUtils.topicObserved("webextension-langpack-startup"), + AddonTestUtils.promiseInstallXPI(langpack), + ]); + + result = await reasons(); + Assert.ok( + result.includes(REASON.LANGPACK_INSTALLED), + "Reasons include LANGPACK_INSTALLED" + ); + + // Now turn off langpack updating. + Services.prefs.setBoolPref("app.update.langpack.enabled", false); + + result = await reasons(); + Assert.ok( + !result.includes(REASON.LANGPACK_INSTALLED), + "Reasons does not include LANGPACK_INSTALLED" + ); + } +); + +add_task( + { + skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS, + }, + async function test_reasons_schedule_default_profile() { + // It's difficult to arrange a default profile in a testing environment, so + // this is not as thorough as we'd like. + let result = await reasons(); + + Assert.ok(result.includes(REASON.NO_DEFAULT_PROFILE_EXISTS)); + Assert.ok(result.includes(REASON.NOT_DEFAULT_PROFILE)); + } +); diff --git a/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_reason_update.js b/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_reason_update.js new file mode 100644 index 0000000000..81b7086401 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_reason_update.js @@ -0,0 +1,199 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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 { BackgroundUpdate } = ChromeUtils.import( + "resource://gre/modules/BackgroundUpdate.jsm" +); +let reasons = () => BackgroundUpdate._reasonsToNotUpdateInstallation(); +let REASON = BackgroundUpdate.REASON; +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); +const { UpdateService } = ChromeUtils.import( + "resource://gre/modules/UpdateService.jsm" +); + +const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + +// We can't reasonably check NO_MOZ_BACKGROUNDTASKS, nor NO_OMNIJAR. + +// These tests use per-installation prefs, and those are a shared resource, so +// they require some non-trivial setup. +setupTestCommon(null); +standardInit(); + +function setup_enterprise_policy_testing() { + // This initializes the policy engine for xpcshell tests + let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService( + Ci.nsIObserver + ); + policies.observe(null, "policies-startup", null); +} +setup_enterprise_policy_testing(); + +async function setupPolicyEngineWithJson(json, customSchema) { + if (typeof json != "object") { + let filePath = do_get_file(json ? json : "non-existing-file.json").path; + return EnterprisePolicyTesting.setupPolicyEngineWithJson( + filePath, + customSchema + ); + } + return EnterprisePolicyTesting.setupPolicyEngineWithJson(json, customSchema); +} + +add_task(async function test_reasons_update_no_app_update_auto() { + let prev = await UpdateUtils.getAppUpdateAutoEnabled(); + try { + await UpdateUtils.setAppUpdateAutoEnabled(false); + let result = await reasons(); + Assert.ok(result.includes(REASON.NO_APP_UPDATE_AUTO)); + + await UpdateUtils.setAppUpdateAutoEnabled(true); + result = await reasons(); + Assert.ok(!result.includes(REASON.NO_APP_UPDATE_AUTO)); + } finally { + await UpdateUtils.setAppUpdateAutoEnabled(prev); + } +}); + +add_task(async function test_reasons_update_no_app_update_background_enabled() { + let prev = await UpdateUtils.readUpdateConfigSetting( + "app.update.background.enabled" + ); + try { + await UpdateUtils.writeUpdateConfigSetting( + "app.update.background.enabled", + false + ); + let result = await reasons(); + Assert.ok(result.includes(REASON.NO_APP_UPDATE_BACKGROUND_ENABLED)); + + await UpdateUtils.writeUpdateConfigSetting( + "app.update.background.enabled", + true + ); + result = await reasons(); + Assert.ok(!result.includes(REASON.NO_APP_UPDATE_BACKGROUND_ENABLED)); + } finally { + await UpdateUtils.writeUpdateConfigSetting( + "app.update.background.enabled", + prev + ); + } +}); + +add_task(async function test_reasons_update_cannot_usually_check() { + // It's difficult to arrange the conditions in a testing environment, so + // we'll use mocks to get a little assurance. + let result = await reasons(); + Assert.ok(!result.includes(REASON.CANNOT_USUALLY_CHECK)); + + let sandbox = sinon.createSandbox(); + try { + sandbox + .stub(UpdateService.prototype, "canUsuallyCheckForUpdates") + .get(() => false); + result = await reasons(); + Assert.ok(result.includes(REASON.CANNOT_USUALLY_CHECK)); + } finally { + sandbox.restore(); + } +}); + +add_task(async function test_reasons_update_can_usually_stage_or_appl() { + // It's difficult to arrange the conditions in a testing environment, so + // we'll use mocks to get a little assurance. + let sandbox = sinon.createSandbox(); + try { + sandbox + .stub(UpdateService.prototype, "canUsuallyStageUpdates") + .get(() => true); + sandbox + .stub(UpdateService.prototype, "canUsuallyApplyUpdates") + .get(() => true); + let result = await reasons(); + Assert.ok( + !result.includes(REASON.CANNOT_USUALLY_STAGE_AND_CANNOT_USUALLY_APPLY) + ); + + sandbox + .stub(UpdateService.prototype, "canUsuallyStageUpdates") + .get(() => false); + sandbox + .stub(UpdateService.prototype, "canUsuallyApplyUpdates") + .get(() => false); + result = await reasons(); + Assert.ok( + result.includes(REASON.CANNOT_USUALLY_STAGE_AND_CANNOT_USUALLY_APPLY) + ); + } finally { + sandbox.restore(); + } +}); + +add_task( + { + skip_if: () => + !AppConstants.MOZ_BITS_DOWNLOAD || AppConstants.platform != "win", + }, + async function test_reasons_update_can_usually_use_bits() { + let prev = Services.prefs.getBoolPref("app.update.BITS.enabled"); + + // Here we use mocks to "get by" preconditions that are not + // satisfied in the testing environment. + let sandbox = sinon.createSandbox(); + try { + sandbox + .stub(UpdateService.prototype, "canUsuallyStageUpdates") + .get(() => true); + sandbox + .stub(UpdateService.prototype, "canUsuallyApplyUpdates") + .get(() => true); + + Services.prefs.setBoolPref("app.update.BITS.enabled", false); + let result = await reasons(); + Assert.ok(result.includes(REASON.WINDOWS_CANNOT_USUALLY_USE_BITS)); + + Services.prefs.setBoolPref("app.update.BITS.enabled", true); + result = await reasons(); + Assert.ok(!result.includes(REASON.WINDOWS_CANNOT_USUALLY_USE_BITS)); + } finally { + sandbox.restore(); + Services.prefs.setBoolPref("app.update.BITS.enabled", prev); + } + } +); + +add_task(async function test_reasons_update_manual_update_only() { + await setupPolicyEngineWithJson({ + policies: { + ManualAppUpdateOnly: true, + }, + }); + Assert.equal( + Services.policies.status, + Ci.nsIEnterprisePolicies.ACTIVE, + "Engine is active" + ); + + let result = await reasons(); + Assert.ok(result.includes(REASON.MANUAL_UPDATE_ONLY)); + + await setupPolicyEngineWithJson({}); + + result = await reasons(); + Assert.ok(!result.includes(REASON.MANUAL_UPDATE_ONLY)); +}); + +add_task(() => { + // `setupTestCommon()` calls `do_test_pending()`; this calls + // `do_test_finish()`. The `add_task` schedules this to run after all the + // other tests have completed. + doTestFinish(); +}); diff --git a/toolkit/mozapps/update/tests/unit_background_update/xpcshell.ini b/toolkit/mozapps/update/tests/unit_background_update/xpcshell.ini new file mode 100644 index 0000000000..dff65608e9 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_background_update/xpcshell.ini @@ -0,0 +1,23 @@ +# 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/. + +[DEFAULT] +firefox-appdir = browser +skip-if = + toolkit == 'android' + (os == 'win' && msix) # Our updater is disabled in MSIX builds +head = head.js +support-files = + ../data/shared.js + ../data/sharedUpdateXML.js + ../data/xpcshellUtilsAUS.js + +[test_backgroundupdate_exitcodes.js] +run-sequentially = very high failure rate in parallel +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + +[test_backgroundupdate_glean.js] +[test_backgroundupdate_reason_update.js] +[test_backgroundupdate_reason_schedule.js] diff --git a/toolkit/mozapps/update/tests/unit_base_updater/head_update.js b/toolkit/mozapps/update/tests/unit_base_updater/head_update.js new file mode 100644 index 0000000000..1ab1a70a0b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/head_update.js @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from ../data/xpcshellUtilsAUS.js */ +load("xpcshellUtilsAUS.js"); +gIsServiceTest = false; diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js new file mode 100644 index 0000000000..cc61a75dd7 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js @@ -0,0 +1,32 @@ +/* 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/. + */ + +/* Callback file not in install directory or a sub-directory of the install + directory failure */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = STATE_FAILED_INVALID_CALLBACK_DIR_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = getTestDirFile(FILE_HELPER_BIN).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, null, path); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_CALLBACK_DIR_ERROR, + 1 + ); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js new file mode 100644 index 0000000000..0b432e2e15 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js @@ -0,0 +1,39 @@ +/* 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/. + */ + +/* Too long callback file path failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = STATE_FAILED_INVALID_CALLBACK_PATH_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = "123456789"; + if (AppConstants.platform == "win") { + path = "\\" + path; + path = path.repeat(30); // 300 characters + path = "C:" + path; + } else { + path = "/" + path; + path = path.repeat(1000); // 10000 characters + } + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, null, path); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_CALLBACK_PATH_ERROR, + 1 + ); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js new file mode 100644 index 0000000000..c2746e2bd1 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js @@ -0,0 +1,53 @@ +/* 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/. + */ + +/* Too long install directory path failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR + : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = "123456789"; + if (AppConstants.platform == "win") { + path = "\\" + path; + path = path.repeat(30); // 300 characters + path = "C:" + path; + } else { + path = "/" + path; + path = path.repeat(1000); // 10000 characters + } + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_INSTALL_DIR_PATH_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js new file mode 100644 index 0000000000..b611fac972 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js @@ -0,0 +1,50 @@ +/* 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/. + */ + +/* Install directory path traversal failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR + : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = "123456789"; + if (AppConstants.platform == "win") { + path = "C:\\" + path + "\\..\\" + path; + } else { + path = "/" + path + "/../" + path; + } + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_INSTALL_DIR_PATH_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js new file mode 100644 index 0000000000..0506d100fd --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js @@ -0,0 +1,45 @@ +/* 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/. + */ + +/* Different install and working directories for a regular update failure */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR + : STATE_FAILED_INVALID_APPLYTO_DIR_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = getApplyDirFile("..", false).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_APPLYTO_DIR_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js new file mode 100644 index 0000000000..cf053e3ff5 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js @@ -0,0 +1,32 @@ +/* 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/. + */ + +/* Patch directory path traversal failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = getUpdateDirFile(DIR_PATCH); + if (AppConstants.platform == "win") { + path = path + "\\..\\"; + } else { + path = path + "/../"; + } + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, path, null, null, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js new file mode 100644 index 0000000000..4624179bfd --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js @@ -0,0 +1,45 @@ +/* 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/. + */ + +/* Different install and working directories for a regular update failure */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR + : STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = getApplyDirFile("..", false).path; + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true, null, null, path, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_APPLYTO_DIR_STAGED_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js new file mode 100644 index 0000000000..7c0af26e37 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js @@ -0,0 +1,45 @@ +/* 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/. + */ + +/* Working directory path local UNC failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR + : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = "\\\\.\\" + getApplyDirFile(null, false).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_WORKING_DIR_PATH_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js new file mode 100644 index 0000000000..cfbd9eaec9 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js @@ -0,0 +1,44 @@ +/* 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/. + */ + +/* Relative working directory path failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR + : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, "test", null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_WORKING_DIR_PATH_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js new file mode 100644 index 0000000000..a845cb70c5 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + createUpdateInProgressLockFile(getGREBinDir()); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, false); + removeUpdateInProgressLockFile(getGREBinDir()); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(PERFORMING_STAGED_UPDATE); + checkUpdateLogContains(ERR_UPDATE_IN_PROGRESS); + await waitForUpdateXMLFiles(true, false); + checkUpdateManager(STATE_AFTER_STAGE, true, STATE_AFTER_STAGE, 0, 0); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js new file mode 100644 index 0000000000..319aee8e34 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + lockDirectory(getGREBinDir().path); + // Switch the application to the staged application that was updated. + await runUpdateUsingApp(STATE_SUCCEEDED); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + + let updatesDir = getUpdateDirFile(DIR_PATCH); + Assert.ok( + updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path) + ); + + let log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSkippedWriteAccess_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSkippedWriteAccess_win.js new file mode 100644 index 0000000000..8a809d8c3e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSkippedWriteAccess_win.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test an update isn't attempted when the update.status file can't be written + * to. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + + // To simulate a user that doesn't have write access to the update directory + // lock the relevant files in the update directory. + let filesToLock = [ + FILE_ACTIVE_UPDATE_XML, + FILE_UPDATE_MAR, + FILE_UPDATE_STATUS, + FILE_UPDATE_TEST, + FILE_UPDATE_VERSION, + ]; + filesToLock.forEach(function(aFileLeafName) { + let file = getUpdateDirFile(aFileLeafName); + if (!file.exists()) { + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o444); + } + file.QueryInterface(Ci.nsILocalFileWin); + file.readOnly = true; + Assert.ok(file.exists(), MSG_SHOULD_EXIST + getMsgPath(file.path)); + Assert.ok(!file.isWritable(), "the file should not be writeable"); + }); + + registerCleanupFunction(() => { + filesToLock.forEach(function(aFileLeafName) { + let file = getUpdateDirFile(aFileLeafName); + if (file.exists()) { + file.QueryInterface(Ci.nsILocalFileWin); + file.readOnly = false; + file.remove(false); + } + }); + }); + + // Reload the update manager now that the update directory files are locked. + reloadUpdateManagerData(); + await runUpdateUsingApp(STATE_PENDING); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateManager(STATE_PENDING, false, STATE_NONE, 0, 0); + + let dir = getUpdateDirFile(DIR_PATCH); + Assert.ok(dir.exists(), MSG_SHOULD_EXIST + getMsgPath(dir.path)); + + let file = getUpdateDirFile(FILE_UPDATES_XML); + Assert.ok(!file.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(file.path)); + + file = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!file.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(file.path)); + + file = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(!file.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(file.path)); + + file = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!file.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(file.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js new file mode 100644 index 0000000000..3dfad0f58e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test a replace request for a staged update with a version file that specifies + * an older version failure. The same check is used in nsUpdateDriver.cpp for + * all update types which is why there aren't tests for the maintenance service + * as well as for other update types. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, null, "", false); + let patchProps = { state: STATE_AFTER_STAGE }; + let patches = getLocalPatchString(patchProps); + let updateProps = { appVersion: "0.9" }; + let updates = getLocalUpdateString(updateProps, patches); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + getUpdateDirFile(FILE_UPDATE_LOG).create( + Ci.nsIFile.NORMAL_FILE_TYPE, + PERMS_FILE + ); + writeStatusFile(STATE_AFTER_STAGE); + // Create the version file with an older version to simulate installing a new + // version of the application while there is an update that has been staged. + writeVersionFile("0.9"); + // Try to switch the application to the fake staged application. + await runUpdateUsingApp(STATE_AFTER_STAGE); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + ERR_OLDER_VERSION_OR_SAME_BUILD, + 1 + ); + + let updatesDir = getUpdateDirFile(DIR_PATCH); + Assert.ok( + updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path) + ); + + let log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js new file mode 100644 index 0000000000..34b47866b1 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, true); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + await runUpdateUsingApp(STATE_SUCCEEDED); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + + let updatesDir = getUpdateDirFile(DIR_PATCH); + Assert.ok( + updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path) + ); + + let log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js new file mode 100644 index 0000000000..980b0cb89a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by launching an application to apply it. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + // The third parameter will test that a full path to the post update binary + // doesn't execute. + await setupUpdaterTest( + FILE_COMPLETE_MAR, + undefined, + getApplyDirFile(null, true).path + "/" + ); + await runUpdateUsingApp(STATE_SUCCEEDED); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + + let updatesDir = getUpdateDirFile(DIR_PATCH); + Assert.ok( + updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path) + ); + + let log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseBackgroundTaskFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseBackgroundTaskFailure_win.js new file mode 100644 index 0000000000..bcb24bee94 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseBackgroundTaskFailure_win.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Fail to apply a complete MAR when the application is in use and the callback is a background task. */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + + // Add a dummy --backgroundtask arg; this will have no effect on the + // callback (TestAUSHelper) but will cause the updater to detect + // that this is a background task. + gCallbackArgs = gCallbackArgs.concat(["--backgroundtask", "not_found"]); + + // Run the update with the helper file in use, expecting failure. + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); + runUpdate( + STATE_FAILED_WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION, + false, // aSwitchApp + 1, // aExpectedExitValue + true // aCheckSvcLog + ); + await waitForHelperExit(); + + standardInit(); + + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_BGTASK_EXCLUSIVE); + + // Check that the update was reset to "pending". + await waitForUpdateXMLFiles( + true, // aActiveUpdateExists + false // aUpdatesExists + ); + checkUpdateManager( + STATE_PENDING, // aStatusFileState + true, // aHasActiveUpdate + STATE_PENDING, // aUpdateStatusState + WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION, + 0 // aUpdateCount + ); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js new file mode 100644 index 0000000000..502561ed1e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js new file mode 100644 index 0000000000..29c2c2a30e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file staged patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, true); + setupSymLinks(); + await runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + checkSymLinks(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} + +/** + * Setup symlinks for the test. + */ +function setupSymLinks() { + if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") { + removeSymlink(); + createSymlink(); + registerCleanupFunction(removeSymlink); + gTestFiles.splice(gTestFiles.length - 3, 0, { + description: "Readable symlink", + fileName: "link", + relPathDir: DIR_RESOURCES, + originalContents: "test", + compareContents: "test", + originalFile: null, + compareFile: null, + originalPerms: 0o666, + comparePerms: 0o666, + }); + } +} + +/** + * Checks the state of the symlinks for the test. + */ +function checkSymLinks() { + if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") { + checkSymlink(); + } +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js new file mode 100644 index 0000000000..e08777d042 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js new file mode 100644 index 0000000000..2f8155c790 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file staged patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + gCallbackBinFile = "exe0.exe"; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js new file mode 100644 index 0000000000..04c72896b4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file staged patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + gCallbackBinFile = "exe0.exe"; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js new file mode 100644 index 0000000000..fa13f07f46 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + gCallbackBinFile = "exe0.exe"; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js new file mode 100644 index 0000000000..e74509c06e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + gCallbackBinFile = "exe0.exe"; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackUmask_unix.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackUmask_unix.js new file mode 100644 index 0000000000..be3b3707b4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackUmask_unix.js @@ -0,0 +1,42 @@ +/* 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/. + */ + +/* Verify that app callback is launched with the same umask as was set + * before applying an update. */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + preventDistributionFiles(); + + // Our callback is `TestAUSHelper check-umask <umask from before updating>`. + // Including the umask from before updating as an argument allows to re-use + // the callback log checking code below. The argument is also used as the log + // file name, so we prefix it with "umask" so that it doesn't clash with + // numericfile and directory names in the update data. In particular, "2" + // clashes with an existing directory name in the update data, leading to + // failing tests. + let umask = Services.sysinfo.getProperty("umask"); + gCallbackArgs = ["check-umask", `umask-${umask}`]; + + await setupUpdaterTest(FILE_COMPLETE_MAR, true); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, false, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + + // This compares the callback arguments given, including the umask before + // updating, to the umask set when the app callback is launched. They should + // be the same. + checkCallbackLog(getApplyDirFile(DIR_RESOURCES + "callback_app.log")); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js new file mode 100644 index 0000000000..de8db067bc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js @@ -0,0 +1,39 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Failure Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + // If execv is used the updater process will turn into the callback process + // and the updater's return code will be that of the callback process. + runUpdate( + STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE, + false, + USE_EXECV ? 0 : 1, + true + ); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_FAILURE); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + LOADSOURCE_ERROR_WRONG_SIZE, + 1 + ); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js new file mode 100644 index 0000000000..b93b023934 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse( + gTestFiles[13].relPathDir + gTestFiles[13].fileName, + false + ); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js new file mode 100644 index 0000000000..b41da12396 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperFileInUse( + gTestFiles[11].relPathDir + gTestFiles[11].fileName, + false + ); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js new file mode 100644 index 0000000000..4b946ac3e4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse( + gTestFiles[13].relPathDir + gTestFiles[13].fileName, + false + ); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js new file mode 100644 index 0000000000..15c3a1121a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperFileInUse( + gTestFiles[11].relPathDir + gTestFiles[11].fileName, + false + ); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js new file mode 100644 index 0000000000..698ccb7fe5 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperLockFile(gTestFiles[3]); + runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_BACKUP_CREATE_7); + checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(true, false); + checkUpdateManager(STATE_PENDING, true, STATE_PENDING, WRITE_ERROR, 0); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js new file mode 100644 index 0000000000..c8c019ec5c --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperLockFile(gTestFiles[2]); + runUpdate(STATE_FAILED_READ_ERROR, false, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_UNABLE_OPEN_DEST); + checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_FAILED, READ_ERROR, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js new file mode 100644 index 0000000000..7b582dbd45 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperLockFile(gTestFiles[3]); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + // Files aren't checked after staging since this test locks a file which + // prevents reading the file. + checkUpdateLogContains(ERR_ENSURE_COPY); + // Switch the application to the staged application that was updated. + runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, false); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_BACKUP_CREATE_7); + checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(true, false); + checkUpdateManager(STATE_PENDING, true, STATE_PENDING, WRITE_ERROR, 0); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js new file mode 100644 index 0000000000..bf3abd8c37 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_PENDING; + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperLockFile(gTestFiles[2]); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + // Files aren't checked after staging since this test locks a file which + // prevents reading the file. + checkUpdateLogContains(ERR_ENSURE_COPY); + // Switch the application to the staged application that was updated. + runUpdate(STATE_FAILED_READ_ERROR, false, 1, false); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_UNABLE_OPEN_DEST); + checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_FAILED, READ_ERROR, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettings.js b/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettings.js new file mode 100644 index 0000000000..b0a0cfe657 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettings.js @@ -0,0 +1,42 @@ +/* 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/. + */ + +/* Test update-settings.ini missing channel MAR security check */ + +async function run_test() { + if (!MOZ_VERIFY_MAR_SIGNATURE) { + return; + } + + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[gTestFiles.length - 2].originalContents = null; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + // If execv is used the updater process will turn into the callback process + // and the updater's return code will be that of the callback process. + runUpdate( + STATE_FAILED_UPDATE_SETTINGS_FILE_CHANNEL, + false, + USE_EXECV ? 0 : 1, + false + ); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(STATE_FAILED_UPDATE_SETTINGS_FILE_CHANNEL); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + UPDATE_SETTINGS_FILE_CHANNEL, + 1 + ); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettingsStage.js b/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettingsStage.js new file mode 100644 index 0000000000..e26d2aefc3 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettingsStage.js @@ -0,0 +1,35 @@ +/* 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/. + */ + +/* Test update-settings.ini missing channel MAR security check */ + +async function run_test() { + if (!MOZ_VERIFY_MAR_SIGNATURE) { + return; + } + + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_FAILED; + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[gTestFiles.length - 2].originalContents = null; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(STATE_FAILED_UPDATE_SETTINGS_FILE_CHANNEL); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + UPDATE_SETTINGS_FILE_CHANNEL, + 1 + ); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsSuccessComplete_win.js new file mode 100644 index 0000000000..4918fea140 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsSuccessComplete_win.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperPIDPersists(DIR_RESOURCES + gCallbackBinFile, false); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContains(ERR_PARENT_PID_PERSISTS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js new file mode 100644 index 0000000000..31c5b8bd7a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file staged patch apply failure + test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse( + gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0], + true + ); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js new file mode 100644 index 0000000000..b57f8c81b7 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file staged patch apply failure + test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperFileInUse( + gTestDirs[2].relPathDir + gTestDirs[2].files[0], + true + ); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js new file mode 100644 index 0000000000..0683df0d8d --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse( + gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0], + true + ); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js new file mode 100644 index 0000000000..d4da3a5f37 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperFileInUse( + gTestDirs[2].relPathDir + gTestDirs[2].files[0], + true + ); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js new file mode 100644 index 0000000000..a1a0de0fe4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js @@ -0,0 +1,31 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Failure Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_FAILED; + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_LOADSOURCEFILE_FAILED); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + LOADSOURCE_ERROR_WRONG_SIZE, + 1 + ); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js new file mode 100644 index 0000000000..943a45ba95 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js @@ -0,0 +1,71 @@ +/* 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/. + */ + +/* General Complete MAR File Staged Patch Apply Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsCompleteSuccess; + setupSymLinks(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + checkSymLinks(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} + +/** + * Setup symlinks for the test. + */ +function setupSymLinks() { + // Don't test symlinks on Mac OS X in this test since it tends to timeout. + // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js + if (AppConstants.platform == "linux") { + removeSymlink(); + createSymlink(); + registerCleanupFunction(removeSymlink); + gTestFiles.splice(gTestFiles.length - 3, 0, { + description: "Readable symlink", + fileName: "link", + relPathDir: DIR_RESOURCES, + originalContents: "test", + compareContents: "test", + originalFile: null, + compareFile: null, + originalPerms: 0o666, + comparePerms: 0o666, + }); + } +} + +/** + * Checks the state of the symlinks for the test. + */ +function checkSymLinks() { + // Don't test symlinks on Mac OS X in this test since it tends to timeout. + // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js + if (AppConstants.platform == "linux") { + checkSymlink(); + } +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js new file mode 100644 index 0000000000..dd5c240919 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js @@ -0,0 +1,35 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + preventDistributionFiles(); + await setupUpdaterTest(FILE_PARTIAL_MAR, true); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true, false, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js new file mode 100644 index 0000000000..2dd1e54f90 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js @@ -0,0 +1,26 @@ +/* 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/. + */ + +/* General Complete MAR File Patch Apply Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + preventDistributionFiles(); + await setupUpdaterTest(FILE_COMPLETE_MAR, true); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, false, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js new file mode 100644 index 0000000000..8e8e9d094a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js @@ -0,0 +1,29 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + // The third parameter will test that a relative path that contains a + // directory traversal to the post update binary doesn't execute. + await setupUpdaterTest(FILE_PARTIAL_MAR, false, "test/../"); + runUpdate(STATE_SUCCEEDED, false, 0, true); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartialWhileBackgroundTaskRunning.js b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartialWhileBackgroundTaskRunning.js new file mode 100644 index 0000000000..31e01998ca --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartialWhileBackgroundTaskRunning.js @@ -0,0 +1,125 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Test */ + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const { BackgroundTasksTestUtils } = ChromeUtils.import( + "resource://testing-common/BackgroundTasksTestUtils.jsm" +); +BackgroundTasksTestUtils.init(this); +const do_backgroundtask = BackgroundTasksTestUtils.do_backgroundtask.bind( + BackgroundTasksTestUtils +); + +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); + +async function run_test() { + // Without omnijars, the background task apparatus will fail to find task + // definitions. + { + let omniJa = Services.dirsvc.get("XCurProcD", Ci.nsIFile); + omniJa.append("omni.ja"); + if (!omniJa.exists()) { + Assert.ok( + false, + "This test requires a packaged build, " + + "run 'mach package' and then use 'mach xpcshell-test --xre-path=...'" + ); + return; + } + } + + if (!setupTestCommon()) { + return; + } + + // `channel-prefs.js` is required for Firefox to launch, including in + // background task mode. The testing partial MAR updates `channel-prefs.js` + // to have contents `FromPartial`, which is not a valid prefs file and causes + // Firefox to crash. However, `channel-prefs.js` is listed as `add-if-not + // "defaults/pref/channel-prefs.js" "defaults/pref/channel-prefs.js"`, so it + // won't be updated if it already exists. The manipulations below arrange a) + // for the file to exist and b) for the comparison afterward to succeed. + gTestFiles = gTestFilesPartialSuccess; + let channelPrefs = gTestFiles[gTestFiles.length - 1]; + Assert.equal("channel-prefs.js", channelPrefs.fileName); + let f = gGREDirOrig.clone(); + f.append("defaults"); + f.append("pref"); + f.append("channel-prefs.js"); + // `originalFile` is a relative path, so we can't just point to the one in the + // original GRE directory. + channelPrefs.originalFile = null; + channelPrefs.originalContents = readFile(f); + channelPrefs.compareContents = channelPrefs.originalContents; + gTestDirs = gTestDirsPartialSuccess; + // The third parameter will test that a relative path that contains a + // directory traversal to the post update binary doesn't execute. + await setupUpdaterTest(FILE_PARTIAL_MAR, false, "test/../", true, { + // We need packaged JavaScript to run background tasks. + requiresOmnijar: true, + }); + + // `0/00/00text2` is just a random file in the testing partial MAR that does + // not exist before the update and is always written by the update. + let exitCode; + exitCode = await do_backgroundtask("file_exists", { + extraArgs: [getApplyDirFile(DIR_RESOURCES + "0/00/00text2").path], + }); + // Before updating, file doesn't exist. + Assert.equal(11, exitCode); + + // This task will wait 10 seconds before exiting, which should overlap with + // the update below. We wait for some output from the wait background task, + // so that there is meaningful overlap. + let taskStarted = PromiseUtils.defer(); + let p = do_backgroundtask("wait", { + onStdoutLine: (line, proc) => { + // This sentinel seems pretty safe: it's printed by the task itself and so + // there should be a straight line between future test failures and + // logging changes. + if (line.includes("runBackgroundTask: wait")) { + taskStarted.resolve(proc); + } + }, + }); + let proc = await taskStarted.promise; + + runUpdate(STATE_SUCCEEDED, false, 0, true); + + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + + // Once we've seen what we want, there's no need to let the task wait complete. + await proc.kill(); + + Assert.ok("Waiting for background task to die after kill()"); + exitCode = await p; + + // Windows does not support killing processes gracefully, so they will + // always exit with -9 there. + let retVal = AppConstants.platform == "win" ? -9 : -15; + Assert.equal(retVal, exitCode); + + exitCode = await do_backgroundtask("file_exists", { + extraArgs: [getApplyDirFile(DIR_RESOURCES + "0/00/00text2").path], + }); + // After updating, file exists. + Assert.equal(0, exitCode); + + // This finishes the test, so must be last. + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js b/toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js new file mode 100644 index 0000000000..6f3e26fe0f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js @@ -0,0 +1,41 @@ +/* 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/. + */ + +/* Test version downgrade MAR security check */ + +async function run_test() { + if (!MOZ_VERIFY_MAR_SIGNATURE) { + return; + } + + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_OLD_VERSION_MAR, false); + // If execv is used the updater process will turn into the callback process + // and the updater's return code will be that of the callback process. + runUpdate( + STATE_FAILED_VERSION_DOWNGRADE_ERROR, + false, + USE_EXECV ? 0 : 1, + false + ); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(STATE_FAILED_VERSION_DOWNGRADE_ERROR); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + VERSION_DOWNGRADE_ERROR, + 1 + ); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js new file mode 100644 index 0000000000..e10c845f8b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js @@ -0,0 +1,47 @@ +/* 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/. + */ + +/* Test product/channel MAR security check */ + +async function run_test() { + if (!MOZ_VERIFY_MAR_SIGNATURE) { + return; + } + + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[ + gTestFiles.length - 2 + ].originalContents = UPDATE_SETTINGS_CONTENTS.replace( + "xpcshell-test", + "wrong-channel" + ); + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + // If execv is used the updater process will turn into the callback process + // and the updater's return code will be that of the callback process. + runUpdate( + STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR, + false, + USE_EXECV ? 0 : 1, + false + ); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + MAR_CHANNEL_MISMATCH_ERROR, + 1 + ); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannelStage.js b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannelStage.js new file mode 100644 index 0000000000..3092e67d71 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannelStage.js @@ -0,0 +1,40 @@ +/* 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/. + */ + +/* Test product/channel MAR security check */ + +async function run_test() { + if (!MOZ_VERIFY_MAR_SIGNATURE) { + return; + } + + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_FAILED; + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[ + gTestFiles.length - 2 + ].originalContents = UPDATE_SETTINGS_CONTENTS.replace( + "xpcshell-test", + "wrong-channel" + ); + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + MAR_CHANNEL_MISMATCH_ERROR, + 1 + ); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini b/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini new file mode 100644 index 0000000000..84325c4312 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini @@ -0,0 +1,136 @@ +# 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/. + +# Tests that require the updater binary. + +[DEFAULT] +tags = appupdate +head = head_update.js +skip-if = + (os == 'win' && (ccov || msix)) # Our updater is disabled in MSIX builds +support-files = + ../data/shared.js + ../data/sharedUpdateXML.js + ../data/xpcshellUtilsAUS.js + +[invalidArgCallbackFileNotInInstallDirFailure.js] +[invalidArgCallbackFilePathTooLongFailure.js] +[invalidArgInstallDirPathTooLongFailure.js] +[invalidArgInstallDirPathTraversalFailure.js] +[invalidArgInstallWorkingDirPathNotSameFailure_win.js] +skip-if = os != 'win' +reason = Windows only test +[invalidArgPatchDirPathTraversalFailure.js] +[invalidArgStageDirNotInInstallDirFailure_win.js] +skip-if = os != 'win' +reason = Windows only test +[invalidArgWorkingDirPathLocalUNCFailure_win.js] +skip-if = os != 'win' +reason = Windows only test +[invalidArgWorkingDirPathRelativeFailure.js] +[marSuccessComplete.js] +[marSuccessPartial.js] +[marSuccessPartialWhileBackgroundTaskRunning.js] +skip-if = + apple_silicon # Bug 1754931 + apple_catalina # Bug 1754931 +[marFailurePartial.js] +[marStageSuccessComplete.js] +skip-if = + apple_silicon # bug 1707753 + apple_catalina # Bug 1713329 +[marStageSuccessPartial.js] +skip-if = + apple_silicon # bug 1707753 + apple_catalina # Bug 1713329 +[marVersionDowngrade.js] +[marMissingUpdateSettings.js] +[marMissingUpdateSettingsStage.js] +[marWrongChannel.js] +[marWrongChannelStage.js] +[marStageFailurePartial.js] +[marCallbackAppSuccessComplete_win.js] +skip-if = os != 'win' +reason = Windows only test +[marCallbackAppSuccessPartial_win.js] +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 +[marCallbackUmask_unix.js] +skip-if = + os == 'win' # not a Windows test +reason = Unix only test +[marAppInUseBackgroundTaskFailure_win.js] +skip-if = os != 'win' +reason = Windows only test +[marAppInUseSuccessComplete.js] +[marAppInUseStageSuccessComplete_unix.js] +skip-if = + os == 'win' # not a Windows test + apple_silicon # bug 1707753 + apple_catalina # Bug 1713329 +[marAppInUseStageFailureComplete_win.js] +skip-if = os != 'win' +reason = Windows only test +[marFileLockedFailureComplete_win.js] +skip-if = os != 'win' +reason = Windows only test +[marFileLockedFailurePartial_win.js] +skip-if = os != 'win' +reason = Windows only test +[marFileLockedStageFailureComplete_win.js] +skip-if = os != 'win' +reason = Windows only test +[marFileLockedStageFailurePartial_win.js] +skip-if = os != 'win' +reason = Windows only test +[marFileInUseSuccessComplete_win.js] +skip-if = os != 'win' +reason = Windows only test +[marFileInUseSuccessPartial_win.js] +skip-if = os != 'win' +reason = Windows only test +[marRMRFDirFileInUseSuccessComplete_win.js] +skip-if = os != 'win' +reason = Windows only test +[marRMRFDirFileInUseSuccessPartial_win.js] +skip-if = os != 'win' +reason = Windows only test +[marFileInUseStageFailureComplete_win.js] +skip-if = os != 'win' +reason = Windows only test +[marFileInUseStageFailurePartial_win.js] +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] +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 +[marAppApplyUpdateSuccess.js] +skip-if = + apple_silicon # bug 1724579 +[marAppApplyUpdateStageSuccess.js] +skip-if = + apple_silicon # bug 1707753 + apple_catalina # Bug 1713329 +[marAppApplyUpdateStageOldVersionFailure.js] +[marAppApplyUpdateSkippedWriteAccess_win.js] +skip-if = os != 'win' +reason = Windows only test diff --git a/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js new file mode 100644 index 0000000000..57eb630248 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Bootstrap the tests using the service by installing our own version of the service */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + // We don't actually care if the MAR has any data, we only care about the + // application return code and update.status result. + gTestFiles = gTestFilesCommon; + gTestDirs = []; + await setupUpdaterTest(FILE_COMPLETE_MAR, null); + runUpdate(STATE_SUCCEEDED, false, 0, true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + + // We need to check the service log even though this is a bootstrap + // because the app bin could be in use by this test by the time the next + // test runs. + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js new file mode 100644 index 0000000000..d6e1753f35 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * We skip authenticode cert checks from the service udpates + * so that we can use updater-xpcshell with the wrong certs for testing. + * This tests that code path. */ + +function run_test() { + if (!IS_AUTHENTICODE_CHECK_ENABLED) { + return; + } + + let binDir = getGREBinDir(); + let maintenanceServiceBin = binDir.clone(); + maintenanceServiceBin.append(FILE_MAINTENANCE_SERVICE_BIN); + + let updaterBin = binDir.clone(); + updaterBin.append(FILE_UPDATER_BIN); + + debugDump( + "Launching maintenance service bin: " + + maintenanceServiceBin.path + + " to check updater: " + + updaterBin.path + + " signature." + ); + + // Bypass the manifest and run as invoker + Services.env.set("__COMPAT_LAYER", "RunAsInvoker"); + + let dummyInstallPath = "---"; + let maintenanceServiceBinArgs = [ + "check-cert", + dummyInstallPath, + updaterBin.path, + ]; + let maintenanceServiceBinProcess = Cc[ + "@mozilla.org/process/util;1" + ].createInstance(Ci.nsIProcess); + maintenanceServiceBinProcess.init(maintenanceServiceBin); + maintenanceServiceBinProcess.run( + true, + maintenanceServiceBinArgs, + maintenanceServiceBinArgs.length + ); + Assert.equal( + maintenanceServiceBinProcess.exitValue, + 0, + "the maintenance service exit value should be 0" + ); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/fallbackOnSvcFailure.js b/toolkit/mozapps/update/tests/unit_service_updater/fallbackOnSvcFailure.js new file mode 100644 index 0000000000..d09ea7b448 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/fallbackOnSvcFailure.js @@ -0,0 +1,38 @@ +/* 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/. + */ + +/** + * If updating with the maintenance service fails in a way specific to the + * maintenance service, we should fall back to not using the maintenance + * service, which should succeed. This test ensures that that happens as + * expected. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + // This variable forces the service to fail by having the updater pass it the + // wrong number of arguments. Then we can verify that the fallback happens + // properly. + gEnvForceServiceFallback = true; + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + preventDistributionFiles(); + await setupUpdaterTest(FILE_COMPLETE_MAR, true); + // It's very important that we pass in true for aCheckSvcLog (4th param), + // because otherwise we may not have used the service at all, so we wouldn't + // really check that we fell back (to not using the service) properly. + runUpdate(STATE_SUCCEEDED, false, 0, true); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/head_update.js b/toolkit/mozapps/update/tests/unit_service_updater/head_update.js new file mode 100644 index 0000000000..8d30c09e4f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/head_update.js @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from ../data/xpcshellUtilsAUS.js */ +load("xpcshellUtilsAUS.js"); +gIsServiceTest = true; diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js new file mode 100644 index 0000000000..c2746e2bd1 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js @@ -0,0 +1,53 @@ +/* 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/. + */ + +/* Too long install directory path failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR + : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = "123456789"; + if (AppConstants.platform == "win") { + path = "\\" + path; + path = path.repeat(30); // 300 characters + path = "C:" + path; + } else { + path = "/" + path; + path = path.repeat(1000); // 10000 characters + } + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_INSTALL_DIR_PATH_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js new file mode 100644 index 0000000000..b611fac972 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js @@ -0,0 +1,50 @@ +/* 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/. + */ + +/* Install directory path traversal failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR + : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = "123456789"; + if (AppConstants.platform == "win") { + path = "C:\\" + path + "\\..\\" + path; + } else { + path = "/" + path + "/../" + path; + } + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_INSTALL_DIR_PATH_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js new file mode 100644 index 0000000000..0506d100fd --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js @@ -0,0 +1,45 @@ +/* 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/. + */ + +/* Different install and working directories for a regular update failure */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR + : STATE_FAILED_INVALID_APPLYTO_DIR_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = getApplyDirFile("..", false).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_APPLYTO_DIR_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathSuffixFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathSuffixFailureSvc.js new file mode 100644 index 0000000000..3eb792ecb4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathSuffixFailureSvc.js @@ -0,0 +1,27 @@ +/* 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/. + */ + +/* Patch directory path must end with \updates\0 failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = getUpdateDirFile(DIR_PATCH).parent.path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, path, null, null, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js new file mode 100644 index 0000000000..cf053e3ff5 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js @@ -0,0 +1,32 @@ +/* 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/. + */ + +/* Patch directory path traversal failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = getUpdateDirFile(DIR_PATCH); + if (AppConstants.platform == "win") { + path = path + "\\..\\"; + } else { + path = path + "/../"; + } + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, path, null, null, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js new file mode 100644 index 0000000000..4624179bfd --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js @@ -0,0 +1,45 @@ +/* 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/. + */ + +/* Different install and working directories for a regular update failure */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR + : STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = getApplyDirFile("..", false).path; + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true, null, null, path, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_APPLYTO_DIR_STAGED_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js new file mode 100644 index 0000000000..7c0af26e37 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js @@ -0,0 +1,45 @@ +/* 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/. + */ + +/* Working directory path local UNC failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR + : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + let path = "\\\\.\\" + getApplyDirFile(null, false).path; + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_WORKING_DIR_PATH_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js new file mode 100644 index 0000000000..cfbd9eaec9 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js @@ -0,0 +1,44 @@ +/* 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/. + */ + +/* Relative working directory path failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR + : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, "test", null); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + await waitForUpdateXMLFiles(); + if (gIsServiceTest) { + // The invalid argument service tests launch the maintenance service + // directly so the unelevated updater doesn't handle the invalid argument. + // By doing this it is possible to test that the maintenance service + // properly handles the invalid argument but since the updater isn't used to + // launch the maintenance service the update.status file isn't copied from + // the secure log directory to the patch directory and the update manager + // won't read the failure from the update.status file. + checkUpdateManager(STATE_NONE, false, STATE_PENDING_SVC, 0, 1); + } else { + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + INVALID_WORKING_DIR_PATH_ERROR, + 1 + ); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js new file mode 100644 index 0000000000..9280a0736e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_PENDING_SVC; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + createUpdateInProgressLockFile(getGREBinDir()); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, false); + removeUpdateInProgressLockFile(getGREBinDir()); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(PERFORMING_STAGED_UPDATE); + checkUpdateLogContains(ERR_UPDATE_IN_PROGRESS); + await waitForUpdateXMLFiles(true, false); + checkUpdateManager(STATE_AFTER_STAGE, true, STATE_AFTER_STAGE, 0, 0); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js new file mode 100644 index 0000000000..319aee8e34 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + lockDirectory(getGREBinDir().path); + // Switch the application to the staged application that was updated. + await runUpdateUsingApp(STATE_SUCCEEDED); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + + let updatesDir = getUpdateDirFile(DIR_PATCH); + Assert.ok( + updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path) + ); + + let log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js new file mode 100644 index 0000000000..34b47866b1 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, true); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + await runUpdateUsingApp(STATE_SUCCEEDED); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + + let updatesDir = getUpdateDirFile(DIR_PATCH); + Assert.ok( + updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path) + ); + + let log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js new file mode 100644 index 0000000000..980b0cb89a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by launching an application to apply it. + */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + // The third parameter will test that a full path to the post update binary + // doesn't execute. + await setupUpdaterTest( + FILE_COMPLETE_MAR, + undefined, + getApplyDirFile(null, true).path + "/" + ); + await runUpdateUsingApp(STATE_SUCCEEDED); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + + let updatesDir = getUpdateDirFile(DIR_PATCH); + Assert.ok( + updatesDir.exists(), + MSG_SHOULD_EXIST + getMsgPath(updatesDir.path) + ); + + let log = getUpdateDirFile(FILE_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_LAST_UPDATE_LOG); + Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path)); + + log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG); + Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path)); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseBackgroundTaskFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseBackgroundTaskFailureSvc_win.js new file mode 100644 index 0000000000..bcb24bee94 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseBackgroundTaskFailureSvc_win.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Fail to apply a complete MAR when the application is in use and the callback is a background task. */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + + // Add a dummy --backgroundtask arg; this will have no effect on the + // callback (TestAUSHelper) but will cause the updater to detect + // that this is a background task. + gCallbackArgs = gCallbackArgs.concat(["--backgroundtask", "not_found"]); + + // Run the update with the helper file in use, expecting failure. + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); + runUpdate( + STATE_FAILED_WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION, + false, // aSwitchApp + 1, // aExpectedExitValue + true // aCheckSvcLog + ); + await waitForHelperExit(); + + standardInit(); + + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_BGTASK_EXCLUSIVE); + + // Check that the update was reset to "pending". + await waitForUpdateXMLFiles( + true, // aActiveUpdateExists + false // aUpdatesExists + ); + checkUpdateManager( + STATE_PENDING, // aStatusFileState + true, // aHasActiveUpdate + STATE_PENDING, // aUpdateStatusState + WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION, + 0 // aUpdateCount + ); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js new file mode 100644 index 0000000000..502561ed1e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js new file mode 100644 index 0000000000..e08777d042 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js new file mode 100644 index 0000000000..2f8155c790 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file staged patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + gCallbackBinFile = "exe0.exe"; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js new file mode 100644 index 0000000000..04c72896b4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file staged patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + gCallbackBinFile = "exe0.exe"; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js new file mode 100644 index 0000000000..fa13f07f46 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + gCallbackBinFile = "exe0.exe"; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js new file mode 100644 index 0000000000..e74509c06e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + gCallbackBinFile = "exe0.exe"; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js new file mode 100644 index 0000000000..de8db067bc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js @@ -0,0 +1,39 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Failure Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + // If execv is used the updater process will turn into the callback process + // and the updater's return code will be that of the callback process. + runUpdate( + STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE, + false, + USE_EXECV ? 0 : 1, + true + ); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_FAILURE); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + LOADSOURCE_ERROR_WRONG_SIZE, + 1 + ); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js new file mode 100644 index 0000000000..b93b023934 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse( + gTestFiles[13].relPathDir + gTestFiles[13].fileName, + false + ); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js new file mode 100644 index 0000000000..b41da12396 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperFileInUse( + gTestFiles[11].relPathDir + gTestFiles[11].fileName, + false + ); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js new file mode 100644 index 0000000000..4b946ac3e4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse( + gTestFiles[13].relPathDir + gTestFiles[13].fileName, + false + ); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js new file mode 100644 index 0000000000..15c3a1121a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperFileInUse( + gTestFiles[11].relPathDir + gTestFiles[11].fileName, + false + ); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js new file mode 100644 index 0000000000..698ccb7fe5 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperLockFile(gTestFiles[3]); + runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_BACKUP_CREATE_7); + checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(true, false); + checkUpdateManager(STATE_PENDING, true, STATE_PENDING, WRITE_ERROR, 0); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js new file mode 100644 index 0000000000..c8c019ec5c --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperLockFile(gTestFiles[2]); + runUpdate(STATE_FAILED_READ_ERROR, false, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_UNABLE_OPEN_DEST); + checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_FAILED, READ_ERROR, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js new file mode 100644 index 0000000000..7b582dbd45 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperLockFile(gTestFiles[3]); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + // Files aren't checked after staging since this test locks a file which + // prevents reading the file. + checkUpdateLogContains(ERR_ENSURE_COPY); + // Switch the application to the staged application that was updated. + runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, false); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains(ERR_BACKUP_CREATE_7); + checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(true, false); + checkUpdateManager(STATE_PENDING, true, STATE_PENDING, WRITE_ERROR, 0); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js new file mode 100644 index 0000000000..bf3abd8c37 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file staged patch apply failure test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_PENDING; + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperLockFile(gTestFiles[2]); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + // Files aren't checked after staging since this test locks a file which + // prevents reading the file. + checkUpdateLogContains(ERR_ENSURE_COPY); + // Switch the application to the staged application that was updated. + runUpdate(STATE_FAILED_READ_ERROR, false, 1, false); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_UNABLE_OPEN_DEST); + checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_FAILED, READ_ERROR, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js new file mode 100644 index 0000000000..31c5b8bd7a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file staged patch apply failure + test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse( + gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0], + true + ); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js new file mode 100644 index 0000000000..b57f8c81b7 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file staged patch apply failure + test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + const STATE_AFTER_RUNUPDATE = gIsServiceTest + ? STATE_PENDING_SVC + : STATE_PENDING; + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperFileInUse( + gTestDirs[2].relPathDir + gTestDirs[2].files[0], + true + ); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true); + await waitForHelperExit(); + standardInit(); + checkPostUpdateRunningFile(false); + setTestFilesAndDirsForFailure(); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_RENAME_FILE); + checkUpdateLogContains( + ERR_MOVE_DESTDIR_7 + "\n" + STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT + ); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_AFTER_RUNUPDATE, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js new file mode 100644 index 0000000000..0683df0d8d --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await runHelperFileInUse( + gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0], + true + ); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js new file mode 100644 index 0000000000..d4da3a5f37 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file patch apply success test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await runHelperFileInUse( + gTestDirs[2].relPathDir + gTestDirs[2].files[0], + true + ); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await waitForHelperExit(); + await checkPostUpdateAppLog(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js new file mode 100644 index 0000000000..a1a0de0fe4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js @@ -0,0 +1,31 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Failure Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = STATE_FAILED; + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + await setupUpdaterTest(FILE_PARTIAL_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateFailure(getApplyDirFile); + checkUpdateLogContains(ERR_LOADSOURCEFILE_FAILED); + await waitForUpdateXMLFiles(); + checkUpdateManager( + STATE_NONE, + false, + STATE_FAILED, + LOADSOURCE_ERROR_WRONG_SIZE, + 1 + ); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js new file mode 100644 index 0000000000..943a45ba95 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js @@ -0,0 +1,71 @@ +/* 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/. + */ + +/* General Complete MAR File Staged Patch Apply Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsCompleteSuccess; + setupSymLinks(); + await setupUpdaterTest(FILE_COMPLETE_MAR, false); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + checkSymLinks(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} + +/** + * Setup symlinks for the test. + */ +function setupSymLinks() { + // Don't test symlinks on Mac OS X in this test since it tends to timeout. + // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js + if (AppConstants.platform == "linux") { + removeSymlink(); + createSymlink(); + registerCleanupFunction(removeSymlink); + gTestFiles.splice(gTestFiles.length - 3, 0, { + description: "Readable symlink", + fileName: "link", + relPathDir: DIR_RESOURCES, + originalContents: "test", + compareContents: "test", + originalFile: null, + compareFile: null, + originalPerms: 0o666, + comparePerms: 0o666, + }); + } +} + +/** + * Checks the state of the symlinks for the test. + */ +function checkSymLinks() { + // Don't test symlinks on Mac OS X in this test since it tends to timeout. + // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js + if (AppConstants.platform == "linux") { + checkSymlink(); + } +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js new file mode 100644 index 0000000000..dd5c240919 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js @@ -0,0 +1,35 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + const STATE_AFTER_STAGE = gIsServiceTest ? STATE_APPLIED_SVC : STATE_APPLIED; + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + preventDistributionFiles(); + await setupUpdaterTest(FILE_PARTIAL_MAR, true); + await stageUpdate(STATE_AFTER_STAGE, true); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getStageDirFile, true); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true, false, true); + // Switch the application to the staged application that was updated. + runUpdate(STATE_SUCCEEDED, true, 0, true); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js new file mode 100644 index 0000000000..2dd1e54f90 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js @@ -0,0 +1,26 @@ +/* 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/. + */ + +/* General Complete MAR File Patch Apply Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + preventDistributionFiles(); + await setupUpdaterTest(FILE_COMPLETE_MAR, true); + runUpdate(STATE_SUCCEEDED, false, 0, true); + await checkPostUpdateAppLog(); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(true); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS, false, false, true); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js new file mode 100644 index 0000000000..8e8e9d094a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js @@ -0,0 +1,29 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Test */ + +async function run_test() { + if (!setupTestCommon()) { + return; + } + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + // The third parameter will test that a relative path that contains a + // directory traversal to the post update binary doesn't execute. + await setupUpdaterTest(FILE_PARTIAL_MAR, false, "test/../"); + runUpdate(STATE_SUCCEEDED, false, 0, true); + checkAppBundleModTime(); + standardInit(); + checkPostUpdateRunningFile(false); + checkFilesAfterUpdateSuccess(getApplyDirFile); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + await waitForUpdateXMLFiles(); + checkUpdateManager(STATE_NONE, false, STATE_SUCCEEDED, 0, 1); + checkCallbackLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini b/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini new file mode 100644 index 0000000000..977162c585 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini @@ -0,0 +1,99 @@ +# 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/. + +# Tests that require the updater binary and the maintenance service. + +[DEFAULT] +skip-if = + os == "win" && verify + os == "win" && ccov # 1532801 + os == "win" && asan # updater binary must be signed for these tests, but it isn't in this build config + os == 'win' && msix # Updates are disabled for MSIX builds +tags = appupdate +head = head_update.js +support-files = + ../data/shared.js + ../data/sharedUpdateXML.js + ../data/xpcshellUtilsAUS.js + +[bootstrapSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgInstallDirPathTooLongFailureSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgInstallDirPathTraversalFailureSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgPatchDirPathSuffixFailureSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgPatchDirPathTraversalFailureSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgStageDirNotInInstallDirFailureSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgWorkingDirPathLocalUNCFailureSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[invalidArgWorkingDirPathRelativeFailureSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marSuccessCompleteSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marSuccessPartialSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFailurePartialSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marStageSuccessCompleteSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marStageSuccessPartialSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marStageFailurePartialSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[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. +[marAppInUseBackgroundTaskFailureSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppInUseSuccessCompleteSvc.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] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedStageFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseSuccessCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseSuccessPartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseSuccessCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseSuccessPartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseStageFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseStageFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyDirLockedStageFailureSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +skip-if = (ccov && os == "win") #Bug 1651090 +[marAppApplyUpdateSuccessSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyUpdateStageSuccessSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[checkUpdaterSigSvc.js] +[fallbackOnSvcFailure.js] +run-sequentially = Uses the Mozilla Maintenance Service. diff --git a/toolkit/mozapps/update/updater/Launchd.plist b/toolkit/mozapps/update/updater/Launchd.plist new file mode 100644 index 0000000000..f0b5cef085 --- /dev/null +++ b/toolkit/mozapps/update/updater/Launchd.plist @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>org.mozilla.updater</string> + <key>RunAtLoad</key> + <true/> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/Makefile.in b/toolkit/mozapps/update/updater/Makefile.in new file mode 100644 index 0000000000..502c09d21f --- /dev/null +++ b/toolkit/mozapps/update/updater/Makefile.in @@ -0,0 +1,28 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# 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/. + +# For changes here, also consider ./updater-xpcshell/Makefile.in + +ifndef MOZ_WINCONSOLE +ifdef MOZ_DEBUG +MOZ_WINCONSOLE = 1 +else +MOZ_WINCONSOLE = 0 +endif +endif + +include $(topsrcdir)/config/rules.mk + +ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) +export:: + $(call py_action,preprocessor,-Fsubstitution -DMOZ_MACBUNDLE_ID='$(MOZ_MACBUNDLE_ID)' $(srcdir)/macbuild/Contents/Info.plist.in -o $(DIST)/bin/Info.plist) +libs:: + $(NSINSTALL) -D $(DIST)/bin/updater.app + rsync -a -C --exclude '*.in' $(srcdir)/macbuild/Contents $(DIST)/bin/updater.app + rsync -a -C $(DIST)/bin/Info.plist $(DIST)/bin/updater.app/Contents + $(call py_action,preprocessor,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in -o $(DIST)/bin/updater.app/Contents/Resources/English.lproj/InfoPlist.strings) + $(NSINSTALL) -D $(DIST)/bin/updater.app/Contents/MacOS + $(NSINSTALL) $(DIST)/bin/org.mozilla.updater $(DIST)/bin/updater.app/Contents/MacOS +endif diff --git a/toolkit/mozapps/update/updater/TsanOptions.cpp b/toolkit/mozapps/update/updater/TsanOptions.cpp new file mode 100644 index 0000000000..44a0b53afc --- /dev/null +++ b/toolkit/mozapps/update/updater/TsanOptions.cpp @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "mozilla/Attributes.h" +#include "mozilla/TsanOptions.h" + +#ifndef _MSC_VER // Not supported by clang-cl yet + +// See also mozglue/build/TsanOptions.cpp before modifying this +extern "C" const char* __tsan_default_suppressions() { + // clang-format off + return "# Add your suppressions below\n" + + // External uninstrumented libraries + MOZ_TSAN_DEFAULT_EXTLIB_SUPPRESSIONS + + // End of suppressions. + ; // Please keep this semicolon. + // clang-format on +} +#endif // _MSC_VER diff --git a/toolkit/mozapps/update/updater/archivereader.cpp b/toolkit/mozapps/update/updater/archivereader.cpp new file mode 100644 index 0000000000..534ed2d176 --- /dev/null +++ b/toolkit/mozapps/update/updater/archivereader.cpp @@ -0,0 +1,344 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include <string.h> +#include <stdlib.h> +#include <fcntl.h> +#ifdef XP_WIN +# include <windows.h> +#endif +#include "archivereader.h" +#include "updatererrors.h" +#ifdef XP_WIN +# include "nsAlgorithm.h" // Needed by nsVersionComparator.cpp +# include "updatehelper.h" +#endif +#define XZ_USE_CRC64 +#include "xz.h" + +// These are generated at compile time based on the DER file for the channel +// being used +#ifdef MOZ_VERIFY_MAR_SIGNATURE +# ifdef TEST_UPDATER +# include "../xpcshellCert.h" +# elif DEP_UPDATER +# include "../dep1Cert.h" +# include "../dep2Cert.h" +# else +# include "primaryCert.h" +# include "secondaryCert.h" +# endif +#endif + +#define UPDATER_NO_STRING_GLUE_STL +#include "nsVersionComparator.cpp" +#undef UPDATER_NO_STRING_GLUE_STL + +#if defined(XP_WIN) +# include <io.h> +#endif + +/** + * Performs a verification on the opened MAR file with the passed in + * certificate name ID and type ID. + * + * @param archive The MAR file to verify the signature on. + * @param certData The certificate data. + * @return OK on success, CERT_VERIFY_ERROR on failure. + */ +template <uint32_t SIZE> +int VerifyLoadedCert(MarFile* archive, const uint8_t (&certData)[SIZE]) { + (void)archive; + (void)certData; + +#ifdef MOZ_VERIFY_MAR_SIGNATURE + const uint32_t size = SIZE; + const uint8_t* const data = &certData[0]; + if (mar_verify_signatures(archive, &data, &size, 1)) { + return CERT_VERIFY_ERROR; + } +#endif + + return OK; +} + +/** + * Performs a verification on the opened MAR file. Both the primary and backup + * keys stored are stored in the current process and at least the primary key + * will be tried. Success will be returned as long as one of the two + * signatures verify. + * + * @return OK on success + */ +int ArchiveReader::VerifySignature() { + if (!mArchive) { + return ARCHIVE_NOT_OPEN; + } + +#ifndef MOZ_VERIFY_MAR_SIGNATURE + return OK; +#else +# ifdef TEST_UPDATER + int rv = VerifyLoadedCert(mArchive, xpcshellCertData); +# elif DEP_UPDATER + int rv = VerifyLoadedCert(mArchive, dep1CertData); + if (rv != OK) { + rv = VerifyLoadedCert(mArchive, dep2CertData); + } +# else + int rv = VerifyLoadedCert(mArchive, primaryCertData); + if (rv != OK) { + rv = VerifyLoadedCert(mArchive, secondaryCertData); + } +# endif + return rv; +#endif +} + +/** + * Verifies that the MAR file matches the current product, channel, and version + * + * @param MARChannelID The MAR channel name to use, only updates from MARs + * with a matching MAR channel name will succeed. + * If an empty string is passed, no check will be done + * for the channel name in the product information block. + * If a comma separated list of values is passed then + * one value must match. + * @param appVersion The application version to use, only MARs with an + * application version >= to appVersion will be applied. + * @return OK on success + * COULD_NOT_READ_PRODUCT_INFO_BLOCK if the product info block + * could not be read. + * MARCHANNEL_MISMATCH_ERROR if update-settings.ini's MAR + * channel ID doesn't match the MAR + * file's MAR channel ID. + * VERSION_DOWNGRADE_ERROR if the application version for + * this updater is newer than the + * one in the MAR. + */ +int ArchiveReader::VerifyProductInformation(const char* MARChannelID, + const char* appVersion) { + if (!mArchive) { + return ARCHIVE_NOT_OPEN; + } + + ProductInformationBlock productInfoBlock; + int rv = mar_read_product_info_block(mArchive, &productInfoBlock); + if (rv != OK) { + return COULD_NOT_READ_PRODUCT_INFO_BLOCK_ERROR; + } + + // Only check the MAR channel name if specified, it should be passed in from + // the update-settings.ini file. + if (MARChannelID && strlen(MARChannelID)) { + // Check for at least one match in the comma separated list of values. + const char* delimiter = " ,\t"; + // Make a copy of the string in case a read only memory buffer + // was specified. strtok modifies the input buffer. + char channelCopy[512] = {0}; + strncpy(channelCopy, MARChannelID, sizeof(channelCopy) - 1); + char* channel = strtok(channelCopy, delimiter); + rv = MAR_CHANNEL_MISMATCH_ERROR; + while (channel) { + if (!strcmp(channel, productInfoBlock.MARChannelID)) { + rv = OK; + break; + } + channel = strtok(nullptr, delimiter); + } + } + + if (rv == OK) { + /* Compare both versions to ensure we don't have a downgrade + -1 if appVersion is older than productInfoBlock.productVersion + 1 if appVersion is newer than productInfoBlock.productVersion + 0 if appVersion is the same as productInfoBlock.productVersion + This even works with strings like: + - 12.0a1 being older than 12.0a2 + - 12.0a2 being older than 12.0b1 + - 12.0a1 being older than 12.0 + - 12.0 being older than 12.1a1 */ + int versionCompareResult = + mozilla::CompareVersions(appVersion, productInfoBlock.productVersion); + if (1 == versionCompareResult) { + rv = VERSION_DOWNGRADE_ERROR; + } + } + + free((void*)productInfoBlock.MARChannelID); + free((void*)productInfoBlock.productVersion); + return rv; +} + +int ArchiveReader::Open(const NS_tchar* path) { + if (mArchive) { + Close(); + } + + if (!mInBuf) { + mInBuf = (uint8_t*)malloc(mInBufSize); + if (!mInBuf) { + // Try again with a smaller buffer. + mInBufSize = 1024; + mInBuf = (uint8_t*)malloc(mInBufSize); + if (!mInBuf) { + return ARCHIVE_READER_MEM_ERROR; + } + } + } + + if (!mOutBuf) { + mOutBuf = (uint8_t*)malloc(mOutBufSize); + if (!mOutBuf) { + // Try again with a smaller buffer. + mOutBufSize = 1024; + mOutBuf = (uint8_t*)malloc(mOutBufSize); + if (!mOutBuf) { + return ARCHIVE_READER_MEM_ERROR; + } + } + } + +#ifdef XP_WIN + mArchive = mar_wopen(path); +#else + mArchive = mar_open(path); +#endif + if (!mArchive) { + return READ_ERROR; + } + + xz_crc32_init(); + xz_crc64_init(); + + return OK; +} + +void ArchiveReader::Close() { + if (mArchive) { + mar_close(mArchive); + mArchive = nullptr; + } + + if (mInBuf) { + free(mInBuf); + mInBuf = nullptr; + } + + if (mOutBuf) { + free(mOutBuf); + mOutBuf = nullptr; + } +} + +int ArchiveReader::ExtractFile(const char* name, const NS_tchar* dest) { + const MarItem* item = mar_find_item(mArchive, name); + if (!item) { + return READ_ERROR; + } + +#ifdef XP_WIN + FILE* fp = _wfopen(dest, L"wb+"); +#else + int fd = creat(dest, item->flags); + if (fd == -1) { + return WRITE_ERROR; + } + + FILE* fp = fdopen(fd, "wb"); +#endif + if (!fp) { + return WRITE_ERROR; + } + + int rv = ExtractItemToStream(item, fp); + + fclose(fp); + return rv; +} + +int ArchiveReader::ExtractFileToStream(const char* name, FILE* fp) { + const MarItem* item = mar_find_item(mArchive, name); + if (!item) { + return READ_ERROR; + } + + return ExtractItemToStream(item, fp); +} + +int ArchiveReader::ExtractItemToStream(const MarItem* item, FILE* fp) { + /* decompress the data chunk by chunk */ + + int offset, inlen, ret = OK; + struct xz_buf strm = {0}; + enum xz_ret xz_rv = XZ_OK; + + struct xz_dec* dec = xz_dec_init(XZ_DYNALLOC, 64 * 1024 * 1024); + if (!dec) { + return UNEXPECTED_XZ_ERROR; + } + + strm.in = mInBuf; + strm.in_pos = 0; + strm.in_size = 0; + strm.out = mOutBuf; + strm.out_pos = 0; + strm.out_size = mOutBufSize; + + offset = 0; + for (;;) { + if (!item->length) { + ret = UNEXPECTED_MAR_ERROR; + break; + } + + if (offset < (int)item->length && strm.in_pos == strm.in_size) { + inlen = mar_read(mArchive, item, offset, mInBuf, mInBufSize); + if (inlen <= 0) { + ret = READ_ERROR; + break; + } + offset += inlen; + strm.in_size = inlen; + strm.in_pos = 0; + } + + xz_rv = xz_dec_run(dec, &strm); + + if (strm.out_pos == mOutBufSize) { + if (fwrite(mOutBuf, 1, strm.out_pos, fp) != strm.out_pos) { + ret = WRITE_ERROR_EXTRACT; + break; + } + + strm.out_pos = 0; + } + + if (xz_rv == XZ_OK) { + // There is still more data to decompress. + continue; + } + + // The return value of xz_dec_run is not XZ_OK and if it isn't XZ_STREAM_END + // an error has occured. + if (xz_rv != XZ_STREAM_END) { + ret = UNEXPECTED_XZ_ERROR; + break; + } + + // Write out the remainder of the decompressed data. In the case of + // strm.out_pos == 0 this is needed to create empty files included in the + // mar file. + if (fwrite(mOutBuf, 1, strm.out_pos, fp) != strm.out_pos) { + ret = WRITE_ERROR_EXTRACT; + } + break; + } + + xz_dec_end(dec); + return ret; +} diff --git a/toolkit/mozapps/update/updater/archivereader.h b/toolkit/mozapps/update/updater/archivereader.h new file mode 100644 index 0000000000..1f18327f1d --- /dev/null +++ b/toolkit/mozapps/update/updater/archivereader.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef ArchiveReader_h__ +#define ArchiveReader_h__ + +#include <stdio.h> +#include "mar.h" + +#ifdef XP_WIN +# include <windows.h> + +typedef WCHAR NS_tchar; +#else +typedef char NS_tchar; +#endif + +// This class provides an API to extract files from an update archive. +class ArchiveReader { + public: + ArchiveReader() = default; + ~ArchiveReader() { Close(); } + + int Open(const NS_tchar* path); + int VerifySignature(); + int VerifyProductInformation(const char* MARChannelID, + const char* appVersion); + void Close(); + + int ExtractFile(const char* item, const NS_tchar* destination); + int ExtractFileToStream(const char* item, FILE* fp); + + private: + int ExtractItemToStream(const MarItem* item, FILE* fp); + + MarFile* mArchive = nullptr; + uint8_t* mInBuf = nullptr; + uint8_t* mOutBuf = nullptr; + size_t mInBufSize = 262144; + size_t mOutBufSize = 262144; +}; + +#endif // ArchiveReader_h__ diff --git a/toolkit/mozapps/update/updater/autograph_stage.pem b/toolkit/mozapps/update/updater/autograph_stage.pem new file mode 100644 index 0000000000..bd8513a04d --- /dev/null +++ b/toolkit/mozapps/update/updater/autograph_stage.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvN249lqptaK9L7VltsDt +6X/Hik/iqHSdJMwAWOoB8exyUvura0VMZhYNGCl046zKnE9aX5aMk4s4MJX0Kw9Q +KofWUZ+hni18gyXFjecg6AyuEiMAJpSDknWnkZ1hucXTLNpwXwRHPW5YHIinKidz +kTCREsZl0IU+gieEYXziQ4eBvc9eSNnprKhN/00AxHlmwCtY+3HLso9PYptcOspf +yuQC/PKLwBb6hqcwEoHsT0w1roRRSACZCHfJYtzXteW7uY3NcUOrSlWFMtZguXuO +K0U/OJaVnfcJ6REB9HTAzgmL54QlXlGTge8Vj+XMx4GqZD1fuM7rctIFclSL/wWi +tq8MOedINL2lj2YKB8ArU2kWmi+v7HLcS94WHHcGsBh7SrNRZQEfiMBKrHHW+mqO +xRRbyR3zAn6M78UOFqMboEQWzWHKFNhw8VI1CA8maylNuArAZhJzdLvUUo2IuQQo +floKjdeooezDYBeeeJXOcGUv3VrulIuL3MA5k1l+c6uBX7NFWX8ukBTG09b3sNP+ +iH4P2AIcKoccxFpjswlUZCnSKF0jRu1Ue+IulHDNzora8WDOqK0IsfNfZMNyykGf +8WsELSO3m4CxXuCbY8hmm67dTQ5DKYf874GUm7yOCe2u4piRSJ20eA4WguwxmEIj +96Kk7NgCLtRU3G754oOTksUCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/toolkit/mozapps/update/updater/bspatch/LICENSE b/toolkit/mozapps/update/updater/bspatch/LICENSE new file mode 100644 index 0000000000..f2521d71cd --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/LICENSE @@ -0,0 +1,23 @@ +Copyright 2003,2004 Colin Percival +All rights reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted providing that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/toolkit/mozapps/update/updater/bspatch/bspatch.cpp b/toolkit/mozapps/update/updater/bspatch/bspatch.cpp new file mode 100644 index 0000000000..8eabd0e427 --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/bspatch.cpp @@ -0,0 +1,216 @@ +/*- + * Copyright 2003,2004 Colin Percival + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Changelog: + * 2005-04-26 - Define the header as a C structure, add a CRC32 checksum to + * the header, and make all the types 32-bit. + * --Benjamin Smedberg <benjamin@smedbergs.us> + */ + +#include "bspatch.h" +#include "updatererrors.h" + +#include <sys/stat.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <limits.h> + +#if defined(XP_WIN) +# include <io.h> +#endif + +#ifdef XP_WIN +# include <winsock2.h> +#else +# include <arpa/inet.h> +#endif + +#ifndef SSIZE_MAX +# define SSIZE_MAX LONG_MAX +#endif + +int MBS_ReadHeader(FILE* file, MBSPatchHeader* header) { + size_t s = fread(header, 1, sizeof(MBSPatchHeader), file); + if (s != sizeof(MBSPatchHeader)) { + return READ_ERROR; + } + + header->slen = ntohl(header->slen); + header->scrc32 = ntohl(header->scrc32); + header->dlen = ntohl(header->dlen); + header->cblen = ntohl(header->cblen); + header->difflen = ntohl(header->difflen); + header->extralen = ntohl(header->extralen); + + struct stat hs; + s = fstat(fileno(file), &hs); + if (s != 0) { + return READ_ERROR; + } + + if (memcmp(header->tag, "MBDIFF10", 8) != 0) { + return UNEXPECTED_BSPATCH_ERROR; + } + + if (hs.st_size > INT_MAX) { + return UNEXPECTED_BSPATCH_ERROR; + } + + size_t size = static_cast<size_t>(hs.st_size); + if (size < sizeof(MBSPatchHeader)) { + return UNEXPECTED_BSPATCH_ERROR; + } + size -= sizeof(MBSPatchHeader); + + if (size < header->cblen) { + return UNEXPECTED_BSPATCH_ERROR; + } + size -= header->cblen; + + if (size < header->difflen) { + return UNEXPECTED_BSPATCH_ERROR; + } + size -= header->difflen; + + if (size < header->extralen) { + return UNEXPECTED_BSPATCH_ERROR; + } + size -= header->extralen; + + if (size != 0) { + return UNEXPECTED_BSPATCH_ERROR; + } + + return OK; +} + +int MBS_ApplyPatch(const MBSPatchHeader* header, FILE* patchFile, + unsigned char* fbuffer, FILE* file) { + unsigned char* fbufstart = fbuffer; + unsigned char* fbufend = fbuffer + header->slen; + + unsigned char* buf = (unsigned char*)malloc(header->cblen + header->difflen + + header->extralen); + if (!buf) { + return BSPATCH_MEM_ERROR; + } + + int rv = OK; + + size_t r = header->cblen + header->difflen + header->extralen; + unsigned char* wb = buf; + while (r) { + const size_t count = (r > SSIZE_MAX) ? SSIZE_MAX : r; + size_t c = fread(wb, 1, count, patchFile); + if (c != count) { + rv = READ_ERROR; + goto end; + } + + r -= c; + wb += c; + + if (c == 0 && r) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + } + + { + MBSPatchTriple* ctrlsrc = (MBSPatchTriple*)buf; + if (header->cblen % sizeof(MBSPatchTriple) != 0) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + + unsigned char* diffsrc = buf + header->cblen; + unsigned char* extrasrc = diffsrc + header->difflen; + + MBSPatchTriple* ctrlend = (MBSPatchTriple*)diffsrc; + unsigned char* diffend = extrasrc; + unsigned char* extraend = extrasrc + header->extralen; + + while (ctrlsrc < ctrlend) { + ctrlsrc->x = ntohl(ctrlsrc->x); + ctrlsrc->y = ntohl(ctrlsrc->y); + ctrlsrc->z = ntohl(ctrlsrc->z); + +#ifdef DEBUG_bsmedberg + printf( + "Applying block:\n" + " x: %u\n" + " y: %u\n" + " z: %i\n", + ctrlsrc->x, ctrlsrc->y, ctrlsrc->z); +#endif + + /* Add x bytes from oldfile to x bytes from the diff block */ + + if (ctrlsrc->x > static_cast<size_t>(fbufend - fbuffer) || + ctrlsrc->x > static_cast<size_t>(diffend - diffsrc)) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + for (uint32_t i = 0; i < ctrlsrc->x; ++i) { + diffsrc[i] += fbuffer[i]; + } + if ((uint32_t)fwrite(diffsrc, 1, ctrlsrc->x, file) != ctrlsrc->x) { + rv = WRITE_ERROR_PATCH_FILE; + goto end; + } + fbuffer += ctrlsrc->x; + diffsrc += ctrlsrc->x; + + /* Copy y bytes from the extra block */ + + if (ctrlsrc->y > static_cast<size_t>(extraend - extrasrc)) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + if ((uint32_t)fwrite(extrasrc, 1, ctrlsrc->y, file) != ctrlsrc->y) { + rv = WRITE_ERROR_PATCH_FILE; + goto end; + } + extrasrc += ctrlsrc->y; + + /* "seek" forwards in oldfile by z bytes */ + + if (ctrlsrc->z < fbufstart - fbuffer || ctrlsrc->z > fbufend - fbuffer) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + fbuffer += ctrlsrc->z; + + /* and on to the next control block */ + + ++ctrlsrc; + } + } + +end: + free(buf); + return rv; +} diff --git a/toolkit/mozapps/update/updater/bspatch/bspatch.h b/toolkit/mozapps/update/updater/bspatch/bspatch.h new file mode 100644 index 0000000000..189e618557 --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/bspatch.h @@ -0,0 +1,93 @@ +/*- + * Copyright 2003,2004 Colin Percival + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Changelog: + * 2005-04-26 - Define the header as a C structure, add a CRC32 checksum to + * the header, and make all the types 32-bit. + * --Benjamin Smedberg <benjamin@smedbergs.us> + */ + +#ifndef bspatch_h__ +#define bspatch_h__ + +#include <stdint.h> +#include <stdio.h> + +typedef struct MBSPatchHeader_ { + /* "MBDIFF10" */ + char tag[8]; + + /* Length of the file to be patched */ + uint32_t slen; + + /* CRC32 of the file to be patched */ + uint32_t scrc32; + + /* Length of the result file */ + uint32_t dlen; + + /* Length of the control block in bytes */ + uint32_t cblen; + + /* Length of the diff block in bytes */ + uint32_t difflen; + + /* Length of the extra block in bytes */ + uint32_t extralen; + + /* Control block (MBSPatchTriple[]) */ + /* Diff block (binary data) */ + /* Extra block (binary data) */ +} MBSPatchHeader; + +/** + * Read the header of a patch file into the MBSPatchHeader structure. + * + * @param fd Must have been opened for reading, and be at the beginning + * of the file. + */ +int MBS_ReadHeader(FILE* file, MBSPatchHeader* header); + +/** + * Apply a patch. This method does not validate the checksum of the original + * file: client code should validate the checksum before calling this method. + * + * @param patchfd Must have been processed by MBS_ReadHeader + * @param fbuffer The original file read into a memory buffer of length + * header->slen. + * @param filefd Must have been opened for writing. Should be truncated + * to header->dlen if it is an existing file. The offset + * should be at the beginning of the file. + */ +int MBS_ApplyPatch(const MBSPatchHeader* header, FILE* patchFile, + unsigned char* fbuffer, FILE* file); + +typedef struct MBSPatchTriple_ { + uint32_t x; /* add x bytes from oldfile to x bytes from the diff block */ + uint32_t y; /* copy y bytes from the extra block */ + int32_t z; /* seek forwards in oldfile by z bytes */ +} MBSPatchTriple; + +#endif // bspatch_h__ diff --git a/toolkit/mozapps/update/updater/bspatch/moz.build b/toolkit/mozapps/update/updater/bspatch/moz.build new file mode 100644 index 0000000000..46c6d11fad --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/moz.build @@ -0,0 +1,22 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +if CONFIG["OS_ARCH"] == "WINNT": + USE_STATIC_LIBS = True + +EXPORTS += [ + "bspatch.h", +] + +SOURCES += [ + "bspatch.cpp", +] + +USE_LIBS += [ + "updatecommon", +] + +Library("bspatch") diff --git a/toolkit/mozapps/update/updater/bspatch/moz.yaml b/toolkit/mozapps/update/updater/bspatch/moz.yaml new file mode 100644 index 0000000000..ce611f97a1 --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/moz.yaml @@ -0,0 +1,30 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: "Toolkit" + component: "Application Update" + +# The source from this directory was adapted from Colin Percival's bspatch +# tool in mid 2005 and was obtained from bsdiff version 4.2. Edits were +# later added by the Chromium dev team and were copied to here as well + +# Document the source of externally hosted code +origin: + name: "bsdiff/bspatch" + description: "Builds and applies patches to binary files" + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: "https://www.daemonology.net/bsdiff/bsdiff-4.2.tar.gz" + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: "version 4.2" + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: "BSD-2-Clause" diff --git a/toolkit/mozapps/update/updater/crctable.h b/toolkit/mozapps/update/updater/crctable.h new file mode 100644 index 0000000000..dadabcc9a1 --- /dev/null +++ b/toolkit/mozapps/update/updater/crctable.h @@ -0,0 +1,71 @@ +/** + * This file was part of bzip2/libbzip2. + * We extracted only this table for bzip2 crc comptability + */ + +/** + I think this is an implementation of the AUTODIN-II, + Ethernet & FDDI 32-bit CRC standard. Vaguely derived + from code by Rob Warnock, in Section 51 of the + comp.compression FAQ. +*/ +unsigned int BZ2_crc32Table[256] = { + + /*-- Ugly, innit? --*/ + + 0x00000000L, 0x04c11db7L, 0x09823b6eL, 0x0d4326d9L, 0x130476dcL, + 0x17c56b6bL, 0x1a864db2L, 0x1e475005L, 0x2608edb8L, 0x22c9f00fL, + 0x2f8ad6d6L, 0x2b4bcb61L, 0x350c9b64L, 0x31cd86d3L, 0x3c8ea00aL, + 0x384fbdbdL, 0x4c11db70L, 0x48d0c6c7L, 0x4593e01eL, 0x4152fda9L, + 0x5f15adacL, 0x5bd4b01bL, 0x569796c2L, 0x52568b75L, 0x6a1936c8L, + 0x6ed82b7fL, 0x639b0da6L, 0x675a1011L, 0x791d4014L, 0x7ddc5da3L, + 0x709f7b7aL, 0x745e66cdL, 0x9823b6e0L, 0x9ce2ab57L, 0x91a18d8eL, + 0x95609039L, 0x8b27c03cL, 0x8fe6dd8bL, 0x82a5fb52L, 0x8664e6e5L, + 0xbe2b5b58L, 0xbaea46efL, 0xb7a96036L, 0xb3687d81L, 0xad2f2d84L, + 0xa9ee3033L, 0xa4ad16eaL, 0xa06c0b5dL, 0xd4326d90L, 0xd0f37027L, + 0xddb056feL, 0xd9714b49L, 0xc7361b4cL, 0xc3f706fbL, 0xceb42022L, + 0xca753d95L, 0xf23a8028L, 0xf6fb9d9fL, 0xfbb8bb46L, 0xff79a6f1L, + 0xe13ef6f4L, 0xe5ffeb43L, 0xe8bccd9aL, 0xec7dd02dL, 0x34867077L, + 0x30476dc0L, 0x3d044b19L, 0x39c556aeL, 0x278206abL, 0x23431b1cL, + 0x2e003dc5L, 0x2ac12072L, 0x128e9dcfL, 0x164f8078L, 0x1b0ca6a1L, + 0x1fcdbb16L, 0x018aeb13L, 0x054bf6a4L, 0x0808d07dL, 0x0cc9cdcaL, + 0x7897ab07L, 0x7c56b6b0L, 0x71159069L, 0x75d48ddeL, 0x6b93dddbL, + 0x6f52c06cL, 0x6211e6b5L, 0x66d0fb02L, 0x5e9f46bfL, 0x5a5e5b08L, + 0x571d7dd1L, 0x53dc6066L, 0x4d9b3063L, 0x495a2dd4L, 0x44190b0dL, + 0x40d816baL, 0xaca5c697L, 0xa864db20L, 0xa527fdf9L, 0xa1e6e04eL, + 0xbfa1b04bL, 0xbb60adfcL, 0xb6238b25L, 0xb2e29692L, 0x8aad2b2fL, + 0x8e6c3698L, 0x832f1041L, 0x87ee0df6L, 0x99a95df3L, 0x9d684044L, + 0x902b669dL, 0x94ea7b2aL, 0xe0b41de7L, 0xe4750050L, 0xe9362689L, + 0xedf73b3eL, 0xf3b06b3bL, 0xf771768cL, 0xfa325055L, 0xfef34de2L, + 0xc6bcf05fL, 0xc27dede8L, 0xcf3ecb31L, 0xcbffd686L, 0xd5b88683L, + 0xd1799b34L, 0xdc3abdedL, 0xd8fba05aL, 0x690ce0eeL, 0x6dcdfd59L, + 0x608edb80L, 0x644fc637L, 0x7a089632L, 0x7ec98b85L, 0x738aad5cL, + 0x774bb0ebL, 0x4f040d56L, 0x4bc510e1L, 0x46863638L, 0x42472b8fL, + 0x5c007b8aL, 0x58c1663dL, 0x558240e4L, 0x51435d53L, 0x251d3b9eL, + 0x21dc2629L, 0x2c9f00f0L, 0x285e1d47L, 0x36194d42L, 0x32d850f5L, + 0x3f9b762cL, 0x3b5a6b9bL, 0x0315d626L, 0x07d4cb91L, 0x0a97ed48L, + 0x0e56f0ffL, 0x1011a0faL, 0x14d0bd4dL, 0x19939b94L, 0x1d528623L, + 0xf12f560eL, 0xf5ee4bb9L, 0xf8ad6d60L, 0xfc6c70d7L, 0xe22b20d2L, + 0xe6ea3d65L, 0xeba91bbcL, 0xef68060bL, 0xd727bbb6L, 0xd3e6a601L, + 0xdea580d8L, 0xda649d6fL, 0xc423cd6aL, 0xc0e2d0ddL, 0xcda1f604L, + 0xc960ebb3L, 0xbd3e8d7eL, 0xb9ff90c9L, 0xb4bcb610L, 0xb07daba7L, + 0xae3afba2L, 0xaafbe615L, 0xa7b8c0ccL, 0xa379dd7bL, 0x9b3660c6L, + 0x9ff77d71L, 0x92b45ba8L, 0x9675461fL, 0x8832161aL, 0x8cf30badL, + 0x81b02d74L, 0x857130c3L, 0x5d8a9099L, 0x594b8d2eL, 0x5408abf7L, + 0x50c9b640L, 0x4e8ee645L, 0x4a4ffbf2L, 0x470cdd2bL, 0x43cdc09cL, + 0x7b827d21L, 0x7f436096L, 0x7200464fL, 0x76c15bf8L, 0x68860bfdL, + 0x6c47164aL, 0x61043093L, 0x65c52d24L, 0x119b4be9L, 0x155a565eL, + 0x18197087L, 0x1cd86d30L, 0x029f3d35L, 0x065e2082L, 0x0b1d065bL, + 0x0fdc1becL, 0x3793a651L, 0x3352bbe6L, 0x3e119d3fL, 0x3ad08088L, + 0x2497d08dL, 0x2056cd3aL, 0x2d15ebe3L, 0x29d4f654L, 0xc5a92679L, + 0xc1683bceL, 0xcc2b1d17L, 0xc8ea00a0L, 0xd6ad50a5L, 0xd26c4d12L, + 0xdf2f6bcbL, 0xdbee767cL, 0xe3a1cbc1L, 0xe760d676L, 0xea23f0afL, + 0xeee2ed18L, 0xf0a5bd1dL, 0xf464a0aaL, 0xf9278673L, 0xfde69bc4L, + 0x89b8fd09L, 0x8d79e0beL, 0x803ac667L, 0x84fbdbd0L, 0x9abc8bd5L, + 0x9e7d9662L, 0x933eb0bbL, 0x97ffad0cL, 0xafb010b1L, 0xab710d06L, + 0xa6322bdfL, 0xa2f33668L, 0xbcb4666dL, 0xb8757bdaL, 0xb5365d03L, + 0xb1f740b4L}; + +/*-------------------------------------------------------------*/ +/*--- end crctable.h ---*/ +/*-------------------------------------------------------------*/ diff --git a/toolkit/mozapps/update/updater/dep1.der b/toolkit/mozapps/update/updater/dep1.der Binary files differnew file mode 100644 index 0000000000..655c2d10d4 --- /dev/null +++ b/toolkit/mozapps/update/updater/dep1.der diff --git a/toolkit/mozapps/update/updater/dep2.der b/toolkit/mozapps/update/updater/dep2.der Binary files differnew file mode 100644 index 0000000000..c59ac7f790 --- /dev/null +++ b/toolkit/mozapps/update/updater/dep2.der diff --git a/toolkit/mozapps/update/updater/gen_cert_header.py b/toolkit/mozapps/update/updater/gen_cert_header.py new file mode 100644 index 0000000000..da78cad674 --- /dev/null +++ b/toolkit/mozapps/update/updater/gen_cert_header.py @@ -0,0 +1,27 @@ +# 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/. + +import os + + +def file_byte_generator(filename, block_size=512): + with open(filename, "rb") as f: + while True: + block = f.read(block_size) + if block: + for byte in block: + yield byte + else: + break + + +def create_header(out_fh, in_filename): + assert out_fh.name.endswith(".h") + array_name = os.path.basename(out_fh.name)[:-2] + "Data" + hexified = ["0x%02x" % byte for byte in file_byte_generator(in_filename)] + + print("const uint8_t " + array_name + "[] = {", file=out_fh) + print(", ".join(hexified), file=out_fh) + print("};", file=out_fh) + return 0 diff --git a/toolkit/mozapps/update/updater/launchchild_osx.mm b/toolkit/mozapps/update/updater/launchchild_osx.mm new file mode 100644 index 0000000000..3d28a064c7 --- /dev/null +++ b/toolkit/mozapps/update/updater/launchchild_osx.mm @@ -0,0 +1,500 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include <Cocoa/Cocoa.h> +#include <CoreServices/CoreServices.h> +#include <crt_externs.h> +#include <stdlib.h> +#include <stdio.h> +#include <spawn.h> +#include <SystemConfiguration/SystemConfiguration.h> +#include <sys/types.h> +#include <sys/sysctl.h> +#include "readstrings.h" + +#define ARCH_PATH "/usr/bin/arch" +#if defined(__x86_64__) +// Work around the fact that this constant is not available in the macOS SDK +# define kCFBundleExecutableArchitectureARM64 0x0100000c +#endif + +class MacAutoreleasePool { + public: + MacAutoreleasePool() { mPool = [[NSAutoreleasePool alloc] init]; } + ~MacAutoreleasePool() { [mPool release]; } + + private: + NSAutoreleasePool* mPool; +}; + +#if defined(__x86_64__) +/* + * Returns true if the process is running under Rosetta translation. Returns + * false if running natively or if an error was encountered. We use the + * `sysctl.proc_translated` sysctl which is documented by Apple to be used + * for this purpose. + */ +bool IsProcessRosettaTranslated() { + int ret = 0; + size_t size = sizeof(ret); + if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) { + if (errno != ENOENT) { + fprintf(stderr, "Failed to check for translation environment\n"); + } + return false; + } + return (ret == 1); +} + +// Returns true if the binary at |executablePath| can be executed natively +// on an arm64 Mac. Returns false otherwise or if an error occurred. +bool IsBinaryArmExecutable(const char* executablePath) { + bool isArmExecutable = false; + + CFURLRef url = ::CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, (const UInt8*)executablePath, strlen(executablePath), false); + if (!url) { + return false; + } + + CFArrayRef archs = ::CFBundleCopyExecutableArchitecturesForURL(url); + if (!archs) { + CFRelease(url); + return false; + } + + CFIndex archCount = ::CFArrayGetCount(archs); + for (CFIndex i = 0; i < archCount; i++) { + CFNumberRef currentArch = static_cast<CFNumberRef>(::CFArrayGetValueAtIndex(archs, i)); + int currentArchInt = 0; + if (!::CFNumberGetValue(currentArch, kCFNumberIntType, ¤tArchInt)) { + continue; + } + if (currentArchInt == kCFBundleExecutableArchitectureARM64) { + isArmExecutable = true; + break; + } + } + + CFRelease(url); + CFRelease(archs); + + return isArmExecutable; +} + +// Returns true if the executable provided in |executablePath| should be +// launched with a preference for arm64. After updating from an x64 version +// running under Rosetta, if the update is to a universal binary with arm64 +// support we want to switch to arm64 execution mode. Returns true if those +// conditions are met and the arch(1) utility at |archPath| is executable. +// It should be safe to always launch with arch and fallback to x64, but we +// limit its use to the only scenario it is necessary to minimize risk. +bool ShouldPreferArmLaunch(const char* archPath, const char* executablePath) { + // If not running under Rosetta, we are not on arm64 hardware. + if (!IsProcessRosettaTranslated()) { + return false; + } + + // Ensure the arch(1) utility is present and executable. + NSFileManager* fileMgr = [NSFileManager defaultManager]; + NSString* archPathString = [NSString stringWithUTF8String:archPath]; + if (![fileMgr isExecutableFileAtPath:archPathString]) { + return false; + } + + // Ensure the binary can be run natively on arm64. + return IsBinaryArmExecutable(executablePath); +} +#endif // __x86_64__ + +void LaunchChild(int argc, const char** argv) { + MacAutoreleasePool pool; + + @try { + bool preferArmLaunch = false; + +#if defined(__x86_64__) + // When running under Rosetta, child processes inherit the architecture + // preference of their parent and therefore universal binaries launched + // by an emulated x64 process will launch in x64 mode. If we are running + // under Rosetta, launch the child process with a preference for arm64 so + // that we will switch to arm64 execution if we have just updated from + // x64 to a universal build. This includes if we were already a universal + // build and the user is intentionally running under Rosetta. + preferArmLaunch = ShouldPreferArmLaunch(ARCH_PATH, argv[0]); +#endif // __x86_64__ + + NSString* launchPath; + NSMutableArray* arguments; + + if (preferArmLaunch) { + launchPath = [NSString stringWithUTF8String:ARCH_PATH]; + + // Size the arguments array to include all the arguments + // in |argv| plus two arguments to pass to the arch(1) utility. + arguments = [NSMutableArray arrayWithCapacity:argc + 2]; + [arguments addObject:[NSString stringWithUTF8String:"-arm64"]]; + [arguments addObject:[NSString stringWithUTF8String:"-x86_64"]]; + + // Add the first argument from |argv|. The rest are added below. + [arguments addObject:[NSString stringWithUTF8String:argv[0]]]; + } else { + launchPath = [NSString stringWithUTF8String:argv[0]]; + arguments = [NSMutableArray arrayWithCapacity:argc - 1]; + } + + for (int i = 1; i < argc; i++) { + [arguments addObject:[NSString stringWithUTF8String:argv[i]]]; + } + [NSTask launchedTaskWithLaunchPath:launchPath arguments:arguments]; + } @catch (NSException* e) { + NSLog(@"%@: %@", e.name, e.reason); + } +} + +void LaunchMacPostProcess(const char* aAppBundle) { + MacAutoreleasePool pool; + + // Launch helper to perform post processing for the update; this is the Mac + // analogue of LaunchWinPostProcess (PostUpdateWin). + NSString* iniPath = [NSString stringWithUTF8String:aAppBundle]; + iniPath = [iniPath stringByAppendingPathComponent:@"Contents/Resources/updater.ini"]; + + NSFileManager* fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:iniPath]) { + // the file does not exist; there is nothing to run + return; + } + + int readResult; + mozilla::UniquePtr<char[]> values[2]; + readResult = + ReadStrings([iniPath UTF8String], "ExeRelPath\0ExeArg\0", 2, values, "PostUpdateMac"); + if (readResult) { + return; + } + + NSString* exeRelPath = [NSString stringWithUTF8String:values[0].get()]; + NSString* exeArg = [NSString stringWithUTF8String:values[1].get()]; + if (!exeArg || !exeRelPath) { + return; + } + + // The path must not traverse directories and it must be a relative path. + if ([exeRelPath isEqualToString:@".."] || [exeRelPath hasPrefix:@"/"] || + [exeRelPath hasPrefix:@"../"] || [exeRelPath hasSuffix:@"/.."] || + [exeRelPath containsString:@"/../"]) { + return; + } + + NSString* exeFullPath = [NSString stringWithUTF8String:aAppBundle]; + exeFullPath = [exeFullPath stringByAppendingPathComponent:exeRelPath]; + + mozilla::UniquePtr<char[]> optVal; + readResult = ReadStrings([iniPath UTF8String], "ExeAsync\0", 1, &optVal, "PostUpdateMac"); + + NSTask* task = [[NSTask alloc] init]; + [task setLaunchPath:exeFullPath]; + [task setArguments:[NSArray arrayWithObject:exeArg]]; + [task launch]; + if (!readResult) { + NSString* exeAsync = [NSString stringWithUTF8String:optVal.get()]; + if ([exeAsync isEqualToString:@"false"]) { + [task waitUntilExit]; + } + } + // ignore the return value of the task, there's nothing we can do with it + [task release]; +} + +id ConnectToUpdateServer() { + MacAutoreleasePool pool; + + id updateServer = nil; + BOOL isConnected = NO; + int currTry = 0; + const int numRetries = 10; // Number of IPC connection retries before + // giving up. + while (!isConnected && currTry < numRetries) { + @try { + updateServer = (id)[NSConnection + rootProxyForConnectionWithRegisteredName:@"org.mozilla.updater.server" + host:nil + usingNameServer:[NSSocketPortNameServer sharedInstance]]; + if (!updateServer || ![updateServer respondsToSelector:@selector(abort)] || + ![updateServer respondsToSelector:@selector(getArguments)] || + ![updateServer respondsToSelector:@selector(shutdown)]) { + NSLog(@"Server doesn't exist or doesn't provide correct selectors."); + sleep(1); // Wait 1 second. + currTry++; + } else { + isConnected = YES; + } + } @catch (NSException* e) { + NSLog(@"Encountered exception, retrying: %@: %@", e.name, e.reason); + sleep(1); // Wait 1 second. + currTry++; + } + } + if (!isConnected) { + NSLog(@"Failed to connect to update server after several retries."); + return nil; + } + return updateServer; +} + +void CleanupElevatedMacUpdate(bool aFailureOccurred) { + MacAutoreleasePool pool; + + id updateServer = ConnectToUpdateServer(); + if (updateServer) { + @try { + if (aFailureOccurred) { + [updateServer performSelector:@selector(abort)]; + } else { + [updateServer performSelector:@selector(shutdown)]; + } + } @catch (NSException* e) { + } + } + + NSFileManager* manager = [NSFileManager defaultManager]; + [manager removeItemAtPath:@"/Library/PrivilegedHelperTools/org.mozilla.updater" error:nil]; + [manager removeItemAtPath:@"/Library/LaunchDaemons/org.mozilla.updater.plist" error:nil]; + const char* launchctlArgs[] = {"/bin/launchctl", "remove", "org.mozilla.updater"}; + // The following call will terminate the current process due to the "remove" + // argument in launchctlArgs. + LaunchChild(3, launchctlArgs); +} + +// Note: Caller is responsible for freeing argv. +bool ObtainUpdaterArguments(int* argc, char*** argv) { + MacAutoreleasePool pool; + + id updateServer = ConnectToUpdateServer(); + if (!updateServer) { + // Let's try our best and clean up. + CleanupElevatedMacUpdate(true); + return false; // Won't actually get here due to CleanupElevatedMacUpdate. + } + + @try { + NSArray* updaterArguments = [updateServer performSelector:@selector(getArguments)]; + *argc = [updaterArguments count]; + char** tempArgv = (char**)malloc(sizeof(char*) * (*argc)); + for (int i = 0; i < *argc; i++) { + int argLen = [[updaterArguments objectAtIndex:i] length] + 1; + tempArgv[i] = (char*)malloc(argLen); + strncpy(tempArgv[i], [[updaterArguments objectAtIndex:i] UTF8String], argLen); + } + *argv = tempArgv; + } @catch (NSException* e) { + // Let's try our best and clean up. + CleanupElevatedMacUpdate(true); + return false; // Won't actually get here due to CleanupElevatedMacUpdate. + } + return true; +} + +/** + * The ElevatedUpdateServer is launched from a non-elevated updater process. + * It allows an elevated updater process (usually a privileged helper tool) to + * connect to it and receive all the necessary arguments to complete a + * successful update. + */ +@interface ElevatedUpdateServer : NSObject { + NSArray* mUpdaterArguments; + BOOL mShouldKeepRunning; + BOOL mAborted; +} +- (id)initWithArgs:(NSArray*)args; +- (BOOL)runServer; +- (NSArray*)getArguments; +- (void)abort; +- (BOOL)wasAborted; +- (void)shutdown; +- (BOOL)shouldKeepRunning; +@end + +@implementation ElevatedUpdateServer + +- (id)initWithArgs:(NSArray*)args { + self = [super init]; + if (!self) { + return nil; + } + mUpdaterArguments = args; + mShouldKeepRunning = YES; + mAborted = NO; + return self; +} + +- (BOOL)runServer { + NSPort* serverPort = [NSSocketPort port]; + NSConnection* server = [NSConnection connectionWithReceivePort:serverPort sendPort:serverPort]; + [server setRootObject:self]; + if ([server registerName:@"org.mozilla.updater.server" + withNameServer:[NSSocketPortNameServer sharedInstance]] == NO) { + NSLog(@"Unable to register as DirectoryServer."); + NSLog(@"Is another copy running?"); + return NO; + } + + while ([self shouldKeepRunning] && [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode + beforeDate:[NSDate distantFuture]]) + ; + return ![self wasAborted]; +} + +- (NSArray*)getArguments { + return mUpdaterArguments; +} + +- (void)abort { + mAborted = YES; + [self shutdown]; +} + +- (BOOL)wasAborted { + return mAborted; +} + +- (void)shutdown { + mShouldKeepRunning = NO; +} + +- (BOOL)shouldKeepRunning { + return mShouldKeepRunning; +} + +@end + +bool ServeElevatedUpdate(int argc, const char** argv) { + MacAutoreleasePool pool; + + NSMutableArray* updaterArguments = [NSMutableArray arrayWithCapacity:argc]; + for (int i = 0; i < argc; i++) { + [updaterArguments addObject:[NSString stringWithUTF8String:argv[i]]]; + } + + ElevatedUpdateServer* updater = + [[ElevatedUpdateServer alloc] initWithArgs:[updaterArguments copy]]; + bool didSucceed = [updater runServer]; + + [updater release]; + return didSucceed; +} + +bool IsOwnedByGroupAdmin(const char* aAppBundle) { + MacAutoreleasePool pool; + + NSString* appDir = [NSString stringWithUTF8String:aAppBundle]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + + NSDictionary* attributes = [fileManager attributesOfItemAtPath:appDir error:nil]; + bool isOwnedByAdmin = false; + if (attributes && [[attributes valueForKey:NSFileGroupOwnerAccountID] intValue] == 80) { + isOwnedByAdmin = true; + } + return isOwnedByAdmin; +} + +void SetGroupOwnershipAndPermissions(const char* aAppBundle) { + MacAutoreleasePool pool; + + NSString* appDir = [NSString stringWithUTF8String:aAppBundle]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSError* error = nil; + NSArray* paths = [fileManager subpathsOfDirectoryAtPath:appDir error:&error]; + if (error) { + return; + } + + // Set group ownership of Firefox.app to 80 ("admin") and permissions to + // 0775. + if (![fileManager setAttributes:@{ + NSFileGroupOwnerAccountID : @(80), + NSFilePosixPermissions : @(0775) + } + ofItemAtPath:appDir + error:&error] || + error) { + return; + } + + NSArray* permKeys = + [NSArray arrayWithObjects:NSFileGroupOwnerAccountID, NSFilePosixPermissions, nil]; + // For all descendants of Firefox.app, set group ownership to 80 ("admin") and + // ensure write permission for the group. + for (NSString* currPath in paths) { + NSString* child = [appDir stringByAppendingPathComponent:currPath]; + NSDictionary* oldAttributes = [fileManager attributesOfItemAtPath:child error:&error]; + if (error) { + return; + } + // Skip symlinks, since they could be pointing to files outside of the .app + // bundle. + if ([oldAttributes fileType] == NSFileTypeSymbolicLink) { + continue; + } + NSNumber* oldPerms = (NSNumber*)[oldAttributes valueForKey:NSFilePosixPermissions]; + NSArray* permObjects = [NSArray + arrayWithObjects:[NSNumber numberWithUnsignedLong:80], + [NSNumber numberWithUnsignedLong:[oldPerms shortValue] | 020], nil]; + NSDictionary* attributes = [NSDictionary dictionaryWithObjects:permObjects forKeys:permKeys]; + if (![fileManager setAttributes:attributes ofItemAtPath:child error:&error] || error) { + return; + } + } +} + +/** + * Helper to launch macOS tasks via NSTask. + */ +static void LaunchTask(NSString* aPath, NSArray* aArguments) { + if (@available(macOS 10.13, *)) { + NSTask* task = [[NSTask alloc] init]; + [task setExecutableURL:[NSURL fileURLWithPath:aPath]]; + if (aArguments) { + [task setArguments:aArguments]; + } + [task launchAndReturnError:nil]; + [task release]; + } else { + NSArray* arguments = aArguments; + if (!arguments) { + arguments = @[]; + } + [NSTask launchedTaskWithLaunchPath:aPath arguments:arguments]; + } +} + +static void RegisterAppWithLaunchServices(NSString* aBundlePath) { + NSArray* arguments = @[ @"-f", aBundlePath ]; + LaunchTask(@"/System/Library/Frameworks/CoreServices.framework/Frameworks/" + @"LaunchServices.framework/Support/lsregister", + arguments); +} + +static void StripQuarantineBit(NSString* aBundlePath) { + NSArray* arguments = @[ @"-d", @"com.apple.quarantine", aBundlePath ]; + LaunchTask(@"/usr/bin/xattr", arguments); +} + +bool PerformInstallationFromDMG(int argc, char** argv) { + MacAutoreleasePool pool; + if (argc < 4) { + return false; + } + NSString* bundlePath = [NSString stringWithUTF8String:argv[2]]; + NSString* destPath = [NSString stringWithUTF8String:argv[3]]; + if ([[NSFileManager defaultManager] copyItemAtPath:bundlePath toPath:destPath error:nil]) { + RegisterAppWithLaunchServices(destPath); + StripQuarantineBit(destPath); + return true; + } + return false; +} diff --git a/toolkit/mozapps/update/updater/loaddlls.cpp b/toolkit/mozapps/update/updater/loaddlls.cpp new file mode 100644 index 0000000000..462bd0bc18 --- /dev/null +++ b/toolkit/mozapps/update/updater/loaddlls.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include <windows.h> + +// Delayed load libraries are loaded when the first symbol is used. +// The following ensures that we load the delayed loaded libraries from the +// system directory. +struct AutoLoadSystemDependencies { + AutoLoadSystemDependencies() { + // Remove the current directory from the search path for dynamically loaded + // DLLs as a precaution. This call has no effect for delay load DLLs. + SetDllDirectory(L""); + + HMODULE module = ::GetModuleHandleW(L"kernel32.dll"); + if (module) { + // SetDefaultDllDirectories is always available on Windows 8 and above. It + // is also available on Windows Vista, Windows Server 2008, and + // Windows 7 when MS KB2533623 has been applied. + decltype(SetDefaultDllDirectories)* setDefaultDllDirectories = + (decltype(SetDefaultDllDirectories)*)GetProcAddress( + module, "SetDefaultDllDirectories"); + if (setDefaultDllDirectories) { + setDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32); + return; + } + } + + // When SetDefaultDllDirectories is not available, fallback to preloading + // dlls. The order that these are loaded does not matter since they are + // loaded using the LOAD_WITH_ALTERED_SEARCH_PATH flag. +#ifdef HAVE_64BIT_BUILD + // DLLs for Firefox x64 on Windows 7 (x64). + // Note: dwmapi.dll is preloaded since a crash will try to load it from the + // application's directory. + static LPCWSTR delayDLLs[] = { + L"apphelp.dll", L"cryptbase.dll", L"cryptsp.dll", L"dwmapi.dll", + L"mpr.dll", L"ntmarta.dll", L"profapi.dll", L"propsys.dll", + L"sspicli.dll", L"wsock32.dll"}; + +#else + // DLLs for Firefox x86 on Windows XP through Windows 7 (x86 and x64). + // Note: dwmapi.dll is preloaded since a crash will try to load it from the + // application's directory. + static LPCWSTR delayDLLs[] = { + L"apphelp.dll", L"crypt32.dll", L"cryptbase.dll", L"cryptsp.dll", + L"dwmapi.dll", L"mpr.dll", L"msasn1.dll", L"ntmarta.dll", + L"profapi.dll", L"propsys.dll", L"psapi.dll", L"secur32.dll", + L"sspicli.dll", L"userenv.dll", L"uxtheme.dll", L"ws2_32.dll", + L"ws2help.dll", L"wsock32.dll"}; +#endif + + WCHAR systemDirectory[MAX_PATH + 1] = {L'\0'}; + // If GetSystemDirectory fails we accept that we'll load the DLLs from the + // normal search path. + GetSystemDirectoryW(systemDirectory, MAX_PATH + 1); + size_t systemDirLen = wcslen(systemDirectory); + + // Make the system directory path terminate with a slash + if (systemDirectory[systemDirLen - 1] != L'\\' && systemDirLen) { + systemDirectory[systemDirLen] = L'\\'; + ++systemDirLen; + // No need to re-null terminate + } + + // For each known DLL ensure it is loaded from the system32 directory + for (size_t i = 0; i < sizeof(delayDLLs) / sizeof(delayDLLs[0]); ++i) { + size_t fileLen = wcslen(delayDLLs[i]); + wcsncpy(systemDirectory + systemDirLen, delayDLLs[i], + MAX_PATH - systemDirLen); + if (systemDirLen + fileLen <= MAX_PATH) { + systemDirectory[systemDirLen + fileLen] = L'\0'; + } else { + systemDirectory[MAX_PATH] = L'\0'; + } + LPCWSTR fullModulePath = systemDirectory; // just for code readability + // LOAD_WITH_ALTERED_SEARCH_PATH makes a dll look in its own directory for + // dependencies and is only available on Win 7 and below. + LoadLibraryExW(fullModulePath, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + } + } +} loadDLLs; diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in b/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in new file mode 100644 index 0000000000..b6d5f60153 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleDisplayName</key> + <string>updater</string> + <key>CFBundleExecutable</key> + <string>org.mozilla.updater</string> + <key>CFBundleIconFile</key> + <string>updater.icns</string> + <key>CFBundleIdentifier</key> + <string>org.mozilla.updater</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>updater</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1.0</string> + <key>LSHasLocalizedDisplayName</key> + <true/> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSRequiresAquaSystemAppearance</key> + <false/> + <key>NSPrincipalClass</key> + <string>NSApplication</string> + <key>LSUIElement</key> + <true/> + <key>SMAuthorizedClients</key> + <array> + <string>identifier "@MOZ_MACBUNDLE_ID@" and ((anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9]) or (anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = "43AQ936H96"))</string> + </array> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in new file mode 100644 index 0000000000..e8036ec8cc --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in @@ -0,0 +1,8 @@ +/* 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/. */ + +/* Localized versions of Info.plist keys */ + +CFBundleName = "Software Update"; +CFBundleDisplayName = "@APP_NAME@ Software Update"; diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib new file mode 100644 index 0000000000..6cfb50406b --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,19 @@ +{ + IBClasses = ( + { + CLASS = FirstResponder; + LANGUAGE = ObjC; + SUPERCLASS = NSObject; +}, + { + CLASS = UpdaterUI; + LANGUAGE = ObjC; + OUTLETS = { + progressBar = NSProgressIndicator; + progressTextField = NSTextField; + }; + SUPERCLASS = NSObject; +} + ); + IBVersion = 1; +}
\ No newline at end of file diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib new file mode 100644 index 0000000000..1509178370 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IBDocumentLocation</key> + <string>111 162 356 240 0 0 1440 878 </string> + <key>IBEditorPositions</key> + <dict> + <key>29</key> + <string>106 299 84 44 0 0 1440 878 </string> + </dict> + <key>IBFramework Version</key> + <string>489.0</string> + <key>IBOpenObjects</key> + <array> + <integer>21</integer> + <integer>29</integer> + </array> + <key>IBSystem Version</key> + <string>10J567</string> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns Binary files differnew file mode 100644 index 0000000000..d7499c6692 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns diff --git a/toolkit/mozapps/update/updater/module.ver b/toolkit/mozapps/update/updater/module.ver new file mode 100644 index 0000000000..771416bb11 --- /dev/null +++ b/toolkit/mozapps/update/updater/module.ver @@ -0,0 +1 @@ +WIN32_MODULE_DESCRIPTION=@MOZ_APP_DISPLAYNAME@ Software Updater diff --git a/toolkit/mozapps/update/updater/moz.build b/toolkit/mozapps/update/updater/moz.build new file mode 100644 index 0000000000..40d7a77a6b --- /dev/null +++ b/toolkit/mozapps/update/updater/moz.build @@ -0,0 +1,76 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + Program("org.mozilla.updater") +else: + Program("updater") + +updater_rel_path = "" +include("updater-common.build") +DIRS += ["updater-dep"] +if CONFIG["ENABLE_TESTS"]: + DIRS += ["updater-xpcshell"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + LDFLAGS += [ + "-sectcreate", + "__TEXT", + "__info_plist", + TOPOBJDIR + "/dist/bin/Info.plist", + "-sectcreate", + "__TEXT", + "__launchd_plist", + SRCDIR + "/Launchd.plist", + ] + +GENERATED_FILES = [ + "dep1Cert.h", + "dep2Cert.h", + "primaryCert.h", + "secondaryCert.h", + "xpcshellCert.h", +] + +primary_cert = GENERATED_FILES["primaryCert.h"] +secondary_cert = GENERATED_FILES["secondaryCert.h"] + +# This is how the xpcshellCertificate.der file is generated, in case we ever +# have to regenerate it. +# ./certutil -L -d modules/libmar/tests/unit/data -n mycert -r > xpcshellCertificate.der +xpcshell_cert = GENERATED_FILES["xpcshellCert.h"] +dep1_cert = GENERATED_FILES["dep1Cert.h"] +dep2_cert = GENERATED_FILES["dep2Cert.h"] + +primary_cert.script = "gen_cert_header.py:create_header" +secondary_cert.script = "gen_cert_header.py:create_header" +xpcshell_cert.script = "gen_cert_header.py:create_header" +dep1_cert.script = "gen_cert_header.py:create_header" +dep2_cert.script = "gen_cert_header.py:create_header" + +if CONFIG["MOZ_UPDATE_CHANNEL"] in ("beta", "release", "esr"): + primary_cert.inputs += ["release_primary.der"] + secondary_cert.inputs += ["release_secondary.der"] +elif CONFIG["MOZ_UPDATE_CHANNEL"] in ( + "nightly", + "aurora", + "nightly-elm", + "nightly-profiling", + "nightly-oak", + "nightly-ux", +): + primary_cert.inputs += ["nightly_aurora_level3_primary.der"] + secondary_cert.inputs += ["nightly_aurora_level3_secondary.der"] +else: + primary_cert.inputs += ["dep1.der"] + secondary_cert.inputs += ["dep2.der"] + +dep1_cert.inputs += ["dep1.der"] +dep2_cert.inputs += ["dep2.der"] +xpcshell_cert.inputs += ["xpcshellCertificate.der"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + FINAL_TARGET_FILES.icons += ["updater.png"] diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der Binary files differnew file mode 100644 index 0000000000..44fd95dcff --- /dev/null +++ b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der Binary files differnew file mode 100644 index 0000000000..90f8e6e82c --- /dev/null +++ b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der diff --git a/toolkit/mozapps/update/updater/progressui.h b/toolkit/mozapps/update/updater/progressui.h new file mode 100644 index 0000000000..e283c4d1cd --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef PROGRESSUI_H__ +#define PROGRESSUI_H__ + +#include "updatedefines.h" + +#if defined(XP_WIN) +typedef WCHAR NS_tchar; +# define NS_main wmain +#else +typedef char NS_tchar; +# define NS_main main +#endif + +// Called to perform any initialization of the widget toolkit +int InitProgressUI(int* argc, NS_tchar*** argv); + +#if defined(XP_WIN) +// Called on the main thread at startup +int ShowProgressUI(bool indeterminate = false, bool initUIStrings = true); +int InitProgressUIStrings(); +#elif defined(XP_MACOSX) +// Called on the main thread at startup +int ShowProgressUI(bool indeterminate = false); +#else +// Called on the main thread at startup +int ShowProgressUI(); +#endif +// May be called from any thread +void QuitProgressUI(); + +// May be called from any thread: progress is a number between 0 and 100 +void UpdateProgressUI(float progress); + +#endif // PROGRESSUI_H__ diff --git a/toolkit/mozapps/update/updater/progressui_gtk.cpp b/toolkit/mozapps/update/updater/progressui_gtk.cpp new file mode 100644 index 0000000000..cfdcd5587c --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_gtk.cpp @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include <gtk/gtk.h> +#include <unistd.h> +#include "mozilla/Sprintf.h" +#include "mozilla/Atomics.h" +#include "progressui.h" +#include "readstrings.h" +#include "updatererrors.h" + +#define TIMER_INTERVAL 100 + +static float sProgressVal; // between 0 and 100 +static mozilla::Atomic<gboolean> sQuit(FALSE); +static gboolean sEnableUI; +static guint sTimerID; + +static GtkWidget* sWin; +static GtkWidget* sLabel; +static GtkWidget* sProgressBar; +static GdkPixbuf* sPixbuf; + +StringTable sStrings; + +static gboolean UpdateDialog(gpointer data) { + if (sQuit) { + gtk_widget_hide(sWin); + gtk_main_quit(); + } + + float progress = sProgressVal; + + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(sProgressBar), + progress / 100.0); + + return TRUE; +} + +static gboolean OnDeleteEvent(GtkWidget* widget, GdkEvent* event, + gpointer user_data) { + return TRUE; +} + +int InitProgressUI(int* pargc, char*** pargv) { + sEnableUI = gtk_init_check(pargc, pargv); + if (sEnableUI) { + // Prepare to show the UI here in case the files are modified by the update. + char ini_path[PATH_MAX]; + SprintfLiteral(ini_path, "%s.ini", (*pargv)[0]); + if (ReadStrings(ini_path, &sStrings) != OK) { + sEnableUI = false; + return -1; + } + + char icon_path[PATH_MAX]; + SprintfLiteral(icon_path, "%s/icons/updater.png", (*pargv)[2]); + sPixbuf = gdk_pixbuf_new_from_file(icon_path, nullptr); + } + return 0; +} + +int ShowProgressUI() { + if (!sEnableUI) { + return -1; + } + + // Only show the Progress UI if the process is taking a significant amount of + // time where a significant amount of time is defined as .5 seconds after + // ShowProgressUI is called sProgress is less than 70. + usleep(500000); + + if (sQuit || sProgressVal > 70.0f) { + return 0; + } + + sWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); + if (!sWin) { + return -1; + } + + g_signal_connect(G_OBJECT(sWin), "delete_event", G_CALLBACK(OnDeleteEvent), + nullptr); + + gtk_window_set_title(GTK_WINDOW(sWin), sStrings.title.get()); + gtk_window_set_type_hint(GTK_WINDOW(sWin), GDK_WINDOW_TYPE_HINT_DIALOG); + gtk_window_set_position(GTK_WINDOW(sWin), GTK_WIN_POS_CENTER_ALWAYS); + gtk_window_set_resizable(GTK_WINDOW(sWin), FALSE); + gtk_window_set_decorated(GTK_WINDOW(sWin), TRUE); + gtk_window_set_deletable(GTK_WINDOW(sWin), FALSE); + gtk_window_set_icon(GTK_WINDOW(sWin), sPixbuf); + g_object_unref(sPixbuf); + + GtkWidget* vbox = gtk_vbox_new(TRUE, 6); + sLabel = gtk_label_new(sStrings.info.get()); + gtk_misc_set_alignment(GTK_MISC(sLabel), 0.0f, 0.0f); + sProgressBar = gtk_progress_bar_new(); + + gtk_box_pack_start(GTK_BOX(vbox), sLabel, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), sProgressBar, TRUE, TRUE, 0); + + sTimerID = g_timeout_add(TIMER_INTERVAL, UpdateDialog, nullptr); + + gtk_container_set_border_width(GTK_CONTAINER(sWin), 10); + gtk_container_add(GTK_CONTAINER(sWin), vbox); + gtk_widget_show_all(sWin); + + gtk_main(); + return 0; +} + +// Called on a background thread +void QuitProgressUI() { sQuit = TRUE; } + +// Called on a background thread +void UpdateProgressUI(float progress) { + sProgressVal = progress; // 32-bit writes are atomic +} diff --git a/toolkit/mozapps/update/updater/progressui_null.cpp b/toolkit/mozapps/update/updater/progressui_null.cpp new file mode 100644 index 0000000000..49877b2faf --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_null.cpp @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include "progressui.h" + +int InitProgressUI(int* argc, char*** argv) { return 0; } + +int ShowProgressUI() { return 0; } + +void QuitProgressUI() {} + +void UpdateProgressUI(float progress) {} diff --git a/toolkit/mozapps/update/updater/progressui_osx.mm b/toolkit/mozapps/update/updater/progressui_osx.mm new file mode 100644 index 0000000000..b6e01ea71e --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_osx.mm @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#import <Cocoa/Cocoa.h> +#include <stdio.h> +#include <unistd.h> +#include "mozilla/Sprintf.h" +#include "progressui.h" +#include "readstrings.h" +#include "updatererrors.h" + +#define TIMER_INTERVAL 0.2 + +static float sProgressVal; // between 0 and 100 +static BOOL sQuit = NO; +static BOOL sIndeterminate = NO; +static StringTable sLabels; +static const char* sUpdatePath; + +@interface UpdaterUI : NSObject { + IBOutlet NSProgressIndicator* progressBar; + IBOutlet NSTextField* progressTextField; +} +@end + +@implementation UpdaterUI + +- (void)awakeFromNib { + NSWindow* w = [progressBar window]; + + [w setTitle:[NSString stringWithUTF8String:sLabels.title.get()]]; + [progressTextField setStringValue:[NSString stringWithUTF8String:sLabels.info.get()]]; + + NSRect origTextFrame = [progressTextField frame]; + [progressTextField sizeToFit]; + + int widthAdjust = progressTextField.frame.size.width - origTextFrame.size.width; + + if (widthAdjust > 0) { + NSRect f; + f.size.width = w.frame.size.width + widthAdjust; + f.size.height = w.frame.size.height; + [w setFrame:f display:YES]; + } + + [w center]; + + [progressBar setIndeterminate:sIndeterminate]; + [progressBar setDoubleValue:0.0]; + + [[NSTimer scheduledTimerWithTimeInterval:TIMER_INTERVAL + target:self + selector:@selector(updateProgressUI:) + userInfo:nil + repeats:YES] retain]; + + // Make sure we are on top initially + [NSApp activateIgnoringOtherApps:YES]; +} + +// called when the timer goes off +- (void)updateProgressUI:(NSTimer*)aTimer { + if (sQuit) { + [aTimer invalidate]; + [aTimer release]; + + // It seems to be necessary to activate and hide ourselves before we stop, + // otherwise the "run" method will not return until the user focuses some + // other app. The activate step is necessary if we are not the active app. + // This is a big hack, but it seems to do the trick. + [NSApp activateIgnoringOtherApps:YES]; + [NSApp hide:self]; + [NSApp stop:self]; + } + + float progress = sProgressVal; + + [progressBar setDoubleValue:(double)progress]; +} + +// leave this as returning a BOOL instead of NSApplicationTerminateReply +// for backward compatibility +- (BOOL)applicationShouldTerminate:(NSApplication*)sender { + return sQuit; +} + +@end + +int InitProgressUI(int* pargc, char*** pargv) { + sUpdatePath = (*pargv)[1]; + + return 0; +} + +int ShowProgressUI(bool indeterminate) { + if (!sUpdatePath) { + // InitProgressUI was never called. + return -1; + } + + // Only show the Progress UI if the process is taking a significant amount of + // time where a significant amount of time is defined as .5 seconds after + // ShowProgressUI is called sProgress is less than 70. + usleep(500000); + + if (sQuit || sProgressVal > 70.0f) { + return 0; + } + + char path[PATH_MAX]; + SprintfLiteral(path, "%s/updater.ini", sUpdatePath); + if (ReadStrings(path, &sLabels) != OK) { + return -1; + } + + sIndeterminate = indeterminate; + [NSApplication sharedApplication]; + [[NSBundle mainBundle] loadNibNamed:@"MainMenu" owner:NSApp topLevelObjects:nil]; + [NSApp run]; + + return 0; +} + +// Called on a background thread +void QuitProgressUI() { sQuit = YES; } + +// Called on a background thread +void UpdateProgressUI(float progress) { + sProgressVal = progress; // 32-bit writes are atomic +} diff --git a/toolkit/mozapps/update/updater/progressui_win.cpp b/toolkit/mozapps/update/updater/progressui_win.cpp new file mode 100644 index 0000000000..51bd2d8cce --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_win.cpp @@ -0,0 +1,302 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include <stdio.h> +#include <windows.h> +#include <commctrl.h> +#include <process.h> +#include <io.h> + +#include "resource.h" +#include "progressui.h" +#include "readstrings.h" +#include "updatererrors.h" + +#define TIMER_ID 1 +#define TIMER_INTERVAL 100 + +#define RESIZE_WINDOW(hwnd, extrax, extray) \ + { \ + RECT windowSize; \ + GetWindowRect(hwnd, &windowSize); \ + SetWindowPos(hwnd, 0, 0, 0, windowSize.right - windowSize.left + extrax, \ + windowSize.bottom - windowSize.top + extray, \ + SWP_NOMOVE | SWP_NOZORDER); \ + } + +#define MOVE_WINDOW(hwnd, dx, dy) \ + { \ + RECT rc; \ + POINT pt; \ + GetWindowRect(hwnd, &rc); \ + pt.x = rc.left; \ + pt.y = rc.top; \ + ScreenToClient(GetParent(hwnd), &pt); \ + SetWindowPos(hwnd, 0, pt.x + dx, pt.y + dy, 0, 0, \ + SWP_NOSIZE | SWP_NOZORDER); \ + } + +static float sProgress; // between 0 and 100 +static BOOL sQuit = FALSE; +static BOOL sIndeterminate = FALSE; +static StringTable sUIStrings; + +static BOOL GetStringsFile(WCHAR filename[MAX_PATH]) { + if (!GetModuleFileNameW(nullptr, filename, MAX_PATH)) { + return FALSE; + } + + WCHAR* dot = wcsrchr(filename, '.'); + if (!dot || wcsicmp(dot + 1, L"exe")) { + return FALSE; + } + + wcscpy(dot + 1, L"ini"); + return TRUE; +} + +static void UpdateDialog(HWND hDlg) { + int pos = int(sProgress + 0.5f); + HWND hWndPro = GetDlgItem(hDlg, IDC_PROGRESS); + SendMessage(hWndPro, PBM_SETPOS, pos, 0L); +} + +// The code in this function is from MSDN: +// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/dialogboxes/usingdialogboxes.asp +static void CenterDialog(HWND hDlg) { + RECT rc, rcOwner, rcDlg; + + // Get the owner window and dialog box rectangles. + HWND desktop = GetDesktopWindow(); + + GetWindowRect(desktop, &rcOwner); + GetWindowRect(hDlg, &rcDlg); + CopyRect(&rc, &rcOwner); + + // Offset the owner and dialog box rectangles so that + // right and bottom values represent the width and + // height, and then offset the owner again to discard + // space taken up by the dialog box. + + OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top); + OffsetRect(&rc, -rc.left, -rc.top); + OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom); + + // The new position is the sum of half the remaining + // space and the owner's original position. + + SetWindowPos(hDlg, HWND_TOP, rcOwner.left + (rc.right / 2), + rcOwner.top + (rc.bottom / 2), 0, 0, // ignores size arguments + SWP_NOSIZE); +} + +static void InitDialog(HWND hDlg) { + mozilla::UniquePtr<WCHAR[]> szwTitle; + mozilla::UniquePtr<WCHAR[]> szwInfo; + + int bufferSize = + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.title.get(), -1, nullptr, 0); + szwTitle = mozilla::MakeUnique<WCHAR[]>(bufferSize); + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.title.get(), -1, szwTitle.get(), + bufferSize); + bufferSize = + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.info.get(), -1, nullptr, 0); + szwInfo = mozilla::MakeUnique<WCHAR[]>(bufferSize); + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.info.get(), -1, szwInfo.get(), + bufferSize); + + SetWindowTextW(hDlg, szwTitle.get()); + SetWindowTextW(GetDlgItem(hDlg, IDC_INFO), szwInfo.get()); + + // Set dialog icon + HICON hIcon = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDI_DIALOG)); + if (hIcon) { + SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon); + } + + HWND hWndPro = GetDlgItem(hDlg, IDC_PROGRESS); + SendMessage(hWndPro, PBM_SETRANGE, 0, MAKELPARAM(0, 100)); + if (sIndeterminate) { + LONG_PTR val = GetWindowLongPtr(hWndPro, GWL_STYLE); + SetWindowLongPtr(hWndPro, GWL_STYLE, val | PBS_MARQUEE); + SendMessage(hWndPro, (UINT)PBM_SETMARQUEE, (WPARAM)TRUE, (LPARAM)50); + } + + // Resize the dialog to fit all of the text if necessary. + RECT infoSize, textSize; + HWND hWndInfo = GetDlgItem(hDlg, IDC_INFO); + + // Get the control's font for calculating the new size for the control + HDC hDCInfo = GetDC(hWndInfo); + HFONT hInfoFont, hOldFont = NULL; + hInfoFont = (HFONT)SendMessage(hWndInfo, WM_GETFONT, 0, 0); + + if (hInfoFont) { + hOldFont = (HFONT)SelectObject(hDCInfo, hInfoFont); + } + + // Measure the space needed for the text on a single line. DT_CALCRECT means + // nothing is drawn. + if (DrawText(hDCInfo, szwInfo.get(), -1, &textSize, + DT_CALCRECT | DT_NOCLIP | DT_SINGLELINE)) { + GetClientRect(hWndInfo, &infoSize); + SIZE extra; + // Calculate the additional space needed for the text by subtracting from + // the rectangle returned by DrawText the existing client rectangle's width + // and height. + extra.cx = + (textSize.right - textSize.left) - (infoSize.right - infoSize.left); + extra.cy = + (textSize.bottom - textSize.top) - (infoSize.bottom - infoSize.top); + if (extra.cx < 0) { + extra.cx = 0; + } + if (extra.cy < 0) { + extra.cy = 0; + } + if ((extra.cx > 0) || (extra.cy > 0)) { + RESIZE_WINDOW(hDlg, extra.cx, extra.cy); + RESIZE_WINDOW(hWndInfo, extra.cx, extra.cy); + RESIZE_WINDOW(hWndPro, extra.cx, 0); + MOVE_WINDOW(hWndPro, 0, extra.cy); + } + } + + if (hOldFont) { + SelectObject(hDCInfo, hOldFont); + } + + ReleaseDC(hWndInfo, hDCInfo); + + CenterDialog(hDlg); // make dialog appear in the center of the screen + + SetTimer(hDlg, TIMER_ID, TIMER_INTERVAL, nullptr); +} + +// Message handler for update dialog. +static LRESULT CALLBACK DialogProc(HWND hDlg, UINT message, WPARAM wParam, + LPARAM lParam) { + switch (message) { + case WM_INITDIALOG: + InitDialog(hDlg); + return TRUE; + + case WM_TIMER: + if (sQuit) { + EndDialog(hDlg, 0); + } else { + UpdateDialog(hDlg); + } + return TRUE; + + case WM_COMMAND: + return TRUE; + } + return FALSE; +} + +int InitProgressUI(int* argc, WCHAR*** argv) { return 0; } + +/** + * Initializes the progress UI strings + * + * @return 0 on success, -1 on error + */ +int InitProgressUIStrings() { + // If we do not have updater.ini, then we should not bother showing UI. + WCHAR filename[MAX_PATH]; + if (!GetStringsFile(filename)) { + return -1; + } + + if (_waccess(filename, 04)) { + return -1; + } + + // If the updater.ini doesn't have the required strings, then we should not + // bother showing UI. + if (ReadStrings(filename, &sUIStrings) != OK) { + return -1; + } + + return 0; +} + +int ShowProgressUI(bool indeterminate, bool initUIStrings) { + sIndeterminate = indeterminate; + if (!indeterminate) { + // Only show the Progress UI if the process is taking a significant amount + // of time where a significant amount of time is defined as .5 seconds after + // ShowProgressUI is called sProgress is less than 70. + Sleep(500); + + if (sQuit || sProgress > 70.0f) { + return 0; + } + } + + // Don't load the UI if there's an <exe_name>.Local directory for redirection. + WCHAR appPath[MAX_PATH + 1] = {L'\0'}; + if (!GetModuleFileNameW(nullptr, appPath, MAX_PATH)) { + return -1; + } + + if (wcslen(appPath) + wcslen(L".Local") >= MAX_PATH) { + return -1; + } + + wcscat(appPath, L".Local"); + + if (!_waccess(appPath, 04)) { + return -1; + } + + // Don't load the UI if the strings for the UI are not provided. + if (initUIStrings && InitProgressUIStrings() == -1) { + return -1; + } + + if (!GetModuleFileNameW(nullptr, appPath, MAX_PATH)) { + return -1; + } + + // Use an activation context that supports visual styles for the controls. + ACTCTXW actx = {0}; + actx.cbSize = sizeof(ACTCTXW); + actx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_HMODULE_VALID; + actx.hModule = GetModuleHandle(NULL); // Use the embedded manifest + // This is needed only for Win XP but doesn't cause a problem with other + // versions of Windows. + actx.lpSource = appPath; + actx.lpResourceName = MAKEINTRESOURCE(IDR_COMCTL32_MANIFEST); + + HANDLE hactx = INVALID_HANDLE_VALUE; + hactx = CreateActCtxW(&actx); + ULONG_PTR actxCookie = NULL; + if (hactx != INVALID_HANDLE_VALUE) { + // Push the specified activation context to the top of the activation stack. + ActivateActCtx(hactx, &actxCookie); + } + + INITCOMMONCONTROLSEX icc = {sizeof(INITCOMMONCONTROLSEX), ICC_PROGRESS_CLASS}; + InitCommonControlsEx(&icc); + + DialogBox(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDD_DIALOG), nullptr, + (DLGPROC)DialogProc); + + if (hactx != INVALID_HANDLE_VALUE) { + // Deactivate the context now that the comctl32.dll is loaded. + DeactivateActCtx(0, actxCookie); + } + + return 0; +} + +void QuitProgressUI() { sQuit = TRUE; } + +void UpdateProgressUI(float progress) { + sProgress = progress; // 32-bit writes are atomic +} diff --git a/toolkit/mozapps/update/updater/release_primary.der b/toolkit/mozapps/update/updater/release_primary.der Binary files differnew file mode 100644 index 0000000000..1d94f88ad7 --- /dev/null +++ b/toolkit/mozapps/update/updater/release_primary.der diff --git a/toolkit/mozapps/update/updater/release_secondary.der b/toolkit/mozapps/update/updater/release_secondary.der Binary files differnew file mode 100644 index 0000000000..474706c4b7 --- /dev/null +++ b/toolkit/mozapps/update/updater/release_secondary.der diff --git a/toolkit/mozapps/update/updater/resource.h b/toolkit/mozapps/update/updater/resource.h new file mode 100644 index 0000000000..1dcb47fca1 --- /dev/null +++ b/toolkit/mozapps/update/updater/resource.h @@ -0,0 +1,29 @@ +/* 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/. */ + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by updater.rc +// +#define IDD_DIALOG 101 +#define IDC_PROGRESS 1000 +#define IDC_INFO 1002 +#define IDI_DIALOG 1003 +#define TYPE_CERT 512 +#define IDR_PRIMARY_CERT 1004 +#define IDR_BACKUP_CERT 1005 +#define IDS_UPDATER_IDENTITY 1006 +#define IDR_XPCSHELL_CERT 1007 +#define IDR_COMCTL32_MANIFEST 17 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +# ifndef APSTUDIO_READONLY_SYMBOLS +# define _APS_NEXT_RESOURCE_VALUE 102 +# define _APS_NEXT_COMMAND_VALUE 40001 +# define _APS_NEXT_CONTROL_VALUE 1008 +# define _APS_NEXT_SYMED_VALUE 101 +# endif +#endif diff --git a/toolkit/mozapps/update/updater/updater-common.build b/toolkit/mozapps/update/updater/updater-common.build new file mode 100644 index 0000000000..16c2d15003 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-common.build @@ -0,0 +1,120 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +srcs = [ + "archivereader.cpp", + "updater.cpp", +] + +have_progressui = 0 + +if CONFIG["MOZ_VERIFY_MAR_SIGNATURE"]: + USE_LIBS += [ + "verifymar", + ] + +if CONFIG["OS_ARCH"] == "WINNT": + have_progressui = 1 + srcs += [ + "loaddlls.cpp", + "progressui_win.cpp", + ] + RCINCLUDE = "%supdater.rc" % updater_rel_path + DEFINES["UNICODE"] = True + DEFINES["_UNICODE"] = True + USE_STATIC_LIBS = True + + # Pick up nsWindowsRestart.cpp + LOCAL_INCLUDES += [ + "/toolkit/xre", + ] + OS_LIBS += [ + "comctl32", + "ws2_32", + "shell32", + "shlwapi", + "crypt32", + "advapi32", + "gdi32", + "user32", + "userenv", + "uuid", + ] + +USE_LIBS += [ + "bspatch", + "mar", + "updatecommon", + "xz-embedded", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + have_progressui = 1 + srcs += [ + "progressui_gtk.cpp", + ] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + have_progressui = 1 + srcs += [ + "launchchild_osx.mm", + "progressui_osx.mm", + ] + OS_LIBS += [ + "-framework Cocoa", + "-framework Security", + "-framework SystemConfiguration", + ] + UNIFIED_SOURCES += [ + "/toolkit/xre/updaterfileutils_osx.mm", + ] + LOCAL_INCLUDES += [ + "/toolkit/xre", + ] + +if have_progressui == 0: + srcs += [ + "progressui_null.cpp", + ] + +SOURCES += sorted(srcs) + +if CONFIG["MOZ_TSAN"]: + # Since mozglue is not linked to the updater, + # we need to include our own TSan suppression list. + SOURCES += [ + "TsanOptions.cpp", + ] + +DEFINES["SPRINTF_H_USES_VSNPRINTF"] = True +DEFINES["NS_NO_XPCOM"] = True +DisableStlWrapping() +for var in ("MAR_CHANNEL_ID", "MOZ_APP_VERSION"): + DEFINES[var] = '"%s"' % CONFIG[var] + +LOCAL_INCLUDES += [ + "/toolkit/mozapps/update/common", + "/xpcom/base", # for nsVersionComparator.cpp +] + +DELAYLOAD_DLLS += [ + "crypt32.dll", + "comctl32.dll", + "userenv.dll", + "wsock32.dll", +] + +if CONFIG["CC_TYPE"] == "clang-cl": + WIN32_EXE_LDFLAGS += ["-ENTRY:wmainCRTStartup"] +elif CONFIG["OS_ARCH"] == "WINNT": + WIN32_EXE_LDFLAGS += ["-municode"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + OS_LIBS += CONFIG["MOZ_GTK3_LIBS"] + +if CONFIG["CC_TYPE"] == "gcc": + CXXFLAGS += ["-Wno-format-truncation"] diff --git a/toolkit/mozapps/update/updater/updater-dep/moz.build b/toolkit/mozapps/update/updater/updater-dep/moz.build new file mode 100644 index 0000000000..89c148987c --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-dep/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +FINAL_TARGET = "_tests/updater-dep" + +Program("updater-dep") + +updater_rel_path = "../" +DEFINES["DEP_UPDATER"] = True +include("../updater-common.build") diff --git a/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in new file mode 100644 index 0000000000..00a43f12d7 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in @@ -0,0 +1,48 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# 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/. + +# For changes here, also consider ../Makefile.in + +XPCSHELLTESTDIR = $(topobjdir)/_tests/xpcshell/toolkit/mozapps/update/tests + +ifeq (,$(MOZ_SUITE)$(MOZ_THUNDERBIRD)) +MOCHITESTBROWSERDIR = $(topobjdir)/_tests/testing/mochitest/browser/toolkit/mozapps/update/tests/browser +endif + +ifndef MOZ_WINCONSOLE +ifdef MOZ_DEBUG +MOZ_WINCONSOLE = 1 +else +MOZ_WINCONSOLE = 0 +endif +endif + +include $(topsrcdir)/config/rules.mk + +ifneq (,$(COMPILE_ENVIRONMENT)$(MOZ_ARTIFACT_BUILDS)) +tools:: +ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) + # Copy for xpcshell tests + $(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app + rsync -a -C --exclude '*.in' $(srcdir)/../macbuild/Contents $(XPCSHELLTESTDIR)/data/updater-xpcshell.app + $(call py_action,preprocessor,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/../macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in -o $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings) + $(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS + $(NSINSTALL) $(FINAL_TARGET)/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS + rm -Rf $(XPCSHELLTESTDIR)/data/updater.app + mv $(XPCSHELLTESTDIR)/data/updater-xpcshell.app $(XPCSHELLTESTDIR)/data/updater.app + mv $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/org.mozilla.updater + +ifdef MOCHITESTBROWSERDIR + rsync -a -C $(XPCSHELLTESTDIR)/data/updater.app $(MOCHITESTBROWSERDIR)/ +endif +else + $(MKDIR) -p $(XPCSHELLTESTDIR)/data + cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(XPCSHELLTESTDIR)/data/updater$(BIN_SUFFIX) +ifdef MOCHITESTBROWSERDIR + $(MKDIR) -p $(MOCHITESTBROWSERDIR) + cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(MOCHITESTBROWSERDIR)/updater$(BIN_SUFFIX) +endif +endif +endif diff --git a/toolkit/mozapps/update/updater/updater-xpcshell/moz.build b/toolkit/mozapps/update/updater/updater-xpcshell/moz.build new file mode 100644 index 0000000000..33558a2c59 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-xpcshell/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +FINAL_TARGET = "_tests/xpcshell/toolkit/mozapps/update/tests" + +Program("updater-xpcshell") + +updater_rel_path = "../" +DEFINES["TEST_UPDATER"] = True +include("../updater-common.build") diff --git a/toolkit/mozapps/update/updater/updater.cpp b/toolkit/mozapps/update/updater/updater.cpp new file mode 100644 index 0000000000..844230fa19 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.cpp @@ -0,0 +1,4741 @@ +/* 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/. */ + +/** + * Manifest Format + * --------------- + * + * contents = 1*( line ) + * line = method LWS *( param LWS ) CRLF + * CRLF = "\r\n" + * LWS = 1*( " " | "\t" ) + * + * Available methods for the manifest file: + * + * updatev3.manifest + * ----------------- + * method = "add" | "add-if" | "add-if-not" | "patch" | "patch-if" | + * "remove" | "rmdir" | "rmrfdir" | type + * + * 'add-if-not' adds a file if it doesn't exist. + * + * 'type' is the update type (e.g. complete or partial) and when present MUST + * be the first entry in the update manifest. The type is used to support + * removing files that no longer exist when when applying a complete update by + * causing the actions defined in the precomplete file to be performed. + * + * precomplete + * ----------- + * method = "remove" | "rmdir" + */ +#include "bspatch.h" +#include "progressui.h" +#include "archivereader.h" +#include "readstrings.h" +#include "updatererrors.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#include <sys/stat.h> +#include <fcntl.h> +#include <limits.h> +#include <errno.h> + +#include "updatecommon.h" +#ifdef XP_MACOSX +# include "updaterfileutils_osx.h" +#endif // XP_MACOSX + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/UniquePtr.h" +#ifdef XP_WIN +# include "mozilla/Maybe.h" +# include "mozilla/WinHeaderOnlyUtils.h" +# include <climits> +#endif // XP_WIN + +// Amount of the progress bar to use in each of the 3 update stages, +// should total 100.0. +#define PROGRESS_PREPARE_SIZE 20.0f +#define PROGRESS_EXECUTE_SIZE 75.0f +#define PROGRESS_FINISH_SIZE 5.0f + +// Maximum amount of time in ms to wait for the parent process to close. The 30 +// seconds is rather long but there have been bug reports where the parent +// process has exited after 10 seconds and it is better to give it a chance. +#define PARENT_WAIT 30000 + +#if defined(XP_MACOSX) +// These functions are defined in launchchild_osx.mm +void CleanupElevatedMacUpdate(bool aFailureOccurred); +bool IsOwnedByGroupAdmin(const char* aAppBundle); +bool IsRecursivelyWritable(const char* aPath); +void LaunchChild(int argc, const char** argv); +void LaunchMacPostProcess(const char* aAppBundle); +bool ObtainUpdaterArguments(int* argc, char*** argv); +bool ServeElevatedUpdate(int argc, const char** argv); +void SetGroupOwnershipAndPermissions(const char* aAppBundle); +bool PerformInstallationFromDMG(int argc, char** argv); +struct UpdateServerThreadArgs { + int argc; + const NS_tchar** argv; +}; +#endif + +#ifndef _O_BINARY +# define _O_BINARY 0 +#endif + +#ifndef NULL +# define NULL (0) +#endif + +#ifndef SSIZE_MAX +# define SSIZE_MAX LONG_MAX +#endif + +// We want to use execv to invoke the callback executable on platforms where +// we were launched using execv. See nsUpdateDriver.cpp. +#if defined(XP_UNIX) && !defined(XP_MACOSX) +# define USE_EXECV +#endif + +#if defined(XP_OPENBSD) +# define stat64 stat +#endif + +#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX) +# include "nss.h" +# include "prerror.h" +#endif + +#include "crctable.h" + +#ifdef XP_WIN +# ifdef MOZ_MAINTENANCE_SERVICE +# include "registrycertificates.h" +# endif +BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); +BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, + LPCWSTR newFileName); +# include "updatehelper.h" + +// Closes the handle if valid and if the updater is elevated returns with the +// return code specified. This prevents multiple launches of the callback +// application by preventing the elevated process from launching the callback. +# define EXIT_WHEN_ELEVATED(path, handle, retCode) \ + { \ + if (handle != INVALID_HANDLE_VALUE) { \ + CloseHandle(handle); \ + } \ + if (NS_tremove(path) && errno != ENOENT) { \ + return retCode; \ + } \ + } +#endif + +//----------------------------------------------------------------------------- + +// This BZ2_crc32Table variable lives in libbz2. We just took the +// data structure from bz2 and created crctables.h + +static unsigned int crc32(const unsigned char* buf, unsigned int len) { + unsigned int crc = 0xffffffffL; + + const unsigned char* end = buf + len; + for (; buf != end; ++buf) + crc = (crc << 8) ^ BZ2_crc32Table[(crc >> 24) ^ *buf]; + + crc = ~crc; + return crc; +} + +//----------------------------------------------------------------------------- + +// A simple stack based container for a FILE struct that closes the +// file descriptor from its destructor. +class AutoFile { + public: + explicit AutoFile(FILE* file = nullptr) : mFile(file) {} + + ~AutoFile() { + if (mFile != nullptr) { + fclose(mFile); + } + } + + AutoFile& operator=(FILE* file) { + if (mFile != 0) { + fclose(mFile); + } + mFile = file; + return *this; + } + + operator FILE*() { return mFile; } + + FILE* get() { return mFile; } + + private: + FILE* mFile; +}; + +struct MARChannelStringTable { + MARChannelStringTable() { + MARChannelID = mozilla::MakeUnique<char[]>(1); + MARChannelID[0] = '\0'; + } + + mozilla::UniquePtr<char[]> MARChannelID; +}; + +//----------------------------------------------------------------------------- + +#ifdef XP_MACOSX + +// Just a simple class that sets a umask value in its constructor and resets +// it in its destructor. +class UmaskContext { + public: + explicit UmaskContext(mode_t umaskToSet); + ~UmaskContext(); + + private: + mode_t mPreviousUmask; +}; + +UmaskContext::UmaskContext(mode_t umaskToSet) { + mPreviousUmask = umask(umaskToSet); +} + +UmaskContext::~UmaskContext() { umask(mPreviousUmask); } + +#endif + +//----------------------------------------------------------------------------- + +typedef void (*ThreadFunc)(void* param); + +#ifdef XP_WIN +# include <process.h> + +class Thread { + public: + int Run(ThreadFunc func, void* param) { + mThreadFunc = func; + mThreadParam = param; + + unsigned int threadID; + + mThread = + (HANDLE)_beginthreadex(nullptr, 0, ThreadMain, this, 0, &threadID); + + return mThread ? 0 : -1; + } + int Join() { + WaitForSingleObject(mThread, INFINITE); + CloseHandle(mThread); + return 0; + } + + private: + static unsigned __stdcall ThreadMain(void* p) { + Thread* self = (Thread*)p; + self->mThreadFunc(self->mThreadParam); + return 0; + } + HANDLE mThread; + ThreadFunc mThreadFunc; + void* mThreadParam; +}; + +#elif defined(XP_UNIX) +# include <pthread.h> + +class Thread { + public: + int Run(ThreadFunc func, void* param) { + return pthread_create(&thr, nullptr, (void* (*)(void*))func, param); + } + int Join() { + void* result; + return pthread_join(thr, &result); + } + + private: + pthread_t thr; +}; + +#else +# error "Unsupported platform" +#endif + +//----------------------------------------------------------------------------- + +static NS_tchar gPatchDirPath[MAXPATHLEN]; +static NS_tchar gInstallDirPath[MAXPATHLEN]; +static NS_tchar gWorkingDirPath[MAXPATHLEN]; +static ArchiveReader gArchiveReader; +static bool gSucceeded = false; +static bool sStagedUpdate = false; +static bool sReplaceRequest = false; +static bool sUsingService = false; + +// Normally, we run updates as a result of user action (the user started Firefox +// or clicked a "Restart to Update" button). But there are some cases when +// we are not: +// a) The callback app is a background task. If true then the updater is +// likely being run as part of a background task. +// The updater could be run with no callback, but this only happens +// when performing a staged update (see calls to ProcessUpdates), and there +// are already checks for sStagedUpdate when showing UI or elevating. +// b) The environment variable MOZ_APP_SILENT_START is set and not empty. This +// is set, for instance, on macOS when Firefox had no windows open for a +// while and restarted to apply updates. +// +// In these cases, the update should be installed silently, so we shouldn't: +// a) show progress UI +// b) prompt for elevation +static bool sUpdateSilently = false; + +#ifdef XP_WIN +static NS_tchar gCallbackRelPath[MAXPATHLEN]; +static NS_tchar gCallbackBackupPath[MAXPATHLEN]; +static NS_tchar gDeleteDirPath[MAXPATHLEN]; + +// Whether to copy the update.log and update.status file to the update patch +// directory from a secure directory. +static bool gCopyOutputFiles = false; +// Whether to write the update.log and update.status file to a secure directory. +static bool gUseSecureOutputPath = false; +#endif + +static const NS_tchar kWhitespace[] = NS_T(" \t"); +static const NS_tchar kNL[] = NS_T("\r\n"); +static const NS_tchar kQuote[] = NS_T("\""); + +static inline size_t mmin(size_t a, size_t b) { return (a > b) ? b : a; } + +static NS_tchar* mstrtok(const NS_tchar* delims, NS_tchar** str) { + if (!*str || !**str) { + *str = nullptr; + return nullptr; + } + + // skip leading "whitespace" + NS_tchar* ret = *str; + const NS_tchar* d; + do { + for (d = delims; *d != NS_T('\0'); ++d) { + if (*ret == *d) { + ++ret; + break; + } + } + } while (*d); + + if (!*ret) { + *str = ret; + return nullptr; + } + + NS_tchar* i = ret; + do { + for (d = delims; *d != NS_T('\0'); ++d) { + if (*i == *d) { + *i = NS_T('\0'); + *str = ++i; + return ret; + } + } + ++i; + } while (*i); + + *str = nullptr; + return ret; +} + +#if defined(TEST_UPDATER) || defined(XP_WIN) || defined(XP_MACOSX) +static bool EnvHasValue(const char* name) { + const char* val = getenv(name); + return (val && *val); +} +#endif + +#ifdef XP_WIN +/** + * Obtains the update ID from the secure id file located in secure output + * directory. + * + * @param outBuf + * A buffer of size UUID_LEN (e.g. 37) to store the result. The uuid is + * 36 characters in length and 1 more for null termination. + * @return true if successful + */ +bool GetSecureID(char* outBuf) { + NS_tchar idFilePath[MAX_PATH + 1] = {L'\0'}; + if (!GetSecureOutputFilePath(gPatchDirPath, L".id", idFilePath)) { + return false; + } + + AutoFile idFile(NS_tfopen(idFilePath, NS_T("rb"))); + if (idFile == nullptr) { + return false; + } + + size_t read = fread(outBuf, UUID_LEN - 1, 1, idFile); + if (read != 1) { + return false; + } + + outBuf[UUID_LEN - 1] = '\0'; + return true; +} +#endif + +/** + * Calls LogFinish for the update log. On Windows, the unelevated updater copies + * the update status file and the update log file that were written by the + * elevated updater from the secure directory to the update patch directory. + * + * NOTE: All calls to WriteStatusFile MUST happen before calling output_finish + * because this function copies the update status file for the elevated + * updater and writing the status file after calling output_finish will + * overwrite it. + */ +static void output_finish() { + LogFinish(); +#ifdef XP_WIN + if (gCopyOutputFiles) { + NS_tchar srcStatusPath[MAXPATHLEN + 1] = {NS_T('\0')}; + if (GetSecureOutputFilePath(gPatchDirPath, L".status", srcStatusPath)) { + NS_tchar dstStatusPath[MAXPATHLEN + 1] = {NS_T('\0')}; + NS_tsnprintf(dstStatusPath, + sizeof(dstStatusPath) / sizeof(dstStatusPath[0]), + NS_T("%s\\update.status"), gPatchDirPath); + CopyFileW(srcStatusPath, dstStatusPath, false); + } + + NS_tchar srcLogPath[MAXPATHLEN + 1] = {NS_T('\0')}; + if (GetSecureOutputFilePath(gPatchDirPath, L".log", srcLogPath)) { + NS_tchar dstLogPath[MAXPATHLEN + 1] = {NS_T('\0')}; + NS_tsnprintf(dstLogPath, sizeof(dstLogPath) / sizeof(dstLogPath[0]), + NS_T("%s\\update.log"), gPatchDirPath); + CopyFileW(srcLogPath, dstLogPath, false); + } + } +#endif +} + +/** + * Coverts a relative update path to a full path. + * + * @param relpath + * The relative path to convert to a full path. + * @return valid filesystem full path or nullptr if memory allocation fails. + */ +static NS_tchar* get_full_path(const NS_tchar* relpath) { + NS_tchar* destpath = sStagedUpdate ? gWorkingDirPath : gInstallDirPath; + size_t lendestpath = NS_tstrlen(destpath); + size_t lenrelpath = NS_tstrlen(relpath); + NS_tchar* s = new NS_tchar[lendestpath + lenrelpath + 2]; + + NS_tchar* c = s; + + NS_tstrcpy(c, destpath); + c += lendestpath; + NS_tstrcat(c, NS_T("/")); + c++; + + NS_tstrcat(c, relpath); + c += lenrelpath; + *c = NS_T('\0'); + return s; +} + +/** + * Converts a full update path into a relative path; reverses get_full_path. + * + * @param fullpath + * The absolute path to convert into a relative path. + * return pointer to the location within fullpath where the relative path starts + * or fullpath itself if it already looks relative. + */ +#ifndef XP_WIN +static const NS_tchar* get_relative_path(const NS_tchar* fullpath) { + if (fullpath[0] != '/') { + return fullpath; + } + + NS_tchar* prefix = sStagedUpdate ? gWorkingDirPath : gInstallDirPath; + + // If the path isn't long enough to be absolute, return it as-is. + if (NS_tstrlen(fullpath) <= NS_tstrlen(prefix)) { + return fullpath; + } + + return fullpath + NS_tstrlen(prefix) + 1; +} +#endif + +/** + * Gets the platform specific path and performs simple checks to the path. If + * the path checks don't pass nullptr will be returned. + * + * @param line + * The line from the manifest that contains the path. + * @param isdir + * Whether the path is a directory path. Defaults to false. + * @return valid filesystem path or nullptr if the path checks fail. + */ +static NS_tchar* get_valid_path(NS_tchar** line, bool isdir = false) { + NS_tchar* path = mstrtok(kQuote, line); + if (!path) { + LOG(("get_valid_path: unable to determine path: " LOG_S, *line)); + return nullptr; + } + + // All paths must be relative from the current working directory + if (path[0] == NS_T('/')) { + LOG(("get_valid_path: path must be relative: " LOG_S, path)); + return nullptr; + } + +#ifdef XP_WIN + // All paths must be relative from the current working directory + if (path[0] == NS_T('\\') || path[1] == NS_T(':')) { + LOG(("get_valid_path: path must be relative: " LOG_S, path)); + return nullptr; + } +#endif + + if (isdir) { + // Directory paths must have a trailing forward slash. + if (path[NS_tstrlen(path) - 1] != NS_T('/')) { + LOG( + ("get_valid_path: directory paths must have a trailing forward " + "slash: " LOG_S, + path)); + return nullptr; + } + + // Remove the trailing forward slash because stat on Windows will return + // ENOENT if the path has a trailing slash. + path[NS_tstrlen(path) - 1] = NS_T('\0'); + } + + // Don't allow relative paths that resolve to a parent directory. + if (NS_tstrstr(path, NS_T("..")) != nullptr) { + LOG(("get_valid_path: paths must not contain '..': " LOG_S, path)); + return nullptr; + } + + return path; +} + +/* + * Gets a quoted path. The return value is malloc'd and it is the responsibility + * of the caller to free it. + * + * @param path + * The path to quote. + * @return On success the quoted path and nullptr otherwise. + */ +static NS_tchar* get_quoted_path(const NS_tchar* path) { + size_t lenQuote = NS_tstrlen(kQuote); + size_t lenPath = NS_tstrlen(path); + size_t len = lenQuote + lenPath + lenQuote + 1; + + NS_tchar* s = (NS_tchar*)malloc(len * sizeof(NS_tchar)); + if (!s) { + return nullptr; + } + + NS_tchar* c = s; + NS_tstrcpy(c, kQuote); + c += lenQuote; + NS_tstrcat(c, path); + c += lenPath; + NS_tstrcat(c, kQuote); + c += lenQuote; + *c = NS_T('\0'); + return s; +} + +static void ensure_write_permissions(const NS_tchar* path) { +#ifdef XP_WIN + (void)_wchmod(path, _S_IREAD | _S_IWRITE); +#else + struct stat fs; + if (!stat(path, &fs) && !(fs.st_mode & S_IWUSR)) { + (void)chmod(path, fs.st_mode | S_IWUSR); + } +#endif +} + +static int ensure_remove(const NS_tchar* path) { + ensure_write_permissions(path); + int rv = NS_tremove(path); + if (rv) { + LOG(("ensure_remove: failed to remove file: " LOG_S ", rv: %d, err: %d", + path, rv, errno)); + } + return rv; +} + +// Remove the directory pointed to by path and all of its files and +// sub-directories. +static int ensure_remove_recursive(const NS_tchar* path, + bool continueEnumOnFailure = false) { + // We use lstat rather than stat here so that we can successfully remove + // symlinks. + struct NS_tstat_t sInfo; + int rv = NS_tlstat(path, &sInfo); + if (rv) { + // This error is benign + return rv; + } + if (!S_ISDIR(sInfo.st_mode)) { + return ensure_remove(path); + } + + NS_tDIR* dir; + NS_tdirent* entry; + + dir = NS_topendir(path); + if (!dir) { + LOG(("ensure_remove_recursive: unable to open directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return rv; + } + + while ((entry = NS_treaddir(dir)) != 0) { + if (NS_tstrcmp(entry->d_name, NS_T(".")) && + NS_tstrcmp(entry->d_name, NS_T(".."))) { + NS_tchar childPath[MAXPATHLEN]; + NS_tsnprintf(childPath, sizeof(childPath) / sizeof(childPath[0]), + NS_T("%s/%s"), path, entry->d_name); + rv = ensure_remove_recursive(childPath); + if (rv && !continueEnumOnFailure) { + break; + } + } + } + + NS_tclosedir(dir); + + if (rv == OK) { + ensure_write_permissions(path); + rv = NS_trmdir(path); + if (rv) { + LOG(("ensure_remove_recursive: unable to remove directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + } + } + return rv; +} + +static bool is_read_only(const NS_tchar* flags) { + size_t length = NS_tstrlen(flags); + if (length == 0) { + return false; + } + + // Make sure the string begins with "r" + if (flags[0] != NS_T('r')) { + return false; + } + + // Look for "r+" or "r+b" + if (length > 1 && flags[1] == NS_T('+')) { + return false; + } + + // Look for "rb+" + if (NS_tstrcmp(flags, NS_T("rb+")) == 0) { + return false; + } + + return true; +} + +static FILE* ensure_open(const NS_tchar* path, const NS_tchar* flags, + unsigned int options) { + ensure_write_permissions(path); + FILE* f = NS_tfopen(path, flags); + if (is_read_only(flags)) { + // Don't attempt to modify the file permissions if the file is being opened + // in read-only mode. + return f; + } + if (NS_tchmod(path, options) != 0) { + if (f != nullptr) { + fclose(f); + } + return nullptr; + } + struct NS_tstat_t ss; + if (NS_tstat(path, &ss) != 0 || ss.st_mode != options) { + if (f != nullptr) { + fclose(f); + } + return nullptr; + } + return f; +} + +// Ensure that the directory containing this file exists. +static int ensure_parent_dir(const NS_tchar* path) { + int rv = OK; + + NS_tchar* slash = (NS_tchar*)NS_tstrrchr(path, NS_T('/')); + if (slash) { + *slash = NS_T('\0'); + rv = ensure_parent_dir(path); + // Only attempt to create the directory if we're not at the root + if (rv == OK && *path) { + rv = NS_tmkdir(path, 0755); + // If the directory already exists, then ignore the error. + if (rv < 0 && errno != EEXIST) { + LOG(("ensure_parent_dir: failed to create directory: " LOG_S ", " + "err: %d", + path, errno)); + rv = WRITE_ERROR; + } else { + rv = OK; + } + } + *slash = NS_T('/'); + } + return rv; +} + +#ifdef XP_UNIX +static int ensure_copy_symlink(const NS_tchar* path, const NS_tchar* dest) { + // Copy symlinks by creating a new symlink to the same target + NS_tchar target[MAXPATHLEN + 1] = {NS_T('\0')}; + int rv = readlink(path, target, MAXPATHLEN); + if (rv == -1) { + LOG(("ensure_copy_symlink: failed to read the link: " LOG_S ", err: %d", + path, errno)); + return READ_ERROR; + } + rv = symlink(target, dest); + if (rv == -1) { + LOG(("ensure_copy_symlink: failed to create the new link: " LOG_S + ", target: " LOG_S " err: %d", + dest, target, errno)); + return READ_ERROR; + } + return 0; +} +#endif + +// Copy the file named path onto a new file named dest. +static int ensure_copy(const NS_tchar* path, const NS_tchar* dest) { +#ifdef XP_WIN + // Fast path for Windows + bool result = CopyFileW(path, dest, false); + if (!result) { + LOG(("ensure_copy: failed to copy the file " LOG_S " over to " LOG_S + ", lasterr: %lx", + path, dest, GetLastError())); + return WRITE_ERROR_FILE_COPY; + } + return OK; +#else + struct NS_tstat_t ss; + int rv = NS_tlstat(path, &ss); + if (rv) { + LOG(("ensure_copy: failed to read file status info: " LOG_S ", err: %d", + path, errno)); + return READ_ERROR; + } + +# ifdef XP_UNIX + if (S_ISLNK(ss.st_mode)) { + return ensure_copy_symlink(path, dest); + } +# endif + + AutoFile infile(ensure_open(path, NS_T("rb"), ss.st_mode)); + if (!infile) { + LOG(("ensure_copy: failed to open the file for reading: " LOG_S ", err: %d", + path, errno)); + return READ_ERROR; + } + AutoFile outfile(ensure_open(dest, NS_T("wb"), ss.st_mode)); + if (!outfile) { + LOG(("ensure_copy: failed to open the file for writing: " LOG_S ", err: %d", + dest, errno)); + return WRITE_ERROR; + } + + // This block size was chosen pretty arbitrarily but seems like a reasonable + // compromise. For example, the optimal block size on a modern OS X machine + // is 100k */ + const int blockSize = 32 * 1024; + void* buffer = malloc(blockSize); + if (!buffer) { + return UPDATER_MEM_ERROR; + } + + while (!feof(infile.get())) { + size_t read = fread(buffer, 1, blockSize, infile); + if (ferror(infile.get())) { + LOG(("ensure_copy: failed to read the file: " LOG_S ", err: %d", path, + errno)); + free(buffer); + return READ_ERROR; + } + + size_t written = 0; + + while (written < read) { + size_t chunkWritten = fwrite(buffer, 1, read - written, outfile); + if (chunkWritten <= 0) { + LOG(("ensure_copy: failed to write the file: " LOG_S ", err: %d", dest, + errno)); + free(buffer); + return WRITE_ERROR_FILE_COPY; + } + + written += chunkWritten; + } + } + + rv = NS_tchmod(dest, ss.st_mode); + + free(buffer); + return rv; +#endif +} + +template <unsigned N> +struct copy_recursive_skiplist { + NS_tchar paths[N][MAXPATHLEN]; + + void append(unsigned index, const NS_tchar* path, const NS_tchar* suffix) { + NS_tsnprintf(paths[index], MAXPATHLEN, NS_T("%s/%s"), path, suffix); + } + + bool find(const NS_tchar* path) { + for (int i = 0; i < static_cast<int>(N); ++i) { + if (!NS_tstricmp(paths[i], path)) { + return true; + } + } + return false; + } +}; + +// Copy all of the files and subdirectories under path to a new directory named +// dest. The path names in the skiplist will be skipped and will not be copied. +template <unsigned N> +static int ensure_copy_recursive(const NS_tchar* path, const NS_tchar* dest, + copy_recursive_skiplist<N>& skiplist) { + struct NS_tstat_t sInfo; + int rv = NS_tlstat(path, &sInfo); + if (rv) { + LOG(("ensure_copy_recursive: path doesn't exist: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return READ_ERROR; + } + +#ifdef XP_UNIX + if (S_ISLNK(sInfo.st_mode)) { + return ensure_copy_symlink(path, dest); + } +#endif + + if (!S_ISDIR(sInfo.st_mode)) { + return ensure_copy(path, dest); + } + + rv = NS_tmkdir(dest, sInfo.st_mode); + if (rv < 0 && errno != EEXIST) { + LOG(("ensure_copy_recursive: could not create destination directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return WRITE_ERROR; + } + + NS_tDIR* dir; + NS_tdirent* entry; + + dir = NS_topendir(path); + if (!dir) { + LOG(("ensure_copy_recursive: path is not a directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return READ_ERROR; + } + + while ((entry = NS_treaddir(dir)) != 0) { + if (NS_tstrcmp(entry->d_name, NS_T(".")) && + NS_tstrcmp(entry->d_name, NS_T(".."))) { + NS_tchar childPath[MAXPATHLEN]; + NS_tsnprintf(childPath, sizeof(childPath) / sizeof(childPath[0]), + NS_T("%s/%s"), path, entry->d_name); + if (skiplist.find(childPath)) { + continue; + } + NS_tchar childPathDest[MAXPATHLEN]; + NS_tsnprintf(childPathDest, + sizeof(childPathDest) / sizeof(childPathDest[0]), + NS_T("%s/%s"), dest, entry->d_name); + rv = ensure_copy_recursive(childPath, childPathDest, skiplist); + if (rv) { + break; + } + } + } + NS_tclosedir(dir); + return rv; +} + +// Renames the specified file to the new file specified. If the destination file +// exists it is removed. +static int rename_file(const NS_tchar* spath, const NS_tchar* dpath, + bool allowDirs = false) { + int rv = ensure_parent_dir(dpath); + if (rv) { + return rv; + } + + struct NS_tstat_t spathInfo; + rv = NS_tstat(spath, &spathInfo); + if (rv) { + LOG(("rename_file: failed to read file status info: " LOG_S ", " + "err: %d", + spath, errno)); + return READ_ERROR; + } + + if (!S_ISREG(spathInfo.st_mode)) { + if (allowDirs && !S_ISDIR(spathInfo.st_mode)) { + LOG(("rename_file: path present, but not a file: " LOG_S ", err: %d", + spath, errno)); + return RENAME_ERROR_EXPECTED_FILE; + } + LOG(("rename_file: proceeding to rename the directory")); + } + + if (!NS_taccess(dpath, F_OK)) { + if (ensure_remove(dpath)) { + LOG( + ("rename_file: destination file exists and could not be " + "removed: " LOG_S, + dpath)); + return WRITE_ERROR_DELETE_FILE; + } + } + + if (NS_trename(spath, dpath) != 0) { + LOG(("rename_file: failed to rename file - src: " LOG_S ", " + "dst:" LOG_S ", err: %d", + spath, dpath, errno)); + return WRITE_ERROR; + } + + return OK; +} + +#ifdef XP_WIN +// Remove the directory pointed to by path and all of its files and +// sub-directories. If a file is in use move it to the tobedeleted directory +// and attempt to schedule removal of the file on reboot +static int remove_recursive_on_reboot(const NS_tchar* path, + const NS_tchar* deleteDir) { + struct NS_tstat_t sInfo; + int rv = NS_tlstat(path, &sInfo); + if (rv) { + // This error is benign + return rv; + } + + if (!S_ISDIR(sInfo.st_mode)) { + NS_tchar tmpDeleteFile[MAXPATHLEN + 1]; + GetUUIDTempFilePath(deleteDir, L"rep", tmpDeleteFile); + if (NS_tremove(tmpDeleteFile) && errno != ENOENT) { + LOG(("remove_recursive_on_reboot: failed to remove temporary file: " LOG_S + ", err: %d", + tmpDeleteFile, errno)); + } + rv = rename_file(path, tmpDeleteFile, false); + if (MoveFileEx(rv ? path : tmpDeleteFile, nullptr, + MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG( + ("remove_recursive_on_reboot: file will be removed on OS " + "reboot: " LOG_S, + rv ? path : tmpDeleteFile)); + } else { + LOG(( + "remove_recursive_on_reboot: failed to schedule OS reboot removal of " + "file: " LOG_S, + rv ? path : tmpDeleteFile)); + } + return rv; + } + + NS_tDIR* dir; + NS_tdirent* entry; + + dir = NS_topendir(path); + if (!dir) { + LOG(("remove_recursive_on_reboot: unable to open directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return rv; + } + + while ((entry = NS_treaddir(dir)) != 0) { + if (NS_tstrcmp(entry->d_name, NS_T(".")) && + NS_tstrcmp(entry->d_name, NS_T(".."))) { + NS_tchar childPath[MAXPATHLEN]; + NS_tsnprintf(childPath, sizeof(childPath) / sizeof(childPath[0]), + NS_T("%s/%s"), path, entry->d_name); + // There is no need to check the return value of this call since this + // function is only called after an update is successful and there is not + // much that can be done to recover if it isn't successful. There is also + // no need to log the value since it will have already been logged. + remove_recursive_on_reboot(childPath, deleteDir); + } + } + + NS_tclosedir(dir); + + if (rv == OK) { + ensure_write_permissions(path); + rv = NS_trmdir(path); + if (rv) { + LOG(("remove_recursive_on_reboot: unable to remove directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + } + } + return rv; +} +#endif + +//----------------------------------------------------------------------------- + +// Create a backup of the specified file by renaming it. +static int backup_create(const NS_tchar* path) { + NS_tchar backup[MAXPATHLEN]; + NS_tsnprintf(backup, sizeof(backup) / sizeof(backup[0]), + NS_T("%s") BACKUP_EXT, path); + + return rename_file(path, backup); +} + +// Rename the backup of the specified file that was created by renaming it back +// to the original file. +static int backup_restore(const NS_tchar* path, const NS_tchar* relPath) { + NS_tchar backup[MAXPATHLEN]; + NS_tsnprintf(backup, sizeof(backup) / sizeof(backup[0]), + NS_T("%s") BACKUP_EXT, path); + + NS_tchar relBackup[MAXPATHLEN]; + NS_tsnprintf(relBackup, sizeof(relBackup) / sizeof(relBackup[0]), + NS_T("%s") BACKUP_EXT, relPath); + + if (NS_taccess(backup, F_OK)) { + LOG(("backup_restore: backup file doesn't exist: " LOG_S, relBackup)); + return OK; + } + + return rename_file(backup, path); +} + +// Discard the backup of the specified file that was created by renaming it. +static int backup_discard(const NS_tchar* path, const NS_tchar* relPath) { + NS_tchar backup[MAXPATHLEN]; + NS_tsnprintf(backup, sizeof(backup) / sizeof(backup[0]), + NS_T("%s") BACKUP_EXT, path); + + NS_tchar relBackup[MAXPATHLEN]; + NS_tsnprintf(relBackup, sizeof(relBackup) / sizeof(relBackup[0]), + NS_T("%s") BACKUP_EXT, relPath); + + // Nothing to discard + if (NS_taccess(backup, F_OK)) { + return OK; + } + + int rv = ensure_remove(backup); +#if defined(XP_WIN) + if (rv && !sStagedUpdate && !sReplaceRequest) { + LOG(("backup_discard: unable to remove: " LOG_S, relBackup)); + NS_tchar path[MAXPATHLEN + 1]; + GetUUIDTempFilePath(gDeleteDirPath, L"moz", path); + if (rename_file(backup, path)) { + LOG(("backup_discard: failed to rename file:" LOG_S ", dst:" LOG_S, + relBackup, relPath)); + return WRITE_ERROR_DELETE_BACKUP; + } + // The MoveFileEx call to remove the file on OS reboot will fail if the + // process doesn't have write access to the HKEY_LOCAL_MACHINE registry key + // but this is ok since the installer / uninstaller will delete the + // directory containing the file along with its contents after an update is + // applied, on reinstall, and on uninstall. + if (MoveFileEx(path, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG( + ("backup_discard: file renamed and will be removed on OS " + "reboot: " LOG_S, + relPath)); + } else { + LOG( + ("backup_discard: failed to schedule OS reboot removal of " + "file: " LOG_S, + relPath)); + } + } +#else + if (rv) { + return WRITE_ERROR_DELETE_BACKUP; + } +#endif + + return OK; +} + +// Helper function for post-processing a temporary backup. +static void backup_finish(const NS_tchar* path, const NS_tchar* relPath, + int status) { + if (status == OK) { + backup_discard(path, relPath); + } else { + backup_restore(path, relPath); + } +} + +//----------------------------------------------------------------------------- + +static int DoUpdate(); + +class Action { + public: + Action() : mProgressCost(1), mNext(nullptr) {} + virtual ~Action() = default; + + virtual int Parse(NS_tchar* line) = 0; + + // Do any preprocessing to ensure that the action can be performed. Execute + // will be called if this Action and all others return OK from this method. + virtual int Prepare() = 0; + + // Perform the operation. Return OK to indicate success. After all actions + // have been executed, Finish will be called. A requirement of Execute is + // that its operation be reversable from Finish. + virtual int Execute() = 0; + + // Finish is called after execution of all actions. If status is OK, then + // all actions were successfully executed. Otherwise, some action failed. + virtual void Finish(int status) = 0; + + int mProgressCost; + + private: + Action* mNext; + + friend class ActionList; +}; + +class RemoveFile : public Action { + public: + RemoveFile() : mSkip(0) {} + + int Parse(NS_tchar* line) override; + int Prepare() override; + int Execute() override; + void Finish(int status) override; + + private: + mozilla::UniquePtr<NS_tchar[]> mFile; + mozilla::UniquePtr<NS_tchar[]> mRelPath; + int mSkip; +}; + +int RemoveFile::Parse(NS_tchar* line) { + // format "<deadfile>" + + NS_tchar* validPath = get_valid_path(&line); + if (!validPath) { + return PARSE_ERROR; + } + + mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mRelPath.get(), validPath); + + mFile.reset(get_full_path(validPath)); + if (!mFile) { + return PARSE_ERROR; + } + + return OK; +} + +int RemoveFile::Prepare() { + // Skip the file if it already doesn't exist. + int rv = NS_taccess(mFile.get(), F_OK); + if (rv) { + mSkip = 1; + mProgressCost = 0; + return OK; + } + + LOG(("PREPARE REMOVEFILE " LOG_S, mRelPath.get())); + + // Make sure that we're actually a file... + struct NS_tstat_t fileInfo; + rv = NS_tstat(mFile.get(), &fileInfo); + if (rv) { + LOG(("failed to read file status info: " LOG_S ", err: %d", mFile.get(), + errno)); + return READ_ERROR; + } + + if (!S_ISREG(fileInfo.st_mode)) { + LOG(("path present, but not a file: " LOG_S, mFile.get())); + return DELETE_ERROR_EXPECTED_FILE; + } + + NS_tchar* slash = (NS_tchar*)NS_tstrrchr(mFile.get(), NS_T('/')); + if (slash) { + *slash = NS_T('\0'); + rv = NS_taccess(mFile.get(), W_OK); + *slash = NS_T('/'); + } else { + rv = NS_taccess(NS_T("."), W_OK); + } + + if (rv) { + LOG(("access failed: %d", errno)); + return WRITE_ERROR_FILE_ACCESS_DENIED; + } + + return OK; +} + +int RemoveFile::Execute() { + if (mSkip) { + return OK; + } + + LOG(("EXECUTE REMOVEFILE " LOG_S, mRelPath.get())); + + // The file is checked for existence here and in Prepare since it might have + // been removed by a separate instruction: bug 311099. + int rv = NS_taccess(mFile.get(), F_OK); + if (rv) { + LOG(("file cannot be removed because it does not exist; skipping")); + mSkip = 1; + return OK; + } + + if (sStagedUpdate) { + // Staged updates don't need backup files so just remove it. + rv = ensure_remove(mFile.get()); + if (rv) { + return rv; + } + } else { + // Rename the old file. It will be removed in Finish. + rv = backup_create(mFile.get()); + if (rv) { + LOG(("backup_create failed: %d", rv)); + return rv; + } + } + + return OK; +} + +void RemoveFile::Finish(int status) { + if (mSkip) { + return; + } + + LOG(("FINISH REMOVEFILE " LOG_S, mRelPath.get())); + + // Staged updates don't create backup files. + if (!sStagedUpdate) { + backup_finish(mFile.get(), mRelPath.get(), status); + } +} + +class RemoveDir : public Action { + public: + RemoveDir() : mSkip(0) {} + + int Parse(NS_tchar* line) override; + int Prepare() override; // check that the source dir exists + int Execute() override; + void Finish(int status) override; + + private: + mozilla::UniquePtr<NS_tchar[]> mDir; + mozilla::UniquePtr<NS_tchar[]> mRelPath; + int mSkip; +}; + +int RemoveDir::Parse(NS_tchar* line) { + // format "<deaddir>/" + + NS_tchar* validPath = get_valid_path(&line, true); + if (!validPath) { + return PARSE_ERROR; + } + + mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mRelPath.get(), validPath); + + mDir.reset(get_full_path(validPath)); + if (!mDir) { + return PARSE_ERROR; + } + + return OK; +} + +int RemoveDir::Prepare() { + // We expect the directory to exist if we are to remove it. + int rv = NS_taccess(mDir.get(), F_OK); + if (rv) { + mSkip = 1; + mProgressCost = 0; + return OK; + } + + LOG(("PREPARE REMOVEDIR " LOG_S "/", mRelPath.get())); + + // Make sure that we're actually a dir. + struct NS_tstat_t dirInfo; + rv = NS_tstat(mDir.get(), &dirInfo); + if (rv) { + LOG(("failed to read directory status info: " LOG_S ", err: %d", + mRelPath.get(), errno)); + return READ_ERROR; + } + + if (!S_ISDIR(dirInfo.st_mode)) { + LOG(("path present, but not a directory: " LOG_S, mRelPath.get())); + return DELETE_ERROR_EXPECTED_DIR; + } + + rv = NS_taccess(mDir.get(), W_OK); + if (rv) { + LOG(("access failed: %d, %d", rv, errno)); + return WRITE_ERROR_DIR_ACCESS_DENIED; + } + + return OK; +} + +int RemoveDir::Execute() { + if (mSkip) { + return OK; + } + + LOG(("EXECUTE REMOVEDIR " LOG_S "/", mRelPath.get())); + + // The directory is checked for existence at every step since it might have + // been removed by a separate instruction: bug 311099. + int rv = NS_taccess(mDir.get(), F_OK); + if (rv) { + LOG(("directory no longer exists; skipping")); + mSkip = 1; + } + + return OK; +} + +void RemoveDir::Finish(int status) { + if (mSkip || status != OK) { + return; + } + + LOG(("FINISH REMOVEDIR " LOG_S "/", mRelPath.get())); + + // The directory is checked for existence at every step since it might have + // been removed by a separate instruction: bug 311099. + int rv = NS_taccess(mDir.get(), F_OK); + if (rv) { + LOG(("directory no longer exists; skipping")); + return; + } + + if (status == OK) { + if (NS_trmdir(mDir.get())) { + LOG(("non-fatal error removing directory: " LOG_S "/, rv: %d, err: %d", + mRelPath.get(), rv, errno)); + } + } +} + +class AddFile : public Action { + public: + AddFile() : mAdded(false) {} + + int Parse(NS_tchar* line) override; + int Prepare() override; + int Execute() override; + void Finish(int status) override; + + private: + mozilla::UniquePtr<NS_tchar[]> mFile; + mozilla::UniquePtr<NS_tchar[]> mRelPath; + bool mAdded; +}; + +int AddFile::Parse(NS_tchar* line) { + // format "<newfile>" + + NS_tchar* validPath = get_valid_path(&line); + if (!validPath) { + return PARSE_ERROR; + } + + mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mRelPath.get(), validPath); + + mFile.reset(get_full_path(validPath)); + if (!mFile) { + return PARSE_ERROR; + } + + return OK; +} + +int AddFile::Prepare() { + LOG(("PREPARE ADD " LOG_S, mRelPath.get())); + + return OK; +} + +int AddFile::Execute() { + LOG(("EXECUTE ADD " LOG_S, mRelPath.get())); + + int rv; + + // First make sure that we can actually get rid of any existing file. + rv = NS_taccess(mFile.get(), F_OK); + if (rv == 0) { + if (sStagedUpdate) { + // Staged updates don't need backup files so just remove it. + rv = ensure_remove(mFile.get()); + } else { + rv = backup_create(mFile.get()); + } + if (rv) { + return rv; + } + } else { + rv = ensure_parent_dir(mFile.get()); + if (rv) { + return rv; + } + } + +#ifdef XP_WIN + char sourcefile[MAXPATHLEN]; + if (!WideCharToMultiByte(CP_UTF8, 0, mRelPath.get(), -1, sourcefile, + MAXPATHLEN, nullptr, nullptr)) { + LOG(("error converting wchar to utf8: %lu", GetLastError())); + return STRING_CONVERSION_ERROR; + } + + rv = gArchiveReader.ExtractFile(sourcefile, mFile.get()); +#else + rv = gArchiveReader.ExtractFile(mRelPath.get(), mFile.get()); +#endif + if (!rv) { + mAdded = true; + } + return rv; +} + +void AddFile::Finish(int status) { + LOG(("FINISH ADD " LOG_S, mRelPath.get())); + // Staged updates don't create backup files. + if (!sStagedUpdate) { + // When there is an update failure and a file has been added it is removed + // here since there might not be a backup to replace it. + if (status && mAdded) { + if (NS_tremove(mFile.get()) && errno != ENOENT) { + LOG(("non-fatal error after update failure removing added file: " LOG_S + ", err: %d", + mFile.get(), errno)); + } + } + backup_finish(mFile.get(), mRelPath.get(), status); + } +} + +class PatchFile : public Action { + public: + PatchFile() : mPatchFile(nullptr), mPatchIndex(-1), buf(nullptr) {} + + ~PatchFile() override; + + int Parse(NS_tchar* line) override; + int Prepare() override; // should check for patch file and for checksum here + int Execute() override; + void Finish(int status) override; + + private: + int LoadSourceFile(FILE* ofile); + + static int sPatchIndex; + + const NS_tchar* mPatchFile; + mozilla::UniquePtr<NS_tchar[]> mFile; + mozilla::UniquePtr<NS_tchar[]> mFileRelPath; + int mPatchIndex; + MBSPatchHeader header; + unsigned char* buf; + NS_tchar spath[MAXPATHLEN]; + AutoFile mPatchStream; +}; + +int PatchFile::sPatchIndex = 0; + +PatchFile::~PatchFile() { + // Make sure mPatchStream gets unlocked on Windows; the system will do that, + // but not until some indeterminate future time, and we want determinism. + // Normally this happens at the end of Execute, when we close the stream; + // this call is here in case Execute errors out. +#ifdef XP_WIN + if (mPatchStream) { + UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1); + } +#endif + // Patch files are written to the <working_dir>/updating directory which is + // removed after the update has finished so don't delete patch files here. + + if (buf) { + free(buf); + } +} + +int PatchFile::LoadSourceFile(FILE* ofile) { + struct stat os; + int rv = fstat(fileno((FILE*)ofile), &os); + if (rv) { + LOG(("LoadSourceFile: unable to stat destination file: " LOG_S ", " + "err: %d", + mFileRelPath.get(), errno)); + return READ_ERROR; + } + + if (uint32_t(os.st_size) != header.slen) { + LOG( + ("LoadSourceFile: destination file size %d does not match expected " + "size %d", + uint32_t(os.st_size), header.slen)); + return LOADSOURCE_ERROR_WRONG_SIZE; + } + + buf = (unsigned char*)malloc(header.slen); + if (!buf) { + return UPDATER_MEM_ERROR; + } + + size_t r = header.slen; + unsigned char* rb = buf; + while (r) { + const size_t count = mmin(SSIZE_MAX, r); + size_t c = fread(rb, 1, count, ofile); + if (c != count) { + LOG(("LoadSourceFile: error reading destination file: " LOG_S, + mFileRelPath.get())); + return READ_ERROR; + } + + r -= c; + rb += c; + } + + // Verify that the contents of the source file correspond to what we expect. + + unsigned int crc = crc32(buf, header.slen); + + if (crc != header.scrc32) { + LOG( + ("LoadSourceFile: destination file crc %d does not match expected " + "crc %d", + crc, header.scrc32)); + return CRC_ERROR; + } + + return OK; +} + +int PatchFile::Parse(NS_tchar* line) { + // format "<patchfile>" "<filetopatch>" + + // Get the path to the patch file inside of the mar + mPatchFile = mstrtok(kQuote, &line); + if (!mPatchFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar* q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + NS_tchar* validPath = get_valid_path(&line); + if (!validPath) { + return PARSE_ERROR; + } + + mFileRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mFileRelPath.get(), validPath); + + mFile.reset(get_full_path(validPath)); + if (!mFile) { + return PARSE_ERROR; + } + + return OK; +} + +int PatchFile::Prepare() { + LOG(("PREPARE PATCH " LOG_S, mFileRelPath.get())); + + // extract the patch to a temporary file + mPatchIndex = sPatchIndex++; + + NS_tsnprintf(spath, sizeof(spath) / sizeof(spath[0]), + NS_T("%s/updating/%d.patch"), gWorkingDirPath, mPatchIndex); + + // The removal of pre-existing patch files here is in case a previous update + // crashed and left these files behind. + if (NS_tremove(spath) && errno != ENOENT) { + LOG(("failure removing pre-existing patch file: " LOG_S ", err: %d", spath, + errno)); + return WRITE_ERROR; + } + + mPatchStream = NS_tfopen(spath, NS_T("wb+")); + if (!mPatchStream) { + return WRITE_ERROR; + } + +#ifdef XP_WIN + // Lock the patch file, so it can't be messed with between + // when we're done creating it and when we go to apply it. + if (!LockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1)) { + LOG(("Couldn't lock patch file: %lu", GetLastError())); + return LOCK_ERROR_PATCH_FILE; + } + + char sourcefile[MAXPATHLEN]; + if (!WideCharToMultiByte(CP_UTF8, 0, mPatchFile, -1, sourcefile, MAXPATHLEN, + nullptr, nullptr)) { + LOG(("error converting wchar to utf8: %lu", GetLastError())); + return STRING_CONVERSION_ERROR; + } + + int rv = gArchiveReader.ExtractFileToStream(sourcefile, mPatchStream); +#else + int rv = gArchiveReader.ExtractFileToStream(mPatchFile, mPatchStream); +#endif + + return rv; +} + +int PatchFile::Execute() { + LOG(("EXECUTE PATCH " LOG_S, mFileRelPath.get())); + + fseek(mPatchStream, 0, SEEK_SET); + + int rv = MBS_ReadHeader(mPatchStream, &header); + if (rv) { + return rv; + } + + FILE* origfile = nullptr; +#ifdef XP_WIN + if (NS_tstrcmp(mFileRelPath.get(), gCallbackRelPath) == 0) { + // Read from the copy of the callback when patching since the callback can't + // be opened for reading to prevent the application from being launched. + origfile = NS_tfopen(gCallbackBackupPath, NS_T("rb")); + } else { + origfile = NS_tfopen(mFile.get(), NS_T("rb")); + } +#else + origfile = NS_tfopen(mFile.get(), NS_T("rb")); +#endif + + if (!origfile) { + LOG(("unable to open destination file: " LOG_S ", err: %d", + mFileRelPath.get(), errno)); + return READ_ERROR; + } + + rv = LoadSourceFile(origfile); + fclose(origfile); + if (rv) { + LOG(("LoadSourceFile failed")); + return rv; + } + + // Rename the destination file if it exists before proceeding so it can be + // used to restore the file to its original state if there is an error. + struct NS_tstat_t ss; + rv = NS_tstat(mFile.get(), &ss); + if (rv) { + LOG(("failed to read file status info: " LOG_S ", err: %d", + mFileRelPath.get(), errno)); + return READ_ERROR; + } + + // Staged updates don't need backup files. + if (!sStagedUpdate) { + rv = backup_create(mFile.get()); + if (rv) { + return rv; + } + } + +#if defined(HAVE_POSIX_FALLOCATE) + AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode)); + posix_fallocate(fileno((FILE*)ofile), 0, header.dlen); +#elif defined(XP_WIN) + bool shouldTruncate = true; + // Creating the file, setting the size, and then closing the file handle + // lessens fragmentation more than any other method tested. Other methods that + // have been tested are: + // 1. _chsize / _chsize_s reduced fragmentation though not completely. + // 2. _get_osfhandle and then setting the size reduced fragmentation though + // not completely. There are also reports of _get_osfhandle failing on + // mingw. + HANDLE hfile = CreateFileW(mFile.get(), GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + + if (hfile != INVALID_HANDLE_VALUE) { + if (SetFilePointer(hfile, header.dlen, nullptr, FILE_BEGIN) != + INVALID_SET_FILE_POINTER && + SetEndOfFile(hfile) != 0) { + shouldTruncate = false; + } + CloseHandle(hfile); + } + + AutoFile ofile(ensure_open( + mFile.get(), shouldTruncate ? NS_T("wb+") : NS_T("rb+"), ss.st_mode)); +#elif defined(XP_MACOSX) + AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode)); + // Modified code from FileUtils.cpp + fstore_t store = {F_ALLOCATECONTIG, F_PEOFPOSMODE, 0, header.dlen}; + // Try to get a continous chunk of disk space + rv = fcntl(fileno((FILE*)ofile), F_PREALLOCATE, &store); + if (rv == -1) { + // OK, perhaps we are too fragmented, allocate non-continuous + store.fst_flags = F_ALLOCATEALL; + rv = fcntl(fileno((FILE*)ofile), F_PREALLOCATE, &store); + } + + if (rv != -1) { + ftruncate(fileno((FILE*)ofile), header.dlen); + } +#else + AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode)); +#endif + + if (ofile == nullptr) { + LOG(("unable to create new file: " LOG_S ", err: %d", mFileRelPath.get(), + errno)); + return WRITE_ERROR_OPEN_PATCH_FILE; + } + +#ifdef XP_WIN + if (!shouldTruncate) { + fseek(ofile, 0, SEEK_SET); + } +#endif + + rv = MBS_ApplyPatch(&header, mPatchStream, buf, ofile); + + // Go ahead and do a bit of cleanup now to minimize runtime overhead. + // Make sure mPatchStream gets unlocked on Windows; the system will do that, + // but not until some indeterminate future time, and we want determinism. +#ifdef XP_WIN + UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1); +#endif + // Set mPatchStream to nullptr to make AutoFile close the file, + // so it can be deleted on Windows. + mPatchStream = nullptr; + // Patch files are written to the <working_dir>/updating directory which is + // removed after the update has finished so don't delete patch files here. + spath[0] = NS_T('\0'); + free(buf); + buf = nullptr; + + return rv; +} + +void PatchFile::Finish(int status) { + LOG(("FINISH PATCH " LOG_S, mFileRelPath.get())); + + // Staged updates don't create backup files. + if (!sStagedUpdate) { + backup_finish(mFile.get(), mFileRelPath.get(), status); + } +} + +class AddIfFile : public AddFile { + public: + int Parse(NS_tchar* line) override; + int Prepare() override; + int Execute() override; + void Finish(int status) override; + + protected: + mozilla::UniquePtr<NS_tchar[]> mTestFile; +}; + +int AddIfFile::Parse(NS_tchar* line) { + // format "<testfile>" "<newfile>" + + mTestFile.reset(get_full_path(get_valid_path(&line))); + if (!mTestFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar* q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + return AddFile::Parse(line); +} + +int AddIfFile::Prepare() { + // If the test file does not exist, then skip this action. + if (NS_taccess(mTestFile.get(), F_OK)) { + mTestFile = nullptr; + return OK; + } + + return AddFile::Prepare(); +} + +int AddIfFile::Execute() { + if (!mTestFile) { + return OK; + } + + return AddFile::Execute(); +} + +void AddIfFile::Finish(int status) { + if (!mTestFile) { + return; + } + + AddFile::Finish(status); +} + +class AddIfNotFile : public AddFile { + public: + int Parse(NS_tchar* line) override; + int Prepare() override; + int Execute() override; + void Finish(int status) override; + + protected: + mozilla::UniquePtr<NS_tchar[]> mTestFile; +}; + +int AddIfNotFile::Parse(NS_tchar* line) { + // format "<testfile>" "<newfile>" + + mTestFile.reset(get_full_path(get_valid_path(&line))); + if (!mTestFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar* q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + return AddFile::Parse(line); +} + +int AddIfNotFile::Prepare() { + // If the test file exists, then skip this action. + if (!NS_taccess(mTestFile.get(), F_OK)) { + mTestFile = NULL; + return OK; + } + + return AddFile::Prepare(); +} + +int AddIfNotFile::Execute() { + if (!mTestFile) { + return OK; + } + + return AddFile::Execute(); +} + +void AddIfNotFile::Finish(int status) { + if (!mTestFile) { + return; + } + + AddFile::Finish(status); +} + +class PatchIfFile : public PatchFile { + public: + int Parse(NS_tchar* line) override; + int Prepare() override; // should check for patch file and for checksum here + int Execute() override; + void Finish(int status) override; + + private: + mozilla::UniquePtr<NS_tchar[]> mTestFile; +}; + +int PatchIfFile::Parse(NS_tchar* line) { + // format "<testfile>" "<patchfile>" "<filetopatch>" + + mTestFile.reset(get_full_path(get_valid_path(&line))); + if (!mTestFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar* q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + return PatchFile::Parse(line); +} + +int PatchIfFile::Prepare() { + // If the test file does not exist, then skip this action. + if (NS_taccess(mTestFile.get(), F_OK)) { + mTestFile = nullptr; + return OK; + } + + return PatchFile::Prepare(); +} + +int PatchIfFile::Execute() { + if (!mTestFile) { + return OK; + } + + return PatchFile::Execute(); +} + +void PatchIfFile::Finish(int status) { + if (!mTestFile) { + return; + } + + PatchFile::Finish(status); +} + +//----------------------------------------------------------------------------- + +#ifdef XP_WIN +# include "nsWindowsRestart.cpp" +# include "nsWindowsHelpers.h" +# include "uachelper.h" +# ifdef MOZ_MAINTENANCE_SERVICE +# include "pathhash.h" +# endif + +/** + * Launch the post update application (helper.exe). It takes in the path of the + * callback application to calculate the path of helper.exe. For service updates + * this is called from both the system account and the current user account. + * + * @param installationDir The path to the callback application binary. + * @param updateInfoDir The directory where update info is stored. + * @return true if there was no error starting the process. + */ +bool LaunchWinPostProcess(const WCHAR* installationDir, + const WCHAR* updateInfoDir) { + WCHAR workingDirectory[MAX_PATH + 1] = {L'\0'}; + wcsncpy(workingDirectory, installationDir, MAX_PATH); + + // Launch helper.exe to perform post processing (e.g. registry and log file + // modifications) for the update. + WCHAR inifile[MAX_PATH + 1] = {L'\0'}; + wcsncpy(inifile, installationDir, MAX_PATH); + if (!PathAppendSafe(inifile, L"updater.ini")) { + return false; + } + + WCHAR exefile[MAX_PATH + 1]; + WCHAR exearg[MAX_PATH + 1]; + if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeRelPath", nullptr, + exefile, MAX_PATH + 1, inifile)) { + return false; + } + + if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeArg", nullptr, exearg, + MAX_PATH + 1, inifile)) { + return false; + } + + // The relative path must not contain directory traversals, current directory, + // or colons. + if (wcsstr(exefile, L"..") != nullptr || wcsstr(exefile, L"./") != nullptr || + wcsstr(exefile, L".\\") != nullptr || wcsstr(exefile, L":") != nullptr) { + return false; + } + + // The relative path must not start with a decimal point, backslash, or + // forward slash. + if (exefile[0] == L'.' || exefile[0] == L'\\' || exefile[0] == L'/') { + return false; + } + + WCHAR exefullpath[MAX_PATH + 1] = {L'\0'}; + wcsncpy(exefullpath, installationDir, MAX_PATH); + if (!PathAppendSafe(exefullpath, exefile)) { + return false; + } + + if (!IsValidFullPath(exefullpath)) { + return false; + } + +# if !defined(TEST_UPDATER) && defined(MOZ_MAINTENANCE_SERVICE) + if (sUsingService && + !DoesBinaryMatchAllowedCertificates(installationDir, exefullpath)) { + return false; + } +# endif + + WCHAR dlogFile[MAX_PATH + 1]; + if (!PathGetSiblingFilePath(dlogFile, exefullpath, L"uninstall.update")) { + return false; + } + + WCHAR slogFile[MAX_PATH + 1] = {L'\0'}; + if (gCopyOutputFiles) { + if (!GetSecureOutputFilePath(gPatchDirPath, L".log", slogFile)) { + return false; + } + } else { + wcsncpy(slogFile, updateInfoDir, MAX_PATH); + if (!PathAppendSafe(slogFile, L"update.log")) { + return false; + } + } + + WCHAR dummyArg[14] = {L'\0'}; + wcsncpy(dummyArg, L"argv0ignored ", + sizeof(dummyArg) / sizeof(dummyArg[0]) - 1); + + size_t len = wcslen(exearg) + wcslen(dummyArg); + WCHAR* cmdline = (WCHAR*)malloc((len + 1) * sizeof(WCHAR)); + if (!cmdline) { + return false; + } + + wcsncpy(cmdline, dummyArg, len); + wcscat(cmdline, exearg); + + // We want to launch the post update helper app to update the Windows + // registry even if there is a failure with removing the uninstall.update + // file or copying the update.log file. + CopyFileW(slogFile, dlogFile, false); + + STARTUPINFOW si = {sizeof(si), 0}; + si.lpDesktop = const_cast<LPWSTR>(L""); // -Wwritable-strings + PROCESS_INFORMATION pi = {0}; + + bool ok = CreateProcessW(exefullpath, cmdline, + nullptr, // no special security attributes + nullptr, // no special thread attributes + false, // don't inherit filehandles + 0, // No special process creation flags + nullptr, // inherit my environment + workingDirectory, &si, &pi); + free(cmdline); + if (ok) { + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + return ok; +} + +#endif + +static void LaunchCallbackApp(const NS_tchar* workingDir, int argc, + NS_tchar** argv, bool usingService) { + putenv(const_cast<char*>("MOZ_LAUNCHED_CHILD=1")); + + // Run from the specified working directory (see bug 312360). + if (NS_tchdir(workingDir) != 0) { + LOG(("Warning: chdir failed")); + } + +#if defined(USE_EXECV) + execv(argv[0], argv); +#elif defined(XP_MACOSX) + LaunchChild(argc, (const char**)argv); +#elif defined(XP_WIN) + // Do not allow the callback to run when running an update through the + // service as session 0. The unelevated updater.exe will do the launching. + if (!usingService) { + HANDLE hProcess; + if (WinLaunchChild(argv[0], argc, argv, nullptr, &hProcess)) { + // Keep the current process around until the callback process has created + // its message queue, to avoid the launched process's windows being forced + // into the background. + mozilla::WaitForInputIdle(hProcess); + CloseHandle(hProcess); + } + } +#else +# warning "Need implementaton of LaunchCallbackApp" +#endif +} + +static bool WriteToFile(const NS_tchar* aFilename, const char* aStatus) { + NS_tchar statusFilePath[MAXPATHLEN + 1] = {NS_T('\0')}; +#if defined(XP_WIN) + if (gUseSecureOutputPath) { + if (!GetSecureOutputFilePath(gPatchDirPath, L".status", statusFilePath)) { + return false; + } + } else { + NS_tsnprintf(statusFilePath, + sizeof(statusFilePath) / sizeof(statusFilePath[0]), + NS_T("%s\\%s"), gPatchDirPath, aFilename); + } +#else + NS_tsnprintf(statusFilePath, + sizeof(statusFilePath) / sizeof(statusFilePath[0]), + NS_T("%s/%s"), gPatchDirPath, aFilename); + // Make sure that the directory for the update status file exists + if (ensure_parent_dir(statusFilePath)) { + return false; + } +#endif + + AutoFile statusFile(NS_tfopen(statusFilePath, NS_T("wb+"))); + if (statusFile == nullptr) { + return false; + } + + if (fwrite(aStatus, strlen(aStatus), 1, statusFile) != 1) { + return false; + } + +#if defined(XP_WIN) + if (gUseSecureOutputPath) { + // This is done after the update status file has been written so if the + // write to the update status file fails an existing update status file + // won't be used. + if (!WriteSecureIDFile(gPatchDirPath)) { + return false; + } + } +#endif + + return true; +} + +/** + * Writes a string to the update.status file. + * + * NOTE: All calls to WriteStatusFile MUST happen before calling output_finish + * because the output_finish function copies the update status file for + * the elevated updater and writing the status file after calling + * output_finish will overwrite it. + * + * @param aStatus + * The string to write to the update.status file. + * @return true on success. + */ +static bool WriteStatusFile(const char* aStatus) { + return WriteToFile(NS_T("update.status"), aStatus); +} + +/** + * Writes a string to the update.status file based on the status param. + * + * NOTE: All calls to WriteStatusFile MUST happen before calling output_finish + * because the output_finish function copies the update status file for + * the elevated updater and writing the status file after calling + * output_finish will overwrite it. + * + * @param status + * A status code used to determine what string to write to the + * update.status file (see code). + */ +static void WriteStatusFile(int status) { + const char* text; + + char buf[32]; + if (status == OK) { + if (sStagedUpdate) { + text = "applied\n"; + } else { + text = "succeeded\n"; + } + } else { + snprintf(buf, sizeof(buf) / sizeof(buf[0]), "failed: %d\n", status); + text = buf; + } + + WriteStatusFile(text); +} + +#if defined(XP_WIN) +/* + * Parses the passed contents of an update status file and checks if the + * contained status matches the expected status. + * + * @param statusString The status file contents. + * @param expectedStatus The status to compare the update status file's + * contents against. + * @param errorCode Optional out parameter. If a pointer is passed and the + * update status file contains an error code, the code + * will be returned via the out parameter. If a pointer is + * passed and the update status file does not contain an error + * code, or any error code after the status could not be + * parsed, mozilla::Nothing will be returned via this + * parameter. + * @return true if the status is set to the value indicated by expectedStatus. + */ +static bool UpdateStatusIs(const char* statusString, const char* expectedStatus, + mozilla::Maybe<int>* errorCode = nullptr) { + if (errorCode) { + *errorCode = mozilla::Nothing(); + } + + // Parse the update status file. Expected format is: + // Update status string + // Optionally followed by: + // Colon character (':') + // Space character (' ') + // Integer error code + // Newline character + const char* statusEnd = strchr(statusString, ':'); + if (statusEnd == nullptr) { + statusEnd = strchr(statusString, '\n'); + } + if (statusEnd == nullptr) { + statusEnd = strchr(statusString, '\0'); + } + size_t statusLen = statusEnd - statusString; + size_t expectedStatusLen = strlen(expectedStatus); + + bool statusMatch = + statusLen == expectedStatusLen && + strncmp(statusString, expectedStatus, expectedStatusLen) == 0; + + // We only need to continue parsing if (a) there is a place to store the error + // code if we parse it, and (b) there is a status code to parse. If the status + // string didn't end with a ':', there won't be an error code after it. + if (!errorCode || *statusEnd != ':') { + return statusMatch; + } + + const char* errorCodeStart = statusEnd + 1; + char* errorCodeEnd = nullptr; + // strtol skips an arbitrary number of leading whitespace characters. This + // technically allows us to successfully consume slightly misformatted status + // files, since the expected format is for there to be a single space only. + long longErrorCode = strtol(errorCodeStart, &errorCodeEnd, 10); + if (errorCodeEnd != errorCodeStart && longErrorCode < INT_MAX && + longErrorCode > INT_MIN) { + // We don't allow equality with INT_MAX/INT_MIN for two reasons. It could + // be that, on this platform, INT_MAX/INT_MIN equal LONG_MAX/LONG_MIN, which + // is what strtol gives us if the parsed value was out of bounds. And those + // values are already way, way outside the set of valid update error codes + // anyways. + errorCode->emplace(static_cast<int>(longErrorCode)); + } + return statusMatch; +} + +/* + * Reads the secure update status file and sets statusMatch to true if the + * status matches the expected status that was passed. + * + * @param expectedStatus The status to compare the update status file's + * contents against. + * @param statusMatch Out parameter for specifying if the status is set to + * the value indicated by expectedStatus + * @param errorCode Optional out parameter. If a pointer is passed and the + * update status file contains an error code, the code + * will be returned via the out parameter. If a pointer is + * passed and the update status file does not contain an error + * code, or any error code after the status could not be + * parsed, mozilla::Nothing will be returned via this + * parameter. + * @return true if the information was retrieved successfully. + */ +static bool CompareSecureUpdateStatus( + const char* expectedStatus, bool& statusMatch, + mozilla::Maybe<int>* errorCode = nullptr) { + NS_tchar statusFilePath[MAX_PATH + 1] = {L'\0'}; + if (!GetSecureOutputFilePath(gPatchDirPath, L".status", statusFilePath)) { + return false; + } + + AutoFile file(NS_tfopen(statusFilePath, NS_T("rb"))); + if (file == nullptr) { + return false; + } + + const size_t bufferLength = 32; + char buf[bufferLength] = {0}; + size_t charsRead = fread(buf, sizeof(buf[0]), bufferLength - 1, file); + if (ferror(file)) { + return false; + } + buf[charsRead] = '\0'; + + statusMatch = UpdateStatusIs(buf, expectedStatus, errorCode); + return true; +} + +/* + * Reads the secure update status file and sets isSucceeded to true if the + * status is set to succeeded. + * + * @param isSucceeded Out parameter for specifying if the status + * is set to succeeded or not. + * @return true if the information was retrieved successfully. + */ +static bool IsSecureUpdateStatusSucceeded(bool& isSucceeded) { + return CompareSecureUpdateStatus("succeeded", isSucceeded); +} +#endif + +#ifdef MOZ_MAINTENANCE_SERVICE +/* + * Read the update.status file and sets isPendingService to true if + * the status is set to pending-service. + * + * @param isPendingService Out parameter for specifying if the status + * is set to pending-service or not. + * @return true if the information was retrieved and it is pending + * or pending-service. + */ +static bool IsUpdateStatusPendingService() { + NS_tchar filename[MAXPATHLEN]; + NS_tsnprintf(filename, sizeof(filename) / sizeof(filename[0]), + NS_T("%s/update.status"), gPatchDirPath); + + AutoFile file(NS_tfopen(filename, NS_T("rb"))); + if (file == nullptr) { + return false; + } + + const size_t bufferLength = 32; + char buf[bufferLength] = {0}; + size_t charsRead = fread(buf, sizeof(buf[0]), bufferLength - 1, file); + if (ferror(file)) { + return false; + } + buf[charsRead] = '\0'; + + return UpdateStatusIs(buf, "pending-service") || + UpdateStatusIs(buf, "applied-service"); +} + +/* + * Reads the secure update status file and sets isFailed to true if the + * status is set to failed. + * + * @param isFailed Out parameter for specifying if the status + * is set to failed or not. + * @param errorCode Optional out parameter. If a pointer is passed and the + * update status file contains an error code, the code + * will be returned via the out parameter. If a pointer is + * passed and the update status file does not contain an error + * code, or any error code after the status could not be + * parsed, mozilla::Nothing will be returned via this + * parameter. + * @return true if the information was retrieved successfully. + */ +static bool IsSecureUpdateStatusFailed( + bool& isFailed, mozilla::Maybe<int>* errorCode = nullptr) { + return CompareSecureUpdateStatus("failed", isFailed, errorCode); +} + +/** + * This function determines whether the error represented by the passed error + * code could potentially be recovered from or bypassed by updating without + * using the Maintenance Service (i.e. by showing a UAC prompt). + * We don't really want to show a UAC prompt, but it's preferable over the + * manual update doorhanger + * + * @param errorCode An integer error code from the update.status file. Should + * be one of the codes enumerated in updatererrors.h. + * @returns true if the code represents a Maintenance Service specific error. + * Otherwise, false. + */ +static bool IsServiceSpecificErrorCode(int errorCode) { + return ((errorCode >= 24 && errorCode <= 33) || + (errorCode >= 49 && errorCode <= 58)); +} +#endif + +/* + * Copy the entire contents of the application installation directory to the + * destination directory for the update process. + * + * @return 0 if successful, an error code otherwise. + */ +static int CopyInstallDirToDestDir() { + // These files should not be copied over to the updated app +#ifdef XP_WIN +# define SKIPLIST_COUNT 3 +#elif XP_MACOSX +# define SKIPLIST_COUNT 0 +#else +# define SKIPLIST_COUNT 2 +#endif + copy_recursive_skiplist<SKIPLIST_COUNT> skiplist; +#ifndef XP_MACOSX + skiplist.append(0, gInstallDirPath, NS_T("updated")); + skiplist.append(1, gInstallDirPath, NS_T("updates/0")); +# ifdef XP_WIN + skiplist.append(2, gInstallDirPath, NS_T("updated.update_in_progress.lock")); +# endif +#endif + + return ensure_copy_recursive(gInstallDirPath, gWorkingDirPath, skiplist); +} + +/* + * Replace the application installation directory with the destination + * directory in order to finish a staged update task + * + * @return 0 if successful, an error code otherwise. + */ +static int ProcessReplaceRequest() { + // The replacement algorithm is like this: + // 1. Move destDir to tmpDir. In case of failure, abort. + // 2. Move newDir to destDir. In case of failure, revert step 1 and abort. + // 3. Delete tmpDir (or defer it to the next reboot). + +#ifdef XP_MACOSX + NS_tchar destDir[MAXPATHLEN]; + NS_tsnprintf(destDir, sizeof(destDir) / sizeof(destDir[0]), + NS_T("%s/Contents"), gInstallDirPath); +#elif XP_WIN + // Windows preserves the case of the file/directory names. We use the + // GetLongPathName API in order to get the correct case for the directory + // name, so that if the user has used a different case when launching the + // application, the installation directory's name does not change. + NS_tchar destDir[MAXPATHLEN]; + if (!GetLongPathNameW(gInstallDirPath, destDir, + sizeof(destDir) / sizeof(destDir[0]))) { + return NO_INSTALLDIR_ERROR; + } +#else + NS_tchar* destDir = gInstallDirPath; +#endif + + NS_tchar tmpDir[MAXPATHLEN]; + NS_tsnprintf(tmpDir, sizeof(tmpDir) / sizeof(tmpDir[0]), NS_T("%s.bak"), + destDir); + + NS_tchar newDir[MAXPATHLEN]; + NS_tsnprintf(newDir, sizeof(newDir) / sizeof(newDir[0]), +#ifdef XP_MACOSX + NS_T("%s/Contents"), gWorkingDirPath); +#else + NS_T("%s.bak/updated"), gInstallDirPath); +#endif + + // First try to remove the possibly existing temp directory, because if this + // directory exists, we will fail to rename destDir. + // No need to error check here because if this fails, we will fail in the + // next step anyways. + ensure_remove_recursive(tmpDir); + + LOG(("Begin moving destDir (" LOG_S ") to tmpDir (" LOG_S ")", destDir, + tmpDir)); + int rv = rename_file(destDir, tmpDir, true); +#ifdef XP_WIN + // On Windows, if Firefox is launched using the shortcut, it will hold a + // handle to its installation directory open, which might not get released in + // time. Therefore we wait a little bit here to see if the handle is released. + // If it's not released, we just fail to perform the replace request. + const int max_retries = 10; + int retries = 0; + while (rv == WRITE_ERROR && (retries++ < max_retries)) { + LOG( + ("PerformReplaceRequest: destDir rename attempt %d failed. " + "File: " LOG_S ". Last error: %lu, err: %d", + retries, destDir, GetLastError(), rv)); + + Sleep(100); + + rv = rename_file(destDir, tmpDir, true); + } +#endif + if (rv) { + // The status file will have 'pending' written to it so there is no value in + // returning an error specific for this failure. + LOG(("Moving destDir to tmpDir failed, err: %d", rv)); + return rv; + } + + LOG(("Begin moving newDir (" LOG_S ") to destDir (" LOG_S ")", newDir, + destDir)); + rv = rename_file(newDir, destDir, true); +#ifdef XP_MACOSX + if (rv) { + LOG(("Moving failed. Begin copying newDir (" LOG_S ") to destDir (" LOG_S + ")", + newDir, destDir)); + copy_recursive_skiplist<0> skiplist; + rv = ensure_copy_recursive(newDir, destDir, skiplist); + } +#endif + if (rv) { + LOG(("Moving newDir to destDir failed, err: %d", rv)); + LOG(("Now, try to move tmpDir back to destDir")); + ensure_remove_recursive(destDir); + int rv2 = rename_file(tmpDir, destDir, true); + if (rv2) { + LOG(("Moving tmpDir back to destDir failed, err: %d", rv2)); + } + // The status file will be have 'pending' written to it so there is no value + // in returning an error specific for this failure. + return rv; + } + +#if !defined(XP_WIN) && !defined(XP_MACOSX) + // Platforms that have their updates directory in the installation directory + // need to have the last-update.log and backup-update.log files moved from the + // old installation directory to the new installation directory. + NS_tchar tmpLog[MAXPATHLEN]; + NS_tsnprintf(tmpLog, sizeof(tmpLog) / sizeof(tmpLog[0]), + NS_T("%s/updates/last-update.log"), tmpDir); + if (!NS_taccess(tmpLog, F_OK)) { + NS_tchar destLog[MAXPATHLEN]; + NS_tsnprintf(destLog, sizeof(destLog) / sizeof(destLog[0]), + NS_T("%s/updates/last-update.log"), destDir); + if (NS_tremove(destLog) && errno != ENOENT) { + LOG(("non-fatal error removing log file: " LOG_S ", err: %d", destLog, + errno)); + } + NS_trename(tmpLog, destLog); + } +#endif + + LOG(("Now, remove the tmpDir")); + rv = ensure_remove_recursive(tmpDir, true); + if (rv) { + LOG(("Removing tmpDir failed, err: %d", rv)); +#ifdef XP_WIN + NS_tchar deleteDir[MAXPATHLEN]; + NS_tsnprintf(deleteDir, sizeof(deleteDir) / sizeof(deleteDir[0]), + NS_T("%s\\%s"), destDir, DELETE_DIR); + // Attempt to remove the tobedeleted directory and then recreate it if it + // was successfully removed. + _wrmdir(deleteDir); + if (NS_taccess(deleteDir, F_OK)) { + NS_tmkdir(deleteDir, 0755); + } + remove_recursive_on_reboot(tmpDir, deleteDir); +#endif + } + +#ifdef XP_MACOSX + // On OS X, we we need to remove the staging directory after its Contents + // directory has been moved. + NS_tchar updatedAppDir[MAXPATHLEN]; + NS_tsnprintf(updatedAppDir, sizeof(updatedAppDir) / sizeof(updatedAppDir[0]), + NS_T("%s/Updated.app"), gPatchDirPath); + ensure_remove_recursive(updatedAppDir); +#endif + + gSucceeded = true; + + return 0; +} + +#if defined(XP_WIN) && defined(MOZ_MAINTENANCE_SERVICE) +static void WaitForServiceFinishThread(void* param) { + // We wait at most 10 minutes, we already waited 5 seconds previously + // before deciding to show this UI. + WaitForServiceStop(SVC_NAME, 595); + QuitProgressUI(); +} +#endif + +#ifdef MOZ_VERIFY_MAR_SIGNATURE +/** + * This function reads in the ACCEPTED_MAR_CHANNEL_IDS from update-settings.ini + * + * @param path The path to the ini file that is to be read + * @param results A pointer to the location to store the read strings + * @return OK on success + */ +static int ReadMARChannelIDs(const NS_tchar* path, + MARChannelStringTable* results) { + const unsigned int kNumStrings = 1; + const char* kUpdaterKeys = "ACCEPTED_MAR_CHANNEL_IDS\0"; + int result = ReadStrings(path, kUpdaterKeys, kNumStrings, + &results->MARChannelID, "Settings"); + + return result; +} +#endif + +static int GetUpdateFileName(NS_tchar* fileName, int maxChars) { + NS_tsnprintf(fileName, maxChars, NS_T("%s/update.mar"), gPatchDirPath); + return OK; +} + +static void UpdateThreadFunc(void* param) { + // open ZIP archive and process... + int rv; + if (sReplaceRequest) { + rv = ProcessReplaceRequest(); + } else { + NS_tchar dataFile[MAXPATHLEN]; + rv = GetUpdateFileName(dataFile, sizeof(dataFile) / sizeof(dataFile[0])); + if (rv == OK) { + rv = gArchiveReader.Open(dataFile); + } + +#ifdef MOZ_VERIFY_MAR_SIGNATURE + if (rv == OK) { + rv = gArchiveReader.VerifySignature(); + } + + if (rv == OK) { + if (rv == OK) { + NS_tchar updateSettingsPath[MAXPATHLEN]; + NS_tsnprintf(updateSettingsPath, + sizeof(updateSettingsPath) / sizeof(updateSettingsPath[0]), +# ifdef XP_MACOSX + NS_T("%s/Contents/Resources/update-settings.ini"), +# else + NS_T("%s/update-settings.ini"), +# endif + gInstallDirPath); + MARChannelStringTable MARStrings; + if (ReadMARChannelIDs(updateSettingsPath, &MARStrings) != OK) { + rv = UPDATE_SETTINGS_FILE_CHANNEL; + } else { + rv = gArchiveReader.VerifyProductInformation( + MARStrings.MARChannelID.get(), MOZ_APP_VERSION); + } + } + } +#endif + + if (rv == OK && sStagedUpdate) { +#ifdef TEST_UPDATER + // The MOZ_TEST_SKIP_UPDATE_STAGE environment variable prevents copying + // the files in dist/bin in the test updater when staging an update since + // this can cause tests to timeout. + if (EnvHasValue("MOZ_TEST_SKIP_UPDATE_STAGE")) { + rv = OK; + } else if (EnvHasValue("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE")) { + // The following is to simulate staging so the UI tests have time to + // show that the update is being staged. + NS_tchar continueFilePath[MAXPATHLEN] = {NS_T('\0')}; + NS_tsnprintf(continueFilePath, + sizeof(continueFilePath) / sizeof(continueFilePath[0]), + NS_T("%s/continueStaging"), gInstallDirPath); + // Use 300 retries for staging requests to lessen the likelihood of + // tests intermittently failing on verify tasks due to launching the + // updater. The total time to wait with the default interval of 100 ms + // is approximately 30 seconds. The total time for tests is longer to + // account for the extra time it takes for the updater to launch. + const int max_retries = 300; + int retries = 0; + while (retries++ < max_retries) { +# ifdef XP_WIN + Sleep(100); +# else + usleep(100000); +# endif + // Continue after the continue file exists and is removed. + if (!NS_tremove(continueFilePath)) { + break; + } + } + rv = OK; + } else { + rv = CopyInstallDirToDestDir(); + } +#else + rv = CopyInstallDirToDestDir(); +#endif + } + + if (rv == OK) { + rv = DoUpdate(); + gArchiveReader.Close(); + NS_tchar updatingDir[MAXPATHLEN]; + NS_tsnprintf(updatingDir, sizeof(updatingDir) / sizeof(updatingDir[0]), + NS_T("%s/updating"), gWorkingDirPath); + ensure_remove_recursive(updatingDir); + } + } + + if (rv && (sReplaceRequest || sStagedUpdate)) { + ensure_remove_recursive(gWorkingDirPath); + // When attempting to replace the application, we should fall back + // to non-staged updates in case of a failure. We do this by + // setting the status to pending, exiting the updater, and + // launching the callback application. The callback application's + // startup path will see the pending status, and will start the + // updater application again in order to apply the update without + // staging. + if (sReplaceRequest) { + WriteStatusFile(sUsingService ? "pending-service" : "pending"); + } else { + WriteStatusFile(rv); + } + LOG(("failed: %d", rv)); +#ifdef TEST_UPDATER + // Some tests need to use --test-process-updates again. + putenv(const_cast<char*>("MOZ_TEST_PROCESS_UPDATES=")); +#endif + } else { + if (rv) { + LOG(("failed: %d", rv)); + } else { +#ifdef XP_MACOSX + // If the update was successful we need to update the timestamp on the + // top-level Mac OS X bundle directory so that Mac OS X's Launch Services + // picks up any major changes when the bundle is updated. + if (!sStagedUpdate && utimes(gInstallDirPath, nullptr) != 0) { + LOG(("Couldn't set access/modification time on application bundle.")); + } +#endif + LOG(("succeeded")); + } + WriteStatusFile(rv); + } + + LOG(("calling QuitProgressUI")); + QuitProgressUI(); +} + +#ifdef XP_MACOSX +static void ServeElevatedUpdateThreadFunc(void* param) { + UpdateServerThreadArgs* threadArgs = (UpdateServerThreadArgs*)param; + gSucceeded = ServeElevatedUpdate(threadArgs->argc, threadArgs->argv); + if (!gSucceeded) { + WriteStatusFile(ELEVATION_CANCELED); + } + QuitProgressUI(); +} + +void freeArguments(int argc, char** argv) { + for (int i = 0; i < argc; i++) { + free(argv[i]); + } + free(argv); +} +#endif + +int LaunchCallbackAndPostProcessApps(int argc, NS_tchar** argv, + int callbackIndex +#ifdef XP_WIN + , + const WCHAR* elevatedLockFilePath, + HANDLE updateLockFileHandle +#elif XP_MACOSX + , + bool isElevated, + mozilla::UniquePtr<UmaskContext> + umaskContext +#endif +) { +#ifdef XP_MACOSX + umaskContext.reset(); +#endif + + if (argc > callbackIndex) { +#if defined(XP_WIN) + if (gSucceeded) { + if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { + fprintf(stderr, "The post update process was not launched"); + } + +# ifdef MOZ_MAINTENANCE_SERVICE + // The service update will only be executed if it is already installed. + // For first time installs of the service, the install will happen from + // the PostUpdate process. We do the service update process here + // because it's possible we are updating with updater.exe without the + // service if the service failed to apply the update. We want to update + // the service to a newer version in that case. If we are not running + // through the service, then MOZ_USING_SERVICE will not exist. + if (!sUsingService) { + StartServiceUpdate(gInstallDirPath); + } +# endif + } + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 0); +#elif XP_MACOSX + if (!isElevated) { + if (gSucceeded) { + LaunchMacPostProcess(gInstallDirPath); + } +#endif + + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); +#ifdef XP_MACOSX + } // if (!isElevated) +#endif /* XP_MACOSX */ +} +return 0; +} + +bool ShouldRunSilently(int argc, NS_tchar** argv) { +#ifdef MOZ_BACKGROUNDTASKS + // If the callback has a --backgroundtask switch, consider it a background + // task. The CheckArg semantics aren't reproduced in full here, + // there's e.g. no check for a parameter and no case-insensitive comparison. + for (int i = 1; i < argc; ++i) { + if (const auto option = mozilla::internal::ReadAsOption(argv[i])) { + const NS_tchar* arg = option.value(); + if (NS_tstrcmp(arg, NS_T("backgroundtask")) == 0) { + return true; + } + } + } +#endif // MOZ_BACKGROUNDTASKS + +#if defined(XP_WIN) || defined(XP_MACOSX) + if (EnvHasValue("MOZ_APP_SILENT_START")) { + return true; + } +#endif + + return false; +} + +int NS_main(int argc, NS_tchar** argv) { +#ifdef MOZ_MAINTENANCE_SERVICE + sUsingService = EnvHasValue("MOZ_USING_SERVICE"); + putenv(const_cast<char*>("MOZ_USING_SERVICE=")); +#endif + + // The callback is the remaining arguments starting at callbackIndex. + // The argument specified by callbackIndex is the callback executable and the + // argument prior to callbackIndex is the working directory. + const int callbackIndex = 6; + + // `isDMGInstall` is only ever true for macOS, but we are declaring it here + // to avoid a ton of extra #ifdef's. + bool isDMGInstall = false; + +#ifdef XP_MACOSX + // We want to control file permissions explicitly, or else we could end up + // corrupting installs for other users on the system. Accordingly, set the + // umask to 0 for all file creations below and reset it on exit. See Bug + // 1337007 + mozilla::UniquePtr<UmaskContext> umaskContext(new UmaskContext(0)); + + bool isElevated = + strstr(argv[0], "/Library/PrivilegedHelperTools/org.mozilla.updater") != + 0; + if (isElevated) { + if (!ObtainUpdaterArguments(&argc, &argv)) { + // Won't actually get here because ObtainUpdaterArguments will terminate + // the current process on failure. + return 1; + } + } + + if (argc == 4 && (strstr(argv[1], "-dmgInstall") != 0)) { + isDMGInstall = true; + if (isElevated) { + PerformInstallationFromDMG(argc, argv); + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + return 0; + } + } +#endif + + if (!isDMGInstall) { + // Skip update-related code path for DMG installs. + +#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX) + // On Windows and Mac we rely on native APIs to do verifications so we don't + // need to initialize NSS at all there. + // Otherwise, minimize the amount of NSS we depend on by avoiding all the + // NSS databases. + if (NSS_NoDB_Init(nullptr) != SECSuccess) { + PRErrorCode error = PR_GetError(); + fprintf(stderr, "Could not initialize NSS: %s (%d)", + PR_ErrorToName(error), (int)error); + _exit(1); + } +#endif + + // To process an update the updater command line must at a minimum have the + // directory path containing the updater.mar file to process as the first + // argument, the install directory as the second argument, and the directory + // to apply the update to as the third argument. When the updater is + // launched by another process the PID of the parent process should be + // provided in the optional fourth argument and the updater will wait on the + // parent process to exit if the value is non-zero and the process is + // present. This is necessary due to not being able to update files that are + // in use on Windows. The optional fifth argument is the callback's working + // directory and the optional sixth argument is the callback path. The + // callback is the application to launch after updating and it will be + // launched when these arguments are provided whether the update was + // successful or not. All remaining arguments are optional and are passed to + // the callback when it is launched. + if (argc < 4) { + fprintf(stderr, + "Usage: updater patch-dir install-dir apply-to-dir [wait-pid " + "[callback-working-dir callback-path args...]]\n"); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + +#if defined(TEST_UPDATER) && defined(XP_WIN) + // The tests use nsIProcess to launch the updater and it is simpler for the + // tests to just set an environment variable and have the test updater set + // the current working directory than it is to set the current working + // directory in the test itself. + if (EnvHasValue("CURWORKDIRPATH")) { + const WCHAR* val = _wgetenv(L"CURWORKDIRPATH"); + NS_tchdir(val); + } +#endif + + } // if (!isDMGInstall) + + // The directory containing the update information. + NS_tstrncpy(gPatchDirPath, argv[1], MAXPATHLEN); + gPatchDirPath[MAXPATHLEN - 1] = NS_T('\0'); + +#ifdef XP_WIN + NS_tchar elevatedLockFilePath[MAXPATHLEN] = {NS_T('\0')}; + NS_tsnprintf(elevatedLockFilePath, + sizeof(elevatedLockFilePath) / sizeof(elevatedLockFilePath[0]), + NS_T("%s\\update_elevated.lock"), gPatchDirPath); + gUseSecureOutputPath = + sUsingService || (NS_tremove(elevatedLockFilePath) && errno != ENOENT); +#endif + + if (!isDMGInstall) { + // This check is also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (!IsValidFullPath(argv[1])) { + // Since the status file is written to the patch directory and the patch + // directory is invalid don't write the status file. + fprintf(stderr, + "The patch directory path is not valid for this " + "application (" LOG_S ")\n", + argv[1]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + // This check is also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (!IsValidFullPath(argv[2])) { + WriteStatusFile(INVALID_INSTALL_DIR_PATH_ERROR); + fprintf(stderr, + "The install directory path is not valid for this " + "application (" LOG_S ")\n", + argv[2]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + } // if (!isDMGInstall) + + // The directory we're going to update to. + // We copy this string because we need to remove trailing slashes. The C++ + // standard says that it's always safe to write to strings pointed to by argv + // elements, but I don't necessarily believe it. + NS_tstrncpy(gInstallDirPath, argv[2], MAXPATHLEN); + gInstallDirPath[MAXPATHLEN - 1] = NS_T('\0'); + NS_tchar* slash = NS_tstrrchr(gInstallDirPath, NS_SLASH); + if (slash && !slash[1]) { + *slash = NS_T('\0'); + } + +#ifdef XP_WIN + bool useService = false; + bool testOnlyFallbackKeyExists = false; + // Prevent the updater from falling back from updating with the Maintenance + // Service to updating without the Service. Used for Service tests. + // This is set below via the MOZ_NO_SERVICE_FALLBACK environment variable. + bool noServiceFallback = false; + // Force the updater to use the Maintenance Service incorrectly, causing it + // to fail. Used to test the mechanism that allows the updater to fall back + // from using the Maintenance Service to updating without it. + // This is set below via the MOZ_FORCE_SERVICE_FALLBACK environment variable. + bool forceServiceFallback = false; +#endif + + if (!isDMGInstall) { +#ifdef XP_WIN + // We never want the service to be used unless we build with + // the maintenance service. +# ifdef MOZ_MAINTENANCE_SERVICE + useService = IsUpdateStatusPendingService(); +# ifdef TEST_UPDATER + noServiceFallback = EnvHasValue("MOZ_NO_SERVICE_FALLBACK"); + putenv(const_cast<char*>("MOZ_NO_SERVICE_FALLBACK=")); + forceServiceFallback = EnvHasValue("MOZ_FORCE_SERVICE_FALLBACK"); + putenv(const_cast<char*>("MOZ_FORCE_SERVICE_FALLBACK=")); + // Our tests run with a different apply directory for each test. + // We use this registry key on our test machines to store the + // allowed name/issuers. + testOnlyFallbackKeyExists = DoesFallbackKeyExist(); +# endif +# endif + + // Remove everything except close window from the context menu + { + HKEY hkApp = nullptr; + RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Classes\\Applications", 0, + nullptr, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr, + &hkApp, nullptr); + RegCloseKey(hkApp); + if (RegCreateKeyExW(HKEY_CURRENT_USER, + L"Software\\Classes\\Applications\\updater.exe", 0, + nullptr, REG_OPTION_VOLATILE, KEY_SET_VALUE, nullptr, + &hkApp, nullptr) == ERROR_SUCCESS) { + RegSetValueExW(hkApp, L"IsHostApp", 0, REG_NONE, 0, 0); + RegSetValueExW(hkApp, L"NoOpenWith", 0, REG_NONE, 0, 0); + RegSetValueExW(hkApp, L"NoStartPage", 0, REG_NONE, 0, 0); + RegCloseKey(hkApp); + } + } +#endif + + } // if (!isDMGInstall) + + // If there is a PID specified and it is not '0' then wait for the process to + // exit. + NS_tpid pid = 0; + if (argc > 4) { + pid = NS_tatoi(argv[4]); + if (pid == -1) { + // This is a signal from the parent process that the updater should stage + // the update. + sStagedUpdate = true; + } else if (NS_tstrstr(argv[4], NS_T("/replace"))) { + // We're processing a request to replace the application with a staged + // update. + sReplaceRequest = true; + } + } + + if (!isDMGInstall) { + // This check is also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (!IsValidFullPath(argv[3])) { + WriteStatusFile(INVALID_WORKING_DIR_PATH_ERROR); + fprintf(stderr, + "The working directory path is not valid for this " + "application (" LOG_S ")\n", + argv[3]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + // The directory we're going to update to. + // We copy this string because we need to remove trailing slashes. The C++ + // standard says that it's always safe to write to strings pointed to by + // argv elements, but I don't necessarily believe it. + NS_tstrncpy(gWorkingDirPath, argv[3], MAXPATHLEN); + gWorkingDirPath[MAXPATHLEN - 1] = NS_T('\0'); + slash = NS_tstrrchr(gWorkingDirPath, NS_SLASH); + if (slash && !slash[1]) { + *slash = NS_T('\0'); + } + + if (argc > callbackIndex) { + if (!IsValidFullPath(argv[callbackIndex])) { + WriteStatusFile(INVALID_CALLBACK_PATH_ERROR); + fprintf(stderr, + "The callback file path is not valid for this " + "application (" LOG_S ")\n", + argv[callbackIndex]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + size_t len = NS_tstrlen(gInstallDirPath); + NS_tchar callbackInstallDir[MAXPATHLEN] = {NS_T('\0')}; + NS_tstrncpy(callbackInstallDir, argv[callbackIndex], len); + if (NS_tstrcmp(gInstallDirPath, callbackInstallDir) != 0) { + WriteStatusFile(INVALID_CALLBACK_DIR_ERROR); + fprintf(stderr, + "The callback file must be located in the " + "installation directory (" LOG_S ")\n", + argv[callbackIndex]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + sUpdateSilently = + ShouldRunSilently(argc - callbackIndex, argv + callbackIndex); + } + + } // if (!isDMGInstall) + + if (!sUpdateSilently && !isDMGInstall +#ifdef XP_MACOSX + && !isElevated +#endif + ) { + InitProgressUI(&argc, &argv); + } + +#ifdef XP_MACOSX + if (!isElevated && (!IsRecursivelyWritable(argv[2]) || isDMGInstall)) { + // If the app directory isn't recursively writeable or if this is a DMG + // install, an elevated helper process is required. + if (sUpdateSilently) { + // An elevated update always requires an elevation dialog, so if we are + // updating silently, don't do an elevated update. + // This means that we cannot successfully perform silent updates from + // non-admin accounts on a Mac. + // It also means that we cannot silently perform the first update by an + // admin who was not the installing user. Once the first update has been + // installed, the permissions of the installation directory should be + // changed such that we don't need to elevate in the future. + // Firefox shouldn't actually launch the updater at all in this case. This + // is defense in depth. + WriteStatusFile(SILENT_UPDATE_NEEDED_ELEVATION_ERROR); + fprintf(stderr, + "Skipping update to avoid elevation prompt from silent update."); + } else { + UpdateServerThreadArgs threadArgs; + threadArgs.argc = argc; + threadArgs.argv = const_cast<const NS_tchar**>(argv); + + Thread t1; + if (t1.Run(ServeElevatedUpdateThreadFunc, &threadArgs) == 0) { + // Show an indeterminate progress bar while an elevated update is in + // progress. + if (!isDMGInstall) { + ShowProgressUI(true); + } + } + t1.Join(); + } + + LaunchCallbackAndPostProcessApps(argc, argv, callbackIndex, false, + std::move(umaskContext)); + return gSucceeded ? 0 : 1; + } +#endif + +#ifdef XP_WIN + HANDLE updateLockFileHandle = INVALID_HANDLE_VALUE; +#endif + + if (!isDMGInstall) { + NS_tchar logFilePath[MAXPATHLEN + 1] = {L'\0'}; +#ifdef XP_WIN + if (gUseSecureOutputPath) { + // Remove the secure output files so it is easier to determine when new + // files are created in the unelevated updater. + RemoveSecureOutputFiles(gPatchDirPath); + + (void)GetSecureOutputFilePath(gPatchDirPath, L".log", logFilePath); + } else { + NS_tsnprintf(logFilePath, sizeof(logFilePath) / sizeof(logFilePath[0]), + NS_T("%s\\update.log"), gPatchDirPath); + } +#else + NS_tsnprintf(logFilePath, sizeof(logFilePath) / sizeof(logFilePath[0]), + NS_T("%s/update.log"), gPatchDirPath); +#endif + LogInit(logFilePath); + + if (!WriteStatusFile("applying")) { + LOG(("failed setting status to 'applying'")); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + output_finish(); + return 1; + } + + if (sStagedUpdate) { + LOG(("Performing a staged update")); + } else if (sReplaceRequest) { + LOG(("Performing a replace request")); + } + + LOG(("PATCH DIRECTORY " LOG_S, gPatchDirPath)); + LOG(("INSTALLATION DIRECTORY " LOG_S, gInstallDirPath)); + LOG(("WORKING DIRECTORY " LOG_S, gWorkingDirPath)); + +#if defined(XP_WIN) + // These checks are also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (_wcsnicmp(gWorkingDirPath, gInstallDirPath, MAX_PATH) != 0) { + if (!sStagedUpdate && !sReplaceRequest) { + WriteStatusFile(INVALID_APPLYTO_DIR_ERROR); + LOG( + ("Installation directory and working directory must be the same " + "for non-staged updates. Exiting.")); + output_finish(); + return 1; + } + + NS_tchar workingDirParent[MAX_PATH]; + NS_tsnprintf(workingDirParent, + sizeof(workingDirParent) / sizeof(workingDirParent[0]), + NS_T("%s"), gWorkingDirPath); + if (!PathRemoveFileSpecW(workingDirParent)) { + WriteStatusFile(REMOVE_FILE_SPEC_ERROR); + LOG(("Error calling PathRemoveFileSpecW: %lu", GetLastError())); + output_finish(); + return 1; + } + + if (_wcsnicmp(workingDirParent, gInstallDirPath, MAX_PATH) != 0) { + WriteStatusFile(INVALID_APPLYTO_DIR_STAGED_ERROR); + LOG( + ("The apply-to directory must be the same as or " + "a child of the installation directory! Exiting.")); + output_finish(); + return 1; + } + } +#endif + +#ifdef XP_WIN + 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.")); + } + } + } +#endif + +#ifdef XP_WIN + if (sReplaceRequest || sStagedUpdate) { + // On Windows, when performing a stage or replace request the current + // working directory for the process must be changed so it isn't locked. + NS_tchar sysDir[MAX_PATH + 1] = {L'\0'}; + if (GetSystemDirectoryW(sysDir, MAX_PATH + 1)) { + NS_tchdir(sysDir); + } + } + + // lastFallbackError keeps track of the last error for the service not being + // used, in case of an error when fallback is not enabled we write the + // error to the update.status file. + // When fallback is disabled (MOZ_NO_SERVICE_FALLBACK does not exist) then + // we will instead fallback to not using the service and display a UAC + // prompt. + int lastFallbackError = FALLBACKKEY_UNKNOWN_ERROR; + + // Check whether a second instance of the updater should be launched by the + // maintenance service or with the 'runas' verb when write access is denied + // to the installation directory. + if (!sUsingService && + (argc > callbackIndex || sStagedUpdate || sReplaceRequest)) { + NS_tchar updateLockFilePath[MAXPATHLEN]; + if (sStagedUpdate) { + // When staging an update, the lock file is: + // <install_dir>\updated.update_in_progress.lock + NS_tsnprintf(updateLockFilePath, + sizeof(updateLockFilePath) / sizeof(updateLockFilePath[0]), + NS_T("%s/updated.update_in_progress.lock"), + gInstallDirPath); + } else if (sReplaceRequest) { + // When processing a replace request, the lock file is: + // <install_dir>\..\moz_update_in_progress.lock + NS_tchar installDir[MAXPATHLEN]; + NS_tstrcpy(installDir, gInstallDirPath); + NS_tchar* slash = (NS_tchar*)NS_tstrrchr(installDir, NS_SLASH); + *slash = NS_T('\0'); + NS_tsnprintf(updateLockFilePath, + sizeof(updateLockFilePath) / sizeof(updateLockFilePath[0]), + NS_T("%s\\moz_update_in_progress.lock"), installDir); + } else { + // In the non-staging update case, the lock file is: + // <install_dir>\<app_name>.exe.update_in_progress.lock + NS_tsnprintf(updateLockFilePath, + sizeof(updateLockFilePath) / sizeof(updateLockFilePath[0]), + NS_T("%s.update_in_progress.lock"), argv[callbackIndex]); + } + + // The update_in_progress.lock file should only exist during an update. In + // case it exists attempt to remove it and exit if that fails to prevent + // simultaneous updates occurring. + if (NS_tremove(updateLockFilePath) && errno != ENOENT) { + // Try to fall back to the old way of doing updates if a staged + // update fails. + if (sReplaceRequest) { + // Note that this could fail, but if it does, there isn't too much we + // can do in order to recover anyways. + WriteStatusFile("pending"); + } else if (sStagedUpdate) { + WriteStatusFile(DELETE_ERROR_STAGING_LOCK_FILE); + } + LOG(("Update already in progress! Exiting")); + output_finish(); + return 1; + } + + updateLockFileHandle = + CreateFileW(updateLockFilePath, GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr); + + // Even if a file has no sharing access, you can still get its attributes + bool startedFromUnelevatedUpdater = + GetFileAttributesW(elevatedLockFilePath) != INVALID_FILE_ATTRIBUTES; + + // If we're running from the service, then we were started with the same + // token as the service so the permissions are already dropped. If we're + // running from an elevated updater that was started from an unelevated + // updater, then we drop the permissions here. We do not drop the + // permissions on the originally called updater because we use its token + // to start the callback application. + if (startedFromUnelevatedUpdater) { + // Disable every privilege we don't need. Processes started using + // CreateProcess will use the same token as this process. + UACHelper::DisablePrivileges(nullptr); + } + + if (updateLockFileHandle == INVALID_HANDLE_VALUE || + (useService && testOnlyFallbackKeyExists && + (noServiceFallback || forceServiceFallback))) { + HANDLE elevatedFileHandle; + if (NS_tremove(elevatedLockFilePath) && errno != ENOENT) { + LOG(("Unable to create elevated lock file! Exiting")); + output_finish(); + return 1; + } + + elevatedFileHandle = CreateFileW( + elevatedLockFilePath, GENERIC_READ | GENERIC_WRITE, 0, nullptr, + OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr); + if (elevatedFileHandle == INVALID_HANDLE_VALUE) { + LOG(("Unable to create elevated lock file! Exiting")); + output_finish(); + return 1; + } + + auto cmdLine = mozilla::MakeCommandLine(argc - 1, argv + 1); + if (!cmdLine) { + CloseHandle(elevatedFileHandle); + output_finish(); + return 1; + } + +# ifdef MOZ_MAINTENANCE_SERVICE +// Only invoke the service for installations in Program Files. +// This check is duplicated in workmonitor.cpp because the service can +// be invoked directly without going through the updater. +# ifndef TEST_UPDATER + if (useService) { + useService = IsProgramFilesPath(gInstallDirPath); + } +# endif + + // Make sure the path to the updater to use for the update is on local. + // We do this check to make sure that file locking is available for + // race condition security checks. + if (useService) { + BOOL isLocal = FALSE; + useService = IsLocalFile(argv[0], isLocal) && isLocal; + } + + // If we have unprompted elevation we should NOT use the service + // for the update. Service updates happen with the SYSTEM account + // which has more privs than we need to update with. + // Windows 8 provides a user interface so users can configure this + // behavior and it can be configured in the registry in all Windows + // versions that support UAC. + if (useService) { + BOOL unpromptedElevation; + if (IsUnpromptedElevation(unpromptedElevation)) { + useService = !unpromptedElevation; + } + } + + // Make sure the service registry entries for the instsallation path + // are available. If not don't use the service. + if (useService) { + WCHAR maintenanceServiceKey[MAX_PATH + 1]; + if (CalculateRegistryPathFromFilePath(gInstallDirPath, + maintenanceServiceKey)) { + HKEY baseKey = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, maintenanceServiceKey, 0, + KEY_READ | KEY_WOW64_64KEY, + &baseKey) == ERROR_SUCCESS) { + RegCloseKey(baseKey); + } else { +# ifdef TEST_UPDATER + useService = testOnlyFallbackKeyExists; +# endif + if (!useService) { + lastFallbackError = FALLBACKKEY_NOKEY_ERROR; + } + } + } else { + useService = false; + lastFallbackError = FALLBACKKEY_REGPATH_ERROR; + } + } + + // Originally we used to write "pending" to update.status before + // launching the service command. This is no longer needed now + // since the service command is launched from updater.exe. If anything + // fails in between, we can fall back to using the normal update process + // on our own. + + // If we still want to use the service try to launch the service + // comamnd for the update. + if (useService) { + // Get the secure ID before trying to update so it is possible to + // determine if the updater or the maintenance service has created a + // new one. + char uuidStringBefore[UUID_LEN] = {'\0'}; + bool checkID = GetSecureID(uuidStringBefore); + // Write a catchall service failure status in case it fails without + // changing the status. + WriteStatusFile(SERVICE_UPDATE_STATUS_UNCHANGED); + + int serviceArgc = argc; + if (forceServiceFallback && serviceArgc > 2) { + // To force the service to fail, we can just pass it too few + // arguments. However, we don't want to pass it no arguments, + // because then it won't have enough information to write out the + // update status file telling us that it failed. + serviceArgc = 2; + } + + // If the update couldn't be started, then set useService to false so + // we do the update the old way. + DWORD ret = + LaunchServiceSoftwareUpdateCommand(serviceArgc, (LPCWSTR*)argv); + useService = (ret == ERROR_SUCCESS); + // If the command was launched then wait for the service to be done. + if (useService) { + bool showProgressUI = false; + // Never show the progress UI when staging updates or in a + // background task. + if (!sStagedUpdate && !sUpdateSilently) { + // We need to call this separately instead of allowing + // ShowProgressUI to initialize the strings because the service + // will move the ini file out of the way when running updater. + showProgressUI = !InitProgressUIStrings(); + } + + // Wait for the service to stop for 5 seconds. If the service + // has still not stopped then show an indeterminate progress bar. + DWORD lastState = WaitForServiceStop(SVC_NAME, 5); + if (lastState != SERVICE_STOPPED) { + Thread t1; + if (t1.Run(WaitForServiceFinishThread, nullptr) == 0 && + showProgressUI) { + ShowProgressUI(true, false); + } + t1.Join(); + } + + lastState = WaitForServiceStop(SVC_NAME, 1); + if (lastState != SERVICE_STOPPED) { + // If the service doesn't stop after 10 minutes there is + // something seriously wrong. + lastFallbackError = FALLBACKKEY_SERVICE_NO_STOP_ERROR; + useService = false; + } else { + // Copy the secure output files if the secure ID has changed. + gCopyOutputFiles = true; + char uuidStringAfter[UUID_LEN] = {'\0'}; + if (checkID && GetSecureID(uuidStringAfter) && + strncmp(uuidStringBefore, uuidStringAfter, + sizeof(uuidStringBefore)) == 0) { + LOG( + ("The secure ID hasn't changed after launching the updater " + "using the service")); + gCopyOutputFiles = false; + } + if (gCopyOutputFiles && !sStagedUpdate && !noServiceFallback) { + // If the Maintenance Service fails for a Service-specific + // reason, we ought to fall back to attempting to update + // without the Service. + // However, we need the secure output files to be able to be + // check the error code, and we can't fall back when we are + // staging, because we will need to show a UAC. + bool updateFailed; + mozilla::Maybe<int> maybeErrorCode; + bool success = + IsSecureUpdateStatusFailed(updateFailed, &maybeErrorCode); + if (success && updateFailed && maybeErrorCode.isSome() && + IsServiceSpecificErrorCode(maybeErrorCode.value())) { + useService = false; + } + } + } + } else { + lastFallbackError = FALLBACKKEY_LAUNCH_ERROR; + } + } +# endif + + // If the service can't be used when staging an update, make sure that + // the UAC prompt is not shown! + if (!useService && sStagedUpdate) { + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + // Set an error so the failure is reported. This will be reset + // to pending so the update can be applied during the next startup, + // see bug 1552853. + WriteStatusFile(UNEXPECTED_STAGING_ERROR); + LOG( + ("Non-critical update staging error! Falling back to non-staged " + "updates and exiting")); + output_finish(); + // We don't have a callback when staging so we can just exit. + return 0; + } + + // If the service can't be used when in a background task, make sure + // that the UAC prompt is not shown! + if (!useService && sUpdateSilently) { + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + // Set an error so we don't get into an update loop when the callback + // runs. This will be reset to pending by handleUpdateFailure in + // UpdateService.jsm. + WriteStatusFile(SILENT_UPDATE_NEEDED_ELEVATION_ERROR); + LOG(("Skipping update to avoid UAC prompt from background task.")); + output_finish(); + + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); + return 0; + } + + // If we didn't want to use the service at all, or if an update was + // already happening, or launching the service command failed, then + // launch the elevated updater.exe as we do without the service. + // We don't launch the elevated updater in the case that we did have + // write access all along because in that case the only reason we're + // using the service is because we are testing. + if (!useService && !noServiceFallback && + (updateLockFileHandle == INVALID_HANDLE_VALUE || + forceServiceFallback)) { + // Get the secure ID before trying to update so it is possible to + // determine if the updater has created a new one. + char uuidStringBefore[UUID_LEN] = {'\0'}; + bool checkID = GetSecureID(uuidStringBefore); + // Write a catchall failure status in case it fails without changing + // the status. + WriteStatusFile(UPDATE_STATUS_UNCHANGED); + + SHELLEXECUTEINFO sinfo; + memset(&sinfo, 0, sizeof(SHELLEXECUTEINFO)); + sinfo.cbSize = sizeof(SHELLEXECUTEINFO); + sinfo.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_FLAG_DDEWAIT | + SEE_MASK_NOCLOSEPROCESS; + sinfo.hwnd = nullptr; + sinfo.lpFile = argv[0]; + sinfo.lpParameters = cmdLine.get(); + if (forceServiceFallback) { + // In testing, we don't actually want a UAC prompt. We should + // already have the permissions such that we shouldn't need it. + // And we don't have a good way of accepting the prompt in + // automation. + sinfo.lpVerb = L"open"; + // This handle is what lets the updater that we spawn below know + // that it's the elevated updater. We are going to close it so that + // it doesn't know that and will run un-elevated. Doing this make + // this makes for an imperfect test of the service fallback + // functionality because it changes how the (usually) elevated + // updater runs. One of the effects of this is that the secure + // output files will not be used. So that functionality won't really + // be covered by testing. But we can't really have the updater run + // elevated, because that would require a UAC, which we have no way + // to deal with in automation. + CloseHandle(elevatedFileHandle); + // We need to let go of the update lock to let the un-elevated + // updater we are about to spawn update. + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + } else { + sinfo.lpVerb = L"runas"; + } + sinfo.nShow = SW_SHOWNORMAL; + + bool result = ShellExecuteEx(&sinfo); + + if (result) { + WaitForSingleObject(sinfo.hProcess, INFINITE); + CloseHandle(sinfo.hProcess); + + // Copy the secure output files if the secure ID has changed. + gCopyOutputFiles = true; + char uuidStringAfter[UUID_LEN] = {'\0'}; + if (checkID && GetSecureID(uuidStringAfter) && + strncmp(uuidStringBefore, uuidStringAfter, + sizeof(uuidStringBefore)) == 0) { + LOG( + ("The secure ID hasn't changed after launching the updater " + "using runas")); + gCopyOutputFiles = false; + } + } else { + // Don't copy the secure output files if the elevation request was + // canceled since the status file written below is in the patch + // directory. At this point it should already be set to false and + // this is set here to make it clear that it should be false at this + // point and to prevent future changes from regressing this code. + gCopyOutputFiles = false; + WriteStatusFile(ELEVATION_CANCELED); + } + } + + // If we started the elevated updater, and it finished, check the secure + // update status file to make sure that it succeeded, and if it did we + // need to launch the PostUpdate process in the unelevated updater which + // is running in the current user's session. Note that we don't need to + // do this when staging an update since the PostUpdate step runs during + // the replace request. + if (!sStagedUpdate) { + bool updateStatusSucceeded = false; + if (IsSecureUpdateStatusSucceeded(updateStatusSucceeded) && + updateStatusSucceeded) { + if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { + fprintf(stderr, + "The post update process which runs as the user" + " for service update could not be launched."); + } + } + } + + CloseHandle(elevatedFileHandle); + + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + + if (!useService && noServiceFallback) { + // When the service command was not launched at all. + // We should only reach this code path because we had write access + // all along to the directory and a fallback key existed, and we + // have fallback disabled (MOZ_NO_SERVICE_FALLBACK env var exists). + // We only currently use this env var from XPCShell tests. + gCopyOutputFiles = false; + WriteStatusFile(lastFallbackError); + } + + // The logging output needs to be finished before launching the callback + // application so the update status file contains the value from the + // secure directory used by the maintenance service and the elevated + // updater. + output_finish(); + if (argc > callbackIndex) { + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); + } + return 0; + + // This is the end of the code block for launching another instance of + // the updater using either the maintenance service or with the 'runas' + // verb when the updater doesn't have write access to the installation + // directory. + } + // This is the end of the code block when the updater was not launched by + // the service that checks whether the updater has write access to the + // installation directory. + } + // If we made it this far this is the updater instance that will perform the + // actual update and gCopyOutputFiles will be false (e.g. the default + // value). +#endif + + if (sStagedUpdate) { +#ifdef TEST_UPDATER + // This allows testing that the correct UI after an update staging failure + // that falls back to applying the update on startup. It is simulated due + // to the difficulty of creating the conditions for this type of staging + // failure. + if (EnvHasValue("MOZ_TEST_STAGING_ERROR")) { +# ifdef XP_WIN + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } +# endif + // WRITE_ERROR is one of the cases where the staging failure falls back + // to applying the update on startup. + WriteStatusFile(WRITE_ERROR); + output_finish(); + return 0; + } +#endif + // When staging updates, blow away the old installation directory and + // create it from scratch. + ensure_remove_recursive(gWorkingDirPath); + } + if (!sReplaceRequest) { + // Try to create the destination directory if it doesn't exist + int rv = NS_tmkdir(gWorkingDirPath, 0755); + if (rv != OK && errno != EEXIST) { +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + output_finish(); + return 1; + } + } + +#ifdef XP_WIN + NS_tchar applyDirLongPath[MAXPATHLEN]; + if (!GetLongPathNameW( + gWorkingDirPath, applyDirLongPath, + sizeof(applyDirLongPath) / sizeof(applyDirLongPath[0]))) { + WriteStatusFile(WRITE_ERROR_APPLY_DIR_PATH); + LOG(("NS_main: unable to find apply to dir: " LOG_S, gWorkingDirPath)); + output_finish(); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + if (argc > callbackIndex) { + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); + } + return 1; + } + + HANDLE callbackFile = INVALID_HANDLE_VALUE; + if (argc > callbackIndex) { + // If the callback executable is specified it must exist for a successful + // update. It is important we null out the whole buffer here because + // later we make the assumption that the callback application is inside + // the apply-to dir. If we don't have a fully null'ed out buffer it can + // lead to stack corruption which causes crashes and other problems. + NS_tchar callbackLongPath[MAXPATHLEN]; + ZeroMemory(callbackLongPath, sizeof(callbackLongPath)); + NS_tchar* targetPath = argv[callbackIndex]; + NS_tchar buffer[MAXPATHLEN * 2] = {NS_T('\0')}; + size_t bufferLeft = MAXPATHLEN * 2; + if (sReplaceRequest) { + // In case of replace requests, we should look for the callback file in + // the destination directory. + size_t commonPrefixLength = + PathCommonPrefixW(argv[callbackIndex], gInstallDirPath, nullptr); + NS_tchar* p = buffer; + NS_tstrncpy(p, argv[callbackIndex], commonPrefixLength); + p += commonPrefixLength; + bufferLeft -= commonPrefixLength; + NS_tstrncpy(p, gInstallDirPath + commonPrefixLength, bufferLeft); + + size_t len = NS_tstrlen(gInstallDirPath + commonPrefixLength); + p += len; + bufferLeft -= len; + *p = NS_T('\\'); + ++p; + bufferLeft--; + *p = NS_T('\0'); + NS_tchar installDir[MAXPATHLEN]; + NS_tstrcpy(installDir, gInstallDirPath); + size_t callbackPrefixLength = + PathCommonPrefixW(argv[callbackIndex], installDir, nullptr); + NS_tstrncpy(p, + argv[callbackIndex] + + std::max(callbackPrefixLength, commonPrefixLength), + bufferLeft); + targetPath = buffer; + } + if (!GetLongPathNameW( + targetPath, callbackLongPath, + sizeof(callbackLongPath) / sizeof(callbackLongPath[0]))) { + WriteStatusFile(WRITE_ERROR_CALLBACK_PATH); + LOG(("NS_main: unable to find callback file: " LOG_S, targetPath)); + output_finish(); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + if (argc > callbackIndex) { + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); + } + return 1; + } + + // Doing this is only necessary when we're actually applying a patch. + if (!sReplaceRequest) { + int len = NS_tstrlen(applyDirLongPath); + NS_tchar* s = callbackLongPath; + NS_tchar* d = gCallbackRelPath; + // advance to the apply to directory and advance past the trailing + // backslash if present. + s += len; + if (*s == NS_T('\\')) { + ++s; + } + + // Copy the string and replace backslashes with forward slashes along + // the way. + do { + if (*s == NS_T('\\')) { + *d = NS_T('/'); + } else { + *d = *s; + } + ++s; + ++d; + } while (*s); + *d = NS_T('\0'); + ++d; + + const size_t callbackBackupPathBufSize = + sizeof(gCallbackBackupPath) / sizeof(gCallbackBackupPath[0]); + const int callbackBackupPathLen = + NS_tsnprintf(gCallbackBackupPath, callbackBackupPathBufSize, + NS_T("%s" CALLBACK_BACKUP_EXT), argv[callbackIndex]); + + if (callbackBackupPathLen < 0 || + callbackBackupPathLen >= + static_cast<int>(callbackBackupPathBufSize)) { + WriteStatusFile(USAGE_ERROR); + LOG(("NS_main: callback backup path truncated")); + output_finish(); + + // Don't attempt to launch the callback when the callback path is + // longer than expected. + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + return 1; + } + + // Make a copy of the callback executable so it can be read when + // patching. + if (!CopyFileW(argv[callbackIndex], gCallbackBackupPath, false)) { + DWORD copyFileError = GetLastError(); + if (copyFileError == ERROR_ACCESS_DENIED) { + WriteStatusFile(WRITE_ERROR_ACCESS_DENIED); + } else { + WriteStatusFile(WRITE_ERROR_CALLBACK_APP); + } + LOG(("NS_main: failed to copy callback file " LOG_S + " into place at " LOG_S, + argv[callbackIndex], gCallbackBackupPath)); + output_finish(); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + LaunchCallbackApp(argv[callbackIndex], argc - callbackIndex, + argv + callbackIndex, sUsingService); + return 1; + } + + // Since the process may be signaled as exited by WaitForSingleObject + // before the release of the executable image try to lock the main + // executable file multiple times before giving up. If we end up giving + // up, we won't fail the update. + const int max_retries = 10; + int retries = 1; + DWORD lastWriteError = 0; + do { + // By opening a file handle wihout FILE_SHARE_READ to the callback + // executable, the OS will prevent launching the process while it is + // being updated. + callbackFile = CreateFileW(targetPath, DELETE | GENERIC_WRITE, + // allow delete, rename, and write + FILE_SHARE_DELETE | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + if (callbackFile != INVALID_HANDLE_VALUE) { + break; + } + + lastWriteError = GetLastError(); + LOG( + ("NS_main: callback app file open attempt %d failed. " + "File: " LOG_S ". Last error: %lu", + 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) { + bool proceedWithoutExclusive = true; + + // Fail the update if the last error was not a sharing violation. + if (lastWriteError != ERROR_SHARING_VIOLATION) { + LOG(( + "NS_main: callback app file in use, failed to exclusively open " + "executable file: " LOG_S, + argv[callbackIndex])); + if (lastWriteError == ERROR_ACCESS_DENIED) { + WriteStatusFile(WRITE_ERROR_ACCESS_DENIED); + } else { + WriteStatusFile(WRITE_ERROR_CALLBACK_APP); + } + + proceedWithoutExclusive = false; + } + + // Fail even on sharing violation from a background task, since a + // background task has a higher risk of interfering with a running + // app. Note that this does not apply when staging (when an exclusive + // lock isn't necessary), as there is no callback. + if (lastWriteError == ERROR_SHARING_VIOLATION && sUpdateSilently) { + LOG(( + "NS_main: callback app file in use, failed to exclusively open " + "executable file from background task: " LOG_S, + argv[callbackIndex])); + WriteStatusFile(WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION); + + proceedWithoutExclusive = false; + } + + if (!proceedWithoutExclusive) { + if (NS_tremove(gCallbackBackupPath) && errno != ENOENT) { + LOG( + ("NS_main: unable to remove backup of callback app file, " + "path: " LOG_S, + gCallbackBackupPath)); + } + output_finish(); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + LaunchCallbackApp(argv[5], argc - callbackIndex, + argv + callbackIndex, sUsingService); + return 1; + } + + LOG( + ("NS_main: callback app file in use, continuing without " + "exclusive access for executable file: " LOG_S, + argv[callbackIndex])); + } + } + } + + // DELETE_DIR is not required when performing a staged update or replace + // request; it can be used during a replace request but then it doesn't + // use gDeleteDirPath. + if (!sStagedUpdate && !sReplaceRequest) { + // The directory to move files that are in use to on Windows. This + // directory will be deleted after the update is finished, on OS reboot + // using MoveFileEx if it contains files that are in use, or by the post + // update process after the update finishes. On Windows when performing a + // normal update (e.g. the update is not a staged update and is not a + // replace request) gWorkingDirPath is the same as gInstallDirPath and + // gWorkingDirPath is used because it is the destination directory. + NS_tsnprintf(gDeleteDirPath, + sizeof(gDeleteDirPath) / sizeof(gDeleteDirPath[0]), + NS_T("%s/%s"), gWorkingDirPath, DELETE_DIR); + + if (NS_taccess(gDeleteDirPath, F_OK)) { + NS_tmkdir(gDeleteDirPath, 0755); + } + } +#endif /* XP_WIN */ + + // Run update process on a background thread. ShowProgressUI may return + // before QuitProgressUI has been called, so wait for UpdateThreadFunc to + // terminate. Avoid showing the progress UI when staging an update, or if + // this is an elevated process on OSX. + Thread t; + if (t.Run(UpdateThreadFunc, nullptr) == 0) { + if (!sStagedUpdate && !sReplaceRequest && !sUpdateSilently +#ifdef XP_MACOSX + && !isElevated +#endif + ) { + ShowProgressUI(); + } + } + t.Join(); + +#ifdef XP_WIN + if (argc > callbackIndex && !sReplaceRequest) { + if (callbackFile != INVALID_HANDLE_VALUE) { + CloseHandle(callbackFile); + } + // Remove the copy of the callback executable. + if (NS_tremove(gCallbackBackupPath) && errno != ENOENT) { + LOG( + ("NS_main: non-fatal error removing backup of callback app file, " + "path: " LOG_S, + gCallbackBackupPath)); + } + } + + if (!sStagedUpdate && !sReplaceRequest && _wrmdir(gDeleteDirPath)) { + LOG(("NS_main: unable to remove directory: " LOG_S ", err: %d", + DELETE_DIR, errno)); + // The directory probably couldn't be removed due to it containing files + // that are in use and will be removed on OS reboot. The call to remove + // the directory on OS reboot is done after the calls to remove the files + // so the files are removed first on OS reboot since the directory must be + // empty for the directory removal to be successful. The MoveFileEx call + // to remove the directory on OS reboot will fail if the process doesn't + // have write access to the HKEY_LOCAL_MACHINE registry key but this is ok + // since the installer / uninstaller will delete the directory along with + // its contents after an update is applied, on reinstall, and on + // uninstall. + if (MoveFileEx(gDeleteDirPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG(("NS_main: directory will be removed on OS reboot: " LOG_S, + DELETE_DIR)); + } else { + LOG( + ("NS_main: failed to schedule OS reboot removal of " + "directory: " LOG_S, + DELETE_DIR)); + } + } +#endif /* XP_WIN */ + + } // if (!isDMGInstall) + +#ifdef XP_MACOSX + if (isElevated) { + SetGroupOwnershipAndPermissions(gInstallDirPath); + freeArguments(argc, argv); + CleanupElevatedMacUpdate(false); + } else if (IsOwnedByGroupAdmin(gInstallDirPath)) { + // If the group ownership of the Firefox .app bundle was set to the "admin" + // group during a previous elevated update, we need to ensure that all files + // in the bundle have group ownership of "admin" as well as write permission + // for the group to not break updates in the future. + SetGroupOwnershipAndPermissions(gInstallDirPath); + } +#endif /* XP_MACOSX */ + + output_finish(); + + int retVal = LaunchCallbackAndPostProcessApps(argc, argv, callbackIndex +#ifdef XP_WIN + , + elevatedLockFilePath, + updateLockFileHandle +#elif XP_MACOSX + , + isElevated, + std::move(umaskContext) +#endif + ); + + return retVal ? retVal : (gSucceeded ? 0 : 1); +} + +class ActionList { + public: + ActionList() : mFirst(nullptr), mLast(nullptr), mCount(0) {} + ~ActionList(); + + void Append(Action* action); + int Prepare(); + int Execute(); + void Finish(int status); + + private: + Action* mFirst; + Action* mLast; + int mCount; +}; + +ActionList::~ActionList() { + Action* a = mFirst; + while (a) { + Action* b = a; + a = a->mNext; + delete b; + } +} + +void ActionList::Append(Action* action) { + if (mLast) { + mLast->mNext = action; + } else { + mFirst = action; + } + + mLast = action; + mCount++; +} + +int ActionList::Prepare() { + // If the action list is empty then we should fail in order to signal that + // something has gone wrong. Otherwise we report success when nothing is + // actually done. See bug 327140. + if (mCount == 0) { + LOG(("empty action list")); + return MAR_ERROR_EMPTY_ACTION_LIST; + } + + Action* a = mFirst; + int i = 0; + while (a) { + int rv = a->Prepare(); + if (rv) { + return rv; + } + + float percent = float(++i) / float(mCount); + UpdateProgressUI(PROGRESS_PREPARE_SIZE * percent); + + a = a->mNext; + } + + return OK; +} + +int ActionList::Execute() { + int currentProgress = 0, maxProgress = 0; + Action* a = mFirst; + while (a) { + maxProgress += a->mProgressCost; + a = a->mNext; + } + + a = mFirst; + while (a) { + int rv = a->Execute(); + if (rv) { + LOG(("### execution failed")); + return rv; + } + + currentProgress += a->mProgressCost; + float percent = float(currentProgress) / float(maxProgress); + UpdateProgressUI(PROGRESS_PREPARE_SIZE + PROGRESS_EXECUTE_SIZE * percent); + + a = a->mNext; + } + + return OK; +} + +void ActionList::Finish(int status) { + Action* a = mFirst; + int i = 0; + while (a) { + a->Finish(status); + + float percent = float(++i) / float(mCount); + UpdateProgressUI(PROGRESS_PREPARE_SIZE + PROGRESS_EXECUTE_SIZE + + PROGRESS_FINISH_SIZE * percent); + + a = a->mNext; + } + + if (status == OK) { + gSucceeded = true; + } +} + +#ifdef XP_WIN +int add_dir_entries(const NS_tchar* dirpath, ActionList* list) { + int rv = OK; + WIN32_FIND_DATAW finddata; + HANDLE hFindFile; + NS_tchar searchspec[MAXPATHLEN]; + NS_tchar foundpath[MAXPATHLEN]; + + NS_tsnprintf(searchspec, sizeof(searchspec) / sizeof(searchspec[0]), + NS_T("%s*"), dirpath); + mozilla::UniquePtr<const NS_tchar> pszSpec(get_full_path(searchspec)); + + hFindFile = FindFirstFileW(pszSpec.get(), &finddata); + if (hFindFile != INVALID_HANDLE_VALUE) { + do { + // Don't process the current or parent directory. + if (NS_tstrcmp(finddata.cFileName, NS_T(".")) == 0 || + NS_tstrcmp(finddata.cFileName, NS_T("..")) == 0) { + continue; + } + + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s%s"), dirpath, finddata.cFileName); + if (finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s/"), foundpath); + // Recurse into the directory. + rv = add_dir_entries(foundpath, list); + if (rv) { + LOG(("add_dir_entries error: " LOG_S ", err: %d", foundpath, rv)); + return rv; + } + } else { + // Add the file to be removed to the ActionList. + NS_tchar* quotedpath = get_quoted_path(foundpath); + if (!quotedpath) { + return PARSE_ERROR; + } + + mozilla::UniquePtr<Action> action(new RemoveFile()); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on recurse: " LOG_S ", err: %d", + quotedpath, rv)); + free(quotedpath); + return rv; + } + free(quotedpath); + + list->Append(action.release()); + } + } while (FindNextFileW(hFindFile, &finddata) != 0); + + FindClose(hFindFile); + { + // Add the directory to be removed to the ActionList. + NS_tchar* quotedpath = get_quoted_path(dirpath); + if (!quotedpath) { + return PARSE_ERROR; + } + + mozilla::UniquePtr<Action> action(new RemoveDir()); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on close: " LOG_S ", err: %d", + quotedpath, rv)); + } else { + list->Append(action.release()); + } + free(quotedpath); + } + } + + return rv; +} + +#elif defined(HAVE_FTS_H) + int add_dir_entries(const NS_tchar* dirpath, ActionList* list) { + int rv = OK; + FTS* ftsdir; + FTSENT* ftsdirEntry; + mozilla::UniquePtr<NS_tchar[]> searchpath(get_full_path(dirpath)); + + // Remove the trailing slash so the paths don't contain double slashes. The + // existence of the slash has already been checked in DoUpdate. + searchpath[NS_tstrlen(searchpath.get()) - 1] = NS_T('\0'); + char* const pathargv[] = {searchpath.get(), nullptr}; + + // FTS_NOCHDIR is used so relative paths from the destination directory are + // returned. + if (!(ftsdir = fts_open(pathargv, + FTS_PHYSICAL | FTS_NOSTAT | FTS_XDEV | FTS_NOCHDIR, + nullptr))) { + return UNEXPECTED_FILE_OPERATION_ERROR; + } + + while ((ftsdirEntry = fts_read(ftsdir)) != nullptr) { + NS_tchar foundpath[MAXPATHLEN]; + NS_tchar* quotedpath = nullptr; + mozilla::UniquePtr<Action> action; + + switch (ftsdirEntry->fts_info) { + // Filesystem objects that shouldn't be in the application's directories + case FTS_SL: + case FTS_SLNONE: + case FTS_DEFAULT: + LOG(("add_dir_entries: found a non-standard file: " LOG_S, + ftsdirEntry->fts_path)); + // Fall through and try to remove as a file + [[fallthrough]]; + + // Files + case FTS_F: + case FTS_NSOK: + // Add the file to be removed to the ActionList. + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s"), ftsdirEntry->fts_accpath); + quotedpath = get_quoted_path(get_relative_path(foundpath)); + if (!quotedpath) { + rv = UPDATER_QUOTED_PATH_MEM_ERROR; + break; + } + action.reset(new RemoveFile()); + rv = action->Parse(quotedpath); + free(quotedpath); + if (!rv) { + list->Append(action.release()); + } + break; + + // Directories + case FTS_DP: + // Add the directory to be removed to the ActionList. + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s/"), ftsdirEntry->fts_accpath); + quotedpath = get_quoted_path(get_relative_path(foundpath)); + if (!quotedpath) { + rv = UPDATER_QUOTED_PATH_MEM_ERROR; + break; + } + + action.reset(new RemoveDir()); + rv = action->Parse(quotedpath); + free(quotedpath); + if (!rv) { + list->Append(action.release()); + } + break; + + // Errors + case FTS_DNR: + case FTS_NS: + // ENOENT is an acceptable error for FTS_DNR and FTS_NS and means that + // we're racing with ourselves. Though strange, the entry will be + // removed anyway. + if (ENOENT == ftsdirEntry->fts_errno) { + rv = OK; + break; + } + [[fallthrough]]; + + case FTS_ERR: + rv = UNEXPECTED_FILE_OPERATION_ERROR; + LOG(("add_dir_entries: fts_read() error: " LOG_S ", err: %d", + ftsdirEntry->fts_path, ftsdirEntry->fts_errno)); + break; + + case FTS_DC: + rv = UNEXPECTED_FILE_OPERATION_ERROR; + LOG(("add_dir_entries: fts_read() returned FT_DC: " LOG_S, + ftsdirEntry->fts_path)); + break; + + default: + // FTS_D is ignored and FTS_DP is used instead (post-order). + rv = OK; + break; + } + + if (rv != OK) { + break; + } + } + + fts_close(ftsdir); + + return rv; + } + +#else + +int add_dir_entries(const NS_tchar* dirpath, ActionList* list) { + int rv = OK; + NS_tchar foundpath[PATH_MAX]; + struct { + dirent dent_buffer; + char chars[NAME_MAX]; + } ent_buf; + struct dirent* ent; + mozilla::UniquePtr<NS_tchar[]> searchpath(get_full_path(dirpath)); + + DIR* dir = opendir(searchpath.get()); + if (!dir) { + LOG(("add_dir_entries error on opendir: " LOG_S ", err: %d", + searchpath.get(), errno)); + return UNEXPECTED_FILE_OPERATION_ERROR; + } + + while (readdir_r(dir, (dirent*)&ent_buf, &ent) == 0 && ent) { + if ((strcmp(ent->d_name, ".") == 0) || (strcmp(ent->d_name, "..") == 0)) { + continue; + } + + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s%s"), searchpath.get(), ent->d_name); + struct stat64 st_buf; + int test = stat64(foundpath, &st_buf); + if (test) { + closedir(dir); + return UNEXPECTED_FILE_OPERATION_ERROR; + } + if (S_ISDIR(st_buf.st_mode)) { + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s%s/"), dirpath, ent->d_name); + // Recurse into the directory. + rv = add_dir_entries(foundpath, list); + if (rv) { + LOG(("add_dir_entries error: " LOG_S ", err: %d", foundpath, rv)); + closedir(dir); + return rv; + } + } else { + // Add the file to be removed to the ActionList. + NS_tchar* quotedpath = get_quoted_path(get_relative_path(foundpath)); + if (!quotedpath) { + closedir(dir); + return PARSE_ERROR; + } + + mozilla::UniquePtr<Action> action(new RemoveFile()); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on recurse: " LOG_S ", err: %d", + quotedpath, rv)); + free(quotedpath); + closedir(dir); + return rv; + } + free(quotedpath); + + list->Append(action.release()); + } + } + closedir(dir); + + // Add the directory to be removed to the ActionList. + NS_tchar* quotedpath = get_quoted_path(get_relative_path(dirpath)); + if (!quotedpath) { + return PARSE_ERROR; + } + + mozilla::UniquePtr<Action> action(new RemoveDir()); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on close: " LOG_S ", err: %d", quotedpath, + rv)); + } else { + list->Append(action.release()); + } + free(quotedpath); + + return rv; +} + +#endif + +/* + * Gets the contents of an update manifest file. The return value is malloc'd + * and it is the responsibility of the caller to free it. + * + * @param manifest + * The full path to the manifest file. + * @return On success the contents of the manifest and nullptr otherwise. + */ +static NS_tchar* GetManifestContents(const NS_tchar* manifest) { + AutoFile mfile(NS_tfopen(manifest, NS_T("rb"))); + if (mfile == nullptr) { + LOG(("GetManifestContents: error opening manifest file: " LOG_S, manifest)); + return nullptr; + } + + struct stat ms; + int rv = fstat(fileno((FILE*)mfile), &ms); + if (rv) { + LOG(("GetManifestContents: error stating manifest file: " LOG_S, manifest)); + return nullptr; + } + + char* mbuf = (char*)malloc(ms.st_size + 1); + if (!mbuf) { + return nullptr; + } + + size_t r = ms.st_size; + char* rb = mbuf; + while (r) { + const size_t count = mmin(SSIZE_MAX, r); + size_t c = fread(rb, 1, count, mfile); + if (c != count) { + LOG(("GetManifestContents: error reading manifest file: " LOG_S, + manifest)); + free(mbuf); + return nullptr; + } + + r -= c; + rb += c; + } + *rb = '\0'; + +#ifndef XP_WIN + return mbuf; +#else + NS_tchar* wrb = (NS_tchar*)malloc((ms.st_size + 1) * sizeof(NS_tchar)); + if (!wrb) { + free(mbuf); + return nullptr; + } + + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, mbuf, -1, wrb, + ms.st_size + 1)) { + LOG(("GetManifestContents: error converting utf8 to utf16le: %lu", + GetLastError())); + free(mbuf); + free(wrb); + return nullptr; + } + free(mbuf); + + return wrb; +#endif +} + +int AddPreCompleteActions(ActionList* list) { +#ifdef XP_MACOSX + mozilla::UniquePtr<NS_tchar[]> manifestPath( + get_full_path(NS_T("Contents/Resources/precomplete"))); +#else + mozilla::UniquePtr<NS_tchar[]> manifestPath( + get_full_path(NS_T("precomplete"))); +#endif + + NS_tchar* buf = GetManifestContents(manifestPath.get()); + if (!buf) { + LOG( + ("AddPreCompleteActions: error getting contents of precomplete " + "manifest")); + // Applications aren't required to have a precomplete manifest. The mar + // generation scripts enforce the presence of a precomplete manifest. + return OK; + } + NS_tchar* rb = buf; + + int rv; + NS_tchar* line; + while ((line = mstrtok(kNL, &rb)) != 0) { + // skip comments + if (*line == NS_T('#')) { + continue; + } + + NS_tchar* token = mstrtok(kWhitespace, &line); + if (!token) { + LOG(("AddPreCompleteActions: token not found in manifest")); + free(buf); + return PARSE_ERROR; + } + + Action* action = nullptr; + if (NS_tstrcmp(token, NS_T("remove")) == 0) { // rm file + action = new RemoveFile(); + } else if (NS_tstrcmp(token, NS_T("remove-cc")) == + 0) { // no longer supported + continue; + } else if (NS_tstrcmp(token, NS_T("rmdir")) == 0) { // rmdir if empty + action = new RemoveDir(); + } else { + LOG(("AddPreCompleteActions: unknown token: " LOG_S, token)); + free(buf); + return PARSE_ERROR; + } + + if (!action) { + free(buf); + return BAD_ACTION_ERROR; + } + + rv = action->Parse(line); + if (rv) { + delete action; + free(buf); + return rv; + } + + list->Append(action); + } + + free(buf); + return OK; +} + +int DoUpdate() { + NS_tchar manifest[MAXPATHLEN]; + NS_tsnprintf(manifest, sizeof(manifest) / sizeof(manifest[0]), + NS_T("%s/updating/update.manifest"), gWorkingDirPath); + ensure_parent_dir(manifest); + + // extract the manifest + int rv = gArchiveReader.ExtractFile("updatev3.manifest", manifest); + if (rv) { + LOG(("DoUpdate: error extracting manifest file")); + return rv; + } + + NS_tchar* buf = GetManifestContents(manifest); + // The manifest is located in the <working_dir>/updating directory which is + // removed after the update has finished so don't delete it here. + if (!buf) { + LOG(("DoUpdate: error opening manifest file: " LOG_S, manifest)); + return READ_ERROR; + } + NS_tchar* rb = buf; + + ActionList list; + NS_tchar* line; + bool isFirstAction = true; + while ((line = mstrtok(kNL, &rb)) != 0) { + // skip comments + if (*line == NS_T('#')) { + continue; + } + + NS_tchar* token = mstrtok(kWhitespace, &line); + if (!token) { + LOG(("DoUpdate: token not found in manifest")); + free(buf); + return PARSE_ERROR; + } + + if (isFirstAction) { + isFirstAction = false; + // The update manifest isn't required to have a type declaration. The mar + // generation scripts enforce the presence of the type declaration. + if (NS_tstrcmp(token, NS_T("type")) == 0) { + const NS_tchar* type = mstrtok(kQuote, &line); + LOG(("UPDATE TYPE " LOG_S, type)); + if (NS_tstrcmp(type, NS_T("complete")) == 0) { + rv = AddPreCompleteActions(&list); + if (rv) { + free(buf); + return rv; + } + } + continue; + } + } + + Action* action = nullptr; + if (NS_tstrcmp(token, NS_T("remove")) == 0) { // rm file + action = new RemoveFile(); + } else if (NS_tstrcmp(token, NS_T("rmdir")) == 0) { // rmdir if empty + action = new RemoveDir(); + } else if (NS_tstrcmp(token, NS_T("rmrfdir")) == 0) { // rmdir recursive + const NS_tchar* reldirpath = mstrtok(kQuote, &line); + if (!reldirpath) { + free(buf); + return PARSE_ERROR; + } + + if (reldirpath[NS_tstrlen(reldirpath) - 1] != NS_T('/')) { + free(buf); + return PARSE_ERROR; + } + + rv = add_dir_entries(reldirpath, &list); + if (rv) { + free(buf); + return rv; + } + + continue; + } else if (NS_tstrcmp(token, NS_T("add")) == 0) { + action = new AddFile(); + } else if (NS_tstrcmp(token, NS_T("patch")) == 0) { + action = new PatchFile(); + } else if (NS_tstrcmp(token, NS_T("add-if")) == 0) { // Add if exists + action = new AddIfFile(); + } else if (NS_tstrcmp(token, NS_T("add-if-not")) == + 0) { // Add if not exists + action = new AddIfNotFile(); + } else if (NS_tstrcmp(token, NS_T("patch-if")) == 0) { // Patch if exists + action = new PatchIfFile(); + } else { + LOG(("DoUpdate: unknown token: " LOG_S, token)); + free(buf); + return PARSE_ERROR; + } + + if (!action) { + free(buf); + return BAD_ACTION_ERROR; + } + + rv = action->Parse(line); + if (rv) { + free(buf); + return rv; + } + + list.Append(action); + } + + rv = list.Prepare(); + if (rv) { + free(buf); + return rv; + } + + rv = list.Execute(); + + list.Finish(rv); + free(buf); + return rv; +} diff --git a/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest b/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest new file mode 100644 index 0000000000..9df0057e64 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="Updater" + type="win32" +/> +<description>Updater</description> +<dependency> + <dependentAssembly> + <assemblyIdentity + type="win32" + name="Microsoft.Windows.Common-Controls" + version="6.0.0.0" + processorArchitecture="*" + publicKeyToken="6595b64144ccf1df" + language="*" + /> + </dependentAssembly> +</dependency> +<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:security> + <ms_asmv3:requestedPrivileges> + <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" /> + </ms_asmv3:requestedPrivileges> + </ms_asmv3:security> +</ms_asmv3:trustInfo> +<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + </application> +</compatibility> +<ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> + <dpiAware>True/PM</dpiAware> + <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness> + </ms_asmv3:windowsSettings> +</ms_asmv3:application> +</assembly> diff --git a/toolkit/mozapps/update/updater/updater.exe.manifest b/toolkit/mozapps/update/updater/updater.exe.manifest new file mode 100644 index 0000000000..6646ec6534 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.exe.manifest @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="Updater" + type="win32" +/> +<description>Updater</description> +<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:security> + <ms_asmv3:requestedPrivileges> + <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" /> + </ms_asmv3:requestedPrivileges> + </ms_asmv3:security> +</ms_asmv3:trustInfo> +<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + </application> +</compatibility> +<ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> + <dpiAware>True/PM</dpiAware> + <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness> + </ms_asmv3:windowsSettings> +</ms_asmv3:application> +</assembly> diff --git a/toolkit/mozapps/update/updater/updater.ico b/toolkit/mozapps/update/updater/updater.ico Binary files differnew file mode 100644 index 0000000000..48457029d6 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.ico diff --git a/toolkit/mozapps/update/updater/updater.png b/toolkit/mozapps/update/updater/updater.png Binary files differnew file mode 100644 index 0000000000..6f1251bd03 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.png diff --git a/toolkit/mozapps/update/updater/updater.rc b/toolkit/mozapps/update/updater/updater.rc new file mode 100644 index 0000000000..aff834a597 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.rc @@ -0,0 +1,137 @@ +/* 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/. */ + +// Microsoft Visual C++ generated resource script. +// +#if defined(TEST_UPDATER) || defined(DEP_UPDATER) +#include "../resource.h" +#define MANIFEST_PATH "../updater.exe.manifest" +#define COMCTL32_MANIFEST_PATH "../updater.exe.comctl32.manifest" +#define ICON_PATH "../updater.ico" +#else +#include "resource.h" +#define MANIFEST_PATH "updater.exe.manifest" +#define COMCTL32_MANIFEST_PATH "updater.exe.comctl32.manifest" +#define ICON_PATH "updater.ico" +#endif + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winresrc.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// RT_MANIFEST +// + +1 RT_MANIFEST MANIFEST_PATH +IDR_COMCTL32_MANIFEST RT_MANIFEST COMCTL32_MANIFEST_PATH + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +IDI_DIALOG ICON ICON_PATH + + +///////////////////////////////////////////////////////////////////////////// +// +// Embedded an identifier to uniquely identiy this as a Mozilla updater. +// + +STRINGTABLE +{ + IDS_UPDATER_IDENTITY, "moz-updater.exe-4cdccec4-5ee0-4a06-9817-4cd899a9db49" +} + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_DIALOG DIALOGEX 0, 0, 253, 41 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "",IDC_PROGRESS,"msctls_progress32",WS_BORDER,7,24,239,10 + LTEXT "",IDC_INFO,7,8,239,13,SS_NOPREFIX +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_DIALOG, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 246 + TOPMARGIN, 7 + BOTTOMMARGIN, 39 + END +END +#endif // APSTUDIO_INVOKED + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winresrc.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/toolkit/mozapps/update/updater/xpcshellCertificate.der b/toolkit/mozapps/update/updater/xpcshellCertificate.der Binary files differnew file mode 100644 index 0000000000..ea1fd47faa --- /dev/null +++ b/toolkit/mozapps/update/updater/xpcshellCertificate.der |