Source code

Revision control

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/. */
"use strict";
const { AppConstants } = ChromeUtils.import(
);
const { AUSTLMY } = ChromeUtils.import(
);
const {
Bits,
BitsRequest,
BitsUnknownError,
BitsVerificationError,
} = ChromeUtils.import("resource://gre/modules/Bits.jsm");
const { FileUtils } = ChromeUtils.import(
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
);
XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser", "XMLHttpRequest"]);
XPCOMUtils.defineLazyModuleGetters(this, {
});
if (AppConstants.ENABLE_REMOTE_AGENT) {
XPCOMUtils.defineLazyServiceGetter(
this,
"RemoteAgent",
"@mozilla.org/remote/agent;1",
"nsIRemoteAgent"
);
} else {
this.RemoteAgent = { listening: false };
}
const UPDATESERVICE_CID = Components.ID(
"{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}"
);
const PREF_APP_UPDATE_ALTUPDATEDIRPATH = "app.update.altUpdateDirPath";
const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
const PREF_APP_UPDATE_BITS_ENABLED = "app.update.BITS.enabled";
const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations";
const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED =
"app.update.checkOnlyInstance.enabled";
const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_INTERVAL =
"app.update.checkOnlyInstance.interval";
const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT =
"app.update.checkOnlyInstance.timeout";
const PREF_APP_UPDATE_DISABLEDFORTESTING = "app.update.disabledForTesting";
const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS = "app.update.download.attempts";
const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts";
const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
const PREF_APP_UPDATE_ELEVATE_VERSION = "app.update.elevate.version";
const PREF_APP_UPDATE_ELEVATE_ATTEMPTS = "app.update.elevate.attempts";
const PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS = "app.update.elevate.maxAttempts";
const PREF_APP_UPDATE_LANGPACK_ENABLED = "app.update.langpack.enabled";
const PREF_APP_UPDATE_LANGPACK_TIMEOUT = "app.update.langpack.timeout";
const PREF_APP_UPDATE_LOG = "app.update.log";
const PREF_APP_UPDATE_LOG_FILE = "app.update.log.file";
const PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD = "app.update.notifyDuringDownload";
const PREF_APP_UPDATE_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 =
const KEY_EXECUTABLE = "XREExeF";
const KEY_PROFILE_DIR = "ProfD";
const KEY_UPDROOT = "UpdRootD";
const DIR_UPDATES = "updates";
const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
const FILE_BT_RESULT = "bt.result";
const FILE_LAST_UPDATE_LOG = "last-update.log";
const FILE_UPDATES_XML = "updates.xml";
const FILE_UPDATE_LOG = "update.log";
const FILE_UPDATE_MAR = "update.mar";
const FILE_UPDATE_STATUS = "update.status";
const FILE_UPDATE_TEST = "update.test";
const FILE_UPDATE_VERSION = "update.version";
const FILE_UPDATE_MESSAGES = "update_messages.log";
const STATE_NONE = "null";
const STATE_DOWNLOADING = "downloading";
const STATE_PENDING = "pending";
const STATE_PENDING_SERVICE = "pending-service";
const STATE_PENDING_ELEVATE = "pending-elevate";
const STATE_APPLYING = "applying";
const STATE_APPLIED = "applied";
const STATE_APPLIED_SERVICE = "applied-service";
const STATE_SUCCEEDED = "succeeded";
const STATE_DOWNLOAD_FAILED = "download-failed";
const STATE_FAILED = "failed";
// BITS will keep retrying a download after transient errors, unless this much
// time has passed since there has been download progress.
// Similarly to ...POLL_RATE_MS below, we are much more aggressive when the user
// is watching the download progress.
const BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS = 3600; // 1 hour
const BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS = 5;
// These value control how frequently we get updates from the BITS client on
// the progress made downloading. The difference between the two is that the
// active interval is the one used when the user is watching. The idle interval
// is the one used when no one is watching.
const BITS_IDLE_POLL_RATE_MS = 1000;
const BITS_ACTIVE_POLL_RATE_MS = 200;
// The values below used by this code are from common/updatererrors.h
const WRITE_ERROR = 7;
const ELEVATION_CANCELED = 9;
const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24;
const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25;
const SERVICE_UPDATER_SIGN_ERROR = 26;
const SERVICE_UPDATER_COMPARE_ERROR = 27;
const SERVICE_UPDATER_IDENTITY_ERROR = 28;
const SERVICE_STILL_APPLYING_ON_SUCCESS = 29;
const SERVICE_STILL_APPLYING_ON_FAILURE = 30;
const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31;
const SERVICE_COULD_NOT_LOCK_UPDATER = 32;
const SERVICE_INSTALLDIR_ERROR = 33;
const WRITE_ERROR_ACCESS_DENIED = 35;
const WRITE_ERROR_CALLBACK_APP = 37;
const UNEXPECTED_STAGING_ERROR = 43;
const DELETE_ERROR_STAGING_LOCK_FILE = 44;
const SERVICE_COULD_NOT_COPY_UPDATER = 49;
const SERVICE_STILL_APPLYING_TERMINATED = 50;
const SERVICE_STILL_APPLYING_NO_EXIT_CODE = 51;
const SERVICE_COULD_NOT_IMPERSONATE = 58;
const WRITE_ERROR_FILE_COPY = 61;
const WRITE_ERROR_DELETE_FILE = 62;
const WRITE_ERROR_OPEN_PATCH_FILE = 63;
const WRITE_ERROR_PATCH_FILE = 64;
const WRITE_ERROR_APPLY_DIR_PATH = 65;
const WRITE_ERROR_CALLBACK_PATH = 66;
const WRITE_ERROR_FILE_ACCESS_DENIED = 67;
const WRITE_ERROR_DIR_ACCESS_DENIED = 68;
const WRITE_ERROR_DELETE_BACKUP = 69;
const WRITE_ERROR_EXTRACT = 70;
// Error codes 80 through 99 are reserved for UpdateService.jsm and are not
// defined in common/updatererrors.h
const ERR_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 BACKGROUND_TASK_NEEDED_ELEVATION_ERROR = 105;
const WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION = 106;
// Array of write errors to simplify checks for write errors
const WRITE_ERRORS = [
WRITE_ERROR,
WRITE_ERROR_ACCESS_DENIED,
WRITE_ERROR_CALLBACK_APP,
WRITE_ERROR_FILE_COPY,
WRITE_ERROR_DELETE_FILE,
WRITE_ERROR_OPEN_PATCH_FILE,
WRITE_ERROR_PATCH_FILE,
WRITE_ERROR_APPLY_DIR_PATH,
WRITE_ERROR_CALLBACK_PATH,
WRITE_ERROR_FILE_ACCESS_DENIED,
WRITE_ERROR_DIR_ACCESS_DENIED,
WRITE_ERROR_DELETE_BACKUP,
WRITE_ERROR_EXTRACT,
WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION,
];
// Array of write errors to simplify checks for service errors
const SERVICE_ERRORS = [
SERVICE_UPDATER_COULD_NOT_BE_STARTED,
SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS,
SERVICE_UPDATER_SIGN_ERROR,
SERVICE_UPDATER_COMPARE_ERROR,
SERVICE_UPDATER_IDENTITY_ERROR,
SERVICE_STILL_APPLYING_ON_SUCCESS,
SERVICE_STILL_APPLYING_ON_FAILURE,
SERVICE_UPDATER_NOT_FIXED_DRIVE,
SERVICE_COULD_NOT_LOCK_UPDATER,
SERVICE_INSTALLDIR_ERROR,
SERVICE_COULD_NOT_COPY_UPDATER,
SERVICE_STILL_APPLYING_TERMINATED,
SERVICE_STILL_APPLYING_NO_EXIT_CODE,
SERVICE_COULD_NOT_IMPERSONATE,
];
// Custom update error codes
const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
const NETWORK_ERROR_OFFLINE = 111;
// Error codes should be < 1000. Errors above 1000 represent http status codes
const HTTP_ERROR_OFFSET = 1000;
// The is an HRESULT error that may be returned from the BITS interface
// indicating that access was denied. Often, this error code is returned when
// attempting to access a job created by a different user.
const HRESULT_E_ACCESSDENIED = -2147024891;
const DOWNLOAD_CHUNK_SIZE = 300000; // bytes
// The number of consecutive failures when updating using the service before
// setting the app.update.service.enabled preference to false.
const DEFAULT_SERVICE_MAX_ERRORS = 10;
// The number of consecutive socket errors to allow before falling back to
// downloading a different MAR file or failing if already downloading the full.
const DEFAULT_SOCKET_MAX_ERRORS = 10;
// The number of milliseconds to wait before retrying a connection error.
const DEFAULT_SOCKET_RETRYTIMEOUT = 2000;
// Default maximum number of elevation cancelations per update version before
// giving up.
const DEFAULT_CANCELATIONS_OSX_MAX = 3;
// This maps app IDs to their respective notification topic which signals when
// the application's user interface has been displayed.
const APPID_TO_TOPIC = {
// Firefox
"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "sessionstore-windows-restored",
// SeaMonkey
"{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "sessionstore-windows-restored",
// Thunderbird
"{3550f703-e582-4d05-9a08-453d09bdfdc6}": "mail-startup-done",
};
// The interval for the update xml write deferred task.
const XML_SAVER_INTERVAL_MS = 200;
// How long after a patch has downloaded should we wait for language packs to
// update before proceeding anyway.
const LANGPACK_UPDATE_DEFAULT_TIMEOUT = 300000;
// Interval between rechecks for other instances after the initial check finds
// at least one other instance.
const ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
// Wait this long after detecting that another instance is running (having been
// polling that entire time) before giving up and applying the update anyway.
const ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS = 6 * 60 * 60 * 1000; // 6 hours
// The other instance check timeout can be overridden via a pref, but we limit
// that value to this so that the pref can't effectively disable the feature.
const ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS = 2 * 24 * 60 * 60 * 1000; // 2 days
// Object to keep track of the current phase of the update and whether there
// has been a write failure for the phase so only one telemetry ping is made
// for the phase.
var gUpdateFileWriteInfo = { phase: null, failure: false };
var gUpdateMutexHandle = null;
// The permissions of the update directory should be fixed no more than once per
// session
var gUpdateDirPermissionFixAttempted = false;
// This is the file stream used for the log file.
var gLogfileOutputStream;
// This value will be set to true if it appears that BITS is being used by
// another user to download updates. We don't really want two users using BITS
// at once. Computers with many users (ex: a school computer), should not end
// up with dozens of BITS jobs.
var gBITSInUseByAnotherUser = false;
// Tracks whether an update is currently being staged. This is slightly more
// accurate than checking for STATE_APPLYING because there are brief periods of
// time at the beginning and end of staging when that will not be the state.
let gStagingInProgress = 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;
XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() {
return (
Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) ||
Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false)
);
});
XPCOMUtils.defineLazyGetter(
this,
"gLogfileEnabled",
function aus_gLogfileEnabled() {
return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false);
}
);
XPCOMUtils.defineLazyGetter(
this,
"gUpdateBundle",
function aus_gUpdateBundle() {
return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
}
);
/**
* When a plain JS object is passed through xpconnect the other side sees a
* wrapped version of the object instead of the real object. Since these two
* objects are different they act as different keys for Map and WeakMap. However
* xpconnect gives us a way to get the underlying JS object from the wrapper so
* this function returns the JS object regardless of whether passed the JS
* object or its wrapper for use in places where it is unclear which one you
* have.
*/
function unwrap(obj) {
return obj.wrappedJSObject ?? obj;
}
/**
* When an update starts to download (and if the feature is enabled) the add-ons
* manager starts downloading updated language packs for the new application
* version. A promise is used to track whether those updates are complete so the
* front-end is only notified that an application update is ready once the
* language pack updates have been staged.
*
* In order to be able to access that promise from various places in the update
* service they are cached in this map using the nsIUpdate object as a weak
* owner. Note that the key should always be the result of calling the above
* unwrap function on the nsIUpdate to ensure a consistent object is used as the
* key.
*
* When the language packs finish staging the nsIUpdate entriy is removed from
* this map so if the entry is still there then language pack updates are in
* progress.
*/
const LangPackUpdates = new WeakMap();
/**
* When we're polling to see if other running instances of the application have
* exited, there's no need to ever start polling again in parallel. To prevent
* doing that, we keep track of the promise that resolves when polling completes
* and return that if a second simultaneous poll is requested, so that the
* multiple callers end up waiting for the same promise to resolve.
*/
let gOtherInstancePollPromise;
/**
* Query the update sync manager to see if another instance of this same
* installation of this application is currently running, under the context of
* any operating system user (not just the current one).
* This function immediately returns the current, instantaneous status of any
* other instances.
*
* @return true if at least one other instance is running, false if not
*/
function isOtherInstanceRunning(callback) {
const checkEnabled = Services.prefs.getBoolPref(
PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED,
true
);
if (!checkEnabled) {
LOG("isOtherInstanceRunning - disabled by pref, skipping check");
return false;
}
try {
let syncManager = Cc[
"@mozilla.org/updates/update-sync-manager;1"
].getService(Ci.nsIUpdateSyncManager);
return syncManager.isOtherInstanceRunning();
} catch (ex) {
LOG(`isOtherInstanceRunning - sync manager failed with exception: ${ex}`);
return false;
}
}
/**
* Query the update sync manager to see if another instance of this same
* installation of this application is currently running, under the context of
* any operating system user (not just the one running this instance).
* This function polls for the status of other instances continually
* (asynchronously) until either none exist or a timeout expires.
*
* @return a Promise that resolves with false if at any point during polling no
* other instances can be found, or resolves with true if the timeout
* expires when other instances are still running
*/
function waitForOtherInstances() {
// If we're already in the middle of a poll, reuse it rather than start again.
if (gOtherInstancePollPromise) {
return gOtherInstancePollPromise;
}
let timeout = Services.prefs.getIntPref(
PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT,
ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS
);
// Don't allow the pref to set a super high timeout and break this feature.
if (timeout > ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS) {
timeout = ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS;
}
let interval = Services.prefs.getIntPref(
PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_INTERVAL,
ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS
);
// Don't allow an interval longer than the timeout.
interval = Math.min(interval, timeout);
let iterations = 0;
const maxIterations = Math.ceil(timeout / interval);
gOtherInstancePollPromise = new Promise(function(resolve, reject) {
let poll = function() {
iterations++;
if (!isOtherInstanceRunning()) {
LOG("waitForOtherInstances - no other instances found, exiting");
resolve(false);
gOtherInstancePollPromise = undefined;
} else if (iterations >= maxIterations) {
LOG(
"waitForOtherInstances - timeout expired while other instances " +
"are still running"
);
resolve(true);
gOtherInstancePollPromise = undefined;
} else if (iterations + 1 == maxIterations && timeout % interval != 0) {
// In case timeout isn't a multiple of interval, set the next timeout
// for the remainder of the time rather than for the usual interval.
setTimeout(poll, timeout % interval);
} else {
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 = ctypes.open("kernel32.dll");
let CloseHandle = lib.declare(
"CloseHandle",
ctypes.winapi_abi,
ctypes.int32_t /* success */,
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 = ctypes.open("kernel32.dll");
let CreateMutexW = lib.declare(
"CreateMutexW",
ctypes.winapi_abi,
ctypes.void_t.ptr /* return handle */,
ctypes.void_t.ptr /* security attributes */,
ctypes.int32_t /* initial owner */,
ctypes.char16_t.ptr
); /* name */
let handle = CreateMutexW(null, INITIAL_OWN, aName);
let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS;
if (handle && !handle.isNull() && !aAllowExisting && alreadyExists) {
closeHandle(handle);
handle = null;
}
lib.close();
if (handle && handle.isNull()) {
handle = null;
}
return handle;
}
/**
* Windows only function that determines a unique mutex name for the
* installation.
*
* @param aGlobal
* true if the function should return a global mutex. A global mutex is
* valid across different sessions.
* @return Global mutex path
*/
function getPerInstallationMutexName(aGlobal = true) {
if (AppConstants.platform != "win") {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
Ci.nsICryptoHash
);
hasher.init(hasher.SHA1);
let exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile);
let converter = Cc[
"@mozilla.org/intl/scriptableunicodeconverter"
].createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
var data = converter.convertToByteArray(exeFile.path.toLowerCase());
hasher.update(data, data.length);
return (
(aGlobal ? "Global\\" : "") + "MozillaUpdateMutex-" + hasher.finish(true)
);
}
/**
* Whether or not the current instance has the update mutex. The update mutex
* gives protection against 2 applications from the same installation updating:
* 1) Running multiple profiles from the same installation path
* 2) Two applications running in 2 different user sessions from the same path
*
* @return true if this instance holds the update mutex
*/
function hasUpdateMutex() {
if (AppConstants.platform != "win") {
return true;
}
if (!gUpdateMutexHandle) {
gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false);
}
return !!gUpdateMutexHandle;
}
/**
* Determines whether or not all descendants of a directory are writeable.
* Note: Does not check the root directory itself for writeability.
*
* @return true if all descendants are writeable, false otherwise
*/
function areDirectoryEntriesWriteable(aDir) {
let items = aDir.directoryEntries;
while (items.hasMoreElements()) {
let item = items.nextFile;
if (!item.isWritable()) {
LOG("areDirectoryEntriesWriteable - unable to write to " + item.path);
return false;
}
if (item.isDirectory() && !areDirectoryEntriesWriteable(item)) {
return false;
}
}
return true;
}
/**
* OSX only function to determine if the user requires elevation to be able to
* write to the application bundle.
*
* @return true if elevation is required, false otherwise
*/
function getElevationRequired() {
if (AppConstants.platform != "macosx") {
return false;
}
try {
// Recursively check that the application bundle (and its descendants) can
// be written to.
LOG(
"getElevationRequired - recursively testing write access on " +
getInstallDirRoot().path
);
if (
!getInstallDirRoot().isWritable() ||
!areDirectoryEntriesWriteable(getInstallDirRoot())
) {
LOG(
"getElevationRequired - unable to write to application bundle, " +
"elevation required"
);
return true;
}
} catch (ex) {
LOG(
"getElevationRequired - unable to write to application bundle, " +
"elevation required. Exception: " +
ex
);
return true;
}
LOG(
"getElevationRequired - able to write to application bundle, elevation " +
"not required"
);
return false;
}
/**
* A promise that resolves when language packs are downloading or if no language
* packs are being downloaded.
*/
function promiseLangPacksUpdated(update) {
let promise = LangPackUpdates.get(unwrap(update));
if (promise) {
LOG(
"promiseLangPacksUpdated - waiting for language pack updates to stage."
);
return promise;
}
// In case callers rely on a promise just return an already resolved promise.
return Promise.resolve();
}
/**
* Determines whether or not an update can be applied. This is always true on
* Windows when the service is used. On Mac OS X and Linux, if the user has
* write access to the update directory this will return true because on OSX we
* offer users the option to perform an elevated update when necessary and on
* Linux the update directory is located in the application directory.
*
* @return true if an update can be applied, false otherwise
*/
function getCanApplyUpdates() {
try {
// Check if it is possible to write to the update directory so clients won't
// repeatedly try to apply an update without the ability to complete the
// update process which requires write access to the update directory.
let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
LOG("getCanApplyUpdates - testing write access " + updateTestFile.path);
testWriteAccess(updateTestFile, false);
} catch (e) {
LOG(
"getCanApplyUpdates - unable to apply updates without write " +
"access to the update directory. Exception: " +
e
);
// Attempt to fix the update directory permissions. If successful the next
// time this function is called the write access check to the update
// directory will succeed.
fixUpdateDirectoryPermissions();
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;
const bts =
"@mozilla.org/backgroundtasks;1" in Cc &&
Cc["@mozilla.org/backgroundtasks;1"].getService(Ci.nsIBackgroundTasks);
if (bts && bts.isBackgroundTaskMode) {
LOG(
"getCanApplyUpdates - in background task mode, assuming user can't elevate"
);
userCanElevate = false;
}
if (!userCanElevate) {
// if we're unable to create the test file this will throw an exception.
let appDirTestFile = getAppBaseDir();
appDirTestFile.append(FILE_UPDATE_TEST);
LOG("getCanApplyUpdates - testing write access " + appDirTestFile.path);
if (appDirTestFile.exists()) {
appDirTestFile.remove(false);
}
appDirTestFile.create(
Ci.nsIFile.NORMAL_FILE_TYPE,
FileUtils.PERMS_FILE
);
appDirTestFile.remove(false);
}
}
} catch (e) {
LOG("getCanApplyUpdates - unable to apply updates. Exception: " + e);
// No write access to the installation directory
return false;
}
LOG("getCanApplyUpdates - able to apply updates");
return true;
}
/**
* Whether or not the application can stage an update for the current session.
* These checks are only performed once per session due to using a lazy getter.
*
* @return true if updates can be staged for this session.
*/
XPCOMUtils.defineLazyGetter(
this,
"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 gCanStageUpdatesSession;
}
/*
* Whether or not the application can use BITS to download updates.
*
* @param {boolean} [transient] Whether transient factors such as the update
* mutex should be considered.
* @return A string with one of these values:
* CanUseBits
* NoBits_NotWindows
* NoBits_FeatureOff
* NoBits_Pref
* NoBits_Proxy
* NoBits_OtherUser
* These strings are directly compatible with the categories for
* UPDATE_CAN_USE_BITS_EXTERNAL and UPDATE_CAN_USE_BITS_NOTIFY telemetry
* probes. If this function is made to return other values, they should
* also be added to the labels lists for those probes in Histograms.json
*/
function getCanUseBits(transient = true) {
if (AppConstants.platform != "win") {
LOG("getCanUseBits - Not using BITS because this is not Windows");
return "NoBits_NotWindows";
}
if (!AppConstants.MOZ_BITS_DOWNLOAD) {
LOG("getCanUseBits - Not using BITS because the feature is disabled");
return "NoBits_FeatureOff";
}
if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED, true)) {
LOG("getCanUseBits - Not using BITS. Disabled by pref.");
return "NoBits_Pref";
}
// Firefox support for passing proxies to BITS is still rudimentary.
// For now, disable BITS support on configurations that are not using the
// standard system proxy.
let defaultProxy = Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM;
if (
Services.prefs.getIntPref(PREF_NETWORK_PROXY_TYPE, defaultProxy) !=
defaultProxy &&
!Cu.isInAutomation
) {
LOG("getCanUseBits - Not using BITS because of proxy usage");
return "NoBits_Proxy";
}
if (transient && gBITSInUseByAnotherUser) {
LOG("getCanUseBits - Not using BITS. Already in use by another user");
return "NoBits_OtherUser";
}
LOG("getCanUseBits - BITS can be used to download updates");
return "CanUseBits";
}
/**
* Logs a string to the error console. If enabled, also logs to the update
* messages file.
* @param string
* The string to write to the error console.
*/
function LOG(string) {
if (gLogEnabled) {
dump("*** AUS:SVC " + string + "\n");
if (!Cu.isInAutomation) {
Services.console.logStringMessage("AUS:SVC " + string);
}
if (gLogfileEnabled) {
if (!gLogfileOutputStream) {
let logfile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile);
logfile.append(FILE_UPDATE_MESSAGES);
gLogfileOutputStream = FileUtils.openAtomicFileOutputStream(logfile);
}
try {
let encoded = new TextEncoder().encode(string + "\n");
gLogfileOutputStream.write(encoded, encoded.length);
gLogfileOutputStream.flush();
} catch (e) {
dump("*** AUS:SVC Unable to write to messages file: " + e + "\n");
Services.console.logStringMessage(
"AUS:SVC Unable to write to messages file: " + e
);
}
}
}
}
/**
* Gets the specified directory at the specified hierarchy under the
* update root directory and creates it if it doesn't exist.
* @param pathArray
* An array of path components to locate beneath the directory
* specified by |key|
* @return nsIFile object for the location specified.
*/
function getUpdateDirCreate(pathArray) {
if (Cu.isInAutomation) {
// This allows tests to use an alternate updates directory so they can test
// startup behavior.
const MAGIC_TEST_ROOT_PREFIX = "<test-root>";
const PREF_TEST_ROOT = "mochitest.testRoot";
let alternatePath = Services.prefs.getCharPref(
PREF_APP_UPDATE_ALTUPDATEDIRPATH,
null
);
if (alternatePath && alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
let testRoot = Services.prefs.getCharPref(PREF_TEST_ROOT);
let relativePath = alternatePath.substring(MAGIC_TEST_ROOT_PREFIX.length);
if (AppConstants.platform == "win") {
relativePath = relativePath.replace(/\//g, "\\");
}
alternatePath = testRoot + relativePath;
let updateDir = Cc["@mozilla.org/file/local;1"].createInstance(
Ci.nsIFile
);
updateDir.initWithPath(alternatePath);
for (let i = 0; i < pathArray.length; ++i) {
updateDir.append(pathArray[i]);
}
return updateDir;
}
}
return FileUtils.getDir(KEY_UPDROOT, pathArray, true);
}
/**
* Gets the application base directory.
*
* @return nsIFile object for the application base directory.
*/
function getAppBaseDir() {
return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent;
}
/**
* Gets the root of the installation directory which is the application
* bundle directory on Mac OS X and the location of the application binary
* on all other platforms.
*
* @return nsIFile object for the directory
*/
function getInstallDirRoot() {
let dir = getAppBaseDir();
if (AppConstants.platform == "macosx") {
// On Mac, we store the Updated.app directory inside the bundle directory.
dir = dir.parent.parent;
}
return dir;
}
/**
* Gets the file at the specified hierarchy under the update root directory.
* @param pathArray
* An array of path components to locate beneath the directory
* specified by |key|. The last item in this array must be the
* leaf name of a file.
* @return nsIFile object for the file specified. The file is NOT created
* if it does not exist, however all required directories along
* the way are.
*/
function getUpdateFile(pathArray) {
let file = getUpdateDirCreate(pathArray.slice(0, -1));
file.append(pathArray[pathArray.length - 1]);
return file;
}
/**
* 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) {
let reason;
try {
reason = gUpdateBundle.GetStringFromName("check_error-" + code);
LOG(
"getStatusTextFromCode - transfer error: " + reason + ", code: " + code
);
} catch (e) {
// Use the default reason
reason = gUpdateBundle.GetStringFromName("check_error-" + defaultCode);
LOG(
"getStatusTextFromCode - transfer error: " +
reason +
", default code: " +
defaultCode
);
}
return reason;
}
/**
* Get the Ready Update directory. This is the directory that an update
* should reside in after download has completed but before it has been
* installed and cleaned up.
* @return The ready updates directory, as a nsIFile object
*/
function getReadyUpdateDir() {
return getUpdateDirCreate([DIR_UPDATES, "0"]);
}
/**
* Get the Downloading Update directory. This is the directory that an update
* should reside in during download. Once download is completed, it will be
* moved to the Ready Update directory.
* @return The downloading update directory, as a nsIFile object
*/
function getDownloadingUpdateDir() {
return getUpdateDirCreate([DIR_UPDATES, "downloading"]);
}
/**
* Reads the update state from the update.status file in the specified
* directory.
* @param dir
* The dir to look for an update.status file in
* @return The status value of the update.
*/
function readStatusFile(dir) {
let statusFile = dir.clone();
statusFile.append(FILE_UPDATE_STATUS);
let status = readStringFromFile(statusFile) || STATE_NONE;
LOG("readStatusFile - status: " + status + ", path: " + statusFile.path);
return status;
}
/**
* Reads the binary transparency result file from the given directory.
* Removes the file if it is present (so don't call this twice and expect a
* result the second time).
* @param dir
* The dir to look for an update.bt file in
* @return A error code from verifying binary transparency information or null
* if the file was not present (indicating there was no error).
*/
function readBinaryTransparencyResult(dir) {
let binaryTransparencyResultFile = dir.clone();
binaryTransparencyResultFile.append(FILE_BT_RESULT);
let result = readStringFromFile(binaryTransparencyResultFile);
LOG(
"readBinaryTransparencyResult - result: " +
result +
", path: " +
binaryTransparencyResultFile.path
);
// If result is non-null, the file exists. We should remove it to avoid
// double-reporting this result.
if (result) {
binaryTransparencyResultFile.remove(false);
}
return result;
}
/**
* Writes the current update operation/state to a file in the patch
* directory, indicating to the patching system that operations need
* to be performed.
* @param dir
* The patch directory where the update.status file should be
* written.
* @param state
* The state value to write.
*/
function writeStatusFile(dir, state) {
let statusFile = dir.clone();
statusFile.append(FILE_UPDATE_STATUS);
let success = writeStringToFile(statusFile, state);
if (!success) {
handleCriticalWriteFailure(statusFile.path);
}
}
/**
* 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);
let success = writeStringToFile(versionFile, version);
if (!success) {
handleCriticalWriteFailure(versionFile.path);
}
}
/**
* Determines if the service should be used to attempt an update
* or not.
*
* @return true if the service should be used for updates.
*/
function shouldUseService() {
// This function will return true if the mantenance service should be used if
// all of the following conditions are met:
// 1) This build was done with the maintenance service enabled
// 2) The maintenance service is installed
// 3) The pref for using the service is enabled
if (
!AppConstants.MOZ_MAINTENANCE_SERVICE ||
!isServiceInstalled() ||
!Services.prefs.getBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false)
) {
LOG("shouldUseService - returning false");
return false;
}
LOG("shouldUseService - returning true");
return true;
}
/**
* Determines if the service is is installed.
*
* @return true if the service is installed.
*/
function isServiceInstalled() {
if (!AppConstants.MOZ_MAINTENANCE_SERVICE || AppConstants.platform != "win") {
LOG("isServiceInstalled - returning false");
return false;
}
let installed = 0;
try {
let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
Ci.nsIWindowsRegKey
);
wrk.open(
wrk.ROOT_KEY_LOCAL_MACHINE,
"SOFTWARE\\Mozilla\\MaintenanceService",
wrk.ACCESS_READ | wrk.WOW64_64
);
installed = wrk.readIntValue("Installed");
wrk.close();
} catch (e) {}
installed = installed == 1; // convert to bool
LOG("isServiceInstalled - returning " + installed);
return installed;
}
/**
* Gets the appropriate pending update state. Returns STATE_PENDING_SERVICE,
* STATE_PENDING_ELEVATE, or STATE_PENDING.
*/
function getBestPendingState() {
if (shouldUseService()) {
return STATE_PENDING_SERVICE;
} else if (getElevationRequired()) {
return STATE_PENDING_ELEVATE;
}
return STATE_PENDING;
}
/**
* Removes the contents of the ready update directory and rotates the update
* logs when present. If the update.log exists in the patch directory this will
* move the last-update.log if it exists to backup-update.log in the parent
* directory of the patch directory and then move the update.log in the patch
* directory to last-update.log in the parent directory of the patch directory.
*
* @param aRemovePatchFiles (optional, defaults to true)
* When true the update's patch directory contents are removed.
*/
function cleanUpReadyUpdateDir(aRemovePatchFiles = true) {
let updateDir;
try {
updateDir = getReadyUpdateDir();
} catch (e) {
LOG(
"cleanUpReadyUpdateDir - unable to get the updates patch directory. " +
"Exception: " +
e
);
return;
}
// Preserve the last update log file for debugging purposes.
let updateLogFile = updateDir.clone();
updateLogFile.append(FILE_UPDATE_LOG);
if (updateLogFile.exists()) {
let dir = updateDir.parent;
let logFile = dir.clone();
logFile.append(FILE_LAST_UPDATE_LOG);
if (logFile.exists()) {
try {
logFile.moveTo(dir, FILE_BACKUP_UPDATE_LOG);
} catch (e) {
LOG(
"cleanUpReadyUpdateDir - failed to rename file " +
logFile.path +
" to " +
FILE_BACKUP_UPDATE_LOG
);
}
}
try {
updateLogFile.moveTo(dir, FILE_LAST_UPDATE_LOG);
} catch (e) {
LOG(
"cleanUpReadyUpdateDir - failed to rename file " +
updateLogFile.path +
" to " +
FILE_LAST_UPDATE_LOG
);
}
}
if (aRemovePatchFiles) {
let dirEntries = updateDir.directoryEntries;
while (dirEntries.hasMoreElements()) {
let file = dirEntries.nextFile;
// Now, recursively remove this file. The recursive removal is needed for
// Mac OSX because this directory will contain a copy of updater.app,
// which is itself a directory and the MozUpdater directory on platforms
// other than Windows.
try {
file.remove(true);
} catch (e) {
LOG("cleanUpReadyUpdateDir - failed to remove file " + file.path);
}
}
}
}
/**
* Removes the contents of the update download directory.
*
*/
function cleanUpDownloadingUpdateDir() {
let updateDir;
try {
updateDir = getDownloadingUpdateDir();
} catch (e) {
LOG(
"cleanUpDownloadUpdatesDir - unable to get the updates patch " +
"directory. Exception: " +
e
);
return;
}
let dirEntries = updateDir.directoryEntries;
while (dirEntries.hasMoreElements()) {
let file = dirEntries.nextFile;
// Now, recursively remove this file.
try {
file.remove(true);
} catch (e) {
LOG("cleanUpDownloadUpdatesDir - failed to remove file " + file.path);
}
}
}
/**
* Clean up the updates list and the directory that contains the update that
* is ready to be installed.
*
* Note - This function causes a state transition to either STATE_DOWNLOADING
* or STATE_NONE, depending on whether an update download is in progress.
*/
function cleanupReadyUpdate() {
// Move the update from the Active Update list into the Past Updates list.
var um = Cc["@mozilla.org/updates/update-manager;1"].getService(
Ci.nsIUpdateManager
);
if (um.readyUpdate) {
um.addUpdateToHistory(um.readyUpdate);
um.readyUpdate = null;
}
um.saveUpdates();
let readyUpdateDir = getReadyUpdateDir();
let shouldSetDownloadingStatus =
um.downloadingUpdate || readStatusFile(readyUpdateDir) == STATE_DOWNLOADING;
// Now trash the ready update directory, since we're done with it
cleanUpReadyUpdateDir();
// We need to handle two similar cases here.
// The first is where we clean up the ready updates directory while we are in
// the downloading state. In this case, we remove the update.status file that
// says we are downloading, even though we should remain in that state.
// The second case is when we clean up a ready update, but there is also a
// downloading update (in which case the update status file's state will
// reflect the state of the ready update, not the downloading one). In that
// case, instead of reverting to STATE_NONE (which is what we do by removing
// the status file), we should set our state to downloading.
if (shouldSetDownloadingStatus) {
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.
var um = Cc["@mozilla.org/updates/update-manager;1"].getService(
Ci.nsIUpdateManager
);
if (um.downloadingUpdate) {
um.addUpdateToHistory(um.downloadingUpdate);
um.downloadingUpdate = null;
}
um.saveUpdates();
// Now trash the update download directory, since we're done with it
cleanUpDownloadingUpdateDir();
// If the update status file says we are downloading, we should remove that
// too, since we aren't doing that anymore.
let readyUpdateDir = getReadyUpdateDir();
let status = readStatusFile(readyUpdateDir);
if (status == STATE_DOWNLOADING) {
let statusFile = readyUpdateDir.clone();
statusFile.append(FILE_UPDATE_STATUS);
statusFile.remove();
}
}
/**
* Clean up updates list, the ready update directory, and the downloading update
* directory.
*
* This is more efficient than calling
* cleanupReadyUpdate();
* cleanupDownloadingUpdate();
* because those need some special handling of the update status file to make
* sure that, for example, cleaning up a ready update doesn't make us forget
* that we are downloading an update. When we cleanup both updates, we don't
* need to worry about things like that.
*
* Note - This function causes a state transition to STATE_NONE.
*/
function cleanupActiveUpdates() {
// Move the update from the Active Update list into the Past Updates list.
var um = Cc["@mozilla.org/updates/update-manager;1"].getService(
Ci.nsIUpdateManager
);
if (um.readyUpdate) {
um.addUpdateToHistory(um.readyUpdate);
um.readyUpdate = null;
}
if (um.downloadingUpdate) {
um.addUpdateToHistory(um.downloadingUpdate);
um.downloadingUpdate = null;
}
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) {
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);
}
function handleUpdateFailure(update, errorCode) {
update.errorCode = parseInt(errorCode);
if (WRITE_ERRORS.includes(update.errorCode)) {
writeStatusFile(getReadyUpdateDir(), (update.state = STATE_PENDING));
return true;
}
if (update.errorCode == BACKGROUND_TASK_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 BACKGROUND_TASK_NEEDED_ELEVATION_ERROR, " +
"returning to " +
bestState
);
writeStatusFile(getReadyUpdateDir(), (update.state = bestState));
// 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) {
cleanupReadyUpdate();
} else {
writeStatusFile(
getReadyUpdateDir(),
(update.state = STATE_PENDING_ELEVATE)
);
}
update.statusText = gUpdateBundle.GetStringFromName("elevationFailure");
} else {
writeStatusFile(getReadyUpdateDir(), (update.state = 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);
}
writeStatusFile(getReadyUpdateDir(), (update.state = 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.