[netemul] Building environments based on cmx facet

Implements environment setup logic based on .cmx facet metadata

TEST: added env_build test to verify environment builds correctly
Change-Id: Ic6db486f07b6031121eefecd242c56f4149eda8f
diff --git a/bin/netemul_runner/BUILD.gn b/bin/netemul_runner/BUILD.gn
index 89cf1eb..186a9f5 100644
--- a/bin/netemul_runner/BUILD.gn
+++ b/bin/netemul_runner/BUILD.gn
@@ -23,6 +23,7 @@
   ]
 
   deps = [
+    "//garnet/bin/netemul_runner/model",
     "//garnet/lib/cmx:cmx",
     "//garnet/lib/process",
     "//garnet/public/fidl/fuchsia.sys",
@@ -30,6 +31,7 @@
     "//garnet/public/lib/component/cpp/testing",
     "//garnet/public/lib/fsl",
     "//garnet/public/lib/fxl",
+    "//garnet/public/lib/json",
     "//garnet/public/lib/netemul/bus:bus_service",
     "//garnet/public/lib/netemul/fidl:bus",
     "//garnet/public/lib/netemul/fidl:environment",
diff --git a/bin/netemul_runner/main.cc b/bin/netemul_runner/main.cc
index 03da980..d111e88 100644
--- a/bin/netemul_runner/main.cc
+++ b/bin/netemul_runner/main.cc
@@ -54,7 +54,7 @@
     zx_process_exit(exit_code);
   });
 
-  sandbox.Start();
+  sandbox.Start(loop.dispatcher());
   loop.Run();
 
   return 0;
diff --git a/bin/netemul_runner/managed_environment.cc b/bin/netemul_runner/managed_environment.cc
index 9883bed..5ac452b 100644
--- a/bin/netemul_runner/managed_environment.cc
+++ b/bin/netemul_runner/managed_environment.cc
@@ -11,10 +11,8 @@
 
 ManagedEnvironment::Ptr ManagedEnvironment::CreateRoot(
     const fuchsia::sys::EnvironmentPtr& parent,
-    const SandboxEnv::Ptr& sandbox_env) {
+    const SandboxEnv::Ptr& sandbox_env, Options options) {
   auto ret = ManagedEnvironment::Ptr(new ManagedEnvironment(sandbox_env));
-  Options options;
-  options.name = "root";
   ret->Create(parent, std::move(options));
   return ret;
 }
@@ -58,17 +56,22 @@
   // prepare service configurations:
   service_config_.clear();
   if (options.inherit_parent_launch_services && managed_parent != nullptr) {
-    service_config_.insert(service_config_.begin(),
-                           managed_parent->service_config_.begin(),
-                           managed_parent->service_config_.end());
+    for (const auto& a : managed_parent->service_config_) {
+      LaunchService clone;
+      a.Clone(&clone);
+      service_config_.push_back(std::move(clone));
+    }
   }
-  service_config_.insert(service_config_.begin(), options.services.begin(),
-                         options.services.end());
+
+  std::move(options.services.begin(), options.services.end(),
+            std::back_inserter(service_config_));
 
   // push all the allowable launch services:
   for (const auto& svc : service_config_) {
     fuchsia::sys::LaunchInfo linfo;
     linfo.url = svc.url;
+    linfo.arguments->insert(linfo.arguments->begin(), svc.arguments->begin(),
+                            svc.arguments->end());
     services->AddServiceWithLaunchInfo(std::move(linfo), svc.name);
   }
 
@@ -82,6 +85,12 @@
       .allow_parent_runners = false,
       .inherit_parent_services = false};
 
+  // Nested environments without a name are not allowed, if empty name is
+  // provided, replace it with a default value:
+  if (options.name.empty()) {
+    options.name = "netemul-env";
+  }
+
   env_ = EnclosingEnvironment::Create(options.name, parent, std::move(services),
                                       sub_options);
 
@@ -105,4 +114,9 @@
   return virtual_data_->GetDirectory();
 }
 
+void ManagedEnvironment::Bind(
+    fidl::InterfaceRequest<ManagedEnvironment::FManagedEnvironment> req) {
+  bindings_.AddBinding(this, std::move(req));
+}
+
 }  // namespace netemul
diff --git a/bin/netemul_runner/managed_environment.h b/bin/netemul_runner/managed_environment.h
index f1e0eeb..60087dc 100644
--- a/bin/netemul_runner/managed_environment.h
+++ b/bin/netemul_runner/managed_environment.h
@@ -26,7 +26,7 @@
   using FManagedEnvironment = fuchsia::netemul::environment::ManagedEnvironment;
   using Ptr = std::unique_ptr<ManagedEnvironment>;
   static Ptr CreateRoot(const fuchsia::sys::EnvironmentPtr& parent,
-                        const SandboxEnv::Ptr& sandbox_env);
+                        const SandboxEnv::Ptr& sandbox_env, Options options);
 
   const SandboxEnv::Ptr& sandbox_env() const { return sandbox_env_; }
 
@@ -49,6 +49,8 @@
     running_callback_ = std::move(cb);
   }
 
+  void Bind(fidl::InterfaceRequest<FManagedEnvironment> req);
+
  protected:
   friend ManagedLauncher;
 
diff --git a/bin/netemul_runner/sandbox.cc b/bin/netemul_runner/sandbox.cc
index 2658946..5c1e9ca 100644
--- a/bin/netemul_runner/sandbox.cc
+++ b/bin/netemul_runner/sandbox.cc
@@ -4,8 +4,10 @@
 
 #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>
@@ -15,17 +17,58 @@
 #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() {
+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) {
@@ -40,16 +83,35 @@
 }
 
 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)) {
@@ -60,6 +122,7 @@
 
   fxl::UniqueFD dirfd =
       fsl::OpenChannelAsFileDescriptor(std::move(package->directory));
+
   sandbox_env_ = std::make_shared<SandboxEnv>(args_.package, std::move(dirfd));
 
   component::CmxMetadata cmx;
@@ -72,25 +135,369 @@
     return;
   }
 
-  root_ = ManagedEnvironment::CreateRoot(parent_env_, sandbox_env_);
+  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;
+  }
 
-  // TODO(brunodalbo) parameterize environment based on
-  // facets in cmx file
+  async::PostTask(helper_loop_->dispatcher(), [this]() {
+    if (!ConfigureNetworks()) {
+      PostTerminate(TerminationReason::INTERNAL_ERROR);
+      return;
+    }
 
-  root_->SetRunningCallback([this] {
-    root_proc_.events().OnTerminated = [this](int64_t code,
-                                              TerminationReason reason) {
-      Terminate(code, reason);  // mimic termination of root process
-    };
+    ManagedEnvironment::Options root_options;
+    if (!CreateEnvironmentOptions(env_config_.environment(), &root_options)) {
+      PostTerminate(TerminationReason::INTERNAL_ERROR);
+      return;
+    }
 
-    // start root test process:
-    fuchsia::sys::LaunchInfo linfo;
-    linfo.url = sandbox_env_->name();
-    linfo.out = component::testing::CloneFileDescriptor(STDOUT_FILENO);
-    linfo.err = component::testing::CloneFileDescriptor(STDERR_FILENO);
-    root_->launcher().CreateComponent(std::move(linfo),
-                                      root_proc_.NewRequest());
+    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);
+                          }
+                        });
+                      });
+                    });
   });
 }
 
-}  // namespace netemul
\ No newline at end of file
+// 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
diff --git a/bin/netemul_runner/sandbox.h b/bin/netemul_runner/sandbox.h
index 71bd3a2..3e21b26 100644
--- a/bin/netemul_runner/sandbox.h
+++ b/bin/netemul_runner/sandbox.h
@@ -6,7 +6,10 @@
 #define GARNET_BIN_NETEMUL_RUNNER_SANDBOX_H_
 
 #include <fuchsia/sys/cpp/fidl.h>
+#include <lib/async-loop/cpp/loop.h>
+#include <unordered_set>
 #include "managed_environment.h"
+#include "model/config.h"
 #include "sandbox_env.h"
 
 namespace netemul {
@@ -28,21 +31,52 @@
     termination_callback_ = std::move(callback);
   }
 
-  void Start();
+  void Start(async_dispatcher_t* dispatcher);
 
  private:
   void LoadPackage(fuchsia::sys::PackagePtr package);
   void Terminate(TerminationReason reason);
   void Terminate(int64_t exit_code, TerminationReason reason);
-  void StartRootEnvironment();
+  void PostTerminate(TerminationReason reason);
+  void PostTerminate(int64_t exit_code, TerminationReason reason);
 
+  void EnableTestObservation();
+  void RegisterTest(size_t ticket);
+  void UnregisterTest(size_t ticket);
+
+  template <typename T>
+  bool LaunchProcess(fuchsia::sys::LauncherSyncPtr* launcher,
+                     const std::string& url,
+                     const std::vector<std::string>& arguments, bool is_test);
+
+  bool LaunchSetup(fuchsia::sys::LauncherSyncPtr* launcher,
+                   const std::string& url,
+                   const std::vector<std::string>& arguments);
+
+  bool CreateEnvironmentOptions(const config::Environment& config,
+                                ManagedEnvironment::Options* options);
+  bool ConfigureRootEnvironment();
+  bool ConfigureEnvironment(
+      fidl::SynchronousInterfacePtr<ManagedEnvironment::FManagedEnvironment>
+          env,
+      const config::Environment& config, bool root = false);
+  bool ConfigureNetworks();
+
+  async_dispatcher_t* main_dispatcher_;
+  std::unique_ptr<async::Loop> helper_loop_;
+  config::Config env_config_;
+  bool setup_done_;
   SandboxArgs args_;
   SandboxEnv::Ptr sandbox_env_;
   TerminationCallback termination_callback_;
   fuchsia::sys::EnvironmentPtr parent_env_;
   fuchsia::sys::LoaderPtr loader_;
   ManagedEnvironment::Ptr root_;
-  fuchsia::sys::ComponentControllerPtr root_proc_;
+  // keep network handles to keep objects alive
+  std::vector<zx::channel> network_handles_;
+  // keep component controller handles to keep processes alive
+  std::vector<fuchsia::sys::ComponentControllerPtr> procs_;
+  std::unordered_set<size_t> tests_;
 };
 
 }  // namespace netemul
diff --git a/bin/netemul_runner/test/BUILD.gn b/bin/netemul_runner/test/BUILD.gn
index e9ad883..d2125a1 100644
--- a/bin/netemul_runner/test/BUILD.gn
+++ b/bin/netemul_runner/test/BUILD.gn
@@ -7,6 +7,7 @@
 test_package("netemul_sandbox_test") {
   deps = [
     "//garnet/bin/netemul_runner/model:model_unittest",
+    "//garnet/bin/netemul_runner/test/env_build",
     "//garnet/bin/netemul_runner/test/netstack_socks",
     "//garnet/bin/netemul_runner/test/svc_list",
   ]
@@ -20,6 +21,10 @@
       path = rebase_path("meta/netstack_socks_run.cmx")
       dest = "netstack_socks_run.cmx"
     },
+    {
+      path = rebase_path("meta/env_build_run.cmx")
+      dest = "env_build_run.cmx"
+    },
   ]
 
   tests = [
@@ -30,6 +35,9 @@
       name = "netstack_socks"
     },
     {
+      name = "env_build"
+    },
+    {
       name = "model_unittest"
     },
   ]
diff --git a/bin/netemul_runner/test/env_build/BUILD.gn b/bin/netemul_runner/test/env_build/BUILD.gn
new file mode 100644
index 0000000..29e23c8
--- /dev/null
+++ b/bin/netemul_runner/test/env_build/BUILD.gn
@@ -0,0 +1,22 @@
+# 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.
+
+import("//build/package.gni")
+import("//build/rust/rustc_binary.gni")
+
+rustc_binary("env_build") {
+  name = "env_build"
+  edition = "2018"
+  deps = [
+    "//garnet/public/lib/fidl/rust/fidl",
+    "//garnet/public/lib/netemul/fidl:bus-rustc",
+    "//garnet/public/lib/netemul/fidl:network-rustc",
+    "//garnet/public/rust/fuchsia-app",
+    "//garnet/public/rust/fuchsia-async",
+    "//garnet/public/rust/fuchsia-zircon",
+    "//third_party/rust-crates/rustc_deps:failure",
+    "//third_party/rust-crates/rustc_deps:futures-preview",
+    "//third_party/rust-crates/rustc_deps:structopt",
+  ]
+}
diff --git a/bin/netemul_runner/test/env_build/src/main.rs b/bin/netemul_runner/test/env_build/src/main.rs
new file mode 100644
index 0000000..67b6a99
--- /dev/null
+++ b/bin/netemul_runner/test/env_build/src/main.rs
@@ -0,0 +1,265 @@
+// 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.
+
+#![feature(async_await, await_macro, futures_api)]
+
+use {
+    failure::{format_err, Error, ResultExt},
+    fidl_fuchsia_netemul_bus::{BusManagerMarker, BusMarker, BusProxy, Event},
+    fidl_fuchsia_netemul_network::{
+        EndpointManagerMarker, NetworkContextMarker, NetworkManagerMarker,
+    },
+    fuchsia_app::client,
+    fuchsia_async::{self as fasync, TimeoutExt},
+    fuchsia_zircon::DurationNum,
+    futures::TryStreamExt,
+    std::fs,
+    std::path::Path,
+    structopt::StructOpt,
+};
+
+#[derive(StructOpt, Debug)]
+struct Opt {
+    #[structopt(short = "t")]
+    test: Option<i32>,
+    #[structopt(short = "n", default_value = "root")]
+    name: String,
+}
+
+const BUS_NAME: &str = "test-bus";
+const NETWORK_NAME: &str = "test-net";
+const EP0_NAME: &str = "ep0";
+const EP1_NAME: &str = "ep1";
+const EVENT_CODE: i32 = 1;
+const TIMEOUT_SECS: i64 = 2;
+const SETUP_FILE: &str = "/vdata/test-setup";
+const SETUP_FILE_DATA: &str = "Hello World";
+
+pub struct BusConnection {
+    bus: BusProxy,
+}
+
+impl BusConnection {
+    pub fn new(client: &str) -> Result<BusConnection, Error> {
+        let busm =
+            client::connect_to_service::<BusManagerMarker>().context("BusManager not available")?;
+        let (bus, busch) = fidl::endpoints::create_proxy::<BusMarker>()?;
+        busm.subscribe(BUS_NAME, client, busch)?;
+        Ok(BusConnection { bus })
+    }
+
+    pub fn publish_code(&self, code: i32) -> Result<(), Error> {
+        self.bus.publish(&mut Event {
+            code: code,
+            message: None,
+            arguments: None,
+        })?;
+        Ok(())
+    }
+
+    pub async fn wait_for_client(&mut self, expect: &'static str) -> Result<(), Error> {
+        let clients = await!(self.bus.get_clients())?;
+        if clients.contains(&String::from(expect)) {
+            return Ok(());
+        }
+
+        let mut stream = self
+            .bus
+            .take_event_stream()
+            .try_filter_map(|event| match event {
+                fidl_fuchsia_netemul_bus::BusEvent::OnClientAttached { client } => {
+                    if client == expect {
+                        futures::future::ok(Some(()))
+                    } else {
+                        futures::future::ok(None)
+                    }
+                }
+                _ => futures::future::ok(None),
+            });
+
+        await!(stream.try_next())?;
+        Ok(())
+    }
+
+    pub async fn wait_for_event(&mut self, code: i32) -> Result<(), Error> {
+        let mut stream = self
+            .bus
+            .take_event_stream()
+            .try_filter_map(|event| match event {
+                fidl_fuchsia_netemul_bus::BusEvent::OnBusData { data } => {
+                    if data.code == code {
+                        futures::future::ok(Some(()))
+                    } else {
+                        futures::future::ok(None)
+                    }
+                }
+                _ => futures::future::ok(None),
+            });
+        await!(stream.try_next())?;
+        Ok(())
+    }
+}
+
+fn service_path(name: &str) -> String {
+    format!("/svc/{}", name)
+}
+
+fn device_path(name: &str) -> String {
+    format!("/vdev/{}", name)
+}
+
+fn check_path_present(path: &str) -> Result<(), Error> {
+    if Path::new(path).exists() {
+        Ok(())
+    } else {
+        Err(format_err!(
+            "Path {} not present, expected it to be there",
+            path
+        ))
+    }
+}
+
+fn check_path_absent(path: &str) -> Result<(), Error> {
+    if Path::new(path).exists() {
+        Err(format_err!(
+            "Path {} present, expected it to be absent.",
+            path
+        ))
+    } else {
+        Ok(())
+    }
+}
+
+fn check_netemul_environment() -> Result<(), Error> {
+    let () = check_path_present(&service_path("fuchsia.netemul.bus.BusManager"))?;
+    let () = check_path_present(&service_path(
+        "fuchsia.netemul.environment.ManagedEnvironment",
+    ))?;
+    let () = check_path_present(&service_path("fuchsia.netemul.network.NetworkContext"))?;
+    Ok(())
+}
+
+async fn check_network() -> Result<(), Error> {
+    let netctx = client::connect_to_service::<NetworkContextMarker>()?;
+    let (epm, epmch) = fidl::endpoints::create_proxy::<EndpointManagerMarker>()?;
+    let () = netctx.get_endpoint_manager(epmch)?;
+    let (netm, netmch) = fidl::endpoints::create_proxy::<NetworkManagerMarker>()?;
+    let () = netctx.get_network_manager(netmch)?;
+
+    let net = await!(netm.get_network(NETWORK_NAME))?;
+    if net == None {
+        return Err(format_err!("Could not retrieve network {}.", NETWORK_NAME));
+    }
+
+    let ep0 = await!(epm.get_endpoint(EP0_NAME))?;
+    if ep0 == None {
+        return Err(format_err!("Could not retrieve endpoint {}", EP0_NAME));
+    }
+
+    let ep1 = await!(epm.get_endpoint(EP1_NAME))?;
+    if ep1 == None {
+        return Err(format_err!("Could not retrieve endpoint {}", EP1_NAME));
+    }
+
+    Ok(())
+}
+
+async fn root_wait_for_children(mut bus: BusConnection) -> Result<(), Error> {
+    // wait for three hits on the bus, representing each child test
+    for i in 0..3 {
+        let () = await!(bus
+            .wait_for_event(EVENT_CODE)
+            .on_timeout(TIMEOUT_SECS.seconds().after_now(), || Err(format_err!(
+                "timed out waiting for children procs"
+            ))))?;
+        println!("Got ping from child {}", i);
+    }
+
+    Ok(())
+}
+
+async fn child_publish_on_bus(mut bus: BusConnection) -> Result<(), Error> {
+    // wait for root to show up on the bus...
+    let () = await!(bus
+        .wait_for_client("root")
+        .on_timeout(TIMEOUT_SECS.seconds().after_now(), || Err(format_err!(
+            "Timed out waiting for root test"
+        ))))?;
+    // ... then publish an event so root knows we were spawned
+    let () = bus.publish_code(EVENT_CODE)?;
+    Ok(())
+}
+
+fn run_root(opt: &Opt) -> Result<(), Error> {
+    println!("Running main test: {}", opt.name);
+    let () = check_netemul_environment()?;
+    let () = check_path_present(&service_path("fuchsia.netstack.Netstack"))?;
+    let () = check_path_present(&service_path("fuchsia.net.SocketProvider"))?;
+    let () = check_path_present(&device_path("class/ethernet/ep0"))?;
+    let () = check_path_present(&device_path("class/ethernet/ep1"))?;
+
+    let mut executor = fasync::Executor::new().context("Error creating executor")?;
+
+    // check that network was created according to spec
+    let () = executor.run_singlethreaded(check_network())?;
+
+    // check that the setup process ran:
+    let setup_data = fs::read_to_string(SETUP_FILE).context("Can't open setup file.")?;
+    if setup_data != SETUP_FILE_DATA {
+        return Err(format_err!(
+            "Setup file contents mismatch, got {}",
+            setup_data
+        ));
+    }
+
+    // wait for children on bus
+    let bus = BusConnection::new(&opt.name)?;
+    executor.run_singlethreaded(root_wait_for_children(bus))
+}
+
+// environment 1 inherits from the root environment
+fn run_test_1(opt: &Opt) -> Result<(), Error> {
+    println!("Running test 1: {}", opt.name);
+    let () = check_netemul_environment()?;
+    let () = check_path_present(&service_path("fuchsia.netstack.Netstack"))?;
+    let () = check_path_present(&service_path("fuchsia.net.SocketProvider"))?;
+    let () = check_path_absent(&device_path("class/ethernet/ep0"))?;
+    let () = check_path_absent(&device_path("class/ethernet/ep1"))?;
+
+    let mut executor = fasync::Executor::new().context("Error creating executor")?;
+    let bus = BusConnection::new(&opt.name)?;
+    executor.run_singlethreaded(child_publish_on_bus(bus))
+}
+
+// environment 2 does NOT inherit from the root environment
+fn run_test_2(opt: &Opt) -> Result<(), Error> {
+    println!("Running test 2: {}", opt.name);
+    let () = check_netemul_environment()?;
+    let () = check_path_absent(&service_path("fuchsia.netstack.Netstack"))?;
+    let () = check_path_absent(&service_path("fuchsia.net.SocketProvider"))?;
+    let () = check_path_absent(&device_path("class/ethernet/ep0"))?;
+    let () = check_path_absent(&device_path("class/ethernet/ep1"))?;
+
+    let mut executor = fasync::Executor::new().context("Error creating executor")?;
+    let bus = BusConnection::new(&opt.name)?;
+    executor.run_singlethreaded(child_publish_on_bus(bus))
+}
+
+fn run_setup_test(opt: &Opt) -> Result<(), Error> {
+    println!("Running setup test: {}", opt.name);
+    // create a file in vdata, that will be verified by root test
+    let () = fs::write(SETUP_FILE, SETUP_FILE_DATA).context("setup can't write file")?;
+    Ok(())
+}
+
+fn main() -> Result<(), Error> {
+    let opt = Opt::from_args();
+    match opt.test {
+        None => run_root(&opt),
+        Some(1) => run_test_1(&opt),
+        Some(2) => run_test_2(&opt),
+        Some(3) => run_setup_test(&opt),
+        _ => Err(format_err!("Unrecognized test option")),
+    }
+}
diff --git a/bin/netemul_runner/test/meta/env_build.cmx b/bin/netemul_runner/test/meta/env_build.cmx
new file mode 100644
index 0000000..44f0254
--- /dev/null
+++ b/bin/netemul_runner/test/meta/env_build.cmx
@@ -0,0 +1,6 @@
+{
+    "program": {
+        "data": "meta/env_build_run.cmx"
+    },
+    "runner": "fuchsia-pkg://fuchsia.com/netemul_runner#meta/netemul_runner.cmx"
+}
diff --git a/bin/netemul_runner/test/meta/env_build_run.cmx b/bin/netemul_runner/test/meta/env_build_run.cmx
new file mode 100644
index 0000000..1311737
--- /dev/null
+++ b/bin/netemul_runner/test/meta/env_build_run.cmx
@@ -0,0 +1,99 @@
+{
+    "facets": {
+        "fuchsia.netemul": {
+            "environment": {
+                "children": [
+                    {
+                        "name": "child-1",
+                        "test": [
+                            {
+                                "arguments": [
+                                    "-t",
+                                    "1",
+                                    "-n",
+                                    "child-1-url"
+                                ],
+                                "url": "fuchsia-pkg://fuchsia.com/netemul_sandbox_test#meta/env_build_run.cmx"
+                            },
+                            {
+                                "arguments": [
+                                    "-t",
+                                    "1",
+                                    "-n",
+                                    "child-1-no-url"
+                                ]
+                            }
+                        ]
+                    },
+                    {
+                        "inherit_services": false,
+                        "name": "child-2",
+                        "test": [
+                            {
+                                "arguments": [
+                                    "-t",
+                                    "2",
+                                    "-n",
+                                    "child-2"
+                                ]
+                            }
+                        ]
+                    }
+                ],
+                "devices": [
+                    "ep0",
+                    "ep1"
+                ],
+                "name": "root",
+                "services": {
+                    "fuchsia.net.SocketProvider": "fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx",
+                    "fuchsia.netstack.Netstack": "fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx"
+                },
+                "setup": [
+                    {
+                        "arguments": [
+                            "-t",
+                            "3",
+                            "-n",
+                            "setup"
+                        ]
+                    }
+                ]
+            },
+            "networks": [
+                {
+                    "endpoints": [
+                        {
+                            "mac": "70:00:01:02:03:04",
+                            "mtu": 1000,
+                            "name": "ep0",
+                            "up": false
+                        },
+                        {
+                            "name": "ep1"
+                        }
+                    ],
+                    "name": "test-net"
+                }
+            ]
+        }
+    },
+    "program": {
+        "binary": "test/env_build"
+    },
+    "sandbox": {
+        "dev": [
+            "class/ethernet"
+        ],
+        "features": [
+            "persistent-storage"
+        ],
+        "services": [
+            "fuchsia.netemul.environment.ManagedEnvironment",
+            "fuchsia.netemul.network.NetworkContext",
+            "fuchsia.netemul.bus.BusManager",
+            "fuchsia.netstack.Netstack",
+            "fuchsia.net.SocketProvider"
+        ]
+    }
+}
diff --git a/bin/netemul_runner/test/netstack_socks/src/main.rs b/bin/netemul_runner/test/netstack_socks/src/main.rs
index e5e29f4..37a6646 100644
--- a/bin/netemul_runner/test/netstack_socks/src/main.rs
+++ b/bin/netemul_runner/test/netstack_socks/src/main.rs
@@ -87,10 +87,12 @@
             LaunchService {
                 name: String::from(NetstackMarker::NAME),
                 url: String::from(NETSTACK_URL),
+                arguments: None,
             },
             LaunchService {
                 name: String::from("fuchsia.net.LegacySocketProvider"),
                 url: String::from(NETSTACK_URL),
+                arguments: None,
             },
             LaunchService {
                 name: String::from("fuchsia.net.SocketProvider"),
diff --git a/bin/netemul_runner/test/svc_list/src/main.rs b/bin/netemul_runner/test/svc_list/src/main.rs
index 6798d6e..d173f84 100644
--- a/bin/netemul_runner/test/svc_list/src/main.rs
+++ b/bin/netemul_runner/test/svc_list/src/main.rs
@@ -124,6 +124,7 @@
         services: vec![LaunchService {
             name: String::from(NetstackMarker::NAME),
             url: String::from(NETSTACK_URL),
+            arguments: None,
         }],
         // pass the endpoint's proxy to create a virtual device
         devices: vec![VirtualDevice {
@@ -196,6 +197,7 @@
         services: vec![LaunchService {
             name: String::from(FAKE_SVC_NAME),
             url: String::from(FAKE_SVC_URL),
+            arguments: None,
         }],
         devices: vec![],
         // inherit parent configuration to check if netstack flows through
diff --git a/public/lib/netemul/fidl/environment.fidl b/public/lib/netemul/fidl/environment.fidl
index 9829493..027c276 100644
--- a/public/lib/netemul/fidl/environment.fidl
+++ b/public/lib/netemul/fidl/environment.fidl
@@ -12,6 +12,8 @@
     string name;
     /// Service launch url (fuchsia component url).
     string url;
+    /// Service launch arguments
+    vector<string>? arguments;
 };
 
 /// A single virtual device to make available for child processes.