sl4f_lib/modular/
facade.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.
4use crate::modular::types::{
5    BasemgrResult, KillSessionResult, RestartSessionResult, StartBasemgrRequest,
6};
7use anyhow::{Error, format_err};
8use fuchsia_component::client::connect_to_protocol;
9use serde_json::{Value, from_value};
10use {
11    fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_session as fsession,
12    fidl_fuchsia_sys2 as fsys,
13};
14
15// The session runs as `./core/session-manager/session:session`. The parts are:
16const SESSION_PARENT_MONIKER: &str = "./core/session-manager";
17const SESSION_COLLECTION_NAME: &str = "session";
18const SESSION_CHILD_NAME: &str = "session";
19const SESSION_MONIKER: &str = "./core/session-manager/session:session";
20
21/// Facade providing access to session testing interfaces.
22#[derive(Debug)]
23pub struct ModularFacade {
24    session_launcher: fsession::LauncherProxy,
25    session_restarter: fsession::RestarterProxy,
26    lifecycle_controller: fsys::LifecycleControllerProxy,
27    realm_query: fsys::RealmQueryProxy,
28}
29
30impl ModularFacade {
31    pub fn new() -> ModularFacade {
32        let session_launcher = connect_to_protocol::<fsession::LauncherMarker>()
33            .expect("failed to connect to fuchsia.session.Launcher");
34        let session_restarter = connect_to_protocol::<fsession::RestarterMarker>()
35            .expect("failed to connect to fuchsia.session.Restarter");
36        let lifecycle_controller = connect_to_protocol::<fsys::LifecycleControllerMarker>()
37            .expect("failed to connect to fuchsia.sys2.LifecycleController");
38        let realm_query =
39            fuchsia_component::client::connect_to_protocol::<fsys::RealmQueryMarker>()
40                .expect("failed to connect to fuchsia.sys2.RealmQuery");
41        ModularFacade { session_launcher, session_restarter, lifecycle_controller, realm_query }
42    }
43
44    pub fn new_with_proxies(
45        session_launcher: fsession::LauncherProxy,
46        session_restarter: fsession::RestarterProxy,
47        lifecycle_controller: fsys::LifecycleControllerProxy,
48        realm_query: fsys::RealmQueryProxy,
49    ) -> ModularFacade {
50        ModularFacade { session_launcher, session_restarter, lifecycle_controller, realm_query }
51    }
52
53    /// Returns true if a session is currently running.
54    pub async fn is_session_running(&self) -> Result<bool, Error> {
55        Ok(self
56            .realm_query
57            .get_instance(SESSION_MONIKER)
58            .await?
59            .ok()
60            .and_then(|instance| instance.resolved_info)
61            .and_then(|info| info.execution_info)
62            .is_some())
63    }
64
65    /// Restarts the currently running session.
66    pub async fn restart_session(&self) -> Result<RestartSessionResult, Error> {
67        if self.session_restarter.restart().await?.is_err() {
68            return Ok(RestartSessionResult::Fail);
69        }
70        Ok(RestartSessionResult::Success)
71    }
72
73    /// Facade to stop the session.
74    pub async fn stop_session(&self) -> Result<KillSessionResult, Error> {
75        if !self.is_session_running().await? {
76            return Ok(KillSessionResult::NoSessionRunning);
77        }
78
79        // Use a root LifecycleController to kill the session. It will send a shutdown signal to the
80        // session so it can terminate gracefully.
81        self.lifecycle_controller
82            .destroy_instance(
83                SESSION_PARENT_MONIKER,
84                &fdecl::ChildRef {
85                    name: SESSION_CHILD_NAME.to_string(),
86                    collection: Some(SESSION_COLLECTION_NAME.to_string()),
87                },
88            )
89            .await?
90            .map_err(|err| format_err!("failed to destroy session: {:?}", err))?;
91
92        Ok(KillSessionResult::Success)
93    }
94
95    /// Starts a session component.
96    ///
97    /// If a session is already running, it will be stopped first.
98    ///
99    /// `session_url` is required.
100    ///
101    /// # Arguments
102    /// * `args`: A serde_json Value parsed into [`StartBasemgrRequest`]
103    pub async fn start_session(&self, args: Value) -> Result<BasemgrResult, Error> {
104        let req: StartBasemgrRequest = from_value(args)?;
105
106        // If a session is running, stop it before starting a new one.
107        if self.is_session_running().await? {
108            self.stop_session().await?;
109        }
110
111        self.launch_session(&req.session_url).await?;
112
113        Ok(BasemgrResult::Success)
114    }
115
116    /// Launches a session.
117    ///
118    /// # Arguments
119    /// * `session_url`: Component URL for the session to launch.
120    async fn launch_session(&self, session_url: &str) -> Result<(), Error> {
121        let config = fsession::LaunchConfiguration {
122            session_url: Some(session_url.to_string()),
123            ..Default::default()
124        };
125        self.session_launcher
126            .launch(&config)
127            .await?
128            .map_err(|err| format_err!("failed to launch session: {:?}", err))
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use crate::modular::facade::{
135        ModularFacade, SESSION_CHILD_NAME, SESSION_COLLECTION_NAME, SESSION_MONIKER,
136        SESSION_PARENT_MONIKER,
137    };
138    use crate::modular::types::{BasemgrResult, KillSessionResult, RestartSessionResult};
139    use anyhow::Error;
140    use assert_matches::assert_matches;
141    use fidl_test_util::spawn_stream_handler;
142    use futures::Future;
143    use serde_json::json;
144    use std::sync::LazyLock;
145    use test_util::Counter;
146    use {fidl_fuchsia_session as fsession, fidl_fuchsia_sys2 as fsys};
147
148    const TEST_SESSION_URL: &str = "fuchsia-pkg://fuchsia.com/test_session#meta/test_session.cm";
149
150    // This function sets up a facade with launcher, restarter, and lifecycle_controller mocks and
151    // provides counters to check how often each is called. To use it, pass in a function that
152    // operates on a facade, executing the operations of the test, and pass in the expected number
153    // of calls to launch, destroy, and restart.
154    async fn test_facade<Fut>(
155        run_facade_fns: impl Fn(ModularFacade) -> Fut,
156        is_session_running: bool,
157        expected_launch_count: usize,
158        expected_destroy_count: usize,
159        expected_restart_count: usize,
160    ) -> Result<(), Error>
161    where
162        Fut: Future<Output = Result<(), Error>>,
163    {
164        static SESSION_LAUNCH_CALL_COUNT: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
165        static DESTROY_CHILD_CALL_COUNT: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
166        static SESSION_RESTART_CALL_COUNT: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
167
168        let session_launcher = spawn_stream_handler(move |launcher_request| async move {
169            match launcher_request {
170                fsession::LauncherRequest::Launch { configuration, responder } => {
171                    assert!(configuration.session_url.is_some());
172                    let session_url = configuration.session_url.unwrap();
173                    assert!(session_url == TEST_SESSION_URL.to_string());
174
175                    SESSION_LAUNCH_CALL_COUNT.inc();
176                    let _ = responder.send(Ok(()));
177                }
178            }
179        });
180
181        let session_restarter = spawn_stream_handler(move |restarter_request| async move {
182            match restarter_request {
183                fsession::RestarterRequest::Restart { responder } => {
184                    SESSION_RESTART_CALL_COUNT.inc();
185                    let _ = responder.send(Ok(()));
186                }
187            }
188        });
189
190        let lifecycle_controller = spawn_stream_handler(|lifecycle_controller_request| async {
191            match lifecycle_controller_request {
192                fsys::LifecycleControllerRequest::DestroyInstance {
193                    parent_moniker,
194                    child,
195                    responder,
196                } => {
197                    assert_eq!(parent_moniker, SESSION_PARENT_MONIKER.to_string());
198                    assert_eq!(child.name, SESSION_CHILD_NAME);
199                    assert_eq!(child.collection.unwrap(), SESSION_COLLECTION_NAME);
200
201                    DESTROY_CHILD_CALL_COUNT.inc();
202                    let _ = responder.send(Ok(()));
203                }
204                r => {
205                    panic!("didn't expect request: {:?}", r)
206                }
207            }
208        });
209
210        let realm_query = spawn_stream_handler(move |realm_query_request| async move {
211            match realm_query_request {
212                fsys::RealmQueryRequest::GetInstance { moniker, responder } => {
213                    assert_eq!(moniker, SESSION_MONIKER.to_string());
214                    let instance = if is_session_running {
215                        fsys::Instance {
216                            moniker: Some(SESSION_MONIKER.to_string()),
217                            url: Some("fake".to_string()),
218                            instance_id: None,
219                            resolved_info: Some(fsys::ResolvedInfo {
220                                resolved_url: Some("fake".to_string()),
221                                execution_info: Some(fsys::ExecutionInfo {
222                                    start_reason: Some("fake".to_string()),
223                                    ..Default::default()
224                                }),
225                                ..Default::default()
226                            }),
227                            ..Default::default()
228                        }
229                    } else {
230                        fsys::Instance {
231                            moniker: Some(SESSION_MONIKER.to_string()),
232                            url: Some("fake".to_string()),
233                            instance_id: None,
234                            resolved_info: None,
235                            ..Default::default()
236                        }
237                    };
238                    let _ = responder.send(Ok(&instance));
239                }
240                r => {
241                    panic!("didn't expect request: {:?}", r)
242                }
243            }
244        });
245
246        let facade = ModularFacade::new_with_proxies(
247            session_launcher,
248            session_restarter,
249            lifecycle_controller,
250            realm_query,
251        );
252
253        run_facade_fns(facade).await?;
254
255        assert_eq!(
256            SESSION_LAUNCH_CALL_COUNT.get(),
257            expected_launch_count,
258            "SESSION_LAUNCH_CALL_COUNT"
259        );
260        assert_eq!(
261            DESTROY_CHILD_CALL_COUNT.get(),
262            expected_destroy_count,
263            "DESTROY_CHILD_CALL_COUNT"
264        );
265        assert_eq!(
266            SESSION_RESTART_CALL_COUNT.get(),
267            expected_restart_count,
268            "SESSION_RESTART_CALL_COUNT"
269        );
270
271        Ok(())
272    }
273
274    #[fuchsia_async::run(2, test)]
275    async fn test_stop_session() -> Result<(), Error> {
276        async fn stop_session_steps(facade: ModularFacade) -> Result<(), Error> {
277            assert_matches!(facade.is_session_running().await, Ok(true));
278            assert_matches!(facade.stop_session().await, Ok(KillSessionResult::Success));
279            Ok(())
280        }
281
282        test_facade(
283            &stop_session_steps,
284            true, // is_session_running
285            0,    // SESSION_LAUNCH_CALL_COUNT
286            1,    // DESTROY_CHILD_CALL_COUNT
287            0,    // SESSION_RESTART_CALL_COUNT
288        )
289        .await
290    }
291
292    #[fuchsia_async::run(2, test)]
293    async fn test_start_session_without_config() -> Result<(), Error> {
294        async fn start_session_v2_steps(facade: ModularFacade) -> Result<(), Error> {
295            let start_session_args = json!({
296                "session_url": TEST_SESSION_URL,
297            });
298            assert_matches!(
299                facade.start_session(start_session_args).await,
300                Ok(BasemgrResult::Success)
301            );
302            Ok(())
303        }
304
305        test_facade(
306            &start_session_v2_steps,
307            false, // is_session_running
308            1,     // SESSION_LAUNCH_CALL_COUNT
309            0,     // DESTROY_CHILD_CALL_COUNT
310            0,     // SESSION_RESTART_CALL_COUNT
311        )
312        .await
313    }
314
315    #[fuchsia_async::run(2, test)]
316    async fn test_start_session_shutdown_existing() -> Result<(), Error> {
317        async fn start_existing_steps(facade: ModularFacade) -> Result<(), Error> {
318            let start_session_args = json!({
319                "session_url": TEST_SESSION_URL,
320            });
321
322            assert_matches!(facade.is_session_running().await, Ok(true));
323            // start_session() will notice that there's an existing session, destroy it, then launch
324            // the new session.
325            assert_matches!(
326                facade.start_session(start_session_args).await,
327                Ok(BasemgrResult::Success)
328            );
329            Ok(())
330        }
331
332        test_facade(
333            &start_existing_steps,
334            true, // is_session_running
335            1,    // SESSION_LAUNCH_CALL_COUNT
336            1,    // DESTROY_CHILD_CALL_COUNT
337            0,    // SESSION_RESTART_CALL_COUNT
338        )
339        .await
340    }
341
342    #[fuchsia_async::run_singlethreaded(test)]
343    async fn test_is_session_running_not_running() -> Result<(), Error> {
344        async fn not_running_steps(facade: ModularFacade) -> Result<(), Error> {
345            assert_matches!(facade.is_session_running().await, Ok(false));
346
347            Ok(())
348        }
349
350        test_facade(
351            &not_running_steps,
352            false, // is_session_running
353            0,     // SESSION_LAUNCH_CALL_COUNT
354            0,     // DESTROY_CHILD_CALL_COUNT
355            0,     // SESSION_RESTART_CALL_COUNT
356        )
357        .await
358    }
359
360    #[fuchsia_async::run(2, test)]
361    async fn test_is_session_running() -> Result<(), Error> {
362        async fn is_running_steps(facade: ModularFacade) -> Result<(), Error> {
363            assert_matches!(facade.is_session_running().await, Ok(true));
364
365            Ok(())
366        }
367
368        test_facade(
369            &is_running_steps,
370            true, // is_session_running
371            0,    // SESSION_LAUNCH_CALL_COUNT
372            0,    // DESTROY_CHILD_CALL_COUNT
373            0,    // SESSION_RESTART_CALL_COUNT
374        )
375        .await
376    }
377
378    #[fuchsia_async::run_singlethreaded(test)]
379    async fn test_restart_session() -> Result<(), Error> {
380        async fn restart_steps(facade: ModularFacade) -> Result<(), Error> {
381            assert_matches!(facade.restart_session().await, Ok(RestartSessionResult::Success));
382
383            Ok(())
384        }
385
386        test_facade(
387            &restart_steps,
388            true, // is_session_running
389            0,    // SESSION_LAUNCH_CALL_COUNT
390            0,    // DESTROY_CHILD_CALL_COUNT
391            1,    // SESSION_RESTART_CALL_COUNT
392        )
393        .await
394    }
395
396    #[fuchsia_async::run(2, test)]
397    async fn test_restart_session_does_not_destroy() -> Result<(), Error> {
398        async fn restart_steps(facade: ModularFacade) -> Result<(), Error> {
399            assert_matches!(facade.is_session_running().await, Ok(true));
400
401            assert_matches!(facade.restart_session().await, Ok(RestartSessionResult::Success));
402
403            Ok(())
404        }
405
406        test_facade(
407            &restart_steps,
408            true, // is_session_running
409            0,    // SESSION_LAUNCH_CALL_COUNT
410            0,    // DESTROY_CHILD_CALL_COUNT
411            1,    // SESSION_RESTART_CALL_COUNT
412        )
413        .await
414    }
415}