Skip to main content

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