// Copyright 2019 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/plugin_vm/plugin_vm_installer.h"
#include <memory>
#include <string>
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/strings/string_util.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/uuid.h"
#include "chrome/browser/ash/guest_os/guest_os_dlc_helper.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_drive_image_download_service.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_features.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_license_checker.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_manager.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_manager_factory.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_metrics_util.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_pref_names.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/download/background_download_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
#include "chromeos/ash/components/dbus/spaced/spaced_client.h"
#include "components/download/public/background_service/background_download_service.h"
#include "components/download/public/background_service/download_metadata.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/device_service.h"
#include "content/public/browser/network_service_instance.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/device/public/mojom/wake_lock_provider.mojom.h"
// This file contains VLOG logging to aid debugging tast tests.
#define LOG_FUNCTION_CALL() \
VLOG(2) << "PluginVmInstaller::" << __func__ << " called"
namespace plugin_vm {
namespace {
constexpr int64_t kBytesPerGigabyte = 1024 * 1024 * 1024;
// Size to use for calculating progress when the actual size isn't available.
constexpr int64_t kDownloadSizeFallbackEstimate = 15LL * kBytesPerGigabyte;
constexpr char kFailureReasonHistogram[] = "PluginVm.SetupFailureReason";
constexpr char kHomeDirectory[] = "/home/chronos/user";
ash::ConciergeClient* GetConciergeClient() {
return ash::ConciergeClient::Get();
}
constexpr char kIsoSignature[] = "CD001";
constexpr int64_t kIsoOffsets[] = {0x8001, 0x8801, 0x9001};
bool IsIsoImage(const base::FilePath& image) {
base::File file(image, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!file.IsValid()) {
LOG(ERROR) << "Failed to open " << image.value();
return false;
}
std::vector<uint8_t> data(strlen(kIsoSignature));
for (auto offset : kIsoOffsets) {
if (file.ReadAndCheck(offset, data) &&
std::string(data.begin(), data.end()) == kIsoSignature) {
return true;
}
}
return false;
}
std::optional<base::ScopedFD> PrepareFD(const base::FilePath& image) {
base::File file(image, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!file.IsValid()) {
LOG(ERROR) << "Failed to open " << image.value();
return std::nullopt;
}
return base::ScopedFD(file.TakePlatformFile());
}
PluginVmSetupResult BucketForCancelledInstall(
PluginVmInstaller::InstallingState installing_state) {
switch (installing_state) {
case PluginVmInstaller::InstallingState::kInactive:
NOTREACHED_IN_MIGRATION();
[[fallthrough]];
case PluginVmInstaller::InstallingState::kCheckingLicense:
return PluginVmSetupResult::kUserCancelledValidatingLicense;
case PluginVmInstaller::InstallingState::kCheckingDiskSpace:
return PluginVmSetupResult::kUserCancelledCheckingDiskSpace;
case PluginVmInstaller::InstallingState::kDownloadingDlc:
return PluginVmSetupResult::kUserCancelledDownloadingPluginVmDlc;
case PluginVmInstaller::InstallingState::kStartingDispatcher:
return PluginVmSetupResult::kUserCancelledStartingDispatcher;
case PluginVmInstaller::InstallingState::kCheckingForExistingVm:
return PluginVmSetupResult::kUserCancelledCheckingForExistingVm;
case PluginVmInstaller::InstallingState::kDownloadingImage:
return PluginVmSetupResult::kUserCancelledDownloadingPluginVmImage;
case PluginVmInstaller::InstallingState::kImporting:
return PluginVmSetupResult::kUserCancelledImportingPluginVmImage;
}
}
} // namespace
PluginVmInstaller::PluginVmInstaller(Profile* profile)
: profile_(profile),
download_service_(BackgroundDownloadServiceFactory::GetForKey(
profile->GetProfileKey())) {}
std::optional<PluginVmInstaller::FailureReason> PluginVmInstaller::Start() {
LOG_FUNCTION_CALL();
if (IsProcessing()) {
LOG(ERROR) << "Download of a PluginVm image couldn't be started as"
<< " another PluginVm image is currently being processed "
<< "in state " << GetStateName(state_) << ", "
<< GetInstallingStateName(installing_state_);
return FailureReason::OPERATION_IN_PROGRESS;
}
// Defensive check preventing any download attempts when PluginVm is
// not allowed to run (this might happen in rare cases if PluginVm has
// been disabled but the installer icon is still visible).
if (!PluginVmFeatures::Get()->IsAllowed(profile_)) {
LOG(ERROR) << "Download of PluginVm image cannot be started because "
<< "the user is not allowed to run PluginVm";
return FailureReason::NOT_ALLOWED;
}
if (content::GetNetworkConnectionTracker()->IsOffline()) {
return FailureReason::OFFLINE;
}
// Reset camera/mic permissions, we don't want it to persist across
// re-installation.
profile_->GetPrefs()->SetBoolean(prefs::kPluginVmCameraAllowed, false);
profile_->GetPrefs()->SetBoolean(prefs::kPluginVmMicAllowed, false);
// Request wake lock when state_ goes to kInstalling, and cancel it when state
// goes back to kIdle.
GetWakeLock()->RequestWakeLock();
state_ = State::kInstalling;
progress_ = 0;
// Perform the first step asynchronously to ensure OnError() isn't called
// before Start() returns.
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&PluginVmInstaller::CheckLicense,
weak_ptr_factory_.GetWeakPtr()));
return std::nullopt;
}
void PluginVmInstaller::Cancel() {
LOG_FUNCTION_CALL();
if (state_ != State::kInstalling) {
RecordPluginVmSetupResultHistogram(
PluginVmSetupResult::kUserCancelledWithoutStarting);
return;
}
RecordPluginVmSetupResultHistogram(
BucketForCancelledInstall(installing_state_));
state_ = State::kCancelling;
switch (installing_state_) {
case InstallingState::kCheckingLicense:
case InstallingState::kCheckingDiskSpace:
case InstallingState::kCheckingForExistingVm:
case InstallingState::kStartingDispatcher:
// These can't be cancelled, so we wait for completion.
return;
case InstallingState::kDownloadingDlc:
// For DLC, we also block progress callbacks.
dlc_installation_.reset();
return;
case InstallingState::kDownloadingImage:
CancelDownload();
return;
case InstallingState::kImporting:
CancelImport();
return;
default:
NOTREACHED_IN_MIGRATION();
}
}
bool PluginVmInstaller::IsProcessing() {
return state_ != State::kIdle;
}
void PluginVmInstaller::SetObserver(Observer* observer) {
observer_ = observer;
}
void PluginVmInstaller::RemoveObserver() {
observer_ = nullptr;
}
std::string PluginVmInstaller::GetCurrentDownloadGuid() {
return current_download_guid_;
}
void PluginVmInstaller::OnDownloadStarted() {}
void PluginVmInstaller::OnDownloadProgressUpdated(uint64_t bytes_downloaded,
int64_t content_length) {
DCHECK_EQ(installing_state_, InstallingState::kDownloadingImage);
if (expected_image_size_ == kImageSizeUnknown) {
if (content_length > 0) {
expected_image_size_ = content_length;
}
} else if (expected_image_size_ > 0) {
if (content_length != expected_image_size_) {
expected_image_size_ = kImageSizeError;
}
}
if (observer_) {
observer_->OnDownloadProgressUpdated(bytes_downloaded, content_length);
}
if (content_length <= 0) {
content_length = kDownloadSizeFallbackEstimate;
}
UpdateProgress(
std::min(1., static_cast<double>(bytes_downloaded) / content_length));
}
void PluginVmInstaller::OnDownloadCompleted(
const download::CompletionInfo& info) {
downloaded_image_ = info.path;
downloaded_image_size_ = info.bytes_downloaded;
current_download_guid_.clear();
if (downloaded_image_for_testing_) {
downloaded_image_ = downloaded_image_for_testing_.value();
}
if (!VerifyDownload(info.hash256)) {
LOG(ERROR) << "Expected image size: " << expected_image_size_
<< ", downloaded image size: " << downloaded_image_size_;
if (expected_image_size_ == kImageSizeUnknown ||
expected_image_size_ == downloaded_image_size_) {
OnDownloadFailed(FailureReason::HASH_MISMATCH);
} else {
OnDownloadFailed(FailureReason::DOWNLOAD_SIZE_MISMATCH);
}
return;
}
StartImport();
}
void PluginVmInstaller::OnDownloadFailed(FailureReason reason) {
RemoveTemporaryImageIfExists();
current_download_guid_.clear();
if (using_drive_download_service_) {
drive_download_service_->ResetState();
using_drive_download_service_ = false;
}
InstallFailed(reason);
}
void PluginVmInstaller::OnDiskImageProgress(
const vm_tools::concierge::DiskImageStatusResponse& signal) {
if (signal.command_uuid() != current_import_command_uuid_) {
return;
}
const uint64_t percent_completed = signal.progress();
const vm_tools::concierge::DiskImageStatus status = signal.status();
switch (status) {
case vm_tools::concierge::DiskImageStatus::DISK_STATUS_CREATED:
VLOG(1) << "Disk image status indicates that importing is done.";
RequestFinalStatus();
return;
case vm_tools::concierge::DiskImageStatus::DISK_STATUS_IN_PROGRESS:
UpdateProgress(percent_completed / 100.);
return;
case vm_tools::concierge::DiskImageStatus::DISK_STATUS_NOT_ENOUGH_SPACE:
LOG(ERROR) << "Disk image import signals out of space condition with "
"current progress: "
<< percent_completed;
OnImported(FailureReason::OUT_OF_DISK_SPACE);
return;
default:
LOG(ERROR) << "Disk image status signal has status: " << status
<< " with error message: " << signal.failure_reason()
<< " and current progress: " << percent_completed;
OnImported(FailureReason::UNEXPECTED_DISK_IMAGE_STATUS);
return;
}
}
bool PluginVmInstaller::VerifyDownload(
const std::string& downloaded_archive_hash) {
if (downloaded_archive_hash.empty()) {
LOG(ERROR) << "No hash found for downloaded PluginVm image archive";
return false;
}
const base::Value* plugin_vm_image_hash_ptr =
profile_->GetPrefs()
->GetDict(prefs::kPluginVmImage)
.Find(prefs::kPluginVmImageHashKeyName);
if (!plugin_vm_image_hash_ptr) {
LOG(ERROR) << "Hash of PluginVm image is not specified";
return false;
}
std::string plugin_vm_image_hash = plugin_vm_image_hash_ptr->GetString();
if (!base::EqualsCaseInsensitiveASCII(plugin_vm_image_hash,
downloaded_archive_hash)) {
LOG(ERROR) << "Downloaded PluginVm image archive hash ("
<< downloaded_archive_hash << ") doesn't match "
<< "hash specified by the PluginVmImage policy ("
<< plugin_vm_image_hash << ")";
return false;
}
return true;
}
int64_t PluginVmInstaller::RequiredFreeDiskSpace() {
return static_cast<int64_t>(profile_->GetPrefs()->GetInteger(
prefs::kPluginVmRequiredFreeDiskSpaceGB)) *
kBytesPerGigabyte;
}
void PluginVmInstaller::SetDownloadServiceForTesting(
download::BackgroundDownloadService* download_service) {
download_service_ = download_service;
}
void PluginVmInstaller::SetDownloadedImageForTesting(
const base::FilePath& downloaded_image) {
downloaded_image_for_testing_ = downloaded_image;
}
void PluginVmInstaller::SetDriveDownloadServiceForTesting(
std::unique_ptr<PluginVmDriveImageDownloadService> drive_download_service) {
drive_download_service_ = std::move(drive_download_service);
}
PluginVmInstaller::~PluginVmInstaller() = default;
void PluginVmInstaller::CheckLicense() {
UpdateInstallingState(InstallingState::kCheckingLicense);
if (skip_license_check_for_testing_) {
OnLicenseChecked(true);
return;
}
license_checker_ = std::make_unique<PluginVmLicenseChecker>(profile_);
license_checker_->CheckLicense(base::BindOnce(
&PluginVmInstaller::OnLicenseChecked, weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnLicenseChecked(bool license_is_valid) {
if (state_ == State::kCancelling) {
CancelFinished();
return;
}
if (!license_is_valid) {
LOG(ERROR) << "Install of a PluginVm image couldn't be started as"
<< " there is not a valid license associated with the user.";
InstallFailed(FailureReason::INVALID_LICENSE);
return;
}
CheckForExistingVm();
}
void PluginVmInstaller::CheckForExistingVm() {
DCHECK_EQ(installing_state_, InstallingState::kCheckingLicense);
UpdateInstallingState(InstallingState::kCheckingForExistingVm);
GetConciergeClient()->WaitForServiceToBeAvailable(
base::BindOnce(&PluginVmInstaller::OnConciergeAvailable,
weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnConciergeAvailable(bool success) {
if (!success) {
LOG(ERROR) << "Concierge did not become available";
OnImported(FailureReason::CONCIERGE_NOT_AVAILABLE);
return;
}
vm_tools::concierge::ListVmDisksRequest request;
request.set_cryptohome_id(
ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
request.set_all_locations(true);
request.set_vm_name(kPluginVmName);
GetConciergeClient()->ListVmDisks(
std::move(request), base::BindOnce(&PluginVmInstaller::OnListVmDisks,
weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnListVmDisks(
std::optional<vm_tools::concierge::ListVmDisksResponse> response) {
if (state_ == State::kCancelling) {
CancelFinished();
return;
}
if (!response || !response->success()) {
LOG(ERROR) << "Failed to list VM disks: "
<< (response ? response->failure_reason() : "[Empty response]");
InstallFailed(FailureReason::LIST_VM_DISKS_FAILED);
return;
}
if (response->images_size() > 0) {
auto& image = response->images(0);
if (image.storage_location() ==
vm_tools::concierge::STORAGE_CRYPTOHOME_PLUGINVM) {
RecordPluginVmSetupResultHistogram(PluginVmSetupResult::kVmAlreadyExists);
if (observer_) {
observer_->OnVmExists();
}
profile_->GetPrefs()->SetBoolean(prefs::kPluginVmImageExists, true);
InstallFinished();
} else {
LOG(ERROR) << "VM " << image.name() << " exists, but in wrong location";
InstallFailed(FailureReason::EXISTING_IMAGE_INVALID);
}
return;
}
CheckDiskSpace();
}
void PluginVmInstaller::CheckDiskSpace() {
DCHECK_EQ(installing_state_, InstallingState::kCheckingForExistingVm);
UpdateInstallingState(InstallingState::kCheckingDiskSpace);
ash::SpacedClient::Get()->GetFreeDiskSpace(
kHomeDirectory, base::BindOnce(&PluginVmInstaller::OnAvailableDiskSpace,
weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnAvailableDiskSpace(std::optional<int64_t> bytes) {
if (state_ == State::kCancelling) {
CancelFinished();
return;
}
if (free_disk_space_for_testing_ != -1) {
bytes = std::optional<int64_t>(free_disk_space_for_testing_);
}
if (!bytes.has_value() || bytes.value() < RequiredFreeDiskSpace()) {
InstallFailed(FailureReason::INSUFFICIENT_DISK_SPACE);
return;
}
StartDlcDownload();
}
void PluginVmInstaller::StartDlcDownload() {
LOG_FUNCTION_CALL();
DCHECK_EQ(installing_state_, InstallingState::kCheckingDiskSpace);
UpdateInstallingState(InstallingState::kDownloadingDlc);
if (!GetPluginVmImageDownloadUrl().is_valid()) {
InstallFailed(FailureReason::INVALID_IMAGE_URL);
return;
}
dlc_installation_ = std::make_unique<guest_os::GuestOsDlcInstallation>(
kPitaDlc,
base::BindOnce(&PluginVmInstaller::OnDlcDownloadCompleted,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&PluginVmInstaller::OnDlcDownloadProgressUpdated,
weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnDlcDownloadProgressUpdated(double progress) {
DCHECK_EQ(installing_state_, InstallingState::kDownloadingDlc);
if (state_ == State::kCancelling) {
return;
}
UpdateProgress(progress);
}
void PluginVmInstaller::OnDlcDownloadCompleted(
guest_os::GuestOsDlcInstallation::Result install_result) {
DCHECK_EQ(installing_state_, InstallingState::kDownloadingDlc);
dlc_installation_.reset();
// If success, continue to the next state.
if (install_result.has_value()) {
RecordPluginVmDlcUseResultHistogram(PluginVmDlcUseResult::kDlcSuccess);
StartDispatcher();
return;
}
// At this point, PluginVM DLC download failed.
PluginVmDlcUseResult result = PluginVmDlcUseResult::kInternalDlcError;
FailureReason reason = FailureReason::DLC_INTERNAL;
switch (install_result.error()) {
case guest_os::GuestOsDlcInstallation::Error::Cancelled:
DCHECK(state_ == State::kCancelling);
CancelFinished();
return;
case guest_os::GuestOsDlcInstallation::Error::Invalid:
LOG(ERROR)
<< "PluginVM DLC is not supported, need to enable PluginVM DLC.";
result = PluginVmDlcUseResult::kInvalidDlcError;
reason = FailureReason::DLC_UNSUPPORTED;
break;
case guest_os::GuestOsDlcInstallation::Error::Busy:
LOG(ERROR)
<< "PluginVM DLC is not able to be downloaded as dlcservice is busy.";
result = PluginVmDlcUseResult::kBusyDlcError;
reason = FailureReason::DLC_BUSY;
break;
case guest_os::GuestOsDlcInstallation::Error::NeedReboot:
LOG(ERROR) << "Device has pending update and needs a reboot to use "
"PluginVM DLC.";
result = PluginVmDlcUseResult::kNeedRebootDlcError;
reason = FailureReason::DLC_NEED_REBOOT;
break;
case guest_os::GuestOsDlcInstallation::Error::DiskFull:
LOG(ERROR) << "Device needs to free space to use PluginVM DLC.";
result = PluginVmDlcUseResult::kNeedSpaceDlcError;
reason = FailureReason::DLC_NEED_SPACE;
break;
case guest_os::GuestOsDlcInstallation::Error::NeedUpdate:
LOG(ERROR) << "The PluginVM DLC could not be found in the server."
<< "The version the OS is on is probably not live.";
result = PluginVmDlcUseResult::kNoImageFoundDlcError;
// Keep using the reason `FailureReason::DLC_INTERNAL`, but distinguish so
// developers can see why it wasn't updated as well as for metrics
// reporting.
break;
case guest_os::GuestOsDlcInstallation::Error::Offline:
case guest_os::GuestOsDlcInstallation::Error::Internal:
case guest_os::GuestOsDlcInstallation::Error::UnknownFailure:
LOG(ERROR) << "Failed to download PluginVM DLC: "
<< install_result.error();
break;
}
RecordPluginVmDlcUseResultHistogram(result);
InstallFailed(reason);
}
void PluginVmInstaller::StartDispatcher() {
LOG_FUNCTION_CALL();
DCHECK_EQ(installing_state_, InstallingState::kDownloadingDlc);
UpdateInstallingState(InstallingState::kStartingDispatcher);
PluginVmManagerFactory::GetForProfile(profile_)->StartDispatcher(
base::BindOnce(&PluginVmInstaller::OnDispatcherStarted,
weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnDispatcherStarted(bool success) {
if (state_ == State::kCancelling) {
CancelFinished();
return;
}
if (!success) {
InstallFailed(FailureReason::DISPATCHER_NOT_AVAILABLE);
return;
}
StartDownload();
}
void PluginVmInstaller::StartDownload() {
DCHECK_EQ(installing_state_, InstallingState::kStartingDispatcher);
UpdateInstallingState(InstallingState::kDownloadingImage);
UpdateProgress(/*state_progress=*/0);
GURL url = GetPluginVmImageDownloadUrl();
// This may have changed since running StartDlcDownload.
if (!url.is_valid()) {
InstallFailed(FailureReason::INVALID_IMAGE_URL);
return;
}
expected_image_size_ = kImageSizeUnknown;
downloaded_image_size_ = kImageSizeUnknown;
std::optional<std::string> drive_id = GetIdFromDriveUrl(url);
using_drive_download_service_ = drive_id.has_value();
if (using_drive_download_service_) {
if (!drive_download_service_) {
drive_download_service_ =
std::make_unique<PluginVmDriveImageDownloadService>(this, profile_);
} else {
drive_download_service_->ResetState();
}
drive_download_service_->StartDownload(drive_id.value());
} else {
download_service_->StartDownload(GetDownloadParams(url));
}
}
void PluginVmInstaller::OnStartDownload(
const std::string& download_guid,
download::DownloadParams::StartResult start_result) {
if (start_result == download::DownloadParams::ACCEPTED) {
current_download_guid_ = download_guid;
} else {
OnDownloadFailed(FailureReason::DOWNLOAD_FAILED_UNKNOWN);
}
}
void PluginVmInstaller::StartImport() {
LOG_FUNCTION_CALL();
DCHECK_EQ(installing_state_, InstallingState::kDownloadingImage);
UpdateInstallingState(InstallingState::kImporting);
UpdateProgress(/*state_progress=*/0);
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(&IsIsoImage, downloaded_image_),
base::BindOnce(&PluginVmInstaller::OnImageTypeDetected,
weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnImageTypeDetected(bool is_iso_image) {
creating_new_vm_ = is_iso_image;
if (!GetConciergeClient()->IsDiskImageProgressSignalConnected()) {
LOG(ERROR) << "Disk image progress signal is not connected";
OnImported(FailureReason::SIGNAL_NOT_CONNECTED);
return;
}
GetConciergeClient()->AddDiskImageObserver(this);
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(&PrepareFD, downloaded_image_),
base::BindOnce(&PluginVmInstaller::OnFDPrepared,
weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnFDPrepared(std::optional<base::ScopedFD> maybeFd) {
// In case import has been cancelled meantime.
if (state_ != State::kInstalling) {
return;
}
if (!maybeFd.has_value()) {
LOG(ERROR) << "Could not open downloaded image";
OnImported(FailureReason::COULD_NOT_OPEN_IMAGE);
return;
}
base::ScopedFD fd(std::move(maybeFd.value()));
if (creating_new_vm_) {
vm_tools::concierge::CreateDiskImageRequest request;
request.set_cryptohome_id(
ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
request.set_vm_name(kPluginVmName);
request.set_storage_location(
vm_tools::concierge::STORAGE_CRYPTOHOME_PLUGINVM);
request.set_source_size(downloaded_image_size_);
VLOG(1) << "Making call to concierge to set up VM from an ISO";
GetConciergeClient()->CreateDiskImageWithFd(
std::move(fd), request,
base::BindOnce(&PluginVmInstaller::OnImportDiskImage<
vm_tools::concierge::CreateDiskImageResponse>,
weak_ptr_factory_.GetWeakPtr()));
} else {
vm_tools::concierge::ImportDiskImageRequest request;
request.set_cryptohome_id(
ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
request.set_vm_name(kPluginVmName);
request.set_storage_location(
vm_tools::concierge::STORAGE_CRYPTOHOME_PLUGINVM);
request.set_source_size(downloaded_image_size_);
VLOG(1) << "Making call to concierge to import disk image";
GetConciergeClient()->ImportDiskImage(
std::move(fd), request,
base::BindOnce(&PluginVmInstaller::OnImportDiskImage<
vm_tools::concierge::ImportDiskImageResponse>,
weak_ptr_factory_.GetWeakPtr()));
}
}
template <typename ReplyType>
void PluginVmInstaller::OnImportDiskImage(std::optional<ReplyType> reply) {
if (!reply.has_value()) {
LOG(ERROR) << "Could not retrieve response from Create/ImportDiskImage "
<< "call to concierge";
OnImported(FailureReason::INVALID_IMPORT_RESPONSE);
return;
}
ReplyType response = reply.value();
switch (response.status()) {
case vm_tools::concierge::DiskImageStatus::DISK_STATUS_IN_PROGRESS:
VLOG(1) << "Disk image creation/import is now in progress";
current_import_command_uuid_ = response.command_uuid();
// Image in progress. Waiting for progress signals...
// TODO(crbug.com/41460680): think about adding a timeout here,
// i.e. what happens if concierge dies and does not report any signal
// back, not even an error signal. Right now, the user would see
// the "Configuring Plugin VM" screen forever. Maybe that's OK
// at this stage though.
break;
case vm_tools::concierge::DiskImageStatus::DISK_STATUS_NOT_ENOUGH_SPACE:
LOG(ERROR) << "Disk image import operation ran out of disk space";
OnImported(FailureReason::OUT_OF_DISK_SPACE);
break;
default:
LOG(ERROR) << "Disk image is not in progress. Status: "
<< response.status() << ", " << response.failure_reason();
OnImported(FailureReason::UNEXPECTED_DISK_IMAGE_STATUS);
break;
}
}
void PluginVmInstaller::RequestFinalStatus() {
vm_tools::concierge::DiskImageStatusRequest status_request;
status_request.set_command_uuid(current_import_command_uuid_);
GetConciergeClient()->DiskImageStatus(
status_request, base::BindOnce(&PluginVmInstaller::OnFinalDiskImageStatus,
weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnFinalDiskImageStatus(
std::optional<vm_tools::concierge::DiskImageStatusResponse> reply) {
if (!reply.has_value()) {
LOG(ERROR) << "Could not retrieve response from DiskImageStatus call to "
<< "concierge";
OnImported(FailureReason::INVALID_DISK_IMAGE_STATUS_RESPONSE);
return;
}
vm_tools::concierge::DiskImageStatusResponse response = reply.value();
DCHECK(response.command_uuid() == current_import_command_uuid_);
switch (response.status()) {
case vm_tools::concierge::DiskImageStatus::DISK_STATUS_CREATED:
OnImported(std::nullopt);
break;
case vm_tools::concierge::DiskImageStatus::DISK_STATUS_NOT_ENOUGH_SPACE:
LOG(ERROR) << "Disk image import operation ran out of disk space "
<< "with current progress: " << response.progress();
OnImported(FailureReason::OUT_OF_DISK_SPACE);
break;
default:
LOG(ERROR) << "Disk image is not created. Status: " << response.status()
<< ", " << response.failure_reason();
OnImported(FailureReason::IMAGE_IMPORT_FAILED);
break;
}
}
void PluginVmInstaller::OnImported(
std::optional<FailureReason> failure_reason) {
LOG_FUNCTION_CALL();
GetConciergeClient()->RemoveDiskImageObserver(this);
RemoveTemporaryImageIfExists();
current_import_command_uuid_.clear();
if (failure_reason) {
if (creating_new_vm_) {
LOG(ERROR) << "New VM creation failed";
} else {
LOG(ERROR) << "Image import failed";
}
InstallFailed(*failure_reason);
return;
}
profile_->GetPrefs()->SetBoolean(prefs::kPluginVmImageExists, true);
RecordPluginVmSetupResultHistogram(PluginVmSetupResult::kSuccess);
if (observer_) {
if (creating_new_vm_) {
observer_->OnCreated();
} else {
observer_->OnImported();
}
}
InstallFinished();
}
void PluginVmInstaller::UpdateInstallingState(
InstallingState installing_state) {
LOG_FUNCTION_CALL() << " with state "
<< GetInstallingStateName(installing_state);
DCHECK_NE(installing_state, InstallingState::kInactive);
installing_state_ = installing_state;
observer_->OnStateUpdated(installing_state_);
}
void PluginVmInstaller::UpdateProgress(double state_progress) {
DCHECK_EQ(state_, State::kInstalling);
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::kDownloadingDlc:
start_range = 0;
end_range = 0.01;
break;
case InstallingState::kDownloadingImage:
start_range = 0.01;
end_range = 0.45;
break;
case InstallingState::kImporting:
start_range = 0.45;
end_range = 1;
break;
default:
// Other states take a negligible amount of time so we don't send progress
// updates.
NOTREACHED_IN_MIGRATION();
}
double new_progress =
start_range + (end_range - start_range) * state_progress;
if (new_progress < progress_) {
LOG(ERROR) << "Progress went backwards from " << progress_ << " to "
<< new_progress;
return;
}
progress_ = new_progress;
if (observer_) {
observer_->OnProgressUpdated(new_progress);
}
}
void PluginVmInstaller::InstallFailed(FailureReason reason) {
LOG_FUNCTION_CALL() << " with failure reason " << static_cast<int>(reason);
state_ = State::kIdle;
GetWakeLock()->CancelWakeLock();
installing_state_ = InstallingState::kInactive;
base::UmaHistogramEnumeration(kFailureReasonHistogram, reason);
RecordPluginVmSetupResultHistogram(PluginVmSetupResult::kError);
if (observer_) {
observer_->OnError(reason);
}
}
void PluginVmInstaller::InstallFinished() {
LOG_FUNCTION_CALL();
state_ = State::kIdle;
GetWakeLock()->CancelWakeLock();
installing_state_ = InstallingState::kInactive;
}
void PluginVmInstaller::CancelDownload() {
if (using_drive_download_service_) {
DCHECK(drive_download_service_);
drive_download_service_->CancelDownload();
} else {
download_service_->CancelDownload(current_download_guid_);
current_download_guid_.clear();
}
CancelFinished();
}
void PluginVmInstaller::CancelImport() {
VLOG(1) << "Cancelling disk image import with command_uuid: "
<< current_import_command_uuid_;
vm_tools::concierge::CancelDiskImageRequest request;
request.set_command_uuid(current_import_command_uuid_);
GetConciergeClient()->CancelDiskImageOperation(
request, base::BindOnce(&PluginVmInstaller::OnImportDiskImageCancelled,
weak_ptr_factory_.GetWeakPtr()));
}
void PluginVmInstaller::OnImportDiskImageCancelled(
std::optional<vm_tools::concierge::CancelDiskImageResponse> reply) {
DCHECK_EQ(state_, State::kCancelling);
DCHECK_EQ(installing_state_, InstallingState::kImporting);
RemoveTemporaryImageIfExists();
if (!reply.has_value()) {
LOG(ERROR) << "Could not retrieve response from CancelDiskImageOperation "
<< "call to concierge";
CancelFinished();
return;
}
vm_tools::concierge::CancelDiskImageResponse response = reply.value();
if (response.success()) {
VLOG(1) << "Import disk image request has been cancelled successfully";
} else {
LOG(ERROR) << "Import disk image request failed to be cancelled, "
<< response.failure_reason();
}
CancelFinished();
}
void PluginVmInstaller::CancelFinished() {
DCHECK_EQ(state_, State::kCancelling);
state_ = State::kIdle;
GetWakeLock()->CancelWakeLock();
installing_state_ = InstallingState::kInactive;
if (observer_) {
observer_->OnCancelFinished();
}
}
std::string PluginVmInstaller::GetStateName(State state) {
switch (state) {
case State::kIdle:
return "kIdle";
case State::kInstalling:
return "kInstalling";
case State::kCancelling:
return "kCancelling";
}
}
std::string PluginVmInstaller::GetInstallingStateName(InstallingState state) {
switch (state) {
case InstallingState::kInactive:
return "kInactive";
case InstallingState::kCheckingDiskSpace:
return "kCheckingDiskSpace";
case InstallingState::kCheckingForExistingVm:
return "kCheckingForExistingVm";
case InstallingState::kDownloadingDlc:
return "kDownloadingDlc";
case InstallingState::kStartingDispatcher:
return "kStartingDispatcher";
case InstallingState::kDownloadingImage:
return "kDownloadingImage";
case InstallingState::kImporting:
return "kImporting";
case InstallingState::kCheckingLicense:
return "kCheckingLicense";
}
}
GURL PluginVmInstaller::GetPluginVmImageDownloadUrl() {
const base::Value* url_ptr = profile_->GetPrefs()
->GetDict(prefs::kPluginVmImage)
.Find(prefs::kPluginVmImageUrlKeyName);
if (!url_ptr) {
LOG(ERROR) << "Url to PluginVm image is not specified";
return GURL();
}
return GURL(url_ptr->GetString());
}
download::DownloadParams PluginVmInstaller::GetDownloadParams(const GURL& url) {
download::DownloadParams params;
// DownloadParams
params.client = download::DownloadClient::PLUGIN_VM_IMAGE;
params.guid = base::Uuid::GenerateRandomV4().AsLowercaseString();
params.callback = base::BindRepeating(&PluginVmInstaller::OnStartDownload,
weak_ptr_factory_.GetWeakPtr());
params.traffic_annotation = net::MutableNetworkTrafficAnnotationTag(
kPluginVmNetworkTrafficAnnotation);
// RequestParams
params.request_params.url = url;
params.request_params.method = "GET";
// Disable Safe Browsing/checks because the download is system-initiated,
// the target is specified via enterprise policy, and contents will be
// validated by comparing hashes.
params.request_params.require_safety_checks = false;
// SchedulingParams
// User initiates download by clicking on PluginVm icon so priorities should
// be the highest.
params.scheduling_params.priority = download::SchedulingParams::Priority::UI;
params.scheduling_params.battery_requirements =
download::SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE;
params.scheduling_params.network_requirements =
download::SchedulingParams::NetworkRequirements::NONE;
return params;
}
void PluginVmInstaller::RemoveTemporaryImageIfExists() {
if (using_drive_download_service_) {
drive_download_service_->RemoveTemporaryArchive(
base::BindOnce(&PluginVmInstaller::OnTemporaryImageRemoved,
weak_ptr_factory_.GetWeakPtr()));
} else if (!downloaded_image_.empty()) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(&base::DeleteFile, downloaded_image_),
base::BindOnce(&PluginVmInstaller::OnTemporaryImageRemoved,
weak_ptr_factory_.GetWeakPtr()));
}
}
void PluginVmInstaller::OnTemporaryImageRemoved(bool success) {
if (!success) {
LOG(ERROR) << "Downloaded PluginVm image located in "
<< downloaded_image_.value() << " failed to be deleted";
return;
}
downloaded_image_.clear();
creating_new_vm_ = false;
}
device::mojom::WakeLock* PluginVmInstaller::GetWakeLock() {
if (!wake_lock_) {
mojo::Remote<device::mojom::WakeLockProvider> wake_lock_provider;
content::GetDeviceService().BindWakeLockProvider(
wake_lock_provider.BindNewPipeAndPassReceiver());
wake_lock_provider->GetWakeLockWithoutContext(
device::mojom::WakeLockType::kPreventAppSuspension,
device::mojom::WakeLockReason::kOther, "Plugin VM Installer",
wake_lock_.BindNewPipeAndPassReceiver());
}
return wake_lock_.get();
}
} // namespace plugin_vm
#undef LOG_FUNCTION_CALL