| // 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 |