toolkit/modules/UpdateUtils.jsm
author Robert Strong <robert.bugzilla@gmail.com>
Thu, 16 May 2019 21:45:40 +0000
changeset 474229 a012ed03628d6192c71ab1561e8dc978c81e9136
parent 455626 6b56696d713a7f7858f16235e37baa8307e73b49
permissions -rw-r--r--
Bug 1552321 - Use logStringMessage instead of reportError for the 'Unable to read app update configuration file' log message. r=bytesized There have been several people that thought app update was broken because this message was reported as an error so just use logStringMessage Differential Revision: https://phabricator.services.mozilla.com/D31524

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var EXPORTED_SYMBOLS = ["UpdateUtils"];

const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const {ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); /* globals fetch */

ChromeUtils.defineModuleGetter(this, "WindowsRegistry",
                               "resource://gre/modules/WindowsRegistry.jsm");

// The file that stores Application Update configuration settings. The file is
// located in the update directory which makes it a common setting across all
// application profiles and allows the Background Update Agent to read it.
const FILE_UPDATE_CONFIG_JSON             = "update-config.json";
const FILE_UPDATE_LOCALE                  = "update.locale";
const PREF_APP_DISTRIBUTION               = "distribution.id";
const PREF_APP_DISTRIBUTION_VERSION       = "distribution.version";
// Do not use the PREF_APP_UPDATE_AUTO preference directly!
// Call getAppUpdateAutoEnabled or setAppUpdateAutoEnabled instead.
const PREF_APP_UPDATE_AUTO                = "app.update.auto";
const PREF_APP_UPDATE_AUTO_MIGRATED       = "app.update.auto.migrated";
// The setting name in the FILE_UPDATE_CONFIG_JSON file for whether the
// Application Update Service automatically downloads and installs updates.
const CONFIG_APP_UPDATE_AUTO               = "app.update.auto";
// The default value for the CONFIG_APP_UPDATE_AUTO setting and the
// PREF_APP_UPDATE_AUTO preference.
const DEFAULT_APP_UPDATE_AUTO              = true;

var UpdateUtils = {
  _locale: undefined,

  /**
   * Read the update channel from defaults only.  We do this to ensure that
   * the channel is tightly coupled with the application and does not apply
   * to other instances of the application that may use the same profile.
   *
   * @param [optional] aIncludePartners
   *        Whether or not to include the partner bits. Default: true.
   */
  getUpdateChannel(aIncludePartners = true) {
    let defaults = Services.prefs.getDefaultBranch(null);
    let channel = defaults.getCharPref("app.update.channel",
                                       AppConstants.MOZ_UPDATE_CHANNEL);

    if (aIncludePartners) {
      try {
        let partners = Services.prefs.getChildList("app.partner.").sort();
        if (partners.length) {
          channel += "-cck";
          partners.forEach(function(prefName) {
            channel += "-" + Services.prefs.getCharPref(prefName);
          });
        }
      } catch (e) {
        Cu.reportError(e);
      }
    }

    return channel;
  },

  get UpdateChannel() {
    return this.getUpdateChannel();
  },

  /**
   * Formats a URL by replacing %...% values with OS, build and locale specific
   * values.
   *
   * @param  url
   *         The URL to format.
   * @return The formatted URL.
   */
  async formatUpdateURL(url) {
    const locale = await this.getLocale();

    return url.replace(/%(\w+)%/g, (match, name) => {
      switch (name) {
        case "PRODUCT":
          return Services.appinfo.name;
        case "VERSION":
          return Services.appinfo.version;
        case "BUILD_ID":
          return Services.appinfo.appBuildID;
        case "BUILD_TARGET":
          return Services.appinfo.OS + "_" + this.ABI;
        case "OS_VERSION":
          return this.OSVersion;
        case "LOCALE":
          return locale;
        case "CHANNEL":
          return this.UpdateChannel;
        case "PLATFORM_VERSION":
          return Services.appinfo.platformVersion;
        case "SYSTEM_CAPABILITIES":
          return getSystemCapabilities();
        case "DISTRIBUTION":
          return getDistributionPrefValue(PREF_APP_DISTRIBUTION);
        case "DISTRIBUTION_VERSION":
          return getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION);
      }
      return match;
    }).replace(/\+/g, "%2B");
  },

  /**
   * Gets the locale from the update.locale file for replacing %LOCALE% in the
   * update url. The update.locale file can be located in the application
   * directory or the GRE directory with preference given to it being located in
   * the application directory.
   */
  async getLocale() {
    if (this._locale !== undefined) {
      return this._locale;
    }

    for (let res of ["app", "gre"]) {
      const url = "resource://" + res + "/" + FILE_UPDATE_LOCALE;
      let data;
      try {
        data = await fetch(url);
      } catch (e) {
        continue;
      }
      const locale = await data.text();
      if (locale) {
        return this._locale = locale.trim();
      }
    }

    Cu.reportError(FILE_UPDATE_LOCALE + " file doesn't exist in either the " +
                   "application or GRE directories");

    return this._locale = null;
  },

  /**
   * Determines whether or not the Application Update Service automatically
   * downloads and installs updates. This corresponds to whether or not the user
   * has selected "Automatically install updates" in about:preferences.
   *
   * On Windows, this setting is shared across all profiles for the installation
   * and is read asynchrnously from the file. On other operating systems, this
   * setting is stored in a pref and is thus a per-profile setting.
   *
   * @return A Promise that resolves with a boolean.
   */
  getAppUpdateAutoEnabled() {
    if (AppConstants.platform != "win") {
      // On platforms other than Windows the setting is stored in a preference.
      let prefValue = Services.prefs.getBoolPref(PREF_APP_UPDATE_AUTO,
                                                 DEFAULT_APP_UPDATE_AUTO);
      return Promise.resolve(prefValue);
    }
    // Justification for the empty catch statement below:
    // All promises returned by (get|set)AutoUpdateIsEnabled are part of a
    // single promise chain in order to serialize disk operations. We don't want
    // the entire promise chain to reject when one operation fails.
    //
    // There is only one situation when a promise in this chain should ever
    // reject, which is when writing fails and the error is logged and
    // re-thrown. All other possible exceptions are wrapped in try blocks, which
    // also log any exception that may occur.
    let readPromise = updateAutoIOPromise.catch(() => {}).then(async () => {
      try {
        let configValue = await readUpdateAutoConfig();
        // If we read a value out of this file, don't later perform migration.
        // If the file is deleted, we don't want some stale pref getting
        // written to it just because a different profile performed migration.
        Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO_MIGRATED, true);
        return configValue;
      } catch (e) {
        // Not being able to read from the app update configuration file is not
        // a serious issue so use logStringMessage to avoid concern from users.
        Services.console.logStringMessage(
          "UpdateUtils.getAppUpdateAutoEnabled - Unable to read app update " +
          "configuration file. Exception: " + e);
        let valueMigrated = Services.prefs.getBoolPref(
                              PREF_APP_UPDATE_AUTO_MIGRATED,
                              false);
        if (!valueMigrated) {
          Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO_MIGRATED, true);
          let prefValue = Services.prefs.getBoolPref(PREF_APP_UPDATE_AUTO,
                                                     DEFAULT_APP_UPDATE_AUTO);
          try {
            let writtenValue = await writeUpdateAutoConfig(prefValue);
            Services.prefs.clearUserPref(PREF_APP_UPDATE_AUTO);
            return writtenValue;
          } catch (e) {
            Cu.reportError("UpdateUtils.getAppUpdateAutoEnabled - Migration " +
                           "failed. Exception: " + e);
          }
        }
      }
      // Fallthrough for if the value could not be read or migrated.
      return DEFAULT_APP_UPDATE_AUTO;
    }).then(maybeUpdateAutoConfigChanged.bind(this));
    updateAutoIOPromise = readPromise;
    return readPromise;
  },

  /**
   * Toggles whether the Update Service automatically downloads and installs
   * updates. This effectively selects between the "Automatically install
   * updates" and "Check for updates but let you choose to install them" options
   * in about:preferences.
   *
   * On Windows, this setting is shared across all profiles for the installation
   * and is written asynchrnously to the file. On other operating systems, this
   * setting is stored in a pref and is thus a per-profile setting.
   *
   * @param  enabled If set to true, automatic download and installation of
   *                 updates will be enabled. If set to false, this will be
   *                 disabled.
   * @return A Promise that, once the setting has been saved, resolves with the
   *         boolean value that was saved. If the setting could not be
   *         successfully saved, the Promise will reject.
   *         On Windows, where this setting is stored in a file, this Promise
   *         may reject with an I/O error.
   *         On other operating systems, this promise should not reject as
   *         this operation simply sets a pref.
   */
  setAppUpdateAutoEnabled(enabledValue) {
    if (AppConstants.platform != "win") {
      // Only in Windows do we store the update config in the update directory
      let prefValue = !!enabledValue;
      Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO, prefValue);
      maybeUpdateAutoConfigChanged(prefValue);
      return Promise.resolve(prefValue);
    }
    // Justification for the empty catch statement below:
    // All promises returned by (get|set)AutoUpdateIsEnabled are part of a
    // single promise chain in order to serialize disk operations. We don't want
    // the entire promise chain to reject when one operation fails.
    //
    // There is only one situation when a promise in this chain should ever
    // reject, which is when writing fails and the error is logged and
    // re-thrown. All other possible exceptions are wrapped in try blocks, which
    // also log any exception that may occur.
    let writePromise = updateAutoIOPromise.catch(() => {}).then(async () => {
      try {
        return await writeUpdateAutoConfig(enabledValue);
      } catch (e) {
        Cu.reportError("UpdateUtils.setAppUpdateAutoEnabled - App update " +
                       "configuration file write failed. Exception: " + e);
        // Rethrow the error so the caller knows that writing the value in the
        // app update config file failed.
        throw e;
      }
    }).then(maybeUpdateAutoConfigChanged.bind(this));
    updateAutoIOPromise = writePromise;
    return writePromise;
  },
};

// Used for serializing reads and writes of the app update json config file so
// the writes don't happen out of order and the last write is the one that
// the sets the value.
var updateAutoIOPromise = Promise.resolve();
var updateAutoSettingCachedVal = null;

async function readUpdateAutoConfig() {
  let configFile = FileUtils.getDir("UpdRootD", [], true);
  configFile.append(FILE_UPDATE_CONFIG_JSON);
  let binaryData = await OS.File.read(configFile.path);
  let jsonData = new TextDecoder().decode(binaryData);
  let configData = JSON.parse(jsonData);
  return !!configData[CONFIG_APP_UPDATE_AUTO];
}

async function writeUpdateAutoConfig(enabledValue) {
  let enabledBoolValue = !!enabledValue;
  let configFile = FileUtils.getDir("UpdRootD", [], true);
  configFile.append(FILE_UPDATE_CONFIG_JSON);
  let configObject = {[CONFIG_APP_UPDATE_AUTO]: enabledBoolValue};
  await OS.File.writeAtomic(configFile.path, JSON.stringify(configObject));
  return enabledBoolValue;
}

// Notifies observers if the value of app.update.auto has changed and returns
// the value for app.update.auto.
function maybeUpdateAutoConfigChanged(newValue) {
  // Don't notify on the first read when updateAutoSettingCachedVal is null.
  if (updateAutoSettingCachedVal !== null &&
      newValue != updateAutoSettingCachedVal) {
    updateAutoSettingCachedVal = newValue;
    Services.obs.notifyObservers(null, "auto-update-config-change",
                                 newValue.toString());
  }
  return newValue;
}

/* Get the distribution pref values, from defaults only */
function getDistributionPrefValue(aPrefName) {
  return Services.prefs.getDefaultBranch(null).getCharPref(aPrefName, "default");
}

function getSystemCapabilities() {
  return "ISET:" + gInstructionSet + ",MEM:" + getMemoryMB();
}

/**
 * Gets the RAM size in megabytes. This will round the value because sysinfo
 * doesn't always provide RAM in multiples of 1024.
 */
function getMemoryMB() {
  let memoryMB = "unknown";
  try {
    memoryMB = Services.sysinfo.getProperty("memsize");
    if (memoryMB) {
      memoryMB = Math.round(memoryMB / 1024 / 1024);
    }
  } catch (e) {
    Cu.reportError("Error getting system info memsize property. " +
                   "Exception: " + e);
  }
  return memoryMB;
}

/**
 * Gets the supported CPU instruction set.
 */
XPCOMUtils.defineLazyGetter(this, "gInstructionSet", function aus_gIS() {
  const CPU_EXTENSIONS = ["hasSSE4_2", "hasSSE4_1", "hasSSE4A", "hasSSSE3",
                          "hasSSE3", "hasSSE2", "hasSSE", "hasMMX",
                          "hasNEON", "hasARMv7", "hasARMv6"];
  for (let ext of CPU_EXTENSIONS) {
    if (Services.sysinfo.getProperty(ext)) {
      return ext.substring(3);
    }
  }

  return "unknown";
});

/* Windows only getter that returns the processor architecture. */
XPCOMUtils.defineLazyGetter(this, "gWinCPUArch", function aus_gWinCPUArch() {
  // Get processor architecture
  let arch = "unknown";

  const WORD = ctypes.uint16_t;
  const DWORD = ctypes.uint32_t;

  // This structure is described at:
  // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx
  const SYSTEM_INFO = new ctypes.StructType("SYSTEM_INFO",
      [
      {wProcessorArchitecture: WORD},
      {wReserved: WORD},
      {dwPageSize: DWORD},
      {lpMinimumApplicationAddress: ctypes.voidptr_t},
      {lpMaximumApplicationAddress: ctypes.voidptr_t},
      {dwActiveProcessorMask: DWORD.ptr},
      {dwNumberOfProcessors: DWORD},
      {dwProcessorType: DWORD},
      {dwAllocationGranularity: DWORD},
      {wProcessorLevel: WORD},
      {wProcessorRevision: WORD},
      ]);

  let kernel32 = false;
  try {
    kernel32 = ctypes.open("Kernel32");
  } catch (e) {
    Cu.reportError("Unable to open kernel32! Exception: " + e);
  }

  if (kernel32) {
    try {
      let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo",
                                                 ctypes.winapi_abi,
                                                 ctypes.void_t,
                                                 SYSTEM_INFO.ptr);
      let winSystemInfo = SYSTEM_INFO();
      // Default to unknown
      winSystemInfo.wProcessorArchitecture = 0xffff;

      GetNativeSystemInfo(winSystemInfo.address());
      switch (winSystemInfo.wProcessorArchitecture) {
        case 12:
          arch = "aarch64";
          break;
        case 9:
          arch = "x64";
          break;
        case 6:
          arch = "IA64";
          break;
        case 0:
          arch = "x86";
          break;
      }
    } catch (e) {
      Cu.reportError("Error getting processor architecture. " +
                     "Exception: " + e);
    } finally {
      kernel32.close();
    }
  }

  return arch;
});

XPCOMUtils.defineLazyGetter(UpdateUtils, "ABI", function() {
  let abi = null;
  try {
    abi = Services.appinfo.XPCOMABI;
  } catch (e) {
    Cu.reportError("XPCOM ABI unknown");
  }

  if (AppConstants.platform == "win") {
    // Windows build should report the CPU architecture that it's running on.
    abi += "-" + gWinCPUArch;
  }

  if (AppConstants.ASAN) {
    // Allow ASan builds to receive their own updates
    abi += "-asan";
  }

  return abi;
});

XPCOMUtils.defineLazyGetter(UpdateUtils, "OSVersion", function() {
  let osVersion;
  try {
    osVersion = Services.sysinfo.getProperty("name") + " " +
                Services.sysinfo.getProperty("version");
  } catch (e) {
    Cu.reportError("OS Version unknown.");
  }

  if (osVersion) {
    if (AppConstants.platform == "win") {
      const BYTE = ctypes.uint8_t;
      const WORD = ctypes.uint16_t;
      const DWORD = ctypes.uint32_t;
      const WCHAR = ctypes.char16_t;
      const BOOL = ctypes.int;

      // This structure is described at:
      // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
      const SZCSDVERSIONLENGTH = 128;
      const OSVERSIONINFOEXW = new ctypes.StructType("OSVERSIONINFOEXW",
          [
          {dwOSVersionInfoSize: DWORD},
          {dwMajorVersion: DWORD},
          {dwMinorVersion: DWORD},
          {dwBuildNumber: DWORD},
          {dwPlatformId: DWORD},
          {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
          {wServicePackMajor: WORD},
          {wServicePackMinor: WORD},
          {wSuiteMask: WORD},
          {wProductType: BYTE},
          {wReserved: BYTE},
          ]);

      let kernel32 = false;
      try {
        kernel32 = ctypes.open("Kernel32");
      } catch (e) {
        Cu.reportError("Unable to open kernel32! " + e);
        osVersion += ".unknown (unknown)";
      }

      if (kernel32) {
        try {
          // Get Service pack info
          try {
            let GetVersionEx = kernel32.declare("GetVersionExW",
                                                ctypes.winapi_abi,
                                                BOOL,
                                                OSVERSIONINFOEXW.ptr);
            let winVer = OSVERSIONINFOEXW();
            winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;

            if (0 !== GetVersionEx(winVer.address())) {
              osVersion += "." + winVer.wServicePackMajor +
                           "." + winVer.wServicePackMinor +
                           "." + winVer.dwBuildNumber;
            } else {
              Cu.reportError("Unknown failure in GetVersionEX (returned 0)");
              osVersion += ".unknown";
            }
          } catch (e) {
            Cu.reportError("Error getting service pack information. Exception: " + e);
            osVersion += ".unknown";
          }

          if (Services.vc.compare(Services.sysinfo.getProperty("version"), "10") >= 0) {
            const WINDOWS_UBR_KEY_PATH = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
            let ubr = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
                                                 WINDOWS_UBR_KEY_PATH, "UBR",
                                                 Ci.nsIWindowsRegKey.WOW64_64);
            osVersion += (ubr !== undefined) ? "." + ubr : ".unknown";
          }
        } finally {
          kernel32.close();
        }

        // Add processor architecture
        osVersion += " (" + gWinCPUArch + ")";
      }
    }

    try {
      osVersion += " (" + Services.sysinfo.getProperty("secondaryLibrary") + ")";
    } catch (e) {
      // Not all platforms have a secondary widget library, so an error is nothing to worry about.
    }
    osVersion = encodeURIComponent(osVersion);
  }
  return osVersion;
});