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