// 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/crostini/crostini_upgrader.h"
#include <optional>
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/no_destructor.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/crostini/crostini_export_import.h"
#include "chrome/browser/ash/crostini/crostini_export_import_status_tracker.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_manager_factory.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_keyed_service_factory.h"
#include "chrome/browser/ui/webui/ash/crostini_upgrader/crostini_upgrader.mojom.h"
#include "components/keyed_service/core/keyed_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/network_service_instance.h"
#include "services/network/public/cpp/network_connection_tracker.h"
namespace crostini {
namespace {
class CrostiniUpgraderFactory : public ProfileKeyedServiceFactory {
public:
static CrostiniUpgrader* GetForProfile(Profile* profile) {
return static_cast<CrostiniUpgrader*>(
GetInstance()->GetServiceForBrowserContext(profile, true));
}
static CrostiniUpgraderFactory* GetInstance() {
static base::NoDestructor<CrostiniUpgraderFactory> factory;
return factory.get();
}
private:
friend class base::NoDestructor<CrostiniUpgraderFactory>;
CrostiniUpgraderFactory()
: ProfileKeyedServiceFactory(
"CrostiniUpgraderService",
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());
}
// BrowserContextKeyedServiceFactory:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* context) const override {
Profile* profile = Profile::FromBrowserContext(context);
return new CrostiniUpgrader(profile);
}
};
const char kLogFileBasename[] = "container_upgrade.log";
} // namespace
CrostiniUpgrader* CrostiniUpgrader::GetForProfile(Profile* profile) {
return CrostiniUpgraderFactory::GetForProfile(profile);
}
CrostiniUpgrader::CrostiniUpgrader(Profile* profile)
: profile_(profile),
container_id_(kCrostiniDefaultVmType, "", ""),
log_sequence_(
base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})),
current_log_file_(std::nullopt),
backup_path_(std::nullopt) {
CrostiniManager::GetForProfile(profile_)->AddUpgradeContainerProgressObserver(
this);
}
CrostiniUpgrader::~CrostiniUpgrader() = default;
void CrostiniUpgrader::Shutdown() {
CrostiniManager::GetForProfile(profile_)
->RemoveUpgradeContainerProgressObserver(this);
upgrader_observers_.Clear();
}
void CrostiniUpgrader::AddObserver(CrostiniUpgraderUIObserver* observer) {
upgrader_observers_.AddObserver(observer);
}
void CrostiniUpgrader::RemoveObserver(CrostiniUpgraderUIObserver* observer) {
upgrader_observers_.RemoveObserver(observer);
}
void CrostiniUpgrader::PageOpened() {
// Clear log path so any log messages get buffered.
current_log_file_ = std::nullopt;
// Clear the buffer, which may have been previously moved from.
log_buffer_ = std::vector<std::string>();
}
void CrostiniUpgrader::CreateNewLogFile() {
base::FilePath path =
file_manager::util::GetMyFilesFolderForProfile(profile_).Append(
kLogFileBasename);
// Create the new log file on the blocking threadpool.
log_sequence_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(
[](base::FilePath path) -> std::optional<base::FilePath> {
path = base::GetUniquePath(path);
base::File file(path,
base::File::FLAG_READ | base::File::FLAG_CREATE);
if (!file.IsValid()) {
PLOG(ERROR) << "Failed to create log file!";
return std::nullopt;
}
return path;
},
path),
// Once the file is created, write out the buffered log messages.
base::BindOnce(
[](base::WeakPtr<CrostiniUpgrader> weak_this,
std::optional<base::FilePath> path) {
if (!weak_this) {
return;
}
weak_this->current_log_file_ = path;
if (path) {
weak_this->WriteLogMessages(std::move(weak_this->log_buffer_));
for (auto& observer : weak_this->upgrader_observers_) {
observer.OnLogFileCreated(path->BaseName());
}
}
},
weak_ptr_factory_.GetWeakPtr()));
}
CrostiniUpgrader::StatusTracker::StatusTracker(
base::WeakPtr<CrostiniUpgrader> upgrader,
ExportImportType type,
base::FilePath path)
: CrostiniExportImportStatusTracker(type, std::move(path)),
upgrader_(upgrader) {}
CrostiniUpgrader::StatusTracker::~StatusTracker() = default;
void CrostiniUpgrader::StatusTracker::SetStatusRunningUI(int progress_percent) {
if (type() == ExportImportType::EXPORT) {
upgrader_->OnBackupProgress(progress_percent);
} else {
upgrader_->OnRestoreProgress(progress_percent);
}
if (has_notified_start_) {
return;
}
for (auto& observer : upgrader_->upgrader_observers_) {
observer.OnBackupMaybeStarted(/*did_start=*/true);
}
has_notified_start_ = true;
}
void CrostiniUpgrader::StatusTracker::SetStatusDoneUI() {
if (type() == ExportImportType::EXPORT) {
upgrader_->OnBackup(CrostiniResult::SUCCESS, path());
} else {
upgrader_->OnRestore(CrostiniResult::SUCCESS);
}
}
void CrostiniUpgrader::StatusTracker::SetStatusCancelledUI() {
// Cancelling the restore results in "success" i.e. we successfully didn't try
// to restore. Cancelling the backup is a no-op that returns you to the
// original screen.
if (type() == ExportImportType::IMPORT) {
upgrader_->OnRestore(CrostiniResult::SUCCESS);
}
for (auto& observer : upgrader_->upgrader_observers_) {
observer.OnBackupMaybeStarted(/*did_start=*/false);
}
}
void CrostiniUpgrader::StatusTracker::SetStatusFailedWithMessageUI(
Status status,
const std::u16string& message) {
CrostiniResult result = CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED;
if (status == Status::FAILED_INSUFFICIENT_SPACE) {
result = CrostiniResult::CONTAINER_EXPORT_IMPORT_FAILED_SPACE;
}
if (type() == ExportImportType::EXPORT) {
upgrader_->OnBackup(result, std::nullopt);
} else {
upgrader_->OnRestore(result);
}
}
void CrostiniUpgrader::Backup(
const guest_os::GuestId& container_id,
bool show_file_chooser,
base::WeakPtr<content::WebContents> web_contents) {
if (show_file_chooser) {
CrostiniExportImport::GetForProfile(profile_)->ExportContainer(
container_id, web_contents.get(), MakeFactory());
return;
}
base::FilePath default_path =
CrostiniExportImport::GetForProfile(profile_)->GetDefaultBackupPath();
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&base::PathExists, default_path),
base::BindOnce(&CrostiniUpgrader::OnBackupPathChecked,
weak_ptr_factory_.GetWeakPtr(), container_id, web_contents,
default_path));
}
void CrostiniUpgrader::OnBackupPathChecked(
const guest_os::GuestId& container_id,
base::WeakPtr<content::WebContents> web_contents,
base::FilePath path,
bool path_exists) {
if (!web_contents) {
// Page has been closed, don't continue
return;
}
if (path_exists) {
CrostiniExportImport::GetForProfile(profile_)->ExportContainer(
container_id, web_contents.get(), MakeFactory());
} else {
CrostiniExportImport::GetForProfile(profile_)->ExportContainer(
container_id, path, MakeFactory());
}
}
void CrostiniUpgrader::OnBackup(CrostiniResult result,
std::optional<base::FilePath> backup_path) {
if (result != CrostiniResult::SUCCESS) {
for (auto& observer : upgrader_observers_) {
observer.OnBackupFailed();
}
return;
}
backup_path_ = backup_path;
for (auto& observer : upgrader_observers_) {
observer.OnBackupSucceeded(!backup_path.has_value());
}
}
void CrostiniUpgrader::OnBackupProgress(int progress_percent) {
for (auto& observer : upgrader_observers_) {
observer.OnBackupProgress(progress_percent);
}
}
void CrostiniUpgrader::StartPrechecks() {
auto* pmc = chromeos::PowerManagerClient::Get();
if (pmc_observation_.IsObservingSource(pmc)) {
// This could happen if two StartPrechecks were run at the same time. If it
// does, drop the second call.
return;
}
prechecks_callback_ = base::BindOnce(&CrostiniUpgrader::DoPrechecks,
weak_ptr_factory_.GetWeakPtr());
pmc_observation_.Observe(pmc);
pmc->RequestStatusUpdate();
}
void CrostiniUpgrader::PowerChanged(
const power_manager::PowerSupplyProperties& proto) {
// A battery can be FULL, CHARGING, DISCHARGING, or NOT_PRESENT. If we're on a
// system with no battery, we can assume stable power from the fact that we
// are running at all. Otherwise we want the battery to be full or charging. A
// less conservative check is possible, but we can expect users to have access
// to a charger.
power_status_good_ = proto.battery_state() !=
power_manager::PowerSupplyProperties::DISCHARGING ||
proto.external_power() !=
power_manager::PowerSupplyProperties::DISCONNECTED;
auto* pmc = chromeos::PowerManagerClient::Get();
DCHECK(pmc_observation_.IsObservingSource(pmc));
pmc_observation_.Reset();
if (prechecks_callback_) {
std::move(prechecks_callback_).Run();
}
}
void CrostiniUpgrader::DoPrechecks() {
ash::crostini_upgrader::mojom::UpgradePrecheckStatus status;
if (content::GetNetworkConnectionTracker()->IsOffline()) {
status =
ash::crostini_upgrader::mojom::UpgradePrecheckStatus::NETWORK_FAILURE;
} else if (!power_status_good_) {
status = ash::crostini_upgrader::mojom::UpgradePrecheckStatus::LOW_POWER;
} else {
status = ash::crostini_upgrader::mojom::UpgradePrecheckStatus::OK;
}
for (auto& observer : upgrader_observers_) {
observer.PrecheckStatus(status);
}
}
void CrostiniUpgrader::Upgrade(const guest_os::GuestId& container_id) {
container_id_ = container_id;
if (!current_log_file_.has_value()) {
CreateNewLogFile();
}
OnUpgradeContainerProgress(container_id,
UpgradeContainerProgressStatus::UPGRADING,
{"---- START OF UPGRADE ----"});
// Shut down the existing VM then upgrade. StopVm doesn't give an error if
// the VM doesn't exist. That's fine.
CrostiniManager::GetForProfile(profile_)->StopVm(
container_id.vm_name,
base::BindOnce(
[](base::WeakPtr<CrostiniUpgrader> weak_this, CrostiniResult result) {
if (!weak_this) {
return;
}
if (result != CrostiniResult::SUCCESS) {
LOG(ERROR) << "Unable to shut down vm before upgrade";
weak_this->OnUpgrade(result);
return;
}
auto target_version = ContainerVersion::BOOKWORM;
CrostiniManager::GetForProfile(weak_this->profile_)
->UpgradeContainer(
weak_this->container_id_, target_version,
base::BindOnce(&CrostiniUpgrader::OnUpgrade, weak_this));
},
weak_ptr_factory_.GetWeakPtr()));
}
void CrostiniUpgrader::OnUpgrade(CrostiniResult result) {
if (result != CrostiniResult::SUCCESS) {
LOG(ERROR) << "OnUpgrade result " << static_cast<int>(result);
for (auto& observer : upgrader_observers_) {
observer.OnUpgradeFailed();
}
return;
}
}
void CrostiniUpgrader::Restore(
const guest_os::GuestId& container_id,
base::WeakPtr<content::WebContents> web_contents) {
if (!backup_path_.has_value()) {
CrostiniExportImport::GetForProfile(profile_)->ImportContainer(
container_id, web_contents.get(), MakeFactory());
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&base::PathExists, *backup_path_),
base::BindOnce(&CrostiniUpgrader::OnRestorePathChecked,
weak_ptr_factory_.GetWeakPtr(), container_id, web_contents,
*backup_path_));
}
void CrostiniUpgrader::OnRestorePathChecked(
const guest_os::GuestId& container_id,
base::WeakPtr<content::WebContents> web_contents,
base::FilePath path,
bool path_exists) {
if (!web_contents) {
// Page has been closed, don't continue
return;
}
if (!path_exists) {
CrostiniExportImport::GetForProfile(profile_)->ImportContainer(
container_id, web_contents.get(), MakeFactory());
} else {
CrostiniExportImport::GetForProfile(profile_)->ImportContainer(
container_id, path, MakeFactory());
}
}
void CrostiniUpgrader::OnRestore(CrostiniResult result) {
if (result != CrostiniResult::SUCCESS) {
for (auto& observer : upgrader_observers_) {
observer.OnRestoreFailed();
}
return;
}
for (auto& observer : upgrader_observers_) {
observer.OnRestoreSucceeded();
}
}
void CrostiniUpgrader::OnRestoreProgress(int progress_percent) {
for (auto& observer : upgrader_observers_) {
observer.OnRestoreProgress(progress_percent);
}
}
void CrostiniUpgrader::Cancel() {
CrostiniManager::GetForProfile(profile_)->CancelUpgradeContainer(
container_id_, base::BindOnce(&CrostiniUpgrader::OnCancel,
weak_ptr_factory_.GetWeakPtr()));
}
void CrostiniUpgrader::OnCancel(CrostiniResult result) {
for (auto& observer : upgrader_observers_) {
observer.OnCanceled();
}
}
void CrostiniUpgrader::CancelBeforeStart() {
for (auto& observer : upgrader_observers_) {
observer.OnCanceled();
}
}
void CrostiniUpgrader::OnUpgradeContainerProgress(
const guest_os::GuestId& container_id,
UpgradeContainerProgressStatus status,
const std::vector<std::string>& messages) {
if (container_id != container_id_) {
return;
}
// Write `messages` to the log file, or append it to the buffer if the log
// file is still pending.
if (current_log_file_) {
WriteLogMessages(messages);
} else {
log_buffer_.insert(log_buffer_.end(), messages.begin(), messages.end());
}
switch (status) {
case UpgradeContainerProgressStatus::UPGRADING:
for (auto& observer : upgrader_observers_) {
observer.OnUpgradeProgress(messages);
}
break;
case UpgradeContainerProgressStatus::SUCCEEDED:
for (auto& observer : upgrader_observers_) {
observer.OnUpgradeProgress(messages);
observer.OnUpgradeSucceeded();
}
break;
case UpgradeContainerProgressStatus::FAILED:
for (auto& observer : upgrader_observers_) {
observer.OnUpgradeProgress(messages);
observer.OnUpgradeFailed();
}
break;
}
}
void CrostiniUpgrader::WriteLogMessages(std::vector<std::string> messages) {
log_sequence_->PostTask(
FROM_HERE,
base::BindOnce(
[](base::FilePath path, std::vector<std::string> messages) {
for (const std::string& s : messages) {
if (!base::AppendToFile(path, s + "\n")) {
PLOG(ERROR) << "Failed to write logs";
}
}
},
*current_log_file_, std::move(messages)));
}
// Return true if internal state allows starting upgrade.
bool CrostiniUpgrader::CanUpgrade() {
return false;
}
CrostiniExportImport::OnceTrackerFactory CrostiniUpgrader::MakeFactory() {
return base::BindOnce(
[](base::WeakPtr<CrostiniUpgrader> upgrader, ExportImportType type,
base::FilePath path)
-> std::unique_ptr<CrostiniExportImportStatusTracker> {
return std::make_unique<StatusTracker>(std::move(upgrader), type,
std::move(path));
},
weak_ptr_factory_.GetWeakPtr());
}
// static
void CrostiniUpgrader::EnsureFactoryBuilt() {
CrostiniUpgraderFactory::GetInstance();
}
} // namespace crostini