session_manager_lib/
startup.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::cobalt;
6use fidl::endpoints::{ServerEnd, create_proxy};
7use fuchsia_component::runtime::{Data, DataValue, Dictionary};
8use log::info;
9use thiserror::Error;
10use {
11    fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl,
12    fidl_fuchsia_io as fio, fidl_fuchsia_session as fsession, fuchsia_async as fasync,
13};
14
15/// Errors returned by calls startup functions.
16#[derive(Debug, Error, Clone, PartialEq)]
17pub enum StartupError {
18    #[error("Existing session not destroyed at \"{}/{}\": {:?}", collection, name, err)]
19    NotDestroyed { name: String, collection: String, err: fcomponent::Error },
20
21    #[error("Session {} not created at \"{}/{}\": Bedrock error {:?}", url, collection, name, err)]
22    BedrockError { name: String, collection: String, url: String, err: String },
23
24    #[error("Session {} not created at \"{}/{}\": {:?}", url, collection, name, err)]
25    NotCreated { name: String, collection: String, url: String, err: fcomponent::Error },
26
27    #[error(
28        "Exposed directory of session {} at \"{}/{}\" not opened: {:?}",
29        url,
30        collection,
31        name,
32        err
33    )]
34    ExposedDirNotOpened { name: String, collection: String, url: String, err: fcomponent::Error },
35
36    #[error("Session {} not launched at \"{}/{}\": {:?}", url, collection, name, err)]
37    NotLaunched { name: String, collection: String, url: String, err: fcomponent::Error },
38
39    #[error("Attempt to restart a not running session")]
40    NotRunning,
41}
42
43impl From<StartupError> for fsession::LaunchError {
44    fn from(e: StartupError) -> fsession::LaunchError {
45        match e {
46            StartupError::NotDestroyed { .. } => fsession::LaunchError::DestroyComponentFailed,
47            StartupError::NotCreated { err, .. } => match err {
48                fcomponent::Error::InstanceCannotResolve => fsession::LaunchError::NotFound,
49                _ => fsession::LaunchError::CreateComponentFailed,
50            },
51            StartupError::ExposedDirNotOpened { .. }
52            | StartupError::BedrockError { .. }
53            | StartupError::NotLaunched { .. } => fsession::LaunchError::CreateComponentFailed,
54            StartupError::NotRunning => fsession::LaunchError::NotFound,
55        }
56    }
57}
58
59impl From<StartupError> for fsession::RestartError {
60    fn from(e: StartupError) -> fsession::RestartError {
61        match e {
62            StartupError::NotDestroyed { .. } => fsession::RestartError::DestroyComponentFailed,
63            StartupError::NotCreated { err, .. } => match err {
64                fcomponent::Error::InstanceCannotResolve => fsession::RestartError::NotFound,
65                _ => fsession::RestartError::CreateComponentFailed,
66            },
67            StartupError::ExposedDirNotOpened { .. }
68            | StartupError::BedrockError { .. }
69            | StartupError::NotLaunched { .. } => fsession::RestartError::CreateComponentFailed,
70            StartupError::NotRunning => fsession::RestartError::NotRunning,
71        }
72    }
73}
74
75impl From<StartupError> for fsession::LifecycleError {
76    fn from(e: StartupError) -> fsession::LifecycleError {
77        match e {
78            StartupError::NotDestroyed { .. } => fsession::LifecycleError::DestroyComponentFailed,
79            StartupError::NotCreated { err, .. } => match err {
80                fcomponent::Error::InstanceCannotResolve => {
81                    fsession::LifecycleError::ResolveComponentFailed
82                }
83                _ => fsession::LifecycleError::CreateComponentFailed,
84            },
85            StartupError::ExposedDirNotOpened { .. }
86            | StartupError::BedrockError { .. }
87            | StartupError::NotLaunched { .. } => fsession::LifecycleError::CreateComponentFailed,
88            StartupError::NotRunning => fsession::LifecycleError::NotFound,
89        }
90    }
91}
92
93/// The name of the session child component.
94const SESSION_NAME: &str = "session";
95
96/// The name of the child collection the session is added to, must match the declaration in
97/// `session_manager.cml`.
98const SESSION_CHILD_COLLECTION: &str = "session";
99
100/// Launches the specified session.
101///
102/// Any existing session child will be destroyed prior to launching the new session.
103///
104/// Returns a controller for the session component, or an error.
105///
106/// # Parameters
107/// - `session_url`: The URL of the session to launch.
108/// - `config_capabilities`: Configuration capabilities that will target the session.
109/// - `exposed_dir`: The server end on which to serve the session's exposed directory.
110/// - `realm`: The realm in which to launch the session.
111///
112/// # Errors
113/// If there was a problem creating or binding to the session component instance.
114pub async fn launch_session(
115    session_url: &str,
116    config_capabilities: Vec<fdecl::Configuration>,
117    exposed_dir: ServerEnd<fio::DirectoryMarker>,
118    realm: &fcomponent::RealmProxy,
119) -> Result<fcomponent::ExecutionControllerProxy, StartupError> {
120    info!(session_url; "Launching session");
121
122    let start_time = zx::MonotonicInstant::get();
123    let controller = set_session(session_url, config_capabilities, realm, exposed_dir).await?;
124    let end_time = zx::MonotonicInstant::get();
125
126    fasync::Task::local(async move {
127        if let Ok(cobalt_logger) = cobalt::get_logger() {
128            // The result is disregarded as there is not retry-logic if it fails, and the error is
129            // not meant to be fatal.
130            let _ = cobalt::log_session_launch_time(cobalt_logger, start_time, end_time).await;
131        }
132    })
133    .detach();
134
135    Ok(controller)
136}
137
138/// Stops the current session, if any.
139///
140/// # Parameters
141/// - `realm`: The realm in which the session exists.
142///
143/// # Errors
144/// `StartupError::NotDestroyed` if the session component could not be destroyed.
145pub async fn stop_session(realm: &fcomponent::RealmProxy) -> Result<(), StartupError> {
146    realm_management::destroy_child_component(SESSION_NAME, SESSION_CHILD_COLLECTION, realm)
147        .await
148        .map_err(|err| StartupError::NotDestroyed {
149            name: SESSION_NAME.to_string(),
150            collection: SESSION_CHILD_COLLECTION.to_string(),
151            err,
152        })
153}
154
155async fn create_config_dict(
156    config_capabilities: Vec<fdecl::Configuration>,
157) -> Result<Option<Dictionary>, anyhow::Error> {
158    if config_capabilities.is_empty() {
159        return Ok(None);
160    }
161    let dictionary = Dictionary::new().await;
162    for config in config_capabilities {
163        let Some(value) = config.value else { continue };
164        let Some(key) = config.name else { continue };
165        let data = Data::new(DataValue::Bytes(fidl::persist(&value)?)).await;
166        dictionary.insert(&key, data).await;
167    }
168    Ok(Some(dictionary))
169}
170
171/// Sets the currently active session.
172///
173/// If an existing session is running, the session's component instance will be destroyed prior to
174/// creating the new session, effectively replacing the session.
175///
176/// # Parameters
177/// - `session_url`: The URL of the session to instantiate.
178/// - `config_capabilities`: Configuration capabilities that will target the session.
179/// - `realm`: The realm in which to create the session.
180/// - `exposed_dir`: The server end on which the session's exposed directory will be served.
181///
182/// # Errors
183/// Returns an error if any of the realm operations fail, or the realm is unavailable.
184async fn set_session(
185    session_url: &str,
186    config_capabilities: Vec<fdecl::Configuration>,
187    realm: &fcomponent::RealmProxy,
188    exposed_dir: ServerEnd<fio::DirectoryMarker>,
189) -> Result<fcomponent::ExecutionControllerProxy, StartupError> {
190    realm_management::destroy_child_component(SESSION_NAME, SESSION_CHILD_COLLECTION, realm)
191        .await
192        .or_else(|err: fcomponent::Error| match err {
193            // Since the intent is simply to clear out the existing session child if it exists,
194            // related errors are disregarded.
195            fcomponent::Error::InvalidArguments
196            | fcomponent::Error::InstanceNotFound
197            | fcomponent::Error::CollectionNotFound => Ok(()),
198            _ => Err(err),
199        })
200        .map_err(|err| StartupError::NotDestroyed {
201            name: SESSION_NAME.to_string(),
202            collection: SESSION_CHILD_COLLECTION.to_string(),
203            err,
204        })?;
205
206    let (controller, controller_server_end) = create_proxy::<fcomponent::ControllerMarker>();
207    let dictionary = create_config_dict(config_capabilities).await.map_err(|err| {
208        StartupError::BedrockError {
209            name: SESSION_NAME.to_string(),
210            collection: SESSION_CHILD_COLLECTION.to_string(),
211            url: session_url.to_string(),
212            err: format!("{err:#?}"),
213        }
214    })?;
215    let create_child_args = fcomponent::CreateChildArgs {
216        controller: Some(controller_server_end),
217        additional_inputs: dictionary.map(|dictionary| dictionary.handle),
218        ..Default::default()
219    };
220    realm_management::create_child_component(
221        SESSION_NAME,
222        session_url,
223        SESSION_CHILD_COLLECTION,
224        create_child_args,
225        realm,
226    )
227    .await
228    .map_err(|err| StartupError::NotCreated {
229        name: SESSION_NAME.to_string(),
230        collection: SESSION_CHILD_COLLECTION.to_string(),
231        url: session_url.to_string(),
232        err,
233    })?;
234
235    realm_management::open_child_component_exposed_dir(
236        SESSION_NAME,
237        SESSION_CHILD_COLLECTION,
238        realm,
239        exposed_dir,
240    )
241    .await
242    .map_err(|err| StartupError::ExposedDirNotOpened {
243        name: SESSION_NAME.to_string(),
244        collection: SESSION_CHILD_COLLECTION.to_string(),
245        url: session_url.to_string(),
246        err,
247    })?;
248
249    // Start the component.
250    let (execution_controller, execution_controller_server_end) =
251        create_proxy::<fcomponent::ExecutionControllerMarker>();
252    controller
253        .start(fcomponent::StartChildArgs::default(), execution_controller_server_end)
254        .await
255        .map_err(|_| fcomponent::Error::Internal)
256        .and_then(std::convert::identity)
257        .map_err(|_err| StartupError::NotLaunched {
258            name: SESSION_NAME.to_string(),
259            collection: SESSION_CHILD_COLLECTION.to_string(),
260            url: session_url.to_string(),
261            err: fcomponent::Error::InstanceCannotStart,
262        })?;
263
264    Ok(execution_controller)
265}
266
267#[cfg(test)]
268#[allow(clippy::unwrap_used)]
269mod tests {
270    use super::{SESSION_CHILD_COLLECTION, SESSION_NAME, set_session, stop_session};
271    use anyhow::Error;
272    use fidl::endpoints::create_endpoints;
273    use fidl_test_util::spawn_stream_handler;
274    use session_testing::{spawn_directory_server, spawn_server};
275    use std::sync::LazyLock;
276    use test_util::Counter;
277    use {fidl_fuchsia_component as fcomponent, fidl_fuchsia_io as fio};
278
279    #[fuchsia::test]
280    async fn set_session_calls_realm_methods_in_appropriate_order() -> Result<(), Error> {
281        // The number of realm calls which have been made so far.
282        static NUM_REALM_REQUESTS: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
283
284        let session_url = "session";
285
286        let directory_request_handler = move |directory_request| match directory_request {
287            fio::DirectoryRequest::Open { path: _, .. } => {
288                assert_eq!(NUM_REALM_REQUESTS.get(), 4);
289            }
290            _ => panic!("Directory handler received an unexpected request"),
291        };
292
293        let realm = spawn_stream_handler(move |realm_request| async move {
294            match realm_request {
295                fcomponent::RealmRequest::DestroyChild { child, responder } => {
296                    assert_eq!(NUM_REALM_REQUESTS.get(), 0);
297                    assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string()));
298                    assert_eq!(child.name, SESSION_NAME);
299
300                    let _ = responder.send(Ok(()));
301                }
302                fcomponent::RealmRequest::CreateChild { collection, decl, args, responder } => {
303                    assert_eq!(NUM_REALM_REQUESTS.get(), 1);
304                    assert_eq!(decl.url.unwrap(), session_url);
305                    assert_eq!(decl.name.unwrap(), SESSION_NAME);
306                    assert_eq!(&collection.name, SESSION_CHILD_COLLECTION);
307
308                    spawn_server(args.controller.unwrap(), move |controller_request| {
309                        match controller_request {
310                            fcomponent::ControllerRequest::Start { responder, .. } => {
311                                let _ = responder.send(Ok(()));
312                            }
313                            fcomponent::ControllerRequest::IsStarted { .. } => unimplemented!(),
314                            fcomponent::ControllerRequest::GetExposedDictionary { .. } => {
315                                unimplemented!()
316                            }
317                            fcomponent::ControllerRequest::GetOutputDictionary { .. } => {
318                                unimplemented!()
319                            }
320                            fcomponent::ControllerRequest::OpenExposedDir { .. } => {
321                                unimplemented!()
322                            }
323                            fcomponent::ControllerRequest::Destroy { .. } => {
324                                unimplemented!()
325                            }
326                            fcomponent::ControllerRequest::_UnknownMethod { .. } => {
327                                unimplemented!()
328                            }
329                        }
330                    });
331
332                    let _ = responder.send(Ok(()));
333                }
334                fcomponent::RealmRequest::OpenExposedDir { child, exposed_dir, responder } => {
335                    assert_eq!(NUM_REALM_REQUESTS.get(), 2);
336                    assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string()));
337                    assert_eq!(child.name, SESSION_NAME);
338
339                    spawn_directory_server(exposed_dir, directory_request_handler);
340                    let _ = responder.send(Ok(()));
341                }
342                _ => panic!("Realm handler received an unexpected request"),
343            }
344            NUM_REALM_REQUESTS.inc();
345        });
346
347        let (_exposed_dir, exposed_dir_server_end) = create_endpoints::<fio::DirectoryMarker>();
348        let _controller = set_session(session_url, vec![], &realm, exposed_dir_server_end).await?;
349
350        Ok(())
351    }
352
353    #[fuchsia::test]
354    async fn set_session_starts_component() -> Result<(), Error> {
355        static NUM_START_CALLS: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
356
357        let session_url = "session";
358
359        let realm = spawn_stream_handler(move |realm_request| async move {
360            match realm_request {
361                fcomponent::RealmRequest::DestroyChild { responder, .. } => {
362                    let _ = responder.send(Ok(()));
363                }
364                fcomponent::RealmRequest::CreateChild { args, responder, .. } => {
365                    spawn_server(args.controller.unwrap(), move |controller_request| {
366                        match controller_request {
367                            fcomponent::ControllerRequest::Start { responder, .. } => {
368                                NUM_START_CALLS.inc();
369                                let _ = responder.send(Ok(()));
370                            }
371                            fcomponent::ControllerRequest::IsStarted { .. } => unimplemented!(),
372                            fcomponent::ControllerRequest::GetExposedDictionary { .. } => {
373                                unimplemented!()
374                            }
375                            fcomponent::ControllerRequest::GetOutputDictionary { .. } => {
376                                unimplemented!()
377                            }
378                            fcomponent::ControllerRequest::OpenExposedDir { .. } => {
379                                unimplemented!()
380                            }
381                            fcomponent::ControllerRequest::Destroy { .. } => {
382                                unimplemented!()
383                            }
384                            fcomponent::ControllerRequest::_UnknownMethod { .. } => {
385                                unimplemented!()
386                            }
387                        }
388                    });
389                    let _ = responder.send(Ok(()));
390                }
391                fcomponent::RealmRequest::OpenExposedDir { responder, .. } => {
392                    let _ = responder.send(Ok(()));
393                }
394                _ => panic!("Realm handler received an unexpected request"),
395            }
396        });
397
398        let (_exposed_dir, exposed_dir_server_end) = create_endpoints::<fio::DirectoryMarker>();
399        let _controller = set_session(session_url, vec![], &realm, exposed_dir_server_end).await?;
400        assert_eq!(NUM_START_CALLS.get(), 1);
401
402        Ok(())
403    }
404
405    #[fuchsia::test]
406    async fn stop_session_calls_destroy_child() -> Result<(), Error> {
407        static NUM_DESTROY_CHILD_CALLS: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
408
409        let realm = spawn_stream_handler(move |realm_request| async move {
410            match realm_request {
411                fcomponent::RealmRequest::DestroyChild { child, responder } => {
412                    assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 0);
413                    assert_eq!(child.collection, Some(SESSION_CHILD_COLLECTION.to_string()));
414                    assert_eq!(child.name, SESSION_NAME);
415
416                    let _ = responder.send(Ok(()));
417                }
418                _ => panic!("Realm handler received an unexpected request"),
419            }
420            NUM_DESTROY_CHILD_CALLS.inc();
421        });
422
423        stop_session(&realm).await?;
424        assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 1);
425
426        Ok(())
427    }
428}