blob: e43055d33b8b9a7887070503e0ae86361abde3d2 [file] [log] [blame]
// Copyright 2025 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/baguette_installer.h"
#include <algorithm>
#include <memory>
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/strings/string_util.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ash/crostini/baguette_download.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_os_dlc_helper.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/ash/components/dbus/vm_applications/apps.pb.h"
#include "components/prefs/pref_service.h"
#include "url/origin.h"
namespace crostini {
namespace {
base::ScopedFD OpenFdBlocking(base::FilePath image_path) {
base::File image(image_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!image.IsValid()) {
LOG(ERROR) << "Failed to open image file";
return base::ScopedFD();
}
return base::ScopedFD(image.TakePlatformFile());
}
} // namespace
BaguetteInstaller::BaguetteInstaller(Profile* profile, PrefService& local_state)
: download_factory_(base::BindRepeating(
[](PrefService& local_state) -> std::unique_ptr<BaguetteDownload> {
return std::make_unique<SimpleURLLoaderDownload>(local_state);
},
std::ref(local_state))),
profile_(profile) {}
BaguetteInstaller::~BaguetteInstaller() = default;
void BaguetteInstaller::Install(BaguetteInstallerCallback callback) {
installations_.push_back(std::make_unique<guest_os::GuestOsDlcInstallation>(
kToolsDlcName,
base::BindOnce(&BaguetteInstaller::OnInstallDlc,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
base::DoNothing()));
}
void BaguetteInstaller::OnInstallDlc(
BaguetteInstallerCallback callback,
guest_os::GuestOsDlcInstallation::Result result) {
InstallResult response =
result
.transform_error(
[](guest_os::GuestOsDlcInstallation::Error err) {
switch (err) {
case guest_os::GuestOsDlcInstallation::Error::Cancelled:
return InstallResult::Cancelled;
case guest_os::GuestOsDlcInstallation::Error::Offline:
LOG(ERROR) << "Failed to install termina-tools-dlc while "
"offline, assuming "
"network issue.";
return InstallResult::Offline;
case guest_os::GuestOsDlcInstallation::Error::NeedUpdate:
case guest_os::GuestOsDlcInstallation::Error::NeedReboot:
LOG(ERROR)
<< "Failed to install termina-tools-dlc because the OS "
"must be updated";
return InstallResult::NeedUpdate;
case guest_os::GuestOsDlcInstallation::Error::DiskFull:
case guest_os::GuestOsDlcInstallation::Error::Busy:
case guest_os::GuestOsDlcInstallation::Error::Internal:
case guest_os::GuestOsDlcInstallation::Error::Invalid:
case guest_os::GuestOsDlcInstallation::Error::UnknownFailure:
LOG(ERROR)
<< "Failed to install termina-tools-dlc: " << err;
return InstallResult::Failure;
}
})
.error_or(InstallResult::Success);
if (response != InstallResult::Success) {
std::move(callback).Run(response, {});
return;
}
auto* concierge_client = ash::ConciergeClient::Get();
concierge_client->WaitForServiceToBeAvailable(
base::BindOnce(&BaguetteInstaller::OnConciergeAvailable,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void BaguetteInstaller::OnConciergeAvailable(BaguetteInstallerCallback callback,
bool service_is_available) {
if (!service_is_available) {
LOG(ERROR) << "vm_concierge service is unavailable.";
std::move(callback).Run(InstallResult::Failure, {});
}
// We always need to check if tools DLC is present for kernel, but we may
// already have a baguette disk image, check for that first.
auto* concierge_client = ash::ConciergeClient::Get();
vm_tools::concierge::ListVmDisksRequest request;
request.set_vm_name(kCrostiniDefaultVmName);
request.set_cryptohome_id(
ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
request.set_storage_location(vm_tools::concierge::STORAGE_CRYPTOHOME_ROOT);
concierge_client->ListVmDisks(
request,
base::BindOnce(&BaguetteInstaller::OnListVmDisks,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void BaguetteInstaller::OnListVmDisks(
BaguetteInstallerCallback callback,
std::optional<vm_tools::concierge::ListVmDisksResponse> response) {
if (!response) {
LOG(WARNING) << "Unable to query disk image status, will potentially "
"re-download baguette image needlessly.";
}
if (!response->success()) {
LOG(WARNING) << "Unsuccessful disk image response, will potentially "
"re-download baguette image needlessly.";
}
if (std::any_of(
response->images().cbegin(), response->images().cend(),
[](auto image) {
return image.vm_type() ==
vm_tools::concierge::VmInfo_VmType::VmInfo_VmType_BAGUETTE;
})) {
std::move(callback).Run(InstallResult::Success, {});
return;
}
auto* concierge_client = ash::ConciergeClient::Get();
concierge_client->GetBaguetteImageUrl(
base::BindOnce(&BaguetteInstaller::DownloadBaguetteImage,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void BaguetteInstaller::DownloadBaguetteImage(
BaguetteInstallerCallback callback,
std::optional<vm_tools::concierge::GetBaguetteImageUrlResponse> response) {
if (!response) {
LOG(ERROR) << "Failed to get baguette disk image URL from vm_concierge.";
std::move(callback).Run(InstallResult::DownloadError, {});
return;
} else if (response->url().empty()) {
LOG(ERROR) << "vm_concierge returned an empty baguette image URL.";
std::move(callback).Run(InstallResult::DownloadError, {});
return;
} else if (response->sha256().empty()) {
LOG(ERROR) << "vm_concierge returned an empty baguette image checksum.";
std::move(callback).Run(InstallResult::DownloadError, {});
return;
}
image_download_ = download_factory_.Run();
image_download_->StartDownload(
profile_, GURL(response->url()),
base::BindOnce(&crostini::BaguetteInstaller::OnDiskImageDownloaded,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
response->sha256()));
}
void BaguetteInstaller::OnDiskImageDownloaded(
BaguetteInstallerCallback callback,
std::string expected_hash,
base::FilePath path,
std::string hash) {
if (path.empty()) {
LOG(ERROR) << "Error downloading.";
std::move(callback).Run(InstallResult::DownloadError, {});
return;
}
if (!base::EqualsCaseInsensitiveASCII(hash, expected_hash)) {
LOG(ERROR) << "Downloaded image has incorrect SHA256 hash.";
std::move(callback).Run(InstallResult::ChecksumError, {});
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()}, base::BindOnce(&OpenFdBlocking, path),
base::BindOnce(&BaguetteInstaller::OnOpenFd,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void BaguetteInstaller::OnOpenFd(BaguetteInstallerCallback callback,
base::ScopedFD image) {
if (!image.is_valid()) {
LOG(ERROR) << "Unable to open image file.";
std::move(callback).Run(InstallResult::Failure, {});
return;
}
std::move(callback).Run(InstallResult::Success, std::move(image));
}
void BaguetteInstaller::Uninstall(base::OnceCallback<void(bool)> callback) {
ash::DlcserviceClient::Get()->GetExistingDlcs(base::BindOnce(
[](base::WeakPtr<BaguetteInstaller> weak_this,
base::OnceCallback<void(bool)> callback, std::string_view err,
const dlcservice::DlcsWithContent& dlcs_with_content) {
if (!weak_this) {
return;
}
if (err != dlcservice::kErrorNone) {
LOG(ERROR) << "Failed to list installed DLCs: " << err;
std::move(callback).Run(false);
return;
}
for (const auto& dlc : dlcs_with_content.dlc_infos()) {
if (dlc.id() == kToolsDlcName) {
VLOG(1) << "DLC present, removing";
weak_this->RemoveDlc(std::move(callback));
return;
}
}
VLOG(1) << "No DLC present, skipping";
std::move(callback).Run(true);
},
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void BaguetteInstaller::RemoveDlc(base::OnceCallback<void(bool)> callback) {
ash::DlcserviceClient::Get()->Uninstall(
kToolsDlcName,
base::BindOnce(
[](base::OnceCallback<void(bool)> callback, std::string_view err) {
if (err == dlcservice::kErrorNone) {
VLOG(1) << "Removed DLC";
std::move(callback).Run(true);
} else {
LOG(ERROR) << "Failed to remove termina-tools-dlc: " << err;
std::move(callback).Run(false);
}
},
std::move(callback)));
}
} // namespace crostini