1use fidl_fuchsia_io as fio;
6use fuchsia_inspect as finspect;
7use futures::FutureExt as _;
8use futures::future::BoxFuture;
9use std::collections::HashMap;
10use std::collections::hash_map::Entry::{Occupied, Vacant};
11use std::sync::{Arc, Weak};
12use vfs::ObjectRequestRef;
13use vfs::directory::entry::{EntryInfo, OpenRequest};
14use vfs::directory::traversal_position::TraversalPosition;
15use vfs::execution_scope::ExecutionScope;
16
17#[derive(Debug, Clone)]
41pub struct RootDirCache<S> {
42 non_meta_storage: S,
43 dirs: Arc<std::sync::Mutex<HashMap<fuchsia_hash::Hash, Weak<CachedRootDir<S>>>>>,
44}
45
46impl<S: crate::NonMetaStorage + Clone> RootDirCache<S> {
47 pub fn new(non_meta_storage: S) -> Self {
50 let dirs = Arc::new(std::sync::Mutex::new(HashMap::new()));
51 Self { non_meta_storage, dirs }
52 }
53
54 pub async fn get_or_insert(
61 &self,
62 hash: fuchsia_hash::Hash,
63 root_dir: Option<crate::RootDir<S>>,
64 ) -> Result<Arc<CachedRootDir<S>>, crate::Error> {
65 if let Some(root_dir) = self.get(&hash) {
66 return Ok(root_dir);
67 }
68
69 let root_dir = match root_dir {
70 Some(root_dir) => root_dir,
71 None => crate::RootDir::new_raw(self.non_meta_storage.clone(), hash).await?,
72 };
73 Ok(match self.dirs.lock().expect("poisoned mutex").entry(hash) {
74 Occupied(mut o) => {
76 let old_root_dir = o.get_mut();
77 if let Some(old_root_dir) = old_root_dir.upgrade() {
78 old_root_dir
79 } else {
80 let new_root_dir = CachedRootDir::new(root_dir, &self.dirs);
81 *old_root_dir = Arc::downgrade(&new_root_dir);
82 new_root_dir
83 }
84 }
85 Vacant(v) => {
86 let new_root_dir = CachedRootDir::new(root_dir, &self.dirs);
87 v.insert(Arc::downgrade(&new_root_dir));
88 new_root_dir
89 }
90 })
91 }
92
93 pub fn get(&self, hash: &fuchsia_hash::Hash) -> Option<Arc<CachedRootDir<S>>> {
98 self.dirs.lock().expect("poisoned mutex").get(hash)?.upgrade()
99 }
100
101 pub fn list(&self) -> Vec<Arc<CachedRootDir<S>>> {
104 self.dirs.lock().expect("poisoned mutex").values().filter_map(|v| v.upgrade()).collect()
105 }
106
107 pub fn record_lazy_inspect(
110 &self,
111 ) -> impl Fn() -> BoxFuture<'static, Result<finspect::Inspector, anyhow::Error>>
112 + Send
113 + Sync
114 + 'static {
115 let dirs = Arc::downgrade(&self.dirs);
116 move || {
117 let dirs = dirs.clone();
118 async move {
119 let inspector = finspect::Inspector::default();
120 if let Some(dirs) = dirs.upgrade() {
121 let package_counts: HashMap<_, _> = {
122 let dirs = dirs.lock().expect("poisoned mutex");
123 dirs.iter().map(|(k, v)| (*k, v.strong_count() as u64)).collect()
124 };
125 let root = inspector.root();
126 let () = package_counts.into_iter().for_each(|(pkg, count)| {
127 root.record_child(pkg.to_string(), |n| n.record_uint("instances", count))
128 });
129 }
130 Ok(inspector)
131 }
132 .boxed()
133 }
134 }
135}
136
137#[derive(Debug)]
139pub struct CachedRootDir<S> {
140 root_dir: crate::RootDir<S>,
141 dirs: Weak<std::sync::Mutex<HashMap<fuchsia_hash::Hash, Weak<Self>>>>,
142}
143
144impl<S> CachedRootDir<S> {
145 #[allow(clippy::type_complexity)]
146 fn new(
147 root_dir: crate::RootDir<S>,
148 dirs: &Arc<std::sync::Mutex<HashMap<fuchsia_hash::Hash, Weak<Self>>>>,
149 ) -> Arc<Self> {
150 Arc::new(Self { root_dir, dirs: Arc::downgrade(dirs) })
151 }
152}
153
154impl<S> std::ops::Deref for CachedRootDir<S> {
155 type Target = crate::RootDir<S>;
156
157 fn deref(&self) -> &Self::Target {
158 &self.root_dir
159 }
160}
161
162impl<S: crate::NonMetaStorage> crate::root_dir::AsRootDir for CachedRootDir<S> {
163 type Storage = S;
164
165 fn as_root_dir(&self) -> &crate::RootDir<S> {
166 &self.root_dir
167 }
168}
169
170impl<S: crate::NonMetaStorage> vfs::directory::entry::DirectoryEntry for CachedRootDir<S> {
171 fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), zx::Status> {
172 request.open_dir(self)
173 }
174}
175
176impl<S: crate::NonMetaStorage> vfs::directory::entry::GetEntryInfo for CachedRootDir<S> {
177 fn entry_info(&self) -> EntryInfo {
178 self.root_dir.entry_info()
179 }
180}
181
182impl<S: crate::NonMetaStorage> vfs::node::Node for CachedRootDir<S> {
183 async fn get_attributes(
184 &self,
185 requested_attributes: fio::NodeAttributesQuery,
186 ) -> Result<fio::NodeAttributes2, zx::Status> {
187 self.root_dir.get_attributes(requested_attributes).await
188 }
189}
190
191impl<S: crate::NonMetaStorage> vfs::directory::entry_container::Directory for CachedRootDir<S> {
192 fn open(
193 self: Arc<Self>,
194 scope: ExecutionScope,
195 path: vfs::Path,
196 flags: fio::Flags,
197 object_request: ObjectRequestRef<'_>,
198 ) -> Result<(), zx::Status> {
199 crate::root_dir::open_impl(self, scope, path, flags, object_request)
200 }
201
202 async fn read_dirents(
203 &self,
204 pos: &TraversalPosition,
205 sink: Box<dyn vfs::directory::dirents_sink::Sink + 'static>,
206 ) -> Result<
207 (TraversalPosition, Box<dyn vfs::directory::dirents_sink::Sealed + 'static>),
208 zx::Status,
209 > {
210 self.root_dir.read_dirents(pos, sink).await
211 }
212
213 fn register_watcher(
214 self: Arc<Self>,
215 _: ExecutionScope,
216 _: fio::WatchMask,
217 _: vfs::directory::entry_container::DirectoryWatcher,
218 ) -> Result<(), zx::Status> {
219 Err(zx::Status::NOT_SUPPORTED)
220 }
221
222 fn unregister_watcher(self: Arc<Self>, _: usize) {}
224}
225
226impl<S> Drop for CachedRootDir<S> {
227 fn drop(&mut self) {
228 let Some(dirs) = self.dirs.upgrade() else {
229 return;
230 };
231 match dirs.lock().expect("poisoned mutex").entry(self.root_dir.hash) {
232 Occupied(o) => {
233 if o.get().strong_count() == 0 {
235 o.remove_entry();
236 }
237 }
238 Vacant(_) => (),
240 };
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use assert_matches::assert_matches;
248 use diagnostics_assertions::assert_data_tree;
249 use fuchsia_async as fasync;
250 use fuchsia_pkg_testing::PackageBuilder;
251 use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
252
253 #[fuchsia::test]
254 async fn get_or_insert_new_entry() {
255 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
256 let (metafar_blob, _) = pkg.contents();
257 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
258 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
259 let server = RootDirCache::new(blobfs_client);
260
261 let dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
262
263 assert_eq!(server.list().len(), 1);
264
265 drop(dir);
266 assert_eq!(server.list().len(), 0);
267 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
268 }
269
270 #[fuchsia::test]
271 async fn closing_package_connection_closes_package() {
272 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
273 let (metafar_blob, _) = pkg.contents();
274 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
275 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
276 let server = RootDirCache::new(blobfs_client);
277
278 let dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
279 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
280 let scope = vfs::execution_scope::ExecutionScope::new();
281 vfs::directory::serve_on(dir, fio::PERM_READABLE, scope.clone(), server_end);
282 let _ = proxy
283 .get_attributes(Default::default())
284 .await
285 .expect("directory succesfully handling requests");
286 assert_eq!(server.list().len(), 1);
287
288 drop(proxy);
289 let () = scope.wait().await;
290 assert_eq!(server.list().len(), 0);
291 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
292 }
293
294 #[fuchsia::test]
295 async fn open_subdirectory_keeps_package_open() {
296 let pkg = PackageBuilder::new("pkg-name")
297 .add_resource_at("dir/file", "bloblob".as_bytes())
298 .add_resource_at("meta/dir/file", "meta-contents".as_bytes())
299 .build()
300 .await
301 .unwrap();
302 let (metafar_blob, content_blobs) = pkg.contents();
303 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
304 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
305 for (hash, bytes) in content_blobs {
306 blobfs_fake.add_blob(hash, bytes);
307 }
308 let server = RootDirCache::new(blobfs_client);
309
310 for subdir_path in ["meta", "meta/dir", "dir"] {
311 let dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
312 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
313 let scope = vfs::execution_scope::ExecutionScope::new();
314 vfs::directory::serve_on(dir, fio::PERM_READABLE, scope.clone(), server_end);
315 let subdir =
316 fuchsia_fs::directory::open_directory(&proxy, subdir_path, fio::PERM_READABLE)
317 .await
318 .unwrap();
319 assert_eq!(Arc::strong_count(&server.list()[0]), 3);
320
321 drop(proxy);
322 while Arc::strong_count(&server.list()[0]) != 2 {
323 let () = fasync::Timer::new(std::time::Duration::from_millis(10)).await;
324 }
325
326 drop(subdir);
327 let () = scope.wait().await;
328 assert_eq!(server.list().len(), 0);
329 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
330 }
331 }
332
333 #[fuchsia::test]
334 async fn get_or_insert_existing_entry() {
335 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
336 let (metafar_blob, _) = pkg.contents();
337 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
338 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
339 let server = RootDirCache::new(blobfs_client);
340
341 let dir0 = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
342
343 let dir1 = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
344 assert_eq!(server.list().len(), 1);
345 assert_eq!(Arc::strong_count(&server.list()[0]), 3);
346
347 drop(dir0);
348 drop(dir1);
349 assert_eq!(server.list().len(), 0);
350 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
351 }
352
353 #[fuchsia::test]
354 async fn get_or_insert_provided_root_dir() {
355 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
356 let (metafar_blob, _) = pkg.contents();
357 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
358 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
359 let root_dir =
360 crate::RootDir::new_raw(blobfs_client.clone(), metafar_blob.merkle).await.unwrap();
361 blobfs_fake.delete_blob(metafar_blob.merkle);
362 let server = RootDirCache::new(blobfs_client);
363
364 let dir = server.get_or_insert(metafar_blob.merkle, Some(root_dir)).await.unwrap();
365 assert_eq!(server.list().len(), 1);
366
367 drop(dir);
368 assert_eq!(server.list().len(), 0);
369 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
370 }
371
372 #[fuchsia::test]
373 async fn get_or_insert_fails_if_root_dir_creation_fails() {
374 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
375 let server = RootDirCache::new(blobfs_client);
376
377 assert_matches!(
378 server.get_or_insert([0; 32].into(), None).await,
379 Err(crate::Error::MissingMetaFar)
380 );
381 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
382 }
383
384 #[fuchsia::test]
385 async fn get_or_insert_concurrent_race_to_insert_new_root_dir() {
386 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
387 let (metafar_blob, _) = pkg.contents();
388 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
389 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
390 let server = RootDirCache::new(blobfs_client);
391
392 let fut0 = server.get_or_insert(metafar_blob.merkle, None);
393
394 let fut1 = server.get_or_insert(metafar_blob.merkle, None);
395
396 let (res0, res1) = futures::future::join(fut0, fut1).await;
397 let (dir0, dir1) = (res0.unwrap(), res1.unwrap());
398
399 assert_eq!(server.list().len(), 1);
400 assert_eq!(Arc::strong_count(&server.list()[0]), 3);
401
402 drop(dir0);
403 drop(dir1);
404 assert_eq!(server.list().len(), 0);
405 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
406 }
407
408 #[fuchsia::test]
409 async fn inspect() {
410 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
411 let (metafar_blob, _) = pkg.contents();
412 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
413 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
414 let server = RootDirCache::new(blobfs_client);
415 let _dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
416
417 let inspector = finspect::Inspector::default();
418 inspector.root().record_lazy_child("open-packages", server.record_lazy_inspect());
419
420 assert_data_tree!(inspector, root: {
421 "open-packages": {
422 pkg.hash().to_string() => {
423 "instances": 1u64,
424 },
425 }
426 });
427 }
428}