1use 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 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 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}