Skip to main content

component_debug/cli/
run.rs

1// Copyright 2023 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::cli::format::{
6    format_create_error, format_destroy_error, format_resolve_error, format_start_error,
7};
8use crate::lifecycle::{
9    ActionError, CreateError, DestroyError, StartError, create_instance_in_collection,
10    destroy_instance_in_collection, resolve_instance, start_instance, start_instance_with_args,
11};
12use anyhow::{Result, bail, format_err};
13#[allow(unused)]
14use flex_client::ProxyHasDomain;
15use flex_client::Socket;
16use flex_fuchsia_component as fcomponent;
17use flex_fuchsia_component_decl as fdecl;
18use flex_fuchsia_process as fprocess;
19use flex_fuchsia_sys2 as fsys;
20use fuchsia_url::fuchsia_pkg::AbsoluteComponentUrl;
21use futures::future::BoxFuture;
22#[allow(unused)]
23use futures::{AsyncReadExt, AsyncWriteExt};
24use moniker::Moniker;
25use std::io::Read;
26
27// This value is fairly arbitrary. The value matches `MAX_BUF` from `fuchsia.io`, but that
28// constant is for `fuchsia.io.File` transfers, which are unrelated to these `zx::socket`
29// transfers.
30const TRANSFER_CHUNK_SIZE: usize = 8192;
31
32async fn copy<W: std::io::Write>(source: Socket, mut sink: W) -> Result<()> {
33    #[cfg(not(feature = "fdomain"))]
34    let mut source = fuchsia_async::Socket::from_socket(source);
35    #[cfg(feature = "fdomain")]
36    let mut source = source;
37    let mut buf = [0u8; TRANSFER_CHUNK_SIZE];
38    loop {
39        let bytes_read = source.read(&mut buf).await?;
40        if bytes_read == 0 {
41            return Ok(());
42        }
43        sink.write_all(&buf[..bytes_read])?;
44        sink.flush()?;
45    }
46}
47
48// The normal Rust representation of this constant is in fuchsia-runtime, which
49// cannot be used on host. Maybe there's a way to move fruntime::HandleType and
50// fruntime::HandleInfo to a place that can be used on host?
51fn handle_id_for_fd(fd: u32) -> u32 {
52    const PA_FD: u32 = 0x30;
53    PA_FD | fd << 16
54}
55
56struct Stdio {
57    local_in: Socket,
58    local_out: Socket,
59    local_err: Socket,
60}
61
62impl Stdio {
63    #[cfg(not(feature = "fdomain"))]
64    fn new() -> (Self, Vec<fprocess::HandleInfo>) {
65        let (local_in, remote_in) = fidl::Socket::create_stream();
66        let (local_out, remote_out) = fidl::Socket::create_stream();
67        let (local_err, remote_err) = fidl::Socket::create_stream();
68
69        (
70            Self { local_in, local_out, local_err },
71            vec![
72                fprocess::HandleInfo { handle: remote_in.into_handle(), id: handle_id_for_fd(0) },
73                fprocess::HandleInfo { handle: remote_out.into_handle(), id: handle_id_for_fd(1) },
74                fprocess::HandleInfo { handle: remote_err.into_handle(), id: handle_id_for_fd(2) },
75            ],
76        )
77    }
78
79    #[cfg(feature = "fdomain")]
80    fn new(client: &std::sync::Arc<flex_client::Client>) -> (Self, Vec<fprocess::HandleInfo>) {
81        use flex_client::HandleBased;
82        let (local_in, remote_in) = client.create_stream_socket();
83        let (local_out, remote_out) = client.create_stream_socket();
84        let (local_err, remote_err) = client.create_stream_socket();
85
86        (
87            Self { local_in, local_out, local_err },
88            vec![
89                fprocess::HandleInfo { handle: remote_in.into_handle(), id: handle_id_for_fd(0) },
90                fprocess::HandleInfo { handle: remote_out.into_handle(), id: handle_id_for_fd(1) },
91                fprocess::HandleInfo { handle: remote_err.into_handle(), id: handle_id_for_fd(2) },
92            ],
93        )
94    }
95
96    async fn forward(self) {
97        let local_in = self.local_in;
98        let local_out = self.local_out;
99        let local_err = self.local_err;
100
101        let mut local_in = flex_client::socket_to_async(local_in);
102
103        std::thread::spawn(move || {
104            let mut term_in = std::io::stdin().lock();
105            let mut buf = [0u8; TRANSFER_CHUNK_SIZE];
106            let mut executor = fuchsia_async::LocalExecutorBuilder::new().build();
107            loop {
108                let bytes_read = term_in.read(&mut buf)?;
109                if bytes_read == 0 {
110                    return Ok::<(), anyhow::Error>(());
111                }
112
113                executor.run_singlethreaded(local_in.write_all(&buf[..bytes_read]))?;
114            }
115        });
116
117        std::thread::spawn(move || {
118            let mut executor = fuchsia_async::LocalExecutorBuilder::new().build();
119            let _result: Result<()> = executor
120                .run_singlethreaded(async move { copy(local_err, std::io::stderr()).await });
121        });
122
123        std::thread::spawn(move || {
124            let mut executor = fuchsia_async::LocalExecutorBuilder::new().build();
125            let _result: Result<()> = executor
126                .run_singlethreaded(async move { copy(local_out, std::io::stdout().lock()).await });
127            std::process::exit(0);
128        });
129
130        // If we're following stdio, we just wait forever. When stdout is
131        // closed, the whole process will exit.
132        let () = futures::future::pending().await;
133    }
134}
135
136pub async fn run_cmd<W: std::io::Write>(
137    moniker: Moniker,
138    url: AbsoluteComponentUrl,
139    recreate: bool,
140    connect_stdio: bool,
141    config_overrides: Vec<fdecl::ConfigOverride>,
142    lifecycle_controller_factory: impl Fn()
143        -> BoxFuture<'static, Result<fsys::LifecycleControllerProxy>>,
144    mut writer: W,
145) -> Result<()> {
146    let lifecycle_controller = lifecycle_controller_factory().await?;
147    let parent = moniker
148        .parent()
149        .ok_or_else(|| format_err!("Error: {} does not reference a dynamic instance", moniker))?;
150    let leaf = moniker
151        .leaf()
152        .ok_or_else(|| format_err!("Error: {} does not reference a dynamic instance", moniker))?;
153    let child_name = leaf.name();
154    let collection = leaf
155        .collection()
156        .ok_or_else(|| format_err!("Error: {} does not reference a dynamic instance", moniker))?;
157
158    if recreate {
159        // First try to destroy any existing instance at this monker.
160        match destroy_instance_in_collection(&lifecycle_controller, &parent, collection, child_name)
161            .await
162        {
163            Ok(()) => {
164                writeln!(writer, "Destroyed existing component instance at {}...", moniker)?;
165            }
166            Err(DestroyError::ActionError(ActionError::InstanceNotFound))
167            | Err(DestroyError::ActionError(ActionError::InstanceNotResolved)) => {
168                // No resolved component exists at this moniker. Nothing to do.
169            }
170            Err(e) => return Err(format_destroy_error(&moniker, e)),
171        }
172    }
173
174    writeln!(writer, "URL: {}", url)?;
175    writeln!(writer, "Moniker: {}", moniker)?;
176    writeln!(writer, "Creating component instance...")?;
177
178    // First try to use StartWithArgs
179
180    let (mut maybe_stdio, numbered_handles) = if connect_stdio {
181        #[cfg(not(feature = "fdomain"))]
182        let (stdio, numbered_handles) = Stdio::new();
183        #[cfg(feature = "fdomain")]
184        let (stdio, numbered_handles) = Stdio::new(&lifecycle_controller.domain());
185        (Some(stdio), Some(numbered_handles))
186    } else {
187        (None, Some(vec![]))
188    };
189
190    let create_result = create_instance_in_collection(
191        &lifecycle_controller,
192        &parent,
193        collection,
194        child_name,
195        &url,
196        config_overrides.clone(),
197        None,
198    )
199    .await;
200
201    match create_result {
202        Err(CreateError::InstanceAlreadyExists) => {
203            bail!(
204                "\nError: {} already exists.\nUse --recreate to destroy and create a new instance, or provide a different moniker.\n",
205                moniker
206            )
207        }
208        Err(e) => {
209            return Err(format_create_error(&moniker, &parent, collection, e));
210        }
211        Ok(()) => {}
212    }
213
214    writeln!(writer, "Resolving component instance...")?;
215    if let Err(e) = resolve_instance(&lifecycle_controller, &moniker).await {
216        writeln!(writer, "Cleaning up component instance...")?;
217        if let Err(de) =
218            destroy_instance_in_collection(&lifecycle_controller, &parent, collection, child_name)
219                .await
220        {
221            writeln!(writer, "Warning: Failed to clean up component instance: {:?}", de)?;
222        }
223        return Err(format_resolve_error(&moniker, e));
224    }
225
226    writeln!(writer, "Starting component instance...")?;
227    let start_args = fcomponent::StartChildArgs { numbered_handles, ..Default::default() };
228    let res = start_instance_with_args(&lifecycle_controller, &moniker, start_args).await;
229    if let Err(StartError::ActionError(ActionError::Fidl(_e))) = &res {
230        // A FIDL error here could indicate that we're talking to a version of component manager
231        // that does not support `fuchsia.sys2/LifecycleController.StartInstanceWithArgs`. Let's
232        // try again with `fuchsia.sys2/LifecycleController.StartInstance`.
233
234        // Component manager will close the lifecycle controller when it encounters a FIDL error,
235        // so we need to create a new one.
236        let lifecycle_controller = lifecycle_controller_factory().await?;
237
238        if connect_stdio {
239            // We want to provide stdio handles to the component, but this is only possible when
240            // creating an instance when we have to use the legacy `StartInstance`. Delete and
241            // recreate the component, providing the handles to the create call.
242
243            #[cfg(not(feature = "fdomain"))]
244            let (stdio, numbered_handles) = Stdio::new();
245            #[cfg(feature = "fdomain")]
246            let (stdio, numbered_handles) = Stdio::new(&lifecycle_controller.domain());
247            maybe_stdio = Some(stdio);
248            let create_args = fcomponent::CreateChildArgs {
249                numbered_handles: Some(numbered_handles),
250                ..Default::default()
251            };
252
253            destroy_instance_in_collection(&lifecycle_controller, &parent, collection, child_name)
254                .await?;
255            create_instance_in_collection(
256                &lifecycle_controller,
257                &parent,
258                collection,
259                child_name,
260                &url,
261                config_overrides,
262                Some(create_args),
263            )
264            .await?;
265            resolve_instance(&lifecycle_controller, &moniker)
266                .await
267                .map_err(|e| format_resolve_error(&moniker, e))?;
268        }
269
270        let _stop_future = start_instance(&lifecycle_controller, &moniker)
271            .await
272            .map_err(|e| format_start_error(&moniker, e))?;
273    } else {
274        let _stop_future = res.map_err(|e| format_start_error(&moniker, e))?;
275    }
276
277    if let Some(stdio) = maybe_stdio {
278        stdio.forward().await;
279    }
280
281    writeln!(writer, "Component instance is running!")?;
282
283    Ok(())
284}
285
286#[cfg(test)]
287mod test {
288    use super::*;
289    use fidl::endpoints::create_proxy_and_stream;
290    use flex_fuchsia_sys2 as fsys;
291    use futures::{FutureExt, TryStreamExt};
292
293    fn setup_fake_lifecycle_controller_ok(
294        expected_parent_moniker: &'static str,
295        expected_collection: &'static str,
296        expected_name: &'static str,
297        expected_url: &'static str,
298        expected_moniker: &'static str,
299        expect_destroy: bool,
300    ) -> fsys::LifecycleControllerProxy {
301        let (lifecycle_controller, mut stream) =
302            create_proxy_and_stream::<fsys::LifecycleControllerMarker>();
303        fuchsia_async::Task::local(async move {
304            if expect_destroy {
305                let req = stream.try_next().await.unwrap().unwrap();
306                match req {
307                    fsys::LifecycleControllerRequest::DestroyInstance {
308                        parent_moniker,
309                        child,
310                        responder,
311                    } => {
312                        assert_eq!(
313                            Moniker::parse_str(expected_parent_moniker),
314                            Moniker::parse_str(&parent_moniker)
315                        );
316                        assert_eq!(expected_name, child.name);
317                        assert_eq!(expected_collection, child.collection.unwrap());
318                        responder.send(Ok(())).unwrap();
319                    }
320                    _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
321                }
322            }
323
324            let req = stream.try_next().await.unwrap().unwrap();
325            match req {
326                fsys::LifecycleControllerRequest::CreateInstance {
327                    parent_moniker,
328                    collection,
329                    decl,
330                    responder,
331                    args: _,
332                } => {
333                    assert_eq!(
334                        Moniker::parse_str(expected_parent_moniker),
335                        Moniker::parse_str(&parent_moniker)
336                    );
337                    assert_eq!(expected_collection, collection.name);
338                    assert_eq!(expected_name, decl.name.unwrap());
339                    assert_eq!(expected_url, decl.url.unwrap());
340                    responder.send(Ok(())).unwrap();
341                }
342                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
343            }
344
345            let req = stream.try_next().await.unwrap().unwrap();
346            match req {
347                fsys::LifecycleControllerRequest::ResolveInstance { moniker, responder } => {
348                    assert_eq!(Moniker::parse_str(expected_moniker), Moniker::parse_str(&moniker));
349                    responder.send(Ok(())).unwrap();
350                }
351                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
352            }
353
354            let req = stream.try_next().await.unwrap().unwrap();
355            match req {
356                fsys::LifecycleControllerRequest::StartInstanceWithArgs {
357                    moniker,
358                    binder: _,
359                    args: _,
360                    responder,
361                } => {
362                    assert_eq!(Moniker::parse_str(expected_moniker), Moniker::parse_str(&moniker));
363                    responder.send(Ok(())).unwrap();
364                }
365                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
366            }
367        })
368        .detach();
369        lifecycle_controller
370    }
371
372    fn setup_fake_lifecycle_controller_fail(
373        expected_parent_moniker: &'static str,
374        expected_collection: &'static str,
375        expected_name: &'static str,
376        expected_url: &'static str,
377    ) -> fsys::LifecycleControllerProxy {
378        let (lifecycle_controller, mut stream) =
379            create_proxy_and_stream::<fsys::LifecycleControllerMarker>();
380        fuchsia_async::Task::local(async move {
381            let req = stream.try_next().await.unwrap().unwrap();
382            match req {
383                fsys::LifecycleControllerRequest::DestroyInstance {
384                    parent_moniker,
385                    child,
386                    responder,
387                } => {
388                    assert_eq!(
389                        Moniker::parse_str(expected_parent_moniker),
390                        Moniker::parse_str(&parent_moniker)
391                    );
392                    assert_eq!(expected_name, child.name);
393                    assert_eq!(expected_collection, child.collection.unwrap());
394                    responder.send(Ok(())).unwrap();
395                }
396                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
397            }
398
399            let req = stream.try_next().await.unwrap().unwrap();
400            match req {
401                fsys::LifecycleControllerRequest::CreateInstance {
402                    parent_moniker,
403                    collection,
404                    decl,
405                    responder,
406                    args: _,
407                } => {
408                    assert_eq!(
409                        Moniker::parse_str(expected_parent_moniker),
410                        Moniker::parse_str(&parent_moniker)
411                    );
412                    assert_eq!(expected_collection, collection.name);
413                    assert_eq!(expected_name, decl.name.unwrap());
414                    assert_eq!(expected_url, decl.url.unwrap());
415                    responder.send(Err(fsys::CreateError::InstanceAlreadyExists)).unwrap();
416                }
417                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
418            }
419        })
420        .detach();
421        lifecycle_controller
422    }
423
424    fn setup_fake_lifecycle_controller_recreate(
425        expected_parent_moniker: &'static str,
426        expected_collection: &'static str,
427        expected_name: &'static str,
428        expected_url: &'static str,
429        expected_moniker: &'static str,
430    ) -> fsys::LifecycleControllerProxy {
431        let (lifecycle_controller, mut stream) =
432            create_proxy_and_stream::<fsys::LifecycleControllerMarker>();
433        fuchsia_async::Task::local(async move {
434            let req = stream.try_next().await.unwrap().unwrap();
435            match req {
436                fsys::LifecycleControllerRequest::DestroyInstance {
437                    parent_moniker,
438                    child,
439                    responder,
440                } => {
441                    assert_eq!(
442                        Moniker::parse_str(expected_parent_moniker),
443                        Moniker::parse_str(&parent_moniker)
444                    );
445                    assert_eq!(expected_name, child.name);
446                    assert_eq!(expected_collection, child.collection.unwrap());
447                    responder.send(Ok(())).unwrap();
448                }
449                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
450            }
451
452            let req = stream.try_next().await.unwrap().unwrap();
453            match req {
454                fsys::LifecycleControllerRequest::CreateInstance {
455                    parent_moniker,
456                    collection,
457                    decl,
458                    responder,
459                    args: _,
460                } => {
461                    assert_eq!(
462                        Moniker::parse_str(expected_parent_moniker),
463                        Moniker::parse_str(&parent_moniker)
464                    );
465                    assert_eq!(expected_collection, collection.name);
466                    assert_eq!(expected_name, decl.name.unwrap());
467                    assert_eq!(expected_url, decl.url.unwrap());
468                    responder.send(Ok(())).unwrap();
469                }
470                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
471            }
472
473            let req = stream.try_next().await.unwrap().unwrap();
474            match req {
475                fsys::LifecycleControllerRequest::ResolveInstance { moniker, responder } => {
476                    assert_eq!(Moniker::parse_str(expected_moniker), Moniker::parse_str(&moniker));
477                    responder.send(Ok(())).unwrap();
478                }
479                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
480            }
481
482            let req = stream.try_next().await.unwrap().unwrap();
483            match req {
484                fsys::LifecycleControllerRequest::StartInstanceWithArgs {
485                    moniker,
486                    binder: _,
487                    args: _,
488                    responder,
489                } => {
490                    assert_eq!(Moniker::parse_str(expected_moniker), Moniker::parse_str(&moniker));
491                    responder.send(Ok(())).unwrap();
492                }
493                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
494            }
495        })
496        .detach();
497        lifecycle_controller
498    }
499
500    #[fuchsia_async::run_singlethreaded(test)]
501    async fn test_ok() -> Result<()> {
502        let mut output = Vec::new();
503        let lifecycle_controller = setup_fake_lifecycle_controller_ok(
504            "/some",
505            "collection",
506            "name",
507            "fuchsia-pkg://fuchsia.com/test#meta/test.cm",
508            "/some/collection:name",
509            true,
510        );
511        let response = run_cmd(
512            "/some/collection:name".try_into().unwrap(),
513            "fuchsia-pkg://fuchsia.com/test#meta/test.cm".try_into().unwrap(),
514            true,
515            false,
516            vec![],
517            move || {
518                let lifecycle_controller = lifecycle_controller.clone();
519                async move { Ok(lifecycle_controller) }.boxed()
520            },
521            &mut output,
522        )
523        .await;
524        response.unwrap();
525        Ok(())
526    }
527
528    #[fuchsia_async::run_singlethreaded(test)]
529    async fn test_name() -> Result<()> {
530        let mut output = Vec::new();
531        let lifecycle_controller = setup_fake_lifecycle_controller_ok(
532            "/core",
533            "ffx-laboratory",
534            "foobar",
535            "fuchsia-pkg://fuchsia.com/test#meta/test.cm",
536            "/core/ffx-laboratory:foobar",
537            false,
538        );
539        let response = run_cmd(
540            "/core/ffx-laboratory:foobar".try_into().unwrap(),
541            "fuchsia-pkg://fuchsia.com/test#meta/test.cm".try_into().unwrap(),
542            false,
543            false,
544            vec![],
545            move || {
546                let lifecycle_controller = lifecycle_controller.clone();
547                async move { Ok(lifecycle_controller) }.boxed()
548            },
549            &mut output,
550        )
551        .await;
552        response.unwrap();
553        Ok(())
554    }
555
556    #[fuchsia_async::run_singlethreaded(test)]
557    async fn test_fail() -> Result<()> {
558        let mut output = Vec::new();
559        let lifecycle_controller = setup_fake_lifecycle_controller_fail(
560            "/core",
561            "ffx-laboratory",
562            "test",
563            "fuchsia-pkg://fuchsia.com/test#meta/test.cm",
564        );
565        let response = run_cmd(
566            "/core/ffx-laboratory:test".try_into().unwrap(),
567            "fuchsia-pkg://fuchsia.com/test#meta/test.cm".try_into().unwrap(),
568            true,
569            false,
570            vec![],
571            move || {
572                let lifecycle_controller = lifecycle_controller.clone();
573                async move { Ok(lifecycle_controller) }.boxed()
574            },
575            &mut output,
576        )
577        .await;
578        response.unwrap_err();
579        Ok(())
580    }
581
582    #[fuchsia_async::run_singlethreaded(test)]
583    async fn test_recreate() -> Result<()> {
584        let mut output = Vec::new();
585        let lifecycle_controller = setup_fake_lifecycle_controller_recreate(
586            "/core",
587            "ffx-laboratory",
588            "test",
589            "fuchsia-pkg://fuchsia.com/test#meta/test.cm",
590            "/core/ffx-laboratory:test",
591        );
592        let response = run_cmd(
593            "/core/ffx-laboratory:test".try_into().unwrap(),
594            "fuchsia-pkg://fuchsia.com/test#meta/test.cm".try_into().unwrap(),
595            true,
596            false,
597            vec![],
598            move || {
599                let lifecycle_controller = lifecycle_controller.clone();
600                async move { Ok(lifecycle_controller) }.boxed()
601            },
602            &mut output,
603        )
604        .await;
605        response.unwrap();
606        Ok(())
607    }
608
609    fn setup_fake_lifecycle_controller_resolve_fail_cleanup(
610        expected_parent_moniker: &'static str,
611        expected_collection: &'static str,
612        expected_name: &'static str,
613        expected_url: &'static str,
614        expected_moniker: &'static str,
615        destroy_sender: futures::channel::oneshot::Sender<()>,
616    ) -> fsys::LifecycleControllerProxy {
617        let (lifecycle_controller, mut stream) =
618            create_proxy_and_stream::<fsys::LifecycleControllerMarker>();
619        fuchsia_async::Task::local(async move {
620            let req = stream.try_next().await.unwrap().unwrap();
621            match req {
622                fsys::LifecycleControllerRequest::CreateInstance {
623                    parent_moniker,
624                    collection,
625                    decl,
626                    responder,
627                    ..
628                } => {
629                    assert_eq!(
630                        Moniker::parse_str(expected_parent_moniker),
631                        Moniker::parse_str(&parent_moniker)
632                    );
633                    assert_eq!(expected_collection, collection.name);
634                    assert_eq!(expected_name, decl.name.unwrap());
635                    assert_eq!(expected_url, decl.url.unwrap());
636                    responder.send(Ok(())).unwrap();
637                }
638                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
639            }
640
641            let req = stream.try_next().await.unwrap().unwrap();
642            match req {
643                fsys::LifecycleControllerRequest::ResolveInstance { moniker, responder } => {
644                    assert_eq!(Moniker::parse_str(expected_moniker), Moniker::parse_str(&moniker));
645                    responder.send(Err(fsys::ResolveError::PackageNotFound)).unwrap();
646                }
647                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
648            }
649
650            let req = stream.try_next().await.unwrap().unwrap();
651            match req {
652                fsys::LifecycleControllerRequest::DestroyInstance {
653                    parent_moniker,
654                    child,
655                    responder,
656                } => {
657                    assert_eq!(
658                        Moniker::parse_str(expected_parent_moniker),
659                        Moniker::parse_str(&parent_moniker)
660                    );
661                    assert_eq!(expected_name, child.name);
662                    assert_eq!(expected_collection, child.collection.unwrap());
663                    responder.send(Ok(())).unwrap();
664                    let _ = destroy_sender.send(());
665                }
666                _ => panic!("Unexpected Lifecycle Controller request: {:?}", req),
667            }
668        })
669        .detach();
670        lifecycle_controller
671    }
672
673    #[fuchsia_async::run_singlethreaded(test)]
674    async fn test_resolve_fail_cleanup() -> Result<()> {
675        let mut output = Vec::new();
676        let (destroy_sender, mut destroy_receiver) = futures::channel::oneshot::channel::<()>();
677
678        let lifecycle_controller = setup_fake_lifecycle_controller_resolve_fail_cleanup(
679            "/core",
680            "ffx-laboratory",
681            "test",
682            "fuchsia-pkg://fuchsia.com/test#meta/test.cm",
683            "/core/ffx-laboratory:test",
684            destroy_sender,
685        );
686        let response = run_cmd(
687            "/core/ffx-laboratory:test".try_into().unwrap(),
688            "fuchsia-pkg://fuchsia.com/test#meta/test.cm".try_into().unwrap(),
689            false,
690            false,
691            vec![],
692            move || {
693                let lifecycle_controller = lifecycle_controller.clone();
694                async move { Ok(lifecycle_controller) }.boxed()
695            },
696            &mut output,
697        )
698        .await;
699
700        assert!(response.is_err());
701        // Verify that destroy was called by checking the channel
702        assert!(destroy_receiver.try_recv().unwrap().is_some());
703        Ok(())
704    }
705}