| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // 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/bind.h" |
| #include "base/files/file.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_file.h" |
| #include "base/guid.h" |
| #include "base/strings/string_util.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.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/dbus/debug_daemon/debug_daemon_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 kSetupTimeHistogram[] = "PluginVm.SetupTime"; |
| |
| constexpr char kHomeDirectory[] = "/home"; |
| |
| chromeos::ConciergeClient* GetConciergeClient() { |
| return chromeos::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; |
| } |
| |
| PluginVmSetupResult BucketForCancelledInstall( |
| PluginVmInstaller::InstallingState installing_state) { |
| switch (installing_state) { |
| case PluginVmInstaller::InstallingState::kInactive: |
| NOTREACHED(); |
| [[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())) {} |
| |
| absl::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; |
| setup_start_tick_ = base::TimeTicks::Now(); |
| 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 absl::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::kDownloadingDlc: |
| case InstallingState::kStartingDispatcher: |
| // These can't be cancelled, so we wait for completion. For DLC, we also |
| // block progress callbacks. |
| return; |
| case InstallingState::kDownloadingImage: |
| CancelDownload(); |
| return; |
| case InstallingState::kImporting: |
| CancelImport(); |
| return; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| 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; |
| } |
| |
| RecordPluginVmImageDownloadedSizeHistogram(info.bytes_downloaded); |
| 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() |
| ->GetDictionary(prefs::kPluginVmImage) |
| ->FindKey(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_storage_location( |
| vm_tools::concierge::STORAGE_CRYPTOHOME_PLUGINVM); |
| request.set_vm_name(kPluginVmName); |
| |
| GetConciergeClient()->ListVmDisks( |
| std::move(request), base::BindOnce(&PluginVmInstaller::OnListVmDisks, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PluginVmInstaller::OnListVmDisks( |
| absl::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() == 1) { |
| RecordPluginVmSetupResultHistogram(PluginVmSetupResult::kVmAlreadyExists); |
| if (observer_) |
| observer_->OnVmExists(); |
| profile_->GetPrefs()->SetBoolean(prefs::kPluginVmImageExists, true); |
| InstallFinished(); |
| return; |
| } |
| |
| CheckDiskSpace(); |
| } |
| |
| void PluginVmInstaller::CheckDiskSpace() { |
| DCHECK_EQ(installing_state_, InstallingState::kCheckingForExistingVm); |
| UpdateInstallingState(InstallingState::kCheckingDiskSpace); |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock()}, |
| base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace, |
| base::FilePath(kHomeDirectory)), |
| base::BindOnce(&PluginVmInstaller::OnAvailableDiskSpace, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PluginVmInstaller::OnAvailableDiskSpace(int64_t bytes) { |
| if (state_ == State::kCancelling) { |
| CancelFinished(); |
| return; |
| } |
| |
| if (free_disk_space_for_testing_ != -1) |
| bytes = free_disk_space_for_testing_; |
| |
| if (bytes < 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; |
| } |
| |
| chromeos::DlcserviceClient::Get()->Install( |
| "pita", |
| 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( |
| const chromeos::DlcserviceClient::InstallResult& install_result) { |
| DCHECK_EQ(installing_state_, InstallingState::kDownloadingDlc); |
| if (state_ == State::kCancelling) { |
| CancelFinished(); |
| return; |
| } |
| |
| // If success, continue to the next state. |
| if (install_result.error == dlcservice::kErrorNone) { |
| RecordPluginVmDlcUseResultHistogram(PluginVmDlcUseResult::kDlcSuccess); |
| StartDispatcher(); |
| return; |
| } |
| |
| // At this point, PluginVM DLC download failed. |
| PluginVmDlcUseResult result = PluginVmDlcUseResult::kInternalDlcError; |
| FailureReason reason = FailureReason::DLC_INTERNAL; |
| |
| if (install_result.error == dlcservice::kErrorInvalidDlc) { |
| LOG(ERROR) << "PluginVM DLC is not supported, need to enable PluginVM DLC."; |
| result = PluginVmDlcUseResult::kInvalidDlcError; |
| reason = FailureReason::DLC_UNSUPPORTED; |
| } else if (install_result.error == dlcservice::kErrorBusy) { |
| LOG(ERROR) |
| << "PluginVM DLC is not able to be downloaded as dlcservice is busy."; |
| result = PluginVmDlcUseResult::kBusyDlcError; |
| reason = FailureReason::DLC_BUSY; |
| } else if (install_result.error == dlcservice::kErrorNeedReboot) { |
| LOG(ERROR) |
| << "Device has pending update and needs a reboot to use PluginVM DLC."; |
| result = PluginVmDlcUseResult::kNeedRebootDlcError; |
| reason = FailureReason::DLC_NEED_REBOOT; |
| } else if (install_result.error == dlcservice::kErrorAllocation) { |
| LOG(ERROR) << "Device needs to free space to use PluginVM DLC."; |
| result = PluginVmDlcUseResult::kNeedSpaceDlcError; |
| reason = FailureReason::DLC_NEED_SPACE; |
| } else if (install_result.error == dlcservice::kErrorNoImageFound) { |
| 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. |
| } else { |
| LOG(ERROR) << "Failed to download PluginVM DLC: " << install_result.error; |
| } |
| |
| 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; |
| absl::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::PostTaskAndReply( |
| FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()}, |
| base::BindOnce(&PluginVmInstaller::DetectImageType, |
| base::Unretained(this)), |
| base::BindOnce(&PluginVmInstaller::OnImageTypeDetected, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PluginVmInstaller::DetectImageType() { |
| creating_new_vm_ = IsIsoImage(downloaded_image_); |
| } |
| |
| void PluginVmInstaller::OnImageTypeDetected() { |
| 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(&PluginVmInstaller::PrepareFD, base::Unretained(this)), |
| base::BindOnce(&PluginVmInstaller::OnFDPrepared, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| absl::optional<base::ScopedFD> PluginVmInstaller::PrepareFD() { |
| // In case import has been cancelled meantime. |
| if (state_ != State::kInstalling) |
| return absl::nullopt; |
| |
| base::File file(downloaded_image_, |
| base::File::FLAG_OPEN | base::File::FLAG_READ); |
| if (!file.IsValid()) { |
| LOG(ERROR) << "Failed to open " << downloaded_image_.value(); |
| return absl::nullopt; |
| } |
| |
| return base::ScopedFD(file.TakePlatformFile()); |
| } |
| |
| void PluginVmInstaller::OnFDPrepared(absl::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(absl::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(https://crbug.com/966398): 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( |
| absl::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(absl::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( |
| absl::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(); |
| } |
| |
| 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(); |
| base::UmaHistogramLongTimes(kSetupTimeHistogram, |
| base::TimeTicks::Now() - setup_start_tick_); |
| 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( |
| absl::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() |
| ->GetDictionary(prefs::kPluginVmImage) |
| ->FindKey(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::GenerateGUID(); |
| 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"; |
| |
| // 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 |