// Copyright 2020 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/borealis/borealis_context_manager_impl.h"
#include <memory>
#include <ostream>
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "chrome/browser/ash/borealis/borealis_context.h"
#include "chrome/browser/ash/borealis/borealis_context_manager.h"
#include "chrome/browser/ash/borealis/borealis_metrics.h"
#include "chrome/browser/ash/borealis/borealis_task.h"
#include "chrome/browser/ash/borealis/infra/described.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/sessions/exit_type_service.h"
#include "chrome/browser/ui/views/borealis/borealis_splash_screen_view.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/ash/components/dbus/dbus_thread_manager.h"
namespace {
// We use a hard-coded name. When multi-instance becomes a feature we'll
// need to determine the name instead.
constexpr char kBorealisVmName[] = "borealis";
} // namespace
namespace borealis {
BorealisContextManagerImpl::Startup::Startup(
Profile* profile,
base::queue<std::unique_ptr<BorealisTask>> task_queue)
: profile_(profile),
context_(nullptr),
task_queue_(std::move(task_queue)),
weak_factory_(this) {}
BorealisContextManagerImpl::Startup::~Startup() = default;
std::unique_ptr<BorealisContext> BorealisContextManagerImpl::Startup::Abort() {
while (!task_queue_.empty()) {
task_queue_.pop();
}
Fail({BorealisStartupResult::kCancelled, "Startup aborted by user"});
return std::move(context_);
}
void BorealisContextManagerImpl::Startup::NextTask() {
if (task_queue_.empty()) {
RecordBorealisStartupResultHistogram(BorealisStartupResult::kSuccess);
RecordBorealisStartupOverallTimeHistogram(base::TimeTicks::Now() -
start_tick_);
Succeed(std::move(context_));
return;
}
task_queue_.front()->Run(
context_.get(),
base::BindOnce(&BorealisContextManagerImpl::Startup::TaskCallback,
weak_factory_.GetWeakPtr()));
}
void BorealisContextManagerImpl::Startup::TaskCallback(
BorealisStartupResult result,
std::string error) {
task_queue_.pop();
if (result == BorealisStartupResult::kSuccess) {
NextTask();
return;
}
RecordBorealisStartupResultHistogram(result);
Fail({result, std::move(error)});
}
void BorealisContextManagerImpl::Startup::Start(
std::unique_ptr<NotRunning> current_state) {
context_ = base::WrapUnique(new BorealisContext(profile_));
context_->set_vm_name(kBorealisVmName);
start_tick_ = base::TimeTicks::Now();
RecordBorealisStartupNumAttemptsHistogram();
NextTask();
}
BorealisContextManagerImpl::BorealisContextManagerImpl(Profile* profile)
: profile_(profile), weak_factory_(this) {
// ConciergeClient may not be initialized in tests.
if (ash::ConciergeClient::Get()) {
ShutDownBorealisIfRunning();
ash::ConciergeClient::Get()->AddVmObserver(this);
}
}
BorealisContextManagerImpl::~BorealisContextManagerImpl() {
// Even if initialized, DBusThreadManager or ConciergeClient may be destroyed
// prior to BorealisService/BorealisContextManagerImpl in tests. Therefore we
// must not keep a pointer to the observed ConciergeClient, either directly or
// via ScopedObservation or similar.
if (ash::ConciergeClient::Get()) {
ash::ConciergeClient::Get()->RemoveVmObserver(this);
}
}
// Note that this method gets called in the constructor.
// If Borealis was running when chrome crashed/restarted then Borealis
// may still be running and should be shut down.
void BorealisContextManagerImpl::ShutDownBorealisIfRunning() {
vm_tools::concierge::GetVmInfoRequest request;
request.set_owner_id(ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
request.set_name(kBorealisVmName);
ash::ConciergeClient::Get()->GetVmInfo(
std::move(request),
base::BindOnce(
[](base::WeakPtr<BorealisContextManagerImpl> weak_this,
std::optional<vm_tools::concierge::GetVmInfoResponse> reply) {
if (reply.has_value() && reply->success() && weak_this) {
weak_this->SendShutdownRequest(base::DoNothing(),
kBorealisVmName);
}
},
weak_factory_.GetWeakPtr()));
}
void BorealisContextManagerImpl::SendShutdownRequest(
base::OnceCallback<void(BorealisShutdownResult)> on_shutdown_callback,
const std::string& vm_name) {
// TODO(b/172178036): This could have been a task-sequence but that
// abstraction is proving insufficient.
vm_tools::concierge::StopVmRequest request;
request.set_owner_id(ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
request.set_name(vm_name);
ash::ConciergeClient::Get()->StopVm(
std::move(request),
base::BindOnce(
[](base::OnceCallback<void(BorealisShutdownResult)>
on_shutdown_callback,
std::optional<vm_tools::concierge::StopVmResponse> response) {
// We don't have a good way to deal with a vm failing to stop (and
// this would be a very rare occurrence anyway). We log an error if
// it actually wasn't successful.
BorealisShutdownResult result = BorealisShutdownResult::kSuccess;
if (!response.has_value()) {
LOG(ERROR) << "Failed to stop Borealis VM: No response";
result = BorealisShutdownResult::kFailed;
} else if (!response.value().success()) {
LOG(ERROR) << "Failed to stop Borealis VM: "
<< response.value().failure_reason();
result = BorealisShutdownResult::kFailed;
}
RecordBorealisShutdownResultHistogram(result);
std::move(on_shutdown_callback).Run(result);
},
std::move(on_shutdown_callback)));
}
void BorealisContextManagerImpl::StartBorealis(ResultCallback callback) {
if (context_) {
std::move(callback).Run(
BorealisContextManager::ContextOrFailure(context_.get()));
return;
}
AddCallback(std::move(callback));
if (!in_progress_startup_) {
in_progress_startup_ = std::make_unique<Startup>(profile_, GetTasks());
in_progress_startup_->Begin(
std::make_unique<NotRunning>(),
base::BindOnce(&BorealisContextManagerImpl::Complete,
weak_factory_.GetWeakPtr()));
}
}
bool BorealisContextManagerImpl::IsRunning() {
return context_.get();
}
void BorealisContextManagerImpl::ShutDownBorealis(
base::OnceCallback<void(BorealisShutdownResult)> on_shutdown_callback) {
CloseBorealisSplashScreenView();
// Get the context we are shutting down, either from an in-progress startup or
// from the running one.
std::unique_ptr<BorealisContext> shutdown_context;
std::swap(shutdown_context, context_);
if (in_progress_startup_) {
shutdown_context = in_progress_startup_->Abort();
}
if (!shutdown_context) {
// TODO(b/172178036): There could be an operation in progress but we can't
// tell because we don't record that state. Fix this by adding proper state
// tracking for the context_manager.
std::move(on_shutdown_callback).Run(BorealisShutdownResult::kSuccess);
return;
}
RecordBorealisShutdownNumAttemptsHistogram();
SendShutdownRequest(std::move(on_shutdown_callback),
shutdown_context->vm_name());
}
base::queue<std::unique_ptr<BorealisTask>>
BorealisContextManagerImpl::GetTasks() {
base::queue<std::unique_ptr<BorealisTask>> task_queue;
task_queue.push(std::make_unique<CheckAllowed>());
task_queue.push(std::make_unique<GetLaunchOptions>());
task_queue.push(std::make_unique<MountDlc>());
task_queue.push(std::make_unique<CreateDiskImage>());
task_queue.push(std::make_unique<StartBorealisVm>());
task_queue.push(std::make_unique<AwaitBorealisStartup>());
task_queue.push(std::make_unique<UpdateChromeFlags>(profile_));
return task_queue;
}
void BorealisContextManagerImpl::AddCallback(ResultCallback callback) {
callback_queue_.push(std::move(callback));
}
void BorealisContextManagerImpl::Complete(Startup::Result completion_result) {
DCHECK(!context_);
DCHECK(in_progress_startup_);
in_progress_startup_.reset();
BorealisContextManager::ContextOrFailure completion_result_for_clients;
if (completion_result.has_value()) {
context_ = std::move(completion_result).value();
completion_result_for_clients = base::ok(context_.get());
} else {
LOG(ERROR) << "Startup failed: failure="
<< completion_result.error().error()
<< " message=" << completion_result.error().description();
completion_result_for_clients = base::unexpected(completion_result.error());
}
while (!callback_queue_.empty()) {
ResultCallback callback = std::move(callback_queue_.front());
callback_queue_.pop();
std::move(callback).Run(completion_result_for_clients);
}
}
// TODO(b/179620544): Move handling of unexpected shutdowns to
// BorealisLaunchWatcher.
void BorealisContextManagerImpl::OnVmStarted(
const vm_tools::concierge::VmStartedSignal& signal) {}
void BorealisContextManagerImpl::OnVmStopped(
const vm_tools::concierge::VmStoppedSignal& signal) {
if (context_ && context_->vm_name() == signal.name() &&
signal.owner_id() ==
ash::ProfileHelper::GetUserIdHashFromProfile(profile_)) {
CloseBorealisSplashScreenView();
// If |context_| exists, it's a "running" Borealis instance which we didn't
// request to shut down.
context_->NotifyUnexpectedVmShutdown();
// Update our state to reflect the unexpected VM exit.
context_.reset();
}
}
} // namespace borealis