component_debug/
route.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 anyhow::{Result, format_err};
6use flex_fuchsia_sys2 as fsys;
7use moniker::{ExtendedMoniker, Moniker};
8use prettytable::format::consts::FORMAT_CLEAN;
9use prettytable::{Table, cell, row};
10use std::fmt;
11
12const SUCCESS_SUMMARY: &'static str = "Success";
13const VOID_SUMMARY: &'static str = "Routed from void";
14
15#[cfg(feature = "serde")]
16use serde::{Deserialize, Serialize};
17
18// Analytical information about a capability.
19#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
20#[derive(Debug)]
21pub struct RouteReport {
22    pub decl_type: DeclType,
23    pub capability: String,
24    pub error_summary: Option<String>,
25    pub source_moniker: Option<String>,
26    pub service_instances: Option<Vec<ServiceInstance>>,
27    pub dictionary_entries: Option<Vec<DictionaryEntry>>,
28    pub outcome: RouteOutcome,
29}
30
31#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
32#[derive(Debug, PartialEq)]
33pub struct ServiceInstance {
34    pub instance_name: String,
35    pub child_name: String,
36    pub child_instance_name: String,
37}
38
39impl TryFrom<fsys::ServiceInstance> for ServiceInstance {
40    type Error = anyhow::Error;
41
42    fn try_from(value: fsys::ServiceInstance) -> std::result::Result<Self, Self::Error> {
43        Ok(Self {
44            instance_name: value
45                .instance_name
46                .ok_or_else(|| format_err!("missing instance_name"))?,
47            child_name: value.child_name.ok_or_else(|| format_err!("missing child_name"))?,
48            child_instance_name: value
49                .child_instance_name
50                .ok_or_else(|| format_err!("missing child_instance_name"))?,
51        })
52    }
53}
54
55#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
56#[derive(Debug, PartialEq)]
57pub struct DictionaryEntry {
58    pub name: String,
59}
60
61impl TryFrom<fsys::DictionaryEntry> for DictionaryEntry {
62    type Error = anyhow::Error;
63
64    fn try_from(value: fsys::DictionaryEntry) -> std::result::Result<Self, Self::Error> {
65        Ok(Self { name: value.name.ok_or_else(|| format_err!("missing name"))? })
66    }
67}
68
69impl TryFrom<fsys::RouteReport> for RouteReport {
70    type Error = anyhow::Error;
71
72    fn try_from(report: fsys::RouteReport) -> Result<Self> {
73        let decl_type =
74            report.decl_type.ok_or_else(|| format_err!("missing decl type"))?.try_into()?;
75        let capability = report.capability.ok_or_else(|| format_err!("missing capability name"))?;
76        let error_summary = if let Some(error) = report.error { error.summary } else { None };
77        let source_moniker = report.source_moniker;
78        let service_instances = report
79            .service_instances
80            .map(|s| s.into_iter().map(|s| s.try_into()).collect())
81            .transpose()?;
82        let dictionary_entries = report
83            .dictionary_entries
84            .map(|s| s.into_iter().map(|s| s.try_into()).collect())
85            .transpose()?;
86        let outcome = match report.outcome {
87            Some(o) => o.try_into()?,
88            None => {
89                // Backward compatibility. `outcome` may be missing if the client (e.g., ffx)
90                // is built at a later version than the target.
91                if error_summary.is_some() { RouteOutcome::Failed } else { RouteOutcome::Success }
92            }
93        };
94        Ok(RouteReport {
95            decl_type,
96            capability,
97            error_summary,
98            source_moniker,
99            service_instances,
100            dictionary_entries,
101            outcome,
102        })
103    }
104}
105
106#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
107#[derive(Debug, PartialEq)]
108pub enum DeclType {
109    Use,
110    Expose,
111}
112
113impl fmt::Display for DeclType {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        let s = match self {
116            DeclType::Use => "use",
117            DeclType::Expose => "expose",
118        };
119        write!(f, "{}", s)
120    }
121}
122
123impl TryFrom<fsys::DeclType> for DeclType {
124    type Error = anyhow::Error;
125
126    fn try_from(value: fsys::DeclType) -> std::result::Result<Self, Self::Error> {
127        match value {
128            fsys::DeclType::Use => Ok(DeclType::Use),
129            fsys::DeclType::Expose => Ok(DeclType::Expose),
130            _ => Err(format_err!("unknown decl type")),
131        }
132    }
133}
134
135#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
136#[derive(Debug, PartialEq)]
137pub enum RouteOutcome {
138    Success,
139    Void,
140    Failed,
141}
142
143impl TryFrom<fsys::RouteOutcome> for RouteOutcome {
144    type Error = anyhow::Error;
145
146    fn try_from(value: fsys::RouteOutcome) -> std::result::Result<Self, Self::Error> {
147        match value {
148            fsys::RouteOutcome::Success => Ok(RouteOutcome::Success),
149            fsys::RouteOutcome::Void => Ok(RouteOutcome::Void),
150            fsys::RouteOutcome::Failed => Ok(RouteOutcome::Failed),
151            _ => Err(format_err!("unknown route outcome")),
152        }
153    }
154}
155
156/// Call `RouteValidator/Route` with `moniker` and `targets`.
157pub async fn route(
158    route_validator: &fsys::RouteValidatorProxy,
159    moniker: Moniker,
160    targets: Vec<fsys::RouteTarget>,
161) -> Result<Vec<RouteReport>> {
162    let reports = match route_validator.route(moniker.as_ref(), &targets).await? {
163        Ok(reports) => reports,
164        Err(e) => {
165            return Err(format_err!(
166                "Component manager returned an unexpected error during routing: {:?}\n\
167                 The state of the component instance may have changed.\n\
168                 Please report this to the Component Framework team.",
169                e
170            ));
171        }
172    };
173
174    reports.into_iter().map(|r| r.try_into()).collect()
175}
176
177/// Construct a table of routes from the given route reports.
178pub fn create_table(reports: Vec<RouteReport>) -> Table {
179    let mut table = Table::new();
180    table.set_format(*FORMAT_CLEAN);
181
182    let mut first = true;
183    for report in reports {
184        if first {
185            first = false;
186        } else {
187            table.add_empty_row();
188        }
189        add_report(report, &mut table);
190    }
191    table
192}
193
194fn add_report(report: RouteReport, table: &mut Table) {
195    table
196        .add_row(row!(r->"Capability: ", &format!("{} ({})", report.capability, report.decl_type)));
197    let (mark, summary) = match report.outcome {
198        RouteOutcome::Success => {
199            let mark = ansi_term::Color::Green.paint("[✓]");
200            (mark, SUCCESS_SUMMARY)
201        }
202        RouteOutcome::Void => {
203            let mark = ansi_term::Color::Yellow.paint("[~]");
204            (mark, VOID_SUMMARY)
205        }
206        RouteOutcome::Failed => {
207            let mark = ansi_term::Color::Red.paint("[✗]");
208            let summary = report
209                .error_summary
210                .as_ref()
211                .map(|s| s.as_str())
212                .unwrap_or("Missing error summary. This is a bug.");
213            (mark, summary)
214        }
215    };
216    table.add_row(row!(r->"Result: ", &format!("{} {}", mark, summary)));
217    if let Some(source_moniker) = report.source_moniker {
218        let source_moniker = match ExtendedMoniker::parse_str(&source_moniker) {
219            Ok(m) => m.to_string(),
220            Err(e) => format!("<invalid moniker>: {}: {}", e, source_moniker),
221        };
222        table.add_row(row!(r->"Source: ", source_moniker));
223    }
224    if let Some(service_instances) = report.service_instances {
225        let mut service_table = Table::new();
226        let mut format = *FORMAT_CLEAN;
227        format.padding(0, 0);
228        service_table.set_format(format);
229        let mut first = true;
230        for service_instance in service_instances {
231            if first {
232                first = false;
233            } else {
234                service_table.add_empty_row();
235            }
236            service_table.add_row(row!(r->"Name: ", &service_instance.instance_name));
237            service_table.add_row(row!(r->"Source child: ", &service_instance.child_name));
238            service_table.add_row(row!(r->"Name in child: ",
239                &service_instance.child_instance_name));
240        }
241        table.add_row(row!(r->"Service instances: ", service_table));
242    }
243    if let Some(dictionary_entries) = report.dictionary_entries {
244        let mut dict_table = Table::new();
245        let mut format = *FORMAT_CLEAN;
246        format.padding(0, 0);
247        dict_table.set_format(format);
248        for e in dictionary_entries {
249            dict_table.add_row(row!(&e.name));
250        }
251        table.add_row(row!(r->"Contents: ", dict_table));
252    }
253}
254
255#[cfg(test)]
256mod test {
257    use super::*;
258    use assert_matches::assert_matches;
259    use fidl::endpoints;
260    use fuchsia_async as fasync;
261    use futures::TryStreamExt;
262
263    fn route_validator(
264        expected_moniker: &'static str,
265        expected_targets: Vec<fsys::RouteTarget>,
266        reports: Vec<fsys::RouteReport>,
267    ) -> fsys::RouteValidatorProxy {
268        let (route_validator, mut stream) =
269            endpoints::create_proxy_and_stream::<fsys::RouteValidatorMarker>();
270        fasync::Task::local(async move {
271            match stream.try_next().await.unwrap().unwrap() {
272                fsys::RouteValidatorRequest::Validate { .. } => {
273                    panic!("unexpected Validate request");
274                }
275                fsys::RouteValidatorRequest::Route { moniker, targets, responder } => {
276                    assert_eq!(
277                        Moniker::parse_str(expected_moniker).unwrap(),
278                        Moniker::parse_str(&moniker).unwrap()
279                    );
280                    assert_eq!(expected_targets, targets);
281                    responder.send(Ok(&reports)).unwrap();
282                }
283            }
284        })
285        .detach();
286        route_validator
287    }
288
289    #[fuchsia::test]
290    async fn test_errors() {
291        let targets =
292            vec![fsys::RouteTarget { decl_type: fsys::DeclType::Use, name: "fuchsia.foo".into() }];
293        let validator = route_validator(
294            "/test",
295            targets.clone(),
296            vec![fsys::RouteReport {
297                capability: Some("fuchsia.foo.bar".into()),
298                decl_type: Some(fsys::DeclType::Use),
299                error: Some(fsys::RouteError {
300                    summary: Some("Access denied".into()),
301                    ..Default::default()
302                }),
303                // Test inference of Failed
304                outcome: None,
305                ..Default::default()
306            }],
307        );
308
309        let mut reports =
310            route(&validator, Moniker::parse_str("./test").unwrap(), targets).await.unwrap();
311        assert_eq!(reports.len(), 1);
312
313        let report = reports.remove(0);
314        assert_matches!(
315            report,
316            RouteReport {
317                capability,
318                decl_type: DeclType::Use,
319                error_summary: Some(s),
320                source_moniker: None,
321                service_instances: None,
322                dictionary_entries: None,
323                outcome: RouteOutcome::Failed,
324            } if capability == "fuchsia.foo.bar" && s == "Access denied"
325        );
326    }
327
328    #[fuchsia::test]
329    async fn test_no_errors() {
330        let targets =
331            vec![fsys::RouteTarget { decl_type: fsys::DeclType::Use, name: "fuchsia.foo".into() }];
332        let validator = route_validator(
333            "/test",
334            targets.clone(),
335            vec![
336                fsys::RouteReport {
337                    capability: Some("fuchsia.foo.bar".into()),
338                    decl_type: Some(fsys::DeclType::Use),
339                    source_moniker: Some("<component manager>".into()),
340                    error: None,
341                    outcome: Some(fsys::RouteOutcome::Void),
342                    ..Default::default()
343                },
344                fsys::RouteReport {
345                    capability: Some("fuchsia.foo.baz".into()),
346                    decl_type: Some(fsys::DeclType::Expose),
347                    source_moniker: Some("/test/src".into()),
348                    service_instances: Some(vec![
349                        fsys::ServiceInstance {
350                            instance_name: Some("1234abcd".into()),
351                            child_name: Some("a".into()),
352                            child_instance_name: Some("default".into()),
353                            ..Default::default()
354                        },
355                        fsys::ServiceInstance {
356                            instance_name: Some("abcd1234".into()),
357                            child_name: Some("b".into()),
358                            child_instance_name: Some("other".into()),
359                            ..Default::default()
360                        },
361                    ]),
362                    dictionary_entries: Some(vec![
363                        fsys::DictionaryEntry { name: Some("k1".into()), ..Default::default() },
364                        fsys::DictionaryEntry { name: Some("k2".into()), ..Default::default() },
365                    ]),
366                    error: None,
367                    // Test inference of Success
368                    outcome: None,
369                    ..Default::default()
370                },
371            ],
372        );
373
374        let mut reports =
375            route(&validator, Moniker::parse_str("./test").unwrap(), targets).await.unwrap();
376        assert_eq!(reports.len(), 2);
377
378        let report = reports.remove(0);
379        assert_matches!(
380            report,
381            RouteReport {
382                capability,
383                decl_type: DeclType::Use,
384                error_summary: None,
385                source_moniker: Some(m),
386                service_instances: None,
387                dictionary_entries: None,
388                outcome: RouteOutcome::Void,
389            } if capability == "fuchsia.foo.bar" && m == "<component manager>"
390        );
391
392        let report = reports.remove(0);
393        assert_matches!(
394            report,
395            RouteReport {
396                capability,
397                decl_type: DeclType::Expose,
398                error_summary: None,
399                source_moniker: Some(m),
400                service_instances: Some(s),
401                dictionary_entries: Some(d),
402                outcome: RouteOutcome::Success,
403            } if capability == "fuchsia.foo.baz" && m == "/test/src"
404                && s == vec![
405                    ServiceInstance {
406                        instance_name: "1234abcd".into(),
407                        child_name: "a".into(),
408                        child_instance_name: "default".into(),
409                    },
410                    ServiceInstance {
411                        instance_name: "abcd1234".into(),
412                        child_name: "b".into(),
413                        child_instance_name: "other".into(),
414                    },
415                ]
416                && d == vec![
417                    DictionaryEntry {
418                        name: "k1".into(),
419                    },
420                    DictionaryEntry {
421                        name: "k2".into(),
422                    },
423                ]
424        );
425    }
426
427    #[fuchsia::test]
428    async fn test_no_routes() {
429        let validator = route_validator("/test", vec![], vec![]);
430
431        let reports =
432            route(&validator, Moniker::parse_str("./test").unwrap(), vec![]).await.unwrap();
433        assert!(reports.is_empty());
434    }
435
436    #[fuchsia::test]
437    async fn test_parse_error() {
438        let validator = route_validator(
439            "/test",
440            vec![],
441            vec![
442                // Don't set any fields
443                fsys::RouteReport::default(),
444            ],
445        );
446
447        let result = route(&validator, Moniker::parse_str("./test").unwrap(), vec![]).await;
448        assert_matches!(result, Err(_));
449    }
450}