| // 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::cobalt, |
| argh::FromArgs, |
| fidl_fuchsia_component as fcomponent, fidl_fuchsia_sys2 as fsys, fuchsia_async as fasync, |
| fuchsia_syslog::fx_log_info, |
| fuchsia_zircon as zx, realm_management, |
| serde::{Deserialize, Serialize}, |
| serde_json, |
| std::fs, |
| thiserror::{self, Error}, |
| }; |
| |
| #[derive(FromArgs)] |
| /// The session manager component. |
| pub struct SessionManagerArgs { |
| #[argh(option, short = 's')] |
| /// the URL for the session to start. |
| pub session_url: Option<String>, |
| } |
| |
| #[derive(Serialize, Deserialize)] |
| /// The session manager component. |
| pub struct SessionManagerConfigs { |
| /// the URL for the session to start. |
| pub session_url: String, |
| } |
| |
| /// Errors returned by calls startup functions. |
| #[derive(Debug, Error, Clone, PartialEq)] |
| pub enum StartupError { |
| #[error("Existing session not destroyed at \"{}/{}\": {:?}", collection, name, err)] |
| NotDestroyed { name: String, collection: String, err: fcomponent::Error }, |
| |
| #[error("Session {} not created at \"{}/{}\": {:?}", url, collection, name, err)] |
| NotCreated { name: String, collection: String, url: String, err: fcomponent::Error }, |
| |
| #[error("Session {} not bound at \"{}/{}\": {:?}", url, collection, name, err)] |
| NotBound { name: String, collection: String, url: String, err: fcomponent::Error }, |
| } |
| |
| /// The name of the session child component. |
| const SESSION_NAME: &str = "session"; |
| |
| /// The name of the child collection the session is added to, must match the declaration in |
| /// session_manager.cml. |
| const SESSION_CHILD_COLLECTION: &str = "session"; |
| |
| /// The path to the configuration file for the session. |
| const CONFIG_PATH: &str = "/config/data/startup.config"; |
| |
| /// Gets the session url from `/config/data/startup.config`. |
| /// |
| /// If no configuration file exists, gets the session url from `std::env::args()`. |
| /// Fails with a comment about the missing --session_url option if the argument isn't provided. |
| /// |
| /// # Returns |
| /// `String` if the session url argument exists, else `None`. |
| pub fn get_session_url() -> Option<String> { |
| let mut session_url: Option<String> = None; |
| if let Ok(config_str) = fs::read_to_string(CONFIG_PATH) { |
| if let Ok(session_manager_args) = serde_json::from_str::<SessionManagerConfigs>(&config_str) |
| { |
| session_url = Some(session_manager_args.session_url); |
| } |
| } |
| |
| if session_url.is_none() { |
| let SessionManagerArgs { session_url } = argh::from_env(); |
| return session_url; |
| } |
| |
| session_url |
| } |
| |
| /// Launches the specified session. |
| /// |
| /// Any existing session child will be destroyed prior to launching the new session. |
| /// |
| /// Returns a channel to the session component's `exposed_dir` directory, or an error. |
| /// |
| /// # Parameters |
| /// - `session_url`: The URL of the session to launch. |
| /// - `realm`: The realm in which to launch the session. |
| /// |
| /// # Errors |
| /// If there was a problem creating or binding to the session component instance. |
| pub async fn launch_session( |
| session_url: &str, |
| realm: &fsys::RealmProxy, |
| ) -> Result<zx::Channel, StartupError> { |
| fx_log_info!("Launching session: {}", session_url); |
| |
| let start_time = zx::Time::get_monotonic(); |
| let exposed_dir = set_session(&session_url, realm).await?; |
| let end_time = zx::Time::get_monotonic(); |
| |
| let url = session_url.to_string(); |
| fasync::Task::local(async move { |
| if let Ok(cobalt_logger) = cobalt::get_logger() { |
| // The result is disregarded as there is not retry-logic if it fails, and the error is |
| // not meant to be fatal. |
| let _ = |
| cobalt::log_session_launch_time(cobalt_logger, &url, start_time, end_time).await; |
| } |
| }) |
| .detach(); |
| |
| Ok(exposed_dir) |
| } |
| |
| /// Sets the currently active session. |
| /// |
| /// If an existing session is running, the session's component instance will be destroyed prior to |
| /// creating the new session, effectively replacing the session. |
| /// |
| /// Returns a channel to the session component's `exposed_dir` directory, or an error. |
| /// |
| /// # Parameters |
| /// - `session_url`: The URL of the session to instantiate. |
| /// - `realm`: The realm in which to create the session. |
| /// |
| /// # Errors |
| /// Returns an error if any of the realm operations fail, or the realm is unavailable. |
| async fn set_session( |
| session_url: &str, |
| realm: &fsys::RealmProxy, |
| ) -> Result<zx::Channel, StartupError> { |
| realm_management::destroy_child_component(SESSION_NAME, SESSION_CHILD_COLLECTION, realm) |
| .await |
| .or_else(|err: fcomponent::Error| match err { |
| // Since the intent is simply to clear out the existing session child if it exists, |
| // related errors are disregarded. |
| fcomponent::Error::InvalidArguments |
| | fcomponent::Error::InstanceNotFound |
| | fcomponent::Error::CollectionNotFound => Ok(()), |
| _ => Err(err), |
| }) |
| .map_err(|err| StartupError::NotDestroyed { |
| name: SESSION_NAME.to_string(), |
| collection: SESSION_CHILD_COLLECTION.to_string(), |
| err, |
| })?; |
| |
| realm_management::create_child_component( |
| SESSION_NAME, |
| &session_url, |
| SESSION_CHILD_COLLECTION, |
| realm, |
| ) |
| .await |
| .map_err(|err| StartupError::NotCreated { |
| name: SESSION_NAME.to_string(), |
| collection: SESSION_CHILD_COLLECTION.to_string(), |
| url: session_url.to_string(), |
| err, |
| })?; |
| |
| realm_management::bind_child_component(SESSION_NAME, SESSION_CHILD_COLLECTION, realm) |
| .await |
| .map_err(|err| StartupError::NotBound { |
| name: SESSION_NAME.to_string(), |
| collection: SESSION_CHILD_COLLECTION.to_string(), |
| url: session_url.to_string(), |
| err, |
| }) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::{set_session, zx, SESSION_CHILD_COLLECTION, SESSION_NAME}, |
| fidl::endpoints::create_proxy_and_stream, |
| fidl_fuchsia_sys2 as fsys, fuchsia_async as fasync, |
| futures::prelude::*, |
| }; |
| |
| /// Spawns a local `fidl_fuchsia_sys2::Realm` server, and returns a proxy to the spawned server. |
| /// The provided `request_handler` is notified when an incoming request is received. |
| /// |
| /// # Parameters |
| /// - `request_handler`: A function which is called with incoming requests to the spawned |
| /// `Realm` server. |
| /// # Returns |
| /// A `RealmProxy` to the spawned server. |
| fn spawn_realm_server<F: 'static>(mut request_handler: F) -> fsys::RealmProxy |
| where |
| F: FnMut(fsys::RealmRequest) + Send, |
| { |
| let (realm_proxy, mut realm_server) = create_proxy_and_stream::<fsys::RealmMarker>() |
| .expect("Failed to create realm proxy and server."); |
| |
| fasync::Task::spawn(async move { |
| while let Some(realm_request) = realm_server.try_next().await.unwrap() { |
| request_handler(realm_request); |
| } |
| }) |
| .detach(); |
| |
| realm_proxy |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn set_session_calls_realm_methods_in_appropriate_order() { |
| let session_url = "session"; |
| // The number of realm calls which have been made so far. |
| let mut num_realm_requests: i32 = 0; |
| |
| let realm = spawn_realm_server(move |realm_request| { |
| match realm_request { |
| fsys::RealmRequest::DestroyChild { child, responder } => { |
| assert_eq!(num_realm_requests, 0); |
| assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string())); |
| assert_eq!(child.name, SESSION_NAME); |
| |
| let _ = responder.send(&mut Ok(())); |
| } |
| fsys::RealmRequest::CreateChild { collection, decl, responder } => { |
| assert_eq!(num_realm_requests, 1); |
| assert_eq!(decl.url.unwrap(), session_url); |
| assert_eq!(decl.name.unwrap(), SESSION_NAME); |
| assert_eq!(&collection.name, SESSION_CHILD_COLLECTION); |
| |
| let _ = responder.send(&mut Ok(())); |
| } |
| fsys::RealmRequest::BindChild { child, exposed_dir: _, responder } => { |
| assert_eq!(num_realm_requests, 2); |
| assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string())); |
| assert_eq!(child.name, SESSION_NAME); |
| |
| let _ = responder.send(&mut Ok(())); |
| } |
| _ => { |
| assert!(false); |
| } |
| }; |
| num_realm_requests += 1; |
| }); |
| |
| assert!(set_session(session_url, &realm).await.is_ok()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn set_session_returns_channel_bound_to_exposed_dir() { |
| let session_url = "session"; |
| let (exposed_dir_server_end_sender, exposed_dir_server_end_receiver) = |
| std::sync::mpsc::channel(); |
| |
| let realm = spawn_realm_server(move |realm_request| { |
| match realm_request { |
| fsys::RealmRequest::DestroyChild { responder, .. } => { |
| let _ = responder.send(&mut Ok(())); |
| } |
| fsys::RealmRequest::CreateChild { responder, .. } => { |
| let _ = responder.send(&mut Ok(())); |
| } |
| fsys::RealmRequest::BindChild { exposed_dir, responder, .. } => { |
| exposed_dir_server_end_sender |
| .send(exposed_dir) |
| .expect("Failed to relay `exposed_dir`"); |
| let _ = responder.send(&mut Ok(())); |
| } |
| _ => { |
| assert!(false); |
| } |
| }; |
| }); |
| |
| let exposed_dir_client_end = match set_session(session_url, &realm).await { |
| Ok(exposed_dir_client_end) => exposed_dir_client_end, |
| Err(e) => panic!("Failed to set_session() {:?}", e), |
| }; |
| let exposed_dir_server_end = |
| exposed_dir_server_end_receiver.recv().expect("Failed to read exposed_dir from relay"); |
| exposed_dir_server_end |
| .into_channel() |
| .write(b"hello world", /* handles */ &mut vec![]) |
| .expect("Failed to write to server end"); |
| |
| let mut read_buf = zx::MessageBuf::new(); |
| exposed_dir_client_end.read(&mut read_buf).expect("Failed to read from client end"); |
| assert_eq!(read_buf.bytes(), b"hello world", "server and client channels do not match"); |
| } |
| } |