| // 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/chromeos/crostini/crostini_export_import.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/files/file_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/post_task.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/chromeos/crostini/crostini_manager_factory.h" |
| #include "chrome/browser/chromeos/crostini/crostini_util.h" |
| #include "chrome/browser/chromeos/guest_os/guest_os_share_path.h" |
| #include "chrome/browser/chromeos/guest_os/guest_os_share_path_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/chrome_select_file_policy.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/keyed_service/content/browser_context_dependency_manager.h" |
| #include "components/keyed_service/content/browser_context_keyed_service_factory.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/web_contents.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace crostini { |
| |
| class CrostiniExportImportFactory : public BrowserContextKeyedServiceFactory { |
| public: |
| static CrostiniExportImport* GetForProfile(Profile* profile) { |
| return static_cast<CrostiniExportImport*>( |
| GetInstance()->GetServiceForBrowserContext(profile, true)); |
| } |
| |
| static CrostiniExportImportFactory* GetInstance() { |
| static base::NoDestructor<CrostiniExportImportFactory> factory; |
| return factory.get(); |
| } |
| |
| private: |
| friend class base::NoDestructor<CrostiniExportImportFactory>; |
| |
| CrostiniExportImportFactory() |
| : BrowserContextKeyedServiceFactory( |
| "CrostiniExportImportService", |
| BrowserContextDependencyManager::GetInstance()) { |
| DependsOn(guest_os::GuestOsSharePathFactory::GetInstance()); |
| DependsOn(CrostiniManagerFactory::GetInstance()); |
| } |
| |
| ~CrostiniExportImportFactory() override = default; |
| |
| // BrowserContextKeyedServiceFactory: |
| KeyedService* BuildServiceInstanceFor( |
| content::BrowserContext* context) const override { |
| Profile* profile = Profile::FromBrowserContext(context); |
| return new CrostiniExportImport(profile); |
| } |
| }; |
| |
| CrostiniExportImport* CrostiniExportImport::GetForProfile(Profile* profile) { |
| return CrostiniExportImportFactory::GetForProfile(profile); |
| } |
| |
| CrostiniExportImport::CrostiniExportImport(Profile* profile) |
| : profile_(profile), weak_ptr_factory_(this) { |
| CrostiniManager* manager = CrostiniManager::GetForProfile(profile_); |
| manager->AddExportContainerProgressObserver(this); |
| manager->AddImportContainerProgressObserver(this); |
| } |
| |
| CrostiniExportImport::~CrostiniExportImport() = default; |
| |
| void CrostiniExportImport::Shutdown() { |
| CrostiniManager* manager = CrostiniManager::GetForProfile(profile_); |
| manager->RemoveExportContainerProgressObserver(this); |
| manager->RemoveImportContainerProgressObserver(this); |
| } |
| |
| void CrostiniExportImport::ExportContainer(content::WebContents* web_contents) { |
| if (!IsCrostiniExportImportUIAllowedForProfile(profile_)) { |
| return; |
| } |
| OpenFileDialog(ExportImportType::EXPORT, web_contents); |
| } |
| |
| void CrostiniExportImport::ImportContainer(content::WebContents* web_contents) { |
| if (!IsCrostiniExportImportUIAllowedForProfile(profile_)) { |
| return; |
| } |
| OpenFileDialog(ExportImportType::IMPORT, web_contents); |
| } |
| |
| void CrostiniExportImport::OpenFileDialog(ExportImportType type, |
| content::WebContents* web_contents) { |
| PrefService* pref_service = profile_->GetPrefs(); |
| ui::SelectFileDialog::Type file_selector_mode; |
| unsigned title = 0; |
| base::FilePath default_path; |
| ui::SelectFileDialog::FileTypeInfo file_types; |
| file_types.allowed_paths = |
| ui::SelectFileDialog::FileTypeInfo::NATIVE_OR_DRIVE_PATH; |
| file_types.extensions = {{"tini", "tar.gz", "tgz"}}; |
| |
| switch (type) { |
| case ExportImportType::EXPORT: |
| file_selector_mode = ui::SelectFileDialog::SELECT_SAVEAS_FILE; |
| title = IDS_SETTINGS_CROSTINI_EXPORT; |
| base::Time::Exploded exploded; |
| base::Time::Now().LocalExplode(&exploded); |
| default_path = |
| pref_service->GetFilePath(prefs::kDownloadDefaultDirectory) |
| .Append(base::StringPrintf("chromeos-linux-%04d-%02d-%02d.tini", |
| exploded.year, exploded.month, |
| exploded.day_of_month)); |
| break; |
| case ExportImportType::IMPORT: |
| file_selector_mode = ui::SelectFileDialog::SELECT_OPEN_FILE, |
| title = IDS_SETTINGS_CROSTINI_IMPORT; |
| default_path = |
| pref_service->GetFilePath(prefs::kDownloadDefaultDirectory); |
| break; |
| } |
| |
| select_folder_dialog_ = ui::SelectFileDialog::Create( |
| this, std::make_unique<ChromeSelectFilePolicy>(web_contents)); |
| select_folder_dialog_->SelectFile( |
| file_selector_mode, l10n_util::GetStringUTF16(title), default_path, |
| &file_types, 0, base::FilePath::StringType(), |
| web_contents->GetTopLevelNativeWindow(), reinterpret_cast<void*>(type)); |
| } |
| |
| void CrostiniExportImport::FileSelected(const base::FilePath& path, |
| int index, |
| void* params) { |
| ExportImportType type = |
| static_cast<ExportImportType>(reinterpret_cast<uintptr_t>(params)); |
| Start(type, |
| ContainerId(kCrostiniDefaultVmName, kCrostiniDefaultContainerName), |
| path, base::DoNothing()); |
| } |
| |
| void CrostiniExportImport::ExportContainer( |
| ContainerId container_id, |
| base::FilePath path, |
| CrostiniManager::CrostiniResultCallback callback) { |
| Start(ExportImportType::EXPORT, container_id, path, std::move(callback)); |
| } |
| |
| void CrostiniExportImport::ImportContainer( |
| ContainerId container_id, |
| base::FilePath path, |
| CrostiniManager::CrostiniResultCallback callback) { |
| Start(ExportImportType::IMPORT, container_id, path, std::move(callback)); |
| } |
| |
| void CrostiniExportImport::Start( |
| ExportImportType type, |
| ContainerId container_id, |
| base::FilePath path, |
| CrostiniManager::CrostiniResultCallback callback) { |
| auto* notification = CrostiniExportImportNotification::Create( |
| profile_, type, GetUniqueNotificationId(), path, container_id); |
| |
| auto it = notifications_.find(container_id); |
| if (it != notifications_.end()) { |
| // There is already an operation in progress. Ensure the existing |
| // notification is (re)displayed so the user knows why this new concurrent |
| // operation failed, and show a failure notification for the new request. |
| it->second->ForceRedisplay(); |
| notification->SetStatusFailedConcurrentOperation(it->second->type()); |
| return; |
| } else { |
| notifications_.emplace_hint(it, container_id, notification); |
| for (auto& observer : observers_) { |
| observer.OnCrostiniExportImportOperationStatusChanged(true); |
| } |
| } |
| |
| switch (type) { |
| case ExportImportType::EXPORT: |
| base::PostTaskAndReply( |
| FROM_HERE, {base::ThreadPool(), base::MayBlock()}, |
| // Ensure file exists so that it can be shared. |
| base::BindOnce( |
| [](const base::FilePath& path) { |
| base::File file(path, base::File::FLAG_CREATE_ALWAYS | |
| base::File::FLAG_WRITE); |
| DCHECK(file.IsValid()) << path << " is invalid"; |
| }, |
| path), |
| base::BindOnce( |
| &guest_os::GuestOsSharePath::SharePath, |
| base::Unretained( |
| guest_os::GuestOsSharePath::GetForProfile(profile_)), |
| kCrostiniDefaultVmName, path, false, |
| base::BindOnce(&CrostiniExportImport::ExportAfterSharing, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(container_id), std::move(callback)) |
| |
| )); |
| break; |
| case ExportImportType::IMPORT: |
| guest_os::GuestOsSharePath::GetForProfile(profile_)->SharePath( |
| kCrostiniDefaultVmName, path, false, |
| base::BindOnce(&CrostiniExportImport::ImportAfterSharing, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(container_id), std::move(callback))); |
| break; |
| } |
| } |
| |
| void CrostiniExportImport::ExportAfterSharing( |
| const ContainerId& container_id, |
| CrostiniManager::CrostiniResultCallback callback, |
| const base::FilePath& container_path, |
| bool result, |
| const std::string& failure_reason) { |
| if (!result) { |
| LOG(ERROR) << "Error sharing for export " << container_path.value() << ": " |
| << failure_reason; |
| auto it = notifications_.find(container_id); |
| DCHECK(it != notifications_.end()) << ContainerIdToString(container_id) |
| << " has no notification to update"; |
| RemoveNotification(it).SetStatusFailed(); |
| return; |
| } |
| CrostiniManager::GetForProfile(profile_)->ExportLxdContainer( |
| kCrostiniDefaultVmName, kCrostiniDefaultContainerName, container_path, |
| base::BindOnce(&CrostiniExportImport::OnExportComplete, |
| weak_ptr_factory_.GetWeakPtr(), base::Time::Now(), |
| container_id, std::move(callback))); |
| } |
| |
| void CrostiniExportImport::OnExportComplete( |
| const base::Time& start, |
| const ContainerId& container_id, |
| CrostiniManager::CrostiniResultCallback callback, |
| CrostiniResult result) { |
| auto it = notifications_.find(container_id); |
| DCHECK(it != notifications_.end()) |
| << ContainerIdToString(container_id) << " has no notification to update"; |
| |
| ExportContainerResult enum_hist_result = ExportContainerResult::kSuccess; |
| if (result == CrostiniResult::SUCCESS) { |
| switch (it->second->status()) { |
| case CrostiniExportImportNotification::Status::CANCELLING: { |
| // If a user requests to cancel, but the export completes before the |
| // cancel can happen (|result| == SUCCESS), then removing the exported |
| // file is functionally the same as a successful cancel. |
| base::PostTask(FROM_HERE, |
| {base::ThreadPool(), base::MayBlock(), |
| base::TaskPriority::BEST_EFFORT}, |
| base::BindOnce(base::IgnoreResult(&base::DeleteFile), |
| it->second->path(), false)); |
| RemoveNotification(it).SetStatusCancelled(); |
| break; |
| } |
| case CrostiniExportImportNotification::Status::RUNNING: |
| UMA_HISTOGRAM_LONG_TIMES("Crostini.BackupTimeSuccess", |
| base::Time::Now() - start); |
| RemoveNotification(it).SetStatusDone(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } else if (result == CrostiniResult::CONTAINER_EXPORT_IMPORT_CANCELLED) { |
| switch (it->second->status()) { |
| case CrostiniExportImportNotification::Status::CANCELLING: { |
| // If a user requests to cancel, and the export is cancelled (|result| |
| // == CONTAINER_EXPORT_IMPORT_CANCELLED), then the partially exported |
| // file needs to be cleaned up. |
| base::PostTask(FROM_HERE, |
| {base::ThreadPool(), base::MayBlock(), |
| base::TaskPriority::BEST_EFFORT}, |
| base::BindOnce(base::IgnoreResult(&base::DeleteFile), |
| it->second->path(), false)); |
| RemoveNotification(it).SetStatusCancelled(); |
| break; |
| } |
| default: |
| NOTREACHED(); |
| } |
| } else { |
| LOG(ERROR) << "Error exporting " << int(result); |
| base::PostTask( |
| FROM_HERE, |
| {base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT}, |
| base::BindOnce(base::IgnoreResult(&base::DeleteFile), |
| it->second->path(), false)); |
| switch (result) { |
| case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STOPPED: |
| enum_hist_result = ExportContainerResult::kFailedVmStopped; |
| break; |
| case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STARTED: |
| enum_hist_result = ExportContainerResult::kFailedVmStarted; |
| break; |
| default: |
| DCHECK(result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED); |
| enum_hist_result = ExportContainerResult::kFailed; |
| } |
| UMA_HISTOGRAM_LONG_TIMES("Crostini.BackupTimeFailed", |
| base::Time::Now() - start); |
| DCHECK(it->second->status() == |
| CrostiniExportImportNotification::Status::RUNNING || |
| it->second->status() == |
| CrostiniExportImportNotification::Status::CANCELLING); |
| RemoveNotification(it).SetStatusFailed(); |
| } |
| UMA_HISTOGRAM_ENUMERATION("Crostini.Backup", enum_hist_result); |
| std::move(callback).Run(result); |
| } |
| |
| void CrostiniExportImport::OnExportContainerProgress( |
| const std::string& vm_name, |
| const std::string& container_name, |
| ExportContainerProgressStatus status, |
| int progress_percent, |
| uint64_t progress_speed) { |
| ContainerId container_id(vm_name, container_name); |
| auto it = notifications_.find(container_id); |
| DCHECK(it != notifications_.end()) |
| << ContainerIdToString(container_id) << " has no notification to update"; |
| |
| switch (status) { |
| // Rescale PACK:1-100 => 0-50. |
| case ExportContainerProgressStatus::PACK: |
| it->second->SetStatusRunning(progress_percent / 2); |
| break; |
| // Rescale DOWNLOAD:1-100 => 50-100. |
| case ExportContainerProgressStatus::DOWNLOAD: |
| it->second->SetStatusRunning(50 + progress_percent / 2); |
| break; |
| default: |
| LOG(WARNING) << "Unknown Export progress status " << int(status); |
| } |
| } |
| |
| void CrostiniExportImport::OnExportContainerProgress( |
| const std::string& vm_name, |
| const std::string& container_name, |
| const StreamingExportStatus& status) { |
| ContainerId container_id(vm_name, container_name); |
| auto it = notifications_.find(container_id); |
| DCHECK(it != notifications_.end()) |
| << ContainerIdToString(container_id) << " has no notification to update"; |
| |
| const auto files_percent = 100.0 * status.exported_files / status.total_files; |
| const auto bytes_percent = 100.0 * status.exported_bytes / status.total_bytes; |
| |
| // Averaging the two percentages gives a more accurate estimation. |
| // TODO(juwa): investigate more accurate approximations of percent. |
| const auto percent = (files_percent + bytes_percent) / 2.0; |
| |
| it->second->SetStatusRunning(static_cast<int>(percent)); |
| } |
| |
| void CrostiniExportImport::ImportAfterSharing( |
| const ContainerId& container_id, |
| CrostiniManager::CrostiniResultCallback callback, |
| const base::FilePath& container_path, |
| bool result, |
| const std::string& failure_reason) { |
| if (!result) { |
| LOG(ERROR) << "Error sharing for import " << container_path.value() << ": " |
| << failure_reason; |
| auto it = notifications_.find(container_id); |
| DCHECK(it != notifications_.end()) << ContainerIdToString(container_id) |
| << " has no notification to update"; |
| RemoveNotification(it).SetStatusFailed(); |
| return; |
| } |
| CrostiniManager::GetForProfile(profile_)->ImportLxdContainer( |
| kCrostiniDefaultVmName, kCrostiniDefaultContainerName, container_path, |
| base::BindOnce(&CrostiniExportImport::OnImportComplete, |
| weak_ptr_factory_.GetWeakPtr(), base::Time::Now(), |
| container_id, std::move(callback))); |
| } |
| |
| void CrostiniExportImport::OnImportComplete( |
| const base::Time& start, |
| const ContainerId& container_id, |
| CrostiniManager::CrostiniResultCallback callback, |
| CrostiniResult result) { |
| auto it = notifications_.find(container_id); |
| |
| ImportContainerResult enum_hist_result = ImportContainerResult::kSuccess; |
| if (result == CrostiniResult::SUCCESS) { |
| UMA_HISTOGRAM_LONG_TIMES("Crostini.RestoreTimeSuccess", |
| base::Time::Now() - start); |
| DCHECK(it != notifications_.end()) << ContainerIdToString(container_id) |
| << " has no notification to update"; |
| switch (it->second->status()) { |
| case CrostiniExportImportNotification::Status::RUNNING: |
| // If a user requests to cancel, but the import completes before the |
| // cancel can happen, then the container will have been imported over |
| // and the cancel will have failed. However the period of time in which |
| // this can happen is very small (<5s), so it feels quite natural to |
| // pretend the cancel did not happen, and instead display success. |
| case CrostiniExportImportNotification::Status::CANCELLING: |
| RemoveNotification(it).SetStatusDone(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } else if (result == |
| crostini::CrostiniResult::CONTAINER_EXPORT_IMPORT_CANCELLED) { |
| DCHECK(it != notifications_.end()) << ContainerIdToString(container_id) |
| << " has no notification to update"; |
| switch (it->second->status()) { |
| case CrostiniExportImportNotification::Status::CANCELLING: |
| RemoveNotification(it).SetStatusCancelled(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } else { |
| LOG(ERROR) << "Error importing " << int(result); |
| switch (result) { |
| case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STOPPED: |
| enum_hist_result = ImportContainerResult::kFailedVmStopped; |
| break; |
| case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STARTED: |
| enum_hist_result = ImportContainerResult::kFailedVmStarted; |
| break; |
| case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_ARCHITECTURE: |
| enum_hist_result = ImportContainerResult::kFailedArchitecture; |
| break; |
| case CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_SPACE: |
| enum_hist_result = ImportContainerResult::kFailedSpace; |
| break; |
| default: |
| DCHECK(result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED); |
| enum_hist_result = ImportContainerResult::kFailed; |
| } |
| // If the operation didn't start successfully or the vm stops during the |
| // import, then the notification status will not have been set in |
| // OnImportContainerProgress, so it needs to be updated. |
| if (result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED || |
| result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STOPPED || |
| result == CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_VM_STARTED) { |
| DCHECK(it != notifications_.end()) << ContainerIdToString(container_id) |
| << " has no notification to update"; |
| DCHECK(it->second->status() == |
| CrostiniExportImportNotification::Status::RUNNING); |
| RemoveNotification(it).SetStatusFailed(); |
| } else { |
| DCHECK(it == notifications_.end()) << ContainerIdToString(container_id) |
| << " has unexpected notification"; |
| } |
| UMA_HISTOGRAM_LONG_TIMES("Crostini.RestoreTimeFailed", |
| base::Time::Now() - start); |
| } |
| UMA_HISTOGRAM_ENUMERATION("Crostini.Restore", enum_hist_result); |
| |
| // Restart from CrostiniManager. |
| CrostiniManager::GetForProfile(profile_)->RestartCrostini( |
| container_id.first, container_id.second, std::move(callback)); |
| } |
| |
| void CrostiniExportImport::OnImportContainerProgress( |
| const std::string& vm_name, |
| const std::string& container_name, |
| ImportContainerProgressStatus status, |
| int progress_percent, |
| uint64_t progress_speed, |
| const std::string& architecture_device, |
| const std::string& architecture_container, |
| uint64_t available_space, |
| uint64_t minimum_required_space) { |
| ContainerId container_id(vm_name, container_name); |
| auto it = notifications_.find(container_id); |
| DCHECK(it != notifications_.end()) |
| << ContainerIdToString(container_id) << " has no notification to update"; |
| |
| switch (status) { |
| // Rescale UPLOAD:1-100 => 0-50. |
| case ImportContainerProgressStatus::UPLOAD: |
| it->second->SetStatusRunning(progress_percent / 2); |
| break; |
| // Rescale UNPACK:1-100 => 50-100. |
| case ImportContainerProgressStatus::UNPACK: |
| it->second->SetStatusRunning(50 + progress_percent / 2); |
| break; |
| // Failure, set error message. |
| case ImportContainerProgressStatus::FAILURE_ARCHITECTURE: |
| RemoveNotification(it).SetStatusFailedArchitectureMismatch( |
| architecture_container, architecture_device); |
| break; |
| case ImportContainerProgressStatus::FAILURE_SPACE: |
| DCHECK_GE(minimum_required_space, available_space); |
| RemoveNotification(it).SetStatusFailedInsufficientSpace( |
| minimum_required_space - available_space); |
| break; |
| default: |
| LOG(WARNING) << "Unknown Export progress status " << int(status); |
| } |
| } |
| |
| std::string CrostiniExportImport::GetUniqueNotificationId() { |
| return base::StringPrintf("crostini_export_import_%d", |
| next_notification_id_++); |
| } |
| |
| CrostiniExportImportNotification& CrostiniExportImport::RemoveNotification( |
| std::map<ContainerId, CrostiniExportImportNotification*>::iterator it) { |
| DCHECK(it != notifications_.end()); |
| auto& notification = *it->second; |
| notifications_.erase(it); |
| for (auto& observer : observers_) { |
| observer.OnCrostiniExportImportOperationStatusChanged(false); |
| } |
| return notification; |
| } |
| |
| void CrostiniExportImport::CancelOperation(ExportImportType type, |
| ContainerId container_id) { |
| auto it = notifications_.find(container_id); |
| if (it == notifications_.end()) { |
| NOTREACHED() << ContainerIdToString(container_id) |
| << " has no notification to cancel"; |
| return; |
| } |
| |
| it->second->SetStatusCancelling(); |
| |
| auto& manager = *CrostiniManager::GetForProfile(profile_); |
| |
| switch (type) { |
| case ExportImportType::EXPORT: |
| manager.CancelExportLxdContainer(std::move(container_id)); |
| return; |
| case ExportImportType::IMPORT: |
| manager.CancelImportLxdContainer(std::move(container_id)); |
| return; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| bool CrostiniExportImport::GetExportImportOperationStatus() const { |
| ContainerId id(kCrostiniDefaultVmName, kCrostiniDefaultContainerName); |
| return notifications_.find(id) != notifications_.end(); |
| } |
| |
| CrostiniExportImportNotification* |
| CrostiniExportImport::GetNotificationForTesting(ContainerId container_id) { |
| auto it = notifications_.find(container_id); |
| return it != notifications_.end() ? it->second : nullptr; |
| } |
| |
| } // namespace crostini |