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