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