blob: 6acee87e938d3b63c9e6834bc28945c8d1aba87f [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/component_updater/downloadable_strings_component_installer.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/task_scheduler/post_task.h"
#include "base/version.h"
#include "build/build_config.h"
#include "components/component_updater/component_updater_paths.h"
#include "components/crx_file/id_util.h"
#include "components/startup_metric_utils/browser/startup_metric_utils.h"
#include "components/update_client/update_client_errors.h"
#if defined(OS_ANDROID)
#include "net/android/network_library.h"
#endif
using component_updater::ComponentUpdateService;
namespace {
// TECHNICAL NOTE:
//
// A lot of the code in this source file implements an experiment that
// downloads a component containing a single test binary file that is
// never used. The experiment collects metrics about network connectivity
// issues and download / install time though. For more details, see:
// http://crbug.com/766746 (related to the experiment) and
// http://crbug.com/757543 (related to the full downloadable strings feature).
// An enum class listing how/when the downloaded binary test file should be
// deleted. Valid values are:
//
// NONE:
// The test file is never deleted, so only one download per Chrome
// install will be performed.
//
// AFTER_INSTALL:
// The file is deleted after it is installed. This will force a new
// download on the next Chrome startup (and thus the collection of more
// metrics). However, this could interfere with differential updates if a
// new version of the payload is available. This setting is only valid if
// it is possible to disable differential downloads on the server side
// (which is ok for the experiment).
//
// DELETE_BEFORE_REGISTRATION:
// The file is deleted in a background task first, then component
// registration will happen. This tends to push registration to a much
// later time in the Chrome startup sequence, and also to increase the
// install time dramatically (e.g. from 600 ms to 2500 ms!).
//
// This number still corresponds to the time between registration and
// installation, the only explanation for now is that this pushes the
// update check to a later part of the Chrome startup phase, which is much
// busier, and thus leaves less CPU cycles to the update tasks.
//
// RENAME_BEFORE_REGISTRATION
// The file/directory is renamed in a background task first, then
// the component registration will happen. Then a new background task
// is posted to delete the now-obsolete renamed directory. Since
// renaming is much faster than deletion, this reduces the variance
// introduced by delaying the registration.
//
enum class DeletionPolicy {
NONE,
AFTER_INSTALL,
DELETE_BEFORE_REGISTRATION,
RENAME_BEFORE_REGISTRATION,
};
// kDeletionPolicy is the deletion policy to adopt here.
// WARNING: See DeletionPolicy comments above before changing this value.
static constexpr DeletionPolicy kDeletionPolicy =
DeletionPolicy::RENAME_BEFORE_REGISTRATION;
// Finch experiment configuration
const base::Feature kDownloadableStringsExperimentFeature = {
"DownloadableStrings", base::FEATURE_DISABLED_BY_DEFAULT,
};
// Name of the binary test file.
const base::FilePath::CharType kDownloadableStringsBinaryTestFileName[] =
FILE_PATH_LITERAL("dummy-locale.pak");
// The SHA256 of the SubjectPublicKeyInfo used to sign the extension.
const uint8_t kDownloadableStringsPublicKeySHA256[32] = {
0x5e, 0xea, 0xe8, 0x4b, 0xba, 0xe2, 0x2b, 0x8f, 0xeb, 0x60, 0x24,
0x01, 0x09, 0x49, 0x92, 0xdf, 0x7b, 0xe9, 0x41, 0x50, 0xba, 0x97,
0x39, 0x7e, 0x72, 0xdd, 0x7b, 0xc0, 0xd3, 0xb7, 0x04, 0xe6,
};
// Relative installation directory for this component.
const base::FilePath::CharType kRelativeInstallDir[] =
FILE_PATH_LITERAL("DownloadableStrings");
// Relative installation renamed directory (for RENAME_BEFORE_REGISTRATION).
const base::FilePath::CharType kRelativeInstallRenamedDir[] =
FILE_PATH_LITERAL("DownloadableStrings.tmp");
// Used to indicate to the OnUpdateResult() function iff the component
// was properly installed. Unfortunately, this is necessary because in
// certain cases (e.g. corrupted CRX), the component updater will call the
// function with an error code 0, while no file was unpacked from the CRX.
bool g_component_ready = false;
// A structure used to record metrics associated with this component.
class Metrics {
public:
// Must be called when component registration starts, and before
// forcing the on-demand update/download of the component.
void RecordComponentRegistrationTime() {
component_registration_ticks_ = base::TimeTicks::Now();
}
// Must be called once the install/update completes. |error_code| must be
// the update_client::Error value passed to the OnDemandUpdate
// callback.
void RecordComponentInstallationTime(update_client::Error error) {
component_installation_ticks_ = base::TimeTicks::Now();
RecordUmaMetrics(error);
}
private:
// Stable enum values sent to UMA for the DownloadableStrings experiment.
// These are persisted to logs, unlike update_client::Error codes which
// they are supposed to match. See ErrorCodeToUmaError() below, and the
// DownloadableStringStatus definition in tools/histograms/enums.xml.
enum class UmaError {
NONE = 0,
UPDATE_IN_PROGRESS = 1,
UPDATE_CANCELED = 2,
RETRY_LATER = 3,
SERVICE_ERROR = 4,
UPDATE_CHECK_ERROR = 5,
UNKNOWN = 15, // Not part of update_client::Error so far.
COUNT // DO NOT REMOVE
};
// Function used to convert an error code received by
// RecordDownloadableStringsComponentInstallComplete() into a UmaError value.
static UmaError ErrorCodeToUmaError(update_client::Error error) {
using update_client::Error;
switch (error) {
case Error::NONE:
return UmaError::NONE;
case Error::UPDATE_IN_PROGRESS:
return UmaError::UPDATE_IN_PROGRESS;
case Error::UPDATE_CANCELED:
return UmaError::UPDATE_CANCELED;
case Error::RETRY_LATER:
return UmaError::RETRY_LATER;
case Error::SERVICE_ERROR:
return UmaError::SERVICE_ERROR;
case Error::UPDATE_CHECK_ERROR:
return UmaError::UPDATE_CHECK_ERROR;
default:
return UmaError::UNKNOWN;
}
}
void RecordUmaMetrics(update_client::Error error) {
UMA_HISTOGRAM_ENUMERATION("DownloadableStrings.Status",
ErrorCodeToUmaError(error), UmaError::COUNT);
// Register times for the three following errors:
// NONE: Succesful download + install of the test file.
// RETRY_LATER: Timeout value after failure to download the file.
// SERVICE_ERROR: Timeout value after a connectivity issue.
// UPDATE_CHECK_ERROR: Timeout value after an update check failure
// (before the actual download).
//
// Other error codes (UPDATE_IN_PROGRESS and UPDATE_CANCELED) are not
// related to network conditions, but to misuse of the update_client
// API that should not apply during this experiment.
const base::TimeDelta time_delta =
component_installation_ticks_ - component_registration_ticks_;
switch (error) {
case update_client::Error::NONE:
UMA_HISTOGRAM_TIMES("DownloadableStrings.InstallTime", time_delta);
break;
case update_client::Error::RETRY_LATER:
UMA_HISTOGRAM_TIMES("DownloadableStrings.Timeout.RetryLater",
time_delta);
break;
case update_client::Error::SERVICE_ERROR:
UMA_HISTOGRAM_TIMES("DownloadableStrings.Timeout.ServiceError",
time_delta);
break;
case update_client::Error::UPDATE_CHECK_ERROR:
UMA_HISTOGRAM_TIMES("DownloadableStrings.Timeout.UpdateCheckError",
time_delta);
break;
default:
break;
}
#if defined(OS_ANDROID)
// Real-world mobile country codes start at 200 and end at 999,
// see https://en.wikipedia.org/wiki/Mobile_country_code
// The value 0 means "undetermined" in this histogram.
const int kCountryCodeMinimum = 200;
const int kCountryCodeMaximum = 999;
enum class UmaCountryCode {
UNDETERMINED = 0,
// Other values are between kCountryCodeMinimum and kCountryCodeMaximum
// but are not enumerated here.
COUNT = 1000 // DO NOT REMOVE
};
static_assert(kCountryCodeMinimum > 0, "Invalid kCountryCodeMinimum value");
static_assert(kCountryCodeMinimum <= kCountryCodeMaximum,
"Invalid kCountryCodeMaximum value");
static_assert(static_cast<int>(UmaCountryCode::COUNT) > kCountryCodeMaximum,
"Invalid UmaCountryCode::COUNT value");
// The value returned by GetTelephonySimOperator() is normally a string
// containing 5 or 6 decimal digits. The first 3 digits are the
// Mobile Country Code (MCC), and the remaining 2 or 3 digits are the
// Mobile Network Code (MNC), which is not useful here. However, the
// Android documentation for the corresponding API [1] states that the
// result is unreliable for CDMA networks (since there are no SIMs in
// CDMA devices). Also, a device without a SIM Card will return an
// empty string. Perform a little sanity check on the value to be
// sure.
//
// [1] TelephonyManager.getSimOperator().
int mobile_country_code = 0;
const std::string sim_mcc_mnc = net::android::GetTelephonySimOperator();
if (sim_mcc_mnc.size() < 5 || sim_mcc_mnc.size() > 6 ||
!std::all_of(sim_mcc_mnc.begin(), sim_mcc_mnc.end(), ::isdigit) ||
!base::StringToInt(base::StringPiece(&sim_mcc_mnc[0], 3U),
&mobile_country_code)) {
LOG(ERROR) << "Invalid MCC+MNC string [" << sim_mcc_mnc << "]";
mobile_country_code = 0;
}
if (mobile_country_code != 0 &&
(mobile_country_code < kCountryCodeMinimum ||
mobile_country_code > kCountryCodeMaximum)) {
LOG(ERROR) << "Mobile country code is out of range "
<< mobile_country_code << "(should be in ["
<< kCountryCodeMinimum << ".." << kCountryCodeMaximum << "])";
mobile_country_code = 0;
}
UMA_HISTOGRAM_ENUMERATION("DownloadableStrings.SimCountryCode",
static_cast<UmaCountryCode>(mobile_country_code),
UmaCountryCode::COUNT);
#else // !OS_ANDROID
const int mobile_country_code = 0;
#endif // !OS_ANDROID
const base::TimeTicks startup_ticks =
startup_metric_utils::MainEntryPointTicks();
const base::TimeDelta registration_time =
component_registration_ticks_ - startup_ticks;
const base::TimeDelta installation_time =
component_installation_ticks_ - startup_ticks;
VLOG(1) << "DownloadableStrings metrics: registration @"
<< registration_time.InMillisecondsF() << " ms, install @"
<< installation_time.InMillisecondsF() << " ms (duration "
<< time_delta.InMillisecondsF()
<< " ms) mobile_country_code=" << mobile_country_code;
}
base::TimeTicks component_registration_ticks_;
base::TimeTicks component_installation_ticks_;
};
base::LazyInstance<Metrics>::Leaky g_metrics = LAZY_INSTANCE_INITIALIZER;
// Delete the installed test files for this experiment.
// |relative_path| is the subdirectory to remove, relative to
// DIR_COMPONENT_USER. In practice this will be either kRelativeInstallDir
// or kRelativeInstallRenamedDir.
void DeleteExperimentTestFiles(const base::FilePath::CharType* relative_path) {
const base::TimeTicks deletion_start = base::TimeTicks::Now();
base::FilePath component_root;
if (!base::PathService::Get(component_updater::DIR_COMPONENT_USER,
&component_root)) {
LOG(ERROR) << "Could not find root directory for components.";
return;
}
const base::FilePath install_dir = component_root.Append(relative_path);
ignore_result(base::DeleteFile(install_dir, true /* recursive */));
const base::TimeDelta deletion_time = base::TimeTicks::Now() - deletion_start;
VLOG(1) << "Deletion time " << deletion_time.InMillisecondsF() << " ms";
}
void RenameComponentDirectory() {
const base::TimeTicks rename_start = base::TimeTicks::Now();
base::FilePath component_root;
if (!base::PathService::Get(component_updater::DIR_COMPONENT_USER,
&component_root)) {
LOG(ERROR) << "Could not find root directory for components.";
return;
}
const base::FilePath src_dir = component_root.Append(kRelativeInstallDir);
const base::FilePath dst_dir =
component_root.Append(kRelativeInstallRenamedDir);
if (!base::Move(src_dir, dst_dir)) {
// Not an error because this can fail if the directory does not exist yet.
VLOG(1) << "Could not rename component directory";
}
const base::TimeDelta rename_time = base::TimeTicks::Now() - rename_start;
VLOG(1) << "Rename time " << rename_time.InMillisecondsF() << " ms";
}
// Post a background task to delete the renamed component directory.
void DeleteRenamedComponentDirectoryInBackgroungTask() {
base::PostTaskWithTraits(
FROM_HERE, {base::TaskPriority::BACKGROUND, base::MayBlock()},
base::BindOnce(&DeleteExperimentTestFiles, kRelativeInstallRenamedDir));
}
} // namespace
namespace component_updater {
bool DownloadableStringsComponentInstallerPolicy::
SupportsGroupPolicyEnabledComponentUpdates() const {
return false;
}
bool DownloadableStringsComponentInstallerPolicy::RequiresNetworkEncryption()
const {
return false;
}
update_client::CrxInstaller::Result
DownloadableStringsComponentInstallerPolicy::OnCustomInstall(
const base::DictionaryValue& manifest,
const base::FilePath& install_dir) {
return update_client::CrxInstaller::Result(0); // Nothing custom here.
}
void DownloadableStringsComponentInstallerPolicy::OnCustomUninstall() {}
base::FilePath DownloadableStringsComponentInstallerPolicy::GetInstalledPath(
const base::FilePath& base) {
return base.Append(kDownloadableStringsBinaryTestFileName);
}
void DownloadableStringsComponentInstallerPolicy::ComponentReady(
const base::Version& version,
const base::FilePath& install_dir,
std::unique_ptr<base::DictionaryValue> manifest) {
VLOG(1) << "Component ready, version " << version.GetString() << " in "
<< install_dir.value();
g_component_ready = true;
g_metrics.Pointer()->RecordComponentInstallationTime(
update_client::Error::NONE);
if (kDeletionPolicy == DeletionPolicy::AFTER_INSTALL) {
// Delete the file in a background task after installation.
base::PostTaskWithTraits(
FROM_HERE, {base::TaskPriority::BACKGROUND, base::MayBlock()},
base::BindOnce(&DeleteExperimentTestFiles,
base::Unretained(kRelativeInstallDir)));
} else if (kDeletionPolicy == DeletionPolicy::RENAME_BEFORE_REGISTRATION) {
DeleteRenamedComponentDirectoryInBackgroungTask();
}
}
// Called during startup and installation before ComponentReady().
bool DownloadableStringsComponentInstallerPolicy::VerifyInstallation(
const base::DictionaryValue& manifest,
const base::FilePath& install_dir) const {
// No need to actually validate the file here.
return base::PathExists(GetInstalledPath(install_dir));
}
base::FilePath
DownloadableStringsComponentInstallerPolicy::GetRelativeInstallDir() const {
return base::FilePath(kRelativeInstallDir);
}
void DownloadableStringsComponentInstallerPolicy::GetHash(
std::vector<uint8_t>* hash) const {
hash->assign(kDownloadableStringsPublicKeySHA256,
kDownloadableStringsPublicKeySHA256 +
arraysize(kDownloadableStringsPublicKeySHA256));
}
std::string DownloadableStringsComponentInstallerPolicy::GetName() const {
return "Downloadable Strings";
}
update_client::InstallerAttributes
DownloadableStringsComponentInstallerPolicy::GetInstallerAttributes() const {
return update_client::InstallerAttributes();
}
std::vector<std::string>
DownloadableStringsComponentInstallerPolicy::GetMimeTypes() const {
return {};
}
static void OnUpdateResult(update_client::Error error) {
if (error == update_client::Error::NONE && !g_component_ready) {
// The component was not downloaded, even though no error was reported.
// This can happen under certain circumstances (e.g. corrupted CRX file).
error = update_client::Error::UPDATE_CHECK_ERROR;
}
if (error != update_client::Error::NONE) {
LOG(ERROR) << "Error: " << static_cast<int>(error);
g_metrics.Pointer()->RecordComponentInstallationTime(error);
if (kDeletionPolicy == DeletionPolicy::RENAME_BEFORE_REGISTRATION) {
DeleteRenamedComponentDirectoryInBackgroungTask();
}
}
}
// static
void DownloadableStringsComponentInstallerPolicy::TriggerComponentUpdate(
OnDemandUpdater* updater) {
const std::string crx_id = crx_file::id_util::GenerateIdFromHash(
kDownloadableStringsPublicKeySHA256,
sizeof(kDownloadableStringsPublicKeySHA256));
DCHECK_EQ(crx_id, "fookoiellkocclipolgaceabajejjcnp");
VLOG(1) << "Triggering component update";
updater->OnDemandUpdate(crx_id, base::Bind(&OnUpdateResult));
}
// static
void DownloadableStringsComponentInstallerPolicy::DoRegistration(
ComponentUpdateService* cus) {
g_metrics.Pointer()->RecordComponentRegistrationTime();
VLOG(1) << "Registering Downloadable Strings component.";
auto policy = base::MakeUnique<DownloadableStringsComponentInstallerPolicy>();
// |cus| will take nership of |installer| during installer->Register(cus).
ComponentInstaller* installer = new ComponentInstaller(std::move(policy));
installer->Register(
cus,
base::Bind(
&DownloadableStringsComponentInstallerPolicy::TriggerComponentUpdate,
&cus->GetOnDemandUpdater()));
}
void RegisterDownloadableStringsComponent(ComponentUpdateService* cus) {
// Return immediately if the feature is not enabled. Otherwise, register
// the component and immediately trigger an update.
if (!base::FeatureList::IsEnabled(kDownloadableStringsExperimentFeature)) {
return;
}
// TODO(digit): Once the experiment is removed, and the full feature
// implemented, call DoRegistration() directly (or inline its content
// directly here).
if (kDeletionPolicy == DeletionPolicy::RENAME_BEFORE_REGISTRATION) {
base::PostTaskWithTraitsAndReply(
FROM_HERE, {base::TaskPriority::BACKGROUND, base::MayBlock()},
base::BindOnce(&RenameComponentDirectory),
base::BindOnce(
&DownloadableStringsComponentInstallerPolicy::DoRegistration,
base::Unretained(cus)));
return;
}
if (kDeletionPolicy == DeletionPolicy::DELETE_BEFORE_REGISTRATION) {
// Delete in a background task, then perform component registration.
base::PostTaskWithTraitsAndReply(
FROM_HERE, {base::TaskPriority::BACKGROUND, base::MayBlock()},
base::BindOnce(&DeleteExperimentTestFiles,
base::Unretained(kRelativeInstallDir)),
base::BindOnce(
&DownloadableStringsComponentInstallerPolicy::DoRegistration,
base::Unretained(cus)));
return;
}
DownloadableStringsComponentInstallerPolicy::DoRegistration(cus);
}
} // namespace component_updater