blob: 5a3e8806d3a0ba98f2745d0e2582372cf7387236 [file] [log] [blame]
// Copyright 2018 The Fuchsia 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 "src/virtualization/bin/linux_runner/guest.h"
#include <arpa/inet.h>
#include <fcntl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <lib/async/default.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fidl/cpp/vector.h>
#include <lib/syslog/cpp/macros.h>
#include <netinet/in.h>
#include <sys/mount.h>
#include <unistd.h>
#include <zircon/processargs.h>
#include <memory>
#include <fbl/unique_fd.h>
#include "src/virtualization/bin/linux_runner/ports.h"
#include "src/virtualization/lib/grpc/grpc_vsock_stub.h"
#include "src/virtualization/lib/guest_config/guest_config.h"
#include "src/virtualization/third_party/vm_tools/vm_guest.grpc.pb.h"
#include <grpc++/grpc++.h>
#include <grpc++/server_posix.h>
namespace {
constexpr const char* kLinuxEnvirionmentName = "termina";
constexpr const char* kLinuxGuestPackage =
"fuchsia-pkg://fuchsia.com/termina_guest#meta/termina_guest.cmx";
constexpr const char* kContainerName = "buster";
constexpr const char* kContainerImageAlias = "debian/buster";
constexpr const char* kContainerImageServer = "https://storage.googleapis.com/cros-containers/%d";
constexpr const char* kDefaultContainerUser = "machina";
constexpr const char* kLinuxUriScheme = "linux://";
constexpr const char* kVshTerminalComponent =
"fuchsia-pkg://fuchsia.com/terminal#meta/vsh-terminal.cmx";
constexpr const char* kWaylandBridgePackage =
"fuchsia-pkg://fuchsia.com/wayland_bridge#meta/wayland_bridge.cmx";
constexpr const char* kLegacyWaylandBridgePackage =
"fuchsia-pkg://fuchsia.com/wayland_bridge#meta/legacy_wayland_bridge.cmx";
#if defined(USE_VOLATILE_BLOCK)
constexpr bool kForceVolatileWrites = true;
#else
constexpr bool kForceVolatileWrites = false;
#endif
// Information about a disk image.
struct DiskImage {
const char* path; // Path to the file containing the image
fuchsia::virtualization::BlockFormat format; // Format of the disk image
bool read_only;
};
#ifdef USE_PREBUILT_STATEFUL_IMAGE
constexpr DiskImage kStatefulImage = DiskImage{
.path = "/pkg/data/stateful.qcow2",
.format = fuchsia::virtualization::BlockFormat::QCOW,
.read_only = true,
};
#else
constexpr DiskImage kStatefulImage = DiskImage{
// Minfs max file size is currently just under 4GB.
.path = "/data/stateful.img",
.format = fuchsia::virtualization::BlockFormat::RAW,
.read_only = false,
};
#endif
constexpr DiskImage kExtrasImage = DiskImage{
.path = "/pkg/data/extras.img",
.format = fuchsia::virtualization::BlockFormat::RAW,
.read_only = true,
};
// Get the underlying fuchsia::io::File handle for the given file descriptor.
//
// Takes ownership of `fd`.
fidl::InterfaceHandle<fuchsia::io::File> GetFileInterfaceFromFd(fbl::unique_fd fd) {
zx_handle_t handle = ZX_HANDLE_INVALID;
zx_status_t status = fdio_get_service_handle(fd.release(), &handle);
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to get service handle: " << status;
return nullptr;
}
return fidl::InterfaceHandle<fuchsia::io::File>(zx::channel(handle));
}
// Open the given disk image.
fidl::InterfaceHandle<fuchsia::io::File> GetPartition(const DiskImage& image) {
TRACE_DURATION("linux_runner", "GetPartition");
fbl::unique_fd fd(open(image.path, image.read_only ? O_RDONLY : O_RDWR));
if (!fd.is_valid()) {
return nullptr;
}
return GetFileInterfaceFromFd(std::move(fd));
}
// Create the given disk image.
fidl::InterfaceHandle<fuchsia::io::File> CreateRawPartition(const char* path, size_t image_size) {
TRACE_DURATION("linux_runner", "CreatePartition");
// Create the image.
fbl::unique_fd fd(open(path, O_RDWR | O_CREAT));
if (!fd.is_valid()) {
FX_LOGS(ERROR) << "Failed to create image: " << path << ": " << strerror(errno);
return nullptr;
}
if (ftruncate(fd.get(), image_size) < 0) {
FX_LOGS(ERROR) << "Failed to truncate image: " << path << ": " << strerror(errno);
return nullptr;
}
return GetFileInterfaceFromFd(std::move(fd));
}
std::vector<fuchsia::virtualization::BlockSpec> GetBlockDevices(size_t stateful_image_size) {
TRACE_DURATION("linux_runner", "GetBlockDevices");
std::vector<fuchsia::virtualization::BlockSpec> devices;
// Get/create the stateful partition.
fidl::InterfaceHandle<fuchsia::io::File> stateful_handle = GetPartition(kStatefulImage);
if (!stateful_handle.is_valid() && !kStatefulImage.read_only) {
static_assert(kStatefulImage.read_only ||
kStatefulImage.format == fuchsia::virtualization::BlockFormat::RAW,
"Read/write images must be in RAW format");
FX_LOGS(INFO) << "Creating stateful partition: " << kStatefulImage.path;
stateful_handle = CreateRawPartition(kStatefulImage.path, stateful_image_size);
}
FX_CHECK(stateful_handle) << "Failed to open or create stateful file";
devices.push_back({
.id = "stateful",
.mode = (kStatefulImage.read_only || kForceVolatileWrites)
? fuchsia::virtualization::BlockMode::VOLATILE_WRITE
: fuchsia::virtualization::BlockMode::READ_WRITE,
.format = kStatefulImage.format,
.file = std::move(stateful_handle),
});
// Add the extras partition if it exists.
fidl::InterfaceHandle<fuchsia::io::File> extras_handle = GetPartition(kExtrasImage);
if (extras_handle) {
devices.push_back({
.id = "extras",
.mode = fuchsia::virtualization::BlockMode::VOLATILE_WRITE,
.format = kExtrasImage.format,
.file = std::move(extras_handle),
});
}
return devices;
}
const char* GetBridgePackage(sys::ComponentContext* context) {
TRACE_DURATION("linux_runner", "GetBridgePackage");
fuchsia::ui::scenic::ScenicSyncPtr scenic;
zx_status_t status = context->svc()->Connect(scenic.NewRequest());
FX_CHECK(status == ZX_OK) << "Failed to connect to Scenic: " << status;
bool scenic_uses_flatland = false;
scenic->UsesFlatland(&scenic_uses_flatland);
FX_LOGS(INFO) << "scenic_uses_flatland: " << scenic_uses_flatland;
return scenic_uses_flatland ? kWaylandBridgePackage : kLegacyWaylandBridgePackage;
}
} // namespace
namespace linux_runner {
// static
zx_status_t Guest::CreateAndStart(sys::ComponentContext* context, GuestConfig config,
std::unique_ptr<Guest>* guest) {
TRACE_DURATION("linux_runner", "Guest::CreateAndStart");
fuchsia::virtualization::ManagerPtr guestmgr;
context->svc()->Connect(guestmgr.NewRequest());
fuchsia::virtualization::RealmPtr guest_env;
guestmgr->Create(kLinuxEnvirionmentName, guest_env.NewRequest());
*guest = std::make_unique<Guest>(context, config, std::move(guest_env));
return ZX_OK;
}
Guest::Guest(sys::ComponentContext* context, GuestConfig config,
fuchsia::virtualization::RealmPtr env)
: async_(async_get_default_dispatcher()),
executor_(async_),
config_(config),
guest_env_(std::move(env)),
wayland_dispatcher_(context, GetBridgePackage(context),
fit::bind_member(this, &Guest::OnNewView),
fit::bind_member(this, &Guest::OnShutdownView)) {
guest_env_->GetHostVsockEndpoint(socket_endpoint_.NewRequest());
executor_.schedule_task(Start());
context->svc()->Connect(launcher_.NewRequest());
}
Guest::~Guest() {
if (grpc_server_) {
grpc_server_->inner()->Shutdown();
grpc_server_->inner()->Wait();
}
}
fpromise::promise<> Guest::Start() {
TRACE_DURATION("linux_runner", "Guest::Start");
return StartGrpcServer()
.and_then([this](std::unique_ptr<GrpcVsockServer>& server) mutable
-> fpromise::result<void, zx_status_t> {
grpc_server_ = std::move(server);
StartGuest();
return fpromise::ok();
})
.or_else([](const zx_status_t& status) {
FX_LOGS(ERROR) << "Failed to start guest: " << status;
return fpromise::ok();
});
}
fpromise::promise<std::unique_ptr<GrpcVsockServer>, zx_status_t> Guest::StartGrpcServer() {
TRACE_DURATION("linux_runner", "Guest::StartGrpcServer");
fuchsia::virtualization::HostVsockEndpointPtr socket_endpoint;
guest_env_->GetHostVsockEndpoint(socket_endpoint.NewRequest());
GrpcVsockServerBuilder builder(std::move(socket_endpoint));
// CrashListener
builder.AddListenPort(kCrashListenerPort);
builder.RegisterService(&crash_listener_);
// LogCollector
builder.AddListenPort(kLogCollectorPort);
builder.RegisterService(&log_collector_);
// StartupListener
builder.AddListenPort(kStartupListenerPort);
builder.RegisterService(static_cast<vm_tools::StartupListener::Service*>(this));
// TremplinListener
builder.AddListenPort(kTremplinListenerPort);
builder.RegisterService(static_cast<vm_tools::tremplin::TremplinListener::Service*>(this));
// ContainerListener
builder.AddListenPort(kGarconPort);
builder.RegisterService(static_cast<vm_tools::container::ContainerListener::Service*>(this));
return builder.Build();
}
void Guest::StartGuest() {
TRACE_DURATION("linux_runner", "Guest::StartGuest");
FX_CHECK(!guest_controller_) << "Called StartGuest with an existing instance";
FX_LOGS(INFO) << "Launching guest...";
fuchsia::virtualization::GuestConfig cfg;
cfg.set_virtio_gpu(false);
cfg.set_block_devices(GetBlockDevices(config_.stateful_image_size));
cfg.mutable_wayland_device()->dispatcher = wayland_dispatcher_.NewBinding();
cfg.set_magma_device(fuchsia::virtualization::MagmaDevice());
auto vm_create_nonce = TRACE_NONCE();
TRACE_FLOW_BEGIN("linux_runner", "LaunchInstance", vm_create_nonce);
guest_env_->LaunchInstance(kLinuxGuestPackage, cpp17::nullopt, std::move(cfg),
guest_controller_.NewRequest(), [this, vm_create_nonce](uint32_t cid) {
TRACE_DURATION("linux_runner", "LaunchInstance Callback");
TRACE_FLOW_END("linux_runner", "LaunchInstance", vm_create_nonce);
FX_LOGS(INFO) << "Guest launched with CID " << cid;
guest_cid_ = cid;
TRACE_FLOW_BEGIN("linux_runner", "TerminaBoot", vm_ready_nonce_);
});
}
void Guest::MountVmTools() {
TRACE_DURATION("linux_runner", "Guest::MountVmTools");
FX_CHECK(maitred_) << "Called MountVmTools without a maitre'd connection";
FX_LOGS(INFO) << "Mounting vm_tools";
grpc::ClientContext context;
vm_tools::MountRequest request;
vm_tools::MountResponse response;
request.mutable_source()->assign("/dev/vdb");
request.mutable_target()->assign("/opt/google/cros-containers");
request.mutable_fstype()->assign("ext4");
request.mutable_options()->assign("");
request.set_mountflags(MS_RDONLY);
{
TRACE_DURATION("linux_runner", "MountRPC");
auto grpc_status = maitred_->Mount(&context, request, &response);
FX_CHECK(grpc_status.ok()) << "Failed to mount vm_tools partition: "
<< grpc_status.error_message();
}
FX_LOGS(INFO) << "Mounted Filesystem: " << response.error();
}
void Guest::MountExtrasPartition() {
TRACE_DURATION("linux_runner", "Guest::MountExtrasPartition");
FX_CHECK(maitred_) << "Called MountExtrasPartition without a maitre'd connection";
FX_LOGS(INFO) << "Mounting Extras Partition";
grpc::ClientContext context;
vm_tools::MountRequest request;
vm_tools::MountResponse response;
request.mutable_source()->assign("/dev/vdd");
request.mutable_target()->assign("/mnt/shared");
request.mutable_fstype()->assign("romfs");
request.mutable_options()->assign("");
request.set_mountflags(0);
{
TRACE_DURATION("linux_runner", "MountRPC");
auto grpc_status = maitred_->Mount(&context, request, &response);
FX_CHECK(grpc_status.ok()) << "Failed to mount extras filesystem: "
<< grpc_status.error_message();
}
FX_LOGS(INFO) << "Mounted Filesystem: " << response.error();
}
void Guest::ConfigureNetwork() {
TRACE_DURATION("linux_runner", "Guest::ConfigureNetwork");
FX_CHECK(maitred_) << "Called ConfigureNetwork without a maitre'd connection";
struct in_addr addr;
uint32_t ip_addr = 0;
FX_LOGS(INFO) << "Using ip: " << LINUX_RUNNER_IP_DEFAULT;
FX_CHECK(inet_aton(LINUX_RUNNER_IP_DEFAULT, &addr) != 0) << "Failed to parse address string";
ip_addr = addr.s_addr;
uint32_t netmask = 0;
FX_LOGS(INFO) << "Using netmask: " << LINUX_RUNNER_NETMASK_DEFAULT;
FX_CHECK(inet_aton(LINUX_RUNNER_NETMASK_DEFAULT, &addr) != 0) << "Failed to parse address string";
netmask = addr.s_addr;
uint32_t gateway = 0;
FX_LOGS(INFO) << "Using gateway: " << LINUX_RUNNER_GATEWAY_DEFAULT;
FX_CHECK(inet_aton(LINUX_RUNNER_GATEWAY_DEFAULT, &addr) != 0) << "Failed to parse address string";
gateway = addr.s_addr;
FX_LOGS(INFO) << "Configuring Guest Network...";
grpc::ClientContext context;
vm_tools::NetworkConfigRequest request;
vm_tools::EmptyMessage response;
vm_tools::IPv4Config* config = request.mutable_ipv4_config();
config->set_address(ip_addr);
config->set_gateway(gateway);
config->set_netmask(netmask);
{
TRACE_DURATION("linux_runner", "ConfigureNetworkRPC");
auto grpc_status = maitred_->ConfigureNetwork(&context, request, &response);
FX_CHECK(grpc_status.ok()) << "Failed to configure guest network: "
<< grpc_status.error_message();
}
FX_LOGS(INFO) << "Network configured.";
}
void Guest::StartTermina() {
TRACE_DURATION("linux_runner", "Guest::StartTermina");
FX_CHECK(maitred_) << "Called StartTermina without a maitre'd connection";
FX_LOGS(INFO) << "Starting Termina...";
grpc::ClientContext context;
vm_tools::StartTerminaRequest request;
vm_tools::StartTerminaResponse response;
std::string lxd_subnet = "100.115.92.1/24";
request.mutable_lxd_ipv4_subnet()->swap(lxd_subnet);
request.set_stateful_device("/dev/vdc");
{
TRACE_DURATION("linux_runner", "StartTerminaRPC");
auto grpc_status = maitred_->StartTermina(&context, request, &response);
FX_CHECK(grpc_status.ok()) << "Failed to start Termina: " << grpc_status.error_message();
}
}
// This exposes a shell on /dev/hvc0 that can be used to interact with the
// VM.
void Guest::LaunchContainerShell() {
FX_CHECK(maitred_) << "Called LaunchShell without a maitre'd connection";
FX_LOGS(INFO) << "Launching container shell...";
grpc::ClientContext context;
vm_tools::LaunchProcessRequest request;
vm_tools::LaunchProcessResponse response;
request.add_argv()->assign("/usr/bin/lxc");
request.add_argv()->assign("exec");
request.add_argv()->assign(kContainerName);
request.add_argv()->assign("--");
request.add_argv()->assign("/bin/login");
request.add_argv()->assign("-f");
request.add_argv()->assign(kDefaultContainerUser);
request.set_respawn(true);
request.set_use_console(true);
request.set_wait_for_exit(false);
{
auto env = request.mutable_env();
env->insert({"LXD_DIR", "/mnt/stateful/lxd"});
env->insert({"LXD_CONF", "/mnt/stateful/lxd_conf"});
env->insert({"LXD_UNPRIVILEGED_ONLY", "true"});
}
{
TRACE_DURATION("linux_runner", "LaunchProcessRPC");
auto status = maitred_->LaunchProcess(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to launch container shell: " << status.error_message();
}
}
void Guest::AddMagmaDeviceToContainer() {
FX_CHECK(maitred_) << "Called AddMagma without a maitre'd connection";
FX_LOGS(INFO) << "Adding magma device to container...";
grpc::ClientContext context;
vm_tools::LaunchProcessRequest request;
vm_tools::LaunchProcessResponse response;
request.add_argv()->assign("/usr/bin/lxc");
request.add_argv()->assign("config");
request.add_argv()->assign("device");
request.add_argv()->assign("add");
request.add_argv()->assign(kContainerName);
request.add_argv()->assign("magma0");
request.add_argv()->assign("unix-char");
request.add_argv()->assign("source=/dev/magma0");
request.add_argv()->assign("mode=0666");
request.set_respawn(false);
request.set_use_console(false);
request.set_wait_for_exit(true);
{
auto env = request.mutable_env();
env->insert({"LXD_DIR", "/mnt/stateful/lxd"});
env->insert({"LXD_CONF", "/mnt/stateful/lxd_conf"});
env->insert({"LXD_UNPRIVILEGED_ONLY", "true"});
}
{
TRACE_DURATION("linux_runner", "LaunchProcessRPC");
auto status = maitred_->LaunchProcess(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to add magma device to container: " << status.error_message();
}
}
void Guest::CreateContainer() {
TRACE_DURATION("linux_runner", "Guest::CreateContainer");
FX_CHECK(tremplin_) << "CreateContainer called without a Tremplin connection";
FX_LOGS(INFO) << "Creating Container...";
grpc::ClientContext context;
vm_tools::tremplin::CreateContainerRequest request;
vm_tools::tremplin::CreateContainerResponse response;
request.mutable_container_name()->assign(kContainerName);
request.mutable_image_alias()->assign(kContainerImageAlias);
request.mutable_image_server()->assign(kContainerImageServer);
{
TRACE_DURATION("linux_runner", "CreateContainerRPC");
auto status = tremplin_->CreateContainer(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to create container: " << status.error_message();
}
switch (response.status()) {
case vm_tools::tremplin::CreateContainerResponse::CREATING:
break;
case vm_tools::tremplin::CreateContainerResponse::EXISTS:
FX_LOGS(INFO) << "Container already exists";
StartContainer();
break;
case vm_tools::tremplin::CreateContainerResponse::FAILED:
FX_LOGS(ERROR) << "Failed to create container: " << response.failure_reason();
break;
case vm_tools::tremplin::CreateContainerResponse::UNKNOWN:
default:
FX_LOGS(ERROR) << "Unknown status: " << response.status();
break;
}
}
void Guest::StartContainer() {
TRACE_DURATION("linux_runner", "Guest::StartContainer");
FX_CHECK(tremplin_) << "StartContainer called without a Tremplin connection";
FX_LOGS(INFO) << "Starting Container...";
grpc::ClientContext context;
vm_tools::tremplin::StartContainerRequest request;
vm_tools::tremplin::StartContainerResponse response;
request.mutable_container_name()->assign(kContainerName);
request.mutable_host_public_key()->assign("");
request.mutable_container_private_key()->assign("");
request.mutable_token()->assign("container_token");
{
TRACE_DURATION("linux_runner", "StartContainerRPC");
auto status = tremplin_->StartContainer(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to start container: " << status.error_message();
}
switch (response.status()) {
case vm_tools::tremplin::StartContainerResponse::RUNNING:
case vm_tools::tremplin::StartContainerResponse::STARTED:
FX_LOGS(INFO) << "Container started";
SetupUser();
break;
case vm_tools::tremplin::StartContainerResponse::STARTING:
FX_LOGS(INFO) << "Container starting";
break;
case vm_tools::tremplin::StartContainerResponse::FAILED:
FX_LOGS(ERROR) << "Failed to start container: " << response.failure_reason();
break;
case vm_tools::tremplin::StartContainerResponse::UNKNOWN:
default:
FX_LOGS(ERROR) << "Unknown status: " << response.status();
break;
}
}
void Guest::SetupUser() {
FX_CHECK(tremplin_) << "SetupUser called without a Tremplin connection";
FX_LOGS(INFO) << "Creating user '" << kDefaultContainerUser << "'...";
grpc::ClientContext context;
vm_tools::tremplin::SetUpUserRequest request;
vm_tools::tremplin::SetUpUserResponse response;
request.mutable_container_name()->assign(kContainerName);
request.mutable_container_username()->assign(kDefaultContainerUser);
{
TRACE_DURATION("linux_runner", "SetUpUserRPC");
auto status = tremplin_->SetUpUser(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to setup user '" << kDefaultContainerUser
<< "': " << status.error_message();
}
switch (response.status()) {
case vm_tools::tremplin::SetUpUserResponse::EXISTS:
case vm_tools::tremplin::SetUpUserResponse::SUCCESS:
FX_LOGS(INFO) << "User created.";
LaunchContainerShell();
AddMagmaDeviceToContainer();
break;
case vm_tools::tremplin::SetUpUserResponse::FAILED:
FX_LOGS(ERROR) << "Failed to create user: " << response.failure_reason();
break;
case vm_tools::tremplin::SetUpUserResponse::UNKNOWN:
default:
FX_LOGS(ERROR) << "Unknown status: " << response.status();
break;
}
}
grpc::Status Guest::VmReady(grpc::ServerContext* context, const vm_tools::EmptyMessage* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::VmReady");
TRACE_FLOW_END("linux_runner", "TerminaBoot", vm_ready_nonce_);
FX_LOGS(INFO) << "VM Ready -- Connecting to Maitre'd...";
std::unique_ptr<vm_tools::Maitred::Stub> maitred_;
auto p = NewGrpcVsockStub<vm_tools::Maitred>(socket_endpoint_, guest_cid_, kMaitredPort)
.then([this](fpromise::result<std::unique_ptr<vm_tools::Maitred::Stub>, zx_status_t>&
result) mutable {
if (result.is_ok()) {
this->maitred_ = std::move(result.value());
MountVmTools();
MountExtrasPartition();
ConfigureNetwork();
StartTermina();
} else {
FX_CHECK(false) << "Failed to connect to Maitre'd";
}
});
executor_.schedule_task(std::move(p));
return grpc::Status::OK;
}
grpc::Status Guest::TremplinReady(grpc::ServerContext* context,
const ::vm_tools::tremplin::TremplinStartupInfo* request,
vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::TremplinReady");
FX_LOGS(INFO) << "Tremplin Ready.";
auto p =
NewGrpcVsockStub<vm_tools::tremplin::Tremplin>(socket_endpoint_, guest_cid_, kTremplinPort)
.then([this](fpromise::result<std::unique_ptr<vm_tools::tremplin::Tremplin::Stub>,
zx_status_t>& result) mutable -> fpromise::result<> {
if (result.is_ok()) {
tremplin_ = std::move(result.value());
CreateContainer();
} else {
FX_LOGS(ERROR) << "Failed to connect to tremplin";
}
return fpromise::ok();
});
executor_.schedule_task(std::move(p));
return grpc::Status::OK;
}
grpc::Status Guest::UpdateCreateStatus(grpc::ServerContext* context,
const vm_tools::tremplin::ContainerCreationProgress* request,
vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateCreateStatus");
switch (request->status()) {
case vm_tools::tremplin::ContainerCreationProgress::CREATED:
FX_LOGS(INFO) << "Container created: " << request->container_name();
StartContainer();
break;
case vm_tools::tremplin::ContainerCreationProgress::DOWNLOADING:
FX_LOGS(INFO) << "Downloading " << request->container_name() << ": "
<< request->download_progress() << "%";
break;
case vm_tools::tremplin::ContainerCreationProgress::DOWNLOAD_TIMED_OUT:
FX_LOGS(INFO) << "Download timed out for " << request->container_name();
break;
case vm_tools::tremplin::ContainerCreationProgress::CANCELLED:
FX_LOGS(INFO) << "Download cancelled for " << request->container_name();
break;
case vm_tools::tremplin::ContainerCreationProgress::FAILED:
FX_LOGS(INFO) << "Download failed for " << request->container_name() << ": "
<< request->failure_reason();
break;
case vm_tools::tremplin::ContainerCreationProgress::UNKNOWN:
default:
FX_LOGS(INFO) << "Unknown download status: " << request->status();
break;
}
return grpc::Status::OK;
}
grpc::Status Guest::UpdateDeletionStatus(
::grpc::ServerContext* context, const ::vm_tools::tremplin::ContainerDeletionProgress* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateDeletionStatus");
FX_LOGS(INFO) << "Update Deletion Status";
return grpc::Status::OK;
}
grpc::Status Guest::UpdateStartStatus(::grpc::ServerContext* context,
const ::vm_tools::tremplin::ContainerStartProgress* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateStartStatus");
FX_LOGS(INFO) << "Update Start Status";
switch (request->status()) {
case vm_tools::tremplin::ContainerStartProgress::STARTED:
FX_LOGS(INFO) << "Container started";
SetupUser();
break;
default:
FX_LOGS(ERROR) << "Unknown start status: " << request->status();
break;
}
return grpc::Status::OK;
}
grpc::Status Guest::UpdateExportStatus(::grpc::ServerContext* context,
const ::vm_tools::tremplin::ContainerExportProgress* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateExportStatus");
FX_LOGS(INFO) << "Update Export Status";
return grpc::Status::OK;
}
grpc::Status Guest::UpdateImportStatus(::grpc::ServerContext* context,
const ::vm_tools::tremplin::ContainerImportProgress* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateImportStatus");
FX_LOGS(INFO) << "Update Import Status";
return grpc::Status::OK;
}
grpc::Status Guest::ContainerShutdown(::grpc::ServerContext* context,
const ::vm_tools::tremplin::ContainerShutdownInfo* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::ContainerShutdown");
FX_LOGS(INFO) << "Container Shutdown";
return grpc::Status::OK;
}
grpc::Status Guest::ContainerReady(grpc::ServerContext* context,
const vm_tools::container::ContainerStartupInfo* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::ContainerReady");
// TODO(tjdetwiler): validate token.
auto garcon_port = request->garcon_port();
FX_LOGS(INFO) << "Container Ready; Garcon listening on port " << garcon_port;
auto p = NewGrpcVsockStub<vm_tools::container::Garcon>(socket_endpoint_, guest_cid_, garcon_port)
.then([this](fpromise::result<std::unique_ptr<vm_tools::container::Garcon::Stub>,
zx_status_t>& result) mutable -> fpromise::result<> {
if (result.is_ok()) {
garcon_ = std::move(result.value());
DumpContainerDebugInfo();
for (auto it = pending_requests_.begin(); it != pending_requests_.end();
it = pending_requests_.erase(it)) {
LaunchApplication(std::move(*it));
}
} else {
FX_LOGS(ERROR) << "Failed to connect to garcon";
}
return fpromise::ok();
});
executor_.schedule_task(std::move(p));
return grpc::Status::OK;
}
grpc::Status Guest::ContainerShutdown(grpc::ServerContext* context,
const vm_tools::container::ContainerShutdownInfo* request,
vm_tools::EmptyMessage* response) {
FX_LOGS(INFO) << "Container Shutdown";
return grpc::Status::OK;
}
grpc::Status Guest::UpdateApplicationList(
grpc::ServerContext* context, const vm_tools::container::UpdateApplicationListRequest* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateApplicationList");
FX_LOGS(INFO) << "Update Application List";
for (const auto& application : request->application()) {
FX_LOGS(INFO) << "ID: " << application.desktop_file_id();
const auto& name = application.name().values().begin();
if (name != application.name().values().end()) {
FX_LOGS(INFO) << "\tname: " << name->value();
}
const auto& comment = application.comment().values().begin();
if (comment != application.comment().values().end()) {
FX_LOGS(INFO) << "\tcomment: " << comment->value();
}
FX_LOGS(INFO) << "\tno_display: " << application.no_display();
FX_LOGS(INFO) << "\tstartup_wm_class: " << application.startup_wm_class();
FX_LOGS(INFO) << "\tstartup_notify: " << application.startup_notify();
FX_LOGS(INFO) << "\tpackage_id: " << application.package_id();
}
return grpc::Status::OK;
}
grpc::Status Guest::OpenUrl(grpc::ServerContext* context,
const vm_tools::container::OpenUrlRequest* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::OpenUrl");
FX_LOGS(INFO) << "Open URL";
return grpc::Status::OK;
}
grpc::Status Guest::InstallLinuxPackageProgress(
grpc::ServerContext* context,
const vm_tools::container::InstallLinuxPackageProgressInfo* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::InstallLinuxPackageProgress");
FX_LOGS(INFO) << "Install Linux Package Progress";
return grpc::Status::OK;
}
grpc::Status Guest::UninstallPackageProgress(
grpc::ServerContext* context, const vm_tools::container::UninstallPackageProgressInfo* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UninstallPackageProgress");
FX_LOGS(INFO) << "Uninstall Package Progress";
return grpc::Status::OK;
}
grpc::Status Guest::OpenTerminal(grpc::ServerContext* context,
const vm_tools::container::OpenTerminalRequest* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::OpenTerminal");
FX_LOGS(INFO) << "Open Terminal";
executor_.schedule_task(fpromise::make_promise([this, request = *request]() {
auto it = dispatched_requests_.begin();
if (it == dispatched_requests_.end()) {
background_terms_.emplace_back(std::move(request));
return;
}
std::vector<std::string> args{request.params().begin(), request.params().end()};
CreateTerminalComponent(std::move(*it), std::move(args));
dispatched_requests_.erase(it);
}));
return grpc::Status::OK;
}
grpc::Status Guest::UpdateMimeTypes(grpc::ServerContext* context,
const vm_tools::container::UpdateMimeTypesRequest* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateMimeTypes");
FX_LOGS(INFO) << "Update Mime Types";
size_t i = 0;
for (const auto& pair : request->mime_type_mappings()) {
FX_LOGS(INFO) << "\t" << pair.first << ": " << pair.second;
if (++i > 10) {
FX_LOGS(INFO) << "\t..." << (request->mime_type_mappings_size() - i) << " more.";
break;
}
}
return grpc::Status::OK;
}
void Guest::DumpContainerDebugInfo() {
FX_CHECK(garcon_) << "Called DumpContainerDebugInfo without a garcon connection";
FX_LOGS(INFO) << "Dumping Container Debug Info...";
grpc::ClientContext context;
vm_tools::container::GetDebugInformationRequest request;
vm_tools::container::GetDebugInformationResponse response;
auto grpc_status = garcon_->GetDebugInformation(&context, request, &response);
if (!grpc_status.ok()) {
FX_LOGS(ERROR) << "Failed to read container debug information: " << grpc_status.error_message();
return;
}
FX_LOGS(INFO) << "Container debug information:";
FX_LOGS(INFO) << response.debug_information();
}
void Guest::Launch(AppLaunchRequest request) {
TRACE_DURATION("linux_runner", "Guest::Launch");
// TODO(fxbug.dev/65874): we use the empty URI to pick up a view that wasn't associated with an
// app launch request. For example, if you started a GUI application from the serial console, a
// wayland view will have been created without a fuchsia component to associate with it.
//
// We'll need to come up with a more proper solution, but this allows us to at least do some
// testing of these views for the time being.
if (request.application.resolved_url == kLinuxUriScheme) {
if (!background_views_.empty()) {
FX_LOGS(INFO) << "Found background view";
auto [id, view] = std::move(background_views_.front());
background_views_.pop_front();
CreateComponent(std::move(request), view.Bind(), id);
} else if (!background_terms_.empty()) {
FX_LOGS(INFO) << "Found background term";
auto term = std::move(background_terms_.front());
background_terms_.pop_front();
std::vector<std::string> args{term.params().begin(), term.params().end()};
CreateTerminalComponent(std::move(request), std::move(args));
} else {
dispatched_requests_.push_back(std::move(request));
}
return;
}
// If we have a garcon connection we can request the launch immediately.
// Otherwise we just retain the request and forward it along once the
// container is started.
if (garcon_) {
LaunchApplication(std::move(request));
return;
}
pending_requests_.push_back(std::move(request));
}
void Guest::LaunchApplication(AppLaunchRequest app) {
TRACE_DURATION("linux_runner", "Guest::LaunchApplication");
FX_CHECK(garcon_) << "Called LaunchApplication without a garcon connection";
std::string desktop_file_id = app.application.resolved_url;
if (desktop_file_id.rfind(kLinuxUriScheme, 0) == std::string::npos) {
FX_LOGS(ERROR) << "Invalid URI: " << desktop_file_id;
return;
}
desktop_file_id.erase(0, strlen(kLinuxUriScheme));
FX_LOGS(INFO) << "Launching: " << desktop_file_id;
grpc::ClientContext context;
vm_tools::container::LaunchApplicationRequest request;
vm_tools::container::LaunchApplicationResponse response;
request.set_desktop_file_id(std::move(desktop_file_id));
{
TRACE_DURATION("linux_runner", "LaunchApplicationRPC");
auto grpc_status = garcon_->LaunchApplication(&context, request, &response);
if (!grpc_status.ok() || !response.success()) {
FX_LOGS(ERROR) << "Failed to launch application: " << grpc_status.error_message() << ", "
<< response.failure_reason();
return;
}
}
FX_LOGS(INFO) << "Application launched successfully";
dispatched_requests_.push_back(std::move(app));
}
void Guest::OnNewView(fidl::InterfaceHandle<fuchsia::ui::app::ViewProvider> view_provider,
uint32_t id) {
TRACE_DURATION("linux_runner", "Guest::OnNewView");
// TODO: This currently just pops a component request off the queue to
// associate with the new view. This is obviously racy but will work until
// we can pipe though a startup id to provide a more accurate correlation.
auto it = dispatched_requests_.begin();
if (it == dispatched_requests_.end()) {
background_views_.push_back({id, std::move(view_provider)});
return;
}
CreateComponent(std::move(*it), std::move(view_provider), id);
dispatched_requests_.erase(it);
}
void Guest::OnShutdownView(uint32_t id) {
TRACE_DURATION("linux_runner", "Guest::OnShutdownView");
auto it = std::remove_if(background_views_.begin(), background_views_.end(),
[id](const BackgroundView& view) { return view.first == id; });
if (it != background_views_.end()) {
background_views_.erase(it, background_views_.end());
} else {
OnComponentTerminated(id);
}
}
void Guest::CreateComponent(AppLaunchRequest request,
fidl::InterfaceHandle<fuchsia::ui::app::ViewProvider> view_provider,
uint32_t id) {
TRACE_DURATION("linux_runner", "Guest::CreateComponent");
auto component = LinuxComponent::Create(
fit::bind_member(this, &Guest::OnComponentTerminated), std::move(request.application),
std::move(request.startup_info.launch_info.directory_request),
std::move(request.controller_request),
{}, // Wayland components are not managed by a ComponentController.
view_provider.Bind(), id);
components_.insert({id, std::move(component)});
}
void Guest::OnComponentTerminated(uint32_t id) { components_.erase(id); }
void Guest::CreateTerminalComponent(AppLaunchRequest app, std::vector<std::string> args) {
TRACE_DURATION("linux_runner", "Guest::CreateTerminalComponent");
static uint32_t next_term_id = 1;
const auto term_id = next_term_id++;
fidl::InterfaceHandle<fuchsia::io::Directory> vsh_svc_dir;
fuchsia::sys::ComponentControllerPtr vsh_controller;
// Transfer most of the launch info except we replace the url, args and directory_request handle.
// The original directory_request is extracted first for use by the LinuxComponent.
zx::channel app_dir_request = std::move(app.startup_info.launch_info.directory_request);
fuchsia::sys::LaunchInfo launch_info = std::move(app.startup_info.launch_info);
launch_info.url = kVshTerminalComponent;
launch_info.arguments = std::move(args); // appended to the existing vsh-terminal.cmx arguments.
launch_info.directory_request = vsh_svc_dir.NewRequest().TakeChannel();
launcher_->CreateComponent(std::move(launch_info), vsh_controller.NewRequest());
auto svc = sys::ServiceDirectory(std::move(vsh_svc_dir));
auto component = LinuxComponent::Create(
[this](uint32_t id) { terminals_.erase(id); }, std::move(app.application),
std::move(app_dir_request), std::move(app.controller_request), std::move(vsh_controller),
svc.Connect<fuchsia::ui::app::ViewProvider>(), term_id);
terminals_.insert({term_id, std::move(component)});
}
} // namespace linux_runner