Skip to main content

fuchsia_inspect_contrib/
content_publisher.rs

1// Copyright 2026 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::{Context, Error};
6use fidl::endpoints::ClientEnd;
7use fidl_fuchsia_inspect::{
8    InspectSinkMarker, InspectSinkPublishRequest, TreeContent, TreeGetContentResponder, TreeMarker,
9    TreeRequest, TreeRequestStream,
10};
11use fuchsia_inspect::Inspector;
12use futures::{Stream, StreamExt};
13use std::pin::Pin;
14use std::task::{Context as TaskContext, Poll};
15
16/// Configuration options for the content publisher.
17pub struct PublishOptions {
18    /// Channel over which the InspectSink protocol will be used.
19    pub inspect_sink_client: ClientEnd<InspectSinkMarker>,
20}
21
22/// A responder for a GetContent request.
23pub struct ContentResponder {
24    responder: TreeGetContentResponder,
25}
26
27impl ContentResponder {
28    /// Responds to the GetContent request with the VMO from the provided Inspector.
29    /// The Inspector is dropped after sending, freeing its resources.
30    pub fn send(self, inspector: Inspector) -> Result<(), Error> {
31        let vmo = inspector.frozen_vmo_copy().context("failed to copy vmo")?;
32        let size = vmo.get_size().context("failed to get vmo size")?;
33        let content = TreeContent {
34            buffer: Some(fidl_fuchsia_mem::Buffer { vmo, size }),
35            ..Default::default()
36        };
37
38        self.responder.send(content).context("failed to send response")?;
39        Ok(())
40    }
41}
42
43/// A stream of incoming GetContent requests.
44pub struct ContentPublisher {
45    stream: TreeRequestStream,
46}
47
48impl Stream for ContentPublisher {
49    type Item = ContentResponder;
50
51    fn poll_next(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
52        loop {
53            match self.stream.poll_next_unpin(cx) {
54                Poll::Ready(Some(Ok(TreeRequest::GetContent { responder }))) => {
55                    return Poll::Ready(Some(ContentResponder { responder }));
56                }
57                Poll::Ready(Some(Ok(_))) => {
58                    // Ignore other requests (e.g. ListChildHost, OpenChild).
59                    // This is a minimal server that only supports GetContent.
60                    continue;
61                }
62                Poll::Ready(Some(Err(_))) => {
63                    // Stream error, terminate.
64                    return Poll::Ready(None);
65                }
66                Poll::Ready(None) => return Poll::Ready(None),
67                Poll::Pending => return Poll::Pending,
68            }
69        }
70    }
71}
72
73/// Publishes a minimal Inspect Tree server that yields responders for data requests.
74///
75/// This can be thought of like a lazy node, but for the root.
76///
77/// - Lazy nodes are _not supported_ in Inspectors exposed this way.
78///
79/// Usage:
80///
81/// ```rust
82/// let mut publisher = content_publisher(options).expect("failed to create publisher");
83/// let mut val = 0;
84/// while let Some(responder) = publisher.next().await {
85///     // Generate a fresh inspector for each request
86///     let inspector = Inspector::default();
87///     inspector.root().record_int("dynamic_data", val);
88///     inspector.root().record_string("status", "ok");
89///
90///     // Send the generated VMO
91///     responder.send(inspector).expect("failed to send inspector");
92///     val += 1
93/// }
94/// ```
95pub fn content_publisher(options: PublishOptions) -> Result<ContentPublisher, Error> {
96    let (tree_client, tree_stream) = fidl::endpoints::create_request_stream::<TreeMarker>();
97
98    let inspect_sink = options.inspect_sink_client.into_proxy();
99
100    inspect_sink
101        .publish(InspectSinkPublishRequest { tree: Some(tree_client), ..Default::default() })
102        .context("failed to publish tree")?;
103
104    Ok(ContentPublisher { stream: tree_stream })
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use diagnostics_assertions::assert_data_tree;
111    use fidl_fuchsia_inspect::InspectSinkRequest;
112    use fuchsia_async as fasync;
113    use fuchsia_inspect::reader::read;
114    use futures::StreamExt;
115
116    #[fuchsia::test]
117    async fn test_content_publisher_usage() {
118        let (sink_client, mut sink_stream) =
119            fidl::endpoints::create_request_stream::<InspectSinkMarker>();
120
121        let options = PublishOptions { inspect_sink_client: sink_client };
122
123        let mut publisher = content_publisher(options).expect("failed to create publisher");
124
125        let tree_proxy = match sink_stream.next().await {
126            Some(Ok(InspectSinkRequest::Publish { payload, .. })) => {
127                payload.tree.expect("tree channel missing").into_proxy()
128            }
129            _ => panic!("Expected Publish request"),
130        };
131
132        // Start a background task representing the user's workload that generates inspect data
133        // on demand.
134        let _task = fasync::Task::spawn(async move {
135            let mut val = 0;
136            while let Some(responder) = publisher.next().await {
137                // Generate a fresh inspector for each request
138                let inspector = Inspector::default();
139                inspector.root().record_int("dynamic_data", val);
140                inspector.root().record_string("status", "ok");
141
142                // Send the generated VMO
143                responder.send(inspector).expect("failed to send inspector");
144                val += 1
145            }
146        });
147
148        // Read the tree, simulating a snapshot by an Archivist
149        let hierarchy = read(&tree_proxy).await.expect("failed to read from tree proxy");
150
151        // Verify the dynamic data was successfully retrieved
152        assert_data_tree!(hierarchy, root: {
153            dynamic_data: 0i64,
154            status: "ok",
155        });
156
157        // Read the tree, simulating a snapshot by an Archivist
158        let hierarchy = read(&tree_proxy).await.expect("failed to read from tree proxy");
159
160        // Verify the dynamic data was successfully retrieved
161        assert_data_tree!(hierarchy, root: {
162            dynamic_data: 1i64,
163            status: "ok",
164        });
165    }
166}