Skip to main content

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