1use fidl::endpoints::ClientEnd;
9use fidl::AsHandleRef;
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#[derive(Clone)]
27pub enum TreeServerSendPreference {
28 Frozen { on_failure: Box<TreeServerSendPreference> },
37
38 Live,
44
45 DeepCopy,
53}
54
55impl TreeServerSendPreference {
56 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#[derive(Default)]
76pub struct PublishOptions {
77 pub(crate) vmo_preference: TreeServerSendPreference,
82
83 pub(crate) tree_name: Option<String>,
87
88 pub(crate) inspect_sink_client: Option<ClientEnd<finspect::InspectSinkMarker>>,
90}
91
92impl PublishOptions {
93 pub fn send_vmo_preference(mut self, preference: TreeServerSendPreference) -> Self {
98 self.vmo_preference = preference;
99 self
100 }
101
102 pub fn inspect_tree_name(mut self, name: impl Into<String>) -> Self {
107 self.tree_name = Some(name.into());
108 self
109 }
110
111 pub fn on_inspect_sink_client(
113 mut self,
114 client: ClientEnd<finspect::InspectSinkMarker>,
115 ) -> Self {
116 self.inspect_sink_client = Some(client);
117 self
118 }
119}
120
121#[must_use]
131pub fn publish(
132 inspector: &Inspector,
133 options: PublishOptions,
134) -> Option<PublishedInspectController> {
135 let PublishOptions { vmo_preference, tree_name, inspect_sink_client } = options;
136 let scope = fasync::Scope::new_with_name("inspect_runtime::publish");
137 let tree = service::spawn_tree_server(inspector.clone(), vmo_preference, &scope);
138
139 let inspect_sink = inspect_sink_client.map(|client| client.into_proxy()).or_else(|| {
140 connect_to_protocol::<finspect::InspectSinkMarker>()
141 .map_err(|err| error!(err:%; "failed to spawn the fuchsia.inspect.Tree server"))
142 .ok()
143 })?;
144
145 let tree_koid = tree.basic_info().unwrap().koid;
147 if let Err(err) = inspect_sink.publish(finspect::InspectSinkPublishRequest {
148 tree: Some(tree),
149 name: tree_name,
150 ..finspect::InspectSinkPublishRequest::default()
151 }) {
152 error!(err:%; "failed to spawn the fuchsia.inspect.Tree server");
153 return None;
154 }
155
156 Some(PublishedInspectController::new(inspector.clone(), scope, tree_koid))
157}
158
159#[pin_project]
160pub struct PublishedInspectController {
161 #[pin]
162 scope: fasync::scope::Join,
163 inspector: Inspector,
164 tree_koid: zx::Koid,
165}
166
167#[cfg(fuchsia_api_level_at_least = "HEAD")]
168#[derive(Default)]
169pub struct EscrowOptions {
170 name: Option<String>,
171 inspect_sink: Option<finspect::InspectSinkProxy>,
172}
173
174#[cfg(fuchsia_api_level_at_least = "HEAD")]
175impl EscrowOptions {
176 pub fn name(mut self, name: impl Into<String>) -> Self {
178 self.name = Some(name.into());
179 self
180 }
181
182 pub fn inspect_sink(mut self, proxy: finspect::InspectSinkProxy) -> Self {
184 self.inspect_sink = Some(proxy);
185 self
186 }
187}
188
189impl PublishedInspectController {
190 fn new(inspector: Inspector, scope: fasync::Scope, tree_koid: zx::Koid) -> Self {
191 Self { inspector, scope: scope.join(), tree_koid }
192 }
193
194 #[cfg(fuchsia_api_level_at_least = "HEAD")]
198 pub async fn escrow_frozen(self, opts: EscrowOptions) -> Option<EscrowToken> {
199 let inspect_sink = match opts.inspect_sink {
200 Some(proxy) => proxy,
201 None => match connect_to_protocol::<finspect::InspectSinkMarker>() {
202 Ok(inspect_sink) => inspect_sink,
203 Err(err) => {
204 error!(err:%; "failed to spawn the fuchsia.inspect.Tree server");
205 return None;
206 }
207 },
208 };
209 let (ep0, ep1) = zx::EventPair::create();
210 let Some(vmo) = self.inspector.frozen_vmo_copy() else {
211 error!("failed to get a frozen vmo, aborting escrow");
212 return None;
213 };
214 if let Err(err) = inspect_sink.escrow(finspect::InspectSinkEscrowRequest {
215 vmo: Some(vmo),
216 name: opts.name,
217 token: Some(EscrowToken { token: ep0 }),
218 tree: Some(self.tree_koid.raw_koid()),
219 ..Default::default()
220 }) {
221 error!(err:%; "failed to escrow inspect data");
222 return None;
223 }
224 self.scope.await;
225 Some(EscrowToken { token: ep1 })
226 }
227
228 pub async fn cancel(self) {
232 let Self { scope, inspector: _, tree_koid: _ } = self;
233 let scope = pin!(scope);
234 scope.cancel().await;
235 }
236}
237
238impl Future for PublishedInspectController {
239 type Output = ();
240
241 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
242 let this = self.project();
243 this.scope.poll(cx)
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use assert_matches::assert_matches;
251 use component_events::events::{EventStream, Started};
252 use component_events::matcher::EventMatcher;
253 use diagnostics_assertions::assert_json_diff;
254 use diagnostics_hierarchy::DiagnosticsHierarchy;
255 use diagnostics_reader::ArchiveReader;
256 use fidl::endpoints::RequestStream;
257 use fidl_fuchsia_inspect::{InspectSinkRequest, InspectSinkRequestStream};
258 use fuchsia_component_test::ScopedInstance;
259 use fuchsia_inspect::reader::snapshot::Snapshot;
260 use fuchsia_inspect::reader::{read, PartialNodeHierarchy};
261 use fuchsia_inspect::InspectorConfig;
262
263 use futures::{FutureExt, StreamExt};
264
265 const TEST_PUBLISH_COMPONENT_URL: &str = "#meta/inspect_test_component.cm";
266
267 #[fuchsia::test]
268 async fn new_no_op() {
269 let inspector = Inspector::new(InspectorConfig::default().no_op());
270 assert!(!inspector.is_valid());
271
272 assert_matches!(
276 publish(&inspector, PublishOptions::default()).unwrap().now_or_never(),
277 None
278 );
279 }
280
281 #[fuchsia::test]
282 async fn connect_to_service() -> Result<(), anyhow::Error> {
283 let mut event_stream = EventStream::open().await.unwrap();
284
285 let app = ScopedInstance::new_with_name(
286 "interesting_name".into(),
287 "coll".to_string(),
288 TEST_PUBLISH_COMPONENT_URL.to_string(),
289 )
290 .await
291 .expect("failed to create test component");
292
293 let started_stream = EventMatcher::ok()
294 .moniker_regex(app.child_name().to_owned())
295 .wait::<Started>(&mut event_stream);
296
297 app.connect_to_binder().expect("failed to connect to Binder protocol");
298
299 started_stream.await.expect("failed to observe Started event");
300
301 let hierarchy = ArchiveReader::inspect()
302 .add_selector("coll\\:interesting_name:[name=tree-0]root")
303 .snapshot()
304 .await?
305 .into_iter()
306 .next()
307 .and_then(|result| result.payload)
308 .expect("one Inspect hierarchy");
309
310 assert_json_diff!(hierarchy, root: {
311 "tree-0": 0u64,
312 int: 3i64,
313 "lazy-node": {
314 a: "test",
315 child: {
316 double: 3.25,
317 },
318 }
319 });
320
321 Ok(())
322 }
323
324 #[fuchsia::test]
325 async fn publish_new_no_op() {
326 let inspector = Inspector::new(InspectorConfig::default().no_op());
327 assert!(!inspector.is_valid());
328
329 let _task = publish(&inspector, PublishOptions::default());
331 }
332
333 #[fuchsia::test]
334 async fn publish_on_provided_channel() {
335 let (client, server) = zx::Channel::create();
336 let inspector = Inspector::default();
337 inspector.root().record_string("hello", "world");
338 let _inspect_sink_server_task = publish(
339 &inspector,
340 PublishOptions::default()
341 .on_inspect_sink_client(ClientEnd::<finspect::InspectSinkMarker>::new(client)),
342 );
343 let mut request_stream =
344 InspectSinkRequestStream::from_channel(fidl::AsyncChannel::from_channel(server));
345
346 let tree = request_stream.next().await.unwrap();
347
348 assert_matches!(tree, Ok(InspectSinkRequest::Publish {
349 payload: finspect::InspectSinkPublishRequest { tree: Some(tree), .. }, ..}) => {
350 let hierarchy = read(&tree.into_proxy()).await.unwrap();
351 assert_json_diff!(hierarchy, root: {
352 hello: "world"
353 });
354 }
355 );
356
357 assert!(request_stream.next().await.is_none());
358 }
359
360 #[fuchsia::test]
361 async fn cancel_published_controller() {
362 let (client, server) = zx::Channel::create();
363 let inspector = Inspector::default();
364 inspector.root().record_string("hello", "world");
365 let controller = publish(
366 &inspector,
367 PublishOptions::default()
368 .on_inspect_sink_client(ClientEnd::<finspect::InspectSinkMarker>::new(client)),
369 )
370 .expect("create controller");
371 let mut request_stream =
372 InspectSinkRequestStream::from_channel(fidl::AsyncChannel::from_channel(server));
373
374 let tree = request_stream.next().await.unwrap();
375
376 let tree = assert_matches!(tree, Ok(InspectSinkRequest::Publish {
377 payload: finspect::InspectSinkPublishRequest { tree: Some(tree), .. }, ..}) => tree
378 );
379
380 assert!(request_stream.next().await.is_none());
381
382 controller.cancel().await;
383 fidl::AsyncChannel::from_channel(tree.into_channel())
384 .on_closed()
385 .await
386 .expect("wait closed");
387 }
388
389 #[fuchsia::test]
390 async fn controller_supports_escrowing_a_copy() {
391 let inspector = Inspector::default();
392 inspector.root().record_string("hello", "world");
393
394 let (client, mut request_stream) = fidl::endpoints::create_request_stream();
395 let controller =
396 publish(&inspector, PublishOptions::default().on_inspect_sink_client(client))
397 .expect("got controller");
398
399 let request = request_stream.next().await.unwrap();
400 let tree_koid = match request {
401 Ok(InspectSinkRequest::Publish {
402 payload: finspect::InspectSinkPublishRequest { tree: Some(tree), .. },
403 ..
404 }) => tree.basic_info().unwrap().koid,
405 other => {
406 panic!("unexpected request: {other:?}");
407 }
408 };
409 let (proxy, mut request_stream) =
410 fidl::endpoints::create_proxy_and_stream::<finspect::InspectSinkMarker>();
411 let (client_token, request) = futures::future::join(
412 controller.escrow_frozen(EscrowOptions {
413 name: Some("test".into()),
414 inspect_sink: Some(proxy),
415 }),
416 request_stream.next(),
417 )
418 .await;
419 match request {
420 Some(Ok(InspectSinkRequest::Escrow {
421 payload:
422 finspect::InspectSinkEscrowRequest {
423 vmo: Some(vmo),
424 name: Some(name),
425 token: Some(EscrowToken { token }),
426 tree: Some(tree),
427 ..
428 },
429 ..
430 })) => {
431 assert_eq!(name, "test");
432 assert_eq!(tree, tree_koid.raw_koid());
433
434 inspector.root().record_string("hey", "not there");
436
437 let snapshot = Snapshot::try_from(&vmo).expect("valid vmo");
438 let hierarchy: DiagnosticsHierarchy =
439 PartialNodeHierarchy::try_from(snapshot).expect("valid snapshot").into();
440 assert_json_diff!(hierarchy, root: {
441 hello: "world"
442 });
443 assert_eq!(
444 client_token.unwrap().token.basic_info().unwrap().koid,
445 token.basic_info().unwrap().related_koid
446 );
447 }
448 other => {
449 panic!("unexpected request: {other:?}");
450 }
451 };
452 }
453}