| // 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 { |
| crate::{power, startup}, |
| anyhow::{anyhow, Context as _, Error}, |
| fidl::endpoints::{create_proxy, ClientEnd, ServerEnd}, |
| fidl_fuchsia_component as fcomponent, fidl_fuchsia_io as fio, |
| fidl_fuchsia_power_broker as fbroker, fidl_fuchsia_session as fsession, |
| fidl_fuchsia_session_power as fpower, |
| fuchsia_component::server::{ServiceFs, ServiceObjLocal}, |
| fuchsia_inspect_contrib::nodes::BoundedListNode, |
| fuchsia_zircon as zx, |
| futures::{StreamExt, TryFutureExt, TryStreamExt}, |
| std::sync::{Arc, Mutex}, |
| tracing::{error, warn}, |
| }; |
| |
| /// Maximum number of concurrent connections to the protocols served by SessionManager. |
| const MAX_CONCURRENT_CONNECTIONS: usize = 10_000; |
| |
| /// The name for the inspect node that tracks session restart timestamps. |
| const DIANGNOSTICS_SESSION_STARTED_AT_NAME: &str = "session_started_at"; |
| |
| /// The max size for the session restart timestamps list. |
| const DIANGNOSTICS_SESSION_STARTED_AT_SIZE: usize = 100; |
| |
| /// The name of the property for each entry in the session_started_at list for |
| /// the start timestamp. |
| const DIAGNOSTICS_TIME_PROPERTY_NAME: &str = "@time"; |
| |
| /// A request to connect to a protocol exposed by SessionManager. |
| pub enum IncomingRequest { |
| Launcher(fsession::LauncherRequestStream), |
| Restarter(fsession::RestarterRequestStream), |
| Lifecycle(fsession::LifecycleRequestStream), |
| Handoff(fpower::HandoffRequestStream), |
| } |
| |
| struct Diagnostics { |
| /// A list of session start/restart timestamps. |
| session_started_at: BoundedListNode, |
| } |
| |
| impl Diagnostics { |
| pub fn record_session_start(&mut self) { |
| self.session_started_at.add_entry(|node| { |
| node.record_int(DIAGNOSTICS_TIME_PROPERTY_NAME, zx::Time::get_monotonic().into_nanos()); |
| }); |
| } |
| } |
| |
| /// State for a session that will be started in the future. |
| struct PendingSession { |
| /// The server end on which the session's exposed directory will be served. |
| /// |
| /// This is the other end of `exposed_dir`. |
| pub exposed_dir_server_end: ServerEnd<fio::DirectoryMarker>, |
| } |
| |
| impl PendingSession { |
| fn new() -> (fio::DirectoryProxy, Self) { |
| let (exposed_dir, exposed_dir_server_end) = create_proxy::<fio::DirectoryMarker>().unwrap(); |
| (exposed_dir, Self { exposed_dir_server_end }) |
| } |
| } |
| |
| /// State of a started session. |
| /// |
| /// The component has been created and started, but is not guaranteed to be running since it |
| /// may be stopped through external means. |
| struct StartedSession { |
| /// The component URL of the session. |
| url: String, |
| } |
| |
| enum Session { |
| Pending(PendingSession), |
| Started(StartedSession), |
| } |
| |
| impl Session { |
| fn new_pending() -> (fio::DirectoryProxy, Self) { |
| let (proxy, pending_session) = PendingSession::new(); |
| (proxy, Self::Pending(pending_session)) |
| } |
| } |
| |
| struct PowerState { |
| /// The power element corresponding to the session. |
| /// |
| /// The async mutex exists to serialize concurrent power lease operations, where |
| /// we need to take a lock over async FIDL calls. |
| power_element: futures::lock::Mutex<Option<power::PowerElement>>, |
| |
| /// Whether the system supports suspending. |
| suspend_enabled: bool, |
| } |
| |
| impl PowerState { |
| pub fn new(suspend_enabled: bool) -> Self { |
| Self { power_element: Default::default(), suspend_enabled } |
| } |
| |
| /// Attempt to ensures that `session_manager` has a lease on the application activity element. |
| /// |
| /// This method is idempotent if it is a success. |
| pub async fn ensure_power_lease(&self) { |
| if !self.suspend_enabled { |
| return; |
| } |
| let power_element = &mut *self.power_element.lock().await; |
| if let Some(power_element) = power_element { |
| if power_element.has_lease() { |
| return; |
| } |
| } |
| *power_element = match power::PowerElement::new().await { |
| Ok(element) => Some(element), |
| Err(err) => { |
| warn!("Failed to create power element: {err}"); |
| None |
| } |
| }; |
| } |
| |
| pub async fn take_power_lease( |
| &self, |
| ) -> Result<ClientEnd<fbroker::LeaseControlMarker>, fpower::HandoffError> { |
| if !self.suspend_enabled { |
| tracing::warn!( |
| "Session component wants to take our power lease, but the platform is \ |
| configured to not support suspend" |
| ); |
| return Err(fpower::HandoffError::Unavailable); |
| } |
| tracing::info!("Session component is taking our power lease"); |
| let lease = match &mut *self.power_element.lock().await { |
| Some(power_element) => power_element.take_lease(), |
| None => return Err(fpower::HandoffError::Unavailable), |
| } |
| .ok_or(fpower::HandoffError::AlreadyTaken)?; |
| Ok(lease) |
| } |
| } |
| |
| struct SessionManagerState { |
| /// The component URL for the default session. |
| default_session_url: Option<String>, |
| |
| /// State of the session. |
| session: futures::lock::Mutex<Session>, |
| |
| /// The realm in which session components will be created. |
| realm: fcomponent::RealmProxy, |
| |
| /// Power-related state. |
| power: PowerState, |
| |
| /// Other mutable state. |
| inner: Mutex<Inner>, |
| } |
| |
| struct Inner { |
| /// Collection of diagnostics nodes. |
| diagnostics: Diagnostics, |
| |
| /// The current directory proxy we should use. When pending, requests are queued. |
| exposed_dir: fio::DirectoryProxy, |
| } |
| |
| impl SessionManagerState { |
| /// Start the session with the default session component URL, if any. |
| /// |
| /// # Errors |
| /// |
| /// Returns an error if the is no default session URL or the session could not be launched. |
| async fn start_default(&self) -> Result<(), Error> { |
| let session_url = self |
| .default_session_url |
| .as_ref() |
| .ok_or_else(|| anyhow!("no default session URL configured"))? |
| .clone(); |
| self.start(session_url).await?; |
| Ok(()) |
| } |
| |
| /// Start a session, replacing any already session. |
| async fn start(&self, url: String) -> Result<(), startup::StartupError> { |
| self.power.ensure_power_lease().await; |
| self.start_impl(&mut *self.session.lock().await, url).await |
| } |
| |
| async fn start_impl( |
| &self, |
| session: &mut Session, |
| url: String, |
| ) -> Result<(), startup::StartupError> { |
| let (proxy_on_failure, new_pending) = Session::new_pending(); |
| let pending_session = std::mem::replace(session, new_pending); |
| let pending = match pending_session { |
| Session::Pending(pending) => pending, |
| Session::Started(_) => { |
| let (proxy, pending) = PendingSession::new(); |
| self.inner.lock().unwrap().exposed_dir = proxy; |
| pending |
| } |
| }; |
| if let Err(e) = |
| startup::launch_session(&url, pending.exposed_dir_server_end, &self.realm).await |
| { |
| self.inner.lock().unwrap().exposed_dir = proxy_on_failure; |
| return Err(e); |
| } |
| *session = Session::Started(StartedSession { url }); |
| self.inner.lock().unwrap().diagnostics.record_session_start(); |
| Ok(()) |
| } |
| |
| /// Stops the session, if any. |
| async fn stop(&self) -> Result<(), startup::StartupError> { |
| self.power.ensure_power_lease().await; |
| let mut session = self.session.lock().await; |
| if let Session::Started(_) = &*session { |
| let (proxy, new_pending) = Session::new_pending(); |
| *session = new_pending; |
| self.inner.lock().unwrap().exposed_dir = proxy; |
| startup::stop_session(&self.realm).await?; |
| } |
| Ok(()) |
| } |
| |
| /// Restarts a session. |
| async fn restart(&self) -> Result<(), startup::StartupError> { |
| self.power.ensure_power_lease().await; |
| let mut session = self.session.lock().await; |
| let Session::Started(StartedSession { url }) = &mut *session else { |
| return Err(startup::StartupError::NotRunning); |
| }; |
| let url = url.clone(); |
| self.start_impl(&mut *session, url).await?; |
| Ok(()) |
| } |
| |
| async fn take_power_lease( |
| &self, |
| ) -> Result<ClientEnd<fbroker::LeaseControlMarker>, fpower::HandoffError> { |
| let lease = self.power.take_power_lease().await?; |
| Ok(lease) |
| } |
| } |
| |
| impl vfs::remote::GetRemoteDir for SessionManagerState { |
| fn get_remote_dir(&self) -> Result<fio::DirectoryProxy, zx::Status> { |
| Ok(Clone::clone(&self.inner.lock().unwrap().exposed_dir)) |
| } |
| } |
| |
| /// Manages the session lifecycle and provides services to control the session. |
| #[derive(Clone)] |
| pub struct SessionManager { |
| state: Arc<SessionManagerState>, |
| } |
| |
| impl SessionManager { |
| /// Constructs a new SessionManager. |
| /// |
| /// # Parameters |
| /// - `realm`: The realm in which session components will be created. |
| pub fn new( |
| realm: fcomponent::RealmProxy, |
| inspector: &fuchsia_inspect::Inspector, |
| default_session_url: Option<String>, |
| suspend_enabled: bool, |
| ) -> Self { |
| let session_started_at = BoundedListNode::new( |
| inspector.root().create_child(DIANGNOSTICS_SESSION_STARTED_AT_NAME), |
| DIANGNOSTICS_SESSION_STARTED_AT_SIZE, |
| ); |
| let diagnostics = Diagnostics { session_started_at }; |
| let (proxy, new_pending) = Session::new_pending(); |
| let state = SessionManagerState { |
| default_session_url, |
| session: futures::lock::Mutex::new(new_pending), |
| realm, |
| power: PowerState::new(suspend_enabled), |
| inner: Mutex::new(Inner { exposed_dir: proxy, diagnostics }), |
| }; |
| SessionManager { state: Arc::new(state) } |
| } |
| |
| #[cfg(test)] |
| pub fn new_default( |
| realm: fcomponent::RealmProxy, |
| inspector: &fuchsia_inspect::Inspector, |
| ) -> Self { |
| Self::new(realm, inspector, None, false) |
| } |
| |
| /// Starts the session with the default session component URL, if any. |
| /// |
| /// # Errors |
| /// |
| /// Returns an error if the is no default session URL or the session could not be launched. |
| pub async fn start_default_session(&mut self) -> Result<(), Error> { |
| self.state.start_default().await?; |
| Ok(()) |
| } |
| |
| /// Starts serving [`IncomingRequest`] from `svc`. |
| /// |
| /// This will return once the [`ServiceFs`] stops serving requests. |
| /// |
| /// # Errors |
| /// Returns an error if there is an issue serving the `svc` directory handle. |
| pub async fn serve( |
| &mut self, |
| fs: &mut ServiceFs<ServiceObjLocal<'_, IncomingRequest>>, |
| ) -> Result<(), Error> { |
| fs.dir("svc") |
| .add_fidl_service(IncomingRequest::Launcher) |
| .add_fidl_service(IncomingRequest::Restarter) |
| .add_fidl_service(IncomingRequest::Lifecycle) |
| .add_fidl_service(IncomingRequest::Handoff); |
| |
| // Requests to /svc_from_session are forwarded to the session's exposed dir. |
| fs.add_entry_at("svc_from_session", self.state.clone()); |
| |
| fs.take_and_serve_directory_handle()?; |
| |
| fs.for_each_concurrent(MAX_CONCURRENT_CONNECTIONS, |request| { |
| let mut session_manager = self.clone(); |
| async move { |
| session_manager |
| .handle_incoming_request(request) |
| .unwrap_or_else(|err| error!(?err)) |
| .await |
| } |
| }) |
| .await; |
| |
| Ok(()) |
| } |
| |
| /// Handles an [`IncomingRequest`]. |
| /// |
| /// This will return once the protocol connection has been closed. |
| /// |
| /// # Errors |
| /// Returns an error if there is an issue serving the request. |
| async fn handle_incoming_request(&mut self, request: IncomingRequest) -> Result<(), Error> { |
| match request { |
| IncomingRequest::Launcher(request_stream) => { |
| self.handle_launcher_request_stream(request_stream) |
| .await |
| .context("Session Launcher request stream got an error.")?; |
| } |
| IncomingRequest::Restarter(request_stream) => { |
| self.handle_restarter_request_stream(request_stream) |
| .await |
| .context("Session Restarter request stream got an error.")?; |
| } |
| IncomingRequest::Lifecycle(request_stream) => { |
| self.handle_lifecycle_request_stream(request_stream) |
| .await |
| .context("Session Lifecycle request stream got an error.")?; |
| } |
| IncomingRequest::Handoff(request_stream) => { |
| self.handle_handoff_request_stream(request_stream) |
| .await |
| .context("Session Handoff request stream got an error.")?; |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Serves a specified [`LauncherRequestStream`]. |
| /// |
| /// # Parameters |
| /// - `request_stream`: the LauncherRequestStream. |
| /// |
| /// # Errors |
| /// When an error is encountered reading from the request stream. |
| pub async fn handle_launcher_request_stream( |
| &mut self, |
| mut request_stream: fsession::LauncherRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(request) = |
| request_stream.try_next().await.context("Error handling Launcher request stream")? |
| { |
| match request { |
| fsession::LauncherRequest::Launch { configuration, responder } => { |
| let result = self.handle_launch_request(configuration).await; |
| let _ = responder.send(result); |
| } |
| }; |
| } |
| Ok(()) |
| } |
| |
| /// Serves a specified [`RestarterRequestStream`]. |
| /// |
| /// # Parameters |
| /// - `request_stream`: the RestarterRequestStream. |
| /// |
| /// # Errors |
| /// When an error is encountered reading from the request stream. |
| pub async fn handle_restarter_request_stream( |
| &mut self, |
| mut request_stream: fsession::RestarterRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(request) = |
| request_stream.try_next().await.context("Error handling Restarter request stream")? |
| { |
| match request { |
| fsession::RestarterRequest::Restart { responder } => { |
| let result = self.handle_restart_request().await; |
| let _ = responder.send(result); |
| } |
| }; |
| } |
| Ok(()) |
| } |
| |
| /// Serves a specified [`LifecycleRequestStream`]. |
| /// |
| /// # Parameters |
| /// - `request_stream`: the LifecycleRequestStream. |
| /// |
| /// # Errors |
| /// When an error is encountered reading from the request stream. |
| pub async fn handle_lifecycle_request_stream( |
| &mut self, |
| mut request_stream: fsession::LifecycleRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(request) = |
| request_stream.try_next().await.context("Error handling Lifecycle request stream")? |
| { |
| match request { |
| fsession::LifecycleRequest::Start { payload, responder } => { |
| let result = self.handle_lifecycle_start_request(payload.session_url).await; |
| let _ = responder.send(result); |
| } |
| fsession::LifecycleRequest::Stop { responder } => { |
| let result = self.handle_lifecycle_stop_request().await; |
| let _ = responder.send(result); |
| } |
| fsession::LifecycleRequest::Restart { responder } => { |
| let result = self.handle_lifecycle_restart_request().await; |
| let _ = responder.send(result); |
| } |
| fsession::LifecycleRequest::_UnknownMethod { ordinal, .. } => { |
| warn!(%ordinal, "Lifecycle received an unknown method"); |
| } |
| }; |
| } |
| Ok(()) |
| } |
| |
| pub async fn handle_handoff_request_stream( |
| &mut self, |
| mut request_stream: fpower::HandoffRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(request) = |
| request_stream.try_next().await.context("Error handling Handoff request stream")? |
| { |
| match request { |
| fpower::HandoffRequest::Take { responder } => { |
| let result = self.handle_handoff_take_request().await; |
| let _ = responder.send(result); |
| } |
| fpower::HandoffRequest::_UnknownMethod { ordinal, .. } => { |
| warn!(%ordinal, "Lifecycle received an unknown method") |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Handles calls to Launcher.Launch(). |
| /// |
| /// # Parameters |
| /// - configuration: The launch configuration for the new session. |
| async fn handle_launch_request( |
| &mut self, |
| configuration: fsession::LaunchConfiguration, |
| ) -> Result<(), fsession::LaunchError> { |
| let session_url = configuration.session_url.ok_or(fsession::LaunchError::InvalidArgs)?; |
| self.state.start(session_url).await.map_err(Into::into) |
| } |
| |
| /// Handles a Restarter.Restart() request. |
| async fn handle_restart_request(&mut self) -> Result<(), fsession::RestartError> { |
| self.state.restart().await.map_err(Into::into) |
| } |
| |
| /// Handles a `Lifecycle.Start()` request. |
| /// |
| /// # Parameters |
| /// - session_url: The component URL for the session to start. |
| async fn handle_lifecycle_start_request( |
| &mut self, |
| session_url: Option<String>, |
| ) -> Result<(), fsession::LifecycleError> { |
| let session_url = session_url |
| .as_ref() |
| .or(self.state.default_session_url.as_ref()) |
| .ok_or(fsession::LifecycleError::NotFound)? |
| .to_owned(); |
| self.state.start(session_url).await.map_err(Into::into) |
| } |
| |
| /// Handles a `Lifecycle.Stop()` request. |
| async fn handle_lifecycle_stop_request(&mut self) -> Result<(), fsession::LifecycleError> { |
| self.state.stop().await.map_err(Into::into) |
| } |
| |
| /// Handles a `Lifecycle.Restart()` request. |
| async fn handle_lifecycle_restart_request(&mut self) -> Result<(), fsession::LifecycleError> { |
| self.state.restart().await.map_err(Into::into) |
| } |
| |
| /// Handles a `Handoff.Take()` request. |
| async fn handle_handoff_take_request( |
| &mut self, |
| ) -> Result<ClientEnd<fbroker::LeaseControlMarker>, fpower::HandoffError> { |
| self.state.take_power_lease().await.map_err(Into::into) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::SessionManager, |
| anyhow::{anyhow, Error}, |
| diagnostics_assertions::assert_data_tree, |
| diagnostics_assertions::AnyProperty, |
| fidl::endpoints::{create_proxy_and_stream, spawn_stream_handler, ServerEnd}, |
| fidl_fuchsia_component as fcomponent, fidl_fuchsia_io as fio, |
| fidl_fuchsia_session as fsession, |
| futures::channel::mpsc, |
| futures::prelude::*, |
| lazy_static::lazy_static, |
| session_testing::{spawn_directory_server, spawn_noop_directory_server, spawn_server}, |
| test_util::Counter, |
| }; |
| |
| fn serve_launcher(session_manager: SessionManager) -> fsession::LauncherProxy { |
| let (launcher_proxy, launcher_stream) = |
| create_proxy_and_stream::<fsession::LauncherMarker>().unwrap(); |
| { |
| let mut session_manager_ = session_manager.clone(); |
| fuchsia_async::Task::spawn(async move { |
| session_manager_ |
| .handle_launcher_request_stream(launcher_stream) |
| .await |
| .expect("Session launcher request stream got an error."); |
| }) |
| .detach(); |
| } |
| launcher_proxy |
| } |
| |
| fn serve_restarter(session_manager: SessionManager) -> fsession::RestarterProxy { |
| let (restarter_proxy, restarter_stream) = |
| create_proxy_and_stream::<fsession::RestarterMarker>().unwrap(); |
| { |
| let mut session_manager_ = session_manager.clone(); |
| fuchsia_async::Task::spawn(async move { |
| session_manager_ |
| .handle_restarter_request_stream(restarter_stream) |
| .await |
| .expect("Session restarter request stream got an error."); |
| }) |
| .detach(); |
| } |
| restarter_proxy |
| } |
| |
| fn serve_lifecycle(session_manager: SessionManager) -> fsession::LifecycleProxy { |
| let (lifecycle_proxy, lifecycle_stream) = |
| create_proxy_and_stream::<fsession::LifecycleMarker>().unwrap(); |
| { |
| let mut session_manager_ = session_manager.clone(); |
| fuchsia_async::Task::spawn(async move { |
| session_manager_ |
| .handle_lifecycle_request_stream(lifecycle_stream) |
| .await |
| .expect("Session lifecycle request stream got an error."); |
| }) |
| .detach(); |
| } |
| lifecycle_proxy |
| } |
| |
| fn spawn_noop_controller_server(server_end: ServerEnd<fcomponent::ControllerMarker>) { |
| spawn_server(server_end, move |controller_request| match controller_request { |
| fcomponent::ControllerRequest::Start { responder, .. } => { |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::ControllerRequest::IsStarted { .. } => unimplemented!(), |
| fcomponent::ControllerRequest::GetExposedDictionary { .. } => { |
| unimplemented!() |
| } |
| fcomponent::ControllerRequest::_UnknownMethod { .. } => { |
| unimplemented!() |
| } |
| }); |
| } |
| |
| fn open_session_exposed_dir( |
| session_manager: SessionManager, |
| flags: fio::OpenFlags, |
| path: &str, |
| server_end: ServerEnd<fio::NodeMarker>, |
| ) { |
| session_manager |
| .state |
| .inner |
| .lock() |
| .unwrap() |
| .exposed_dir |
| .open(flags, fio::ModeType::empty(), path, server_end) |
| .unwrap(); |
| } |
| |
| /// Verifies that Launcher.Launch creates a new session. |
| #[fuchsia::test] |
| async fn test_launch() { |
| let session_url = "session"; |
| |
| let realm = spawn_stream_handler(move |realm_request| async move { |
| match realm_request { |
| fcomponent::RealmRequest::DestroyChild { child: _, responder } => { |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => { |
| assert_eq!(decl.url.unwrap(), session_url); |
| spawn_noop_controller_server(args.controller.unwrap()); |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => { |
| spawn_noop_directory_server(exposed_dir); |
| let _ = responder.send(Ok(())); |
| } |
| _ => panic!("Realm handler received an unexpected request"), |
| }; |
| }) |
| .unwrap(); |
| |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let session_manager = SessionManager::new_default(realm, &inspector); |
| let launcher = serve_launcher(session_manager); |
| |
| assert!(launcher |
| .launch(&fsession::LaunchConfiguration { |
| session_url: Some(session_url.to_string()), |
| ..Default::default() |
| }) |
| .await |
| .is_ok()); |
| assert_data_tree!(inspector, root: { |
| session_started_at: { |
| "0": { |
| "@time": AnyProperty |
| } |
| } |
| }); |
| } |
| |
| /// Verifies that Restarter.Restart restarts an existing session. |
| #[fuchsia::test] |
| async fn test_restarter_restart() { |
| let session_url = "session"; |
| |
| let realm = spawn_stream_handler(move |realm_request| async move { |
| match realm_request { |
| fcomponent::RealmRequest::DestroyChild { child: _, responder } => { |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => { |
| assert_eq!(decl.url.unwrap(), session_url); |
| spawn_noop_controller_server(args.controller.unwrap()); |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => { |
| spawn_noop_directory_server(exposed_dir); |
| let _ = responder.send(Ok(())); |
| } |
| _ => panic!("Realm handler received an unexpected request"), |
| }; |
| }) |
| .unwrap(); |
| |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let session_manager = SessionManager::new_default(realm, &inspector); |
| let launcher = serve_launcher(session_manager.clone()); |
| let restarter = serve_restarter(session_manager); |
| |
| assert!(launcher |
| .launch(&fsession::LaunchConfiguration { |
| session_url: Some(session_url.to_string()), |
| ..Default::default() |
| }) |
| .await |
| .expect("could not call Launch") |
| .is_ok()); |
| |
| assert!(restarter.restart().await.expect("could not call Restart").is_ok()); |
| |
| assert_data_tree!(inspector, root: { |
| session_started_at: { |
| "0": { |
| "@time": AnyProperty |
| }, |
| "1": { |
| "@time": AnyProperty |
| } |
| } |
| }); |
| } |
| |
| /// Verifies that Launcher.Restart return an error if there is no running existing session. |
| #[fuchsia::test] |
| async fn test_restarter_restart_error_not_running() { |
| let realm = spawn_stream_handler(move |_realm_request| async move { |
| panic!("Realm should not receive any requests as there is no session to launch") |
| }) |
| .unwrap(); |
| |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let session_manager = SessionManager::new_default(realm, &inspector); |
| let restarter = serve_restarter(session_manager); |
| |
| assert_eq!( |
| Err(fsession::RestartError::NotRunning), |
| restarter.restart().await.expect("could not call Restart") |
| ); |
| |
| assert_data_tree!(inspector, root: { |
| session_started_at: {} |
| }); |
| } |
| |
| /// Verifies that Lifecycle.Start creates a new session. |
| #[fuchsia::test] |
| async fn test_start() { |
| let session_url = "session"; |
| |
| let realm = spawn_stream_handler(move |realm_request| async move { |
| match realm_request { |
| fcomponent::RealmRequest::DestroyChild { child: _, responder } => { |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => { |
| assert_eq!(decl.url.unwrap(), session_url); |
| spawn_noop_controller_server(args.controller.unwrap()); |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => { |
| spawn_noop_directory_server(exposed_dir); |
| let _ = responder.send(Ok(())); |
| } |
| _ => panic!("Realm handler received an unexpected request"), |
| }; |
| }) |
| .unwrap(); |
| |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let session_manager = SessionManager::new_default(realm, &inspector); |
| let lifecycle = serve_lifecycle(session_manager); |
| |
| assert!(lifecycle |
| .start(&fsession::LifecycleStartRequest { |
| session_url: Some(session_url.to_string()), |
| ..Default::default() |
| }) |
| .await |
| .is_ok()); |
| assert_data_tree!(inspector, root: { |
| session_started_at: { |
| "0": { |
| "@time": AnyProperty |
| } |
| } |
| }); |
| } |
| |
| /// Verifies that Lifecycle.Start starts the default session if no URL is provided. |
| #[fuchsia::test] |
| async fn test_start_default() { |
| let default_session_url = "session"; |
| |
| let realm = spawn_stream_handler(move |realm_request| async move { |
| match realm_request { |
| fcomponent::RealmRequest::DestroyChild { child: _, responder } => { |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => { |
| assert_eq!(decl.url.unwrap(), default_session_url); |
| spawn_noop_controller_server(args.controller.unwrap()); |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => { |
| spawn_noop_directory_server(exposed_dir); |
| let _ = responder.send(Ok(())); |
| } |
| _ => panic!("Realm handler received an unexpected request"), |
| }; |
| }) |
| .unwrap(); |
| |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let session_manager = |
| SessionManager::new(realm, &inspector, Some(default_session_url.to_owned()), false); |
| let lifecycle = serve_lifecycle(session_manager); |
| |
| assert!(lifecycle |
| .start(&fsession::LifecycleStartRequest { session_url: None, ..Default::default() }) |
| .await |
| .is_ok()); |
| assert_data_tree!(inspector, root: { |
| session_started_at: { |
| "0": { |
| "@time": AnyProperty |
| } |
| } |
| }); |
| } |
| |
| /// Verifies that Lifecycle.Stop stops an existing session by destroying its component. |
| #[fuchsia::test] |
| async fn test_stop_destroys_component() { |
| lazy_static! { |
| static ref NUM_DESTROY_CHILD_CALLS: Counter = Counter::new(0); |
| } |
| |
| let session_url = "session"; |
| |
| let realm = spawn_stream_handler(move |realm_request| async move { |
| match realm_request { |
| fcomponent::RealmRequest::DestroyChild { child: _, responder } => { |
| NUM_DESTROY_CHILD_CALLS.inc(); |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => { |
| assert_eq!(decl.url.unwrap(), session_url); |
| spawn_noop_controller_server(args.controller.unwrap()); |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => { |
| spawn_noop_directory_server(exposed_dir); |
| let _ = responder.send(Ok(())); |
| } |
| _ => panic!("Realm handler received an unexpected request"), |
| }; |
| }) |
| .unwrap(); |
| |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let session_manager = SessionManager::new_default(realm, &inspector); |
| let lifecycle = serve_lifecycle(session_manager); |
| |
| assert!(lifecycle |
| .start(&fsession::LifecycleStartRequest { |
| session_url: Some(session_url.to_string()), |
| ..Default::default() |
| }) |
| .await |
| .is_ok()); |
| // Start attempts to destroy any existing session first. |
| assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 1); |
| assert_data_tree!(inspector, root: { |
| session_started_at: { |
| "0": { |
| "@time": AnyProperty |
| } |
| } |
| }); |
| |
| assert!(lifecycle.stop().await.is_ok()); |
| assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 2); |
| } |
| |
| /// Verifies that Lifecycle.Restart restarts an existing session. |
| #[fuchsia::test] |
| async fn test_lifecycle_restart() { |
| let session_url = "session"; |
| |
| let realm = spawn_stream_handler(move |realm_request| async move { |
| match realm_request { |
| fcomponent::RealmRequest::DestroyChild { child: _, responder } => { |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => { |
| assert_eq!(decl.url.unwrap(), session_url); |
| spawn_noop_controller_server(args.controller.unwrap()); |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => { |
| spawn_noop_directory_server(exposed_dir); |
| let _ = responder.send(Ok(())); |
| } |
| _ => panic!("Realm handler received an unexpected request"), |
| }; |
| }) |
| .unwrap(); |
| |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let session_manager = SessionManager::new_default(realm, &inspector); |
| let lifecycle = serve_lifecycle(session_manager.clone()); |
| |
| assert!(lifecycle |
| .start(&fsession::LifecycleStartRequest { |
| session_url: Some(session_url.to_string()), |
| ..Default::default() |
| }) |
| .await |
| .expect("could not call Launch") |
| .is_ok()); |
| |
| assert!(lifecycle.restart().await.expect("could not call Restart").is_ok()); |
| |
| assert_data_tree!(inspector, root: { |
| session_started_at: { |
| "0": { |
| "@time": AnyProperty |
| }, |
| "1": { |
| "@time": AnyProperty |
| } |
| } |
| }); |
| } |
| |
| /// Verifies that a node can be opened in the session's exposed dir before the session is |
| /// started, and that it is connected once the session is started. |
| #[fuchsia::test] |
| async fn test_svc_from_session_before_start() -> Result<(), Error> { |
| let session_url = "session"; |
| let svc_path = "foo"; |
| |
| let (path_sender, mut path_receiver) = mpsc::channel(1); |
| |
| let session_exposed_dir_handler = move |directory_request| match directory_request { |
| fio::DirectoryRequest::Open { path, .. } => { |
| let mut path_sender = path_sender.clone(); |
| path_sender.try_send(path).unwrap(); |
| } |
| _ => panic!("Directory handler received an unexpected request"), |
| }; |
| |
| let realm = spawn_stream_handler(move |realm_request| { |
| let session_exposed_dir_handler = session_exposed_dir_handler.clone(); |
| async move { |
| match realm_request { |
| fcomponent::RealmRequest::DestroyChild { responder, .. } => { |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::CreateChild { args, responder, .. } => { |
| spawn_noop_controller_server(args.controller.unwrap()); |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::OpenExposedDir { exposed_dir, responder, .. } => { |
| spawn_directory_server(exposed_dir, session_exposed_dir_handler); |
| let _ = responder.send(Ok(())); |
| } |
| _ => panic!("Realm handler received an unexpected request"), |
| }; |
| } |
| })?; |
| |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let session_manager = SessionManager::new_default(realm, &inspector); |
| let lifecycle = serve_lifecycle(session_manager.clone()); |
| |
| // Open an arbitrary node in the session's exposed dir. |
| // The actual protocol does not matter because it's not being served. |
| let (_client_end, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>().unwrap(); |
| |
| open_session_exposed_dir(session_manager, fio::OpenFlags::empty(), svc_path, server_end); |
| // Start the session. |
| lifecycle |
| .start(&fsession::LifecycleStartRequest { |
| session_url: Some(session_url.to_string()), |
| ..Default::default() |
| }) |
| .await? |
| .map_err(|err| anyhow!("failed to start: {:?}", err))?; |
| |
| // The exposed dir should have received the Open request. |
| assert_eq!(path_receiver.next().await.unwrap(), svc_path); |
| |
| Ok(()) |
| } |
| |
| /// Verifies that a node in the session's exposed dir can be opened after the session has |
| /// started. |
| #[fuchsia::test] |
| async fn test_svc_from_session_after_start() -> Result<(), Error> { |
| let session_url = "session"; |
| let svc_path = "foo"; |
| |
| let (path_sender, mut path_receiver) = mpsc::channel(1); |
| |
| let session_exposed_dir_handler = move |directory_request| match directory_request { |
| fio::DirectoryRequest::Open { path, .. } => { |
| let mut path_sender = path_sender.clone(); |
| path_sender.try_send(path).unwrap(); |
| } |
| _ => panic!("Directory handler received an unexpected request"), |
| }; |
| |
| let realm = spawn_stream_handler(move |realm_request| { |
| let session_exposed_dir_handler = session_exposed_dir_handler.clone(); |
| async move { |
| match realm_request { |
| fcomponent::RealmRequest::DestroyChild { responder, .. } => { |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::CreateChild { args, responder, .. } => { |
| spawn_noop_controller_server(args.controller.unwrap()); |
| let _ = responder.send(Ok(())); |
| } |
| fcomponent::RealmRequest::OpenExposedDir { exposed_dir, responder, .. } => { |
| spawn_directory_server(exposed_dir, session_exposed_dir_handler); |
| let _ = responder.send(Ok(())); |
| } |
| _ => panic!("Realm handler received an unexpected request"), |
| }; |
| } |
| })?; |
| |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let session_manager = SessionManager::new_default(realm, &inspector); |
| let lifecycle = serve_lifecycle(session_manager.clone()); |
| |
| lifecycle |
| .start(&fsession::LifecycleStartRequest { |
| session_url: Some(session_url.to_string()), |
| ..Default::default() |
| }) |
| .await? |
| .map_err(|err| anyhow!("failed to start: {:?}", err))?; |
| |
| // Open an arbitrary node in the session's exposed dir. |
| // The actual protocol does not matter because it's not being served. |
| let (_client_end, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>().unwrap(); |
| |
| open_session_exposed_dir(session_manager, fio::OpenFlags::empty(), svc_path, server_end); |
| |
| assert_eq!(path_receiver.next().await.unwrap(), svc_path); |
| |
| Ok(()) |
| } |
| } |