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