Skip to main content

driver_tools/subcommands/dump/
mod.rs

1// Copyright 2022 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
5pub 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        /* exact_match= */ 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        // Run the command and mock driver development server.
185        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            // Root device
375            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            // Composite parent
387            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            // Composite child
399            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}