// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/installer/setup/downgrade_cleanup.h"
#include <optional>
#include <string_view>
#include <vector>
#include "base/check.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/version.h"
#include "base/win/registry.h"
#include "chrome/install_static/install_util.h"
#include "chrome/installer/setup/installer_state.h"
#include "chrome/installer/setup/setup_constants.h"
#include "chrome/installer/util/callback_work_item.h"
#include "chrome/installer/util/google_update_constants.h"
#include "chrome/installer/util/install_util.h"
#include "chrome/installer/util/util_constants.h"
#include "chrome/installer/util/work_item_list.h"
namespace {
constexpr std::wstring_view kCleanupOperation = L"cleanup";
constexpr std::wstring_view kRevertCleaunpOperation = L"revert";
// Returns the last version of Chrome which introduced breaking changes to the
// installer, or no value if Chrome is not installed or the version installed
// predates support for this feature.
std::optional<base::Version> GetLastBreakingInstallerVersion(HKEY reg_root) {
base::win::RegKey key;
std::wstring last_breaking_installer_version;
if (key.Open(reg_root, install_static::GetClientStateKeyPath().c_str(),
KEY_QUERY_VALUE | KEY_WOW64_32KEY) != ERROR_SUCCESS ||
key.ReadValue(google_update::kRegCleanInstallRequiredForVersionBelowField,
&last_breaking_installer_version) != ERROR_SUCCESS ||
last_breaking_installer_version.empty()) {
return std::nullopt;
}
base::Version version(base::WideToASCII(last_breaking_installer_version));
if (!version.IsValid())
return std::nullopt;
return version;
}
// Formats `cmd_line_with_placeholders` by replacing the placeholders with
// `version` and `operation`. Returns an empty string if some placeholder
// replacements are missing.
std::wstring GetCleanupCommandLine(
const std::wstring& cmd_line_with_placeholders,
const base::Version& version,
std::wstring_view operation) {
DCHECK(version.IsValid());
DCHECK(!cmd_line_with_placeholders.empty());
DCHECK(operation == kCleanupOperation ||
operation == kRevertCleaunpOperation);
std::vector<size_t> offsets;
std::vector<std::u16string> replacements{
base::ASCIIToUTF16(version.GetString()), base::AsString16(operation)};
auto cmd = base::ReplaceStringPlaceholders(
base::AsString16(cmd_line_with_placeholders), replacements, &offsets);
// The `offsets` size and `replacements` size should be equal. If they are
// not, no command should be returned to avoid running an invalid command
// line.
if (offsets.size() != replacements.size())
cmd.clear();
return base::AsWString(std::move(cmd));
}
// Returns true if after a downgrade, `cmd` was run successfully to cleanup
// after a downgrade crossing a breaking installer version. `cmd` is expected to
// be a correctly formatted command line that calls the installer of the version
// we downgraded from with the right version and 'cleanup' as operation
// parameter.
bool LaunchCleanupForBreakingDowngradeProcess(
const std::wstring& cmd,
const CallbackWorkItem& work_item) {
DCHECK(!cmd.empty());
VLOG(1) << "Launching downgrade cleanup process: " << cmd;
base::Process process = base::LaunchProcess(cmd, base::LaunchOptions());
if (!process.IsValid()) {
PLOG(ERROR) << "Failed to launch child process \"" << cmd << "\"";
return false;
}
int exit_code = installer::DOWNGRADE_CLEANUP_SUCCESS;
process.WaitForExit(&exit_code);
if (exit_code == installer::DOWNGRADE_CLEANUP_SUCCESS) {
VLOG(1) << "Downgrade cleanup process succeeded";
return true;
}
LOG(ERROR) << "Downgrade cleanup process \"" << cmd
<< "\" failed with exit code " << exit_code;
return false;
}
// Runs `cmd` to revert any cleanup done after a downgrade crossing a breaking
// installer version in the context of a CallbackWorkItem. `cmd` is expected to
// be a correctly formatted command line that calls the installer of the version
// we downgraded from with the right version and 'revert' as operation
// parameter.
void LaunchUndoCleanupForBreakingDowngradeProcess(
const std::wstring& cmd,
const CallbackWorkItem& work_item) {
DCHECK(!cmd.empty());
VLOG(1) << "Launching downgrade cleanup undo process: " << cmd;
base::Process process = base::LaunchProcess(cmd, base::LaunchOptions());
if (!process.IsValid()) {
PLOG(ERROR) << "Failed to launch child process \"" << cmd << "\"";
return;
}
int exit_code = installer::UNDO_DOWNGRADE_CLEANUP_SUCCESS;
process.WaitForExit(&exit_code);
if (exit_code == installer::UNDO_DOWNGRADE_CLEANUP_SUCCESS) {
VLOG(1) << "Downgrade cleanup undo process succeeded";
return;
}
LOG(ERROR) << "Downgrade cleanup undo process \"" << cmd
<< "\" failed with exit code " << exit_code;
}
} // namespace
namespace installer {
InstallStatus ProcessCleanupForDowngrade(const base::Version& version,
bool revert) {
if (revert) {
return version.IsValid() ? UNDO_DOWNGRADE_CLEANUP_SUCCESS
: UNDO_DOWNGRADE_CLEANUP_FAILED;
}
return version.IsValid() ? DOWNGRADE_CLEANUP_SUCCESS
: DOWNGRADE_CLEANUP_FAILED;
}
std::wstring GetDowngradeCleanupCommandWithPlaceholders(
const base::FilePath& installer_path,
const InstallerState& installer_state) {
base::CommandLine downgrade_cleanup_cmd(installer_path);
downgrade_cleanup_cmd.AppendSwitchNative(
switches::kCleanupForDowngradeVersion, L"$1");
downgrade_cleanup_cmd.AppendSwitchNative(
switches::kCleanupForDowngradeOperation, L"$2");
InstallUtil::AppendModeAndChannelSwitches(&downgrade_cleanup_cmd);
if (installer_state.system_install())
downgrade_cleanup_cmd.AppendSwitch(switches::kSystemLevel);
if (installer_state.verbose_logging())
downgrade_cleanup_cmd.AppendSwitch(switches::kVerboseLogging);
return downgrade_cleanup_cmd.GetCommandLineString();
}
bool AddDowngradeCleanupItems(const base::Version& new_version,
WorkItemList* list) {
DCHECK(new_version.IsValid());
HKEY reg_root = install_static::IsSystemInstall() ? HKEY_LOCAL_MACHINE
: HKEY_CURRENT_USER;
if (GetLastBreakingInstallerVersion(reg_root) <= new_version)
return false;
std::wstring dowgrade_cleanup_cmd;
base::win::RegKey(reg_root, install_static::GetClientStateKeyPath().c_str(),
KEY_QUERY_VALUE | KEY_WOW64_32KEY)
.ReadValue(google_update::kRegDowngradeCleanupCommandField,
&dowgrade_cleanup_cmd);
if (dowgrade_cleanup_cmd.empty())
return false;
auto cleanup_cmd = GetCleanupCommandLine(dowgrade_cleanup_cmd, new_version,
kCleanupOperation);
if (cleanup_cmd.empty()) {
LOG(ERROR) << "Unable to format the downgrade cleanup command \""
<< dowgrade_cleanup_cmd << "\"";
return false;
}
auto revert_cmd = GetCleanupCommandLine(dowgrade_cleanup_cmd, new_version,
kRevertCleaunpOperation);
if (revert_cmd.empty()) {
LOG(ERROR) << "Unable to format the revert downgrade cleanup command \""
<< dowgrade_cleanup_cmd << "\"";
return false;
}
VLOG(1) << "Setting up cleanup for downgrade to version " << new_version;
list->AddCallbackWorkItem(
base::BindOnce(&LaunchCleanupForBreakingDowngradeProcess,
std::move(cleanup_cmd)),
base::BindOnce(&LaunchUndoCleanupForBreakingDowngradeProcess,
std::move(revert_cmd)));
return true;
}
} // namespace installer