1use 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
21const MAX_CONCURRENT_CONNECTIONS: usize = 10_000;
23
24const DIAGNOSTICS_SESSION_STARTED_AT_NAME: &str = "session_started_at";
26
27const DIAGNOSTICS_SESSION_STARTED_AT_SIZE: usize = 100;
29
30const DIAGNOSTICS_TIME_PROPERTY_NAME: &str = "@time";
33
34pub enum IncomingRequest {
36 Launcher(fsession::LauncherRequestStream),
37 Restarter(fsession::RestarterRequestStream),
38 Lifecycle(fsession::LifecycleRequestStream),
39 Handoff(fpower::HandoffRequestStream),
40}
41
42struct Diagnostics {
43 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
58struct PendingSession {
60 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
73struct StartedSession {
78 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 power_element: futures::lock::Mutex<Option<power::PowerElement>>,
100
101 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 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 default_session_url: Option<String>,
155
156 session: futures::lock::Mutex<Session>,
158
159 realm: fcomponent::RealmProxy,
161
162 power: PowerState,
164
165 inner: Mutex<Inner>,
167}
168
169struct Inner {
170 diagnostics: Diagnostics,
172
173 exposed_dir: fio::DirectoryProxy,
175}
176
177impl SessionManagerState {
178 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 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 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 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#[derive(Clone)]
277pub struct SessionManager {
278 state: Arc<SessionManagerState>,
279}
280
281impl SessionManager {
282 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 pub async fn start_default_session(&mut self) -> Result<(), Error> {
322 self.state.start_default().await?;
323 Ok(())
324 }
325
326 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 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 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 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 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 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 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 async fn handle_restart_request(&mut self) -> Result<(), fsession::RestartError> {
512 self.state.restart().await.map_err(Into::into)
513 }
514
515 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 async fn handle_lifecycle_stop_request(&mut self) -> Result<(), fsession::LifecycleError> {
533 self.state.stop().await.map_err(Into::into)
534 }
535
536 async fn handle_lifecycle_restart_request(&mut self) -> Result<(), fsession::LifecycleError> {
538 self.state.restart().await.map_err(Into::into)
539 }
540
541 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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 let (_client_end, server_end) = fidl::endpoints::create_proxy();
1006
1007 open_session_exposed_dir(session_manager, svc_path, server_end);
1008 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 assert_eq!(path_receiver.next().await.unwrap(), svc_path);
1019
1020 Ok(())
1021 }
1022
1023 #[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 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}