| // 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 "garnet/bin/guest/pkg/biscotti_guest/bin/guest.h" |
| |
| #include <arpa/inet.h> |
| #include <fcntl.h> |
| #include <netinet/in.h> |
| #include <unistd.h> |
| #include <memory> |
| |
| #include <grpc++/grpc++.h> |
| #include <grpc++/server_posix.h> |
| #include <lib/async/default.h> |
| #include <lib/fdio/util.h> |
| #include <lib/fidl/cpp/vector.h> |
| #include <lib/fxl/logging.h> |
| #include <lib/fzl/fdio.h> |
| #include <zircon/processargs.h> |
| |
| #include "garnet/bin/guest/pkg/biscotti_guest/third_party/protos/vm_guest.grpc.pb.h" |
| |
| namespace biscotti { |
| |
| // If this is true, a container shell is spawned on /dev/hvc0 logged into the |
| // default 'machina' user. If this is false then the shell on /dev/hvc0 will |
| // be a root shell for the VM. |
| // |
| // Generally 'true' here will be more useful but we'll keep it around to enable |
| // debugging any issues with container startup. |
| static constexpr bool kBootToContainer = true; |
| |
| static constexpr const char* kLinuxEnvirionmentName = "biscotti"; |
| static constexpr const char* kLinuxGuestPackage = |
| "fuchsia-pkg://fuchsia.com/biscotti_guest#meta/biscotti_guest.cmx"; |
| static constexpr uint32_t kStartupListenerPort = 7777; |
| static constexpr uint32_t kTremplinListenerPort = 7778; |
| static constexpr uint32_t kMaitredPort = 8888; |
| static constexpr uint32_t kGarconPort = 8889; |
| static constexpr uint32_t kTremplinPort = 8890; |
| static constexpr uint32_t kLogCollectorPort = 9999; |
| static constexpr const char* kVmShellCommand = "/bin/sh"; |
| static constexpr const char* kContainerName = "stretch"; |
| static constexpr const char* kContainerImageAlias = "debian/stretch"; |
| static constexpr const char* kContainerImageServer = |
| "https://storage.googleapis.com/cros-containers"; |
| static constexpr const char* kDefaultContainerUser = "machina"; |
| static constexpr const char* kLinuxUriScheme = "linux://"; |
| |
| // Minfs max file size is currently just under 4GB. |
| static constexpr off_t kStatefulImageSize = 4000ul * 1024 * 1024; |
| static constexpr const char* kStatefulImagePath = "/data/stateful.img"; |
| |
| static fidl::InterfaceHandle<fuchsia::io::File> GetOrCreateStatefulPartition() { |
| int fd = open(kStatefulImagePath, O_RDWR | O_CREAT); |
| if (fd < 0) { |
| FXL_LOG(ERROR) << "Failed to open image: " << strerror(errno); |
| return nullptr; |
| } |
| if (ftruncate(fd, kStatefulImageSize) < 0) { |
| FXL_LOG(ERROR) << "Failed to truncate image: " << strerror(errno); |
| return nullptr; |
| } |
| |
| zx_handle_t handle = ZX_HANDLE_INVALID; |
| zx_status_t status = fdio_get_service_handle(fd, &handle); |
| if (status != ZX_OK) { |
| FXL_LOG(ERROR) << "Failed to get service handle: " << status; |
| return nullptr; |
| } |
| return fidl::InterfaceHandle<fuchsia::io::File>(zx::channel(handle)); |
| } |
| |
| static fidl::VectorPtr<fuchsia::guest::BlockDevice> GetBlockDevices() { |
| auto file_handle = GetOrCreateStatefulPartition(); |
| FXL_CHECK(file_handle) << "Failed to open stateful file"; |
| fidl::VectorPtr<fuchsia::guest::BlockDevice> devices; |
| devices.push_back({ |
| "stateful", |
| fuchsia::guest::BlockMode::READ_WRITE, |
| fuchsia::guest::BlockFormat::RAW, |
| std::move(file_handle), |
| }); |
| return devices; |
| } |
| |
| static int convert_socket_to_fd(zx::socket socket) { |
| int fd; |
| uint32_t type = PA_FDIO_SOCKET; |
| auto handle = socket.release(); |
| zx_status_t status = fdio_create_fd(&handle, &type, 1, &fd); |
| if (status != ZX_OK) { |
| FXL_LOG(ERROR) << "Could not get client fdio endpoint"; |
| return -1; |
| } |
| |
| auto flags = fcntl(fd, F_GETFL); |
| if (flags == -1) { |
| FXL_LOG(ERROR) << "fcntl(F_GETFL) failed: " << strerror(errno); |
| return -1; |
| } |
| |
| if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { |
| FXL_LOG(ERROR) << "fcntl(F_SETFL) failed: " << strerror(errno); |
| return -1; |
| } |
| return fd; |
| } |
| |
| // static |
| zx_status_t Guest::CreateAndStart(component::StartupContext* context, |
| fxl::CommandLine cl, |
| std::unique_ptr<Guest>* guest) { |
| FXL_LOG(INFO) << "Creating Guest Environment..."; |
| fuchsia::guest::EnvironmentManagerPtr guestmgr; |
| context->ConnectToEnvironmentService(guestmgr.NewRequest()); |
| fuchsia::guest::EnvironmentControllerPtr guest_env; |
| guestmgr->Create(kLinuxEnvirionmentName, guest_env.NewRequest()); |
| |
| *guest = |
| std::make_unique<Guest>(context, std::move(guest_env), std::move(cl)); |
| return ZX_OK; |
| } |
| |
| Guest::Guest(component::StartupContext* context, |
| fuchsia::guest::EnvironmentControllerPtr env, fxl::CommandLine cl) |
| : guest_env_(std::move(env)), |
| cl_(std::move(cl)), |
| wayland_dispatcher_(context, fit::bind_member(this, &Guest::OnNewView)) { |
| guest_env_->GetHostVsockEndpoint(socket_endpoint_.NewRequest()); |
| async_ = async_get_default_dispatcher(); |
| Start(); |
| } |
| |
| void Guest::Start() { |
| StartGrpcServer(); |
| StartGuest(); |
| } |
| |
| // A thin wrapper around |grpc::ServerBuilder| that also registers the service |
| // ports with the |HostVsockEndpoint|. |
| class GrpcServerBuilder { |
| public: |
| // The BindingFactory is a function that returns an |InterfaceHandle| to use |
| // for a new binding. |
| using BindingFactory = |
| fit::function<fidl::InterfaceHandle<fuchsia::guest::HostVsockAcceptor>()>; |
| |
| GrpcServerBuilder( |
| const fuchsia::guest::HostVsockEndpointSyncPtr& socket_endpoint, |
| BindingFactory binding_factory) |
| : binding_factory_(std::move(binding_factory)), |
| socket_endpoint_(socket_endpoint) {} |
| |
| // Registers the service on the provided vsock port. |
| // |
| // Note that this actually makes all services available on all ports. Ex, |
| // if you register 'service A' on 'port A' and 'service B' on 'port B', |
| // requests for 'service B' that are sent to 'port A' would still be handled. |
| // This is because all the services are backed by the same gRPC server |
| // instance. |
| zx_status_t RegisterService(uint32_t vsock_port, grpc::Service* service) { |
| builder_.RegisterService(service); |
| zx_status_t listen_status, fidl_status; |
| fidl_status = socket_endpoint_->Listen(vsock_port, binding_factory_(), |
| &listen_status); |
| if (fidl_status != ZX_OK) { |
| FXL_LOG(ERROR) << "Failed to perform vsock Listen RCP"; |
| return fidl_status; |
| } |
| return listen_status; |
| } |
| |
| // Constructs the |grpc::Server| and starts processing any in-bound requests |
| // on the sockets. |
| std::unique_ptr<grpc::Server> Build() { return builder_.BuildAndStart(); } |
| |
| private: |
| BindingFactory binding_factory_; |
| const fuchsia::guest::HostVsockEndpointSyncPtr& socket_endpoint_; |
| grpc::ServerBuilder builder_; |
| }; |
| |
| void Guest::StartGrpcServer() { |
| FXL_LOG(INFO) << "Starting GRPC server..."; |
| zx_status_t status; |
| GrpcServerBuilder builder(socket_endpoint_, [this]() { |
| return acceptor_bindings_.AddBinding(this); |
| }); |
| |
| status = builder.RegisterService(kLogCollectorPort, &log_collector_); |
| FXL_CHECK(status == ZX_OK) << "Failed to register LogCollector service."; |
| |
| status = builder.RegisterService( |
| kStartupListenerPort, |
| static_cast<vm_tools::StartupListener::Service*>(this)); |
| FXL_CHECK(status == ZX_OK) << "Failed to register StartupListener service."; |
| |
| status = builder.RegisterService( |
| kTremplinListenerPort, |
| static_cast<vm_tools::tremplin::TremplinListener::Service*>(this)); |
| FXL_CHECK(status == ZX_OK) << "Failed to register TremplinListener service."; |
| |
| status = builder.RegisterService( |
| kGarconPort, |
| static_cast<vm_tools::container::ContainerListener::Service*>(this)); |
| FXL_CHECK(status == ZX_OK) << "Failed to register ContainerListener service."; |
| |
| grpc_server_ = builder.Build(); |
| } |
| |
| void Guest::StartGuest() { |
| FXL_CHECK(!guest_controller_) |
| << "Called StartGuest with an existing instance"; |
| FXL_LOG(INFO) << "Launching guest..."; |
| |
| fuchsia::guest::LaunchInfo launch_info; |
| launch_info.url = kLinuxGuestPackage; |
| launch_info.args.push_back("--virtio-gpu=false"); |
| launch_info.args.push_back("--legacy-net=false"); |
| launch_info.block_devices = GetBlockDevices(); |
| launch_info.wayland_device = fuchsia::guest::WaylandDevice::New(); |
| launch_info.wayland_device->dispatcher = wayland_dispatcher_.NewBinding(); |
| guest_env_->LaunchInstance( |
| std::move(launch_info), guest_controller_.NewRequest(), |
| [this](uint32_t cid) { |
| FXL_LOG(INFO) << "Guest launched with CID " << cid; |
| guest_cid_ = cid; |
| }); |
| } |
| |
| void Guest::ConfigureNetwork() { |
| FXL_CHECK(maitred_) |
| << "Called ConfigureNetwork without a maitre'd connection"; |
| std::string arg; |
| struct in_addr addr; |
| |
| uint32_t ip_addr = 0; |
| if (!cl_.GetOptionValue("ip", &arg)) { |
| arg = BISCOTTI_IP_DEFAULT; |
| } |
| FXL_LOG(INFO) << "Using ip: " << arg; |
| FXL_CHECK(inet_aton(arg.c_str(), &addr) != 0) |
| << "Failed to parse address string"; |
| ip_addr = addr.s_addr; |
| |
| uint32_t netmask = 0; |
| if (!cl_.GetOptionValue("netmask", &arg)) { |
| arg = BISCOTTI_NETMASK_DEFAULT; |
| } |
| FXL_LOG(INFO) << "Using netmask: " << arg; |
| FXL_CHECK(inet_aton(arg.c_str(), &addr) != 0) |
| << "Failed to parse address string"; |
| netmask = addr.s_addr; |
| |
| uint32_t gateway = 0; |
| if (!cl_.GetOptionValue("gateway", &arg)) { |
| arg = BISCOTTI_GATEWAY_DEFAULT; |
| } |
| FXL_LOG(INFO) << "Using gateway: " << arg; |
| FXL_CHECK(inet_aton(arg.c_str(), &addr) != 0) |
| << "Failed to parse address string"; |
| gateway = addr.s_addr; |
| FXL_LOG(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); |
| |
| auto grpc_status = maitred_->ConfigureNetwork(&context, request, &response); |
| FXL_CHECK(grpc_status.ok()) |
| << "Failed to configure guest network: " << grpc_status.error_message(); |
| FXL_LOG(INFO) << "Network configured."; |
| } |
| |
| void Guest::StartTermina() { |
| FXL_CHECK(maitred_) << "Called StartTermina without a maitre'd connection"; |
| FXL_LOG(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); |
| |
| auto grpc_status = maitred_->StartTermina(&context, request, &response); |
| FXL_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::LaunchVmShell() { |
| FXL_CHECK(maitred_) << "Called LaunchShell without a maitre'd connection"; |
| FXL_LOG(INFO) << "Launching '" << kVmShellCommand << "'..."; |
| |
| grpc::ClientContext context; |
| vm_tools::LaunchProcessRequest request; |
| vm_tools::LaunchProcessResponse response; |
| |
| request.add_argv()->assign(kVmShellCommand); |
| request.set_respawn(true); |
| request.set_use_console(true); |
| request.set_wait_for_exit(false); |
| { |
| auto env = request.mutable_env(); |
| // These make the lxd/lxc commands behave as expected from the shell. |
| env->insert({"LXD_DIR", "/mnt/stateful/lxd"}); |
| env->insert({"LXD_CONF", "/mnt/stateful/lxd_conf"}); |
| env->insert({"LXD_UNPRIVILEGED_ONLY", "true"}); |
| } |
| |
| auto status = maitred_->LaunchProcess(&context, request, &response); |
| FXL_CHECK(status.ok()) << "Failed to launch '" << kVmShellCommand |
| << "': " << status.error_message(); |
| } |
| |
| // This exposes a shell on /dev/hvc0 that can be used to interact with the |
| // VM. |
| void Guest::LaunchContainerShell() { |
| FXL_CHECK(maitred_) << "Called LaunchShell without a maitre'd connection"; |
| FXL_LOG(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"}); |
| } |
| |
| auto status = maitred_->LaunchProcess(&context, request, &response); |
| FXL_CHECK(status.ok()) << "Failed to launch container shell: " |
| << status.error_message(); |
| } |
| |
| void Guest::CreateContainer() { |
| FXL_CHECK(tremplin_) |
| << "CreateContainer called without a Tremplin connection"; |
| FXL_LOG(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); |
| |
| auto status = tremplin_->CreateContainer(&context, request, &response); |
| FXL_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: |
| FXL_LOG(INFO) << "Container already exists"; |
| StartContainer(); |
| break; |
| case vm_tools::tremplin::CreateContainerResponse::FAILED: |
| FXL_LOG(ERROR) << "Failed to create container: " |
| << response.failure_reason(); |
| break; |
| case vm_tools::tremplin::CreateContainerResponse::UNKNOWN: |
| default: |
| FXL_LOG(ERROR) << "Unknown status: " << response.status(); |
| break; |
| } |
| } |
| |
| void Guest::StartContainer() { |
| FXL_CHECK(tremplin_) << "StartContainer called without a Tremplin connection"; |
| FXL_LOG(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"); |
| |
| auto status = tremplin_->StartContainer(&context, request, &response); |
| FXL_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: |
| FXL_LOG(INFO) << "Container started"; |
| SetupUser(); |
| break; |
| case vm_tools::tremplin::StartContainerResponse::FAILED: |
| FXL_LOG(ERROR) << "Failed to start container: " |
| << response.failure_reason(); |
| break; |
| case vm_tools::tremplin::StartContainerResponse::UNKNOWN: |
| default: |
| FXL_LOG(ERROR) << "Unknown status: " << response.status(); |
| break; |
| } |
| } |
| |
| void Guest::SetupUser() { |
| FXL_CHECK(tremplin_) << "SetupUser called without a Tremplin connection"; |
| FXL_LOG(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); |
| auto status = tremplin_->SetUpUser(&context, request, &response); |
| FXL_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: |
| FXL_LOG(INFO) << "User created."; |
| if (kBootToContainer) { |
| LaunchContainerShell(); |
| } |
| break; |
| case vm_tools::tremplin::SetUpUserResponse::FAILED: |
| FXL_LOG(ERROR) << "Failed to create user: " << response.failure_reason(); |
| break; |
| case vm_tools::tremplin::SetUpUserResponse::UNKNOWN: |
| default: |
| FXL_LOG(ERROR) << "Unknown status: " << response.status(); |
| break; |
| } |
| } |
| |
| // We've received a new vsock connection from a guest. We need to create a |
| // socket for this client and hand one end over to the |grpc::Server|. |
| void Guest::Accept(uint32_t src_cid, uint32_t src_port, uint32_t port, |
| AcceptCallback callback) { |
| FXL_CHECK(grpc_server_); |
| FXL_LOG(INFO) << "Inbound connection request from CID " << src_cid |
| << " on port " << src_port; |
| zx::socket h1, h2; |
| zx_status_t status = zx::socket::create(ZX_SOCKET_STREAM, &h1, &h2); |
| if (status != ZX_OK) { |
| FXL_LOG(ERROR) << "Failed to create socket " << status; |
| callback(ZX_ERR_CONNECTION_REFUSED, zx::handle()); |
| return; |
| } |
| int fd = convert_socket_to_fd(std::move(h1)); |
| if (fd < 0) { |
| FXL_LOG(ERROR) << "Failed get file descriptor for socket"; |
| callback(ZX_ERR_INTERNAL, zx::socket()); |
| return; |
| } |
| grpc::AddInsecureChannelFromFd(grpc_server_.get(), fd); |
| callback(status, std::move(h2)); |
| } |
| |
| // Creates a new GRPC stub for a service. |
| template <typename T> |
| std::unique_ptr<typename T::Stub> Guest::NewVsockStub(uint32_t cid, |
| uint32_t port) { |
| // Create the socket for the connection. |
| zx::socket h1, h2; |
| zx_status_t status = zx::socket::create(ZX_SOCKET_STREAM, &h1, &h2); |
| if (status != ZX_OK) { |
| FXL_LOG(ERROR) << "Failed to create socket"; |
| return nullptr; |
| } |
| |
| // Establish connection, hand first socket endpoint over to the guest. |
| zx_status_t fidl_status = |
| socket_endpoint_->Connect(cid, port, std::move(h1), &status); |
| if (fidl_status != ZX_OK) { |
| FXL_LOG(ERROR) << "Failed to perform vsock Connect RPC for " |
| << T::service_full_name() << ": " << status; |
| return nullptr; |
| } |
| if (status != ZX_OK) { |
| FXL_LOG(ERROR) << "Failed to connect to " << T::service_full_name() << ": " |
| << status; |
| return nullptr; |
| } |
| |
| // Hand the second socket endpoint to GRPC. We need to use a FDIO interface |
| // to the socket for gRPC. |
| int fd = convert_socket_to_fd(std::move(h2)); |
| if (fd < 0) { |
| FXL_LOG(ERROR) << "Failed to get socket FD"; |
| return nullptr; |
| } |
| auto chan = grpc::CreateInsecureChannelFromFd("vsock", fd); |
| return T::NewStub(std::move(chan)); |
| } |
| |
| grpc::Status Guest::VmReady(grpc::ServerContext* context, |
| const vm_tools::EmptyMessage* request, |
| vm_tools::EmptyMessage* response) { |
| FXL_LOG(INFO) << "VM Ready -- Connecting to Maitre'd..."; |
| maitred_ = NewVsockStub<vm_tools::Maitred>(guest_cid_, kMaitredPort); |
| FXL_CHECK(maitred_) << "Failed to connect to Maitre'd"; |
| |
| // If we're not booting to a container; we'll drop the VM inside a root shell. |
| const bool vm_only = cl_.HasOption("vm"); |
| if (!kBootToContainer || vm_only) { |
| LaunchVmShell(); |
| } |
| if (!vm_only) { |
| ConfigureNetwork(); |
| StartTermina(); |
| } |
| return grpc::Status::OK; |
| } |
| |
| grpc::Status Guest::ContainerStartupFailed( |
| grpc::ServerContext* context, const vm_tools::ContainerName* request, |
| vm_tools::EmptyMessage* response) { |
| FXL_LOG(ERROR) << "Container Startup Failed"; |
| return grpc::Status::OK; |
| } |
| |
| grpc::Status Guest::TremplinReady( |
| grpc::ServerContext* context, |
| const ::vm_tools::tremplin::TremplinStartupInfo* request, |
| vm_tools::tremplin::EmptyMessage* response) { |
| FXL_LOG(INFO) << "Tremplin Ready."; |
| tremplin_ = |
| NewVsockStub<vm_tools::tremplin::Tremplin>(guest_cid_, kTremplinPort); |
| FXL_CHECK(tremplin_) << "Failed to connect to tremplin"; |
| // The post is important here because the guest won't process requests until |
| // this RPC has completed. |
| async::PostTask(async_, [this]() { CreateContainer(); }); |
| return grpc::Status::OK; |
| } |
| |
| grpc::Status Guest::UpdateCreateStatus( |
| grpc::ServerContext* context, |
| const vm_tools::tremplin::ContainerCreationProgress* request, |
| vm_tools::tremplin::EmptyMessage* response) { |
| switch (request->status()) { |
| case vm_tools::tremplin::ContainerCreationProgress::CREATED: |
| FXL_LOG(INFO) << "Container created: " << request->container_name(); |
| StartContainer(); |
| break; |
| case vm_tools::tremplin::ContainerCreationProgress::DOWNLOADING: |
| FXL_LOG(INFO) << "Downloading " << request->container_name() << ": " |
| << request->download_progress() << "%"; |
| break; |
| case vm_tools::tremplin::ContainerCreationProgress::DOWNLOAD_TIMED_OUT: |
| FXL_LOG(INFO) << "Download timed out for " << request->container_name(); |
| break; |
| case vm_tools::tremplin::ContainerCreationProgress::CANCELLED: |
| FXL_LOG(INFO) << "Download cancelled for " << request->container_name(); |
| break; |
| case vm_tools::tremplin::ContainerCreationProgress::FAILED: |
| FXL_LOG(INFO) << "Download failed for " << request->container_name() |
| << ": " << request->failure_reason(); |
| break; |
| case vm_tools::tremplin::ContainerCreationProgress::UNKNOWN: |
| default: |
| FXL_LOG(INFO) << "Unknown download status: " << request->status(); |
| break; |
| } |
| return grpc::Status::OK; |
| } |
| |
| grpc::Status Guest::ContainerReady( |
| grpc::ServerContext* context, |
| const vm_tools::container::ContainerStartupInfo* request, |
| vm_tools::EmptyMessage* response) { |
| // TODO(tjdetwiler): validate token. |
| auto garcon_port = request->garcon_port(); |
| FXL_LOG(INFO) << "Container Ready; Garcon listening on port " << garcon_port; |
| garcon_ = NewVsockStub<vm_tools::container::Garcon>(guest_cid_, garcon_port); |
| |
| DumpContainerDebugInfo(); |
| |
| for (auto it = pending_requests_.begin(); it != pending_requests_.end(); |
| it = pending_requests_.erase(it)) { |
| LaunchApplication(std::move(*it)); |
| } |
| |
| return grpc::Status::OK; |
| } |
| |
| grpc::Status Guest::ContainerShutdown( |
| grpc::ServerContext* context, |
| const vm_tools::container::ContainerShutdownInfo* request, |
| vm_tools::EmptyMessage* response) { |
| FXL_LOG(INFO) << "Container Shutdown"; |
| return grpc::Status::OK; |
| } |
| |
| grpc::Status Guest::UpdateApplicationList( |
| grpc::ServerContext* context, |
| const vm_tools::container::UpdateApplicationListRequest* request, |
| vm_tools::EmptyMessage* response) { |
| FXL_LOG(INFO) << "Update Application List"; |
| for (const auto& application : request->application()) { |
| FXL_LOG(INFO) << "ID: " << application.desktop_file_id(); |
| const auto& name = application.name().values().begin(); |
| if (name != application.name().values().end()) { |
| FXL_LOG(INFO) << "\tname: " << name->value(); |
| } |
| const auto& comment = application.comment().values().begin(); |
| if (comment != application.comment().values().end()) { |
| FXL_LOG(INFO) << "\tcomment: " << comment->value(); |
| } |
| FXL_LOG(INFO) << "\tno_display: " << application.no_display(); |
| FXL_LOG(INFO) << "\tstartup_wm_class: " << application.startup_wm_class(); |
| FXL_LOG(INFO) << "\tstartup_notify: " << application.startup_notify(); |
| FXL_LOG(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) { |
| FXL_LOG(INFO) << "Open URL"; |
| return grpc::Status::OK; |
| } |
| |
| grpc::Status Guest::InstallLinuxPackageProgress( |
| grpc::ServerContext* context, |
| const vm_tools::container::InstallLinuxPackageProgressInfo* request, |
| vm_tools::EmptyMessage* response) { |
| FXL_LOG(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) { |
| FXL_LOG(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) { |
| FXL_LOG(INFO) << "Open Terminal"; |
| return grpc::Status::OK; |
| } |
| |
| grpc::Status Guest::UpdateMimeTypes( |
| grpc::ServerContext* context, |
| const vm_tools::container::UpdateMimeTypesRequest* request, |
| vm_tools::EmptyMessage* response) { |
| FXL_LOG(INFO) << "Update Mime Types"; |
| size_t i = 0; |
| for (const auto& pair : request->mime_type_mappings()) { |
| FXL_LOG(INFO) << "\t" << pair.first << ": " << pair.second; |
| if (++i > 10) { |
| FXL_LOG(INFO) << "\t..." << (request->mime_type_mappings_size() - i) |
| << " more."; |
| break; |
| } |
| } |
| return grpc::Status::OK; |
| } |
| |
| void Guest::DumpContainerDebugInfo() { |
| FXL_CHECK(garcon_) |
| << "Called DumpContainerDebugInfo without a garcon connection"; |
| FXL_LOG(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()) { |
| FXL_LOG(ERROR) << "Failed to read container debug information: " |
| << grpc_status.error_message(); |
| return; |
| } |
| |
| FXL_LOG(INFO) << "Container debug information:"; |
| FXL_LOG(INFO) << response.debug_information(); |
| } |
| |
| void Guest::Launch(AppLaunchRequest request) { |
| // 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) { |
| FXL_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) { |
| FXL_LOG(ERROR) << "Invalid URI: " << desktop_file_id; |
| return; |
| } |
| desktop_file_id.erase(0, strlen(kLinuxUriScheme)); |
| if (desktop_file_id == "") { |
| // HACK: 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. |
| auto it = background_views_.begin(); |
| if (it == background_views_.end()) { |
| FXL_LOG(INFO) << "No background views available"; |
| return; |
| } |
| CreateComponent(std::move(app), it->Bind()); |
| background_views_.erase(it); |
| return; |
| } |
| |
| FXL_LOG(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)); |
| auto grpc_status = garcon_->LaunchApplication(&context, request, &response); |
| if (!grpc_status.ok() || !response.success()) { |
| FXL_LOG(ERROR) << "Failed to launch application: " |
| << grpc_status.error_message() << ", " |
| << response.failure_reason(); |
| return; |
| } |
| |
| FXL_LOG(INFO) << "Application launched successfully"; |
| pending_views_.push_back(std::move(app)); |
| } |
| |
| void Guest::OnNewView( |
| fidl::InterfaceHandle<fuchsia::ui::app::ViewProvider> view_provider) { |
| // 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 = pending_views_.begin(); |
| if (it == pending_views_.end()) { |
| background_views_.push_back(std::move(view_provider)); |
| return; |
| } |
| CreateComponent(std::move(*it), std::move(view_provider)); |
| pending_views_.erase(it); |
| } |
| |
| void Guest::CreateComponent( |
| AppLaunchRequest request, |
| fidl::InterfaceHandle<fuchsia::ui::app::ViewProvider> view_provider) { |
| auto component = LinuxComponent::Create( |
| fit::bind_member(this, &Guest::OnComponentTerminated), |
| std::move(request.application), std::move(request.startup_info), |
| std::move(request.controller_request), view_provider.Bind()); |
| components_.insert({component.get(), std::move(component)}); |
| } |
| |
| void Guest::OnComponentTerminated(const LinuxComponent* component) { |
| components_.erase(component); |
| } |
| |
| } // namespace biscotti |