/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; var gLogfileOutputStream; 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. */ export 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 ); } } } }