settings_common/
service_context.rs

1// Copyright 2019 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::clock;
6use crate::inspect::event::ExternalEventPublisher;
7use anyhow::{format_err, Error};
8use fidl::endpoints::{DiscoverableProtocolMarker, ProtocolMarker, Proxy};
9use fuchsia_async as fasync;
10use fuchsia_component::client::{connect_to_protocol, connect_to_protocol_at_path};
11use futures::future::LocalBoxFuture;
12use glob::glob;
13use std::borrow::Cow;
14use std::fmt::Debug;
15use std::future::Future;
16
17pub type GenerateService =
18    Box<dyn Fn(&str, zx::Channel) -> LocalBoxFuture<'static, Result<(), Error>>>;
19
20/// A wrapper around service operations, allowing redirection to a nested
21/// environment.
22pub struct ServiceContext {
23    generate_service: Option<GenerateService>,
24}
25
26impl ServiceContext {
27    pub fn new(generate_service: Option<GenerateService>) -> Self {
28        Self { generate_service }
29    }
30
31    pub async fn connect<P: DiscoverableProtocolMarker, Publisher, F>(
32        &self,
33        gen_publisher_fn: impl Fn() -> F,
34    ) -> Result<ExternalServiceProxy<P::Proxy, Publisher>, Error>
35    where
36        Publisher: EventPublisher + Clone,
37        F: Future<Output = Option<Publisher>>,
38    {
39        let proxy = if let Some(generate_service) = &self.generate_service {
40            let (client, server) = zx::Channel::create();
41            ((generate_service)(P::PROTOCOL_NAME, server)).await?;
42            P::Proxy::from_channel(fasync::Channel::from_channel(client))
43        } else {
44            connect_to_protocol::<P>()?
45        };
46
47        let publisher = gen_publisher_fn().await;
48
49        let external_proxy = ExternalServiceProxy::new(proxy, publisher.clone());
50        if let Some(publisher) = publisher {
51            publisher.send_event(ExternalServiceEvent::Created(
52                P::PROTOCOL_NAME,
53                clock::inspect_format_now().into(),
54            ));
55        }
56
57        Ok(external_proxy)
58    }
59
60    pub async fn connect_with_publisher<P: DiscoverableProtocolMarker, Publisher>(
61        &self,
62        publisher: Publisher,
63    ) -> Result<ExternalServiceProxy<P::Proxy, Publisher>, Error>
64    where
65        Publisher: EventPublisher + Clone,
66    {
67        let proxy = if let Some(generate_service) = &self.generate_service {
68            let (client, server) = zx::Channel::create();
69            ((generate_service)(P::PROTOCOL_NAME, server)).await?;
70            P::Proxy::from_channel(fasync::Channel::from_channel(client))
71        } else {
72            connect_to_protocol::<P>()?
73        };
74
75        let external_proxy = ExternalServiceProxy::new(proxy, Some(publisher.clone()));
76        publisher.send_event(ExternalServiceEvent::Created(
77            P::PROTOCOL_NAME,
78            clock::inspect_format_now().into(),
79        ));
80
81        Ok(external_proxy)
82    }
83
84    /// Connect to a service by discovering a hardware device at the given glob-style pattern.
85    ///
86    /// The first discovered path will be used to connected.
87    ///
88    /// If a GenerateService was specified at creation, the name of the service marker will be used
89    /// to generate a service and the path will be ignored.
90    pub async fn connect_device_path<P: DiscoverableProtocolMarker, Publisher>(
91        &self,
92        glob_pattern: &str,
93        publisher: Publisher,
94    ) -> Result<ExternalServiceProxy<P::Proxy, Publisher>, Error>
95    where
96        Publisher: EventPublisher + Clone,
97    {
98        if self.generate_service.is_some() {
99            // If a generate_service is already specified, just connect through there
100            return self.connect_with_publisher::<P, Publisher>(publisher).await;
101        }
102
103        let found_path = glob(glob_pattern)?
104            .filter_map(|entry| entry.ok())
105            .next()
106            .ok_or_else(|| format_err!("failed to enumerate devices"))?;
107
108        let path_str =
109            found_path.to_str().ok_or_else(|| format_err!("failed to convert path to str"))?;
110
111        let external_proxy = ExternalServiceProxy::new(
112            connect_to_protocol_at_path::<P>(path_str)?,
113            Some(publisher.clone()),
114        );
115        publisher.send_event(ExternalServiceEvent::Created(
116            P::DEBUG_NAME,
117            clock::inspect_format_now().into(),
118        ));
119
120        Ok(external_proxy)
121    }
122}
123
124/// Definition of events related to external api calls outside of the setting service.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub enum ExternalServiceEvent {
127    /// Event sent when an external service proxy is created. Contains
128    /// the protocol name and the timestamp at which the connection
129    /// was created.
130    Created(
131        &'static str,      // Protocol
132        Cow<'static, str>, // Timestamp
133    ),
134
135    /// Event sent when a call is made on an external service proxy.
136    /// Contains the protocol name, the stringified request, and the
137    /// request timestamp.
138    ApiCall(
139        &'static str,      // Protocol
140        Cow<'static, str>, // Request
141        Cow<'static, str>, // Request timestamp
142    ),
143
144    /// Event sent when a non-error response is received on an external
145    /// service proxy. Contains the protocol name, the response, the
146    /// associated stringified request, and the request/response timestamps.
147    ApiResponse(
148        &'static str,      // Protocol
149        Cow<'static, str>, // Response
150        Cow<'static, str>, // Request
151        Cow<'static, str>, // Request Timestamp
152        Cow<'static, str>, // Response timestamp
153    ),
154
155    /// Event sent when an error is received on an external service proxy.
156    /// Contains the protocol name, the error message, the associated
157    /// stringified request, and the request/response timestamps.
158    ApiError(
159        &'static str,      // Protocol
160        Cow<'static, str>, // Error msg
161        Cow<'static, str>, // Request
162        Cow<'static, str>, // Request timestamp
163        Cow<'static, str>, // Error timestamp
164    ),
165
166    /// Event sent when an external service proxy is closed. Contains the
167    /// protocol name, the associated stringified request, and the
168    /// request/response timestamps.
169    Closed(
170        &'static str,      // Protocol
171        Cow<'static, str>, // Request
172        Cow<'static, str>, // Request timestamp
173        Cow<'static, str>, // Response timestamp
174    ),
175}
176
177pub trait EventPublisher {
178    fn send_event(&self, event: ExternalServiceEvent);
179}
180
181impl EventPublisher for ExternalEventPublisher {
182    fn send_event(&self, event: ExternalServiceEvent) {
183        let _ = self.publish(event);
184    }
185}
186
187/// A wrapper around a proxy, used to track disconnections.
188///
189/// This wraps any type implementing `Proxy`. Whenever any call returns a
190/// `ClientChannelClosed` error, this wrapper publishes a closed event for
191/// the wrapped proxy.
192#[derive(Clone, Debug)]
193pub struct ExternalServiceProxy<P, Publisher>
194where
195    P: Proxy,
196{
197    proxy: P,
198    publisher: Option<Publisher>,
199}
200
201impl<P, Publisher> ExternalServiceProxy<P, Publisher>
202where
203    P: Proxy,
204    Publisher: EventPublisher,
205{
206    pub fn new(proxy: P, publisher: Option<Publisher>) -> Self {
207        Self { proxy, publisher }
208    }
209
210    /// Handle the `result` of the event sent via the call or call_async methods
211    /// and send the corresponding information to be logged to inspect.
212    pub fn inspect_result<T>(
213        &self,
214        result: &Result<T, fidl::Error>,
215        arg_str: String,
216        req_timestamp: String,
217        resp_timestamp: String,
218    ) where
219        T: Debug,
220    {
221        if let Some(p) = self.publisher.as_ref() {
222            if let Err(fidl::Error::ClientChannelClosed { .. }) = result {
223                p.send_event(ExternalServiceEvent::Closed(
224                    P::Protocol::DEBUG_NAME,
225                    arg_str.into(),
226                    req_timestamp.into(),
227                    resp_timestamp.into(),
228                ));
229            } else if let Err(e) = result {
230                p.send_event(ExternalServiceEvent::ApiError(
231                    P::Protocol::DEBUG_NAME,
232                    format!("{e:?}").into(),
233                    arg_str.into(),
234                    req_timestamp.into(),
235                    resp_timestamp.into(),
236                ));
237            } else {
238                let payload = result.as_ref().expect("Could not extract external api call result");
239                p.send_event(ExternalServiceEvent::ApiResponse(
240                    P::Protocol::DEBUG_NAME,
241                    format!("{payload:?}").into(),
242                    arg_str.into(),
243                    req_timestamp.into(),
244                    resp_timestamp.into(),
245                ));
246            }
247        }
248    }
249
250    /// Make a call to a synchronous API of the wrapped proxy. This should not be called directly,
251    /// only from the call macro.
252    pub fn call<T, F>(&self, func: F, arg_str: String) -> Result<T, fidl::Error>
253    where
254        F: FnOnce(&P) -> Result<T, fidl::Error>,
255        T: std::fmt::Debug,
256    {
257        let req_timestamp = clock::inspect_format_now();
258        if let Some(p) = self.publisher.as_ref() {
259            p.send_event(ExternalServiceEvent::ApiCall(
260                P::Protocol::DEBUG_NAME,
261                arg_str.clone().into(),
262                req_timestamp.clone().into(),
263            ));
264        }
265        let result = func(&self.proxy);
266        self.inspect_result(&result, arg_str, req_timestamp, clock::inspect_format_now());
267        result
268    }
269
270    /// Make a call to an asynchronous API of the wrapped proxy. This should not be called directly,
271    /// only from the call_async macro.
272    pub async fn call_async<T, F, Fut>(&self, func: F, arg_str: String) -> Result<T, fidl::Error>
273    where
274        F: FnOnce(&P) -> Fut,
275        Fut: Future<Output = Result<T, fidl::Error>>,
276        T: std::fmt::Debug,
277    {
278        let req_timestamp = clock::inspect_format_now();
279        if let Some(p) = self.publisher.as_ref() {
280            p.send_event(ExternalServiceEvent::ApiCall(
281                P::Protocol::DEBUG_NAME,
282                arg_str.clone().into(),
283                req_timestamp.clone().into(),
284            ));
285        }
286        let result = func(&self.proxy).await;
287        self.inspect_result(&result, arg_str, req_timestamp, clock::inspect_format_now());
288        result
289    }
290}
291
292/// Helper macro to simplify calls to proxy objects.
293#[macro_export]
294macro_rules! call {
295    ($proxy:expr => $($call:tt)+) => {
296        {
297            let arg_string = $crate::format_call!($($call)+);
298            $proxy.call(|p| p.$($call)+, arg_string)
299        }
300    };
301}
302
303/// Helper macro to simplify async calls to proxy objects.
304#[macro_export]
305macro_rules! call_async {
306    ($proxy:expr => $($call:tt)+) => {
307        {
308            let arg_string = $crate::format_call!($($call)+);
309            $proxy.call_async(|p| p.$($call)+, arg_string)
310        }
311    };
312}
313
314/// Helper macro to parse and stringify the arguments to `call` and `call_async`.
315#[macro_export]
316macro_rules! format_call {
317    ($fn_name:ident($($arg:expr),*)) => {
318        {
319            let mut s = format!("{}(", stringify!($fn_name));
320            $(
321                s += &format!("{:?}", $arg);
322            )*
323
324            s += ")";
325            s
326        }
327    };
328}