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