component_debug/cli/
show.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::query::get_single_instance_from_query;
6use crate::realm::{
7    get_config_fields, get_merkle_root, get_outgoing_capabilities, get_resolved_declaration,
8    get_runtime, ConfigField, ExecutionInfo, ResolvedInfo, Runtime,
9};
10use ansi_term::Colour;
11use anyhow::Result;
12use cm_rust::ExposeDeclCommon;
13use flex_fuchsia_sys2 as fsys;
14use moniker::Moniker;
15use prettytable::format::FormatBuilder;
16use prettytable::{cell, row, Table};
17
18#[cfg(feature = "serde")]
19use {schemars::JsonSchema, serde::Serialize};
20
21#[cfg_attr(feature = "serde", derive(Serialize, JsonSchema))]
22pub struct ShowCmdInstance {
23    pub moniker: Moniker,
24    pub url: String,
25    pub environment: Option<String>,
26    pub instance_id: Option<String>,
27    pub resolved: Option<ShowCmdResolvedInfo>,
28}
29
30#[cfg_attr(feature = "serde", derive(Serialize, JsonSchema))]
31pub struct ShowCmdResolvedInfo {
32    pub resolved_url: String,
33    pub merkle_root: Option<String>,
34    pub runner: Option<String>,
35    pub incoming_capabilities: Vec<String>,
36    pub exposed_capabilities: Vec<String>,
37    pub config: Option<Vec<ConfigField>>,
38    pub started: Option<ShowCmdExecutionInfo>,
39    pub collections: Vec<String>,
40}
41
42#[cfg_attr(feature = "serde", derive(Serialize, JsonSchema))]
43pub struct ShowCmdExecutionInfo {
44    pub runtime: Runtime,
45    pub outgoing_capabilities: Vec<String>,
46    pub start_reason: String,
47}
48
49pub async fn show_cmd_print<W: std::io::Write>(
50    query: String,
51    realm_query: fsys::RealmQueryProxy,
52    mut writer: W,
53    with_style: bool,
54) -> Result<()> {
55    let instance = get_instance_by_query(query, realm_query).await?;
56    let table = create_table(instance, with_style);
57    table.print(&mut writer)?;
58    writeln!(&mut writer, "")?;
59
60    Ok(())
61}
62
63pub async fn show_cmd_serialized(
64    query: String,
65    realm_query: fsys::RealmQueryProxy,
66) -> Result<ShowCmdInstance> {
67    let instance = get_instance_by_query(query, realm_query).await?;
68    Ok(instance)
69}
70
71pub(crate) async fn config_table_print<W: std::io::Write>(
72    query: String,
73    realm_query: fsys::RealmQueryProxy,
74    mut writer: W,
75) -> Result<()> {
76    let instance = get_instance_by_query(query, realm_query).await?;
77    let table = create_config_table(instance);
78    table.print(&mut writer)?;
79    writeln!(&mut writer, "")?;
80
81    Ok(())
82}
83
84async fn get_instance_by_query(
85    query: String,
86    realm_query: fsys::RealmQueryProxy,
87) -> Result<ShowCmdInstance> {
88    let instance = get_single_instance_from_query(&query, &realm_query).await?;
89
90    let resolved_info = match instance.resolved_info {
91        Some(ResolvedInfo { execution_info, resolved_url }) => {
92            // Get the manifest
93            let manifest = get_resolved_declaration(&instance.moniker, &realm_query).await?;
94            let structured_config = get_config_fields(&instance.moniker, &realm_query).await?;
95            let merkle_root = get_merkle_root(&instance.moniker, &realm_query).await.ok();
96            let runner = if let Some(runner) = manifest.program.and_then(|p| p.runner) {
97                Some(runner.to_string())
98            } else if let Some(runner) = manifest.uses.iter().find_map(|u| match u {
99                cm_rust::UseDecl::Runner(cm_rust::UseRunnerDecl { source_name, .. }) => {
100                    Some(source_name)
101                }
102                _ => None,
103            }) {
104                Some(runner.to_string())
105            } else {
106                None
107            };
108            let incoming_capabilities = IntoIterator::into_iter(manifest.uses)
109                .filter_map(|u| u.path().map(|n| n.to_string()))
110                .collect();
111            let exposed_capabilities = IntoIterator::into_iter(manifest.exposes)
112                .map(|e| e.target_name().to_string())
113                .collect();
114
115            let execution_info = match execution_info {
116                Some(ExecutionInfo { start_reason }) => {
117                    let runtime = get_runtime(&instance.moniker, &realm_query)
118                        .await
119                        .unwrap_or(Runtime::Unknown);
120                    let outgoing_capabilities =
121                        get_outgoing_capabilities(&instance.moniker, &realm_query)
122                            .await
123                            .unwrap_or(vec![]);
124                    Some(ShowCmdExecutionInfo { start_reason, runtime, outgoing_capabilities })
125                }
126                None => None,
127            };
128
129            let collections =
130                IntoIterator::into_iter(manifest.collections).map(|c| c.name.to_string()).collect();
131
132            Some(ShowCmdResolvedInfo {
133                resolved_url,
134                runner,
135                incoming_capabilities,
136                exposed_capabilities,
137                merkle_root,
138                config: structured_config,
139                started: execution_info,
140                collections,
141            })
142        }
143        None => None,
144    };
145
146    Ok(ShowCmdInstance {
147        moniker: instance.moniker,
148        url: instance.url,
149        environment: instance.environment,
150        instance_id: instance.instance_id,
151        resolved: resolved_info,
152    })
153}
154
155fn create_table(instance: ShowCmdInstance, with_style: bool) -> Table {
156    let mut table = Table::new();
157    table.set_format(FormatBuilder::new().padding(2, 0).build());
158
159    table.add_row(row!(r->"Moniker:", instance.moniker));
160    table.add_row(row!(r->"URL:", instance.url));
161    table.add_row(
162        row!(r->"Environment:", instance.environment.unwrap_or_else(|| "N/A".to_string())),
163    );
164
165    if let Some(instance_id) = instance.instance_id {
166        table.add_row(row!(r->"Instance ID:", instance_id));
167    } else {
168        table.add_row(row!(r->"Instance ID:", "None"));
169    }
170
171    add_resolved_info_to_table(&mut table, instance.resolved, with_style);
172
173    table
174}
175
176fn create_config_table(instance: ShowCmdInstance) -> Table {
177    let mut table = Table::new();
178    table.set_format(FormatBuilder::new().padding(2, 0).build());
179    if let Some(resolved) = instance.resolved {
180        add_config_info_to_table(&mut table, &resolved);
181    }
182    table
183}
184
185fn colorized(string: &str, color: Colour, with_style: bool) -> String {
186    if with_style {
187        color.paint(string).to_string()
188    } else {
189        string.to_string()
190    }
191}
192
193fn add_resolved_info_to_table(
194    table: &mut Table,
195    resolved: Option<ShowCmdResolvedInfo>,
196    with_style: bool,
197) {
198    if let Some(resolved) = resolved {
199        table
200            .add_row(row!(r->"Component State:", colorized("Resolved", Colour::Green, with_style)));
201        table.add_row(row!(r->"Resolved URL:", resolved.resolved_url));
202
203        if let Some(runner) = &resolved.runner {
204            table.add_row(row!(r->"Runner:", runner));
205        }
206
207        let namespace_capabilities = resolved.incoming_capabilities.join("\n");
208        table.add_row(row!(r->"Namespace Capabilities:", namespace_capabilities));
209
210        let exposed_capabilities = resolved.exposed_capabilities.join("\n");
211        table.add_row(row!(r->"Exposed Capabilities:", exposed_capabilities));
212
213        if let Some(merkle_root) = &resolved.merkle_root {
214            table.add_row(row!(r->"Merkle root:", merkle_root));
215        } else {
216            table.add_row(row!(r->"Merkle root:", "Unknown"));
217        }
218
219        add_config_info_to_table(table, &resolved);
220
221        if !resolved.collections.is_empty() {
222            table.add_row(row!(r->"Collections:", resolved.collections.join("\n")));
223        }
224
225        add_execution_info_to_table(table, resolved.started, with_style)
226    } else {
227        table
228            .add_row(row!(r->"Component State:", colorized("Unresolved", Colour::Red, with_style)));
229    }
230}
231
232fn add_config_info_to_table(table: &mut Table, resolved: &ShowCmdResolvedInfo) {
233    if let Some(config) = &resolved.config {
234        if !config.is_empty() {
235            let mut config_table = Table::new();
236            let format = FormatBuilder::new().padding(0, 0).build();
237            config_table.set_format(format);
238
239            for field in config {
240                config_table.add_row(row!(field.key, " -> ", field.value));
241            }
242
243            table.add_row(row!(r->"Configuration:", config_table));
244        }
245    }
246}
247
248fn add_execution_info_to_table(
249    table: &mut Table,
250    exec: Option<ShowCmdExecutionInfo>,
251    with_style: bool,
252) {
253    if let Some(exec) = exec {
254        table.add_row(row!(r->"Execution State:", colorized("Running", Colour::Green, with_style)));
255        table.add_row(row!(r->"Start reason:", exec.start_reason));
256
257        let outgoing_capabilities = exec.outgoing_capabilities.join("\n");
258        table.add_row(row!(r->"Outgoing Capabilities:", outgoing_capabilities));
259
260        match exec.runtime {
261            Runtime::Elf {
262                job_id,
263                process_id,
264                process_start_time,
265                process_start_time_utc_estimate,
266            } => {
267                table.add_row(row!(r->"Runtime:", "ELF"));
268                if let Some(utc_estimate) = process_start_time_utc_estimate {
269                    table.add_row(row!(r->"Running since:", utc_estimate));
270                } else if let Some(nanos_since_boot) = process_start_time {
271                    table.add_row(
272                        row!(r->"Running since:", format!("{} ns since boot", nanos_since_boot)),
273                    );
274                }
275
276                table.add_row(row!(r->"Job ID:", job_id));
277
278                if let Some(process_id) = process_id {
279                    table.add_row(row!(r->"Process ID:", process_id));
280                }
281            }
282            Runtime::Unknown => {
283                table.add_row(row!(r->"Runtime:", "Unknown"));
284            }
285        }
286    } else {
287        table.add_row(row!(r->"Execution State:", colorized("Stopped", Colour::Red, with_style)));
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::test_utils::*;
295    use fidl_fuchsia_component_decl as fdecl;
296    use std::collections::HashMap;
297    use std::fs;
298    use tempfile::TempDir;
299
300    pub fn create_pkg_dir() -> TempDir {
301        let temp_dir = TempDir::new_in("/tmp").unwrap();
302        let root = temp_dir.path();
303
304        fs::write(root.join("meta"), "1234").unwrap();
305
306        temp_dir
307    }
308
309    pub fn create_out_dir() -> TempDir {
310        let temp_dir = TempDir::new_in("/tmp").unwrap();
311        let root = temp_dir.path();
312
313        fs::create_dir(root.join("diagnostics")).unwrap();
314
315        temp_dir
316    }
317
318    pub fn create_runtime_dir() -> TempDir {
319        let temp_dir = TempDir::new_in("/tmp").unwrap();
320        let root = temp_dir.path();
321
322        fs::create_dir_all(root.join("elf")).unwrap();
323        fs::write(root.join("elf/job_id"), "1234").unwrap();
324        fs::write(root.join("elf/process_id"), "2345").unwrap();
325        fs::write(root.join("elf/process_start_time"), "3456").unwrap();
326        fs::write(root.join("elf/process_start_time_utc_estimate"), "abcd").unwrap();
327
328        temp_dir
329    }
330
331    fn create_query() -> fsys::RealmQueryProxy {
332        // Serve RealmQuery for CML components.
333        let out_dir = create_out_dir();
334        let pkg_dir = create_pkg_dir();
335        let runtime_dir = create_runtime_dir();
336
337        let query = serve_realm_query(
338            vec![
339                fsys::Instance {
340                    moniker: Some("./my_foo".to_string()),
341                    url: Some("fuchsia-pkg://fuchsia.com/foo#meta/foo.cm".to_string()),
342                    instance_id: Some("1234567890".to_string()),
343                    resolved_info: Some(fsys::ResolvedInfo {
344                        resolved_url: Some("fuchsia-pkg://fuchsia.com/foo#meta/foo.cm".to_string()),
345                        execution_info: Some(fsys::ExecutionInfo {
346                            start_reason: Some("Debugging Workflow".to_string()),
347                            ..Default::default()
348                        }),
349                        ..Default::default()
350                    }),
351                    ..Default::default()
352                },
353                fsys::Instance {
354                    moniker: Some("./core/appmgr".to_string()),
355                    url: Some("fuchsia-pkg://fuchsia.com/appmgr#meta/appmgr.cm".to_string()),
356                    instance_id: None,
357                    resolved_info: Some(fsys::ResolvedInfo {
358                        resolved_url: Some(
359                            "fuchsia-pkg://fuchsia.com/appmgr#meta/appmgr.cm".to_string(),
360                        ),
361                        execution_info: Some(fsys::ExecutionInfo {
362                            start_reason: Some("Debugging Workflow".to_string()),
363                            ..Default::default()
364                        }),
365                        ..Default::default()
366                    }),
367                    ..Default::default()
368                },
369            ],
370            HashMap::from([(
371                "./my_foo".to_string(),
372                fdecl::Component {
373                    uses: Some(vec![
374                        fdecl::Use::Protocol(fdecl::UseProtocol {
375                            source: Some(fdecl::Ref::Parent(fdecl::ParentRef)),
376                            source_name: Some("fuchsia.foo.bar".to_string()),
377                            target_path: Some("/svc/fuchsia.foo.bar".to_string()),
378                            dependency_type: Some(fdecl::DependencyType::Strong),
379                            availability: Some(fdecl::Availability::Required),
380                            ..Default::default()
381                        }),
382                        fdecl::Use::Runner(fdecl::UseRunner {
383                            source: Some(fdecl::Ref::Parent(fdecl::ParentRef)),
384                            source_name: Some("elf".to_string()),
385                            ..Default::default()
386                        }),
387                    ]),
388                    exposes: Some(vec![fdecl::Expose::Protocol(fdecl::ExposeProtocol {
389                        source: Some(fdecl::Ref::Self_(fdecl::SelfRef)),
390                        source_name: Some("fuchsia.bar.baz".to_string()),
391                        target: Some(fdecl::Ref::Parent(fdecl::ParentRef)),
392                        target_name: Some("fuchsia.bar.baz".to_string()),
393                        ..Default::default()
394                    })]),
395                    capabilities: Some(vec![fdecl::Capability::Protocol(fdecl::Protocol {
396                        name: Some("fuchsia.bar.baz".to_string()),
397                        source_path: Some("/svc/fuchsia.bar.baz".to_string()),
398                        ..Default::default()
399                    })]),
400                    collections: Some(vec![fdecl::Collection {
401                        name: Some("my-collection".to_string()),
402                        durability: Some(fdecl::Durability::Transient),
403                        ..Default::default()
404                    }]),
405                    ..Default::default()
406                },
407            )]),
408            HashMap::from([(
409                "./my_foo".to_string(),
410                fdecl::ResolvedConfig {
411                    fields: vec![fdecl::ResolvedConfigField {
412                        key: "foo".to_string(),
413                        value: fdecl::ConfigValue::Single(fdecl::ConfigSingleValue::Bool(false)),
414                    }],
415                    checksum: fdecl::ConfigChecksum::Sha256([0; 32]),
416                },
417            )]),
418            HashMap::from([
419                (("./my_foo".to_string(), fsys::OpenDirType::RuntimeDir), runtime_dir),
420                (("./my_foo".to_string(), fsys::OpenDirType::PackageDir), pkg_dir),
421                (("./my_foo".to_string(), fsys::OpenDirType::OutgoingDir), out_dir),
422            ]),
423        );
424        query
425    }
426
427    #[fuchsia::test]
428    async fn basic_cml() {
429        let query = create_query();
430
431        let instance = get_instance_by_query("foo.cm".to_string(), query).await.unwrap();
432
433        assert_eq!(instance.moniker, Moniker::parse_str("/my_foo").unwrap());
434        assert_eq!(instance.url, "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm");
435        assert_eq!(instance.instance_id.unwrap(), "1234567890");
436        assert!(instance.resolved.is_some());
437
438        let resolved = instance.resolved.unwrap();
439        assert_eq!(resolved.runner.unwrap(), "elf");
440        assert_eq!(resolved.incoming_capabilities.len(), 1);
441        assert_eq!(resolved.incoming_capabilities[0], "/svc/fuchsia.foo.bar");
442
443        assert_eq!(resolved.exposed_capabilities.len(), 1);
444        assert_eq!(resolved.exposed_capabilities[0], "fuchsia.bar.baz");
445
446        assert_eq!(resolved.merkle_root.unwrap(), "1234");
447
448        let config = resolved.config.unwrap();
449        assert_eq!(
450            config,
451            vec![ConfigField { key: "foo".to_string(), value: "Bool(false)".to_string() }]
452        );
453
454        assert_eq!(resolved.collections, vec!["my-collection"]);
455
456        let started = resolved.started.unwrap();
457        assert_eq!(started.outgoing_capabilities, vec!["diagnostics".to_string()]);
458        assert_eq!(started.start_reason, "Debugging Workflow".to_string());
459
460        match started.runtime {
461            Runtime::Elf {
462                job_id,
463                process_id,
464                process_start_time,
465                process_start_time_utc_estimate,
466            } => {
467                assert_eq!(job_id, 1234);
468                assert_eq!(process_id, Some(2345));
469                assert_eq!(process_start_time, Some(3456));
470                assert_eq!(process_start_time_utc_estimate, Some("abcd".to_string()));
471            }
472            _ => panic!("unexpected runtime"),
473        }
474    }
475
476    #[fuchsia::test]
477    async fn find_by_moniker() {
478        let query = create_query();
479
480        let instance = get_instance_by_query("my_foo".to_string(), query).await.unwrap();
481
482        assert_eq!(instance.moniker, Moniker::parse_str("/my_foo").unwrap());
483        assert_eq!(instance.url, "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm");
484        assert_eq!(instance.instance_id.unwrap(), "1234567890");
485    }
486
487    #[fuchsia::test]
488    async fn find_by_instance_id() {
489        let query = create_query();
490
491        let instance = get_instance_by_query("1234567".to_string(), query).await.unwrap();
492
493        assert_eq!(instance.moniker, Moniker::parse_str("/my_foo").unwrap());
494        assert_eq!(instance.url, "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm");
495        assert_eq!(instance.instance_id.unwrap(), "1234567890");
496    }
497}