| // 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 { |
| async_trait::async_trait, |
| cm_types::NamespacePath, |
| fidl::{endpoints::ServerEnd, epitaph::ChannelEpitaphExt, prelude::*}, |
| fidl_fuchsia_component as fcomp, fidl_fuchsia_component_runner as fcrunner, |
| fidl_fuchsia_io as fio, fidl_fuchsia_process as fproc, fuchsia_async as fasync, |
| fuchsia_runtime::{job_default, HandleInfo, HandleType}, |
| fuchsia_zircon::{self as zx, HandleBased, Status}, |
| futures::{ |
| future::{BoxFuture, Either}, |
| prelude::*, |
| stream::BoxStream, |
| }, |
| lazy_static::lazy_static, |
| namespace::Namespace, |
| thiserror::Error, |
| tracing::*, |
| }; |
| |
| lazy_static! { |
| pub static ref PKG_PATH: NamespacePath = "/pkg".parse().unwrap(); |
| } |
| |
| /// Object implementing this type can be killed by calling kill function. |
| #[async_trait] |
| pub trait Controllable { |
| /// Should kill self and do cleanup. |
| /// Should not return error or panic, should log error instead. |
| async fn kill(&mut self); |
| |
| /// Stop the component. Once the component is stopped, the |
| /// ComponentControllerControlHandle should be closed. If the component is |
| /// not stopped quickly enough, kill will be called. The amount of time |
| /// `stop` is allowed may vary based on a variety of factors. |
| fn stop<'a>(&mut self) -> BoxFuture<'a, ()>; |
| |
| /// Perform any teardown tasks before closing the controller channel. |
| fn teardown<'a>(&mut self) -> BoxFuture<'a, ()> { |
| async {}.boxed() |
| } |
| |
| /// Monitor any escrow requests from the component. |
| fn on_escrow<'a>(&self) -> BoxStream<'a, fcrunner::ComponentControllerOnEscrowRequest> { |
| futures::stream::empty().boxed() |
| } |
| } |
| |
| /// Holds information about the component that allows the controller to |
| /// interact with and control the component. |
| pub struct Controller<C: Controllable> { |
| /// stream via which the component manager will ask the controller to |
| /// manipulate the component |
| request_stream: fcrunner::ComponentControllerRequestStream, |
| |
| /// Controllable object which controls the underlying component. |
| /// This would be None once the object is killed. |
| controllable: Option<C>, |
| |
| /// Task that forwards the `on_escrow` event. |
| on_escrow_monitor: fasync::Task<()>, |
| } |
| |
| pub struct ChannelEpitaph(u32); |
| |
| impl ChannelEpitaph { |
| pub fn ok() -> Self { |
| static_assertions::const_assert_eq!(fuchsia_zircon_types::ZX_OK, 0); |
| Self(0) |
| } |
| } |
| |
| impl From<ChannelEpitaph> for Status { |
| fn from(value: ChannelEpitaph) -> Self { |
| Status::from_raw(i32::try_from(value.0).unwrap_or_else(|_| i32::MAX)) |
| } |
| } |
| |
| impl From<u32> for ChannelEpitaph { |
| fn from(v: u32) -> Self { |
| Self(v) |
| } |
| } |
| |
| impl TryFrom<Status> for ChannelEpitaph { |
| type Error = std::num::TryFromIntError; |
| fn try_from(value: Status) -> Result<Self, std::num::TryFromIntError> { |
| Ok(Self(u32::try_from(value.into_raw())?)) |
| } |
| } |
| |
| impl From<fcomp::Error> for ChannelEpitaph { |
| fn from(value: fcomp::Error) -> Self { |
| Self(u32::from(value.into_primitive())) |
| } |
| } |
| |
| impl<C: Controllable + 'static> Controller<C> { |
| /// Creates new instance |
| pub fn new( |
| controllable: C, |
| requests: fcrunner::ComponentControllerRequestStream, |
| ) -> Controller<C> { |
| let on_escrow = controllable.on_escrow(); |
| let on_escrow_monitor = |
| fasync::Task::spawn(Self::monitor_events(on_escrow, requests.control_handle())); |
| Controller { controllable: Some(controllable), request_stream: requests, on_escrow_monitor } |
| } |
| |
| async fn serve_controller(&mut self) -> Result<(), ()> { |
| while let Ok(Some(request)) = self.request_stream.try_next().await { |
| match request { |
| fcrunner::ComponentControllerRequest::Stop { control_handle: _c } => { |
| // Since stop takes some period of time to complete, call |
| // it in a separate context so we can respond to other |
| // requests. |
| let stop_func = self.stop(); |
| fasync::Task::spawn(stop_func).detach(); |
| } |
| fcrunner::ComponentControllerRequest::Kill { control_handle: _c } => { |
| self.kill().await; |
| return Ok(()); |
| } |
| } |
| } |
| // The channel closed |
| Err(()) |
| } |
| |
| async fn monitor_events( |
| mut on_escrow: impl Stream<Item = fcrunner::ComponentControllerOnEscrowRequest> + Unpin + Send, |
| control_handle: fcrunner::ComponentControllerControlHandle, |
| ) { |
| while let Some(event) = on_escrow.next().await { |
| control_handle |
| .send_on_escrow(event) |
| .unwrap_or_else(|err| error!(%err, "failed to send OnEscrow event")); |
| } |
| } |
| |
| /// Serve the request stream held by this Controller. `exit_fut` should |
| /// complete if the component exits and should return a value which is |
| /// either 0 (ZX_OK) or one of the fuchsia.component/Error constants |
| /// defined as valid in the fuchsia.component.runner/ComponentController |
| /// documentation. This function will return after `exit_fut` completes |
| /// or the request stream closes. In either case the request stream is |
| /// closed once this function returns since the stream itself, which owns |
| /// the channel, is dropped. |
| pub async fn serve(mut self, exit_fut: impl Future<Output = ChannelEpitaph> + Unpin) { |
| let result_code: ChannelEpitaph = { |
| // Pin the server_controller future so we can use it with select |
| let request_server = self.serve_controller(); |
| futures::pin_mut!(request_server); |
| |
| // Get the result of waiting for `exit_fut` to complete while also |
| // polling the request server. |
| match future::select(exit_fut, request_server).await { |
| Either::Left((return_code, _controller_server)) => return_code, |
| Either::Right((serve_result, pending_close)) => match serve_result { |
| Ok(()) => pending_close.await, |
| Err(_) => { |
| // Return directly because the controller channel |
| // closed so there's no point in an epitaph. |
| return; |
| } |
| }, |
| } |
| }; |
| |
| // Before closing the controller channel, perform teardown tasks if the runner configured |
| // them. This will only run if the component was not killed (otherwise `controllable` is |
| // `None`). |
| if let Some(mut controllable) = self.controllable.take() { |
| controllable.teardown().await; |
| } |
| |
| // Drain any escrow events. |
| // TODO(https://fxbug.dev/326626515): Drain the escrow requests until no long readable |
| // instead of waiting for an unbounded amount of time if `on_escrow` never completes. |
| self.on_escrow_monitor.await; |
| self.request_stream.control_handle().shutdown_with_epitaph(result_code.into()); |
| } |
| |
| /// Kill the job and shutdown control handle supplied to this function. |
| async fn kill(&mut self) { |
| if let Some(mut controllable) = self.controllable.take() { |
| controllable.kill().await; |
| } |
| } |
| |
| /// If we have a Controllable, ask it to stop the component. |
| fn stop<'a>(&mut self) -> BoxFuture<'a, ()> { |
| if self.controllable.is_some() { |
| self.controllable.as_mut().unwrap().stop() |
| } else { |
| async {}.boxed() |
| } |
| } |
| } |
| |
| /// An error encountered trying to launch a component. |
| #[derive(Clone, Debug, PartialEq, Eq, Error)] |
| pub enum LaunchError { |
| #[error("invalid binary path {}", _0)] |
| InvalidBinaryPath(String), |
| |
| #[error("/pkg missing in the namespace")] |
| MissingPkg, |
| |
| #[error("error loading executable: {:?}", _0)] |
| LoadingExecutable(String), |
| |
| #[error("cannot convert proxy to channel")] |
| DirectoryToChannel, |
| |
| #[error("cannot create channels: {}", _0)] |
| ChannelCreation(fuchsia_zircon_status::Status), |
| |
| #[error("error loading 'lib' in /pkg: {:?}", _0)] |
| LibLoadError(String), |
| |
| #[error("cannot create job: {}", _0)] |
| JobCreation(fuchsia_zircon_status::Status), |
| |
| #[error("cannot duplicate job: {}", _0)] |
| DuplicateJob(fuchsia_zircon_status::Status), |
| |
| #[error("cannot add args to launcher: {:?}", _0)] |
| AddArgs(String), |
| |
| #[error("cannot add args to launcher: {:?}", _0)] |
| AddHandles(String), |
| |
| #[error("cannot add args to launcher: {:?}", _0)] |
| AddNames(String), |
| |
| #[error("cannot add env to launcher: {:?}", _0)] |
| AddEnvirons(String), |
| |
| #[error("cannot set options for launcher: {:?}", _0)] |
| SetOptions(String), |
| } |
| |
| /// Arguments to `configure_launcher` function. |
| pub struct LauncherConfigArgs<'a> { |
| /// relative binary path to /pkg in `ns`. |
| pub bin_path: &'a str, |
| |
| /// Name of the binary to add to `LaunchInfo`. This will be truncated to |
| /// `zx::sys::ZX_MAX_NAME_LEN` bytes. |
| pub name: &'a str, |
| |
| /// The options used to create the process. |
| pub options: zx::ProcessOptions, |
| |
| /// Arguments to binary. Binary path will be automatically |
| /// prepended so that should not be passed as first argument. |
| pub args: Option<Vec<String>>, |
| |
| /// Namespace for binary process to be launched. |
| pub ns: Namespace, |
| |
| /// Job in which process is launched. If None, a child job would be created in default one. |
| pub job: Option<zx::Job>, |
| |
| /// Extra handle infos to add. This function all ready adds handles for default job and svc |
| /// loader. |
| pub handle_infos: Option<Vec<fproc::HandleInfo>>, |
| |
| /// Extra names to add to namespace. by default only names from `ns` are added. |
| pub name_infos: Option<Vec<fproc::NameInfo>>, |
| |
| /// Process environment to add to launcher. |
| pub environs: Option<Vec<String>>, |
| |
| /// proxy for `fuchsia.proc.Launcher`. |
| pub launcher: &'a fproc::LauncherProxy, |
| |
| /// Custom loader proxy. If None, /pkg/lib would be used to load libraries. |
| pub loader_proxy_chan: Option<zx::Channel>, |
| |
| /// VMO containing mapping to executable binary. If None, it would be loaded from /pkg. |
| pub executable_vmo: Option<zx::Vmo>, |
| } |
| |
| /// Configures launcher to launch process using passed params and creates launch info. |
| /// This starts a library loader service, that will live as long as the handle for it given to the |
| /// launcher is alive. |
| pub async fn configure_launcher( |
| config_args: LauncherConfigArgs<'_>, |
| ) -> Result<fproc::LaunchInfo, LaunchError> { |
| // Locate the '/pkg' directory proxy previously added to the new component's namespace. |
| let pkg_dir = config_args.ns.get(&PKG_PATH).ok_or(LaunchError::MissingPkg)?; |
| |
| // library_loader provides a helper function that we use to load the main executable from the |
| // package directory as a VMO in the same way that dynamic libraries are loaded. Doing this |
| // first allows launching to fail quickly and clearly in case the main executable can't be |
| // loaded with ZX_RIGHT_EXECUTE from the package directory. |
| let executable_vmo = match config_args.executable_vmo { |
| Some(v) => v, |
| None => library_loader::load_vmo(pkg_dir, &config_args.bin_path) |
| .await |
| .map_err(|e| LaunchError::LoadingExecutable(e.to_string()))?, |
| }; |
| |
| let ll_client_chan = match config_args.loader_proxy_chan { |
| None => { |
| // 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. |
| let lib_proxy = fuchsia_component::directory::open_directory_no_describe( |
| pkg_dir, |
| "lib", |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| ) |
| .map_err(|e| LaunchError::LibLoadError(e.to_string()))?; |
| let (ll_client_chan, ll_service_chan) = zx::Channel::create(); |
| library_loader::start(lib_proxy.into(), ll_service_chan); |
| ll_client_chan |
| } |
| Some(chan) => chan, |
| }; |
| |
| // Get the provided job to create the new process in, if one was provided, or else create a new |
| // child job of this process's (this process that this code is running in) own 'default job'. |
| let job = config_args |
| .job |
| .unwrap_or(job_default().create_child_job().map_err(LaunchError::JobCreation)?); |
| |
| // Build the command line args for the new process and send them to the launcher. |
| let bin_arg = PKG_PATH |
| .to_path_buf() |
| .join(&config_args.bin_path) |
| .to_str() |
| .ok_or(LaunchError::InvalidBinaryPath(config_args.bin_path.to_string()))? |
| .as_bytes() |
| .to_vec(); |
| let mut all_args = vec![bin_arg]; |
| if let Some(args) = config_args.args { |
| all_args.extend(args.into_iter().map(|s| s.into_bytes())); |
| } |
| config_args.launcher.add_args(&all_args).map_err(|e| LaunchError::AddArgs(e.to_string()))?; |
| |
| // Get any initial handles to provide to the new process, if any were provided by the caller. |
| // Add handles for the new process's default job (by convention, this is the same job that the |
| // new process is launched in) and the fuchsia.ldsvc.Loader service created above, then send to |
| // the launcher. |
| let job_dup = |
| job.duplicate_handle(zx::Rights::SAME_RIGHTS).map_err(LaunchError::DuplicateJob)?; |
| let mut handle_infos = config_args.handle_infos.unwrap_or(vec![]); |
| handle_infos.append(&mut vec![ |
| fproc::HandleInfo { |
| handle: ll_client_chan.into_handle(), |
| id: HandleInfo::new(HandleType::LdsvcLoader, 0).as_raw(), |
| }, |
| fproc::HandleInfo { |
| handle: job_dup.into_handle(), |
| id: HandleInfo::new(HandleType::DefaultJob, 0).as_raw(), |
| }, |
| ]); |
| config_args |
| .launcher |
| .add_handles(handle_infos) |
| .map_err(|e| LaunchError::AddHandles(e.to_string()))?; |
| |
| if !config_args.options.is_empty() { |
| config_args |
| .launcher |
| .set_options(config_args.options.bits()) |
| .map_err(|e| LaunchError::SetOptions(e.to_string()))?; |
| } |
| |
| // Send environment variables for the new process, if any, to the launcher. |
| let environs: Vec<_> = config_args.environs.unwrap_or(vec![]); |
| if environs.len() > 0 { |
| let environs_bytes: Vec<_> = environs.into_iter().map(|s| s.into_bytes()).collect(); |
| config_args |
| .launcher |
| .add_environs(&environs_bytes) |
| .map_err(|e| LaunchError::AddEnvirons(e.to_string()))?; |
| } |
| |
| // Combine any manually provided namespace entries with the provided Namespace, and |
| // then send the new process's namespace to the launcher. |
| let mut name_infos = config_args.name_infos.unwrap_or(vec![]); |
| let ns: Vec<_> = config_args.ns.into(); |
| name_infos.extend(ns.into_iter()); |
| config_args.launcher.add_names(name_infos).map_err(|e| LaunchError::AddNames(e.to_string()))?; |
| |
| let name = truncate_str(config_args.name, zx::sys::ZX_MAX_NAME_LEN).to_owned(); |
| |
| Ok(fproc::LaunchInfo { executable: executable_vmo, job, name }) |
| } |
| |
| /// Truncates `s` to be at most `max_len` bytes. |
| fn truncate_str(s: &str, max_len: usize) -> &str { |
| if s.len() <= max_len { |
| return s; |
| } |
| // TODO(https://github.com/rust-lang/rust/issues/93743): Use floor_char_boundary when stable. |
| let mut index = max_len; |
| while index > 0 && !s.is_char_boundary(index) { |
| index -= 1; |
| } |
| &s[..index] |
| } |
| |
| static CONNECT_ERROR_HELP: &'static str = "To learn more, see \ |
| https://fuchsia.dev/go/components/connect-errors"; |
| |
| /// Sets an epitaph on `ComponentController` `server_end` for a runner failure and the outgoing |
| /// directory, and logs it. |
| pub fn report_start_error( |
| err: zx::Status, |
| err_str: String, |
| resolved_url: &str, |
| controller_server_end: ServerEnd<fcrunner::ComponentControllerMarker>, |
| ) { |
| let _ = controller_server_end.into_channel().close_with_epitaph(err); |
| warn!("Failed to start component `{}`: {}\n{}", resolved_url, err_str, CONNECT_ERROR_HELP); |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::{ |
| configure_launcher, truncate_str, ChannelEpitaph, Controllable, Controller, |
| LaunchError, LauncherConfigArgs, |
| }, |
| anyhow::{Context, Error}, |
| assert_matches::assert_matches, |
| async_trait::async_trait, |
| fidl::endpoints::{create_endpoints, create_proxy, ClientEnd}, |
| fidl_fuchsia_component_runner::{self as fcrunner, ComponentControllerProxy}, |
| fidl_fuchsia_io as fio, fidl_fuchsia_process as fproc, fuchsia_async as fasync, |
| fuchsia_runtime::{HandleInfo, HandleType}, |
| fuchsia_zircon::{self as zx, HandleBased}, |
| futures::{future::BoxFuture, poll, prelude::*}, |
| namespace::{Namespace, NamespaceError}, |
| std::{pin::Pin, task::Poll}, |
| }; |
| |
| #[test] |
| fn test_truncate_str() { |
| assert_eq!(truncate_str("", 0), ""); |
| assert_eq!(truncate_str("", 1), ""); |
| |
| assert_eq!(truncate_str("été", 0), ""); |
| assert_eq!(truncate_str("été", 1), ""); |
| assert_eq!(truncate_str("été", 2), "é"); |
| assert_eq!(truncate_str("été", 3), "ét"); |
| assert_eq!(truncate_str("été", 4), "ét"); |
| assert_eq!(truncate_str("été", 5), "été"); |
| assert_eq!(truncate_str("été", 6), "été"); |
| } |
| |
| struct FakeComponent<K, J> |
| where |
| K: FnOnce() + std::marker::Send, |
| J: FnOnce() + std::marker::Send, |
| { |
| pub onkill: Option<K>, |
| |
| pub onstop: Option<J>, |
| |
| pub onteardown: Option<BoxFuture<'static, ()>>, |
| } |
| |
| #[async_trait] |
| impl<K: 'static, J: 'static> Controllable for FakeComponent<K, J> |
| where |
| K: FnOnce() + std::marker::Send, |
| J: FnOnce() + std::marker::Send, |
| { |
| async fn kill(&mut self) { |
| let func = self.onkill.take().unwrap(); |
| func(); |
| } |
| |
| fn stop<'a>(&mut self) -> BoxFuture<'a, ()> { |
| let func = self.onstop.take().unwrap(); |
| async move { func() }.boxed() |
| } |
| |
| fn teardown<'a>(&mut self) -> BoxFuture<'a, ()> { |
| self.onteardown.take().unwrap() |
| } |
| } |
| |
| #[fuchsia::test] |
| async fn test_kill_component() -> Result<(), Error> { |
| let (sender, recv) = futures::channel::oneshot::channel::<()>(); |
| let (epitaph_tx, epitaph_rx) = futures::channel::oneshot::channel::<ChannelEpitaph>(); |
| const CHANNEL_EPITAPH: zx::Status = zx::Status::OK; |
| let fake_component = FakeComponent { |
| onkill: Some(move || { |
| sender.send(()).unwrap(); |
| // After acknowledging that we received kill, send the epitaph |
| // value so `serve` completes. |
| let _ = epitaph_tx.send(CHANNEL_EPITAPH.try_into().unwrap()); |
| }), |
| onstop: Some(|| {}), |
| onteardown: Some(async {}.boxed()), |
| }; |
| |
| let (controller, client_proxy) = create_controller_and_proxy(fake_component)?; |
| |
| client_proxy.kill().expect("FIDL error returned from kill request to controller"); |
| |
| let epitaph_receiver = Box::pin(async move { epitaph_rx.await.unwrap() }); |
| // this should return after kill call |
| controller.serve(epitaph_receiver).await; |
| |
| // this means kill was called |
| recv.await?; |
| |
| // Check the epitaph on the controller channel, this should match what |
| // is sent by `epitaph_tx` |
| assert_matches!( |
| client_proxy.take_event_stream().try_next().await, |
| Err(fidl::Error::ClientChannelClosed { status, .. }) if status == CHANNEL_EPITAPH |
| ); |
| |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn test_stop_component() -> Result<(), Error> { |
| let (sender, recv) = futures::channel::oneshot::channel::<()>(); |
| let (teardown_signal_tx, teardown_signal_rx) = futures::channel::oneshot::channel::<()>(); |
| let (teardown_fence_tx, teardown_fence_rx) = futures::channel::oneshot::channel::<()>(); |
| let (epitaph_tx, epitaph_rx) = futures::channel::oneshot::channel::<ChannelEpitaph>(); |
| const CHANNEL_EPITAPH: zx::Status = zx::Status::OK; |
| |
| let fake_component = FakeComponent { |
| onstop: Some(move || { |
| sender.send(()).unwrap(); |
| let _ = epitaph_tx.send(CHANNEL_EPITAPH.try_into().unwrap()); |
| }), |
| onkill: Some(move || {}), |
| onteardown: Some( |
| async move { |
| teardown_signal_tx.send(()).unwrap(); |
| teardown_fence_rx.await.unwrap(); |
| } |
| .boxed(), |
| ), |
| }; |
| |
| let (controller, client_proxy) = create_controller_and_proxy(fake_component)?; |
| |
| client_proxy.stop().expect("FIDL error returned from kill request to controller"); |
| |
| let epitaph_receiver = Box::pin(async move { epitaph_rx.await.unwrap() }); |
| |
| // This should return once the channel is closed, that is after stop and teardown |
| let controller_serve = fasync::Task::spawn(controller.serve(epitaph_receiver)); |
| |
| // This means stop was called |
| recv.await?; |
| |
| // Teardown should be called |
| teardown_signal_rx.await?; |
| |
| // Teardown is blocked. Verify there's no epitaph on the channel yet, then unblock it. |
| let mut client_stream = client_proxy.take_event_stream(); |
| let mut client_stream_fut = client_stream.try_next(); |
| assert_matches!(poll!(Pin::new(&mut client_stream_fut)), Poll::Pending); |
| teardown_fence_tx.send(()).unwrap(); |
| controller_serve.await; |
| |
| // Check the epitaph on the controller channel, this should match what |
| // is sent by `epitaph_tx` |
| assert_matches!( |
| client_stream_fut.await, |
| Err(fidl::Error::ClientChannelClosed { status, .. }) if status == CHANNEL_EPITAPH |
| ); |
| |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| fn test_stop_then_kill() -> Result<(), Error> { |
| let mut exec = fasync::TestExecutor::new(); |
| let (sender, mut recv) = futures::channel::oneshot::channel::<()>(); |
| let (epitaph_tx, epitaph_rx) = futures::channel::oneshot::channel::<ChannelEpitaph>(); |
| const CHANNEL_EPITAPH: zx::Status = zx::Status::OK; |
| |
| // This component will only 'exit' after kill is called. |
| let fake_component = FakeComponent { |
| onstop: Some(move || { |
| sender.send(()).unwrap(); |
| }), |
| onkill: Some(move || { |
| let _ = epitaph_tx.send(CHANNEL_EPITAPH.try_into().unwrap()); |
| }), |
| onteardown: Some(async {}.boxed()), |
| }; |
| |
| let (controller, client_proxy) = create_controller_and_proxy(fake_component)?; |
| // Send a stop request, note that the controller isn't even running |
| // yet, but the request will be waiting in the channel when it does. |
| client_proxy.stop().expect("FIDL error returned from stop request to controller"); |
| |
| // Set up the controller to run. |
| let epitaph_receiver = Box::pin(async move { epitaph_rx.await.unwrap() }); |
| let mut controller_fut = Box::pin(controller.serve(epitaph_receiver)); |
| |
| // Run the serve loop until it is stalled, it shouldn't return because |
| // stop doesn't automatically call exit. |
| match exec.run_until_stalled(&mut controller_fut) { |
| Poll::Pending => {} |
| x => panic!("Serve future should have been pending but was not {:?}", x), |
| } |
| |
| // Check that stop was called |
| assert_eq!(exec.run_until_stalled(&mut recv), Poll::Ready(Ok(()))); |
| |
| // Kill the component which should call the `onkill` we passed in. |
| // This should cause the epitaph future to complete, which should then |
| // cause the controller future to complete. |
| client_proxy.kill().expect("FIDL error returned from kill request to controller"); |
| match exec.run_until_stalled(&mut controller_fut) { |
| Poll::Ready(()) => {} |
| x => panic!("Unexpected controller poll state {:?}", x), |
| } |
| |
| // Check the controller channel closed with an epitaph that matches |
| // what was sent in the `onkill` closure. |
| let mut event_stream = client_proxy.take_event_stream(); |
| let mut next_fut = event_stream.try_next(); |
| assert_matches!( |
| exec.run_until_stalled(&mut next_fut), |
| Poll::Ready(Err(fidl::Error::ClientChannelClosed { status, .. })) if status == CHANNEL_EPITAPH |
| ); |
| |
| Ok(()) |
| } |
| |
| fn create_controller_and_proxy<K: 'static, J: 'static>( |
| fake_component: FakeComponent<K, J>, |
| ) -> Result<(Controller<FakeComponent<K, J>>, ComponentControllerProxy), Error> |
| where |
| K: FnOnce() + std::marker::Send, |
| J: FnOnce() + std::marker::Send, |
| { |
| let (client_endpoint, server_endpoint) = |
| create_endpoints::<fcrunner::ComponentControllerMarker>(); |
| |
| // Get a proxy to the ComponentController channel. |
| let controller_stream = |
| server_endpoint.into_stream().context("failed to convert server end to controller")?; |
| Ok(( |
| Controller::new(fake_component, controller_stream), |
| client_endpoint.into_proxy().expect("conversion to proxy failed."), |
| )) |
| } |
| |
| mod launch_info { |
| use fidl::endpoints::Proxy; |
| |
| use {super::*, anyhow::format_err, futures::channel::oneshot}; |
| |
| fn setup_empty_namespace() -> Result<Namespace, NamespaceError> { |
| setup_namespace(false, vec![]) |
| } |
| |
| fn setup_namespace( |
| include_pkg: bool, |
| // All the handles created for this will have server end closed. |
| // Clients cannot send messages on those handles in ns. |
| extra_paths: Vec<&str>, |
| ) -> Result<Namespace, NamespaceError> { |
| let mut ns = Vec::<fcrunner::ComponentNamespaceEntry>::new(); |
| if include_pkg { |
| let pkg_path = "/pkg".to_string(); |
| let pkg_chan = fuchsia_fs::directory::open_in_namespace( |
| "/pkg", |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| ) |
| .unwrap() |
| .into_channel() |
| .unwrap() |
| .into_zx_channel(); |
| let pkg_handle = ClientEnd::new(pkg_chan); |
| |
| ns.push(fcrunner::ComponentNamespaceEntry { |
| path: Some(pkg_path), |
| directory: Some(pkg_handle), |
| ..Default::default() |
| }); |
| } |
| |
| for path in extra_paths { |
| let (client, _server) = create_endpoints::<fio::DirectoryMarker>(); |
| ns.push(fcrunner::ComponentNamespaceEntry { |
| path: Some(path.to_string()), |
| directory: Some(client), |
| ..Default::default() |
| }); |
| } |
| Namespace::try_from(ns) |
| } |
| |
| #[derive(Default)] |
| struct FakeLauncherServiceResults { |
| names: Vec<String>, |
| handles: Vec<u32>, |
| args: Vec<String>, |
| options: zx::ProcessOptions, |
| } |
| |
| fn start_launcher( |
| ) -> Result<(fproc::LauncherProxy, oneshot::Receiver<FakeLauncherServiceResults>), Error> |
| { |
| let (launcher_proxy, server_end) = create_proxy::<fproc::LauncherMarker>()?; |
| let (sender, receiver) = oneshot::channel(); |
| fasync::Task::local(async move { |
| let stream = server_end.into_stream().expect("error making stream"); |
| run_launcher_service(stream, sender) |
| .await |
| .expect("error running fake launcher service"); |
| }) |
| .detach(); |
| Ok((launcher_proxy, receiver)) |
| } |
| |
| async fn run_launcher_service( |
| mut stream: fproc::LauncherRequestStream, |
| sender: oneshot::Sender<FakeLauncherServiceResults>, |
| ) -> Result<(), Error> { |
| let mut res = FakeLauncherServiceResults::default(); |
| while let Some(event) = stream.try_next().await? { |
| match event { |
| fproc::LauncherRequest::AddArgs { args, .. } => { |
| res.args.extend( |
| args.into_iter() |
| .map(|a| { |
| std::str::from_utf8(&a) |
| .expect("cannot convert bytes to utf8 string") |
| .to_owned() |
| }) |
| .collect::<Vec<String>>(), |
| ); |
| } |
| fproc::LauncherRequest::AddEnvirons { .. } => {} |
| fproc::LauncherRequest::AddNames { names, .. } => { |
| res.names |
| .extend(names.into_iter().map(|m| m.path).collect::<Vec<String>>()); |
| } |
| fproc::LauncherRequest::AddHandles { handles, .. } => { |
| res.handles.extend(handles.into_iter().map(|m| m.id).collect::<Vec<u32>>()); |
| } |
| fproc::LauncherRequest::SetOptions { options, .. } => { |
| res.options = zx::ProcessOptions::from_bits_retain(options); |
| } |
| fproc::LauncherRequest::CreateWithoutStarting { .. } => {} |
| fproc::LauncherRequest::Launch { .. } => {} |
| } |
| } |
| sender.send(res).map_err(|_e| format_err!("can't send result"))?; |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn missing_pkg() -> Result<(), Error> { |
| let (launcher_proxy, _server_end) = create_proxy::<fproc::LauncherMarker>()?; |
| let ns = setup_empty_namespace()?; |
| |
| assert_eq!( |
| configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/path", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: None |
| }) |
| .await, |
| Err(LaunchError::MissingPkg), |
| ); |
| |
| drop(_server_end); |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn invalid_executable() -> Result<(), Error> { |
| let (launcher_proxy, _server_end) = create_proxy::<fproc::LauncherMarker>()?; |
| let ns = setup_namespace(true, vec![])?; |
| |
| match configure_launcher(LauncherConfigArgs { |
| bin_path: "test/path", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: None, |
| }) |
| .await |
| .expect_err("should error out") |
| { |
| LaunchError::LoadingExecutable(_) => {} |
| e => panic!("Expected LoadingExecutable error, got {:?}", e), |
| } |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn invalid_pkg() -> Result<(), Error> { |
| let (launcher_proxy, _server_end) = create_proxy::<fproc::LauncherMarker>()?; |
| let ns = setup_namespace(false, vec!["/pkg"])?; |
| |
| match configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/path", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: None, |
| }) |
| .await |
| .expect_err("should error out") |
| { |
| LaunchError::LoadingExecutable(_) => {} |
| e => panic!("Expected LoadingExecutable error, got {:?}", e), |
| } |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn default_args() -> Result<(), Error> { |
| let (launcher_proxy, recv) = start_launcher()?; |
| |
| let ns = setup_namespace(true, vec![])?; |
| |
| let _launch_info = configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/runner_lib_test", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: None, |
| }) |
| .await?; |
| |
| drop(launcher_proxy); |
| |
| let ls = recv.await?; |
| |
| assert_eq!(ls.args, vec!("/pkg/bin/runner_lib_test".to_owned())); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn custom_executable_vmo() -> Result<(), Error> { |
| let (launcher_proxy, _recv) = start_launcher()?; |
| |
| let ns = setup_namespace(true, vec![])?; |
| let vmo = zx::Vmo::create(100)?; |
| vmo.write(b"my_data", 0)?; |
| let launch_info = configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/runner_lib_test", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: Some(vmo), |
| }) |
| .await?; |
| |
| let mut bytes: [u8; 10] = [0; 10]; |
| launch_info.executable.read(&mut bytes, 0)?; |
| let expected = b"my_data"; |
| assert_eq!(bytes[0..expected.len()], expected[..]); |
| Ok(()) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn extra_args() -> Result<(), Error> { |
| let (launcher_proxy, recv) = start_launcher()?; |
| |
| let ns = setup_namespace(true, vec![])?; |
| |
| let args = vec!["args1".to_owned(), "arg2".to_owned()]; |
| |
| let _launch_info = configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/runner_lib_test", |
| name: "name", |
| args: Some(args.clone()), |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: None, |
| }) |
| .await?; |
| |
| drop(launcher_proxy); |
| |
| let ls = recv.await?; |
| |
| let mut expected = vec!["/pkg/bin/runner_lib_test".to_owned()]; |
| expected.extend(args); |
| assert_eq!(ls.args, expected); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn namespace_added() -> Result<(), Error> { |
| let (launcher_proxy, recv) = start_launcher()?; |
| |
| let ns = setup_namespace(true, vec!["/some_path1", "/some_path2"])?; |
| |
| let _launch_info = configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/runner_lib_test", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: None, |
| }) |
| .await?; |
| |
| drop(launcher_proxy); |
| |
| let ls = recv.await?; |
| |
| let mut names = ls.names; |
| names.sort(); |
| assert_eq!( |
| names, |
| vec!("/pkg", "/some_path1", "/some_path2") |
| .into_iter() |
| .map(|s| s.to_string()) |
| .collect::<Vec<String>>() |
| ); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn extra_namespace_entries() -> Result<(), Error> { |
| let (launcher_proxy, recv) = start_launcher()?; |
| |
| let ns = setup_namespace(true, vec!["/some_path1", "/some_path2"])?; |
| |
| let mut names = vec![]; |
| |
| let extra_paths = vec!["/extra1", "/extra2"]; |
| |
| for path in &extra_paths { |
| let (client, _server) = create_endpoints::<fio::DirectoryMarker>(); |
| |
| names.push(fproc::NameInfo { path: path.to_string(), directory: client }); |
| } |
| |
| let _launch_info = configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/runner_lib_test", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: Some(names), |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: None, |
| }) |
| .await?; |
| |
| drop(launcher_proxy); |
| |
| let ls = recv.await?; |
| |
| let mut paths = vec!["/pkg", "/some_path1", "/some_path2"]; |
| paths.extend(extra_paths.into_iter()); |
| paths.sort(); |
| |
| let mut ls_names = ls.names; |
| ls_names.sort(); |
| |
| assert_eq!(ls_names, paths.into_iter().map(|s| s.to_string()).collect::<Vec<String>>()); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn handles_added() -> Result<(), Error> { |
| let (launcher_proxy, recv) = start_launcher()?; |
| |
| let ns = setup_namespace(true, vec![])?; |
| |
| let _launch_info = configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/runner_lib_test", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: None, |
| }) |
| .await?; |
| |
| drop(launcher_proxy); |
| |
| let ls = recv.await?; |
| |
| assert_eq!( |
| ls.handles, |
| vec!( |
| HandleInfo::new(HandleType::LdsvcLoader, 0).as_raw(), |
| HandleInfo::new(HandleType::DefaultJob, 0).as_raw() |
| ) |
| ); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn handles_added_with_custom_loader_chan() -> Result<(), Error> { |
| let (launcher_proxy, recv) = start_launcher()?; |
| |
| let (c1, _c2) = zx::Channel::create(); |
| |
| let ns = setup_namespace(true, vec![])?; |
| |
| let _launch_info = configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/runner_lib_test", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: None, |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: Some(c1), |
| executable_vmo: None, |
| }) |
| .await?; |
| |
| drop(launcher_proxy); |
| |
| let ls = recv.await?; |
| |
| assert_eq!( |
| ls.handles, |
| vec!( |
| HandleInfo::new(HandleType::LdsvcLoader, 0).as_raw(), |
| HandleInfo::new(HandleType::DefaultJob, 0).as_raw() |
| ) |
| ); |
| |
| Ok(()) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn extra_handles() -> Result<(), Error> { |
| let (launcher_proxy, recv) = start_launcher()?; |
| |
| let ns = setup_namespace(true, vec![])?; |
| |
| let mut handle_infos = vec![]; |
| for fd in 0..3 { |
| let (client, _server) = create_endpoints::<fio::DirectoryMarker>(); |
| handle_infos.push(fproc::HandleInfo { |
| handle: client.into_channel().into_handle(), |
| id: fd, |
| }); |
| } |
| |
| let _launch_info = configure_launcher(LauncherConfigArgs { |
| bin_path: "bin/runner_lib_test", |
| name: "name", |
| args: None, |
| options: zx::ProcessOptions::empty(), |
| ns: ns, |
| job: None, |
| handle_infos: Some(handle_infos), |
| name_infos: None, |
| environs: None, |
| launcher: &launcher_proxy, |
| loader_proxy_chan: None, |
| executable_vmo: None, |
| }) |
| .await?; |
| |
| drop(launcher_proxy); |
| |
| let ls = recv.await?; |
| |
| assert_eq!( |
| ls.handles, |
| vec!( |
| 0, |
| 1, |
| 2, |
| HandleInfo::new(HandleType::LdsvcLoader, 0).as_raw(), |
| HandleInfo::new(HandleType::DefaultJob, 0).as_raw(), |
| ) |
| ); |
| |
| Ok(()) |
| } |
| } |
| } |