Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { AUSTLMY } from "resource://gre/modules/UpdateTelemetry.sys.mjs";
import {
Bits,
BitsRequest,
BitsUnknownError,
BitsVerificationError,
} from "resource://gre/modules/Bits.sys.mjs";
import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
UpdateLog: "resource://gre/modules/UpdateLog.sys.mjs",
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
ctypes: "resource://gre/modules/ctypes.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
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"
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"UpdateServiceStub",
"@mozilla.org/updates/update-service-stub;1",
"nsIApplicationUpdateServiceStub"
);
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_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_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_UPDROOT = "UpdRootD";
const KEY_OLD_UPDROOT = "OldUpdRootD";
const DIR_UPDATES = "updates";
const DIR_UPDATE_READY = "0";
const DIR_UPDATE_DOWNLOADING = "downloading";
const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
const FILE_BACKUP_UPDATE_ELEVATED_LOG = "backup-update-elevated.log";
const FILE_LAST_UPDATE_LOG = "last-update.log";
const FILE_LAST_UPDATE_ELEVATED_LOG = "last-update-elevated.log";
const FILE_UPDATES_XML = "updates.xml";
const FILE_UPDATE_LOG = "update.log";
const FILE_UPDATE_ELEVATED_LOG = "update-elevated.log";
const FILE_UPDATE_MAR = "update.mar";
const FILE_UPDATE_STATUS = "update.status";
const FILE_UPDATE_TEST = "update.test";
const FILE_UPDATE_VERSION = "update.version";
const STATE_NONE = "null";
const STATE_DOWNLOADING = "downloading";
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 number of update attempts when a write error occurs during an attempt
const MAX_TOTAL_INSTALL_ATTEMPTS = 2;
// 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.sys.mjs 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 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,
];
// 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;
// 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 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();
ChromeUtils.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.
*/
ChromeUtils.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() {
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
);
// return immediately if timeout value is invalid.
if (timeout <= 0) {
return Promise.resolve(isOtherInstanceRunning());
}
// 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
);
if (interval <= 0) {
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) {
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);
var data = new TextEncoder().encode(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();
}
/*
* See nsIUpdateService.idl
*/
function isAppBaseDirWritable() {
let appDirTestFile = "";
try {
appDirTestFile = getAppBaseDir();
appDirTestFile.append(FILE_UPDATE_TEST);
} catch (e) {
LOG(
"isAppBaseDirWritable - Base directory or test path could not be " +
`determined: ${e}`
);
return false;
}
try {
LOG(
`isAppBaseDirWritable - testing write access for ${appDirTestFile.path}`
);
if (appDirTestFile.exists()) {
appDirTestFile.remove(false);
}
// if we're unable to create the test file this will throw an exception:
appDirTestFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
appDirTestFile.remove(false);
LOG(`isAppBaseDirWritable - Path is writable: ${appDirTestFile.path}`);
return true;
} catch (e) {
LOG(
`isAppBaseDirWritable - Path '${appDirTestFile.path}' ` +
`is not writable: ${e}`
);
}
// No write access to the installation directory
return false;
}
/**
* 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 && !isAppBaseDirWritable) {
LOG(
"getCanApplyUpdates - unable to apply updates, because the base " +
"directory is not writable."
);
}
}
} 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.
*/
ChromeUtils.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) {
lazy.UpdateLog.logPrefixedString("AUS:SVC", string);
}
/**
* 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;
}
}
let dir = FileUtils.getDir(KEY_UPDROOT, pathArray);
try {
dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
} catch (ex) {
if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
throw ex;
}
// Ignore the exception due to a directory that already exists.
}
return dir;
}
/**
* 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 macOS, the executable is stored under Contents/MacOS.
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, DIR_UPDATE_READY]);
}
/**
* 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, DIR_UPDATE_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;
}
/**
* 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 files for debugging purposes.
// Make sure to keep the pairs of logs (ex "last-update.log" and
// "last-update-elevated.log") together. We don't want to skip moving
// "last-update-elevated.log" just because there isn't an
// "update-elevated.log" to take its place.
let updateLogFile = updateDir.clone();
updateLogFile.append(FILE_UPDATE_LOG);
let updateElevatedLogFile = updateDir.clone();
updateElevatedLogFile.append(FILE_UPDATE_ELEVATED_LOG);
if (updateLogFile.exists() || updateElevatedLogFile.exists()) {
const overwriteOrRemoveBackupLog = (log, shouldOverwrite, backupName) => {
if (shouldOverwrite) {
try {
log.moveTo(dir, backupName);
} catch (e) {
LOG(
`cleanUpReadyUpdateDir - failed to rename file '${log.path}' to ` +
`'${backupName}': ${e.result}`
);
}
} else {
// If we don't have a file to overwrite this one, make sure we remove
// it anyways to prevent log pairs from getting mismatched.
let backupLogFile = dir.clone();
backupLogFile.append(backupName);
try {
backupLogFile.remove(false);
} catch (e) {
if (e.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
LOG(
`cleanUpReadyUpdateDir - failed to remove file ` +
`'${backupLogFile.path}': ${e.result}`
);
}
}
}
};
let dir = updateDir.parent;
let logFile = dir.clone();
logFile.append(FILE_LAST_UPDATE_LOG);
const logFileExists = logFile.exists();
let elevatedLogFile = dir.clone();
elevatedLogFile.append(FILE_LAST_UPDATE_ELEVATED_LOG);
const elevatedLogFileExists = elevatedLogFile.exists();
if (logFileExists || elevatedLogFileExists) {
overwriteOrRemoveBackupLog(
logFile,
logFileExists,
FILE_BACKUP_UPDATE_LOG
);
overwriteOrRemoveBackupLog(
elevatedLogFile,
elevatedLogFileExists,
FILE_BACKUP_UPDATE_ELEVATED_LOG
);
}
overwriteOrRemoveBackupLog(updateLogFile, true, FILE_LAST_UPDATE_LOG);
overwriteOrRemoveBackupLog(
updateElevatedLogFile,
true,
FILE_LAST_UPDATE_ELEVATED_LOG
);
}
if (aRemovePatchFiles) {
let dirEntries;
try {
dirEntries = updateDir.directoryEntries;
} catch (ex) {
// If if doesn't exist, our job is already done.
if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
return;
}
throw ex;
}
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;
try {
dirEntries = updateDir.directoryEntries;
} catch (ex) {
// If if doesn't exist, our job is already done.
if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
return;
}
throw ex;
}
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.internal.readyUpdate) {
LOG("cleanupReadyUpdate - Clearing readyUpdate");
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.readyUpdate);
lazy.UM.internal.readyUpdate = null;
}
lazy.UM.saveUpdates();
let readyUpdateDir = getReadyUpdateDir();
let shouldSetDownloadingStatus =
lazy.UM.internal.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.internal.downloadingUpdate) {
LOG("cleanupDownloadingUpdate - Clearing downloadingUpdate.");
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
lazy.UM.internal.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(false);
}
}
/**
* 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.internal.readyUpdate) {
LOG("cleanupActiveUpdates - Clearing readyUpdate");
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.readyUpdate);
lazy.UM.internal.readyUpdate = null;
}
if (lazy.UM.internal.downloadingUpdate) {
LOG("cleanupActiveUpdates - Clearing downloadingUpdate.");
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
lazy.UM.internal.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)) {
let nextState = getBestPendingState();
// Check how many install attempts we have with this patch
let totalInstallAttempts =
update.selectedPatch
.QueryInterface(Ci.nsIWritablePropertyBag)
.getProperty("numTotalInstallAttempts") ?? 0;
// Out of retries, unable to handle the update failure here
if (totalInstallAttempts >= MAX_TOTAL_INSTALL_ATTEMPTS) {
return false;
}
LOG(
"handleUpdateFailure - Failure is a write error. Setting state to " +
nextState
);
writeStatusFile(getReadyUpdateDir(), (update.state = nextState));
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
return true;
}
if (update.errorCode == BACKGROUND_TASK_SHARING_VIOLATION) {
let newState = getBestPendingState();
LOG(
"handleUpdateFailure - witnessed BACKGROUND_TASK_SHARING_VIOLATION, setting state to " +
newState
);
writeStatusFile(getReadyUpdateDir(), (update.state = newState));
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 {
const nextState = getBestPendingState();
LOG(
`handleUpdateFailure - Failure because elevation was cancelled. ` +
`Setting status to ${nextState}.`
);
writeStatusFile(getReadyUpdateDir(), (update.state = nextState));
}
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.internal.downloadingUpdate || lazy.UM.internal.readyUpdate;
if (!update) {
LOG(
"handleFallbackToCompleteUpdate - Unable to find an update to fall " +
"