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