Skip to main content

inspect_runtime/
lib.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
3
4//! # Inspect Runtime
5//!
6//! This library contains the necessary functions to serve inspect from a component.
7
8use fidl::AsHandleRef;
9use fidl::endpoints::ClientEnd;
10use fuchsia_component_client::connect_to_protocol;
11use fuchsia_inspect::Inspector;
12use log::error;
13use pin_project::pin_project;
14use std::future::Future;
15use std::pin::{Pin, pin};
16use std::task::{Context, Poll};
17use {fidl_fuchsia_inspect as finspect, fuchsia_async as fasync};
18
19#[cfg(fuchsia_api_level_at_least = "HEAD")]
20pub use finspect::EscrowToken;
21
22pub mod service;
23
24/// A setting for the fuchsia.inspect.Tree server that indicates how the server should send
25/// the Inspector's VMO. For fallible methods of sending, a fallback is also set.
26#[derive(Clone)]
27pub enum TreeServerSendPreference {
28    /// Frozen denotes sending a copy-on-write VMO.
29    /// `on_failure` refers to failure behavior, as not all VMOs
30    /// can be frozen. In particular, freezing a VMO requires writing to it,
31    /// so if an Inspector is created with a read-only VMO, freezing will fail.
32    ///
33    /// Failure behavior should be one of Live or DeepCopy.
34    ///
35    /// Frozen { on_failure: Live } is the default value of TreeServerSendPreference.
36    Frozen { on_failure: Box<TreeServerSendPreference> },
37
38    /// Live denotes sending a live handle to the VMO.
39    ///
40    /// A client might want this behavior if they have time sensitive writes
41    /// to the VMO, because copy-on-write behavior causes the initial write
42    /// to a page to be around 1% slower.
43    Live,
44
45    /// DeepCopy will send a private copy of the VMO. This should probably
46    /// not be a client's first choice, as Frozen(DeepCopy) will provide the
47    /// same semantic behavior while possibly avoiding an expensive copy.
48    ///
49    /// A client might want this behavior if they have time sensitive writes
50    /// to the VMO, because copy-on-write behavior causes the initial write
51    /// to a page to be around 1% slower.
52    DeepCopy,
53}
54
55impl TreeServerSendPreference {
56    /// Create a new [`TreeServerSendPreference`] that sends a frozen/copy-on-write VMO of the tree,
57    /// falling back to the specified `failure_mode` if a frozen VMO cannot be provided.
58    ///
59    /// # Arguments
60    ///
61    /// * `failure_mode` - Fallback behavior to use if freezing the Inspect VMO fails.
62    ///
63    pub fn frozen_or(failure_mode: TreeServerSendPreference) -> Self {
64        TreeServerSendPreference::Frozen { on_failure: Box::new(failure_mode) }
65    }
66}
67
68impl Default for TreeServerSendPreference {
69    fn default() -> Self {
70        TreeServerSendPreference::frozen_or(TreeServerSendPreference::Live)
71    }
72}
73
74/// Optional settings for serving `fuchsia.inspect.Tree`
75#[derive(Default)]
76pub struct PublishOptions {
77    /// This specifies how the VMO should be sent over the `fuchsia.inspect.Tree` server.
78    ///
79    /// Default behavior is
80    /// `TreeServerSendPreference::Frozen { on_failure: TreeServerSendPreference::Live }`.
81    pub(crate) vmo_preference: TreeServerSendPreference,
82
83    /// An name value which will show up in the metadata of snapshots
84    /// taken from this `fuchsia.inspect.Tree` server. Defaults to
85    /// fuchsia.inspect#DEFAULT_TREE_NAME.
86    pub(crate) tree_name: Option<String>,
87
88    /// Channel over which the InspectSink protocol will be used.
89    pub(crate) inspect_sink_client: Option<ClientEnd<finspect::InspectSinkMarker>>,
90
91    /// Scope on which the server will be spawned.
92    pub(crate) custom_scope: Option<fasync::ScopeHandle>,
93
94    /// If provided, `publish` will use this tree instead of creating a new one.
95    pub(crate) tree: Option<TreeServerHandle>,
96}
97
98impl PublishOptions {
99    /// This specifies how the VMO should be sent over the `fuchsia.inspect.Tree` server.
100    ///
101    /// Default behavior is
102    /// `TreeServerSendPreference::Frozen { on_failure: TreeServerSendPreference::Live }`.
103    pub fn send_vmo_preference(mut self, preference: TreeServerSendPreference) -> Self {
104        self.vmo_preference = preference;
105        self
106    }
107
108    /// This sets an optional name value which will show up in the metadata of snapshots
109    /// taken from this `fuchsia.inspect.Tree` server.
110    ///
111    /// Default behavior is an empty string.
112    pub fn inspect_tree_name(mut self, name: impl Into<String>) -> Self {
113        self.tree_name = Some(name.into());
114        self
115    }
116
117    /// Sets a custom fuchsia_async::Scope to use for serving Inspect.
118    pub fn custom_scope(mut self, scope: fasync::ScopeHandle) -> Self {
119        self.custom_scope = Some(scope);
120        self
121    }
122
123    /// This allows the client to provide the InspectSink client channel.
124    pub fn on_inspect_sink_client(
125        mut self,
126        client: ClientEnd<finspect::InspectSinkMarker>,
127    ) -> Self {
128        self.inspect_sink_client = Some(client);
129        self
130    }
131
132    /// Use the provided [`TreeServerHandle`] instead of creating a new one. Skips the
133    /// call to InspectSink.Publish, but still spawns a new Tree server to
134    /// handle incoming requests.
135    pub fn on_tree_server(mut self, tree: TreeServerHandle) -> Self {
136        self.tree = Some(tree);
137        self
138    }
139}
140
141/// Spawns a server handling `fuchsia.inspect.Tree` requests and a handle
142/// to the `fuchsia.inspect.Tree` is published using `fuchsia.inspect.InspectSink`.
143///
144/// Whenever the client wishes to stop publishing Inspect, the Controller may be dropped.
145///
146/// `None` will be returned on FIDL failures. This includes:
147/// * Failing to convert a FIDL endpoint for `fuchsia.inspect.Tree`'s `TreeMarker` into a stream
148/// * Failing to connect to the `InspectSink` protocol
149/// * Failing to send the connection over the wire
150#[must_use]
151pub fn publish(
152    inspector: &Inspector,
153    options: PublishOptions,
154) -> Option<PublishedInspectController> {
155    let PublishOptions { vmo_preference, tree_name, inspect_sink_client, custom_scope, tree } =
156        options;
157    let scope = custom_scope
158        .map(|handle| handle.new_child_with_name("inspect_runtime::publish"))
159        .unwrap_or_else(|| fasync::Scope::new_with_name("inspect_runtime::publish"));
160
161    if let Some(TreeServerHandle { client_koid: client, stream }) = tree {
162        service::spawn_tree_server_with_stream(inspector.clone(), vmo_preference, stream, &scope);
163        return Some(PublishedInspectController::new(inspector.clone(), scope, client));
164    }
165
166    let tree = service::spawn_tree_server(inspector.clone(), vmo_preference, &scope);
167
168    let inspect_sink = inspect_sink_client.map(|client| client.into_proxy()).or_else(|| {
169        connect_to_protocol::<finspect::InspectSinkMarker>()
170            .map_err(|err| error!(err:%; "failed to spawn the fuchsia.inspect.Tree server"))
171            .ok()
172    })?;
173
174    // unwrap: safe since we have a valid tree handle coming from the server we spawn.
175    let tree_koid = tree.as_handle_ref().koid().unwrap();
176    if let Err(err) = inspect_sink.publish(finspect::InspectSinkPublishRequest {
177        tree: Some(tree),
178        name: tree_name,
179        ..finspect::InspectSinkPublishRequest::default()
180    }) {
181        error!(err:%; "failed to spawn the fuchsia.inspect.Tree server");
182        return None;
183    }
184
185    Some(PublishedInspectController::new(inspector.clone(), scope, tree_koid))
186}
187
188/// Options for fetching a VMO that was previously escrowed.
189#[derive(Debug, Default)]
190pub struct FetchEscrowOptions {
191    /// Channel over which the InspectSink protocol will be used.
192    pub(crate) inspect_sink_client: Option<ClientEnd<finspect::InspectSinkMarker>>,
193
194    /// If true, the escrowed Inspect tree will be replaced with a new one, and a handle
195    /// to the new tree will be returned in [`FetchEscrowResult`].
196    pub(crate) should_replace_with_tree: bool,
197}
198
199impl FetchEscrowOptions {
200    /// Creates new default options for fetching an escrowed VMO.
201    pub fn new() -> Self {
202        Self::default()
203    }
204
205    /// This allows the client to provide the InspectSink client channel.
206    pub fn on_inspect_sink_client(
207        mut self,
208        client: ClientEnd<finspect::InspectSinkMarker>,
209    ) -> Self {
210        self.inspect_sink_client = Some(client);
211        self
212    }
213
214    /// If true, the escrowed Inspect tree will be replaced with a new one, and a handle
215    /// to the new tree will be returned in [`FetchEscrowResult`].
216    pub fn replace_with_tree(mut self) -> Self {
217        self.should_replace_with_tree = true;
218        self
219    }
220}
221
222/// The result of fetching an escrowed VMO.
223pub struct FetchEscrowResult {
224    /// The VMO containing the escrowed Inspect data.
225    pub vmo: zx::Vmo,
226    /// A handle to the new Inspect Tree if one was requested.
227    pub server: Option<TreeServerHandle>,
228}
229
230/// A handle to a `fuchsia.inspect.Tree` server.
231pub struct TreeServerHandle {
232    client_koid: zx::Koid,
233    stream: finspect::TreeRequestStream,
234}
235
236/// Fetches a VMO that was previously escrowed.
237///
238/// This function connects to `fuchsia.inspect.InspectSink` and exchanges the provided
239/// `escrow_token` for the VMO it represents.
240///
241/// If `FetchEscrowOptions::replace_with_tree` is set, a new `fuchsia.inspect.Tree` server
242/// will be created to replace the one that was torn down when the VMO was originally escrowed.
243/// A handle to this new tree will be returned.
244#[cfg(fuchsia_api_level_at_least = "HEAD")]
245pub async fn fetch_escrow(
246    escrow_token: finspect::EscrowToken,
247    options: FetchEscrowOptions,
248) -> Result<FetchEscrowResult, anyhow::Error> {
249    use anyhow::{Context as _, anyhow};
250
251    let FetchEscrowOptions { inspect_sink_client, should_replace_with_tree } = options;
252
253    let (tree, handle) = if should_replace_with_tree {
254        let (client, stream) = fidl::endpoints::create_request_stream::<finspect::TreeMarker>();
255        // unwrap: safe since we have a valid tree handle coming from above.
256        let client_koid = client.as_handle_ref().koid().unwrap();
257        (Some(client), Some(TreeServerHandle { client_koid, stream }))
258    } else {
259        (None, None)
260    };
261
262    let inspect_sink = match inspect_sink_client {
263        Some(client) => client.into_proxy(),
264        None => connect_to_protocol::<finspect::InspectSinkMarker>()?,
265    };
266
267    let vmo = inspect_sink
268        .fetch_escrow(finspect::InspectSinkFetchEscrowRequest {
269            token: Some(escrow_token),
270            tree,
271            ..Default::default()
272        })
273        .await
274        .context("Failed to fetch escrow")?
275        .vmo
276        .ok_or_else(|| {
277            anyhow!("VMO missing from response; perhaps the provided escrow_token is invalid")
278        })?;
279
280    Ok(FetchEscrowResult { vmo, server: handle })
281}
282
283#[pin_project]
284pub struct PublishedInspectController {
285    #[pin]
286    scope: fasync::scope::Join,
287    inspector: Inspector,
288    tree_koid: zx::Koid,
289}
290
291#[cfg(fuchsia_api_level_at_least = "HEAD")]
292#[derive(Default)]
293pub struct EscrowOptions {
294    name: Option<String>,
295    inspect_sink: Option<finspect::InspectSinkProxy>,
296}
297
298#[cfg(fuchsia_api_level_at_least = "HEAD")]
299impl EscrowOptions {
300    /// Sets the name with which the Inspect handle will be escrowed.
301    pub fn name(mut self, name: impl Into<String>) -> Self {
302        self.name = Some(name.into());
303        self
304    }
305
306    /// Sets the inspect sink channel to use for escrowing.
307    pub fn inspect_sink(mut self, proxy: finspect::InspectSinkProxy) -> Self {
308        self.inspect_sink = Some(proxy);
309        self
310    }
311}
312
313#[cfg(fuchsia_api_level_at_least = "HEAD")]
314#[derive(Debug, thiserror::Error)]
315pub enum EscrowError {
316    #[error("Failed to spawn the fuchsia.inspect.Tree server: {0}")]
317    SpawnTreeServer(#[from] anyhow::Error),
318    #[error("Failed to get a frozen vmo, aborting escrow: {0}")]
319    GetFrozenVmo(#[from] fuchsia_inspect::Error),
320    #[error("Failed to escrow inspect data: {0}")]
321    Escrow(#[from] fidl::Error),
322}
323
324impl PublishedInspectController {
325    fn new(inspector: Inspector, scope: fasync::Scope, tree_koid: zx::Koid) -> Self {
326        Self { inspector, scope: scope.join(), tree_koid }
327    }
328
329    /// Escrows a frozen copy of the VMO of the associated Inspector replacing the current live
330    /// handle in the server.
331    /// This will not capture lazy nodes or properties.
332    #[cfg(fuchsia_api_level_at_least = "HEAD")]
333    pub async fn escrow_frozen(self, opts: EscrowOptions) -> Result<EscrowToken, EscrowError> {
334        let inspect_sink = match opts.inspect_sink {
335            Some(proxy) => proxy,
336            None => match connect_to_protocol::<finspect::InspectSinkMarker>() {
337                Ok(inspect_sink) => inspect_sink,
338                Err(err) => {
339                    return Err(EscrowError::SpawnTreeServer(err));
340                }
341            },
342        };
343        let (ep0, ep1) = zx::EventPair::create();
344        let vmo = match self.inspector.frozen_vmo_copy() {
345            Ok(vmo) => vmo,
346            Err(err) => {
347                return Err(EscrowError::GetFrozenVmo(err));
348            }
349        };
350        if let Err(err) = inspect_sink.escrow(finspect::InspectSinkEscrowRequest {
351            vmo: Some(vmo),
352            name: opts.name,
353            token: Some(EscrowToken { token: ep0 }),
354            tree: Some(self.tree_koid.raw_koid()),
355            ..Default::default()
356        }) {
357            return Err(EscrowError::Escrow(err));
358        }
359        self.scope.await;
360        Ok(EscrowToken { token: ep1 })
361    }
362
363    /// Cancels the running published controller.
364    ///
365    /// The future resolves when no more serving tasks are running.
366    pub async fn cancel(self) {
367        let Self { scope, inspector: _, tree_koid: _ } = self;
368        let scope = pin!(scope);
369        scope.cancel().await;
370    }
371}
372
373impl Future for PublishedInspectController {
374    type Output = ();
375
376    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
377        let this = self.project();
378        this.scope.poll(cx)
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use assert_matches::assert_matches;
386    use component_events::events::{EventStream, Started};
387    use component_events::matcher::EventMatcher;
388    use diagnostics_assertions::assert_json_diff;
389    use diagnostics_hierarchy::DiagnosticsHierarchy;
390    use diagnostics_reader::ArchiveReader;
391    use fidl::endpoints::RequestStream;
392    use fidl_fuchsia_inspect::{InspectSinkRequest, InspectSinkRequestStream};
393    use fuchsia_component_test::ScopedInstance;
394    use fuchsia_inspect::InspectorConfig;
395    use fuchsia_inspect::reader::snapshot::Snapshot;
396    use fuchsia_inspect::reader::{PartialNodeHierarchy, read};
397
398    use futures::{FutureExt, StreamExt};
399
400    const TEST_PUBLISH_COMPONENT_URL: &str = "#meta/inspect_test_component.cm";
401
402    #[fuchsia::test]
403    async fn new_no_op() {
404        let inspector = Inspector::new(InspectorConfig::default().no_op());
405        assert!(!inspector.is_valid());
406
407        // Ensure publish doesn't crash on a No-Op inspector.
408        // The idea is that in this context, publish will hang if the server is running
409        // correctly. That is, if there is an error condition, it will be immediate.
410        assert_matches!(
411            publish(&inspector, PublishOptions::default()).unwrap().now_or_never(),
412            None
413        );
414    }
415
416    #[fuchsia::test]
417    async fn connect_to_service() -> Result<(), anyhow::Error> {
418        let mut event_stream = EventStream::open().await.unwrap();
419
420        let app = ScopedInstance::new_with_name(
421            "interesting_name".into(),
422            "coll".to_string(),
423            TEST_PUBLISH_COMPONENT_URL.to_string(),
424        )
425        .await
426        .expect("failed to create test component");
427
428        let started_stream = EventMatcher::ok()
429            .moniker_regex(app.child_name().to_owned())
430            .wait::<Started>(&mut event_stream);
431
432        app.connect_to_binder().expect("failed to connect to Binder protocol");
433
434        started_stream.await.expect("failed to observe Started event");
435
436        let hierarchy = ArchiveReader::inspect()
437            .add_selector("coll\\:interesting_name:[name=tree-0]root")
438            .snapshot()
439            .await?
440            .into_iter()
441            .next()
442            .and_then(|result| result.payload)
443            .expect("one Inspect hierarchy");
444
445        assert_json_diff!(hierarchy, root: {
446            "tree-0": 0u64,
447            int: 3i64,
448            "lazy-node": {
449                a: "test",
450                child: {
451                    double: 3.25,
452                },
453            }
454        });
455
456        Ok(())
457    }
458
459    #[fuchsia::test]
460    async fn publish_new_no_op() {
461        let inspector = Inspector::new(InspectorConfig::default().no_op());
462        assert!(!inspector.is_valid());
463
464        // Ensure publish doesn't crash on a No-Op inspector
465        let _task = publish(&inspector, PublishOptions::default());
466    }
467
468    #[fuchsia::test]
469    async fn publish_on_provided_channel() {
470        let (client, server) = zx::Channel::create();
471        let inspector = Inspector::default();
472        inspector.root().record_string("hello", "world");
473        let _inspect_sink_server_task = publish(
474            &inspector,
475            PublishOptions::default()
476                .on_inspect_sink_client(ClientEnd::<finspect::InspectSinkMarker>::new(client)),
477        );
478        let mut request_stream =
479            InspectSinkRequestStream::from_channel(fidl::AsyncChannel::from_channel(server));
480
481        let tree = request_stream.next().await.unwrap();
482
483        assert_matches!(tree, Ok(InspectSinkRequest::Publish {
484            payload: finspect::InspectSinkPublishRequest { tree: Some(tree), .. }, ..}) => {
485                let hierarchy = read(&tree.into_proxy()).await.unwrap();
486                assert_json_diff!(hierarchy, root: {
487                    hello: "world"
488                });
489            }
490        );
491
492        assert!(request_stream.next().await.is_none());
493    }
494
495    #[fuchsia::test]
496    async fn cancel_published_controller() {
497        let (client, server) = zx::Channel::create();
498        let inspector = Inspector::default();
499        inspector.root().record_string("hello", "world");
500        let controller = publish(
501            &inspector,
502            PublishOptions::default()
503                .on_inspect_sink_client(ClientEnd::<finspect::InspectSinkMarker>::new(client)),
504        )
505        .expect("create controller");
506        let mut request_stream =
507            InspectSinkRequestStream::from_channel(fidl::AsyncChannel::from_channel(server));
508
509        let tree = request_stream.next().await.unwrap();
510
511        let tree = assert_matches!(tree, Ok(InspectSinkRequest::Publish {
512            payload: finspect::InspectSinkPublishRequest { tree: Some(tree), .. }, ..}) => tree
513        );
514
515        assert!(request_stream.next().await.is_none());
516
517        controller.cancel().await;
518        fidl::AsyncChannel::from_channel(tree.into_channel())
519            .on_closed()
520            .await
521            .expect("wait closed");
522    }
523
524    #[fuchsia::test]
525    async fn controller_supports_escrowing_a_copy() {
526        let inspector = Inspector::default();
527        inspector.root().record_string("hello", "world");
528
529        let (client, mut request_stream) = fidl::endpoints::create_request_stream();
530        let controller =
531            publish(&inspector, PublishOptions::default().on_inspect_sink_client(client))
532                .expect("got controller");
533
534        let request = request_stream.next().await.unwrap();
535        let tree_koid = match request {
536            Ok(InspectSinkRequest::Publish {
537                payload: finspect::InspectSinkPublishRequest { tree: Some(tree), .. },
538                ..
539            }) => tree.as_handle_ref().basic_info().unwrap().koid,
540            other => {
541                panic!("unexpected request: {other:?}");
542            }
543        };
544        let (proxy, mut request_stream) =
545            fidl::endpoints::create_proxy_and_stream::<finspect::InspectSinkMarker>();
546        let (client_token, request) = futures::future::join(
547            controller.escrow_frozen(EscrowOptions {
548                name: Some("test".into()),
549                inspect_sink: Some(proxy),
550            }),
551            request_stream.next(),
552        )
553        .await;
554        match request {
555            Some(Ok(InspectSinkRequest::Escrow {
556                payload:
557                    finspect::InspectSinkEscrowRequest {
558                        vmo: Some(vmo),
559                        name: Some(name),
560                        token: Some(EscrowToken { token }),
561                        tree: Some(tree),
562                        ..
563                    },
564                ..
565            })) => {
566                assert_eq!(name, "test");
567                assert_eq!(tree, tree_koid.raw_koid());
568
569                // An update to the inspector isn't reflected here, since it was  CoW.
570                inspector.root().record_string("hey", "not there");
571
572                let snapshot = Snapshot::try_from(&vmo).expect("valid vmo");
573                let hierarchy: DiagnosticsHierarchy =
574                    PartialNodeHierarchy::try_from(snapshot).expect("valid snapshot").into();
575                assert_json_diff!(hierarchy, root: {
576                    hello: "world"
577                });
578                assert_eq!(
579                    client_token.unwrap().token.as_handle_ref().basic_info().unwrap().koid,
580                    token.as_handle_ref().basic_info().unwrap().related_koid
581                );
582            }
583            other => {
584                panic!("unexpected request: {other:?}");
585            }
586        };
587    }
588
589    #[cfg(fuchsia_api_level_at_least = "HEAD")]
590    #[fuchsia::test]
591    async fn fetch_escrow_works() {
592        let (client, mut request_stream) =
593            fidl::endpoints::create_request_stream::<finspect::InspectSinkMarker>();
594        let (_local_token, remote_token) = zx::EventPair::create();
595        let token = EscrowToken { token: remote_token };
596        let expected_koid = token.token.as_handle_ref().basic_info().unwrap().koid;
597
598        let publisher_fut =
599            fetch_escrow(token, FetchEscrowOptions::new().on_inspect_sink_client(client));
600
601        let server_fut = async {
602            let (payload, responder) = assert_matches!(
603                request_stream.next().await,
604                Some(Ok(InspectSinkRequest::FetchEscrow { payload, responder })) => (payload, responder)
605            );
606            let received_token = payload.token.unwrap();
607            assert_eq!(
608                received_token.token.as_handle_ref().basic_info().unwrap().koid,
609                expected_koid
610            );
611            responder
612                .send(finspect::InspectSinkFetchEscrowResponse {
613                    vmo: Some(zx::Vmo::create(0).unwrap()),
614                    ..Default::default()
615                })
616                .unwrap();
617        };
618
619        let (result, _) = futures::join!(publisher_fut, server_fut);
620        assert!(result.is_ok());
621    }
622}