Skip to main content

session_manager_lib/
session_manager.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::{power, startup};
6use anyhow::{Context as _, Error, anyhow};
7use fidl::endpoints::{ClientEnd, ServerEnd, create_proxy};
8use fidl_fuchsia_component as fcomponent;
9use fidl_fuchsia_component_decl as fdecl;
10use fidl_fuchsia_io as fio;
11use fidl_fuchsia_power_broker as fbroker;
12use fidl_fuchsia_session as fsession;
13use fidl_fuchsia_session_power as fpower;
14use fuchsia_component::server::{ServiceFs, ServiceObjLocal};
15use fuchsia_inspect_contrib::nodes::BoundedListNode;
16use fuchsia_sync::Mutex;
17use futures::{StreamExt, TryFutureExt, TryStreamExt};
18use log::{error, warn};
19use std::sync::Arc;
20
21/// Maximum number of concurrent connections to the protocols served by `SessionManager`.
22const MAX_CONCURRENT_CONNECTIONS: usize = 10_000;
23
24/// The name for the inspect node that tracks session restart timestamps.
25const DIAGNOSTICS_SESSION_STARTED_AT_NAME: &str = "session_started_at";
26
27/// The max size for the session restart timestamps list.
28const DIAGNOSTICS_SESSION_STARTED_AT_SIZE: usize = 100;
29
30/// The name of the property for each entry in the `session_started_at` list for
31/// the start timestamp.
32const DIAGNOSTICS_TIME_PROPERTY_NAME: &str = "@time";
33
34/// A request to connect to a protocol exposed by `SessionManager`.
35pub enum IncomingRequest {
36    Launcher(fsession::LauncherRequestStream),
37    Restarter(fsession::RestarterRequestStream),
38    Lifecycle(fsession::LifecycleRequestStream),
39    Handoff(fpower::HandoffRequestStream),
40}
41
42struct Diagnostics {
43    /// A list of session start/restart timestamps.
44    session_started_at: BoundedListNode,
45}
46
47impl Diagnostics {
48    pub fn record_session_start(&mut self) {
49        self.session_started_at.add_entry(|node| {
50            node.record_int(
51                DIAGNOSTICS_TIME_PROPERTY_NAME,
52                zx::MonotonicInstant::get().into_nanos(),
53            );
54        });
55    }
56}
57
58/// State for a session that will be started in the future.
59struct PendingSession {
60    /// The server end on which the session's exposed directory will be served.
61    ///
62    /// This is the other end of `exposed_dir`.
63    pub exposed_dir_server_end: ServerEnd<fio::DirectoryMarker>,
64}
65
66impl PendingSession {
67    fn new() -> (fio::DirectoryProxy, Self) {
68        let (exposed_dir, exposed_dir_server_end) = create_proxy::<fio::DirectoryMarker>();
69        (exposed_dir, Self { exposed_dir_server_end })
70    }
71}
72
73/// State of a started session.
74///
75/// The component has been created and started, but is not guaranteed to be running since it
76/// may be stopped through external means.
77struct StartedSession {
78    /// The component URL of the session.
79    url: String,
80}
81
82enum Session {
83    Pending(PendingSession),
84    Started(StartedSession),
85}
86
87impl Session {
88    fn new_pending() -> (fio::DirectoryProxy, Self) {
89        let (proxy, pending_session) = PendingSession::new();
90        (proxy, Self::Pending(pending_session))
91    }
92}
93
94struct PowerState {
95    /// The power element corresponding to the session.
96    ///
97    /// The async mutex exists to serialize concurrent power lease operations, where
98    /// we need to take a lock over async FIDL calls.
99    power_element: futures::lock::Mutex<Option<power::PowerElement>>,
100
101    /// Whether the system supports suspending.
102    suspend_enabled: bool,
103}
104
105impl PowerState {
106    pub fn new(suspend_enabled: bool) -> Self {
107        Self { power_element: futures::lock::Mutex::default(), suspend_enabled }
108    }
109
110    /// Attempt to ensures that `session_manager` has a lease on the application activity element.
111    ///
112    /// This method is idempotent if it is a success.
113    pub async fn ensure_power_lease(&self) {
114        if !self.suspend_enabled {
115            return;
116        }
117        let power_element = &mut *self.power_element.lock().await;
118        if let Some(power_element) = power_element
119            && power_element.has_lease()
120        {
121            return;
122        }
123        *power_element = match power::PowerElement::new().await {
124            Ok(element) => Some(element),
125            Err(err) => {
126                warn!("Failed to create power element: {err}");
127                None
128            }
129        };
130    }
131
132    pub async fn take_power_lease(
133        &self,
134    ) -> Result<ClientEnd<fbroker::LeaseControlMarker>, fpower::HandoffError> {
135        if !self.suspend_enabled {
136            log::warn!(
137                "Session component wants to take our power lease, but the platform is \
138                configured to not support suspend"
139            );
140            return Err(fpower::HandoffError::Unavailable);
141        }
142        log::info!("Session component is taking our power lease");
143        let lease = match &mut *self.power_element.lock().await {
144            Some(power_element) => power_element.take_lease(),
145            None => return Err(fpower::HandoffError::Unavailable),
146        }
147        .ok_or(fpower::HandoffError::AlreadyTaken)?;
148        Ok(lease)
149    }
150}
151
152struct SessionManagerState {
153    /// The component URL for the default session.
154    default_session_url: Option<String>,
155
156    /// State of the session.
157    session: futures::lock::Mutex<Session>,
158
159    /// The realm in which session components will be created.
160    realm: fcomponent::RealmProxy,
161
162    /// Power-related state.
163    power: PowerState,
164
165    // Debug-related state.
166    debug: Arc<crate::debug::DebugManager>,
167
168    /// Other mutable state.
169    inner: Mutex<Inner>,
170}
171
172struct Inner {
173    /// Collection of diagnostics nodes.
174    diagnostics: Diagnostics,
175
176    /// The current directory proxy we should use.  When pending, requests are queued.
177    exposed_dir: fio::DirectoryProxy,
178}
179
180impl SessionManagerState {
181    /// Start the session with the default session component URL, if any.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if the is no default session URL or the session could not be launched.
186    async fn start_default(&self) -> Result<(), Error> {
187        let session_url = self
188            .default_session_url
189            .as_ref()
190            .ok_or_else(|| anyhow!("no default session URL configured"))?
191            .clone();
192        self.start(session_url, vec![]).await?;
193        Ok(())
194    }
195
196    /// Start a session, replacing any already session.
197    async fn start(
198        &self,
199        url: String,
200        config_capabilities: Vec<fdecl::Configuration>,
201    ) -> Result<(), startup::StartupError> {
202        self.power.ensure_power_lease().await;
203        self.start_impl(&mut *self.session.lock().await, config_capabilities, url).await
204    }
205
206    async fn start_impl(
207        &self,
208        session: &mut Session,
209        config_capabilities: Vec<fdecl::Configuration>,
210        url: String,
211    ) -> Result<(), startup::StartupError> {
212        let (proxy_on_failure, new_pending) = Session::new_pending();
213        let pending_session = std::mem::replace(session, new_pending);
214        let pending = match pending_session {
215            Session::Pending(pending) => pending,
216            Session::Started(_) => {
217                let (proxy, pending) = PendingSession::new();
218                self.inner.lock().exposed_dir = proxy;
219                pending
220            }
221        };
222        if let Err(e) = startup::launch_session(
223            &url,
224            config_capabilities,
225            pending.exposed_dir_server_end,
226            &self.realm,
227        )
228        .await
229        {
230            self.inner.lock().exposed_dir = proxy_on_failure;
231            return Err(e);
232        }
233        *session = Session::Started(StartedSession { url });
234        self.inner.lock().diagnostics.record_session_start();
235
236        self.debug.clone().start_media_buttons_listener();
237
238        Ok(())
239    }
240
241    /// Stops the session, if any.
242    async fn stop(&self) -> Result<(), startup::StartupError> {
243        self.power.ensure_power_lease().await;
244        self.debug.stop();
245        let mut session = self.session.lock().await;
246        if let Session::Started(_) = &*session {
247            let (proxy, new_pending) = Session::new_pending();
248            *session = new_pending;
249            self.inner.lock().exposed_dir = proxy;
250            startup::stop_session(&self.realm).await?;
251        }
252        Ok(())
253    }
254
255    /// Restarts a session.
256    async fn restart(&self) -> Result<(), startup::StartupError> {
257        self.power.ensure_power_lease().await;
258        let mut session = self.session.lock().await;
259        let Session::Started(StartedSession { url }) = &mut *session else {
260            return Err(startup::StartupError::NotRunning);
261        };
262        let url = url.clone();
263        self.start_impl(&mut session, vec![], url).await?;
264        Ok(())
265    }
266
267    async fn take_power_lease(
268        &self,
269    ) -> Result<ClientEnd<fbroker::LeaseControlMarker>, fpower::HandoffError> {
270        let lease = self.power.take_power_lease().await?;
271        Ok(lease)
272    }
273}
274
275impl vfs::remote::GetRemoteDir for SessionManagerState {
276    #[allow(clippy::unwrap_in_result)]
277    fn get_remote_dir(&self) -> Result<fio::DirectoryProxy, zx::Status> {
278        Ok(Clone::clone(&self.inner.lock().exposed_dir))
279    }
280}
281
282/// Manages the session lifecycle and provides services to control the session.
283#[derive(Clone)]
284pub struct SessionManager {
285    state: Arc<SessionManagerState>,
286}
287
288impl SessionManager {
289    /// Constructs a new `SessionManager`.
290    ///
291    /// # Parameters
292    /// - `realm`: The realm in which session components will be created.
293    pub fn new(
294        realm: fcomponent::RealmProxy,
295        inspector: &fuchsia_inspect::Inspector,
296        default_session_url: Option<String>,
297        suspend_enabled: bool,
298        enable_debug_shortcut: bool,
299    ) -> Self {
300        let session_started_at = BoundedListNode::new(
301            inspector.root().create_child(DIAGNOSTICS_SESSION_STARTED_AT_NAME),
302            DIAGNOSTICS_SESSION_STARTED_AT_SIZE,
303        );
304        let diagnostics = Diagnostics { session_started_at };
305        let (proxy, new_pending) = Session::new_pending();
306        let state = SessionManagerState {
307            default_session_url,
308            session: futures::lock::Mutex::new(new_pending),
309            realm,
310            power: PowerState::new(suspend_enabled),
311            debug: Arc::new(crate::debug::DebugState::new(enable_debug_shortcut)),
312            inner: Mutex::new(Inner { exposed_dir: proxy, diagnostics }),
313        };
314        SessionManager { state: Arc::new(state) }
315    }
316
317    #[cfg(test)]
318    pub fn new_default(
319        realm: fcomponent::RealmProxy,
320        inspector: &fuchsia_inspect::Inspector,
321    ) -> Self {
322        Self::new(realm, inspector, None, false, false)
323    }
324
325    /// Starts the session with the default session component URL, if any.
326    ///
327    /// # Errors
328    ///
329    /// Returns an error if the is no default session URL or the session could not be launched.
330    pub async fn start_default_session(&mut self) -> Result<(), Error> {
331        self.state.start_default().await?;
332        Ok(())
333    }
334
335    /// Starts serving [`IncomingRequest`] from `svc`.
336    ///
337    /// This will return once the [`ServiceFs`] stops serving requests.
338    ///
339    /// # Errors
340    /// Returns an error if there is an issue serving the `svc` directory handle.
341    pub async fn serve(
342        &mut self,
343        fs: &mut ServiceFs<ServiceObjLocal<'_, IncomingRequest>>,
344    ) -> Result<(), Error> {
345        fs.dir("svc")
346            .add_fidl_service(IncomingRequest::Launcher)
347            .add_fidl_service(IncomingRequest::Restarter)
348            .add_fidl_service(IncomingRequest::Lifecycle)
349            .add_fidl_service(IncomingRequest::Handoff);
350
351        // Requests to /svc_from_session are forwarded to the session's exposed dir.
352        fs.add_entry_at("svc_from_session", self.state.clone());
353
354        fs.take_and_serve_directory_handle()?;
355
356        fs.for_each_concurrent(MAX_CONCURRENT_CONNECTIONS, |request| {
357            let mut session_manager = self.clone();
358            async move {
359                session_manager
360                    .handle_incoming_request(request)
361                    .unwrap_or_else(|err| error!("{err:?}"))
362                    .await
363            }
364        })
365        .await;
366
367        Ok(())
368    }
369
370    /// Handles an [`IncomingRequest`].
371    ///
372    /// This will return once the protocol connection has been closed.
373    ///
374    /// # Errors
375    /// Returns an error if there is an issue serving the request.
376    async fn handle_incoming_request(&mut self, request: IncomingRequest) -> Result<(), Error> {
377        match request {
378            IncomingRequest::Launcher(request_stream) => {
379                self.handle_launcher_request_stream(request_stream)
380                    .await
381                    .context("Session Launcher request stream got an error.")?;
382            }
383            IncomingRequest::Restarter(request_stream) => {
384                self.handle_restarter_request_stream(request_stream)
385                    .await
386                    .context("Session Restarter request stream got an error.")?;
387            }
388            IncomingRequest::Lifecycle(request_stream) => {
389                self.handle_lifecycle_request_stream(request_stream)
390                    .await
391                    .context("Session Lifecycle request stream got an error.")?;
392            }
393            IncomingRequest::Handoff(request_stream) => {
394                self.handle_handoff_request_stream(request_stream)
395                    .await
396                    .context("Session Handoff request stream got an error.")?;
397            }
398        }
399
400        Ok(())
401    }
402
403    /// Serves a specified [`LauncherRequestStream`].
404    ///
405    /// # Parameters
406    /// - `request_stream`: the `LauncherRequestStream`.
407    ///
408    /// # Errors
409    /// When an error is encountered reading from the request stream.
410    pub async fn handle_launcher_request_stream(
411        &mut self,
412        mut request_stream: fsession::LauncherRequestStream,
413    ) -> Result<(), Error> {
414        while let Some(request) =
415            request_stream.try_next().await.context("Error handling Launcher request stream")?
416        {
417            match request {
418                fsession::LauncherRequest::Launch { configuration, responder } => {
419                    let result = self.handle_launch_request(configuration).await;
420                    let _ = responder.send(result);
421                }
422            }
423        }
424        Ok(())
425    }
426
427    /// Serves a specified [`RestarterRequestStream`].
428    ///
429    /// # Parameters
430    /// - `request_stream`: the `RestarterRequestStream`.
431    ///
432    /// # Errors
433    /// When an error is encountered reading from the request stream.
434    pub async fn handle_restarter_request_stream(
435        &mut self,
436        mut request_stream: fsession::RestarterRequestStream,
437    ) -> Result<(), Error> {
438        while let Some(request) =
439            request_stream.try_next().await.context("Error handling Restarter request stream")?
440        {
441            match request {
442                fsession::RestarterRequest::Restart { responder } => {
443                    let result = self.handle_restart_request().await;
444                    let _ = responder.send(result);
445                }
446            }
447        }
448        Ok(())
449    }
450
451    /// Serves a specified [`LifecycleRequestStream`].
452    ///
453    /// # Parameters
454    /// - `request_stream`: the `LifecycleRequestStream`.
455    ///
456    /// # Errors
457    /// When an error is encountered reading from the request stream.
458    pub async fn handle_lifecycle_request_stream(
459        &mut self,
460        mut request_stream: fsession::LifecycleRequestStream,
461    ) -> Result<(), Error> {
462        while let Some(request) =
463            request_stream.try_next().await.context("Error handling Lifecycle request stream")?
464        {
465            match request {
466                fsession::LifecycleRequest::Start { payload, responder } => {
467                    let result = self.handle_lifecycle_start_request(payload.session_url).await;
468                    let _ = responder.send(result);
469                }
470                fsession::LifecycleRequest::Stop { responder } => {
471                    let result = self.handle_lifecycle_stop_request().await;
472                    let _ = responder.send(result);
473                }
474                fsession::LifecycleRequest::Restart { responder } => {
475                    let result = self.handle_lifecycle_restart_request().await;
476                    let _ = responder.send(result);
477                }
478                fsession::LifecycleRequest::_UnknownMethod { ordinal, .. } => {
479                    warn!(ordinal:%; "Lifecycle received an unknown method");
480                }
481            }
482        }
483        Ok(())
484    }
485
486    pub async fn handle_handoff_request_stream(
487        &mut self,
488        mut request_stream: fpower::HandoffRequestStream,
489    ) -> Result<(), Error> {
490        while let Some(request) =
491            request_stream.try_next().await.context("Error handling Handoff request stream")?
492        {
493            match request {
494                fpower::HandoffRequest::Take { responder } => {
495                    let result = self.handle_handoff_take_request().await;
496                    let _ = responder.send(result.map(|lease| lease.into_channel().into_handle()));
497                }
498                fpower::HandoffRequest::_UnknownMethod { ordinal, .. } => {
499                    warn!(ordinal:%; "Lifecycle received an unknown method")
500                }
501            }
502        }
503        Ok(())
504    }
505
506    /// Handles calls to `Launcher.Launch()`.
507    ///
508    /// # Parameters
509    /// - configuration: The launch configuration for the new session.
510    async fn handle_launch_request(
511        &mut self,
512        configuration: fsession::LaunchConfiguration,
513    ) -> Result<(), fsession::LaunchError> {
514        let session_url = configuration.session_url.ok_or(fsession::LaunchError::InvalidArgs)?;
515        let config_capabilities = configuration.config_capabilities.unwrap_or_default();
516        self.state.start(session_url, config_capabilities).await.map_err(Into::into)
517    }
518
519    /// Handles a `Restarter.Restart()` request.
520    async fn handle_restart_request(&mut self) -> Result<(), fsession::RestartError> {
521        self.state.restart().await.map_err(Into::into)
522    }
523
524    /// Handles a `Lifecycle.Start()` request.
525    ///
526    /// # Parameters
527    /// - `session_url`: The component URL for the session to start.
528    async fn handle_lifecycle_start_request(
529        &mut self,
530        session_url: Option<String>,
531    ) -> Result<(), fsession::LifecycleError> {
532        let session_url = session_url
533            .as_ref()
534            .or(self.state.default_session_url.as_ref())
535            .ok_or(fsession::LifecycleError::NotFound)?
536            .to_owned();
537        self.state.start(session_url, vec![]).await.map_err(Into::into)
538    }
539
540    /// Handles a `Lifecycle.Stop()` request.
541    async fn handle_lifecycle_stop_request(&mut self) -> Result<(), fsession::LifecycleError> {
542        self.state.stop().await.map_err(Into::into)
543    }
544
545    /// Handles a `Lifecycle.Restart()` request.
546    async fn handle_lifecycle_restart_request(&mut self) -> Result<(), fsession::LifecycleError> {
547        self.state.restart().await.map_err(Into::into)
548    }
549
550    /// Handles a `Handoff.Take()` request.
551    async fn handle_handoff_take_request(
552        &mut self,
553    ) -> Result<ClientEnd<fbroker::LeaseControlMarker>, fpower::HandoffError> {
554        self.state.take_power_lease().await
555    }
556}
557
558#[cfg(test)]
559#[allow(clippy::unwrap_used)]
560mod tests {
561    use super::SessionManager;
562    use anyhow::{Error, anyhow};
563    use diagnostics_assertions::{AnyProperty, assert_data_tree};
564    use fidl::endpoints::{ServerEnd, create_proxy_and_stream};
565    use fidl_fuchsia_component as fcomponent;
566    use fidl_fuchsia_io as fio;
567    use fidl_fuchsia_session as fsession;
568    use fidl_test_util::spawn_stream_handler;
569    use futures::channel::mpsc;
570    use futures::prelude::*;
571    use session_testing::{spawn_directory_server, spawn_noop_directory_server, spawn_server};
572    use std::sync::LazyLock;
573    use test_util::Counter;
574
575    fn serve_launcher(session_manager: SessionManager) -> fsession::LauncherProxy {
576        let (launcher_proxy, launcher_stream) =
577            create_proxy_and_stream::<fsession::LauncherMarker>();
578        {
579            let mut session_manager_ = session_manager.clone();
580            fuchsia_async::Task::spawn(async move {
581                session_manager_
582                    .handle_launcher_request_stream(launcher_stream)
583                    .await
584                    .expect("Session launcher request stream got an error.");
585            })
586            .detach();
587        }
588        launcher_proxy
589    }
590
591    fn serve_restarter(session_manager: SessionManager) -> fsession::RestarterProxy {
592        let (restarter_proxy, restarter_stream) =
593            create_proxy_and_stream::<fsession::RestarterMarker>();
594        {
595            let mut session_manager_ = session_manager.clone();
596            fuchsia_async::Task::spawn(async move {
597                session_manager_
598                    .handle_restarter_request_stream(restarter_stream)
599                    .await
600                    .expect("Session restarter request stream got an error.");
601            })
602            .detach();
603        }
604        restarter_proxy
605    }
606
607    fn serve_lifecycle(session_manager: SessionManager) -> fsession::LifecycleProxy {
608        let (lifecycle_proxy, lifecycle_stream) =
609            create_proxy_and_stream::<fsession::LifecycleMarker>();
610        {
611            let mut session_manager_ = session_manager.clone();
612            fuchsia_async::Task::spawn(async move {
613                session_manager_
614                    .handle_lifecycle_request_stream(lifecycle_stream)
615                    .await
616                    .expect("Session lifecycle request stream got an error.");
617            })
618            .detach();
619        }
620        lifecycle_proxy
621    }
622
623    fn spawn_noop_controller_server(server_end: ServerEnd<fcomponent::ControllerMarker>) {
624        spawn_server(server_end, move |controller_request| match controller_request {
625            fcomponent::ControllerRequest::Start { responder, .. } => {
626                let _ = responder.send(Ok(()));
627            }
628            fcomponent::ControllerRequest::IsStarted { .. } => unimplemented!(),
629            fcomponent::ControllerRequest::GetExposedDictionary { .. } => {
630                unimplemented!()
631            }
632            fcomponent::ControllerRequest::GetOutputDictionary { .. } => {
633                unimplemented!()
634            }
635            fcomponent::ControllerRequest::OpenExposedDir { .. } => {
636                unimplemented!()
637            }
638            fcomponent::ControllerRequest::Destroy { .. } => {
639                unimplemented!()
640            }
641            fcomponent::ControllerRequest::_UnknownMethod { .. } => {
642                unimplemented!()
643            }
644        });
645    }
646
647    fn open_session_exposed_dir(
648        session_manager: SessionManager,
649        path: &str,
650        server_end: ServerEnd<fio::DirectoryMarker>,
651    ) {
652        session_manager
653            .state
654            .inner
655            .lock()
656            .exposed_dir
657            .open(path, fio::PERM_READABLE, &fio::Options::default(), server_end.into_channel())
658            .unwrap();
659    }
660
661    /// Verifies that Launcher.Launch creates a new session.
662    #[fuchsia::test]
663    async fn test_launch() {
664        let session_url = "session";
665
666        let realm = spawn_stream_handler(move |realm_request| async move {
667            match realm_request {
668                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
669                    let _ = responder.send(Ok(()));
670                }
671                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
672                    assert_eq!(decl.url.unwrap(), session_url);
673                    spawn_noop_controller_server(args.controller.unwrap());
674                    let _ = responder.send(Ok(()));
675                }
676                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
677                    spawn_noop_directory_server(exposed_dir);
678                    let _ = responder.send(Ok(()));
679                }
680                _ => panic!("Realm handler received an unexpected request"),
681            }
682        });
683
684        let inspector = fuchsia_inspect::Inspector::default();
685        let session_manager = SessionManager::new_default(realm, &inspector);
686        let launcher = serve_launcher(session_manager);
687
688        assert!(
689            launcher
690                .launch(&fsession::LaunchConfiguration {
691                    session_url: Some(session_url.to_string()),
692                    ..Default::default()
693                })
694                .await
695                .is_ok()
696        );
697        assert_data_tree!(inspector, root: {
698            session_started_at: {
699                "0": {
700                    "@time": AnyProperty
701                }
702            }
703        });
704    }
705
706    /// Verifies that Restarter.Restart restarts an existing session.
707    #[fuchsia::test]
708    async fn test_restarter_restart() {
709        let session_url = "session";
710
711        let realm = spawn_stream_handler(move |realm_request| async move {
712            match realm_request {
713                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
714                    let _ = responder.send(Ok(()));
715                }
716                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
717                    assert_eq!(decl.url.unwrap(), session_url);
718                    spawn_noop_controller_server(args.controller.unwrap());
719                    let _ = responder.send(Ok(()));
720                }
721                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
722                    spawn_noop_directory_server(exposed_dir);
723                    let _ = responder.send(Ok(()));
724                }
725                _ => panic!("Realm handler received an unexpected request"),
726            }
727        });
728
729        let inspector = fuchsia_inspect::Inspector::default();
730        let session_manager = SessionManager::new_default(realm, &inspector);
731        let launcher = serve_launcher(session_manager.clone());
732        let restarter = serve_restarter(session_manager);
733
734        assert!(
735            launcher
736                .launch(&fsession::LaunchConfiguration {
737                    session_url: Some(session_url.to_string()),
738                    ..Default::default()
739                })
740                .await
741                .expect("could not call Launch")
742                .is_ok()
743        );
744
745        assert!(restarter.restart().await.expect("could not call Restart").is_ok());
746
747        assert_data_tree!(inspector, root: {
748            session_started_at: {
749                "0": {
750                    "@time": AnyProperty
751                },
752                "1": {
753                    "@time": AnyProperty
754                }
755            }
756        });
757    }
758
759    /// Verifies that Launcher.Restart return an error if there is no running existing session.
760    #[fuchsia::test]
761    async fn test_restarter_restart_error_not_running() {
762        let realm = spawn_stream_handler(move |_realm_request| async move {
763            panic!("Realm should not receive any requests as there is no session to launch")
764        });
765
766        let inspector = fuchsia_inspect::Inspector::default();
767        let session_manager = SessionManager::new_default(realm, &inspector);
768        let restarter = serve_restarter(session_manager);
769
770        assert_eq!(
771            Err(fsession::RestartError::NotRunning),
772            restarter.restart().await.expect("could not call Restart")
773        );
774
775        assert_data_tree!(inspector, root: {
776            session_started_at: {}
777        });
778    }
779
780    /// Verifies that Lifecycle.Start creates a new session.
781    #[fuchsia::test]
782    async fn test_start() {
783        let session_url = "session";
784
785        let realm = spawn_stream_handler(move |realm_request| async move {
786            match realm_request {
787                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
788                    let _ = responder.send(Ok(()));
789                }
790                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
791                    assert_eq!(decl.url.unwrap(), session_url);
792                    spawn_noop_controller_server(args.controller.unwrap());
793                    let _ = responder.send(Ok(()));
794                }
795                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
796                    spawn_noop_directory_server(exposed_dir);
797                    let _ = responder.send(Ok(()));
798                }
799                _ => panic!("Realm handler received an unexpected request"),
800            }
801        });
802
803        let inspector = fuchsia_inspect::Inspector::default();
804        let session_manager = SessionManager::new_default(realm, &inspector);
805        let lifecycle = serve_lifecycle(session_manager);
806
807        assert!(
808            lifecycle
809                .start(&fsession::LifecycleStartRequest {
810                    session_url: Some(session_url.to_string()),
811                    ..Default::default()
812                })
813                .await
814                .is_ok()
815        );
816        assert_data_tree!(inspector, root: {
817            session_started_at: {
818                "0": {
819                    "@time": AnyProperty
820                }
821            }
822        });
823    }
824
825    /// Verifies that Lifecycle.Start starts the default session if no URL is provided.
826    #[fuchsia::test]
827    async fn test_start_default() {
828        let default_session_url = "session";
829
830        let realm = spawn_stream_handler(move |realm_request| async move {
831            match realm_request {
832                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
833                    let _ = responder.send(Ok(()));
834                }
835                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
836                    assert_eq!(decl.url.unwrap(), default_session_url);
837                    spawn_noop_controller_server(args.controller.unwrap());
838                    let _ = responder.send(Ok(()));
839                }
840                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
841                    spawn_noop_directory_server(exposed_dir);
842                    let _ = responder.send(Ok(()));
843                }
844                _ => panic!("Realm handler received an unexpected request"),
845            }
846        });
847
848        let inspector = fuchsia_inspect::Inspector::default();
849        let session_manager = SessionManager::new(
850            realm,
851            &inspector,
852            Some(default_session_url.to_owned()),
853            false,
854            false,
855        );
856        let lifecycle = serve_lifecycle(session_manager);
857
858        assert!(
859            lifecycle
860                .start(&fsession::LifecycleStartRequest { session_url: None, ..Default::default() })
861                .await
862                .is_ok()
863        );
864        assert_data_tree!(inspector, root: {
865            session_started_at: {
866                "0": {
867                    "@time": AnyProperty
868                }
869            }
870        });
871    }
872
873    /// Verifies that Lifecycle.Stop stops an existing session by destroying its component.
874    #[fuchsia::test]
875    async fn test_stop_destroys_component() {
876        static NUM_DESTROY_CHILD_CALLS: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
877
878        let session_url = "session";
879
880        let realm = spawn_stream_handler(move |realm_request| async move {
881            match realm_request {
882                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
883                    NUM_DESTROY_CHILD_CALLS.inc();
884                    let _ = responder.send(Ok(()));
885                }
886                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
887                    assert_eq!(decl.url.unwrap(), session_url);
888                    spawn_noop_controller_server(args.controller.unwrap());
889                    let _ = responder.send(Ok(()));
890                }
891                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
892                    spawn_noop_directory_server(exposed_dir);
893                    let _ = responder.send(Ok(()));
894                }
895                _ => panic!("Realm handler received an unexpected request"),
896            }
897        });
898
899        let inspector = fuchsia_inspect::Inspector::default();
900        let session_manager = SessionManager::new_default(realm, &inspector);
901        let lifecycle = serve_lifecycle(session_manager);
902
903        assert!(
904            lifecycle
905                .start(&fsession::LifecycleStartRequest {
906                    session_url: Some(session_url.to_string()),
907                    ..Default::default()
908                })
909                .await
910                .is_ok()
911        );
912        // Start attempts to destroy any existing session first.
913        assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 1);
914        assert_data_tree!(inspector, root: {
915            session_started_at: {
916                "0": {
917                    "@time": AnyProperty
918                }
919            }
920        });
921
922        assert!(lifecycle.stop().await.is_ok());
923        assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 2);
924    }
925
926    /// Verifies that Lifecycle.Restart restarts an existing session.
927    #[fuchsia::test]
928    async fn test_lifecycle_restart() {
929        let session_url = "session";
930
931        let realm = spawn_stream_handler(move |realm_request| async move {
932            match realm_request {
933                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
934                    let _ = responder.send(Ok(()));
935                }
936                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
937                    assert_eq!(decl.url.unwrap(), session_url);
938                    spawn_noop_controller_server(args.controller.unwrap());
939                    let _ = responder.send(Ok(()));
940                }
941                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
942                    spawn_noop_directory_server(exposed_dir);
943                    let _ = responder.send(Ok(()));
944                }
945                _ => panic!("Realm handler received an unexpected request"),
946            }
947        });
948
949        let inspector = fuchsia_inspect::Inspector::default();
950        let session_manager = SessionManager::new_default(realm, &inspector);
951        let lifecycle = serve_lifecycle(session_manager.clone());
952
953        assert!(
954            lifecycle
955                .start(&fsession::LifecycleStartRequest {
956                    session_url: Some(session_url.to_string()),
957                    ..Default::default()
958                })
959                .await
960                .expect("could not call Launch")
961                .is_ok()
962        );
963
964        assert!(lifecycle.restart().await.expect("could not call Restart").is_ok());
965
966        assert_data_tree!(inspector, root: {
967            session_started_at: {
968                "0": {
969                    "@time": AnyProperty
970                },
971                "1": {
972                    "@time": AnyProperty
973                }
974            }
975        });
976    }
977
978    /// Verifies that a node can be opened in the session's exposed dir before the session is
979    /// started, and that it is connected once the session is started.
980    #[fuchsia::test]
981    async fn test_svc_from_session_before_start() -> Result<(), Error> {
982        let session_url = "session";
983        let svc_path = "foo";
984
985        let (path_sender, mut path_receiver) = mpsc::channel(1);
986
987        let session_exposed_dir_handler = move |directory_request| match directory_request {
988            fio::DirectoryRequest::Open { path, .. } => {
989                let mut path_sender: mpsc::Sender<String> = path_sender.clone();
990                path_sender.try_send(path).unwrap();
991            }
992            _ => panic!("Directory handler received an unexpected request"),
993        };
994
995        let realm = spawn_stream_handler(move |realm_request| {
996            let session_exposed_dir_handler = session_exposed_dir_handler.clone();
997            async move {
998                match realm_request {
999                    fcomponent::RealmRequest::DestroyChild { responder, .. } => {
1000                        let _ = responder.send(Ok(()));
1001                    }
1002                    fcomponent::RealmRequest::CreateChild { args, responder, .. } => {
1003                        spawn_noop_controller_server(args.controller.unwrap());
1004                        let _ = responder.send(Ok(()));
1005                    }
1006                    fcomponent::RealmRequest::OpenExposedDir { exposed_dir, responder, .. } => {
1007                        spawn_directory_server(exposed_dir, session_exposed_dir_handler);
1008                        let _ = responder.send(Ok(()));
1009                    }
1010                    _ => panic!("Realm handler received an unexpected request"),
1011                }
1012            }
1013        });
1014
1015        let inspector = fuchsia_inspect::Inspector::default();
1016        let session_manager = SessionManager::new_default(realm, &inspector);
1017        let lifecycle = serve_lifecycle(session_manager.clone());
1018
1019        // Open an arbitrary node in the session's exposed dir.
1020        // The actual protocol does not matter because it's not being served.
1021        let (_client_end, server_end) = fidl::endpoints::create_proxy();
1022
1023        open_session_exposed_dir(session_manager, svc_path, server_end);
1024        // Start the session.
1025        lifecycle
1026            .start(&fsession::LifecycleStartRequest {
1027                session_url: Some(session_url.to_string()),
1028                ..Default::default()
1029            })
1030            .await?
1031            .map_err(|err| anyhow!("failed to start: {err:?}"))?;
1032
1033        // The exposed dir should have received the Open request.
1034        assert_eq!(path_receiver.next().await.unwrap(), svc_path);
1035
1036        Ok(())
1037    }
1038
1039    /// Verifies that a node in the session's exposed dir can be opened after the session has
1040    /// started.
1041    #[fuchsia::test]
1042    async fn test_svc_from_session_after_start() -> Result<(), Error> {
1043        let session_url = "session";
1044        let svc_path = "foo";
1045
1046        let (path_sender, mut path_receiver) = mpsc::channel(1);
1047
1048        let session_exposed_dir_handler = move |directory_request| match directory_request {
1049            fio::DirectoryRequest::Open { path, .. } => {
1050                let mut path_sender = path_sender.clone();
1051                path_sender.try_send(path).unwrap();
1052            }
1053            _ => panic!("Directory handler received an unexpected request"),
1054        };
1055
1056        let realm = spawn_stream_handler(move |realm_request| {
1057            let session_exposed_dir_handler = session_exposed_dir_handler.clone();
1058            async move {
1059                match realm_request {
1060                    fcomponent::RealmRequest::DestroyChild { responder, .. } => {
1061                        let _ = responder.send(Ok(()));
1062                    }
1063                    fcomponent::RealmRequest::CreateChild { args, responder, .. } => {
1064                        spawn_noop_controller_server(args.controller.unwrap());
1065                        let _ = responder.send(Ok(()));
1066                    }
1067                    fcomponent::RealmRequest::OpenExposedDir { exposed_dir, responder, .. } => {
1068                        spawn_directory_server(exposed_dir, session_exposed_dir_handler);
1069                        let _ = responder.send(Ok(()));
1070                    }
1071                    _ => panic!("Realm handler received an unexpected request"),
1072                }
1073            }
1074        });
1075
1076        let inspector = fuchsia_inspect::Inspector::default();
1077        let session_manager = SessionManager::new_default(realm, &inspector);
1078        let lifecycle = serve_lifecycle(session_manager.clone());
1079
1080        lifecycle
1081            .start(&fsession::LifecycleStartRequest {
1082                session_url: Some(session_url.to_string()),
1083                ..Default::default()
1084            })
1085            .await?
1086            .map_err(|err| anyhow!("failed to start: {err:?}"))?;
1087
1088        // Open an arbitrary node in the session's exposed dir.
1089        // The actual protocol does not matter because it's not being served.
1090        let (_client_end, server_end) = fidl::endpoints::create_proxy();
1091
1092        open_session_exposed_dir(session_manager, svc_path, server_end);
1093
1094        assert_eq!(path_receiver.next().await.unwrap(), svc_path);
1095
1096        Ok(())
1097    }
1098
1099    /// Verifies that `DebugState` is reset when a session is stopped.
1100    #[fuchsia::test]
1101    async fn test_stop_resets_debug_state() {
1102        let session_url = "session";
1103
1104        let realm = spawn_stream_handler(move |realm_request| async move {
1105            match realm_request {
1106                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
1107                    let _ = responder.send(Ok(()));
1108                }
1109                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
1110                    assert_eq!(decl.url.unwrap(), session_url);
1111                    spawn_noop_controller_server(args.controller.unwrap());
1112                    let _ = responder.send(Ok(()));
1113                }
1114                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
1115                    spawn_noop_directory_server(exposed_dir);
1116                    let _ = responder.send(Ok(()));
1117                }
1118                _ => panic!("Realm handler received an unexpected request"),
1119            }
1120        });
1121
1122        let inspector = fuchsia_inspect::Inspector::default();
1123        let session_manager = SessionManager::new(realm, &inspector, None, false, true);
1124        let lifecycle = serve_lifecycle(session_manager.clone());
1125
1126        assert!(
1127            lifecycle
1128                .start(&fsession::LifecycleStartRequest {
1129                    session_url: Some(session_url.to_string()),
1130                    ..Default::default()
1131                })
1132                .await
1133                .is_ok()
1134        );
1135
1136        session_manager.state.debug.set_button_press_state_count(3);
1137        assert_eq!(session_manager.state.debug.get_button_press_state_count(), 3);
1138
1139        assert!(lifecycle.stop().await.is_ok());
1140
1141        assert_eq!(session_manager.state.debug.get_button_press_state_count(), 0);
1142    }
1143}