archivist_lib/inspect/
collector.rs

1// Copyright 2020 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 crate::inspect::container::InspectHandle;
6use diagnostics_data::InspectHandleName;
7use fidl::endpoints::{DiscoverableProtocolMarker, Proxy};
8use fidl_fuchsia_inspect::{TreeMarker, TreeProxy};
9use fidl_fuchsia_inspect_deprecated::{InspectMarker, InspectProxy};
10use fidl_fuchsia_io as fio;
11use futures::stream::StreamExt;
12use log::error;
13use std::pin::pin;
14use std::sync::{Arc, Weak};
15
16/// Pairs a diagnostics data-object's name to the underlying encoding of that data.
17pub type InspectHandleDeque = std::collections::VecDeque<(Option<InspectHandleName>, InspectData)>;
18
19/// Data associated with a component.
20/// This data is stored by data collectors and passed by the collectors to processors.
21#[derive(Debug)]
22pub enum InspectData {
23    /// A VMO containing data associated with the event.
24    Vmo { data: Arc<zx::Vmo>, escrowed: bool },
25
26    /// A file containing data associated with the event.
27    ///
28    /// Because we can't synchronously retrieve file contents like we can for VMOs, this holds
29    /// the full file contents. Future changes should make streaming ingestion feasible.
30    File(Vec<u8>),
31
32    /// A connection to a Tree service.
33    Tree(TreeProxy),
34
35    /// A connection to the deprecated Inspect service.
36    DeprecatedFidl(InspectProxy),
37}
38
39fn maybe_load_service<P: DiscoverableProtocolMarker>(
40    dir_proxy: &fio::DirectoryProxy,
41    entry: &fuchsia_fs::directory::DirEntry,
42) -> Result<Option<P::Proxy>, anyhow::Error> {
43    if entry.name.ends_with(P::PROTOCOL_NAME) {
44        let (proxy, server) = fidl::endpoints::create_proxy::<P>();
45        fdio::service_connect_at(
46            dir_proxy.as_channel().as_ref(),
47            &entry.name,
48            server.into_channel(),
49        )?;
50        return Ok(Some(proxy));
51    }
52    Ok(None)
53}
54
55pub async fn populate_data_map(inspect_handles: &[Weak<InspectHandle>]) -> InspectHandleDeque {
56    let mut data_map = InspectHandleDeque::new();
57    for inspect_handle in inspect_handles {
58        let Some(handle) = inspect_handle.upgrade() else {
59            continue;
60        };
61        match handle.as_ref() {
62            InspectHandle::Directory { proxy: ref dir } => {
63                return populate_data_map_from_dir(dir).await;
64            }
65            InspectHandle::Tree { proxy, name } => {
66                data_map.push_back((
67                    name.as_ref().map(|name| InspectHandleName::name(name.clone())),
68                    InspectData::Tree(proxy.clone()),
69                ));
70            }
71            InspectHandle::Escrow { vmo, name, .. } => {
72                data_map.push_back((
73                    name.as_ref().map(|name| InspectHandleName::name(name.clone())),
74                    InspectData::Vmo { data: Arc::clone(vmo), escrowed: true },
75                ));
76            }
77        }
78    }
79
80    data_map
81}
82
83/// Searches the directory specified by inspect_directory_proxy for
84/// .inspect files and populates the `inspect_data_map` with the found VMOs.
85async fn populate_data_map_from_dir(inspect_proxy: &fio::DirectoryProxy) -> InspectHandleDeque {
86    // TODO(https://fxbug.dev/42112326): Use a streaming and bounded readdir API when available to avoid
87    // being hung.
88    let mut entries = pin!(
89        fuchsia_fs::directory::readdir_recursive(inspect_proxy, /* timeout= */ None).filter_map(
90            |result| {
91                async move {
92                    // TODO(https://fxbug.dev/42126094): decide how to show directories that we
93                    // failed to read.
94                    result.ok()
95                }
96            }
97        )
98    );
99    let mut data_map = InspectHandleDeque::new();
100    // TODO(https://fxbug.dev/42138410) convert this async loop to a stream so we can carry backpressure
101    while let Some(entry) = entries.next().await {
102        // We are only currently interested in inspect VMO files (root.inspect) and
103        // inspect services.
104        if let Ok(Some(proxy)) = maybe_load_service::<TreeMarker>(inspect_proxy, &entry) {
105            data_map.push_back((
106                Some(InspectHandleName::filename(entry.name)),
107                InspectData::Tree(proxy),
108            ));
109            continue;
110        }
111
112        if let Ok(Some(proxy)) = maybe_load_service::<InspectMarker>(inspect_proxy, &entry) {
113            data_map.push_back((
114                Some(InspectHandleName::filename(entry.name)),
115                InspectData::DeprecatedFidl(proxy),
116            ));
117            continue;
118        }
119
120        if !entry.name.ends_with(".inspect")
121            || entry.kind != fuchsia_fs::directory::DirentKind::File
122        {
123            continue;
124        }
125
126        let file_proxy = match fuchsia_fs::directory::open_file_async(
127            inspect_proxy,
128            &entry.name,
129            fio::PERM_READABLE,
130        ) {
131            Ok(proxy) => proxy,
132            Err(_) => {
133                // It should be ok to not be able to read a file. The file might be closed by the
134                // time we get here.
135                continue;
136            }
137        };
138
139        // Obtain the backing vmo.
140        let vmo = match file_proxy.get_backing_memory(fio::VmoFlags::READ).await {
141            Ok(vmo) => vmo,
142            Err(_) => {
143                // It should be ok to not be able to read a file. The file might be closed by the
144                // time we get here.
145                continue;
146            }
147        };
148
149        let data = match vmo.map_err(zx::Status::from_raw) {
150            Ok(vmo) => InspectData::Vmo { data: Arc::new(vmo), escrowed: false },
151            Err(err) => {
152                match err {
153                    zx::Status::NOT_SUPPORTED => {}
154                    err => {
155                        error!(
156                            file:% = entry.name, err:?;
157                            "unexpected error from GetBackingMemory",
158                        )
159                    }
160                }
161                match fuchsia_fs::file::read(&file_proxy).await {
162                    Ok(contents) => InspectData::File(contents),
163                    Err(_) => {
164                        // It should be ok to not be able to read a file. The file might be closed
165                        // by the time we get here.
166                        continue;
167                    }
168                }
169            }
170        };
171        data_map.push_back((Some(InspectHandleName::filename(entry.name)), data));
172    }
173
174    data_map
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use assert_matches::assert_matches;
181    use diagnostics_assertions::assert_data_tree;
182    use fidl::endpoints::create_request_stream;
183    use fuchsia_async as fasync;
184    use fuchsia_component::server::ServiceFs;
185    use fuchsia_inspect::{Inspector, reader};
186    use inspect_runtime::TreeServerSendPreference;
187    use inspect_runtime::service::spawn_tree_server_with_stream;
188    use zx::Peered;
189
190    fn get_vmo(text: &[u8]) -> zx::Vmo {
191        let vmo = zx::Vmo::create(4096).unwrap();
192        vmo.write(text, 0).unwrap();
193        vmo
194    }
195
196    #[fuchsia::test]
197    async fn populate_data_map_with_trees() {
198        let insp1 = Inspector::default();
199        let insp2 = Inspector::default();
200        let insp3 = Inspector::default();
201
202        insp1.root().record_int("one", 1);
203        insp2.root().record_int("two", 2);
204        insp3.root().record_int("three", 3);
205
206        let scope = fasync::Scope::new();
207
208        let (tree1, request_stream) = create_request_stream::<TreeMarker>();
209        spawn_tree_server_with_stream(
210            insp1,
211            TreeServerSendPreference::default(),
212            request_stream,
213            &scope,
214        );
215        let (tree2, request_stream) = create_request_stream::<TreeMarker>();
216        spawn_tree_server_with_stream(
217            insp2,
218            TreeServerSendPreference::default(),
219            request_stream,
220            &scope,
221        );
222        let (tree3, request_stream) = create_request_stream::<TreeMarker>();
223        spawn_tree_server_with_stream(
224            insp3,
225            TreeServerSendPreference::default(),
226            request_stream,
227            &scope,
228        );
229
230        let name1 = Some(InspectHandleName::name("tree1"));
231        let name2 = Some(InspectHandleName::name("tree2"));
232        let name3 = None;
233
234        let handles = [
235            Arc::new(InspectHandle::Tree { proxy: tree1.into_proxy(), name: Some("tree1".into()) }),
236            Arc::new(InspectHandle::Tree { proxy: tree2.into_proxy(), name: Some("tree2".into()) }),
237            Arc::new(InspectHandle::Tree { proxy: tree3.into_proxy(), name: None }),
238        ];
239        let data = populate_data_map(&handles.iter().map(Arc::downgrade).collect::<Vec<_>>()).await;
240        assert_eq!(data.len(), 3);
241
242        let (_, tree1) = data.iter().find(|(n, _)| *n == name1).unwrap();
243
244        assert_matches!(tree1, InspectData::Tree(t) => {
245            let h = reader::read(t).await.unwrap();
246            assert_data_tree!(h, root: {
247                one: 1i64,
248            });
249        });
250
251        let (_, tree2) = data.iter().find(|(n, _)| *n == name2).unwrap();
252
253        assert_matches!(tree2, InspectData::Tree(t) => {
254            let h = reader::read(t).await.unwrap();
255            assert_data_tree!(h, root: {
256                two: 2i64,
257            });
258        });
259
260        let (_, tree3) = data.iter().find(|(n, _)| *n == name3).unwrap();
261
262        assert_matches!(tree3, InspectData::Tree(t) => {
263            let h = reader::read(t).await.unwrap();
264            assert_data_tree!(h, root: {
265                three: 3i64,
266            });
267        });
268    }
269
270    #[fuchsia::test]
271    async fn inspect_data_collector() {
272        let path = "/test-bindings/out";
273        // Make a ServiceFs containing two files.
274        // One is an inspect file, and one is not.
275        let mut fs = ServiceFs::new();
276        let vmo = get_vmo(b"test1");
277        let vmo2 = get_vmo(b"test2");
278        let vmo3 = get_vmo(b"test3");
279        let vmo4 = get_vmo(b"test4");
280        fs.dir("diagnostics").add_vmo_file_at("root.inspect", vmo);
281        fs.dir("diagnostics").add_vmo_file_at("root_not_inspect", vmo2);
282        fs.dir("diagnostics").dir("a").add_vmo_file_at("root.inspect", vmo3);
283        fs.dir("diagnostics").dir("b").add_vmo_file_at("root.inspect", vmo4);
284        // Create a connection to the ServiceFs.
285        let (h0, h1) = fidl::endpoints::create_endpoints();
286        fs.serve_connection(h1).unwrap();
287
288        let ns = fdio::Namespace::installed().unwrap();
289        ns.bind(path, h0).unwrap();
290
291        fasync::Task::spawn(fs.collect()).detach();
292
293        let (done0, done1) = zx::Channel::create();
294
295        // Run the actual test in a separate thread so that it does not block on FS operations.
296        // Use signalling on a zx::Channel to indicate that the test is done.
297        std::thread::spawn(move || {
298            let done = done1;
299            let mut executor = fasync::LocalExecutor::default();
300
301            executor.run_singlethreaded(async {
302                let inspect_proxy = Arc::new(InspectHandle::directory(
303                    fuchsia_fs::directory::open_in_namespace(
304                        &format!("{path}/diagnostics"),
305                        fio::PERM_READABLE,
306                    )
307                    .expect("Failed to open directory"),
308                ));
309                let extra_data = populate_data_map(&[Arc::downgrade(&inspect_proxy)]).await;
310                assert_eq!(3, extra_data.len());
311
312                let assert_extra_data = |path: &str, content: &[u8]| {
313                    let (_, extra) = extra_data
314                        .iter()
315                        .find(|(n, _)| *n == Some(InspectHandleName::filename(path)))
316                        .unwrap();
317
318                    match extra {
319                        InspectData::Vmo { data: vmo, escrowed: _ } => {
320                            let mut buf = [0u8; 5];
321                            vmo.read(&mut buf, 0).expect("reading vmo");
322                            assert_eq!(content, &buf);
323                        }
324                        v => {
325                            panic!("Expected Vmo, got {v:?}");
326                        }
327                    }
328                };
329
330                assert_extra_data("root.inspect", b"test1");
331                assert_extra_data("a/root.inspect", b"test3");
332                assert_extra_data("b/root.inspect", b"test4");
333
334                done.signal_peer(zx::Signals::NONE, zx::Signals::USER_0).expect("signalling peer");
335            });
336        });
337
338        fasync::OnSignals::new(&done0, zx::Signals::USER_0).await.unwrap();
339        ns.unbind(path).unwrap();
340    }
341}