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