1pub mod args;
6
7use anyhow::{Result, format_err};
8use args::DumpCommand;
9use flex_fuchsia_driver_development as fdd;
10use fuchsia_driver_dev::Device;
11#[cfg(feature = "fdomain")]
12use fuchsia_driver_dev_fdomain as fuchsia_driver_dev;
13use safe_string::DotSafe;
14use std::collections::{BTreeMap, VecDeque};
15use std::io::Write;
16
17const INDENT_SIZE: usize = 2;
18
19trait NodeInfoPrinter {
20 fn print(&self, writer: &mut dyn Write, indent_level: usize) -> Result<()>;
21
22 fn print_graph_node(&self, writer: &mut dyn Write) -> Result<()>;
23 fn print_graph_edge(&self, writer: &mut dyn Write, child: &fdd::NodeInfo) -> Result<()>;
24}
25
26impl NodeInfoPrinter for Device {
27 fn print(&self, writer: &mut dyn Write, indent_level: usize) -> Result<()> {
28 let koid_str = match &self.0.driver_host_koid {
29 Some(koid) => format!("{}", koid),
30 None => format!("None"),
31 };
32
33 writeln!(
34 writer,
35 "{:indent$}[{}] pid={} {}",
36 "",
37 self.extract_name()?,
38 koid_str,
39 self.0.bound_driver_url.as_deref().unwrap_or(""),
40 indent = indent_level * INDENT_SIZE,
41 )?;
42 Ok(())
43 }
44
45 fn print_graph_node(&self, writer: &mut dyn Write) -> Result<()> {
46 writeln!(
47 writer,
48 " \"{}\" [label=\"{}\"]",
49 self.0.id.as_ref().ok_or_else(|| format_err!("Node missing id"))?,
50 DotSafe::from_str_lossy(self.extract_name()?),
51 )?;
52 Ok(())
53 }
54
55 fn print_graph_edge(&self, writer: &mut dyn Write, child: &fdd::NodeInfo) -> Result<()> {
56 writeln!(
57 writer,
58 " \"{}\" -> \"{}\"",
59 self.0.id.as_ref().ok_or_else(|| format_err!("Node missing id"))?,
60 child.id.as_ref().ok_or_else(|| format_err!("Child node missing id"))?
61 )?;
62 Ok(())
63 }
64}
65
66fn print_tree(
67 writer: &mut dyn Write,
68 root: &Device,
69 device_map: &BTreeMap<u64, &Device>,
70) -> Result<()> {
71 let mut stack = VecDeque::new();
72 stack.push_front((root, 0));
73 while let Some((device, indent_level)) = stack.pop_front() {
74 device.print(writer, indent_level)?;
75 if let Some(child_ids) = &device.0.child_ids {
76 for id in child_ids.iter().rev() {
77 if let Some(child) = device_map.get(id) {
78 stack.push_front((child, indent_level + 1));
79 }
80 }
81 }
82 }
83 Ok(())
84}
85
86pub async fn dump(
87 cmd: DumpCommand,
88 writer: &mut dyn Write,
89 driver_development_proxy: fdd::ManagerProxy,
90) -> Result<()> {
91 let devices: Vec<Device> = fuchsia_driver_dev::get_device_info(
92 &driver_development_proxy,
93 &[],
94 false,
95 )
96 .await?
97 .into_iter()
98 .map(|device| device.into())
99 .collect();
100
101 let device_map = devices
102 .iter()
103 .map(|device| {
104 if let Some(id) = device.0.id {
105 Ok((id, device))
106 } else {
107 Err(format_err!("Missing device id"))
108 }
109 })
110 .collect::<Result<BTreeMap<_, _>>>()?;
111
112 if cmd.graph {
113 let digraph_prefix = r#"digraph {
114 forcelabels = true; splines="ortho"; ranksep = 1.2; nodesep = 0.5;
115 node [ shape = "box" color = " #2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 margin = 0.22 ];
116 edge [ color = " #37474f" penwidth = 1 style = dashed fontname = "roboto mono" fontsize = 10 ];"#;
117 writeln!(writer, "{}", digraph_prefix)?;
118 for device in devices.iter() {
119 device.print_graph_node(writer)?;
120 }
121
122 for device in devices.iter() {
123 if let Some(child_ids) = &device.0.child_ids {
124 for id in child_ids.iter().rev() {
125 let child = &device_map[&id];
126 device.print_graph_edge(writer, &child.0)?;
127 }
128 }
129 }
130
131 writeln!(writer, "}}")?;
132 } else {
133 let roots = devices.iter().filter(|device| {
134 if let Some(node_filter) = &cmd.device {
135 let name = device.extract_name().unwrap_or("");
136 name == node_filter
137 } else {
138 if let Some(parent_ids) = device.0.parent_ids.as_ref() {
139 for parent_id in parent_ids.iter() {
140 if device_map.contains_key(parent_id) {
141 return false;
142 }
143 }
144 true
145 } else {
146 true
147 }
148 }
149 });
150
151 for root in roots {
152 print_tree(writer, root, &device_map)?;
153 }
154 }
155 Ok(())
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use anyhow::Context;
162 use argh::FromArgs;
163 use flex_client::fidl::ServerEnd;
164 use fuchsia_async as fasync;
165 use futures::future::{Future, FutureExt};
166 use futures::stream::StreamExt;
167 #[cfg(feature = "fdomain")]
168 use std::sync::Arc;
169
170 async fn test_dump<F, Fut>(
171 #[cfg(feature = "fdomain")] client: Arc<flex_client::Client>,
172 cmd: DumpCommand,
173 on_driver_development_request: F,
174 ) -> Result<String>
175 where
176 F: Fn(fdd::ManagerRequest) -> Fut + Send + Sync + 'static,
177 Fut: Future<Output = Result<()>> + Send + Sync,
178 {
179 #[cfg(not(feature = "fdomain"))]
180 let client = flex_client::fidl::ZirconClient;
181 let (driver_development_proxy, mut driver_development_requests) =
182 client.create_proxy_and_stream::<fdd::ManagerMarker>();
183
184 let mut writer = Vec::new();
186 let request_handler_task = fasync::Task::spawn(async move {
187 while let Some(res) = driver_development_requests.next().await {
188 let request = res.context("Failed to get next request")?;
189 on_driver_development_request(request).await.context("Failed to handle request")?;
190 }
191 anyhow::bail!("Driver development request stream unexpectedly closed");
192 });
193 futures::select! {
194 res = request_handler_task.fuse() => {
195 res?;
196 anyhow::bail!("Request handler task unexpectedly finished");
197 }
198 res = dump(cmd, &mut writer, driver_development_proxy).fuse() => res.context("Dump command failed")?,
199 }
200
201 String::from_utf8(writer).context("Failed to convert dump output to a string")
202 }
203
204 async fn run_device_info_iterator_server(
205 mut device_infos: Vec<fdd::NodeInfo>,
206 iterator: ServerEnd<fdd::NodeInfoIteratorMarker>,
207 ) -> Result<()> {
208 let mut iterator = iterator.into_stream();
209 while let Some(res) = iterator.next().await {
210 let request = res.context("Failed to get request")?;
211 match request {
212 fdd::NodeInfoIteratorRequest::GetNext { responder } => {
213 responder
214 .send(&device_infos)
215 .context("Failed to send device infos to responder")?;
216 device_infos.clear();
217 }
218 }
219 }
220 Ok(())
221 }
222
223 #[fuchsia::test]
224 async fn test_simple() {
225 #[cfg(feature = "fdomain")]
226 let client = fdomain_local::local_client_empty();
227 let cmd = DumpCommand::from_args(&["dump"], &[]).unwrap();
228
229 let output = test_dump(
230 #[cfg(feature = "fdomain")]
231 Arc::clone(&client),
232 cmd,
233 |request: fdd::ManagerRequest| async move {
234 match request {
235 fdd::ManagerRequest::GetNodeInfo {
236 node_filter: _,
237 iterator,
238 control_handle: _,
239 exact_match: _,
240 } => {
241 let parent_id = 0;
242 let child_id = 1;
243 run_device_info_iterator_server(
244 vec![
245 fdd::NodeInfo {
246 id: Some(parent_id),
247 parent_ids: Some(Vec::new()),
248 child_ids: Some(vec![child_id]),
249 driver_host_koid: Some(0),
250 bound_driver_url: Some(String::from(
251 "fuchsia-pkg://fuchsia.com/foo-package#meta/foo.cm",
252 )),
253 moniker: Some(String::from("foo")),
254 ..Default::default()
255 },
256 fdd::NodeInfo {
257 id: Some(child_id),
258 parent_ids: Some(vec![parent_id]),
259 child_ids: Some(Vec::new()),
260 driver_host_koid: Some(0),
261 bound_driver_url: Some(String::from(
262 "fuchsia-pkg://fuchsia.com/bar-package#meta/bar.cm",
263 )),
264 moniker: Some(String::from("foo.bar")),
265 ..Default::default()
266 },
267 ],
268 iterator,
269 )
270 .await
271 .context("Failed to run device info iterator server")?;
272 }
273 _ => {}
274 }
275 Ok(())
276 },
277 )
278 .await
279 .unwrap();
280
281 assert_eq!(
282 output,
283 r#"[foo] pid=0 fuchsia-pkg://fuchsia.com/foo-package#meta/foo.cm
284 [bar] pid=0 fuchsia-pkg://fuchsia.com/bar-package#meta/bar.cm
285"#
286 );
287 }
288
289 #[fuchsia::test]
290 async fn test_duplicates_are_filtered() {
291 #[cfg(feature = "fdomain")]
292 let client = fdomain_local::local_client_empty();
293 let cmd = DumpCommand::from_args(&["dump"], &[]).unwrap();
294
295 let output = test_dump(
296 #[cfg(feature = "fdomain")]
297 Arc::clone(&client),
298 cmd,
299 |request: fdd::ManagerRequest| async move {
300 match request {
301 fdd::ManagerRequest::GetNodeInfo {
302 node_filter: _,
303 iterator,
304 control_handle: _,
305 exact_match: _,
306 } => {
307 run_device_info_iterator_server(make_test_devices(), iterator)
308 .await
309 .context("Failed to run device info iterator server")?;
310 }
311 _ => {}
312 }
313 Ok(())
314 },
315 )
316 .await
317 .unwrap();
318
319 assert_eq!(
320 output,
321 r#"[platform] pid=0 fuchsia-pkg://fuchsia.com/root-package#meta/root.cm
322 [parent] pid=0 fuchsia-pkg://fuchsia.com/parent-package#meta/parent.cm
323 [child] pid=0 fuchsia-pkg://fuchsia.com/child-package#meta/child.cm
324[parent] pid=0 fuchsia-pkg://fuchsia.com/parent-package#meta/parent.cm
325 [child] pid=0 fuchsia-pkg://fuchsia.com/child-package#meta/child.cm
326"#
327 );
328 }
329
330 #[fuchsia::test]
331 async fn test_with_node_filter() {
332 #[cfg(feature = "fdomain")]
333 let client = fdomain_local::local_client_empty();
334 let cmd = DumpCommand::from_args(&["dump"], &["parent"]).unwrap();
335
336 let output = test_dump(
337 #[cfg(feature = "fdomain")]
338 Arc::clone(&client),
339 cmd,
340 |request: fdd::ManagerRequest| async move {
341 match request {
342 fdd::ManagerRequest::GetNodeInfo {
343 node_filter: _,
344 iterator,
345 control_handle: _,
346 exact_match: _,
347 } => {
348 run_device_info_iterator_server(make_test_devices(), iterator)
349 .await
350 .context("Failed to run device info iterator server")?;
351 }
352 _ => {}
353 }
354 Ok(())
355 },
356 )
357 .await
358 .unwrap();
359
360 assert_eq!(
361 output,
362 r#"[parent] pid=0 fuchsia-pkg://fuchsia.com/parent-package#meta/parent.cm
363 [child] pid=0 fuchsia-pkg://fuchsia.com/child-package#meta/child.cm
364"#
365 );
366 }
367
368 fn make_test_devices() -> Vec<fdd::NodeInfo> {
369 let null_id = 0;
370 let root_id = 1;
371 let composite_parent_id = 2;
372 let composite_child_id = 3;
373 vec![
374 fdd::NodeInfo {
376 id: Some(root_id),
377 parent_ids: Some(vec![null_id]),
378 child_ids: Some(vec![composite_parent_id]),
379 driver_host_koid: Some(0),
380 bound_driver_url: Some(String::from(
381 "fuchsia-pkg://fuchsia.com/root-package#meta/root.cm",
382 )),
383 moniker: Some(String::from("sys.platform")),
384 ..Default::default()
385 },
386 fdd::NodeInfo {
388 id: Some(composite_parent_id),
389 parent_ids: None,
390 child_ids: Some(vec![composite_child_id]),
391 driver_host_koid: Some(0),
392 bound_driver_url: Some(String::from(
393 "fuchsia-pkg://fuchsia.com/parent-package#meta/parent.cm",
394 )),
395 moniker: Some(String::from("parent")),
396 ..Default::default()
397 },
398 fdd::NodeInfo {
400 id: Some(composite_child_id),
401 parent_ids: Some(vec![composite_parent_id]),
402 child_ids: Some(Vec::new()),
403 driver_host_koid: Some(0),
404 bound_driver_url: Some(String::from(
405 "fuchsia-pkg://fuchsia.com/child-package#meta/child.cm",
406 )),
407 moniker: Some(String::from("parent.child")),
408 ..Default::default()
409 },
410 ]
411 }
412
413 #[fuchsia::test]
414 async fn test_graph_simple() {
415 #[cfg(feature = "fdomain")]
416 let client = fdomain_local::local_client_empty();
417 let cmd = DumpCommand::from_args(&["dump"], &["--graph"]).unwrap();
418
419 let output = test_dump(
420 #[cfg(feature = "fdomain")]
421 Arc::clone(&client),
422 cmd,
423 |request: fdd::ManagerRequest| async move {
424 match request {
425 fdd::ManagerRequest::GetNodeInfo {
426 node_filter: _,
427 iterator,
428 control_handle: _,
429 exact_match: _,
430 } => {
431 let parent_id = 0;
432 let child_id = 1;
433 run_device_info_iterator_server(
434 vec![
435 fdd::NodeInfo {
436 id: Some(parent_id),
437 parent_ids: Some(Vec::new()),
438 child_ids: Some(vec![child_id]),
439 driver_host_koid: Some(0),
440 bound_driver_url: Some(String::from(
441 "fuchsia-pkg://fuchsia.com/foo-package#meta/foo.cm",
442 )),
443 moniker: Some(String::from("sys.foo")),
444 ..Default::default()
445 },
446 fdd::NodeInfo {
447 id: Some(child_id),
448 parent_ids: Some(vec![parent_id]),
449 child_ids: Some(Vec::new()),
450 driver_host_koid: Some(0),
451 bound_driver_url: Some(String::from(
452 "fuchsia-pkg://fuchsia.com/bar-package#meta/bar.cm",
453 )),
454 moniker: Some(String::from("sys.foo.bar")),
455 ..Default::default()
456 },
457 ],
458 iterator,
459 )
460 .await
461 .context("Failed to run device info iterator server")?;
462 }
463 _ => {}
464 }
465 Ok(())
466 },
467 )
468 .await
469 .unwrap();
470
471 let expected = r#"digraph {
472 forcelabels = true; splines="ortho"; ranksep = 1.2; nodesep = 0.5;
473 node [ shape = "box" color = " #2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 margin = 0.22 ];
474 edge [ color = " #37474f" penwidth = 1 style = dashed fontname = "roboto mono" fontsize = 10 ];
475 "0" [label="foo"]
476 "1" [label="bar"]
477 "0" -> "1"
478}
479"#;
480 assert_eq!(output, expected);
481 }
482
483 #[fuchsia::test]
484 async fn test_graph_escaping_quotes_and_backslashes() {
485 #[cfg(feature = "fdomain")]
486 let client = fdomain_local::local_client_empty();
487 let cmd = DumpCommand::from_args(&["dump"], &["--graph"]).unwrap();
488
489 let output = test_dump(
490 #[cfg(feature = "fdomain")]
491 Arc::clone(&client),
492 cmd,
493 |request: fdd::ManagerRequest| async move {
494 match request {
495 fdd::ManagerRequest::GetNodeInfo {
496 node_filter: _,
497 iterator,
498 control_handle: _,
499 exact_match: _,
500 } => {
501 run_device_info_iterator_server(
502 vec![
503 fdd::NodeInfo {
504 id: Some(0),
505 parent_ids: Some(Vec::new()),
506 child_ids: Some(Vec::new()),
507 moniker: Some(String::from(r#"sys.quote"node"#)),
508 ..Default::default()
509 },
510 fdd::NodeInfo {
511 id: Some(1),
512 parent_ids: Some(Vec::new()),
513 child_ids: Some(Vec::new()),
514 moniker: Some(String::from(r#"sys.path\to\device"#)),
515 ..Default::default()
516 },
517 fdd::NodeInfo {
518 id: Some(2),
519 parent_ids: Some(Vec::new()),
520 child_ids: Some(Vec::new()),
521 moniker: Some(String::from(r#"sys.trailing\"#)),
522 ..Default::default()
523 },
524 ],
525 iterator,
526 )
527 .await
528 .context("Failed to run device info iterator server")?;
529 }
530 _ => {}
531 }
532 Ok(())
533 },
534 )
535 .await
536 .unwrap();
537
538 let expected = r#"digraph {
539 forcelabels = true; splines="ortho"; ranksep = 1.2; nodesep = 0.5;
540 node [ shape = "box" color = " #2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 margin = 0.22 ];
541 edge [ color = " #37474f" penwidth = 1 style = dashed fontname = "roboto mono" fontsize = 10 ];
542 "0" [label="quote\"node"]
543 "1" [label="path\\to\\device"]
544 "2" [label="trailing\\"]
545}
546"#;
547 assert_eq!(output, expected);
548 }
549
550 #[fuchsia::test]
551 async fn test_graph_escaping_newlines_and_control_chars() {
552 #[cfg(feature = "fdomain")]
553 let client = fdomain_local::local_client_empty();
554 let cmd = DumpCommand::from_args(&["dump"], &["--graph"]).unwrap();
555
556 let output = test_dump(
557 #[cfg(feature = "fdomain")]
558 Arc::clone(&client),
559 cmd,
560 |request: fdd::ManagerRequest| async move {
561 match request {
562 fdd::ManagerRequest::GetNodeInfo {
563 node_filter: _,
564 iterator,
565 control_handle: _,
566 exact_match: _,
567 } => {
568 run_device_info_iterator_server(
569 vec![
570 fdd::NodeInfo {
571 id: Some(0),
572 parent_ids: Some(Vec::new()),
573 child_ids: Some(Vec::new()),
574 moniker: Some(String::from("sys.multi\nline")),
575 ..Default::default()
576 },
577 fdd::NodeInfo {
578 id: Some(1),
579 parent_ids: Some(Vec::new()),
580 child_ids: Some(Vec::new()),
581 moniker: Some(String::from("sys.crlf\r\nline")),
582 ..Default::default()
583 },
584 fdd::NodeInfo {
585 id: Some(2),
586 parent_ids: Some(Vec::new()),
587 child_ids: Some(Vec::new()),
588 moniker: Some(String::from("sys.ansi\x1b[31mcolor\x07")),
589 ..Default::default()
590 },
591 ],
592 iterator,
593 )
594 .await
595 .context("Failed to run device info iterator server")?;
596 }
597 _ => {}
598 }
599 Ok(())
600 },
601 )
602 .await
603 .unwrap();
604
605 let expected = format!(
606 r#"digraph {{
607 forcelabels = true; splines="ortho"; ranksep = 1.2; nodesep = 0.5;
608 node [ shape = "box" color = " #2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 margin = 0.22 ];
609 edge [ color = " #37474f" penwidth = 1 style = dashed fontname = "roboto mono" fontsize = 10 ];
610 "0" [label="multi\nline"]
611 "1" [label="crlf\nline"]
612 "2" [label="ansi{0}[31mcolor{0}"]
613}}
614"#,
615 char::REPLACEMENT_CHARACTER
616 );
617 assert_eq!(output, expected);
618 }
619
620 #[fuchsia::test]
621 async fn test_graph_injection_payloads() {
622 #[cfg(feature = "fdomain")]
623 let client = fdomain_local::local_client_empty();
624 let cmd = DumpCommand::from_args(&["dump"], &["--graph"]).unwrap();
625
626 let output = test_dump(
627 #[cfg(feature = "fdomain")]
628 Arc::clone(&client),
629 cmd,
630 |request: fdd::ManagerRequest| async move {
631 match request {
632 fdd::ManagerRequest::GetNodeInfo {
633 node_filter: _,
634 iterator,
635 control_handle: _,
636 exact_match: _,
637 } => {
638 run_device_info_iterator_server(
639 vec![
640 fdd::NodeInfo {
641 id: Some(0),
642 parent_ids: Some(Vec::new()),
643 child_ids: Some(Vec::new()),
644 moniker: Some(String::from(
645 r#"sys.breakout" [color=red, style=filled]; evil_node [label="injected"]; "#,
646 )),
647 ..Default::default()
648 },
649 fdd::NodeInfo {
650 id: Some(1),
651 parent_ids: Some(Vec::new()),
652 child_ids: Some(Vec::new()),
653 moniker: Some(String::from(
654 r#"sys.graph_close"} subgraph evil { "pwned" [label="bad"] } //"#,
655 )),
656 ..Default::default()
657 },
658 ],
659 iterator,
660 )
661 .await
662 .context("Failed to run device info iterator server")?;
663 }
664 _ => {}
665 }
666 Ok(())
667 },
668 )
669 .await
670 .unwrap();
671
672 let expected = r#"digraph {
673 forcelabels = true; splines="ortho"; ranksep = 1.2; nodesep = 0.5;
674 node [ shape = "box" color = " #2a5b4f" penwidth = 2.25 fontname = "prompt medium" fontsize = 10 margin = 0.22 ];
675 edge [ color = " #37474f" penwidth = 1 style = dashed fontname = "roboto mono" fontsize = 10 ];
676 "0" [label="breakout\" [color=red, style=filled]; evil_node [label=\"injected\"]; "]
677 "1" [label="graph_close\"} subgraph evil { \"pwned\" [label=\"bad\"] } //"]
678}
679"#;
680 assert_eq!(output, expected);
681 }
682}