forked from mirrors/gecko-dev
845 lines
27 KiB
JavaScript
845 lines
27 KiB
JavaScript
/* 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.import(
|
|
"resource://gre/modules/AppConstants.jsm"
|
|
);
|
|
const { FileUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/FileUtils.jsm"
|
|
);
|
|
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 = {};
|
|
XPCOMUtils.defineLazyModuleGetters(lazy, {
|
|
UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
|
|
});
|
|
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";
|
|
|
|
/**
|
|
* 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 {
|
|
constructor() {
|
|
this._listeners = new Set();
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"aus",
|
|
"@mozilla.org/updates/update-service;1",
|
|
"nsIApplicationUpdateService"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"checker",
|
|
"@mozilla.org/updates/update-checker;1",
|
|
"nsIUpdateChecker"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"um",
|
|
"@mozilla.org/updates/update-manager;1",
|
|
"nsIUpdateManager"
|
|
);
|
|
this.QueryInterface = ChromeUtils.generateQI([
|
|
"nsIObserver",
|
|
"nsIProgressEventSink",
|
|
"nsIRequestObserver",
|
|
"nsISupportsWeakReference",
|
|
]);
|
|
Services.obs.addObserver(this, "update-swap", /* ownsWeak */ true);
|
|
|
|
// This one call observes PREF_APP_UPDATE_LOG and PREF_APP_UPDATE_LOG_FILE
|
|
Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
check() {
|
|
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.updateDisabledByPolicy) {
|
|
LOG("AppUpdater:check - this.updateDisabledByPolicy");
|
|
this._setStatus(AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY);
|
|
return;
|
|
}
|
|
|
|
if (this.isReadyForRestart) {
|
|
LOG("AppUpdater:check - this.isReadyForRestart");
|
|
this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
|
|
return;
|
|
}
|
|
|
|
if (this.aus.isOtherInstanceHandlingUpdates) {
|
|
LOG("AppUpdater:check - this.aus.isOtherInstanceHandlingUpdates");
|
|
this._setStatus(AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES);
|
|
return;
|
|
}
|
|
|
|
if (this.isDownloading) {
|
|
LOG("AppUpdater:check - this.isDownloading");
|
|
this.startDownload();
|
|
return;
|
|
}
|
|
|
|
if (this.isStaging) {
|
|
LOG("AppUpdater:check - this.isStaging");
|
|
this._waitForUpdateToStage();
|
|
return;
|
|
}
|
|
|
|
// We might need this value later, so start loading it from the disk now.
|
|
this.promiseAutoUpdateSetting = lazy.UpdateUtils.getAppUpdateAutoEnabled();
|
|
|
|
// That leaves the options
|
|
// "Check for updates, but let me choose whether to install them", and
|
|
// "Automatically install updates".
|
|
// In both cases, we check for updates without asking.
|
|
// In the "let me choose" case, we ask before downloading though, in onCheckComplete.
|
|
this.checkForUpdates();
|
|
}
|
|
|
|
// true when there is an update ready to be applied on restart or staged.
|
|
get isPending() {
|
|
if (this.update) {
|
|
return (
|
|
this.update.state == "pending" ||
|
|
this.update.state == "pending-service" ||
|
|
this.update.state == "pending-elevate"
|
|
);
|
|
}
|
|
return (
|
|
this.um.readyUpdate &&
|
|
(this.um.readyUpdate.state == "pending" ||
|
|
this.um.readyUpdate.state == "pending-service" ||
|
|
this.um.readyUpdate.state == "pending-elevate")
|
|
);
|
|
}
|
|
|
|
// true when there is an update already staged.
|
|
get isApplied() {
|
|
if (this.update) {
|
|
return (
|
|
this.update.state == "applied" || this.update.state == "applied-service"
|
|
);
|
|
}
|
|
return (
|
|
this.um.readyUpdate &&
|
|
(this.um.readyUpdate.state == "applied" ||
|
|
this.um.readyUpdate.state == "applied-service")
|
|
);
|
|
}
|
|
|
|
get isStaging() {
|
|
if (!this.updateStagingEnabled) {
|
|
return false;
|
|
}
|
|
let errorCode;
|
|
if (this.update) {
|
|
errorCode = this.update.errorCode;
|
|
} else if (this.um.readyUpdate) {
|
|
errorCode = this.um.readyUpdate.errorCode;
|
|
}
|
|
// If the state is pending and the error code is not 0, staging must have
|
|
// failed.
|
|
return this.isPending && errorCode == 0;
|
|
}
|
|
|
|
// true when an update ready to restart to finish the update process.
|
|
get isReadyForRestart() {
|
|
if (this.updateStagingEnabled) {
|
|
let errorCode;
|
|
if (this.update) {
|
|
errorCode = this.update.errorCode;
|
|
} else if (this.um.readyUpdate) {
|
|
errorCode = this.um.readyUpdate.errorCode;
|
|
}
|
|
// If the state is pending and the error code is not 0, staging must have
|
|
// failed and Firefox should be restarted to try to apply the update
|
|
// without staging.
|
|
return this.isApplied || (this.isPending && errorCode != 0);
|
|
}
|
|
return this.isPending;
|
|
}
|
|
|
|
// true when there is an update download in progress.
|
|
get isDownloading() {
|
|
if (this.update) {
|
|
return this.update.state == "downloading";
|
|
}
|
|
return (
|
|
this.um.downloadingUpdate &&
|
|
this.um.downloadingUpdate.state == "downloading"
|
|
);
|
|
}
|
|
|
|
// true when updating has been disabled by enterprise policy
|
|
get updateDisabledByPolicy() {
|
|
return Services.policies && !Services.policies.isAllowed("appUpdate");
|
|
}
|
|
|
|
// true if updating is disabled because we're running in an app package.
|
|
// This is distinct from updateDisabledByPolicy 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() {
|
|
try {
|
|
return Services.sysinfo.getProperty("hasWinPackageId");
|
|
} catch (_ex) {
|
|
// The hasWinPackageId property doesn't exist; assume it would be false.
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// true when updating in background is enabled.
|
|
get updateStagingEnabled() {
|
|
LOG(
|
|
"AppUpdater:updateStagingEnabled" +
|
|
"canStageUpdates: " +
|
|
this.aus.canStageUpdates
|
|
);
|
|
return (
|
|
!this.updateDisabledByPolicy &&
|
|
!this.updateDisabledByPackage &&
|
|
this.aus.canStageUpdates
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check for updates
|
|
*/
|
|
checkForUpdates() {
|
|
// 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);
|
|
this.checker.checkForUpdates(this._updateCheckListener, true);
|
|
// after checking, onCheckComplete() is called
|
|
LOG("AppUpdater:checkForUpdates - waiting for onCheckComplete()");
|
|
}
|
|
|
|
/**
|
|
* Implements nsIUpdateCheckListener. The methods implemented by
|
|
* nsIUpdateCheckListener are in a different scope from nsIIncrementalDownload
|
|
* to make it clear which are used by each interface.
|
|
*/
|
|
get _updateCheckListener() {
|
|
if (!this.__updateCheckListener) {
|
|
this.__updateCheckListener = {
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
onCheckComplete: async (aRequest, aUpdates) => {
|
|
LOG("AppUpdater:_updateCheckListener:onCheckComplete - reached.");
|
|
this.update = this.aus.selectUpdate(aUpdates);
|
|
if (!this.update) {
|
|
LOG(
|
|
"AppUpdater:_updateCheckListener:onCheckComplete - result: " +
|
|
"NO_UPDATES_FOUND"
|
|
);
|
|
this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
|
|
return;
|
|
}
|
|
|
|
if (this.update.unsupported) {
|
|
LOG(
|
|
"AppUpdater:_updateCheckListener:onCheckComplete - result: " +
|
|
"UNSUPPORTED SYSTEM"
|
|
);
|
|
this._setStatus(AppUpdater.STATUS.UNSUPPORTED_SYSTEM);
|
|
return;
|
|
}
|
|
|
|
if (!this.aus.canApplyUpdates) {
|
|
LOG(
|
|
"AppUpdater:_updateCheckListener:onCheckComplete - result: " +
|
|
"MANUAL_UPDATE"
|
|
);
|
|
this._setStatus(AppUpdater.STATUS.MANUAL_UPDATE);
|
|
return;
|
|
}
|
|
|
|
if (!this.promiseAutoUpdateSetting) {
|
|
this.promiseAutoUpdateSetting = lazy.UpdateUtils.getAppUpdateAutoEnabled();
|
|
}
|
|
this.promiseAutoUpdateSetting.then(updateAuto => {
|
|
if (updateAuto && !this.aus.manualUpdateOnly) {
|
|
LOG(
|
|
"AppUpdater:_updateCheckListener:onCheckComplete - " +
|
|
"updateAuto is active and " +
|
|
"manualUpdateOnlydateOnly is inactive." +
|
|
"start the download."
|
|
);
|
|
// automatically download and install
|
|
this.startDownload();
|
|
} else {
|
|
// ask
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_AND_INSTALL);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
onError: async (aRequest, aUpdate) => {
|
|
// Errors in the update check are treated as no updates found. If the
|
|
// update check fails repeatedly without a success the user will be
|
|
// notified with the normal app update user interface so this is safe.
|
|
LOG("AppUpdater:_updateCheckListener:onError: NO_UPDATES_FOUND");
|
|
this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
|
|
},
|
|
|
|
/**
|
|
* See nsISupports.idl
|
|
*/
|
|
QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckListener"]),
|
|
};
|
|
}
|
|
return this.__updateCheckListener;
|
|
}
|
|
|
|
/**
|
|
* Sets the status to STAGING. The status will then be set again when the
|
|
* update finishes staging.
|
|
*/
|
|
_waitForUpdateToStage() {
|
|
if (!this.update) {
|
|
this.update = this.um.readyUpdate;
|
|
}
|
|
this.update.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
this.update.setProperty("foregroundDownload", "true");
|
|
this._setStatus(AppUpdater.STATUS.STAGING);
|
|
this._awaitStagingComplete();
|
|
}
|
|
|
|
/**
|
|
* Starts the download of an update mar.
|
|
*/
|
|
startDownload() {
|
|
if (!this.update) {
|
|
this.update = this.um.downloadingUpdate;
|
|
}
|
|
this.update.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
this.update.setProperty("foregroundDownload", "true");
|
|
|
|
let success = this.aus.downloadUpdate(this.update, false);
|
|
if (!success) {
|
|
LOG("AppUpdater:startDownload - downloadUpdate failed.");
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
return;
|
|
}
|
|
|
|
this._setupDownloadListener();
|
|
}
|
|
|
|
/**
|
|
* Starts tracking the download.
|
|
*/
|
|
_setupDownloadListener() {
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOADING);
|
|
this.aus.addDownloadListener(this);
|
|
LOG("AppUpdater:_setupDownloadListener - registered a download listener");
|
|
}
|
|
|
|
/**
|
|
* See nsIRequestObserver.idl
|
|
*/
|
|
onStartRequest(aRequest) {
|
|
LOG("AppUpdater:onStartRequest - aRequest: " + aRequest);
|
|
}
|
|
|
|
/**
|
|
* See nsIRequestObserver.idl
|
|
*/
|
|
onStopRequest(aRequest, aStatusCode) {
|
|
LOG(
|
|
"AppUpdater:onStopRequest " +
|
|
"- aRequest: " +
|
|
aRequest +
|
|
", aStatusCode: " +
|
|
aStatusCode
|
|
);
|
|
switch (aStatusCode) {
|
|
case Cr.NS_ERROR_UNEXPECTED:
|
|
if (
|
|
this.update.selectedPatch.state == "download-failed" &&
|
|
(this.update.isCompleteUpdate || this.update.patchCount != 2)
|
|
) {
|
|
// Verification error of complete patch, informational text is held in
|
|
// the update object.
|
|
this.aus.removeDownloadListener(this);
|
|
LOG(
|
|
"AppUpdater:onStopRequest " +
|
|
"- download failed with unexpected error" +
|
|
", removed download listener"
|
|
);
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
break;
|
|
}
|
|
// Verification failed for a partial patch, complete patch is now
|
|
// downloading so return early and do NOT remove the download listener!
|
|
break;
|
|
case Cr.NS_BINDING_ABORTED:
|
|
// Do not remove UI listener since the user may resume downloading again.
|
|
break;
|
|
case Cr.NS_OK:
|
|
this.aus.removeDownloadListener(this);
|
|
LOG(
|
|
"AppUpdater:onStopRequest " +
|
|
"- download ok" +
|
|
", removed download listener"
|
|
);
|
|
if (this.updateStagingEnabled) {
|
|
// It could be that another instance was started during the download,
|
|
// and if that happened, then we actually should not advance to the
|
|
// STAGING status because the staging process isn't really happening
|
|
// until that instance exits (or we time out waiting).
|
|
if (this.aus.isOtherInstanceHandlingUpdates) {
|
|
LOG(
|
|
"AppUpdater:onStopRequest " +
|
|
"- aStatusCode=Cr.NS_OK" +
|
|
", another instance is handling updates"
|
|
);
|
|
this._setStatus(AppUpdater.OTHER_INSTANCE_HANDLING_UPDATES);
|
|
} else {
|
|
LOG(
|
|
"AppUpdater:onStopRequest " +
|
|
"- aStatusCode=Cr.NS_OK" +
|
|
", no competitive instance found."
|
|
);
|
|
this._setStatus(AppUpdater.STATUS.STAGING);
|
|
}
|
|
// But we should register the staging observer in either case, because
|
|
// if we do time out waiting for the other instance to exit, then
|
|
// staging really will start at that point.
|
|
this._awaitStagingComplete();
|
|
} else {
|
|
this._awaitDownloadComplete();
|
|
}
|
|
break;
|
|
default:
|
|
this.aus.removeDownloadListener(this);
|
|
LOG(
|
|
"AppUpdater:onStopRequest " +
|
|
"- case default" +
|
|
", removing download listener" +
|
|
", because the download failed."
|
|
);
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIProgressEventSink.idl
|
|
*/
|
|
onStatus(aRequest, aStatus, aStatusArg) {
|
|
LOG(
|
|
"AppUpdater:onStatus " +
|
|
"- aRequest: " +
|
|
aRequest +
|
|
", aStatus: " +
|
|
aStatus +
|
|
", aStatusArg: " +
|
|
aStatusArg
|
|
);
|
|
}
|
|
|
|
/**
|
|
* See nsIProgressEventSink.idl
|
|
*/
|
|
onProgress(aRequest, aProgress, aProgressMax) {
|
|
LOG(
|
|
"AppUpdater:onProgress " +
|
|
"- aRequest: " +
|
|
aRequest +
|
|
", aProgress: " +
|
|
aProgress +
|
|
", aProgressMax: " +
|
|
aProgressMax
|
|
);
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOADING, aProgress, aProgressMax);
|
|
}
|
|
|
|
/**
|
|
* This function registers an observer that watches for the download
|
|
* to complete. Once it does, it updates the status accordingly.
|
|
*/
|
|
_awaitDownloadComplete() {
|
|
let observer = (aSubject, aTopic, aData) => {
|
|
// Update the UI when the download is finished
|
|
LOG(
|
|
"AppUpdater:_awaitStagingComplete - observer reached" +
|
|
", status changes to READY_FOR_RESTART"
|
|
);
|
|
this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
|
|
Services.obs.removeObserver(observer, "update-downloaded");
|
|
};
|
|
Services.obs.addObserver(observer, "update-downloaded");
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
_awaitStagingComplete() {
|
|
let observer = (aSubject, aTopic, aData) => {
|
|
LOG(
|
|
"AppUpdater:_awaitStagingComplete:observer" +
|
|
"- aSubject: " +
|
|
aSubject +
|
|
"- aTopic: " +
|
|
aTopic +
|
|
"- aData (=status): " +
|
|
aData
|
|
);
|
|
// Update the UI when the background updater is finished
|
|
switch (aTopic) {
|
|
case "update-staged":
|
|
let status = aData;
|
|
if (
|
|
status == "applied" ||
|
|
status == "applied-service" ||
|
|
status == "pending" ||
|
|
status == "pending-service" ||
|
|
status == "pending-elevate"
|
|
) {
|
|
// If the update is successfully applied, or if the updater has
|
|
// fallen back to non-staged updates, show the "Restart to Update"
|
|
// button.
|
|
this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
|
|
} else if (status == "failed") {
|
|
// Background update has failed, let's show the UI responsible for
|
|
// prompting the user to update manually.
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
} else if (status == "downloading") {
|
|
// We've fallen back to downloading the complete update because the
|
|
// partial update failed to get staged in the background.
|
|
// Therefore we need to keep our observer.
|
|
this._setupDownloadListener();
|
|
return;
|
|
}
|
|
break;
|
|
case "update-error":
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
|
break;
|
|
}
|
|
Services.obs.removeObserver(observer, "update-staged");
|
|
Services.obs.removeObserver(observer, "update-error");
|
|
};
|
|
Services.obs.addObserver(observer, "update-staged");
|
|
Services.obs.addObserver(observer, "update-error");
|
|
}
|
|
|
|
/**
|
|
* Stops the current check for updates and any ongoing download.
|
|
*/
|
|
stop() {
|
|
LOG("AppUpdater:stop called, remove download listener");
|
|
this.checker.stopCurrentCheck();
|
|
this.aus.removeDownloadListener(this);
|
|
}
|
|
|
|
/**
|
|
* {AppUpdater.STATUS} The status of the current check or update.
|
|
*/
|
|
get status() {
|
|
if (!this._status) {
|
|
if (!AppConstants.MOZ_UPDATER || this.updateDisabledByPackage) {
|
|
LOG("AppUpdater:status - no updater or updates disabled by package.");
|
|
this._status = AppUpdater.STATUS.NO_UPDATER;
|
|
} else if (this.updateDisabledByPolicy) {
|
|
LOG("AppUpdater:status - updateDisabledByPolicy");
|
|
this._status = AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY;
|
|
} else if (this.isReadyForRestart) {
|
|
LOG("AppUpdater:status - isReadyForRestart");
|
|
this._status = AppUpdater.STATUS.READY_FOR_RESTART;
|
|
} else if (this.aus.isOtherInstanceHandlingUpdates) {
|
|
LOG("AppUpdater:status - another instance is handling updates");
|
|
this._status = AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES;
|
|
} else if (this.isDownloading) {
|
|
LOG("AppUpdater:status - isDownloading");
|
|
this._status = AppUpdater.STATUS.DOWNLOADING;
|
|
} else if (this.isStaging) {
|
|
LOG("AppUpdater:status - isStaging");
|
|
this._status = AppUpdater.STATUS.STAGING;
|
|
} else {
|
|
LOG("AppUpdater:status - NEVER_CHECKED");
|
|
this._status = AppUpdater.STATUS.NEVER_CHECKED;
|
|
}
|
|
}
|
|
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._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;
|
|
case "quit-application":
|
|
Services.prefs.removeObserver(PREF_APP_UPDATE_LOG, this);
|
|
Services.obs.removeObserver(this, topic);
|
|
}
|
|
}
|
|
|
|
_handleUpdateSwap() {
|
|
// This function exists to deal with the fact that we support handling 2
|
|
// updates at once: a ready update and a downloading update. But AppUpdater
|
|
// only ever really considers a single update at a time.
|
|
// We see an update swap just when the downloading update has finished
|
|
// downloading and is being swapped into UpdateManager.readyUpdate. At this
|
|
// point, we are in one of two states. Either:
|
|
// a) The update that is being swapped in is the update that this
|
|
// AppUpdater has already been tracking, or
|
|
// b) We've been tracking the ready update. Now that the downloading
|
|
// update is about to be swapped into the place of the ready update, we
|
|
// need to switch over to tracking the new update.
|
|
if (
|
|
this._status == AppUpdater.STATUS.DOWNLOADING ||
|
|
this._status == AppUpdater.STATUS.STAGING
|
|
) {
|
|
// We are already tracking the correct update.
|
|
return;
|
|
}
|
|
|
|
if (this.updateStagingEnabled) {
|
|
LOG("AppUpdater:_handleUpdateSwap - updateStagingEnabled");
|
|
this._setStatus(AppUpdater.STATUS.STAGING);
|
|
this._awaitStagingComplete();
|
|
} else {
|
|
LOG("AppUpdater:_handleUpdateSwap - updateStagingDisabled");
|
|
this._setStatus(AppUpdater.STATUS.DOWNLOADING);
|
|
this._awaitDownloadComplete();
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
|
|
/**
|
|
* 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
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|