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