1use 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#[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 pub(crate) custom_scope: Option<fasync::ScopeHandle>,
93
94 pub(crate) tree: Option<TreeServerHandle>,
96}
97
98impl PublishOptions {
99 pub fn send_vmo_preference(mut self, preference: TreeServerSendPreference) -> Self {
104 self.vmo_preference = preference;
105 self
106 }
107
108 pub fn inspect_tree_name(mut self, name: impl Into<String>) -> Self {
113 self.tree_name = Some(name.into());
114 self
115 }
116
117 pub fn custom_scope(mut self, scope: fasync::ScopeHandle) -> Self {
119 self.custom_scope = Some(scope);
120 self
121 }
122
123 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 pub fn on_tree_server(mut self, tree: TreeServerHandle) -> Self {
136 self.tree = Some(tree);
137 self
138 }
139}
140
141#[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 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#[derive(Debug, Default)]
190pub struct FetchEscrowOptions {
191 pub(crate) inspect_sink_client: Option<ClientEnd<finspect::InspectSinkMarker>>,
193
194 pub(crate) should_replace_with_tree: bool,
197}
198
199impl FetchEscrowOptions {
200 pub fn new() -> Self {
202 Self::default()
203 }
204
205 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 pub fn replace_with_tree(mut self) -> Self {
217 self.should_replace_with_tree = true;
218 self
219 }
220}
221
222pub struct FetchEscrowResult {
224 pub vmo: zx::Vmo,
226 pub server: Option<TreeServerHandle>,
228}
229
230pub struct TreeServerHandle {
232 client_koid: zx::Koid,
233 stream: finspect::TreeRequestStream,
234}
235
236#[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 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 pub fn name(mut self, name: impl Into<String>) -> Self {
302 self.name = Some(name.into());
303 self
304 }
305
306 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 #[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 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 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 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 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}