blob: 8fd03334e9d8b3690ef15beecbfe24e29c83cecb [file] [log] [blame]
// Copyright 2022 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.
#![deny(missing_docs)]
//! Some functions to help run command line processes for tests.
//!
//! ## Usage:
//! ```
//! let mut fs = ServiceFs::new();
//! let (svc_client_end, svc_server_end) = zx::Channel::create().expect("create channel");
//! let svc_proxy = fidl_fuchsia_io::DirectoryProxy::from_channel(
//! fuchsia_async::Channel::from_channel(svc_client_end),
//! );
//! let env = fs.serve_connection(svc_server_end);
//! ...
//! let output = shell_process::run_process(
//! "someprocess",
//! ["--arg", "foo"],
//! [("/svc", &svc_proxy)],
//! );
//! assert!(output.is_ok());
//! ```
use {
fdio::{SpawnAction, SpawnOptions},
fuchsia_async::{self as fasync},
fuchsia_runtime::{job_default, HandleInfo, HandleType},
fuchsia_zircon::{self as zx, HandleBased, ProcessInfo},
futures::prelude::*,
libc::{STDERR_FILENO, STDOUT_FILENO},
std::ffi::{CStr, CString},
};
/// A struct to contain the results of a shell process.
pub struct ProcessOutput {
/// The return code from a process.
pub return_code: i64,
/// The stderr text output of a process.
pub stderr: Vec<u8>,
/// The stdout text output of a process.
pub stdout: Vec<u8>,
}
impl ProcessOutput {
/// Whether or not a process ended with return_code zero.
pub fn is_ok(&self) -> bool {
self.return_code == 0
}
/// The return code with which a process ended.
pub fn return_code(&self) -> i64 {
self.return_code
}
/// The stdout text output of a process, as a &str.
pub fn stdout_str(&self) -> &str {
std::str::from_utf8(&self.stdout).unwrap()
}
/// The stderr text output of a process, as a &str.
pub fn stderr_str(&self) -> &str {
std::str::from_utf8(&self.stderr).unwrap()
}
}
/// Runs a binary with some arguments asynchronously; returns a delayed exit
/// code and two sockets with stdout and stderr. For a simpler API, use
/// run_process().
pub async fn run_process_async<'a>(
binary_path: &'a str,
args: impl IntoIterator<Item = &'a str>,
proxies: impl IntoIterator<Item = (&'a str, &fidl_fuchsia_io::DirectoryProxy)>,
) -> (fasync::Task<i64>, fasync::Socket, fasync::Socket) {
//
let (stdout_reader, stdout_writer) = zx::Socket::create_stream();
let (stderr_reader, stderr_writer) = zx::Socket::create_stream();
// The reader-ends should not write.
let () = stdout_writer.half_close().expect("stdout_reader.half_close");
let () = stderr_writer.half_close().expect("stderr_reader.half_close");
let args: Vec<CString> = std::iter::once(binary_path)
.chain(args)
.map(|a| CString::new(a).unwrap_or_else(|e| panic!("failed to parse {a} to CString: {e}")))
.collect();
let args: Vec<&CStr> = args.iter().map(|s| s.as_c_str()).collect();
let mut spawn_actions = vec![];
let proxy_by_path_cstring: Vec<(CString, &fidl_fuchsia_io::DirectoryProxy)> =
proxies.into_iter().map(|(path, dir)| (CString::new(path).unwrap(), dir)).collect();
let proxy_by_path_cstr: Vec<(&CStr, &fidl_fuchsia_io::DirectoryProxy)> =
proxy_by_path_cstring.iter().map(|(path, dir)| (path.as_c_str(), *dir)).collect();
for (path, proxy) in proxy_by_path_cstr {
let (proxy_client_end, proxy_server_end) = fidl::endpoints::create_endpoints();
fuchsia_fs::directory::clone_onto_no_describe(proxy, None, proxy_server_end).unwrap();
let proxy_client_channel = proxy_client_end.into_channel();
spawn_actions
.push(SpawnAction::add_namespace_entry(path, proxy_client_channel.into_handle()));
}
spawn_actions.push(SpawnAction::add_handle(
HandleInfo::new(
HandleType::FileDescriptor,
STDOUT_FILENO.try_into().expect("STDOUT_FILENO.try_into"),
),
stdout_writer.into(),
));
spawn_actions.push(SpawnAction::add_handle(
HandleInfo::new(
HandleType::FileDescriptor,
STDERR_FILENO.try_into().expect("STDERR_FILENO.try_into"),
),
stderr_writer.into(),
));
let process = fdio::spawn_etc(
/*job=*/ &job_default(),
/*options=*/ SpawnOptions::DEFAULT_LOADER,
/*path=*/ CString::new(binary_path).expect("cstring path").as_c_str(),
/*argv=*/ &args[..],
/*environ=*/ None,
/*actions=*/ &mut spawn_actions,
)
.expect("spawn process");
(
fasync::Task::spawn(async move {
assert_eq!(
fasync::OnSignals::new(&process, zx::Signals::PROCESS_TERMINATED)
.await
.expect("wait for process termination"),
zx::Signals::PROCESS_TERMINATED
);
let ProcessInfo { return_code, start_time: _, flags: _ } =
process.info().expect("process info");
return_code
}),
fasync::Socket::from_socket(stdout_reader),
fasync::Socket::from_socket(stderr_reader),
)
}
/// Runs a binary with some arguments synchronously; returns a struct with exit
/// code, stdout, and stderr.
pub async fn run_process<'a>(
binary_path: &'a str,
args: impl IntoIterator<Item = &'a str>,
proxies: impl IntoIterator<Item = (&'a str, &fidl_fuchsia_io::DirectoryProxy)>,
) -> ProcessOutput {
let (update, stdout_reader, stderr_reader) =
run_process_async(binary_path, args, proxies).await;
let drain = |pipe: fasync::Socket| pipe.into_datagram_stream().try_concat().err_into();
future::try_join3(
update.map(Result::<_, anyhow::Error>::Ok),
drain(stdout_reader),
drain(stderr_reader),
)
.map_ok(|(return_code, stdout, stderr)| ProcessOutput { return_code, stdout, stderr })
.await
.unwrap()
}