chromium/chrome/browser/ash/borealis/borealis_installer_impl.cc

// Copyright 2020 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/borealis/borealis_installer_impl.h"

#include <memory>
#include <sstream>
#include <string_view>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/ash/borealis/borealis_context_manager.h"
#include "chrome/browser/ash/borealis/borealis_features.h"
#include "chrome/browser/ash/borealis/borealis_prefs.h"
#include "chrome/browser/ash/borealis/borealis_service.h"
#include "chrome/browser/ash/borealis/borealis_types.mojom.h"
#include "chrome/browser/ash/borealis/borealis_util.h"
#include "chrome/browser/ash/borealis/infra/transition.h"
#include "chrome/browser/ash/guest_os/guest_os_dlc_helper.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/ash/components/dbus/vm_applications/apps.pb.h"
#include "chromeos/ash/components/dbus/vm_concierge/concierge_service.pb.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/network_service_instance.h"

namespace borealis {

namespace {

// Time to wait for borealis' main app to appear. This is done almost
// immediately by garcon on launch so a short timeout is sufficient.
constexpr base::TimeDelta kWaitForMainAppTimeout = base::Seconds(5);

}  // namespace

using borealis::mojom::InstallResult;

class BorealisInstallerImpl::Installation
    : public Transition<BorealisInstallerImpl::InstallInfo,
                        BorealisInstallerImpl::InstallInfo,
                        Described<InstallResult>>,
      public guest_os::GuestOsRegistryService::Observer {
 public:
  Installation(
      Profile* profile,
      base::RepeatingCallback<void(double)> update_progress_callback,
      base::RepeatingCallback<void(InstallingState)> update_state_callback)
      : profile_(profile),
        installation_start_tick_(base::TimeTicks::Now()),
        update_progress_callback_(std::move(update_progress_callback)),
        update_state_callback_(std::move(update_state_callback)),
        apps_observation_(this),
        weak_factory_(this) {}

  base::TimeTicks start_time() { return installation_start_tick_; }

  void Cancel() {
    Fail({InstallResult::kCancelled, "Installation cancelled by user"});
  }

 private:
  void Start(std::unique_ptr<BorealisInstallerImpl::InstallInfo> start_instance)
      override {
    install_info_ = std::move(start_instance);
    SetState(InstallingState::kCheckingIfAllowed);
    BorealisService::GetForProfile(profile_)->Features().IsAllowed(
        base::BindOnce(&Installation::OnAllowedCheckCompleted,
                       weak_factory_.GetWeakPtr()));
  }

  void SetState(InstallingState state) {
    update_state_callback_.Run(state);
    installing_state_ = state;
  }

  void OnAllowedCheckCompleted(BorealisFeatures::AllowStatus allow_status) {
    if (allow_status != BorealisFeatures::AllowStatus::kAllowed) {
      std::stringstream ss;
      ss << "Borealis is not allowed: " << allow_status;
      Fail({InstallResult::kBorealisNotAllowed, ss.str()});
      return;
    }
    SetState(InstallingState::kInstallingDlc);
    InstallDlc();
  }

  void InstallDlc() {
    dlc_installation_ = std::make_unique<guest_os::GuestOsDlcInstallation>(
        kBorealisDlcName,
        base::BindOnce(&Installation::OnDlcInstallationCompleted,
                       weak_factory_.GetWeakPtr()),
        base::BindRepeating(&Installation::OnDlcInstallationProgressUpdated,
                            weak_factory_.GetWeakPtr()));
  }

  void OnDlcInstallationProgressUpdated(double progress) {
    DCHECK_EQ(installing_state_, InstallingState::kInstallingDlc);
    update_progress_callback_.Run(progress);
  }

  void OnDlcInstallationCompleted(
      guest_os::GuestOsDlcInstallation::Result install_result) {
    DCHECK_EQ(installing_state_, InstallingState::kInstallingDlc);

    // If success, continue to the next state.
    if (install_result.has_value()) {
      // We are in the callback of DLC completion, and the first thing startup
      // will do is try to mount the DLC, so we need to use a PostTask to avoid
      // deadlocking ourselves.
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&Installation::StartupBorealis,
                                    weak_factory_.GetWeakPtr()));
      return;
    }

    // At this point, the Borealis DLC installation has failed.
    Fail(DescribeDlcFailure(install_result.error()));
  }

  Described<InstallResult> DescribeDlcFailure(
      guest_os::GuestOsDlcInstallation::Error error) {
    switch (error) {
      case guest_os::GuestOsDlcInstallation::Error::Cancelled:
        return {InstallResult::kCancelled, "Installation cancelled by user."};
      case guest_os::GuestOsDlcInstallation::Error::Offline:
        return {InstallResult::kOffline,
                "Failed to download DLC while device is offline."};
      case guest_os::GuestOsDlcInstallation::Error::NeedUpdate:
        return {
            InstallResult::kDlcNeedUpdateError,
            "Omaha could not provide an image, device may need to be updated."};
      case guest_os::GuestOsDlcInstallation::Error::NeedReboot:
        return {InstallResult::kDlcNeedRebootError,
                "Device has pending update and needs a reboot to use Borealis "
                "DLC."};
      case guest_os::GuestOsDlcInstallation::Error::DiskFull:
        return {InstallResult::kDlcNeedSpaceError,
                "Device needs to free space to use Borealis DLC."};
      case guest_os::GuestOsDlcInstallation::Error::Busy:
        return {
            InstallResult::kDlcBusyError,
            "Borealis DLC is not able to be installed as dlcservice is busy."};
      case guest_os::GuestOsDlcInstallation::Error::Internal:
        return {InstallResult::kDlcInternalError,
                "Something went wrong internally with DlcService."};
      case guest_os::GuestOsDlcInstallation::Error::Invalid:
        return {InstallResult::kDlcUnsupportedError,
                "Borealis DLC is not supported, need to enable Borealis DLC."};
      case guest_os::GuestOsDlcInstallation::Error::UnknownFailure:
        return {InstallResult::kDlcUnknownError,
                "Unexpected DLC failure, please file feedback."};
    }

    NOTREACHED();
  }

  // As part of its installation we perform a dry run of borealis. This ensures
  // that the VM works somewhat and allows the container_guest daemon to update
  // Chrome. See go/borealis-mid-launch for details.
  void StartupBorealis() {
    SetState(InstallingState::kStartingUp);
    BorealisService::GetForProfile(profile_)->ContextManager().StartBorealis(
        base::BindOnce(&Installation::OnBorealisStarted,
                       weak_factory_.GetWeakPtr()));
  }

  void OnBorealisStarted(BorealisContextManager::ContextOrFailure result) {
    if (result.has_value()) {
      WaitForMainApp();
      return;
    }
    std::stringstream ss;
    ss << "Failed to start borealis (code "
       << static_cast<int>(result.error().error())
       << "): " << result.error().description();
    Fail({InstallResult::kStartupFailed, ss.str()});
  }

  void WaitForMainApp() {
    SetState(InstallingState::kAwaitingApplications);
    guest_os::GuestOsRegistryService* apps_registry =
        guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_);
    apps_observation_.Observe(apps_registry);
    std::optional<guest_os::GuestOsRegistryService::Registration> main_app =
        apps_registry->GetRegistration(kClientAppId);
    if (main_app.has_value() &&
        main_app->VmType() == guest_os::VmType::BOREALIS) {
      apps_observation_.Reset();
      MainAppFound(true);
      return;
    }
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&Installation::MainAppFound, weak_factory_.GetWeakPtr(),
                       false),
        kWaitForMainAppTimeout);
  }

  void OnRegistryUpdated(
      guest_os::GuestOsRegistryService* registry_service,
      guest_os::VmType vm_type,
      const std::vector<std::string>& updated_apps,
      const std::vector<std::string>& removed_apps,
      const std::vector<std::string>& inserted_apps) override {
    if (vm_type != guest_os::VmType::BOREALIS) {
      return;
    }

    for (const auto& app : inserted_apps) {
      if (app == kClientAppId) {
        MainAppFound(true);
        break;
      }
    }
  }

  void MainAppFound(bool found) {
    // We use the presence of the install_info_ object to prevent races here, so
    // return if it has already been removed.
    if (!install_info_) {
      return;
    }
    if (!found) {
      install_info_.reset();
      Fail({InstallResult::kMainAppNotPresent,
            "Failed to verify that the main app has been created"});
      return;
    }
    Succeed(std::move(install_info_));
  }

  const raw_ptr<Profile> profile_;
  base::TimeTicks installation_start_tick_;
  InstallingState installing_state_;
  base::RepeatingCallback<void(double)> update_progress_callback_;
  base::RepeatingCallback<void(InstallingState)> update_state_callback_;
  std::unique_ptr<BorealisInstallerImpl::InstallInfo> install_info_;
  std::unique_ptr<guest_os::GuestOsDlcInstallation> dlc_installation_;
  base::ScopedObservation<guest_os::GuestOsRegistryService,
                          guest_os::GuestOsRegistryService::Observer>
      apps_observation_;
  base::WeakPtrFactory<Installation> weak_factory_;
};

class BorealisInstallerImpl::Uninstallation
    : public Transition<BorealisInstallerImpl::InstallInfo,
                        BorealisInstallerImpl::InstallInfo,
                        BorealisUninstallResult> {
 public:
  explicit Uninstallation(Profile* profile)
      : profile_(profile), weak_factory_(this) {}

  void Start(std::unique_ptr<BorealisInstallerImpl::InstallInfo> start_instance)
      override {
    uninstall_info_ = std::move(start_instance);
    BorealisService::GetForProfile(profile_)->ContextManager().ShutDownBorealis(
        base::BindOnce(&Uninstallation::OnShutdownCompleted,
                       weak_factory_.GetWeakPtr()));
  }

 private:
  void OnShutdownCompleted(BorealisShutdownResult result) {
    if (result != BorealisShutdownResult::kSuccess) {
      LOG(ERROR) << "Failed to shut down before uninstall (code="
                 << static_cast<int>(result) << ")";
      Fail(BorealisUninstallResult::kShutdownFailed);
      return;
    }
    // Clear the borealis apps.
    guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_)
        ->ClearApplicationList(vm_tools::apps::BOREALIS,
                               uninstall_info_->vm_name,
                               uninstall_info_->container_name);

    vm_tools::concierge::DestroyDiskImageRequest request;
    request.set_cryptohome_id(
        ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
    request.set_vm_name(uninstall_info_->vm_name);
    ash::ConciergeClient::Get()->DestroyDiskImage(
        std::move(request), base::BindOnce(&Uninstallation::OnDiskRemoved,
                                           weak_factory_.GetWeakPtr()));
  }

  void OnDiskRemoved(
      std::optional<vm_tools::concierge::DestroyDiskImageResponse> response) {
    if (!response) {
      LOG(ERROR) << "Failed to destroy disk image. Empty response.";
      Fail(BorealisUninstallResult::kRemoveDiskFailed);
      return;
    }
    if (response->status() != vm_tools::concierge::DISK_STATUS_DESTROYED &&
        response->status() != vm_tools::concierge::DISK_STATUS_DOES_NOT_EXIST) {
      LOG(ERROR) << "Failed to destroy disk image: "
                 << response->failure_reason();
      Fail(BorealisUninstallResult::kRemoveDiskFailed);
      return;
    }

    ash::DlcserviceClient::Get()->Uninstall(
        kBorealisDlcName, base::BindOnce(&Uninstallation::OnDlcUninstalled,
                                         weak_factory_.GetWeakPtr()));
  }

  void OnDlcUninstalled(std::string_view dlc_err) {
    if (dlc_err.empty()) {
      LOG(ERROR) << "Failed to remove DLC: no response.";
      Fail(BorealisUninstallResult::kRemoveDlcFailed);
      return;
    }
    if (dlc_err != dlcservice::kErrorNone) {
      LOG(ERROR) << "Failed to remove DLC: " << dlc_err;
      Fail(BorealisUninstallResult::kRemoveDlcFailed);
      return;
    }

    // Remove the pref last. This way we are still considered "installed" if we
    // fail to uninstall.  The practical effect is that every step is
    // recoverable from, so as far as the user is concerned things will go back
    // to the normal installed state.
    profile_->GetPrefs()->SetBoolean(prefs::kBorealisInstalledOnDevice, false);

    Succeed(std::move(uninstall_info_));
  }

  const raw_ptr<Profile> profile_;
  std::unique_ptr<BorealisInstallerImpl::InstallInfo> uninstall_info_;
  base::WeakPtrFactory<Uninstallation> weak_factory_;
};

BorealisInstallerImpl::BorealisInstallerImpl(Profile* profile)
    : profile_(profile), weak_ptr_factory_(this) {}

BorealisInstallerImpl::~BorealisInstallerImpl() = default;

bool BorealisInstallerImpl::IsProcessing() {
  return !!in_progress_installation_;
}

void BorealisInstallerImpl::Start() {
  RecordBorealisInstallNumAttemptsHistogram();
  if (IsProcessing()) {
    OnInstallComplete(base::unexpected(Installation::ErrorState{
        InstallResult::kBorealisInstallInProgress,
        "Installation of Borealis is already in progress"}));
    return;
  }

  if (content::GetNetworkConnectionTracker()->IsOffline()) {
    OnInstallComplete(base::unexpected(Installation::ErrorState{
        InstallResult::kOffline, "Can not install Borealis while offline"}));
    return;
  }

  // Reset mic permission, we don't want it to persist across
  // re-installation.
  profile_->GetPrefs()->SetBoolean(prefs::kBorealisMicAllowed, false);

  auto install_info = std::make_unique<InstallInfo>();
  install_info->vm_name = "borealis";
  install_info->container_name = "penguin";
  in_progress_installation_ = std::make_unique<Installation>(
      profile_,
      base::BindRepeating(&BorealisInstallerImpl::UpdateProgress,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindRepeating(&BorealisInstallerImpl::UpdateInstallingState,
                          weak_ptr_factory_.GetWeakPtr()));
  in_progress_installation_->Begin(
      std::move(install_info),
      base::BindOnce(&BorealisInstallerImpl::OnInstallComplete,
                     weak_ptr_factory_.GetWeakPtr()));
}

void BorealisInstallerImpl::Cancel() {
  if (in_progress_installation_) {
    in_progress_installation_->Cancel();
  }
  for (auto& observer : observers_) {
    observer.OnCancelInitiated();
  }
}

void BorealisInstallerImpl::Uninstall(
    base::OnceCallback<void(BorealisUninstallResult)> on_uninstall_callback) {
  if (in_progress_uninstallation_) {
    std::move(on_uninstall_callback)
        .Run(BorealisUninstallResult::kAlreadyInProgress);
    return;
  }
  RecordBorealisUninstallNumAttemptsHistogram();
  // TODO(b/179303903): The installer should own the relevant bits so the VM and
  // container names are kept by it (and not context_manager).
  auto uninstall_info = std::make_unique<InstallInfo>();
  uninstall_info->vm_name = "borealis";
  uninstall_info->container_name = "penguin";
  in_progress_uninstallation_ = std::make_unique<Uninstallation>(profile_);
  in_progress_uninstallation_->Begin(
      std::move(uninstall_info),
      base::BindOnce(&BorealisInstallerImpl::OnUninstallComplete,
                     weak_ptr_factory_.GetWeakPtr(),
                     std::move(on_uninstall_callback)));
}

void BorealisInstallerImpl::AddObserver(Observer* observer) {
  DCHECK(observer);
  DCHECK(observers_.empty());
  observers_.AddObserver(observer);
}

void BorealisInstallerImpl::RemoveObserver(Observer* observer) {
  DCHECK(observer);
  DCHECK(observers_.HasObserver(observer));
  observers_.RemoveObserver(observer);
  DCHECK(observers_.empty());
}

void BorealisInstallerImpl::UpdateProgress(double state_progress) {
  if (state_progress < 0 || state_progress > 1) {
    LOG(ERROR) << "Unexpected progress value " << state_progress
               << " in installing state "
               << GetInstallingStateName(installing_state_);
    return;
  }

  double start_range = 0;
  double end_range = 0;
  switch (installing_state_) {
    case InstallingState::kCheckingIfAllowed:
      start_range = 0;
      end_range = 0.1;
      break;
    case InstallingState::kInstallingDlc:
      start_range = 0.1;
      end_range = 0.5;
      break;
    case InstallingState::kStartingUp:
      start_range = 0.5;
      end_range = 0.8;
      break;
    case InstallingState::kAwaitingApplications:
      start_range = 0.8;
      end_range = 1.0;
      break;
    case InstallingState::kInactive:
      NOTREACHED_IN_MIGRATION();
  }

  double new_progress =
      start_range + (end_range - start_range) * state_progress;

  for (auto& observer : observers_) {
    observer.OnProgressUpdated(new_progress);
  }
}

void BorealisInstallerImpl::UpdateInstallingState(
    InstallingState installing_state) {
  DCHECK_NE(installing_state, InstallingState::kInactive);
  installing_state_ = installing_state;
  for (auto& observer : observers_) {
    observer.OnStateUpdated(installing_state_);
  }
  // The state just changed, so the progress towards that state is 0.
  UpdateProgress(0);
}

void BorealisInstallerImpl::OnInstallComplete(
    base::expected<std::unique_ptr<InstallInfo>, Described<InstallResult>>
        result_or_error) {
  InstallResult result = result_or_error.has_value()
                             ? InstallResult::kSuccess
                             : result_or_error.error().error();
  // If another installation is in progress, we don't want to reset any states
  // and interfere with the process. When that process completes, it will reset
  // these states.
  if (result != InstallResult::kBorealisInstallInProgress) {
    base::TimeDelta duration =
        in_progress_installation_
            ? base::TimeTicks::Now() - in_progress_installation_->start_time()
            : base::Seconds(0);
    in_progress_installation_.reset();
    installing_state_ = InstallingState::kInactive;
    if (result == InstallResult::kSuccess) {
      profile_->GetPrefs()->SetBoolean(prefs::kBorealisInstalledOnDevice, true);
      RecordBorealisInstallOverallTimeHistogram(duration);
    }
    // TODO(b/188713071): Clean up if installation fails.
    RecordBorealisInstallResultHistogram(result);
  }
  for (auto& observer : observers_) {
    observer.OnInstallationEnded(result,
                                 result_or_error.has_value()
                                     ? ""
                                     : result_or_error.error().description());
  }
}

void BorealisInstallerImpl::OnUninstallComplete(
    base::OnceCallback<void(BorealisUninstallResult)> on_uninstall_callback,
    base::expected<std::unique_ptr<InstallInfo>, BorealisUninstallResult>
        result) {
  in_progress_uninstallation_.reset();
  BorealisUninstallResult uninstall_result = BorealisUninstallResult::kSuccess;
  if (!result.has_value()) {
    uninstall_result = result.error();
  }
  RecordBorealisUninstallResultHistogram(uninstall_result);
  std::move(on_uninstall_callback).Run(uninstall_result);
}

}  // namespace borealis