blob: 9d81b943dfee686d5887bdb439f2fafd2bda640e [file] [log] [blame]
// Copyright 2019 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.
//! Tools for starting or connecting to existing Fuchsia applications and services.
use {
failure::{Error, Fail, ResultExt},
fidl::endpoints::{Proxy, ServiceMarker},
fidl_fuchsia_sys::{
ComponentControllerEvent, ComponentControllerProxy, FileDescriptor, FlatNamespace,
LaunchInfo, LauncherMarker, LauncherProxy, TerminationReason,
},
fuchsia_async as fasync,
fuchsia_runtime::HandleType,
fuchsia_zircon::{self as zx, Socket, SocketOpts},
futures::{
future::{self, FutureExt, TryFutureExt},
stream::{StreamExt, TryStreamExt},
Future,
},
std::{fmt, fs::File, sync::Arc},
};
/// Connect to a FIDL service using the provided channel and namespace prefix.
pub fn connect_channel_to_service_at<S: ServiceMarker>(
server_end: zx::Channel,
service_prefix: &str,
) -> Result<(), Error> {
let service_path = format!("{}/{}", service_prefix, S::NAME);
fdio::service_connect(&service_path, server_end)
.with_context(|_| format!("Error connecting to service path: {}", service_path))?;
Ok(())
}
/// Connect to a FIDL service using the provided channel.
pub fn connect_channel_to_service<S: ServiceMarker>(server_end: zx::Channel) -> Result<(), Error> {
connect_channel_to_service_at::<S>(server_end, "/svc")
}
/// Connect to a FIDL service using the provided namespace prefix.
pub fn connect_to_service_at<S: ServiceMarker>(service_prefix: &str) -> Result<S::Proxy, Error> {
let (proxy, server) = zx::Channel::create()?;
connect_channel_to_service_at::<S>(server, service_prefix)?;
let proxy = fasync::Channel::from_channel(proxy)?;
Ok(S::Proxy::from_channel(proxy))
}
/// Connect to a FIDL service using the application root namespace.
pub fn connect_to_service<S: ServiceMarker>() -> Result<S::Proxy, Error> {
connect_to_service_at::<S>("/svc")
}
/// Adds a new directory to the namespace for the new process.
pub fn add_dir_to_namespace(
namespace: &mut FlatNamespace,
path: String,
dir: File,
) -> Result<(), Error> {
let handle = fdio::transfer_fd(dir)?;
namespace.paths.push(path);
namespace.directories.push(zx::Channel::from(handle));
Ok(())
}
/// Returns a connection to the application launcher service.
pub fn launcher() -> Result<LauncherProxy, Error> {
connect_to_service::<LauncherMarker>()
}
/// Launch an application at the specified URL.
pub fn launch(
launcher: &LauncherProxy,
url: String,
arguments: Option<Vec<String>>,
) -> Result<App, Error> {
launch_with_options(launcher, url, arguments, LaunchOptions::new())
}
/// Options for the launcher when starting an applications.
pub struct LaunchOptions {
namespace: Option<Box<FlatNamespace>>,
out: Option<Box<FileDescriptor>>,
}
impl LaunchOptions {
/// Creates default launch options.
pub fn new() -> LaunchOptions {
LaunchOptions { namespace: None, out: None }
}
/// Adds a new directory to the namespace for the new process.
pub fn add_dir_to_namespace(&mut self, path: String, dir: File) -> Result<&mut Self, Error> {
let handle = fdio::transfer_fd(dir)?;
let namespace = self
.namespace
.get_or_insert_with(|| Box::new(FlatNamespace { paths: vec![], directories: vec![] }));
namespace.paths.push(path);
namespace.directories.push(zx::Channel::from(handle));
Ok(self)
}
/// Sets the out handle.
pub fn set_out(&mut self, f: FileDescriptor) {
self.out = Some(Box::new(f));
}
}
/// Launch an application at the specified URL.
pub fn launch_with_options(
launcher: &LauncherProxy,
url: String,
arguments: Option<Vec<String>>,
options: LaunchOptions,
) -> Result<App, Error> {
let (controller, controller_server_end) = fidl::endpoints::create_proxy()?;
let (directory_request, directory_server_chan) = zx::Channel::create()?;
let directory_request = Arc::new(directory_request);
let mut launch_info = LaunchInfo {
url,
arguments,
out: options.out,
err: None,
directory_request: Some(directory_server_chan),
flat_namespace: options.namespace,
additional_services: None,
};
launcher
.create_component(&mut launch_info, Some(controller_server_end.into()))
.context("Failed to start a new Fuchsia application.")?;
Ok(App { directory_request, controller, stdout: None, stderr: None })
}
/// `App` represents a launched application.
///
/// When `App` is dropped, launched application will be terminated.
#[must_use = "Dropping `App` will cause the application to be terminated."]
pub struct App {
// directory_request is a directory protocol channel
directory_request: Arc<zx::Channel>,
// Keeps the component alive until `App` is dropped.
controller: ComponentControllerProxy,
//TODO pub accessors to take stdout/stderr in wrapper types that implement AsyncRead.
stdout: Option<fasync::Socket>,
stderr: Option<fasync::Socket>,
}
impl App {
/// Returns a reference to the directory protocol channel of the application.
#[inline]
pub fn directory_channel(&self) -> &zx::Channel {
&self.directory_request
}
/// Returns a reference to the component controller.
#[inline]
pub fn controller(&self) -> &ComponentControllerProxy {
&self.controller
}
/// Connect to a service provided by the `App`.
#[inline]
pub fn connect_to_service<S: ServiceMarker>(&self) -> Result<S::Proxy, Error> {
let (client_channel, server_channel) = zx::Channel::create()?;
self.pass_to_service::<S>(server_channel)?;
Ok(S::Proxy::from_channel(fasync::Channel::from_channel(client_channel)?))
}
/// Connect to a service by passing a channel for the server.
#[inline]
pub fn pass_to_service<S: ServiceMarker>(
&self,
server_channel: zx::Channel,
) -> Result<(), Error> {
self.pass_to_named_service(S::NAME, server_channel)
}
/// Connect to a service by name.
#[inline]
pub fn pass_to_named_service(
&self,
service_name: &str,
server_channel: zx::Channel,
) -> Result<(), Error> {
fdio::service_connect_at(&self.directory_request, service_name, server_channel)?;
Ok(())
}
/// Forces the component to exit.
pub fn kill(&mut self) -> Result<(), fidl::Error> {
self.controller.kill()
}
/// Wait for the component to terminate and return its exit status.
pub fn wait(&mut self) -> impl Future<Output = Result<ExitStatus, Error>> {
self.controller
.take_event_stream()
.try_filter_map(|event| {
future::ready(match event {
ComponentControllerEvent::OnTerminated { return_code, termination_reason } => {
Ok(Some(ExitStatus { return_code, termination_reason }))
}
_ => Ok(None),
})
})
.into_future()
.map(|(next, _rest)| match next {
Some(result) => result.map_err(|err| err.into()),
_ => Ok(ExitStatus {
return_code: -1,
termination_reason: TerminationReason::Unknown,
}),
})
}
/// Wait for the component to terminate and return its exit status, stdout, and stderr.
pub fn wait_with_output(mut self) -> impl Future<Output = Result<Output, Error>> {
let drain = |pipe: Option<fasync::Socket>| match pipe {
None => future::ready(Ok(vec![])).left_future(),
Some(pipe) => pipe
.take_while(|maybe_result| {
future::ready(match maybe_result {
Err(zx::Status::PEER_CLOSED) => false,
_ => true,
})
})
.try_concat()
.map_err(|err| err.into())
.right_future(),
};
future::try_join3(self.wait(), drain(self.stdout), drain(self.stderr))
.map_ok(|(exit_status, stdout, stderr)| Output { exit_status, stdout, stderr })
}
}
/// A component builder, providing a simpler interface to
/// [`fidl_fuchsia_sys::LauncherProxy::create_component`].
///
/// `AppBuilder`s interface matches
/// [`std:process:Command`](https://doc.rust-lang.org/std/process/struct.Command.html) as
/// closely as possible, except where the semantics of spawning a process differ from the
/// semantics of spawning a Fuchsia component:
///
/// * Fuchsia components do not support stdin, a current working directory, or environment
/// variables.
///
/// * `AppBuilder` will move certain handles into the spawned component (see
/// [`AppBuilder::add_dir_to_namespace`]), so a single instance of `AppBuilder` can only be
/// used to create a single component.
#[derive(Debug)]
pub struct AppBuilder {
launch_info: LaunchInfo,
directory_request: Option<Arc<zx::Channel>>,
stdout: Option<Stdio>,
stderr: Option<Stdio>,
}
impl AppBuilder {
/// Creates a new `AppBuilder` for the component referenced by the given `url`.
pub fn new(url: impl Into<String>) -> Self {
Self {
launch_info: LaunchInfo {
url: url.into(),
arguments: None,
out: None,
err: None,
directory_request: None,
flat_namespace: None,
additional_services: None,
},
directory_request: None,
stdout: None,
stderr: None,
}
}
/// Returns a reference to the local end of the component's directory_request channel,
/// creating it if necessary.
pub fn directory_request(&mut self) -> Result<&Arc<zx::Channel>, Error> {
Ok(match self.directory_request {
Some(ref channel) => channel,
None => {
let (directory_request, directory_server_chan) = zx::Channel::create()?;
let directory_request = Arc::new(directory_request);
self.launch_info.directory_request = Some(directory_server_chan);
self.directory_request = Some(directory_request);
self.directory_request.as_ref().unwrap()
}
})
}
/// Configures stdout for the new process.
pub fn stdout(mut self, cfg: impl Into<Stdio>) -> Self {
self.stdout = Some(cfg.into());
self
}
/// Configures stderr for the new process.
pub fn stderr(mut self, cfg: impl Into<Stdio>) -> Self {
self.stderr = Some(cfg.into());
self
}
/// Mounts an opened directory in the namespace of the component.
pub fn add_dir_to_namespace(mut self, path: String, dir: File) -> Result<Self, Error> {
let handle = fdio::transfer_fd(dir)?;
let namespace = self
.launch_info
.flat_namespace
.get_or_insert_with(|| Box::new(FlatNamespace { paths: vec![], directories: vec![] }));
namespace.paths.push(path);
namespace.directories.push(zx::Channel::from(handle));
Ok(self)
}
/// Append the given `arg` to the sequence of arguments passed to the new process.
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.launch_info.arguments.get_or_insert_with(Vec::new).push(arg.into());
self
}
/// Append all the given `args` to the sequence of arguments passed to the new process.
pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.launch_info
.arguments
.get_or_insert_with(Vec::new)
.extend(args.into_iter().map(|arg| arg.into()));
self
}
/// Launch the component using the provided `launcher` proxy, returning the launched
/// application or the error encountered while launching it.
///
/// By default:
/// * when the returned [`App`] is dropped, the launched application will be terminated.
/// * stdout and stderr will use the the default stdout and stderr for the environment.
pub fn spawn(self, launcher: &LauncherProxy) -> Result<App, Error> {
self.launch(launcher, Stdio::Inherit)
}
/// Launches the component using the provided `launcher` proxy, waits for it to finish, and
/// collects all of its output.
///
/// By default, stdout and stderr are captured (and used to provide the resulting output).
pub fn output(
self,
launcher: &LauncherProxy,
) -> Result<impl Future<Output = Result<Output, Error>>, Error> {
Ok(self.launch(launcher, Stdio::MakePipe)?.wait_with_output())
}
/// Launches the component using the provided `launcher` proxy, waits for it to finish, and
/// collects its exit status.
///
/// By default, stdout and stderr will use the default stdout and stderr for the
/// environment.
pub fn status(
self,
launcher: &LauncherProxy,
) -> Result<impl Future<Output = Result<ExitStatus, Error>>, Error> {
Ok(self.launch(launcher, Stdio::Inherit)?.wait())
}
fn launch(mut self, launcher: &LauncherProxy, default: Stdio) -> Result<App, Error> {
let (controller, controller_server_end) = fidl::endpoints::create_proxy()?;
let directory_request = if let Some(directory_request) = self.directory_request.take() {
directory_request
} else {
let (directory_request, directory_server_chan) = zx::Channel::create()?;
self.launch_info.directory_request = Some(directory_server_chan);
Arc::new(directory_request)
};
let (stdout, remote_stdout) =
self.stdout.as_ref().unwrap_or(&default).to_local_and_remote()?;
if let Some(fd) = remote_stdout {
self.launch_info.out = Some(Box::new(fd));
}
let (stderr, remote_stderr) =
self.stderr.as_ref().unwrap_or(&default).to_local_and_remote()?;
if let Some(fd) = remote_stderr {
self.launch_info.err = Some(Box::new(fd));
}
launcher
.create_component(&mut self.launch_info, Some(controller_server_end.into()))
.context("Failed to start a new Fuchsia application.")?;
Ok(App { directory_request, controller, stdout, stderr })
}
}
/// Describes what to do with a standard I/O stream for a child component.
#[derive(Debug)]
pub enum Stdio {
/// Use the default output sink for the environment.
Inherit,
/// Provide a socket to the component to write output to.
MakePipe,
}
impl Stdio {
fn to_local_and_remote(
&self,
) -> Result<(Option<fasync::Socket>, Option<FileDescriptor>), Error> {
match *self {
Stdio::Inherit => Ok((None, None)),
Stdio::MakePipe => {
let (local, remote) = Socket::create(SocketOpts::STREAM)?;
// local end is read-only
local.half_close()?;
let local = fasync::Socket::from_socket(local)?;
let remote = FileDescriptor {
type0: HandleType::FileDescriptor as i32,
type1: 0,
type2: 0,
handle0: Some(remote.into()),
handle1: None,
handle2: None,
};
Ok((Some(local), Some(remote)))
}
}
}
}
/// Describes the result of a component after it has terminated.
#[derive(Debug, Clone, Fail)]
pub struct ExitStatus {
return_code: i64,
termination_reason: TerminationReason,
}
impl ExitStatus {
/// Did the component exit without an error? Success is defined as a reason of exited and
/// a code of 0.
#[inline]
pub fn success(&self) -> bool {
self.exited() && self.return_code == 0
}
/// Returns true if the component exited, as opposed to not starting at all due to some
/// error or terminating with any reason other than `EXITED`.
#[inline]
pub fn exited(&self) -> bool {
self.termination_reason == TerminationReason::Exited
}
/// The reason the component was terminated.
#[inline]
pub fn reason(&self) -> TerminationReason {
self.termination_reason
}
/// The return code from the component. Guaranteed to be non-zero if termination reason is
/// not `EXITED`.
#[inline]
pub fn code(&self) -> i64 {
self.return_code
}
/// Converts the `ExitStatus` to a `Result<(), ExitStatus>`, mapping to `Ok(())` if the
/// component exited with status code 0, or to `Err(ExitStatus)` otherwise.
pub fn ok(&self) -> Result<(), Self> {
if self.success() {
Ok(())
} else {
Err(self.clone())
}
}
}
impl fmt::Display for ExitStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.exited() {
write!(f, "Exited with {}", self.code())
} else {
write!(f, "Terminated with reason {:?}", self.reason())
}
}
}
/// The output of a finished component.
pub struct Output {
/// The exit status of the component.
pub exit_status: ExitStatus,
/// The data that the component wrote to stdout.
pub stdout: Vec<u8>,
/// The data that the component wrote to stderr.
pub stderr: Vec<u8>,
}
/// The output of a component that terminated with a failure.
#[derive(Clone, Fail)]
#[fail(display = "{}", exit_status)]
pub struct OutputError {
#[cause]
exit_status: ExitStatus,
stdout: String,
stderr: String,
}
impl Output {
/// Converts the `Output` to a `Result<(), OutputError>`, mapping to `Ok(())` if the component
/// exited with status code 0, or to `Err(OutputError)` otherwise.
pub fn ok(&self) -> Result<(), OutputError> {
if self.exit_status.success() {
Ok(())
} else {
let stdout = String::from_utf8_lossy(&self.stdout).into_owned();
let stderr = String::from_utf8_lossy(&self.stderr).into_owned();
Err(OutputError { exit_status: self.exit_status.clone(), stdout, stderr })
}
}
}
impl fmt::Debug for OutputError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
struct RawMultilineString<'a>(&'a str);
impl<'a> fmt::Debug for RawMultilineString<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.0.is_empty() {
f.write_str(r#""""#)
} else {
f.write_str("r#\"")?;
f.write_str(self.0)?;
f.write_str("\"#")
}
}
}
f.debug_struct("OutputError")
.field("exit_status", &self.exit_status)
.field("stdout", &RawMultilineString(&self.stdout))
.field("stderr", &RawMultilineString(&self.stderr))
.finish()
}
}