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 fuchsia_component::server::{ServiceFs, ServiceObjLocal};
9use fuchsia_inspect_contrib::nodes::BoundedListNode;
10use fuchsia_sync::Mutex;
11use futures::{StreamExt, TryFutureExt, TryStreamExt};
12use log::{error, warn};
13use std::sync::Arc;
14use zx::HandleBased;
15use {
16    fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl,
17    fidl_fuchsia_io as fio, fidl_fuchsia_power_broker as fbroker, fidl_fuchsia_session as fsession,
18    fidl_fuchsia_session_power as fpower,
19};
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_test_util::spawn_stream_handler;
566    use futures::channel::mpsc;
567    use futures::prelude::*;
568    use session_testing::{spawn_directory_server, spawn_noop_directory_server, spawn_server};
569    use std::sync::LazyLock;
570    use test_util::Counter;
571    use {
572        fidl_fuchsia_component as fcomponent, fidl_fuchsia_io as fio,
573        fidl_fuchsia_session as fsession,
574    };
575
576    fn serve_launcher(session_manager: SessionManager) -> fsession::LauncherProxy {
577        let (launcher_proxy, launcher_stream) =
578            create_proxy_and_stream::<fsession::LauncherMarker>();
579        {
580            let mut session_manager_ = session_manager.clone();
581            fuchsia_async::Task::spawn(async move {
582                session_manager_
583                    .handle_launcher_request_stream(launcher_stream)
584                    .await
585                    .expect("Session launcher request stream got an error.");
586            })
587            .detach();
588        }
589        launcher_proxy
590    }
591
592    fn serve_restarter(session_manager: SessionManager) -> fsession::RestarterProxy {
593        let (restarter_proxy, restarter_stream) =
594            create_proxy_and_stream::<fsession::RestarterMarker>();
595        {
596            let mut session_manager_ = session_manager.clone();
597            fuchsia_async::Task::spawn(async move {
598                session_manager_
599                    .handle_restarter_request_stream(restarter_stream)
600                    .await
601                    .expect("Session restarter request stream got an error.");
602            })
603            .detach();
604        }
605        restarter_proxy
606    }
607
608    fn serve_lifecycle(session_manager: SessionManager) -> fsession::LifecycleProxy {
609        let (lifecycle_proxy, lifecycle_stream) =
610            create_proxy_and_stream::<fsession::LifecycleMarker>();
611        {
612            let mut session_manager_ = session_manager.clone();
613            fuchsia_async::Task::spawn(async move {
614                session_manager_
615                    .handle_lifecycle_request_stream(lifecycle_stream)
616                    .await
617                    .expect("Session lifecycle request stream got an error.");
618            })
619            .detach();
620        }
621        lifecycle_proxy
622    }
623
624    fn spawn_noop_controller_server(server_end: ServerEnd<fcomponent::ControllerMarker>) {
625        spawn_server(server_end, move |controller_request| match controller_request {
626            fcomponent::ControllerRequest::Start { responder, .. } => {
627                let _ = responder.send(Ok(()));
628            }
629            fcomponent::ControllerRequest::IsStarted { .. } => unimplemented!(),
630            fcomponent::ControllerRequest::GetExposedDictionary { .. } => {
631                unimplemented!()
632            }
633            fcomponent::ControllerRequest::GetOutputDictionary { .. } => {
634                unimplemented!()
635            }
636            fcomponent::ControllerRequest::OpenExposedDir { .. } => {
637                unimplemented!()
638            }
639            fcomponent::ControllerRequest::Destroy { .. } => {
640                unimplemented!()
641            }
642            fcomponent::ControllerRequest::_UnknownMethod { .. } => {
643                unimplemented!()
644            }
645        });
646    }
647
648    fn open_session_exposed_dir(
649        session_manager: SessionManager,
650        path: &str,
651        server_end: ServerEnd<fio::DirectoryMarker>,
652    ) {
653        session_manager
654            .state
655            .inner
656            .lock()
657            .exposed_dir
658            .open(path, fio::PERM_READABLE, &fio::Options::default(), server_end.into_channel())
659            .unwrap();
660    }
661
662    /// Verifies that Launcher.Launch creates a new session.
663    #[fuchsia::test]
664    async fn test_launch() {
665        let session_url = "session";
666
667        let realm = spawn_stream_handler(move |realm_request| async move {
668            match realm_request {
669                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
670                    let _ = responder.send(Ok(()));
671                }
672                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
673                    assert_eq!(decl.url.unwrap(), session_url);
674                    spawn_noop_controller_server(args.controller.unwrap());
675                    let _ = responder.send(Ok(()));
676                }
677                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
678                    spawn_noop_directory_server(exposed_dir);
679                    let _ = responder.send(Ok(()));
680                }
681                _ => panic!("Realm handler received an unexpected request"),
682            }
683        });
684
685        let inspector = fuchsia_inspect::Inspector::default();
686        let session_manager = SessionManager::new_default(realm, &inspector);
687        let launcher = serve_launcher(session_manager);
688
689        assert!(
690            launcher
691                .launch(&fsession::LaunchConfiguration {
692                    session_url: Some(session_url.to_string()),
693                    ..Default::default()
694                })
695                .await
696                .is_ok()
697        );
698        assert_data_tree!(inspector, root: {
699            session_started_at: {
700                "0": {
701                    "@time": AnyProperty
702                }
703            }
704        });
705    }
706
707    /// Verifies that Restarter.Restart restarts an existing session.
708    #[fuchsia::test]
709    async fn test_restarter_restart() {
710        let session_url = "session";
711
712        let realm = spawn_stream_handler(move |realm_request| async move {
713            match realm_request {
714                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
715                    let _ = responder.send(Ok(()));
716                }
717                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
718                    assert_eq!(decl.url.unwrap(), session_url);
719                    spawn_noop_controller_server(args.controller.unwrap());
720                    let _ = responder.send(Ok(()));
721                }
722                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
723                    spawn_noop_directory_server(exposed_dir);
724                    let _ = responder.send(Ok(()));
725                }
726                _ => panic!("Realm handler received an unexpected request"),
727            }
728        });
729
730        let inspector = fuchsia_inspect::Inspector::default();
731        let session_manager = SessionManager::new_default(realm, &inspector);
732        let launcher = serve_launcher(session_manager.clone());
733        let restarter = serve_restarter(session_manager);
734
735        assert!(
736            launcher
737                .launch(&fsession::LaunchConfiguration {
738                    session_url: Some(session_url.to_string()),
739                    ..Default::default()
740                })
741                .await
742                .expect("could not call Launch")
743                .is_ok()
744        );
745
746        assert!(restarter.restart().await.expect("could not call Restart").is_ok());
747
748        assert_data_tree!(inspector, root: {
749            session_started_at: {
750                "0": {
751                    "@time": AnyProperty
752                },
753                "1": {
754                    "@time": AnyProperty
755                }
756            }
757        });
758    }
759
760    /// Verifies that Launcher.Restart return an error if there is no running existing session.
761    #[fuchsia::test]
762    async fn test_restarter_restart_error_not_running() {
763        let realm = spawn_stream_handler(move |_realm_request| async move {
764            panic!("Realm should not receive any requests as there is no session to launch")
765        });
766
767        let inspector = fuchsia_inspect::Inspector::default();
768        let session_manager = SessionManager::new_default(realm, &inspector);
769        let restarter = serve_restarter(session_manager);
770
771        assert_eq!(
772            Err(fsession::RestartError::NotRunning),
773            restarter.restart().await.expect("could not call Restart")
774        );
775
776        assert_data_tree!(inspector, root: {
777            session_started_at: {}
778        });
779    }
780
781    /// Verifies that Lifecycle.Start creates a new session.
782    #[fuchsia::test]
783    async fn test_start() {
784        let session_url = "session";
785
786        let realm = spawn_stream_handler(move |realm_request| async move {
787            match realm_request {
788                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
789                    let _ = responder.send(Ok(()));
790                }
791                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
792                    assert_eq!(decl.url.unwrap(), session_url);
793                    spawn_noop_controller_server(args.controller.unwrap());
794                    let _ = responder.send(Ok(()));
795                }
796                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
797                    spawn_noop_directory_server(exposed_dir);
798                    let _ = responder.send(Ok(()));
799                }
800                _ => panic!("Realm handler received an unexpected request"),
801            }
802        });
803
804        let inspector = fuchsia_inspect::Inspector::default();
805        let session_manager = SessionManager::new_default(realm, &inspector);
806        let lifecycle = serve_lifecycle(session_manager);
807
808        assert!(
809            lifecycle
810                .start(&fsession::LifecycleStartRequest {
811                    session_url: Some(session_url.to_string()),
812                    ..Default::default()
813                })
814                .await
815                .is_ok()
816        );
817        assert_data_tree!(inspector, root: {
818            session_started_at: {
819                "0": {
820                    "@time": AnyProperty
821                }
822            }
823        });
824    }
825
826    /// Verifies that Lifecycle.Start starts the default session if no URL is provided.
827    #[fuchsia::test]
828    async fn test_start_default() {
829        let default_session_url = "session";
830
831        let realm = spawn_stream_handler(move |realm_request| async move {
832            match realm_request {
833                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
834                    let _ = responder.send(Ok(()));
835                }
836                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
837                    assert_eq!(decl.url.unwrap(), default_session_url);
838                    spawn_noop_controller_server(args.controller.unwrap());
839                    let _ = responder.send(Ok(()));
840                }
841                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
842                    spawn_noop_directory_server(exposed_dir);
843                    let _ = responder.send(Ok(()));
844                }
845                _ => panic!("Realm handler received an unexpected request"),
846            }
847        });
848
849        let inspector = fuchsia_inspect::Inspector::default();
850        let session_manager = SessionManager::new(
851            realm,
852            &inspector,
853            Some(default_session_url.to_owned()),
854            false,
855            false,
856        );
857        let lifecycle = serve_lifecycle(session_manager);
858
859        assert!(
860            lifecycle
861                .start(&fsession::LifecycleStartRequest { session_url: None, ..Default::default() })
862                .await
863                .is_ok()
864        );
865        assert_data_tree!(inspector, root: {
866            session_started_at: {
867                "0": {
868                    "@time": AnyProperty
869                }
870            }
871        });
872    }
873
874    /// Verifies that Lifecycle.Stop stops an existing session by destroying its component.
875    #[fuchsia::test]
876    async fn test_stop_destroys_component() {
877        static NUM_DESTROY_CHILD_CALLS: LazyLock<Counter> = LazyLock::new(|| Counter::new(0));
878
879        let session_url = "session";
880
881        let realm = spawn_stream_handler(move |realm_request| async move {
882            match realm_request {
883                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
884                    NUM_DESTROY_CHILD_CALLS.inc();
885                    let _ = responder.send(Ok(()));
886                }
887                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
888                    assert_eq!(decl.url.unwrap(), session_url);
889                    spawn_noop_controller_server(args.controller.unwrap());
890                    let _ = responder.send(Ok(()));
891                }
892                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
893                    spawn_noop_directory_server(exposed_dir);
894                    let _ = responder.send(Ok(()));
895                }
896                _ => panic!("Realm handler received an unexpected request"),
897            }
898        });
899
900        let inspector = fuchsia_inspect::Inspector::default();
901        let session_manager = SessionManager::new_default(realm, &inspector);
902        let lifecycle = serve_lifecycle(session_manager);
903
904        assert!(
905            lifecycle
906                .start(&fsession::LifecycleStartRequest {
907                    session_url: Some(session_url.to_string()),
908                    ..Default::default()
909                })
910                .await
911                .is_ok()
912        );
913        // Start attempts to destroy any existing session first.
914        assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 1);
915        assert_data_tree!(inspector, root: {
916            session_started_at: {
917                "0": {
918                    "@time": AnyProperty
919                }
920            }
921        });
922
923        assert!(lifecycle.stop().await.is_ok());
924        assert_eq!(NUM_DESTROY_CHILD_CALLS.get(), 2);
925    }
926
927    /// Verifies that Lifecycle.Restart restarts an existing session.
928    #[fuchsia::test]
929    async fn test_lifecycle_restart() {
930        let session_url = "session";
931
932        let realm = spawn_stream_handler(move |realm_request| async move {
933            match realm_request {
934                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
935                    let _ = responder.send(Ok(()));
936                }
937                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
938                    assert_eq!(decl.url.unwrap(), session_url);
939                    spawn_noop_controller_server(args.controller.unwrap());
940                    let _ = responder.send(Ok(()));
941                }
942                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
943                    spawn_noop_directory_server(exposed_dir);
944                    let _ = responder.send(Ok(()));
945                }
946                _ => panic!("Realm handler received an unexpected request"),
947            }
948        });
949
950        let inspector = fuchsia_inspect::Inspector::default();
951        let session_manager = SessionManager::new_default(realm, &inspector);
952        let lifecycle = serve_lifecycle(session_manager.clone());
953
954        assert!(
955            lifecycle
956                .start(&fsession::LifecycleStartRequest {
957                    session_url: Some(session_url.to_string()),
958                    ..Default::default()
959                })
960                .await
961                .expect("could not call Launch")
962                .is_ok()
963        );
964
965        assert!(lifecycle.restart().await.expect("could not call Restart").is_ok());
966
967        assert_data_tree!(inspector, root: {
968            session_started_at: {
969                "0": {
970                    "@time": AnyProperty
971                },
972                "1": {
973                    "@time": AnyProperty
974                }
975            }
976        });
977    }
978
979    /// Verifies that a node can be opened in the session's exposed dir before the session is
980    /// started, and that it is connected once the session is started.
981    #[fuchsia::test]
982    async fn test_svc_from_session_before_start() -> Result<(), Error> {
983        let session_url = "session";
984        let svc_path = "foo";
985
986        let (path_sender, mut path_receiver) = mpsc::channel(1);
987
988        let session_exposed_dir_handler = move |directory_request| match directory_request {
989            fio::DirectoryRequest::Open { path, .. } => {
990                let mut path_sender: mpsc::Sender<String> = path_sender.clone();
991                path_sender.try_send(path).unwrap();
992            }
993            _ => panic!("Directory handler received an unexpected request"),
994        };
995
996        let realm = spawn_stream_handler(move |realm_request| {
997            let session_exposed_dir_handler = session_exposed_dir_handler.clone();
998            async move {
999                match realm_request {
1000                    fcomponent::RealmRequest::DestroyChild { responder, .. } => {
1001                        let _ = responder.send(Ok(()));
1002                    }
1003                    fcomponent::RealmRequest::CreateChild { args, responder, .. } => {
1004                        spawn_noop_controller_server(args.controller.unwrap());
1005                        let _ = responder.send(Ok(()));
1006                    }
1007                    fcomponent::RealmRequest::OpenExposedDir { exposed_dir, responder, .. } => {
1008                        spawn_directory_server(exposed_dir, session_exposed_dir_handler);
1009                        let _ = responder.send(Ok(()));
1010                    }
1011                    _ => panic!("Realm handler received an unexpected request"),
1012                }
1013            }
1014        });
1015
1016        let inspector = fuchsia_inspect::Inspector::default();
1017        let session_manager = SessionManager::new_default(realm, &inspector);
1018        let lifecycle = serve_lifecycle(session_manager.clone());
1019
1020        // Open an arbitrary node in the session's exposed dir.
1021        // The actual protocol does not matter because it's not being served.
1022        let (_client_end, server_end) = fidl::endpoints::create_proxy();
1023
1024        open_session_exposed_dir(session_manager, svc_path, server_end);
1025        // Start the session.
1026        lifecycle
1027            .start(&fsession::LifecycleStartRequest {
1028                session_url: Some(session_url.to_string()),
1029                ..Default::default()
1030            })
1031            .await?
1032            .map_err(|err| anyhow!("failed to start: {err:?}"))?;
1033
1034        // The exposed dir should have received the Open request.
1035        assert_eq!(path_receiver.next().await.unwrap(), svc_path);
1036
1037        Ok(())
1038    }
1039
1040    /// Verifies that a node in the session's exposed dir can be opened after the session has
1041    /// started.
1042    #[fuchsia::test]
1043    async fn test_svc_from_session_after_start() -> Result<(), Error> {
1044        let session_url = "session";
1045        let svc_path = "foo";
1046
1047        let (path_sender, mut path_receiver) = mpsc::channel(1);
1048
1049        let session_exposed_dir_handler = move |directory_request| match directory_request {
1050            fio::DirectoryRequest::Open { path, .. } => {
1051                let mut path_sender = path_sender.clone();
1052                path_sender.try_send(path).unwrap();
1053            }
1054            _ => panic!("Directory handler received an unexpected request"),
1055        };
1056
1057        let realm = spawn_stream_handler(move |realm_request| {
1058            let session_exposed_dir_handler = session_exposed_dir_handler.clone();
1059            async move {
1060                match realm_request {
1061                    fcomponent::RealmRequest::DestroyChild { responder, .. } => {
1062                        let _ = responder.send(Ok(()));
1063                    }
1064                    fcomponent::RealmRequest::CreateChild { args, responder, .. } => {
1065                        spawn_noop_controller_server(args.controller.unwrap());
1066                        let _ = responder.send(Ok(()));
1067                    }
1068                    fcomponent::RealmRequest::OpenExposedDir { exposed_dir, responder, .. } => {
1069                        spawn_directory_server(exposed_dir, session_exposed_dir_handler);
1070                        let _ = responder.send(Ok(()));
1071                    }
1072                    _ => panic!("Realm handler received an unexpected request"),
1073                }
1074            }
1075        });
1076
1077        let inspector = fuchsia_inspect::Inspector::default();
1078        let session_manager = SessionManager::new_default(realm, &inspector);
1079        let lifecycle = serve_lifecycle(session_manager.clone());
1080
1081        lifecycle
1082            .start(&fsession::LifecycleStartRequest {
1083                session_url: Some(session_url.to_string()),
1084                ..Default::default()
1085            })
1086            .await?
1087            .map_err(|err| anyhow!("failed to start: {err:?}"))?;
1088
1089        // Open an arbitrary node in the session's exposed dir.
1090        // The actual protocol does not matter because it's not being served.
1091        let (_client_end, server_end) = fidl::endpoints::create_proxy();
1092
1093        open_session_exposed_dir(session_manager, svc_path, server_end);
1094
1095        assert_eq!(path_receiver.next().await.unwrap(), svc_path);
1096
1097        Ok(())
1098    }
1099
1100    /// Verifies that `DebugState` is reset when a session is stopped.
1101    #[fuchsia::test]
1102    async fn test_stop_resets_debug_state() {
1103        let session_url = "session";
1104
1105        let realm = spawn_stream_handler(move |realm_request| async move {
1106            match realm_request {
1107                fcomponent::RealmRequest::DestroyChild { child: _, responder } => {
1108                    let _ = responder.send(Ok(()));
1109                }
1110                fcomponent::RealmRequest::CreateChild { collection: _, decl, args, responder } => {
1111                    assert_eq!(decl.url.unwrap(), session_url);
1112                    spawn_noop_controller_server(args.controller.unwrap());
1113                    let _ = responder.send(Ok(()));
1114                }
1115                fcomponent::RealmRequest::OpenExposedDir { child: _, exposed_dir, responder } => {
1116                    spawn_noop_directory_server(exposed_dir);
1117                    let _ = responder.send(Ok(()));
1118                }
1119                _ => panic!("Realm handler received an unexpected request"),
1120            }
1121        });
1122
1123        let inspector = fuchsia_inspect::Inspector::default();
1124        let session_manager = SessionManager::new(realm, &inspector, None, false, true);
1125        let lifecycle = serve_lifecycle(session_manager.clone());
1126
1127        assert!(
1128            lifecycle
1129                .start(&fsession::LifecycleStartRequest {
1130                    session_url: Some(session_url.to_string()),
1131                    ..Default::default()
1132                })
1133                .await
1134                .is_ok()
1135        );
1136
1137        session_manager.state.debug.set_button_press_state_count(3);
1138        assert_eq!(session_manager.state.debug.get_button_press_state_count(), 3);
1139
1140        assert!(lifecycle.stop().await.is_ok());
1141
1142        assert_eq!(session_manager.state.debug.get_button_press_state_count(), 0);
1143    }
1144}