1use 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#[derive(Clone)]
28pub enum TreeServerSendPreference {
29 Frozen { on_failure: Box<TreeServerSendPreference> },
38
39 Live,
45
46 DeepCopy,
54}
55
56impl TreeServerSendPreference {
57 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#[derive(Default)]
77pub struct PublishOptions {
78 pub(crate) vmo_preference: TreeServerSendPreference,
83
84 pub(crate) tree_name: Option<String>,
88
89 pub(crate) inspect_sink_client: Option<ClientEnd<finspect::InspectSinkMarker>>,
91
92 pub(crate) custom_scope: Option<fasync::ScopeHandle>,
94
95 pub(crate) tree: Option<TreeServerHandle>,
97}
98
99impl PublishOptions {
100 pub fn send_vmo_preference(mut self, preference: TreeServerSendPreference) -> Self {
105 self.vmo_preference = preference;
106 self
107 }
108
109 pub fn inspect_tree_name(mut self, name: impl Into<String>) -> Self {
114 self.tree_name = Some(name.into());
115 self
116 }
117
118 pub fn custom_scope(mut self, scope: fasync::ScopeHandle) -> Self {
120 self.custom_scope = Some(scope);
121 self
122 }
123
124 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 pub fn on_tree_server(mut self, tree: TreeServerHandle) -> Self {
137 self.tree = Some(tree);
138 self
139 }
140}
141
142#[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 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#[derive(Debug, Default)]
196pub struct FetchEscrowOptions {
197 pub(crate) inspect_sink_client: Option<ClientEnd<finspect::InspectSinkMarker>>,
199
200 pub(crate) should_replace_with_tree: bool,
203}
204
205impl FetchEscrowOptions {
206 pub fn new() -> Self {
208 Self::default()
209 }
210
211 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 pub fn replace_with_tree(mut self) -> Self {
223 self.should_replace_with_tree = true;
224 self
225 }
226}
227
228pub struct FetchEscrowResult {
230 pub vmo: zx::Vmo,
232 pub server: Option<TreeServerHandle>,
234}
235
236pub struct TreeServerHandle {
238 client_koid: zx::Koid,
239 stream: finspect::TreeRequestStream,
240 inspect_sink: finspect::InspectSinkProxy,
241}
242
243#[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 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 pub fn name(mut self, name: impl Into<String>) -> Self {
313 self.name = Some(name.into());
314 self
315 }
316
317 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 #[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 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 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 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 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}