blob: 8f026f3caf080eff436a39ed5819f9c83e82a005 [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.
use {
anyhow::{format_err, Context as _, Error},
by_addr::ByAddr,
fdio::fdio_sys,
fidl::endpoints::{ClientEnd, Proxy, ServerEnd, ServiceMarker},
fidl_fuchsia_io::{DirectoryProxy, OPEN_RIGHT_READABLE},
fidl_fuchsia_process as fproc,
fidl_fuchsia_sys::{
ComponentControllerMarker, RunnerMarker, RunnerRequest, RunnerRequestStream,
},
fuchsia_async as fasync,
fuchsia_component::server::ServiceFs,
fuchsia_runtime::{job_default, HandleInfo, HandleType},
fuchsia_syslog::{fx_log_err, fx_log_info},
fuchsia_zircon as zx,
futures::prelude::*,
lazy_static::lazy_static,
library_loader,
std::{
collections::HashSet,
path::{Path, PathBuf},
sync::{Arc, Mutex},
},
zx::HandleBased,
};
lazy_static! {
pub static ref PKG_PATH: PathBuf = PathBuf::from("/pkg");
}
fn get_program_binary(startup_info: &fidl_fuchsia_sys::StartupInfo) -> Result<String, Error> {
if let Some(metadata) = &startup_info.program_metadata {
let m = metadata
.iter()
.find(|m| m.key == "binary")
.ok_or(format_err!("\"binary\" must be specified"))?;
return Ok(m.value.clone());
}
Err(format_err!("\"binary\" must be specified"))
}
fn get_program_args(startup_info: &fidl_fuchsia_sys::StartupInfo) -> Result<Vec<String>, Error> {
if let Some(args) = &startup_info.launch_info.arguments {
return Ok(args.clone());
}
Ok(vec![])
}
fn handle_info_from_fd(fd: i32) -> Result<Option<fproc::HandleInfo>, Error> {
unsafe {
let mut fd_handle = zx::sys::ZX_HANDLE_INVALID;
let status = fdio_sys::fdio_fd_clone(fd, &mut fd_handle as *mut zx::sys::zx_handle_t);
if status == zx::sys::ZX_ERR_INVALID_ARGS {
// This file descriptor is closed. We just skip it rather than
// generating an error.
return Ok(None);
}
if status != zx::sys::ZX_OK {
return Err(format_err!("failed to clone fd {}: {}", fd, status));
}
Ok(Some(fproc::HandleInfo {
handle: zx::Handle::from_raw(fd_handle),
id: HandleInfo::new(HandleType::FileDescriptor, fd as u16).as_raw(),
}))
}
}
/// Representation of Test Component which will implement 'fuchsia.test.Suite', launch gtest
/// process and map the output into our protocol and pass results back to test executor.
struct Component {
/// Package definition of our test component.
package: fidl_fuchsia_sys::Package,
/// Startup info for this component pased to runner.
startup_info: Option<fidl_fuchsia_sys::StartupInfo>,
// TODO(anmittal): implement it. Currently keeping it so that it doesn't die.
/// fuchsia.sys.Controller channel
_controller: Option<ServerEnd<ComponentControllerMarker>>,
/// True if this component is already running, i.e process was launched.
running: bool,
}
impl Component {
/// Create a new Component object
pub fn new(
package: fidl_fuchsia_sys::Package,
startup_info: fidl_fuchsia_sys::StartupInfo,
controller: Option<ServerEnd<ComponentControllerMarker>>,
) -> Self {
// TODO(anmittal): clone things in startup_info so that we can launch test again and again.
Component {
package: package,
startup_info: Some(startup_info),
_controller: controller,
running: false,
}
}
// TODO(anmittal): Explore how we can share code and abstract most part into a library.
async fn load_launch_info(
&mut self,
launcher: &fproc::LauncherProxy,
) -> Result<fproc::LaunchInfo, Error> {
let startup_info = self.startup_info.take().unwrap();
let bin_path = get_program_binary(&startup_info).map_err(|e| {
format_err!("invalid metadata for: {}, {}", self.package.resolved_url, e)
})?;
let bin_arg = &[String::from(
PKG_PATH.join(&bin_path).to_str().ok_or(format_err!("invalid binary path"))?,
)];
let args = get_program_args(&startup_info)?;
let name = Path::new(&self.package.resolved_url)
.file_name()
.ok_or(format_err!("invalid url"))?
.to_str()
.ok_or(format_err!("invalid url"))?;
// Convert the directories into proxies, so we can find "/pkg" and open "lib" and bin_path
let ns = startup_info.flat_namespace;
let mut paths = ns.paths;
let directories: Result<Vec<DirectoryProxy>, std::io::Error> = ns
.directories
.into_iter()
.map(|d| {
let chan = fasync::Channel::from_channel(d)?;
Ok(DirectoryProxy::new(chan))
})
.collect();
let mut directories = directories?;
// Start the library loader service
let pkg_str = PKG_PATH.to_str().unwrap();
let (ll_client_chan, ll_service_chan) = zx::Channel::create()?;
let (_, pkg_proxy) = paths
.iter()
.zip(directories.iter())
.find(|(p, _)| p.as_str() == pkg_str)
.ok_or(format_err!("/pkg missing from namespace"))?;
let lib_proxy = io_util::open_directory(pkg_proxy, &Path::new("lib"), OPEN_RIGHT_READABLE)?;
// The loader service should only be able to load files from `/pkg/lib`. Giving it a larger
// scope is potentially a security vulnerability, as it could make it trivial for parts of
// applications to get handles to things the application author didn't intend.
library_loader::start(lib_proxy, ll_service_chan);
let executable_vmo = library_loader::load_vmo(pkg_proxy, &bin_path)
.await
.context("error loading executable")?;
let child_job = job_default().create_child_job()?;
let child_job_dup = child_job.duplicate_handle(zx::Rights::SAME_RIGHTS)?;
let string_iters: Vec<_> =
bin_arg.iter().chain(args.iter()).map(|s| s.as_bytes()).collect();
launcher.add_args(&mut string_iters.into_iter())?;
// TODO(anmittal): capture these fds and read them to implement fuchsia.test.Suite
// currently implementing stdout so that we can debug till we implement fuchsia.test.Suite
// and put a reader on stdout.
let mut handle_infos = vec![];
for fd in 0..3 {
handle_infos.extend(handle_info_from_fd(fd)?);
}
handle_infos.append(&mut vec![
fproc::HandleInfo {
handle: ll_client_chan.into_handle(),
id: HandleInfo::new(HandleType::LdsvcLoader, 0).as_raw(),
},
fproc::HandleInfo {
handle: child_job_dup.into_handle(),
id: HandleInfo::new(HandleType::DefaultJob, 0).as_raw(),
},
]);
// TODO(anmittal): Implement fuchsia.test.Suite instead of passing it to test process.
if let Some(outgoing_dir) = startup_info.launch_info.directory_request {
handle_infos.push(fproc::HandleInfo {
handle: outgoing_dir.into_handle(),
id: HandleInfo::new(HandleType::DirectoryRequest, 0).as_raw(),
});
}
launcher.add_handles(&mut handle_infos.iter_mut())?;
let mut name_infos = vec![];
while let Some(path) = paths.pop() {
if let Some(directory) = directories.pop() {
let directory = ClientEnd::new(
directory
.into_channel()
.map_err(|_| format_err!("into_channel failed"))?
.into_zx_channel(),
);
name_infos.push(fproc::NameInfo { path, directory });
}
}
launcher.add_names(&mut name_infos.iter_mut())?;
Ok(fproc::LaunchInfo { executable: executable_vmo, job: child_job, name: name.to_owned() })
}
// TODO(anmittal): Call list on gtest and keep test list
pub async fn start(&mut self) -> Result<(), Error> {
if !self.running {
self.running = true;
} else {
panic!("run called two times");
}
fx_log_info!("Received runner request for component {}", self.package.resolved_url);
let launcher = fuchsia_component::client::connect_to_service::<fproc::LauncherMarker>()
.expect("cannot connect to Launcher");
let mut launch_info = self.load_launch_info(&launcher).await?;
// TODO(anmittal): Wait on process to terminate.
let (status, _process) = launcher.launch(&mut launch_info).await?;
if zx::Status::from_raw(status) != zx::Status::OK {
return Err(format_err!("failed to launch component: {}", status));
}
Ok(())
}
}
struct State {
/// safe keep map of components to prevent them from dying.
pub components: HashSet<ByAddr<Component>>,
}
impl State {
pub fn new() -> Self {
State { components: HashSet::new() }
}
}
async fn run_runner_server(
mut stream: RunnerRequestStream,
state: Arc<Mutex<State>>,
) -> Result<(), Error> {
while let Some(RunnerRequest::StartComponent {
package,
startup_info,
controller,
control_handle: _,
}) = stream.try_next().await.context("error running server")?
{
let mut component = Component::new(package, startup_info, controller);
component.start().await?;
// TODO(anmittal): figure out when to kill this component and prevent leak.
let mut state = state.lock().unwrap();
state.components.insert(ByAddr::new(Arc::new(component)));
}
Ok(())
}
fn main() -> Result<(), Error> {
fuchsia_syslog::init_with_tags(&["gtest_v1_runner"])?;
fx_log_info!("runner started");
let mut executor = fasync::Executor::new().context("Error creating executor")?;
let mut fs = ServiceFs::new_local();
let state = Arc::new(Mutex::new(State::new()));
fs.dir("svc").add_fidl_service_at(RunnerMarker::NAME, move |stream| {
let state = state.clone();
fasync::Task::spawn(
async move {
run_runner_server(stream, state).await?;
Ok(())
}
.unwrap_or_else(|e: anyhow::Error| fx_log_err!("runner failed: {:?}", e)),
)
.detach();
});
fs.take_and_serve_directory_handle()?;
executor.run_singlethreaded(fs.collect::<()>());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
// TODO(anmittal): Add a unit test to launch a process.
fn default_startup_info() -> fidl_fuchsia_sys::StartupInfo {
fidl_fuchsia_sys::StartupInfo {
launch_info: fidl_fuchsia_sys::LaunchInfo {
url: "".to_string(),
arguments: None,
out: None,
err: None,
directory_request: None,
flat_namespace: None,
additional_services: None,
},
flat_namespace: fidl_fuchsia_sys::FlatNamespace { paths: vec![], directories: vec![] },
program_metadata: Some(vec![]),
}
}
#[test]
fn get_program_args_test() {
let mut e: Vec<String> = vec![];
let mut startup_info = default_startup_info();
// no arguments
assert_eq!(e, get_program_args(&startup_info).unwrap());
// empty arguments
startup_info.launch_info.arguments = Some(vec![]);
assert_eq!(e, get_program_args(&startup_info).unwrap());
// one argument
e = vec!["a1".to_string()];
startup_info.launch_info.arguments = Some(e.clone());
assert_eq!(e, get_program_args(&startup_info).unwrap());
// two argument
e = vec!["a1".to_string(), "a2".to_string()];
startup_info.launch_info.arguments = Some(e.clone());
assert_eq!(e, get_program_args(&startup_info).unwrap());
// more than two argument
e = vec!["a1".to_string(), "a2".to_string(), "random".to_string(), "random2".to_string()];
startup_info.launch_info.arguments = Some(e.clone());
assert_eq!(e, get_program_args(&startup_info).unwrap());
}
#[test]
fn get_program_binary_test() {
let mut startup_info = default_startup_info();
// no program metadata
assert!(get_program_binary(&startup_info).is_err());
// blank program metadata
startup_info.program_metadata = Some(vec![fidl_fuchsia_sys::ProgramMetadata {
key: "".to_string(),
value: "".to_string(),
}]);
assert!(get_program_binary(&startup_info).is_err());
// no binary program metadata
startup_info.program_metadata = Some(vec![fidl_fuchsia_sys::ProgramMetadata {
key: "data".to_string(),
value: "sample_data".to_string(),
}]);
assert!(get_program_binary(&startup_info).is_err());
// no binary program metadata with multiple metadata
startup_info.program_metadata = Some(vec![
fidl_fuchsia_sys::ProgramMetadata {
key: "data".to_string(),
value: "sample_data".to_string(),
},
fidl_fuchsia_sys::ProgramMetadata {
key: "arg".to_string(),
value: "hello".to_string(),
},
]);
assert!(get_program_binary(&startup_info).is_err());
// binary program metadata with one metadata
startup_info.program_metadata = Some(vec![fidl_fuchsia_sys::ProgramMetadata {
key: "binary".to_string(),
value: "/app".to_string(),
}]);
assert_eq!("/app", get_program_binary(&startup_info).unwrap());
// binary program metadata with multiple metadata
startup_info.program_metadata = Some(vec![
fidl_fuchsia_sys::ProgramMetadata {
key: "binary".to_string(),
value: "/app".to_string(),
},
fidl_fuchsia_sys::ProgramMetadata {
key: "data".to_string(),
value: "sample_data".to_string(),
},
]);
assert_eq!("/app", get_program_binary(&startup_info).unwrap());
// binary program metadata with multiple metadata
startup_info.program_metadata = Some(vec![
fidl_fuchsia_sys::ProgramMetadata {
key: "data".to_string(),
value: "sample_data".to_string(),
},
fidl_fuchsia_sys::ProgramMetadata {
key: "binary".to_string(),
value: "/app".to_string(),
},
]);
assert_eq!("/app", get_program_binary(&startup_info).unwrap());
}
}