blob: 5c1e9cab41f3a94ff65fa895efd07c8d7ac7f800 [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 "sandbox.h"
#include <fcntl.h>
#include <lib/async/cpp/task.h>
#include <lib/async/default.h>
#include <lib/component/cpp/startup_context.h>
#include <lib/component/cpp/termination_reason.h>
#include <lib/component/cpp/testing/test_util.h>
#include <lib/fdio/watcher.h>
#include <lib/fsl/io/fd.h>
#include <lib/fxl/logging.h>
#include <lib/fxl/strings/concatenate.h>
#include <lib/pkg_url/fuchsia_pkg_url.h>
#include <zircon/status.h>
#include "garnet/lib/cmx/cmx.h"
using namespace fuchsia::netemul;
namespace netemul {
static const char* kEndpointMountPath = "class/ethernet/";
// give setup processes a maximum of 10s before bailing
static const int kSetupTimeoutSecs = 10;
#define STATIC_MSG_STRUCT(name, msgv) \
struct name { \
static const char* msg; \
}; \
const char* name::msg = msgv;
STATIC_MSG_STRUCT(kMsgApp, "app");
STATIC_MSG_STRUCT(kMsgTest, "test");
STATIC_MSG_STRUCT(kMsgRoot, "root test");
// Sandbox uses two threads to operate:
// a main thread (which it's initialized with)
// + a helper thread.
// The macros below are used to assert that methods on
// the sandbox class are called on the proper thread
#define ASSERT_DISPATCHER(disp) \
ZX_ASSERT((disp) == async_get_default_dispatcher())
#define ASSERT_MAIN_DISPATCHER ASSERT_DISPATCHER(main_dispatcher_)
#define ASSERT_HELPER_DISPATCHER ASSERT_DISPATCHER(helper_loop_->dispatcher())
Sandbox::Sandbox(SandboxArgs args) : args_(std::move(args)) {
auto startup_context = component::StartupContext::CreateFromStartupInfo();
startup_context->ConnectToEnvironmentService(parent_env_.NewRequest());
startup_context->ConnectToEnvironmentService(loader_.NewRequest());
parent_env_.set_error_handler([](zx_status_t err) {
FXL_LOG(ERROR) << "Lost connection to parent environment";
});
}
void Sandbox::Start(async_dispatcher_t* dispatcher) {
if (!parent_env_ || !loader_) {
Terminate(TerminationReason::INTERNAL_ERROR);
return;
}
main_dispatcher_ = dispatcher;
setup_done_ = false;
helper_loop_ =
std::make_unique<async::Loop>(&kAsyncLoopConfigNoAttachToThread);
if (helper_loop_->StartThread("helper-thread") != ZX_OK) {
FXL_LOG(ERROR) << "Can't start config thread";
Terminate(TerminationReason::INTERNAL_ERROR);
return;
}
loader_->LoadUrl(args_.package, [this](fuchsia::sys::PackagePtr package) {
if (!package) {
Terminate(TerminationReason::PACKAGE_NOT_FOUND);
return;
} else if (!package->directory) {
Terminate(TerminationReason::INTERNAL_ERROR);
}
LoadPackage(std::move(package));
});
}
void Sandbox::Terminate(int64_t exit_code, Sandbox::TerminationReason reason) {
// all processes must have been emptied to call callback
ASSERT_MAIN_DISPATCHER;
ZX_ASSERT(procs_.empty());
if (termination_callback_) {
termination_callback_(exit_code, reason);
}
}
void Sandbox::PostTerminate(TerminationReason reason) {
ASSERT_HELPER_DISPATCHER;
PostTerminate(-1, reason);
}
void Sandbox::PostTerminate(int64_t exit_code, TerminationReason reason) {
ASSERT_HELPER_DISPATCHER;
// kill all component controllers before posting termination
procs_.clear();
async::PostTask(main_dispatcher_, [this, exit_code, reason]() {
Terminate(exit_code, reason);
});
}
void Sandbox::Terminate(Sandbox::TerminationReason reason) {
ASSERT_MAIN_DISPATCHER;
Terminate(-1, reason);
}
void Sandbox::LoadPackage(fuchsia::sys::PackagePtr package) {
ASSERT_MAIN_DISPATCHER;
// package is loaded, proceed to parsing cmx and starting child env
component::FuchsiaPkgUrl pkgUrl;
if (!pkgUrl.Parse(package->resolved_url)) {
FXL_LOG(ERROR) << "Can't parse fuchsia url: " << package->resolved_url;
Terminate(TerminationReason::INTERNAL_ERROR);
return;
}
fxl::UniqueFD dirfd =
fsl::OpenChannelAsFileDescriptor(std::move(package->directory));
sandbox_env_ = std::make_shared<SandboxEnv>(args_.package, std::move(dirfd));
component::CmxMetadata cmx;
json::JSONParser json_parser;
if (!cmx.ParseFromFileAt(sandbox_env_->dir().get(), pkgUrl.resource_path(),
&json_parser)) {
FXL_LOG(ERROR) << "cmx file failed to parse: " << json_parser.error_str();
Terminate(TerminationReason::INTERNAL_ERROR);
return;
}
if (!env_config_.ParseFromJSON(cmx.GetFacet(config::Config::Facet),
&json_parser)) {
FXL_LOG(ERROR) << "netemul facet failed to parse: "
<< json_parser.error_str();
Terminate(TerminationReason::INTERNAL_ERROR);
return;
}
async::PostTask(helper_loop_->dispatcher(), [this]() {
if (!ConfigureNetworks()) {
PostTerminate(TerminationReason::INTERNAL_ERROR);
return;
}
ManagedEnvironment::Options root_options;
if (!CreateEnvironmentOptions(env_config_.environment(), &root_options)) {
PostTerminate(TerminationReason::INTERNAL_ERROR);
return;
}
async::PostTask(main_dispatcher_,
[this, root_options = std::move(root_options)]() mutable {
ASSERT_MAIN_DISPATCHER;
root_ = ManagedEnvironment::CreateRoot(
parent_env_, sandbox_env_, std::move(root_options));
root_->SetRunningCallback([this]() {
// configure root environment:
async::PostTask(helper_loop_->dispatcher(), [this]() {
if (!ConfigureRootEnvironment()) {
PostTerminate(TerminationReason::INTERNAL_ERROR);
}
});
});
});
});
}
// configure networks runs in an auxiliary thread, so we can use
// synchronous calls to the fidl service
bool Sandbox::ConfigureNetworks() {
ASSERT_HELPER_DISPATCHER;
// start by configuring the networks:
if (env_config_.networks().empty()) {
return true;
}
network::NetworkContextSyncPtr net_ctx;
auto req = net_ctx.NewRequest();
// bind to network context
async::PostTask(main_dispatcher_, [req = std::move(req), this]() mutable {
sandbox_env_->network_context().GetHandler()(std::move(req));
});
network::NetworkManagerSyncPtr net_manager;
network::EndpointManagerSyncPtr endp_manager;
net_ctx->GetNetworkManager(net_manager.NewRequest());
net_ctx->GetEndpointManager(endp_manager.NewRequest());
for (const auto& net_cfg : env_config_.networks()) {
zx_status_t status;
fidl::InterfaceHandle<network::Network> network_h;
if (net_manager->CreateNetwork(net_cfg.name(), network::NetworkConfig(),
&status, &network_h) != ZX_OK ||
status != ZX_OK) {
FXL_LOG(ERROR) << "Create network failed";
return false;
}
auto network = network_h.BindSync();
for (const auto& endp_cfg : net_cfg.endpoints()) {
network::EndpointConfig fidl_config;
fidl::InterfaceHandle<network::Endpoint> endp_h;
fidl_config.backing = network::EndpointBacking::ETHERTAP;
fidl_config.mtu = endp_cfg.mtu();
if (endp_cfg.mac()) {
fidl_config.mac =
std::make_unique<fuchsia::hardware::ethernet::MacAddress>();
memcpy(&fidl_config.mac->octets[0], endp_cfg.mac()->d, 6);
}
if (endp_manager->CreateEndpoint(endp_cfg.name(), std::move(fidl_config),
&status, &endp_h) != ZX_OK ||
status != ZX_OK) {
FXL_LOG(ERROR) << "Create endpoint failed";
return false;
}
auto endp = endp_h.BindSync();
if (endp_cfg.up()) {
if (endp->SetLinkUp(true) != ZX_OK) {
FXL_LOG(ERROR) << "Set endpoint up failed";
return false;
}
}
// add endpoint to network:
if (network->AttachEndpoint(endp_cfg.name(), &status) != ZX_OK ||
status != ZX_OK) {
FXL_LOG(ERROR) << "Attaching endpoint " << endp_cfg.name()
<< " to network " << net_cfg.name() << " failed";
return false;
}
// save the endpoint handle:
network_handles_.emplace_back(endp.Unbind().TakeChannel());
}
// save the network handle:
network_handles_.emplace_back(network.Unbind().TakeChannel());
}
return true;
}
// Create environment options runs in an auxiliary thread, so we can use
// synchronous calls to fidl services
bool Sandbox::CreateEnvironmentOptions(const config::Environment& config,
ManagedEnvironment::Options* options) {
ASSERT_HELPER_DISPATCHER;
options->name = config.name();
options->inherit_parent_launch_services = config.inherit_services();
std::vector<environment::VirtualDevice>& devices = options->devices;
;
if (!config.devices().empty()) {
network::EndpointManagerSyncPtr epm;
async::PostTask(main_dispatcher_, [req = epm.NewRequest(), this]() mutable {
sandbox_env_->network_context().endpoint_manager().Bind(std::move(req));
});
for (const auto& device : config.devices()) {
auto& nd = devices.emplace_back();
nd.path = fxl::Concatenate({std::string(kEndpointMountPath), device});
fidl::InterfaceHandle<network::Endpoint> endp_h;
if (epm->GetEndpoint(device, &endp_h) != ZX_OK) {
FXL_LOG(ERROR) << "Can't find endpoint " << device
<< " on endpoint manager";
return false;
}
auto endp = endp_h.BindSync();
if (endp->GetProxy(nd.device.NewRequest()) != ZX_OK) {
FXL_LOG(ERROR) << "Can't get proxy on endpoint " << device;
return false;
}
}
}
std::vector<environment::LaunchService>& services = options->services;
for (const auto& svc : config.services()) {
auto& ns = services.emplace_back();
ns.name = svc.name();
ns.url = svc.launch().GetUrlOrDefault(sandbox_env_->name());
ns.arguments->insert(ns.arguments->end(), svc.launch().arguments().begin(),
svc.launch().arguments().end());
}
return true;
}
bool Sandbox::ConfigureRootEnvironment() {
ASSERT_HELPER_DISPATCHER;
// connect to environment:
environment::ManagedEnvironmentSyncPtr svc;
auto req = svc.NewRequest();
async::PostTask(main_dispatcher_, [this, req = std::move(req)]() mutable {
root_->Bind(std::move(req));
});
// configure with service pointer:
return ConfigureEnvironment(std::move(svc), env_config_.environment(), true);
}
// Configure environment runs in an auxiliary thread, so we can use synchronous
// calls to fidl services
bool Sandbox::ConfigureEnvironment(
fidl::SynchronousInterfacePtr<ManagedEnvironment::FManagedEnvironment> env,
const config::Environment& config, bool root) {
ASSERT_HELPER_DISPATCHER;
// iterate on children
for (const auto& child : config.children()) {
ManagedEnvironment::Options options;
if (!CreateEnvironmentOptions(child, &options)) {
return false;
}
environment::ManagedEnvironmentSyncPtr child_env;
if (env->CreateChildEnvironment(child_env.NewRequest(),
std::move(options)) != ZX_OK) {
FXL_LOG(ERROR) << "Creating environment \"" << child.name()
<< "\" failed";
return false;
}
// child environment was successfully created, configure child:
if (!ConfigureEnvironment(std::move(child_env), child)) {
return false;
}
}
// get launcher
fuchsia::sys::LauncherSyncPtr launcher;
if (env->GetLauncher(launcher.NewRequest()) != ZX_OK) {
FXL_LOG(ERROR) << "Can't get environment launcher";
return false;
}
for (const auto& app : config.apps()) {
if (!LaunchProcess<kMsgApp>(&launcher,
app.GetUrlOrDefault(sandbox_env_->name()),
app.arguments(), false)) {
return false;
}
}
for (const auto& setup : config.setup()) {
if (!LaunchSetup(&launcher, setup.GetUrlOrDefault(sandbox_env_->name()),
setup.arguments())) {
return false;
}
}
for (const auto& test : config.test()) {
if (!LaunchProcess<kMsgTest>(&launcher,
test.GetUrlOrDefault(sandbox_env_->name()),
test.arguments(), true)) {
return false;
}
}
if (root) {
if (!LaunchProcess<kMsgRoot>(&launcher, sandbox_env_->name(), args_.args,
true)) {
return false;
}
EnableTestObservation();
}
return true;
}
template <typename T>
bool Sandbox::LaunchProcess(fuchsia::sys::LauncherSyncPtr* launcher,
const std::string& url,
const std::vector<std::string>& arguments,
bool is_test) {
ASSERT_HELPER_DISPATCHER;
fuchsia::sys::LaunchInfo linfo;
linfo.url = url;
linfo.arguments->insert(linfo.arguments->end(), arguments.begin(),
arguments.end());
linfo.out = component::testing::CloneFileDescriptor(STDOUT_FILENO);
linfo.err = component::testing::CloneFileDescriptor(STDERR_FILENO);
auto& proc = procs_.emplace_back();
auto ticket = procs_.size();
if (is_test) {
RegisterTest(ticket);
}
// we observe test processes return code
proc.events().OnTerminated = [url, this, is_test, ticket](
int64_t code, TerminationReason reason) {
FXL_LOG(INFO) << T::msg << " " << url << " terminated with (" << code
<< ") reason: "
<< component::HumanReadableTerminationReason(reason);
if (is_test) {
if (code != 0 || reason != TerminationReason::EXITED) {
// test failed, early bail
PostTerminate(code, reason);
} else {
// unregister test ticket
UnregisterTest(ticket);
}
}
};
if ((*launcher)->CreateComponent(std::move(linfo), proc.NewRequest()) !=
ZX_OK) {
FXL_LOG(ERROR) << "couldn't launch " << T::msg << ": " << url;
return false;
}
return true;
}
bool Sandbox::LaunchSetup(fuchsia::sys::LauncherSyncPtr* launcher,
const std::string& url,
const std::vector<std::string>& arguments) {
ASSERT_HELPER_DISPATCHER;
fuchsia::sys::LaunchInfo linfo;
linfo.url = url;
linfo.arguments->insert(linfo.arguments->end(), arguments.begin(),
arguments.end());
linfo.out = component::testing::CloneFileDescriptor(STDOUT_FILENO);
linfo.err = component::testing::CloneFileDescriptor(STDERR_FILENO);
fuchsia::sys::ComponentControllerPtr proc;
bool done = false;
bool success = false;
// we observe test processes return code
proc.events().OnTerminated = [url, this, &done, &success](
int64_t code, TerminationReason reason) {
FXL_LOG(INFO) << "Setup " << url << " terminated with (" << code
<< ") reason: "
<< component::HumanReadableTerminationReason(reason);
done = true;
success = code == 0 && reason == TerminationReason::EXITED;
};
if ((*launcher)->CreateComponent(std::move(linfo), proc.NewRequest()) !=
ZX_OK) {
FXL_LOG(ERROR) << "couldn't launch setup: " << url;
return false;
}
while (!done) {
auto status =
helper_loop_->Run(zx::deadline_after(zx::sec(kSetupTimeoutSecs)), true);
if (status != ZX_OK) {
FXL_LOG(ERROR) << "Setup " << url << " run loop exited with: "
<< zx_status_get_string(status);
return false;
}
}
return success;
}
void Sandbox::EnableTestObservation() {
ASSERT_HELPER_DISPATCHER;
setup_done_ = true;
if (tests_.empty()) {
// all tests finished successfully
PostTerminate(0, TerminationReason::EXITED);
}
}
void Sandbox::RegisterTest(size_t ticket) {
ASSERT_HELPER_DISPATCHER;
tests_.insert(ticket);
}
void Sandbox::UnregisterTest(size_t ticket) {
ASSERT_HELPER_DISPATCHER;
tests_.erase(ticket);
if (setup_done_ && tests_.empty()) {
// all tests finished successfully
PostTerminate(0, TerminationReason::EXITED);
}
}
} // namespace netemul