chromium/chrome/browser/ash/crostini/crostini_package_service.cc

// Copyright 2018 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/crostini/crostini_package_service.h"

#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_manager_factory.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/guest_os_share_path.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_keyed_service_factory.h"

namespace crostini {

namespace {

class CrostiniPackageServiceFactory : public ProfileKeyedServiceFactory {
 public:
  static CrostiniPackageService* GetForProfile(Profile* profile) {
    return static_cast<CrostiniPackageService*>(
        GetInstance()->GetServiceForBrowserContext(profile, true));
  }

  static CrostiniPackageServiceFactory* GetInstance() {
    static base::NoDestructor<CrostiniPackageServiceFactory> factory;
    return factory.get();
  }

 private:
  friend class base::NoDestructor<CrostiniPackageServiceFactory>;

  CrostiniPackageServiceFactory()
      : ProfileKeyedServiceFactory(
            "CrostiniPackageService",
            ProfileSelections::Builder()
                .WithRegular(ProfileSelection::kOriginalOnly)
                // TODO(crbug.com/40257657): Check if this service is needed in
                // Guest mode.
                .WithGuest(ProfileSelection::kOriginalOnly)
                // TODO(crbug.com/41488885): Check if this service is needed for
                // Ash Internals.
                .WithAshInternals(ProfileSelection::kOriginalOnly)
                .Build()) {
    DependsOn(CrostiniManagerFactory::GetInstance());
  }

  ~CrostiniPackageServiceFactory() override = default;

  // BrowserContextKeyedServiceFactory:
  KeyedService* BuildServiceInstanceFor(
      content::BrowserContext* context) const override {
    Profile* profile = Profile::FromBrowserContext(context);
    return new CrostiniPackageService(profile);
  }
};

PackageOperationStatus InstallStatusToOperationStatus(
    InstallLinuxPackageProgressStatus status) {
  switch (status) {
    case InstallLinuxPackageProgressStatus::SUCCEEDED:
      return PackageOperationStatus::SUCCEEDED;
    case InstallLinuxPackageProgressStatus::FAILED:
      return PackageOperationStatus::FAILED;
    case InstallLinuxPackageProgressStatus::DOWNLOADING:
    case InstallLinuxPackageProgressStatus::INSTALLING:
      return PackageOperationStatus::RUNNING;
    default:
      NOTREACHED_IN_MIGRATION();
  }
}

PackageOperationStatus UninstallStatusToOperationStatus(
    UninstallPackageProgressStatus status) {
  switch (status) {
    case UninstallPackageProgressStatus::SUCCEEDED:
      return PackageOperationStatus::SUCCEEDED;
    case UninstallPackageProgressStatus::FAILED:
      return PackageOperationStatus::FAILED;
    case UninstallPackageProgressStatus::UNINSTALLING:
      return PackageOperationStatus::RUNNING;
    default:
      NOTREACHED_IN_MIGRATION();
  }
}

}  // namespace

struct CrostiniPackageService::QueuedInstall {
  QueuedInstall(const std::string& package_path,
                CrostiniManager::InstallLinuxPackageCallback callback,
                std::unique_ptr<CrostiniPackageNotification> notification)
      : package_path(package_path),
        callback(std::move(callback)),
        notification(std::move(notification)) {}
  ~QueuedInstall() = default;

  std::string package_path;
  CrostiniManager::InstallLinuxPackageCallback callback;

  // Notification displaying "install queued"
  std::unique_ptr<CrostiniPackageNotification> notification;
};

struct CrostiniPackageService::QueuedUninstall {
  QueuedUninstall(
      const std::string& app_id,
      std::unique_ptr<CrostiniPackageNotification> notification_argument)
      : app_id(app_id), notification(std::move(notification_argument)) {}
  ~QueuedUninstall() = default;

  // App to uninstall
  std::string app_id;

  // Notification displaying "uninstall queued"
  std::unique_ptr<CrostiniPackageNotification> notification;
};

CrostiniPackageService* CrostiniPackageService::GetForProfile(
    Profile* profile) {
  return CrostiniPackageServiceFactory::GetForProfile(profile);
}

CrostiniPackageService::CrostiniPackageService(Profile* profile)
    : profile_(profile) {
  CrostiniManager* manager = CrostiniManager::GetForProfile(profile);

  manager->AddLinuxPackageOperationProgressObserver(this);
  manager->AddPendingAppListUpdatesObserver(this);
  manager->AddVmShutdownObserver(this);
}

CrostiniPackageService::~CrostiniPackageService() = default;

void CrostiniPackageService::Shutdown() {
  CrostiniManager* manager = CrostiniManager::GetForProfile(profile_);
  manager->RemoveLinuxPackageOperationProgressObserver(this);
  manager->RemovePendingAppListUpdatesObserver(this);
  manager->RemoveVmShutdownObserver(this);

  // CrostiniPackageNotification registers itself as a GuestOsRegistryService
  // observer, so they need to be destroyed here while the
  // GuestOsRegistryService still exists.
  running_notifications_.clear();
  queued_installs_.clear();
  queued_uninstalls_.clear();
  finished_notifications_.clear();
}

void CrostiniPackageService::SetNotificationStateChangeCallbackForTesting(
    StateChangeCallback state_change_callback) {
  testing_state_change_callback_ = std::move(state_change_callback);
}

void CrostiniPackageService::NotificationCompleted(
    CrostiniPackageNotification* notification) {
  for (auto it = finished_notifications_.begin();
       it != finished_notifications_.end(); ++it) {
    if (it->get() == notification) {
      finished_notifications_.erase(it);
      return;
    }
  }
  // Notifications should never delete themselves while queued or running.
  NOTREACHED_IN_MIGRATION();
}

void CrostiniPackageService::GetLinuxPackageInfo(
    const guest_os::GuestId& container_id,
    const storage::FileSystemURL& package_url,
    CrostiniManager::GetLinuxPackageInfoCallback callback) {
  base::FilePath path;
  if (!file_manager::util::ConvertFileSystemURLToPathInsideCrostini(
          profile_, package_url, &path)) {
    LinuxPackageInfo info;
    info.success = false;
    info.failure_reason = "Invalid package url: " + package_url.DebugString();
    return std::move(callback).Run(info);
  }

  // Share path if it is not in crostini.
  if (package_url.mount_filesystem_id() !=
      file_manager::util::GetCrostiniMountPointName(profile_)) {
    CrostiniManager::RestartOptions options;
    options.share_paths.push_back(package_url.path());
    restart_id_for_testing_ =
        CrostiniManager::GetForProfile(profile_)->RestartCrostiniWithOptions(
            container_id, std::move(options),
            base::BindOnce(
                &CrostiniPackageService::OnSharePathForGetLinuxPackageInfo,
                weak_ptr_factory_.GetWeakPtr(), container_id, package_url, path,
                std::move(callback)));
  } else {
    OnSharePathForGetLinuxPackageInfo(container_id, package_url, path,
                                      std::move(callback),
                                      CrostiniResult::SUCCESS);
  }
}

void CrostiniPackageService::OnSharePathForGetLinuxPackageInfo(
    const guest_os::GuestId& container_id,
    const storage::FileSystemURL& package_url,
    const base::FilePath& package_path,
    CrostiniManager::GetLinuxPackageInfoCallback callback,
    CrostiniResult result) {
  if (result != CrostiniResult::SUCCESS) {
    LinuxPackageInfo info;
    info.success = false;
    info.failure_reason = "Error sharing package " + package_url.DebugString() +
                          ": " + CrostiniResultString(result);
    return std::move(callback).Run(info);
  }
  CrostiniManager::GetForProfile(profile_)->GetLinuxPackageInfo(
      container_id, package_path.value(),
      base::BindOnce(&CrostiniPackageService::OnGetLinuxPackageInfo,
                     weak_ptr_factory_.GetWeakPtr(), container_id,
                     std::move(callback)));
}

void CrostiniPackageService::QueueInstallLinuxPackage(
    const guest_os::GuestId& container_id,
    const storage::FileSystemURL& package_url,
    CrostiniManager::InstallLinuxPackageCallback callback) {
  base::FilePath path;
  if (!file_manager::util::ConvertFileSystemURLToPathInsideCrostini(
          profile_, package_url, &path)) {
    LOG(ERROR) << "Invalid install linux package: "
               << package_url.DebugString();
    return std::move(callback).Run(
        CrostiniResult::INSTALL_LINUX_PACKAGE_FAILED);
  }

  if (ContainerHasRunningOperation(container_id)) {
    CreateQueuedInstall(container_id, path.value(), std::move(callback));
    return;
  }

  CreateRunningNotification(
      container_id,
      CrostiniPackageNotification::NotificationType::PACKAGE_INSTALL,
      /*app_name=*/"");

  CrostiniManager::GetForProfile(profile_)->InstallLinuxPackage(
      container_id, path.value(),
      base::BindOnce(&CrostiniPackageService::OnInstallLinuxPackage,
                     weak_ptr_factory_.GetWeakPtr(), container_id,
                     std::move(callback)));
}

void CrostiniPackageService::OnInstallLinuxPackageProgress(
    const guest_os::GuestId& container_id,
    InstallLinuxPackageProgressStatus status,
    int progress_percent,
    const std::string& error_message) {
  // Linux package install has two phases, downloading and installing, which we
  // map to a single progess percentage amount by dividing the range in half --
  // 0-50% for the downloading phase, 51-100% for the installing phase.
  int display_progress = progress_percent / 2;
  if (status == InstallLinuxPackageProgressStatus::INSTALLING) {
    display_progress += 50;  // Second phase
  }

  UpdatePackageOperationStatus(container_id,
                               InstallStatusToOperationStatus(status),
                               display_progress, error_message);
}

void CrostiniPackageService::OnUninstallPackageProgress(
    const guest_os::GuestId& container_id,
    UninstallPackageProgressStatus status,
    int progress_percent) {
  UpdatePackageOperationStatus(
      container_id, UninstallStatusToOperationStatus(status), progress_percent);
}

void CrostiniPackageService::OnVmShutdown(const std::string& vm_name) {
  // Making a notification as failed removes it from |running_notifications_|,
  // which invalidates the iterators. To avoid this, we record all the
  // containers that just shut down before removing any notifications.
  std::vector<guest_os::GuestId> to_remove;
  for (auto& running_notification : running_notifications_) {
    if (running_notification.first.vm_name == vm_name) {
      to_remove.push_back(running_notification.first);
    }
  }
  for (auto iter : to_remove) {
    // Use a loop because removing a notification from the running set can cause
    // a queued operation to start, which will also need to be removed.
    while (ContainerHasRunningOperation(iter)) {
      UpdatePackageOperationStatus(iter, PackageOperationStatus::FAILED, 0);
    }
  }
}

void CrostiniPackageService::QueueUninstallApplication(
    const std::string& app_id) {
  auto registration =
      guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_)
          ->GetRegistration(app_id);
  if (!registration.has_value()) {
    LOG(ERROR)
        << "Tried to uninstall application that has already been uninstalled";
    return;
  }

  const guest_os::GuestId container_id(registration->VmType(),
                                       registration->VmName(),
                                       registration->ContainerName());
  const std::string app_name = registration->Name();
  if (ContainerHasRunningOperation(container_id)) {
    CreateQueuedUninstall(container_id, app_id, app_name);
    return;
  }

  CreateRunningNotification(
      container_id,
      CrostiniPackageNotification::NotificationType::APPLICATION_UNINSTALL,
      app_name);

  UninstallApplication(*registration, app_id);
}

bool CrostiniPackageService::ContainerHasRunningOperation(
    const guest_os::GuestId& container_id) const {
  return base::Contains(running_notifications_, container_id);
}

bool CrostiniPackageService::ContainerHasQueuedOperation(
    const guest_os::GuestId& container_id) const {
  return (base::Contains(queued_installs_, container_id) &&
          !queued_installs_.at(container_id).empty()) ||
         (base::Contains(queued_uninstalls_, container_id) &&
          !queued_uninstalls_.at(container_id).empty());
}

void CrostiniPackageService::CreateRunningNotification(
    const guest_os::GuestId& container_id,
    CrostiniPackageNotification::NotificationType notification_type,
    const std::string& app_name) {
  {  // Scope limit for |it|, which will become invalid shortly.
    auto it = running_notifications_.find(container_id);
    if (it != running_notifications_.end()) {
      // We could reach this if the final progress update signal from a previous
      // operation doesn't get sent, so we wouldn't end up moving the
      // previous notification out of running_notifications_. Clear it out by
      // moving to finished_notifications_.
      LOG(ERROR) << "Notification for package operation already exists.";
      it->second->ForceAllowAutoHide();
      finished_notifications_.emplace_back(std::move(it->second));
      running_notifications_.erase(it);
    }
  }

  running_notifications_[container_id] =
      std::make_unique<CrostiniPackageNotification>(
          profile_, notification_type, PackageOperationStatus::RUNNING,
          container_id, base::UTF8ToUTF16(app_name), GetUniqueNotificationId(),
          this);
}

void CrostiniPackageService::CreateQueuedUninstall(
    const guest_os::GuestId& container_id,
    const std::string& app_id,
    const std::string& app_name) {
  queued_uninstalls_[container_id].emplace(
      app_id,
      std::make_unique<CrostiniPackageNotification>(
          profile_,
          CrostiniPackageNotification::NotificationType::APPLICATION_UNINSTALL,
          PackageOperationStatus::QUEUED, container_id,
          base::UTF8ToUTF16(app_name), GetUniqueNotificationId(), this));
}

void CrostiniPackageService::CreateQueuedInstall(
    const guest_os::GuestId& container_id,
    const std::string& package,
    CrostiniManager::InstallLinuxPackageCallback callback) {
  queued_installs_[container_id].emplace(
      package, std::move(callback),
      std::make_unique<CrostiniPackageNotification>(
          profile_,
          CrostiniPackageNotification::NotificationType::PACKAGE_INSTALL,
          PackageOperationStatus::QUEUED, container_id,
          /*app_name=*/std::u16string(), GetUniqueNotificationId(), this));
}

void CrostiniPackageService::UpdatePackageOperationStatus(
    const guest_os::GuestId& container_id,
    PackageOperationStatus status,
    int progress_percent,
    const std::string& error_message) {
  // Update the notification window, if any.
  auto it = running_notifications_.find(container_id);
  if (it == running_notifications_.end()) {
    LOG(ERROR) << container_id << " has no notification to update";
    return;
  }
  if (it->second == nullptr) {
    LOG(ERROR) << container_id << " has null notification pointer";
    running_notifications_.erase(it);
    return;
  }

  // If an operation has finished, but there are still app list updates pending,
  // don't finish the flow yet.
  if (status == PackageOperationStatus::SUCCEEDED &&
      has_pending_app_list_updates_.count(container_id)) {
    status = PackageOperationStatus::WAITING_FOR_APP_REGISTRY_UPDATE;
  }

  it->second->UpdateProgress(status, progress_percent, error_message);

  if (status == PackageOperationStatus::SUCCEEDED ||
      status == PackageOperationStatus::FAILED) {
    finished_notifications_.emplace_back(std::move(it->second));
    running_notifications_.erase(it);

    // Kick off the next operation if we just finished one.
    if (ContainerHasQueuedOperation(container_id)) {
      StartQueuedOperation(container_id);
    }
  }
  if (testing_state_change_callback_) {
    testing_state_change_callback_.Run(status);
  }
}

void CrostiniPackageService::OnPendingAppListUpdates(
    const guest_os::GuestId& container_id,
    int count) {
  if (count != 0) {
    has_pending_app_list_updates_.insert(container_id);
  } else {
    has_pending_app_list_updates_.erase(container_id);
  }

  auto it = running_notifications_.find(container_id);
  if (it != running_notifications_.end()) {
    if (it->second->GetOperationStatus() ==
            PackageOperationStatus::WAITING_FOR_APP_REGISTRY_UPDATE &&
        count == 0) {
      UpdatePackageOperationStatus(container_id,
                                   PackageOperationStatus::SUCCEEDED, 100);
    }
  }
}

void CrostiniPackageService::OnGetLinuxPackageInfo(
    const guest_os::GuestId& container_id,
    CrostiniManager::GetLinuxPackageInfoCallback callback,
    const LinuxPackageInfo& linux_package_info) {
  std::move(callback).Run(linux_package_info);
}

void CrostiniPackageService::OnInstallLinuxPackage(
    const guest_os::GuestId& container_id,
    CrostiniManager::InstallLinuxPackageCallback callback,
    CrostiniResult result) {
  std::move(callback).Run(result);
  if (result != CrostiniResult::SUCCESS) {
    UpdatePackageOperationStatus(container_id, PackageOperationStatus::FAILED,
                                 0);
  }
}

void CrostiniPackageService::UninstallApplication(
    const guest_os::GuestOsRegistryService::Registration& registration,
    const std::string& app_id) {
  const guest_os::GuestId container_id(registration.VmType(),
                                       registration.VmName(),
                                       registration.ContainerName());

  // Policies can change under us, and crostini may now be forbidden.
  if (!CrostiniFeatures::Get()->IsAllowedNow(profile_)) {
    LOG(ERROR) << "Can't uninstall because policy no longer allows Crostini";
    UpdatePackageOperationStatus(container_id, PackageOperationStatus::FAILED,
                                 0);
    return;
  }

  // If Crostini is not running, launch it. This is a no-op if Crostini is
  // already running.
  CrostiniManager::GetForProfile(profile_)->RestartCrostini(
      container_id,
      base::BindOnce(&CrostiniPackageService::OnCrostiniRunningForUninstall,
                     weak_ptr_factory_.GetWeakPtr(), container_id,
                     registration.DesktopFileId()));
}

void CrostiniPackageService::OnCrostiniRunningForUninstall(
    const guest_os::GuestId& container_id,
    const std::string& desktop_file_id,
    CrostiniResult result) {
  if (result != CrostiniResult::SUCCESS) {
    LOG(ERROR) << "Failed to launch Crostini; uninstall aborted";
    UpdatePackageOperationStatus(container_id, PackageOperationStatus::FAILED,
                                 0);
    return;
  }
  CrostiniManager::GetForProfile(profile_)->UninstallPackageOwningFile(
      container_id, desktop_file_id,
      base::BindOnce(&CrostiniPackageService::OnUninstallPackageOwningFile,
                     weak_ptr_factory_.GetWeakPtr(), container_id));
}

void CrostiniPackageService::OnUninstallPackageOwningFile(
    const guest_os::GuestId& container_id,
    CrostiniResult result) {
  if (result != CrostiniResult::SUCCESS) {
    // Let user know the uninstall failed.
    UpdatePackageOperationStatus(container_id, PackageOperationStatus::FAILED,
                                 0);
    return;
  }
  // Otherwise, just leave the notification alone in the "running" state.
  // SUCCESS just means we successfully *started* the uninstall.
}

void CrostiniPackageService::StartQueuedOperation(
    const guest_os::GuestId& container_id) {
  auto uninstall_queue_iter = queued_uninstalls_.find(container_id);
  if (uninstall_queue_iter != queued_uninstalls_.end() &&
      !uninstall_queue_iter->second.empty()) {
    std::string app_id;
    std::queue<QueuedUninstall>& uninstall_queue = uninstall_queue_iter->second;
    {  // Scope |next|; it becomes an invalid reference when we pop()
      QueuedUninstall& next = uninstall_queue.front();

      next.notification->UpdateProgress(PackageOperationStatus::RUNNING, 0);
      running_notifications_.emplace(container_id,
                                     std::move(next.notification));

      app_id = next.app_id;
      uninstall_queue.pop();  // Invalidates |next|
    }

    auto registration =
        guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_)
            ->GetRegistration(app_id);

    // It's possible that some other process has uninstalled this application
    // already. If this happens, we want to skip the notification directly to
    // the success state.
    if (registration.has_value()) {
      UninstallApplication(*registration, app_id);
    } else {
      // Note that this may call StartQueuedOperation, so we must allow for
      // potential re-entrancy.
      UpdatePackageOperationStatus(container_id,
                                   PackageOperationStatus::SUCCEEDED, 100);
    }

    // As recursive calls to StartQueuedOperation might delete |uninstall_queue|
    // and invalidate |uninstall_queue_iter| we must look it up again.
    uninstall_queue_iter = queued_uninstalls_.find(container_id);
    if (uninstall_queue_iter != queued_uninstalls_.end() &&
        uninstall_queue_iter->second.empty()) {
      // Clean up memory.
      queued_uninstalls_.erase(uninstall_queue_iter);
      // Invalidates |uninstall_queue_iter|.
    }
    return;
  }

  auto install_queue_iter = queued_installs_.find(container_id);
  if (install_queue_iter != queued_installs_.end() &&
      !install_queue_iter->second.empty()) {
    std::string package_path;
    CrostiniManager::InstallLinuxPackageCallback callback;

    std::queue<QueuedInstall>& install_queue = install_queue_iter->second;
    {  // Scope |next|; it becomes an invalid reference when we pop()
      QueuedInstall& next = install_queue.front();

      next.notification->UpdateProgress(PackageOperationStatus::RUNNING, 0);
      running_notifications_.emplace(container_id,
                                     std::move(next.notification));

      package_path = std::move(next.package_path);
      callback = std::move(next.callback);
      install_queue.pop();  // Invalidates |next|
    }

    CrostiniManager::GetForProfile(profile_)->InstallLinuxPackage(
        container_id, package_path,
        base::BindOnce(&CrostiniPackageService::OnInstallLinuxPackage,
                       weak_ptr_factory_.GetWeakPtr(), container_id,
                       std::move(callback)));

    // InstallLinuxPackage shouldn't be able to recursively call this method,
    // but as future proofing consider |install_queue_iter| to be invalidated
    // anyway.
    install_queue_iter = queued_installs_.find(container_id);
    if (install_queue_iter != queued_installs_.end() &&
        install_queue_iter->second.empty()) {
      // Clean up memory.
      queued_installs_.erase(install_queue_iter);
      // Invalidates |install_queue_iter|.
    }
    return;
  }

  NOTREACHED_IN_MIGRATION();
}

std::string CrostiniPackageService::GetUniqueNotificationId() {
  return base::StringPrintf("crostini_package_operation_%d",
                            next_notification_id_++);
}

CrostiniManager::RestartId CrostiniPackageService::GetRestartIdForTesting() {
  return restart_id_for_testing_;
}

// static
void CrostiniPackageService::EnsureFactoryBuilt() {
  CrostiniPackageServiceFactory::GetInstance();
}

}  // namespace crostini