component_debug/
doctor.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::route::DictionaryEntry;
6use anyhow::{format_err, Result};
7use flex_fuchsia_sys2 as fsys;
8use moniker::Moniker;
9use prettytable::format::consts::FORMAT_CLEAN;
10use prettytable::{cell, row, Row, Table};
11
12const USE_TITLE: &'static str = "Used Capability";
13const EXPOSE_TITLE: &'static str = "Exposed Capability";
14const SUCCESS_SUMMARY: &'static str = "Success";
15const CAPABILITY_COLUMN_WIDTH: usize = 50;
16const SUMMARY_COLUMN_WIDTH: usize = 80;
17
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20
21// Analytical information about a capability.
22#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
23#[derive(Debug)]
24pub struct RouteReport {
25    pub decl_type: DeclType,
26
27    /// The name of the capability (for DeclType::Expose), or the path of
28    /// the capability in the namespace (for DeclType::Use).
29    pub capability: String,
30
31    /// If Some, indicates a routing error for this route.
32    pub error_summary: Option<String>,
33
34    /// The requested level of availability of the capability.
35    pub availability: Option<cm_rust::Availability>,
36
37    /// The contents of the dictionary, if the capability was a dictionary.
38    pub dictionary_entries: Option<Vec<DictionaryEntry>>,
39}
40
41impl TryFrom<fsys::RouteReport> for RouteReport {
42    type Error = anyhow::Error;
43
44    fn try_from(report: fsys::RouteReport) -> Result<Self> {
45        let decl_type =
46            report.decl_type.ok_or_else(|| format_err!("missing decl type"))?.try_into()?;
47        let capability = report.capability.ok_or_else(|| format_err!("missing capability name"))?;
48        let availability: Option<cm_rust::Availability> =
49            report.availability.map(cm_rust::Availability::from);
50        let dictionary_entries = report
51            .dictionary_entries
52            .map(|e| e.into_iter().map(DictionaryEntry::try_from).collect())
53            .transpose()?;
54        let error_summary = if let Some(error) = report.error { error.summary } else { None };
55        Ok(RouteReport { decl_type, capability, error_summary, availability, dictionary_entries })
56    }
57}
58
59#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
60#[derive(Debug, PartialEq)]
61pub enum DeclType {
62    Use,
63    Expose,
64}
65
66impl TryFrom<fsys::DeclType> for DeclType {
67    type Error = anyhow::Error;
68
69    fn try_from(value: fsys::DeclType) -> std::result::Result<Self, Self::Error> {
70        match value {
71            fsys::DeclType::Use => Ok(DeclType::Use),
72            fsys::DeclType::Expose => Ok(DeclType::Expose),
73            _ => Err(format_err!("unknown decl type")),
74        }
75    }
76}
77
78/// Returns a list of individual RouteReports for use and expose declarations
79/// for the component. Any individual report with `error_summary` set to Some()
80/// indicates a routing error.
81pub async fn validate_routes(
82    route_validator: &fsys::RouteValidatorProxy,
83    moniker: &Moniker,
84) -> Result<Vec<RouteReport>> {
85    let reports = match route_validator.validate(&moniker.to_string()).await? {
86        Ok(reports) => reports,
87        Err(e) => {
88            return Err(format_err!(
89                "Component manager returned an unexpected error during validation: {:?}\n\
90                 The state of the component instance may have changed.\n\
91                 Please report this to the Component Framework team.",
92                e
93            ));
94        }
95    };
96
97    reports.into_iter().map(|r| r.try_into()).collect()
98}
99
100fn format(report: &RouteReport) -> Vec<Row> {
101    let capability = match report.dictionary_entries {
102        Some(_) => format!("{} (Dictionary)", report.capability),
103        None => report.capability.clone(),
104    };
105    let capability = match report.availability {
106        Some(cm_rust::Availability::Required) | None => capability,
107        Some(availability) => format!("{} ({})", capability, availability),
108    };
109    let capability = textwrap::fill(&capability, CAPABILITY_COLUMN_WIDTH);
110    let (mark, summary) = if let Some(summary) = &report.error_summary {
111        let mark = ansi_term::Color::Red.paint("[✗]");
112        let summary = textwrap::fill(summary, SUMMARY_COLUMN_WIDTH);
113        (mark, summary)
114    } else {
115        let mark = ansi_term::Color::Green.paint("[✓]");
116        let summary = textwrap::fill(SUCCESS_SUMMARY, SUMMARY_COLUMN_WIDTH);
117        (mark, summary)
118    };
119    let mut rows = vec![row!(mark, capability, summary)];
120    if let Some(dictionary_entries) = &report.dictionary_entries {
121        let mut table = Table::new();
122        let mut format = *FORMAT_CLEAN;
123        format.padding(0, 0);
124        table.set_format(format);
125        for e in dictionary_entries {
126            table.add_row(row!(&e.name));
127        }
128        rows.push(row!("", table))
129    }
130    rows
131}
132
133// Construct the used and exposed capability tables from the given route reports.
134pub fn create_tables(reports: &Vec<RouteReport>) -> (Table, Table) {
135    let mut use_table = new_table(USE_TITLE);
136    let mut expose_table = new_table(EXPOSE_TITLE);
137
138    for report in reports {
139        match &report.decl_type {
140            DeclType::Use => {
141                for r in format(&report) {
142                    use_table.add_row(r);
143                }
144            }
145            DeclType::Expose => {
146                for r in format(&report) {
147                    expose_table.add_row(r);
148                }
149            }
150        };
151    }
152    (use_table, expose_table)
153}
154
155// Create a new table with the given title.
156fn new_table(title: &str) -> Table {
157    let mut table = Table::new();
158    table.set_format(*FORMAT_CLEAN);
159    table.set_titles(row!("", title.to_string(), "Result"));
160    table
161}
162
163#[cfg(test)]
164mod test {
165    use super::*;
166    use fidl::endpoints::create_proxy_and_stream;
167    use futures::TryStreamExt;
168
169    fn route_validator(
170        expected_moniker: &'static str,
171        reports: Vec<fsys::RouteReport>,
172    ) -> fsys::RouteValidatorProxy {
173        let (route_validator, mut stream) = create_proxy_and_stream::<fsys::RouteValidatorMarker>();
174        fuchsia_async::Task::local(async move {
175            match stream.try_next().await.unwrap().unwrap() {
176                fsys::RouteValidatorRequest::Validate { moniker, responder, .. } => {
177                    assert_eq!(Moniker::parse_str(expected_moniker), Moniker::parse_str(&moniker));
178                    responder.send(Ok(&reports)).unwrap();
179                }
180                fsys::RouteValidatorRequest::Route { .. } => {
181                    panic!("unexpected Route request");
182                }
183            }
184        })
185        .detach();
186        route_validator
187    }
188
189    #[fuchsia_async::run_singlethreaded(test)]
190    async fn test_errors() {
191        let validator = route_validator(
192            "/test",
193            vec![fsys::RouteReport {
194                capability: Some("fuchsia.foo.bar".to_string()),
195                decl_type: Some(fsys::DeclType::Use),
196                error: Some(fsys::RouteError {
197                    summary: Some("Access denied".to_string()),
198                    ..Default::default()
199                }),
200                ..Default::default()
201            }],
202        );
203
204        let mut reports =
205            validate_routes(&validator, &Moniker::parse_str("test").unwrap()).await.unwrap();
206        assert_eq!(reports.len(), 1);
207
208        let report = reports.remove(0);
209        assert_eq!(report.capability, "fuchsia.foo.bar");
210        assert_eq!(report.decl_type, DeclType::Use);
211
212        let error = report.error_summary.unwrap();
213        assert_eq!(error, "Access denied");
214    }
215
216    #[fuchsia_async::run_singlethreaded(test)]
217    async fn test_no_errors() {
218        let validator = route_validator(
219            "/test",
220            vec![fsys::RouteReport {
221                capability: Some("fuchsia.foo.bar".to_string()),
222                decl_type: Some(fsys::DeclType::Use),
223                dictionary_entries: Some(vec![fsys::DictionaryEntry {
224                    name: Some("k1".into()),
225                    ..Default::default()
226                }]),
227                error: None,
228                ..Default::default()
229            }],
230        );
231
232        let mut reports =
233            validate_routes(&validator, &Moniker::parse_str("test").unwrap()).await.unwrap();
234        assert_eq!(reports.len(), 1);
235
236        let report = reports.remove(0);
237        assert_eq!(report.capability, "fuchsia.foo.bar");
238        assert_eq!(report.decl_type, DeclType::Use);
239        assert_eq!(report.dictionary_entries.unwrap(), [DictionaryEntry { name: "k1".into() }]);
240        assert!(report.error_summary.is_none());
241    }
242
243    #[fuchsia_async::run_singlethreaded(test)]
244    async fn test_no_routes() {
245        let validator = route_validator("test", vec![]);
246
247        let reports =
248            validate_routes(&validator, &Moniker::parse_str("test").unwrap()).await.unwrap();
249        assert!(reports.is_empty());
250    }
251
252    #[fuchsia_async::run_singlethreaded(test)]
253    async fn test_parse_error() {
254        let validator = route_validator(
255            "/test",
256            vec![
257                // Don't set any fields
258                fsys::RouteReport::default(),
259            ],
260        );
261
262        let result = validate_routes(&validator, &Moniker::parse_str("test").unwrap()).await;
263        assert!(result.is_err());
264    }
265}