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