| // 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()); |
| } |
| } |