| // Copyright 2023 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/browser/ash/guest_os/guest_os_dlc_helper.h" |
| |
| #include "ash/constants/ash_features.h" |
| #include "base/feature_list.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "chromeos/ash/components/dbus/dlcservice/dlcservice.pb.h" |
| #include "content/public/browser/network_service_instance.h" |
| #include "third_party/cros_system_api/dbus/dlcservice/dbus-constants.h" |
| |
| namespace guest_os { |
| |
| namespace { |
| |
| // How long to wait between retry attempts. |
| constexpr base::TimeDelta kBetweenRetryDelay = base::Seconds(5); |
| |
| // Maximum number of times the installation will retry before giving up. |
| constexpr int kMaxRetries = 5; |
| |
| GuestOsDlcInstallation::Error ToError(const std::string& error) { |
| if (error == dlcservice::kErrorInternal) { |
| return GuestOsDlcInstallation::Error::Internal; |
| } else if (error == dlcservice::kErrorBusy) { |
| return GuestOsDlcInstallation::Error::Busy; |
| } else if (error == dlcservice::kErrorNeedReboot) { |
| return GuestOsDlcInstallation::Error::NeedReboot; |
| } else if (error == dlcservice::kErrorInvalidDlc) { |
| return GuestOsDlcInstallation::Error::Invalid; |
| } else if (error == dlcservice::kErrorAllocation) { |
| return GuestOsDlcInstallation::Error::DiskFull; |
| } else if (error == dlcservice::kErrorNoImageFound) { |
| // Actually this isn't always actionable with an update but it often is so |
| // we advise it. |
| return GuestOsDlcInstallation::Error::NeedUpdate; |
| } |
| // DLC records success the same way as failure, but this method should never |
| // be called on success. |
| CHECK(error != dlcservice::kErrorNone); |
| LOG(ERROR) << "DLC Installation failed with unrecognized error: " << error; |
| return GuestOsDlcInstallation::Error::UnknownFailure; |
| } |
| |
| enum class Actionability { |
| // Errors which we know will just happen again until the user does something. |
| UserIntervention, |
| // Errors which may not happen again so we automatically retry them. |
| Retry, |
| // Errors which can neither be actioned or retried by the user. |
| None, |
| }; |
| |
| Actionability GetActionability(GuestOsDlcInstallation::Error err) { |
| switch (err) { |
| case GuestOsDlcInstallation::Error::Cancelled: |
| case GuestOsDlcInstallation::Error::Offline: |
| case GuestOsDlcInstallation::Error::NeedUpdate: |
| case GuestOsDlcInstallation::Error::NeedReboot: |
| case GuestOsDlcInstallation::Error::DiskFull: |
| return Actionability::UserIntervention; |
| case GuestOsDlcInstallation::Error::Busy: |
| case GuestOsDlcInstallation::Error::Internal: |
| return Actionability::Retry; |
| case GuestOsDlcInstallation::Error::Invalid: |
| case GuestOsDlcInstallation::Error::UnknownFailure: |
| return Actionability::None; |
| } |
| } |
| |
| } // namespace |
| |
| GuestOsDlcInstallation::GuestOsDlcInstallation( |
| std::string dlc_id, |
| base::OnceCallback<void(Result)> completion_callback, |
| ProgressCallback progress_callback) |
| : dlc_id_(std::move(dlc_id)), |
| retries_remaining_(kMaxRetries), |
| completion_callback_(std::move(completion_callback)), |
| progress_callback_(std::move(progress_callback)) { |
| // This object represents the installation so begin that installation in |
| // its constructor. First, check if the DLC is installed. |
| CheckState(); |
| } |
| |
| GuestOsDlcInstallation::~GuestOsDlcInstallation() { |
| if (completion_callback_) { |
| std::move(completion_callback_).Run(base::unexpected(Error::Cancelled)); |
| } |
| } |
| |
| void GuestOsDlcInstallation::CancelGracefully() { |
| gracefully_cancelled_ = true; |
| retries_remaining_ = 0; |
| } |
| |
| void GuestOsDlcInstallation::CheckState() { |
| ash::DlcserviceClient::Get()->GetDlcState( |
| dlc_id_, base::BindOnce(&GuestOsDlcInstallation::OnGetDlcStateCompleted, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void GuestOsDlcInstallation::OnGetDlcStateCompleted( |
| const std::string& err, |
| const dlcservice::DlcState& dlc_state) { |
| ash::DlcserviceClient::InstallResult result; |
| switch (dlc_state.state()) { |
| case dlcservice::DlcState::INSTALLED: |
| result.dlc_id = dlc_state.id(); |
| result.root_path = dlc_state.root_path(); |
| result.error = dlcservice::kErrorNone; |
| OnDlcInstallCompleted(result); |
| break; |
| case dlcservice::DlcState::NOT_INSTALLED: |
| StartInstall(); |
| break; |
| case dlcservice::DlcState::INSTALLING: |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&GuestOsDlcInstallation::CheckState, |
| weak_factory_.GetWeakPtr()), |
| kBetweenRetryDelay); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void GuestOsDlcInstallation::StartInstall() { |
| // Skip calling install if we've canceled. |
| if (gracefully_cancelled_) { |
| OnDlcInstallCompleted({}); |
| return; |
| } |
| dlcservice::InstallRequest install_request; |
| install_request.set_id(dlc_id_); |
| if (base::FeatureList::IsEnabled( |
| ash::features::kCrostiniTerminaDlcForceOta)) { |
| install_request.set_force_ota(true); |
| } |
| ash::DlcserviceClient::Get()->Install( |
| install_request, |
| base::BindOnce(&GuestOsDlcInstallation::OnDlcInstallCompleted, |
| weak_factory_.GetWeakPtr()), |
| progress_callback_); |
| } |
| |
| void GuestOsDlcInstallation::OnDlcInstallCompleted( |
| const ash::DlcserviceClient::InstallResult& result) { |
| if (gracefully_cancelled_) { |
| std::move(completion_callback_).Run(base::unexpected(Error::Cancelled)); |
| return; |
| } |
| CHECK(result.dlc_id == dlc_id_); |
| if (result.error == dlcservice::kErrorNone) { |
| std::move(completion_callback_) |
| .Run(base::ok(base::FilePath(result.root_path))); |
| return; |
| } |
| |
| Error err = ToError(result.error); |
| |
| switch (GetActionability(err)) { |
| case Actionability::UserIntervention: |
| std::move(completion_callback_).Run(base::unexpected(err)); |
| return; |
| |
| case Actionability::Retry: |
| if (retries_remaining_ > 0) { |
| --retries_remaining_; |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&GuestOsDlcInstallation::StartInstall, |
| weak_factory_.GetWeakPtr()), |
| kBetweenRetryDelay); |
| return; |
| } |
| // If we're out of retries then we can't action this error ourselves. |
| ABSL_FALLTHROUGH_INTENDED; |
| |
| case Actionability::None: |
| // Unless we know the cause of an error (because it was actionable or we |
| // have run out of retries), assume being offline causes errors. |
| if (content::GetNetworkConnectionTracker()->IsOffline()) { |
| err = Error::Offline; |
| } |
| |
| std::move(completion_callback_).Run(base::unexpected(err)); |
| return; |
| } |
| } |
| |
| } // namespace guest_os |
| |
| std::ostream& operator<<(std::ostream& stream, |
| guest_os::GuestOsDlcInstallation::Error err) { |
| switch (err) { |
| case guest_os::GuestOsDlcInstallation::Error::Cancelled: |
| return stream << "cancelled"; |
| case guest_os::GuestOsDlcInstallation::Error::Offline: |
| return stream << "offline"; |
| case guest_os::GuestOsDlcInstallation::Error::NeedUpdate: |
| return stream << "need update"; |
| case guest_os::GuestOsDlcInstallation::Error::NeedReboot: |
| return stream << "need reboot"; |
| case guest_os::GuestOsDlcInstallation::Error::DiskFull: |
| return stream << "disk full"; |
| case guest_os::GuestOsDlcInstallation::Error::Busy: |
| return stream << "busy"; |
| case guest_os::GuestOsDlcInstallation::Error::Internal: |
| return stream << "internal"; |
| case guest_os::GuestOsDlcInstallation::Error::Invalid: |
| return stream << "invalid"; |
| case guest_os::GuestOsDlcInstallation::Error::UnknownFailure: |
| return stream << "unknown"; |
| } |
| } |