fs_management/
filesystem.rs

1// Copyright 2021 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
5//! Contains the asynchronous version of [`Filesystem`][`crate::Filesystem`].
6
7use crate::error::{QueryError, ShutdownError};
8use crate::{ComponentType, FSConfig, Options};
9use anyhow::{anyhow, bail, ensure, Context, Error};
10use fidl::endpoints::{create_endpoints, create_proxy, ClientEnd, ServerEnd};
11use fidl_fuchsia_component::{self as fcomponent, RealmMarker};
12use fidl_fuchsia_fs::AdminMarker;
13use fidl_fuchsia_fs_startup::{CheckOptions, CreateOptions, MountOptions, StartupMarker};
14use fidl_fuchsia_hardware_block_volume::VolumeMarker;
15use fuchsia_component_client::{
16    connect_to_named_protocol_at_dir_root, connect_to_protocol, connect_to_protocol_at_dir_root,
17    connect_to_protocol_at_dir_svc, open_childs_exposed_directory,
18};
19use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20use std::sync::Arc;
21use zx::{self as zx, AsHandleRef as _, Status};
22use {fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_io as fio};
23
24/// An abstract connector for things that speak fuchsia.hardware.block.Block and similar protocols.
25pub trait BlockConnector: Send + Sync {
26    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error>;
27    fn connect_volume(&self) -> Result<ClientEnd<VolumeMarker>, Error> {
28        let (client, server) = fidl::endpoints::create_endpoints();
29        self.connect_channel_to_volume(server)?;
30        Ok(client)
31    }
32    fn connect_partition(
33        &self,
34    ) -> Result<ClientEnd<fidl_fuchsia_hardware_block_partition::PartitionMarker>, Error> {
35        self.connect_volume().map(|v| ClientEnd::new(v.into_channel()))
36    }
37    fn connect_block(&self) -> Result<ClientEnd<fidl_fuchsia_hardware_block::BlockMarker>, Error> {
38        self.connect_volume().map(|v| ClientEnd::new(v.into_channel()))
39    }
40}
41
42/// Implements `BlockConnector` via a service dir.  Wraps `connect_to_named_protocol_at_dir_root`.
43#[derive(Clone, Debug)]
44pub struct DirBasedBlockConnector(fio::DirectoryProxy, String);
45
46impl DirBasedBlockConnector {
47    pub fn new(dir: fio::DirectoryProxy, path: String) -> Self {
48        Self(dir, path)
49    }
50
51    pub fn path(&self) -> &str {
52        &self.1
53    }
54}
55
56impl BlockConnector for DirBasedBlockConnector {
57    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
58        self.0.open(
59            self.path(),
60            fio::Flags::PROTOCOL_SERVICE,
61            &fio::Options::default(),
62            server_end.into_channel(),
63        )?;
64        Ok(())
65    }
66}
67
68impl BlockConnector for fidl_fuchsia_device::ControllerProxy {
69    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
70        let () = self.connect_to_device_fidl(server_end.into_channel())?;
71        Ok(())
72    }
73}
74
75impl BlockConnector for fidl_fuchsia_storage_partitions::PartitionServiceProxy {
76    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
77        self.connect_channel_to_volume(server_end)?;
78        Ok(())
79    }
80}
81
82// NB: We have to be specific here; we cannot do a blanket impl for AsRef<T: BlockConnector> because
83// that would conflict with a downstream crate that implements AsRef for a concrete BlockConnector
84// defined here already.
85impl<T: BlockConnector> BlockConnector for Arc<T> {
86    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
87        self.as_ref().connect_channel_to_volume(server_end)
88    }
89}
90
91impl<F> BlockConnector for F
92where
93    F: Fn(ServerEnd<VolumeMarker>) -> Result<(), Error> + Send + Sync,
94{
95    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
96        self(server_end)
97    }
98}
99
100/// Asynchronously manages a block device for filesystem operations.
101pub struct Filesystem {
102    /// The filesystem struct keeps the FSConfig in a Box<dyn> instead of holding it directly for
103    /// code size reasons. Using a type parameter instead would make monomorphized versions of the
104    /// Filesystem impl block for each filesystem type, which duplicates several multi-kilobyte
105    /// functions (get_component_exposed_dir and serve in particular) that are otherwise quite
106    /// generic over config. Clients that want to be generic over filesystem type also pay the
107    /// monomorphization cost, with some, like fshost, paying a lot.
108    config: Box<dyn FSConfig>,
109    block_connector: Box<dyn BlockConnector>,
110    component: Option<Arc<DynamicComponentInstance>>,
111}
112
113// Used to disambiguate children in our component collection.
114static COLLECTION_COUNTER: AtomicU64 = AtomicU64::new(0);
115
116impl Filesystem {
117    pub fn config(&self) -> &dyn FSConfig {
118        self.config.as_ref()
119    }
120
121    pub fn into_config(self) -> Box<dyn FSConfig> {
122        self.config
123    }
124
125    /// Creates a new `Filesystem`.
126    pub fn new<B: BlockConnector + 'static, FSC: FSConfig>(
127        block_connector: B,
128        config: FSC,
129    ) -> Self {
130        Self::from_boxed_config(Box::new(block_connector), Box::new(config))
131    }
132
133    /// Creates a new `Filesystem`.
134    pub fn from_boxed_config(
135        block_connector: Box<dyn BlockConnector>,
136        config: Box<dyn FSConfig>,
137    ) -> Self {
138        Self { config, block_connector, component: None }
139    }
140
141    /// If the filesystem is a currently running component, returns its (relative) moniker.
142    pub fn get_component_moniker(&self) -> Option<String> {
143        Some(match self.config.options().component_type {
144            ComponentType::StaticChild => self.config.options().component_name.to_string(),
145            ComponentType::DynamicChild { .. } => {
146                let component = self.component.as_ref()?;
147                format!("{}:{}", component.collection, component.name)
148            }
149        })
150    }
151
152    async fn get_component_exposed_dir(&mut self) -> Result<fio::DirectoryProxy, Error> {
153        let options = self.config.options();
154        let component_name = options.component_name;
155        match options.component_type {
156            ComponentType::StaticChild => open_childs_exposed_directory(component_name, None).await,
157            ComponentType::DynamicChild { collection_name } => {
158                if let Some(component) = &self.component {
159                    return open_childs_exposed_directory(
160                        component.name.clone(),
161                        Some(component.collection.clone()),
162                    )
163                    .await;
164                }
165
166                // We need a unique name, so we pull in the process Koid here since it's possible
167                // for the same binary in a component to be launched multiple times and we don't
168                // want to collide with children created by other processes.
169                let name = format!(
170                    "{}-{}-{}",
171                    component_name,
172                    fuchsia_runtime::process_self().get_koid().unwrap().raw_koid(),
173                    COLLECTION_COUNTER.fetch_add(1, Ordering::Relaxed)
174                );
175
176                let collection_ref = fdecl::CollectionRef { name: collection_name };
177                let child_decls = vec![
178                    fdecl::Child {
179                        name: Some(format!("{}-relative", name)),
180                        url: Some(format!("#meta/{}.cm", component_name)),
181                        startup: Some(fdecl::StartupMode::Lazy),
182                        ..Default::default()
183                    },
184                    fdecl::Child {
185                        name: Some(name),
186                        url: Some(format!(
187                            "fuchsia-boot:///{}#meta/{}.cm",
188                            component_name, component_name
189                        )),
190                        startup: Some(fdecl::StartupMode::Lazy),
191                        ..Default::default()
192                    },
193                ];
194                let realm_proxy = connect_to_protocol::<RealmMarker>()?;
195                for child_decl in child_decls {
196                    // Launch a new component in our collection.
197                    realm_proxy
198                        .create_child(
199                            &collection_ref,
200                            &child_decl,
201                            fcomponent::CreateChildArgs::default(),
202                        )
203                        .await?
204                        .map_err(|e| anyhow!("create_child failed: {:?}", e))?;
205
206                    let component = Arc::new(DynamicComponentInstance {
207                        name: child_decl.name.unwrap(),
208                        collection: collection_ref.name.clone(),
209                        should_not_drop: AtomicBool::new(false),
210                    });
211
212                    if let Ok(proxy) = open_childs_exposed_directory(
213                        component.name.clone(),
214                        Some(component.collection.clone()),
215                    )
216                    .await
217                    {
218                        self.component = Some(component);
219                        return Ok(proxy);
220                    }
221                }
222                Err(anyhow!("Failed to open exposed directory"))
223            }
224        }
225    }
226
227    /// Calls fuchsia.fs.startup/Startup.Format on the configured filesystem component.
228    ///
229    /// Which component is used and the options passed to it are controlled by the config this
230    /// `Filesystem` was created with.
231    ///
232    /// See [`FSConfig`].
233    ///
234    /// # Errors
235    ///
236    /// Returns any errors from the Format method. Also returns an error if the startup protocol is
237    /// not found, if it couldn't launch or find the filesystem component, or if it couldn't get
238    /// the block device channel.
239    pub async fn format(&mut self) -> Result<(), Error> {
240        let channel = self.block_connector.connect_block()?;
241
242        let exposed_dir = self.get_component_exposed_dir().await?;
243        let proxy = connect_to_protocol_at_dir_root::<StartupMarker>(&exposed_dir)?;
244        proxy
245            .format(channel, &self.config().options().format_options)
246            .await?
247            .map_err(Status::from_raw)?;
248
249        Ok(())
250    }
251
252    /// Calls fuchsia.fs.startup/Startup.Check on the configured filesystem component.
253    ///
254    /// Which component is used and the options passed to it are controlled by the config this
255    /// `Filesystem` was created with.
256    ///
257    /// See [`FSConfig`].
258    ///
259    /// # Errors
260    ///
261    /// Returns any errors from the Check method. Also returns an error if the startup protocol is
262    /// not found, if it couldn't launch or find the filesystem component, or if it couldn't get
263    /// the block device channel.
264    pub async fn fsck(&mut self) -> Result<(), Error> {
265        let channel = self.block_connector.connect_block()?;
266        let exposed_dir = self.get_component_exposed_dir().await?;
267        let proxy = connect_to_protocol_at_dir_root::<StartupMarker>(&exposed_dir)?;
268        proxy.check(channel, CheckOptions::default()).await?.map_err(Status::from_raw)?;
269        Ok(())
270    }
271
272    /// Serves the filesystem on the block device and returns a [`ServingSingleVolumeFilesystem`]
273    /// representing the running filesystem component.
274    ///
275    /// # Errors
276    ///
277    /// Returns [`Err`] if serving the filesystem failed.
278    pub async fn serve(&mut self) -> Result<ServingSingleVolumeFilesystem, Error> {
279        if self.config.is_multi_volume() {
280            bail!("Can't serve a multivolume filesystem; use serve_multi_volume");
281        }
282        let Options { start_options, reuse_component_after_serving, .. } = self.config.options();
283
284        let exposed_dir = self.get_component_exposed_dir().await?;
285        let proxy = connect_to_protocol_at_dir_root::<StartupMarker>(&exposed_dir)?;
286        proxy
287            .start(self.block_connector.connect_block()?, &start_options)
288            .await?
289            .map_err(Status::from_raw)?;
290
291        let (root_dir, server_end) = create_endpoints::<fio::NodeMarker>();
292        exposed_dir.open(
293            "root",
294            fio::PERM_READABLE | fio::Flags::PERM_INHERIT_WRITE | fio::Flags::PERM_INHERIT_EXECUTE,
295            &Default::default(),
296            server_end.into_channel(),
297        )?;
298        let component = self.component.clone();
299        if !reuse_component_after_serving {
300            self.component = None;
301        }
302        Ok(ServingSingleVolumeFilesystem {
303            component,
304            exposed_dir: Some(exposed_dir),
305            root_dir: ClientEnd::<fio::DirectoryMarker>::new(root_dir.into_channel()).into_proxy(),
306            binding: None,
307        })
308    }
309
310    /// Serves the filesystem on the block device and returns a [`ServingMultiVolumeFilesystem`]
311    /// representing the running filesystem component.  No volumes are opened; clients have to do
312    /// that explicitly.
313    ///
314    /// # Errors
315    ///
316    /// Returns [`Err`] if serving the filesystem failed.
317    pub async fn serve_multi_volume(&mut self) -> Result<ServingMultiVolumeFilesystem, Error> {
318        if !self.config.is_multi_volume() {
319            bail!("Can't serve_multi_volume a single-volume filesystem; use serve");
320        }
321
322        let exposed_dir = self.get_component_exposed_dir().await?;
323        let proxy = connect_to_protocol_at_dir_root::<StartupMarker>(&exposed_dir)?;
324        proxy
325            .start(self.block_connector.connect_block()?, &self.config.options().start_options)
326            .await?
327            .map_err(Status::from_raw)?;
328
329        Ok(ServingMultiVolumeFilesystem {
330            component: self.component.clone(),
331            exposed_dir: Some(exposed_dir),
332        })
333    }
334}
335
336// Destroys the child when dropped.
337struct DynamicComponentInstance {
338    name: String,
339    collection: String,
340    should_not_drop: AtomicBool,
341}
342
343impl DynamicComponentInstance {
344    fn forget(&self) {
345        self.should_not_drop.store(true, Ordering::Relaxed);
346    }
347}
348
349impl Drop for DynamicComponentInstance {
350    fn drop(&mut self) {
351        if self.should_not_drop.load(Ordering::Relaxed) {
352            return;
353        }
354        if let Ok(realm_proxy) = connect_to_protocol::<RealmMarker>() {
355            let _ = realm_proxy.destroy_child(&fdecl::ChildRef {
356                name: self.name.clone(),
357                collection: Some(self.collection.clone()),
358            });
359        }
360    }
361}
362
363/// Manages the binding of a `fuchsia_io::DirectoryProxy` into the local namespace.  When the object
364/// is dropped, the binding is removed.
365#[derive(Default)]
366pub struct NamespaceBinding(String);
367
368impl NamespaceBinding {
369    pub fn create(root_dir: &fio::DirectoryProxy, path: String) -> Result<NamespaceBinding, Error> {
370        let (client_end, server_end) = create_endpoints();
371        root_dir.clone(ServerEnd::new(server_end.into_channel()))?;
372        let namespace = fdio::Namespace::installed()?;
373        namespace.bind(&path, client_end)?;
374        Ok(Self(path))
375    }
376}
377
378impl std::ops::Deref for NamespaceBinding {
379    type Target = str;
380    fn deref(&self) -> &Self::Target {
381        &self.0
382    }
383}
384
385impl Drop for NamespaceBinding {
386    fn drop(&mut self) {
387        if let Ok(namespace) = fdio::Namespace::installed() {
388            let _ = namespace.unbind(&self.0);
389        }
390    }
391}
392
393// TODO(https://fxbug.dev/42174810): Soft migration; remove this after completion
394pub type ServingFilesystem = ServingSingleVolumeFilesystem;
395
396/// Asynchronously manages a serving filesystem. Created from [`Filesystem::serve()`].
397pub struct ServingSingleVolumeFilesystem {
398    component: Option<Arc<DynamicComponentInstance>>,
399    // exposed_dir will always be Some, except when the filesystem is shutting down.
400    exposed_dir: Option<fio::DirectoryProxy>,
401    root_dir: fio::DirectoryProxy,
402
403    // The path in the local namespace that this filesystem is bound to (optional).
404    binding: Option<NamespaceBinding>,
405}
406
407impl ServingSingleVolumeFilesystem {
408    /// Returns a proxy to the exposed directory of the serving filesystem.
409    pub fn exposed_dir(&self) -> &fio::DirectoryProxy {
410        self.exposed_dir.as_ref().unwrap()
411    }
412
413    /// Returns a proxy to the root directory of the serving filesystem.
414    pub fn root(&self) -> &fio::DirectoryProxy {
415        &self.root_dir
416    }
417
418    /// Binds the root directory being served by this filesystem to a path in the local namespace.
419    /// The path must be absolute, containing no "." nor ".." entries.  The binding will be dropped
420    /// when self is dropped.  Only one binding is supported.
421    ///
422    /// # Errors
423    ///
424    /// Returns [`Err`] if binding failed.
425    pub fn bind_to_path(&mut self, path: &str) -> Result<(), Error> {
426        ensure!(self.binding.is_none(), "Already bound");
427        self.binding = Some(NamespaceBinding::create(&self.root_dir, path.to_string())?);
428        Ok(())
429    }
430
431    pub fn bound_path(&self) -> Option<&str> {
432        self.binding.as_deref()
433    }
434
435    /// Returns a [`FilesystemInfo`] object containing information about the serving filesystem.
436    ///
437    /// # Errors
438    ///
439    /// Returns [`Err`] if querying the filesystem failed.
440    pub async fn query(&self) -> Result<Box<fio::FilesystemInfo>, QueryError> {
441        let (status, info) = self.root_dir.query_filesystem().await?;
442        Status::ok(status).map_err(QueryError::DirectoryQuery)?;
443        info.ok_or(QueryError::DirectoryEmptyResult)
444    }
445
446    /// Take the exposed dir from this filesystem instance, dropping the management struct without
447    /// shutting the filesystem down. This leaves the caller with the responsibility of shutting
448    /// down the filesystem, and the filesystem component if necessary.
449    pub fn take_exposed_dir(mut self) -> fio::DirectoryProxy {
450        self.component.take().expect("BUG: component missing").forget();
451        self.exposed_dir.take().expect("BUG: exposed dir missing")
452    }
453
454    /// Attempts to shutdown the filesystem using the
455    /// [`fidl_fuchsia_fs::AdminProxy::shutdown()`] FIDL method and waiting for the filesystem
456    /// process to terminate.
457    ///
458    /// # Errors
459    ///
460    /// Returns [`Err`] if the shutdown failed or the filesystem process did not terminate.
461    pub async fn shutdown(mut self) -> Result<(), ShutdownError> {
462        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(
463            &self.exposed_dir.take().expect("BUG: exposed dir missing"),
464        )?
465        .shutdown()
466        .await?;
467        Ok(())
468    }
469
470    /// Attempts to kill the filesystem process and waits for the process to terminate.
471    ///
472    /// # Errors
473    ///
474    /// Returns [`Err`] if the filesystem process could not be terminated. There is no way to
475    /// recover the [`Filesystem`] from this error.
476    pub async fn kill(self) -> Result<(), Error> {
477        // For components, just shut down the filesystem.
478        // TODO(https://fxbug.dev/293949323): Figure out a way to make this more abrupt - the use-cases are
479        // either testing or when the filesystem isn't responding.
480        self.shutdown().await?;
481        Ok(())
482    }
483}
484
485impl Drop for ServingSingleVolumeFilesystem {
486    fn drop(&mut self) {
487        // Make a best effort attempt to shut down to the filesystem, if we need to.
488        if let Some(exposed_dir) = self.exposed_dir.take() {
489            if let Ok(proxy) =
490                connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(&exposed_dir)
491            {
492                let _ = proxy.shutdown();
493            }
494        }
495    }
496}
497
498/// Asynchronously manages a serving multivolume filesystem. Created from
499/// [`Filesystem::serve_multi_volume()`].
500pub struct ServingMultiVolumeFilesystem {
501    component: Option<Arc<DynamicComponentInstance>>,
502    // exposed_dir will always be Some, except in Self::shutdown.
503    exposed_dir: Option<fio::DirectoryProxy>,
504}
505
506/// Represents an opened volume in a [`ServingMultiVolumeFilesystem'] instance.
507pub struct ServingVolume {
508    root_dir: fio::DirectoryProxy,
509    binding: Option<NamespaceBinding>,
510    exposed_dir: fio::DirectoryProxy,
511}
512
513impl ServingVolume {
514    fn new(exposed_dir: fio::DirectoryProxy) -> Result<Self, Error> {
515        let (root_dir, server_end) = create_endpoints::<fio::NodeMarker>();
516        exposed_dir.open(
517            "root",
518            fio::PERM_READABLE | fio::Flags::PERM_INHERIT_WRITE | fio::Flags::PERM_INHERIT_EXECUTE,
519            &Default::default(),
520            server_end.into_channel(),
521        )?;
522        Ok(ServingVolume {
523            root_dir: ClientEnd::<fio::DirectoryMarker>::new(root_dir.into_channel()).into_proxy(),
524            binding: None,
525            exposed_dir,
526        })
527    }
528
529    /// Returns a proxy to the root directory of the serving volume.
530    pub fn root(&self) -> &fio::DirectoryProxy {
531        &self.root_dir
532    }
533
534    /// Returns a proxy to the exposed directory of the serving volume.
535    pub fn exposed_dir(&self) -> &fio::DirectoryProxy {
536        &self.exposed_dir
537    }
538
539    /// Binds the root directory being served by this filesystem to a path in the local namespace.
540    /// The path must be absolute, containing no "." nor ".." entries.  The binding will be dropped
541    /// when self is dropped, or when unbind_path is called.  Only one binding is supported.
542    ///
543    /// # Errors
544    ///
545    /// Returns [`Err`] if binding failed, or if a binding already exists.
546    pub fn bind_to_path(&mut self, path: &str) -> Result<(), Error> {
547        ensure!(self.binding.is_none(), "Already bound");
548        self.binding = Some(NamespaceBinding::create(&self.root_dir, path.to_string())?);
549        Ok(())
550    }
551
552    /// Remove the namespace binding to the root directory being served by this volume, if there is
553    /// one. If there is no binding, this function does nothing. After this, it is safe to call
554    /// bind_to_path again.
555    pub fn unbind_path(&mut self) {
556        let _ = self.binding.take();
557    }
558
559    pub fn bound_path(&self) -> Option<&str> {
560        self.binding.as_deref()
561    }
562
563    /// Returns a [`FilesystemInfo`] object containing information about the serving volume.
564    ///
565    /// # Errors
566    ///
567    /// Returns [`Err`] if querying the filesystem failed.
568    pub async fn query(&self) -> Result<Box<fio::FilesystemInfo>, QueryError> {
569        let (status, info) = self.root_dir.query_filesystem().await?;
570        Status::ok(status).map_err(QueryError::DirectoryQuery)?;
571        info.ok_or(QueryError::DirectoryEmptyResult)
572    }
573
574    /// Attempts to shutdown the filesystem using the [`fidl_fuchsia_fs::AdminProxy::shutdown()`]
575    /// FIDL method. Fails if the volume is not already open.
576    pub async fn shutdown(self) -> Result<(), Error> {
577        let admin_proxy = connect_to_protocol_at_dir_svc::<AdminMarker>(self.exposed_dir())?;
578        admin_proxy.shutdown().await.context("failed to shutdown volume")?;
579        Ok(())
580    }
581}
582
583impl ServingMultiVolumeFilesystem {
584    /// Returns whether the given volume exists.
585    pub async fn has_volume(&mut self, volume: &str) -> Result<bool, Error> {
586        let path = format!("volumes/{}", volume);
587        fuchsia_fs::directory::open_node(
588            self.exposed_dir.as_ref().unwrap(),
589            &path,
590            fio::Flags::PROTOCOL_NODE,
591        )
592        .await
593        .map(|_| true)
594        .or_else(|e| {
595            if let fuchsia_fs::node::OpenError::OpenError(status) = &e {
596                if *status == zx::Status::NOT_FOUND {
597                    return Ok(false);
598                }
599            }
600            Err(e.into())
601        })
602    }
603
604    /// Creates and mounts the volume.  Fails if the volume already exists.
605    /// If `options.crypt` is set, the volume will be encrypted using the provided Crypt instance.
606    /// If `options.as_blob` is set, creates a blob volume that is mounted as a blob filesystem.
607    pub async fn create_volume(
608        &self,
609        volume: &str,
610        create_options: CreateOptions,
611        options: MountOptions,
612    ) -> Result<ServingVolume, Error> {
613        let (exposed_dir, server) = create_proxy::<fio::DirectoryMarker>();
614        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumesMarker>(
615            self.exposed_dir.as_ref().unwrap(),
616        )?
617        .create(volume, server, create_options, options)
618        .await?
619        .map_err(|e| anyhow!(zx::Status::from_raw(e)))?;
620        ServingVolume::new(exposed_dir)
621    }
622
623    /// Deletes the volume. Fails if the volume is already mounted.
624    pub async fn remove_volume(&self, volume: &str) -> Result<(), Error> {
625        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumesMarker>(
626            self.exposed_dir.as_ref().unwrap(),
627        )?
628        .remove(volume)
629        .await?
630        .map_err(|e| anyhow!(zx::Status::from_raw(e)))
631    }
632
633    /// Mounts an existing volume.  Fails if the volume is already mounted or doesn't exist.
634    /// If `crypt` is set, the volume will be decrypted using the provided Crypt instance.
635    pub async fn open_volume(
636        &self,
637        volume: &str,
638        options: MountOptions,
639    ) -> Result<ServingVolume, Error> {
640        let (exposed_dir, server) = create_proxy::<fio::DirectoryMarker>();
641        let path = format!("volumes/{}", volume);
642        connect_to_named_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumeMarker>(
643            self.exposed_dir.as_ref().unwrap(),
644            &path,
645        )?
646        .mount(server, options)
647        .await?
648        .map_err(|e| anyhow!(zx::Status::from_raw(e)))?;
649
650        ServingVolume::new(exposed_dir)
651    }
652
653    /// Sets the max byte limit for a volume. Fails if the volume is not mounted.
654    pub async fn set_byte_limit(&self, volume: &str, byte_limit: u64) -> Result<(), Error> {
655        if byte_limit == 0 {
656            return Ok(());
657        }
658        let path = format!("volumes/{}", volume);
659        connect_to_named_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumeMarker>(
660            self.exposed_dir.as_ref().unwrap(),
661            &path,
662        )?
663        .set_limit(byte_limit)
664        .await?
665        .map_err(|e| anyhow!(zx::Status::from_raw(e)))
666    }
667
668    pub async fn check_volume(&self, volume: &str, options: CheckOptions) -> Result<(), Error> {
669        let path = format!("volumes/{}", volume);
670        connect_to_named_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumeMarker>(
671            self.exposed_dir.as_ref().unwrap(),
672            &path,
673        )?
674        .check(options)
675        .await?
676        .map_err(|e| anyhow!(zx::Status::from_raw(e)))?;
677        Ok(())
678    }
679
680    /// Provides access to the internal |exposed_dir| for use in testing
681    /// callsites which need directory access.
682    pub fn exposed_dir(&self) -> &fio::DirectoryProxy {
683        self.exposed_dir.as_ref().expect("BUG: exposed dir missing")
684    }
685
686    /// Attempts to shutdown the filesystem using the [`fidl_fuchsia_fs::AdminProxy::shutdown()`]
687    /// FIDL method.
688    ///
689    /// # Errors
690    ///
691    /// Returns [`Err`] if the shutdown failed.
692    pub async fn shutdown(mut self) -> Result<(), ShutdownError> {
693        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(
694            // Take exposed_dir so we don't attempt to shut down again in Drop.
695            &self.exposed_dir.take().expect("BUG: exposed dir missing"),
696        )?
697        .shutdown()
698        .await?;
699        Ok(())
700    }
701
702    /// Take the exposed dir from this filesystem instance, dropping the management struct without
703    /// shutting the filesystem down. This leaves the caller with the responsibility of shutting
704    /// down the filesystem, and the filesystem component if necessary.
705    pub fn take_exposed_dir(mut self) -> fio::DirectoryProxy {
706        self.component.take().expect("BUG: missing component").forget();
707        self.exposed_dir.take().expect("BUG: exposed dir missing")
708    }
709}
710
711impl Drop for ServingMultiVolumeFilesystem {
712    fn drop(&mut self) {
713        if let Some(exposed_dir) = self.exposed_dir.take() {
714            // Make a best effort attempt to shut down to the filesystem.
715            if let Ok(proxy) =
716                connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(&exposed_dir)
717            {
718                let _ = proxy.shutdown();
719            }
720        }
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727    use crate::{BlobCompression, BlobEvictionPolicy, Blobfs, F2fs, Fxfs, Minfs};
728    use fuchsia_async as fasync;
729    use ramdevice_client::RamdiskClient;
730    use std::io::{Read as _, Write as _};
731    use std::time::Duration;
732
733    async fn ramdisk(block_size: u64) -> RamdiskClient {
734        RamdiskClient::create(block_size, 1 << 16).await.unwrap()
735    }
736
737    async fn new_fs<FSC: FSConfig>(ramdisk: &mut RamdiskClient, config: FSC) -> Filesystem {
738        Filesystem::new(ramdisk.take_controller().unwrap(), config)
739    }
740
741    #[fuchsia::test]
742    async fn blobfs_custom_config() {
743        let block_size = 512;
744        let mut ramdisk = ramdisk(block_size).await;
745        let config = Blobfs {
746            verbose: true,
747            readonly: true,
748            write_compression_algorithm: BlobCompression::Uncompressed,
749            cache_eviction_policy_override: BlobEvictionPolicy::EvictImmediately,
750            ..Default::default()
751        };
752        let mut blobfs = new_fs(&mut ramdisk, config).await;
753
754        blobfs.format().await.expect("failed to format blobfs");
755        blobfs.fsck().await.expect("failed to fsck blobfs");
756        let _ = blobfs.serve().await.expect("failed to serve blobfs");
757
758        ramdisk.destroy().await.expect("failed to destroy ramdisk");
759    }
760
761    #[fuchsia::test]
762    async fn blobfs_format_fsck_success() {
763        let block_size = 512;
764        let mut ramdisk = ramdisk(block_size).await;
765        let mut blobfs = new_fs(&mut ramdisk, Blobfs::default()).await;
766
767        blobfs.format().await.expect("failed to format blobfs");
768        blobfs.fsck().await.expect("failed to fsck blobfs");
769
770        ramdisk.destroy().await.expect("failed to destroy ramdisk");
771    }
772
773    #[fuchsia::test]
774    async fn blobfs_format_serve_write_query_restart_read_shutdown() {
775        let block_size = 512;
776        let mut ramdisk = ramdisk(block_size).await;
777        let mut blobfs = new_fs(&mut ramdisk, Blobfs::default()).await;
778
779        blobfs.format().await.expect("failed to format blobfs");
780
781        let serving = blobfs.serve().await.expect("failed to serve blobfs the first time");
782
783        // snapshot of FilesystemInfo
784        let fs_info1 =
785            serving.query().await.expect("failed to query filesystem info after first serving");
786
787        // pre-generated merkle test fixture data
788        let merkle = "be901a14ec42ee0a8ee220eb119294cdd40d26d573139ee3d51e4430e7d08c28";
789        let content = String::from("test content").into_bytes();
790
791        {
792            let test_file = fuchsia_fs::directory::open_file(
793                serving.root(),
794                merkle,
795                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_WRITABLE,
796            )
797            .await
798            .expect("failed to create test file");
799            let () = test_file
800                .resize(content.len() as u64)
801                .await
802                .expect("failed to send resize FIDL")
803                .map_err(Status::from_raw)
804                .expect("failed to resize file");
805            let _: u64 = test_file
806                .write(&content)
807                .await
808                .expect("failed to write to test file")
809                .map_err(Status::from_raw)
810                .expect("write error");
811        }
812
813        // check against the snapshot FilesystemInfo
814        let fs_info2 = serving.query().await.expect("failed to query filesystem info after write");
815        assert_eq!(
816            fs_info2.used_bytes - fs_info1.used_bytes,
817            fs_info2.block_size as u64 // assuming content < 8K
818        );
819
820        serving.shutdown().await.expect("failed to shutdown blobfs the first time");
821        let serving = blobfs.serve().await.expect("failed to serve blobfs the second time");
822        {
823            let test_file =
824                fuchsia_fs::directory::open_file(serving.root(), merkle, fio::PERM_READABLE)
825                    .await
826                    .expect("failed to open test file");
827            let read_content =
828                fuchsia_fs::file::read(&test_file).await.expect("failed to read from test file");
829            assert_eq!(content, read_content);
830        }
831
832        // once more check against the snapshot FilesystemInfo
833        let fs_info3 = serving.query().await.expect("failed to query filesystem info after read");
834        assert_eq!(
835            fs_info3.used_bytes - fs_info1.used_bytes,
836            fs_info3.block_size as u64 // assuming content < 8K
837        );
838
839        serving.shutdown().await.expect("failed to shutdown blobfs the second time");
840
841        ramdisk.destroy().await.expect("failed to destroy ramdisk");
842    }
843
844    #[fuchsia::test]
845    async fn blobfs_bind_to_path() {
846        let block_size = 512;
847        let merkle = "be901a14ec42ee0a8ee220eb119294cdd40d26d573139ee3d51e4430e7d08c28";
848        let test_content = b"test content";
849        let mut ramdisk = ramdisk(block_size).await;
850        let mut blobfs = new_fs(&mut ramdisk, Blobfs::default()).await;
851
852        blobfs.format().await.expect("failed to format blobfs");
853        let mut serving = blobfs.serve().await.expect("failed to serve blobfs");
854        serving.bind_to_path("/test-blobfs-path").expect("bind_to_path failed");
855        let test_path = format!("/test-blobfs-path/{}", merkle);
856
857        {
858            let mut file = std::fs::File::create(&test_path).expect("failed to create test file");
859            file.set_len(test_content.len() as u64).expect("failed to set size");
860            file.write_all(test_content).expect("write bytes");
861        }
862
863        {
864            let mut file = std::fs::File::open(&test_path).expect("failed to open test file");
865            let mut buf = Vec::new();
866            file.read_to_end(&mut buf).expect("failed to read test file");
867            assert_eq!(buf, test_content);
868        }
869
870        serving.shutdown().await.expect("failed to shutdown blobfs");
871
872        std::fs::File::open(&test_path).expect_err("test file was not unbound");
873    }
874
875    #[fuchsia::test]
876    async fn minfs_custom_config() {
877        let block_size = 512;
878        let mut ramdisk = ramdisk(block_size).await;
879        let config = Minfs {
880            verbose: true,
881            readonly: true,
882            fsck_after_every_transaction: true,
883            ..Default::default()
884        };
885        let mut minfs = new_fs(&mut ramdisk, config).await;
886
887        minfs.format().await.expect("failed to format minfs");
888        minfs.fsck().await.expect("failed to fsck minfs");
889        let _ = minfs.serve().await.expect("failed to serve minfs");
890
891        ramdisk.destroy().await.expect("failed to destroy ramdisk");
892    }
893
894    #[fuchsia::test]
895    async fn minfs_format_fsck_success() {
896        let block_size = 8192;
897        let mut ramdisk = ramdisk(block_size).await;
898        let mut minfs = new_fs(&mut ramdisk, Minfs::default()).await;
899
900        minfs.format().await.expect("failed to format minfs");
901        minfs.fsck().await.expect("failed to fsck minfs");
902
903        ramdisk.destroy().await.expect("failed to destroy ramdisk");
904    }
905
906    #[fuchsia::test]
907    async fn minfs_format_serve_write_query_restart_read_shutdown() {
908        let block_size = 8192;
909        let mut ramdisk = ramdisk(block_size).await;
910        let mut minfs = new_fs(&mut ramdisk, Minfs::default()).await;
911
912        minfs.format().await.expect("failed to format minfs");
913        let serving = minfs.serve().await.expect("failed to serve minfs the first time");
914
915        // snapshot of FilesystemInfo
916        let fs_info1 =
917            serving.query().await.expect("failed to query filesystem info after first serving");
918
919        let filename = "test_file";
920        let content = String::from("test content").into_bytes();
921
922        {
923            let test_file = fuchsia_fs::directory::open_file(
924                serving.root(),
925                filename,
926                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_WRITABLE,
927            )
928            .await
929            .expect("failed to create test file");
930            let _: u64 = test_file
931                .write(&content)
932                .await
933                .expect("failed to write to test file")
934                .map_err(Status::from_raw)
935                .expect("write error");
936        }
937
938        // check against the snapshot FilesystemInfo
939        let fs_info2 = serving.query().await.expect("failed to query filesystem info after write");
940        assert_eq!(
941            fs_info2.used_bytes - fs_info1.used_bytes,
942            fs_info2.block_size as u64 // assuming content < 8K
943        );
944
945        serving.shutdown().await.expect("failed to shutdown minfs the first time");
946        let serving = minfs.serve().await.expect("failed to serve minfs the second time");
947
948        {
949            let test_file =
950                fuchsia_fs::directory::open_file(serving.root(), filename, fio::PERM_READABLE)
951                    .await
952                    .expect("failed to open test file");
953            let read_content =
954                fuchsia_fs::file::read(&test_file).await.expect("failed to read from test file");
955            assert_eq!(content, read_content);
956        }
957
958        // once more check against the snapshot FilesystemInfo
959        let fs_info3 = serving.query().await.expect("failed to query filesystem info after read");
960        assert_eq!(
961            fs_info3.used_bytes - fs_info1.used_bytes,
962            fs_info3.block_size as u64 // assuming content < 8K
963        );
964
965        let _ = serving.shutdown().await.expect("failed to shutdown minfs the second time");
966
967        ramdisk.destroy().await.expect("failed to destroy ramdisk");
968    }
969
970    #[fuchsia::test]
971    async fn minfs_bind_to_path() {
972        let block_size = 8192;
973        let test_content = b"test content";
974        let mut ramdisk = ramdisk(block_size).await;
975        let mut minfs = new_fs(&mut ramdisk, Minfs::default()).await;
976
977        minfs.format().await.expect("failed to format minfs");
978        let mut serving = minfs.serve().await.expect("failed to serve minfs");
979        serving.bind_to_path("/test-minfs-path").expect("bind_to_path failed");
980        let test_path = "/test-minfs-path/test_file";
981
982        {
983            let mut file = std::fs::File::create(test_path).expect("failed to create test file");
984            file.write_all(test_content).expect("write bytes");
985        }
986
987        {
988            let mut file = std::fs::File::open(test_path).expect("failed to open test file");
989            let mut buf = Vec::new();
990            file.read_to_end(&mut buf).expect("failed to read test file");
991            assert_eq!(buf, test_content);
992        }
993
994        serving.shutdown().await.expect("failed to shutdown minfs");
995
996        std::fs::File::open(test_path).expect_err("test file was not unbound");
997    }
998
999    #[fuchsia::test]
1000    async fn minfs_take_exposed_dir_does_not_drop() {
1001        let block_size = 512;
1002        let test_content = b"test content";
1003        let test_file_name = "test-file";
1004        let mut ramdisk = ramdisk(block_size).await;
1005        let mut minfs = new_fs(&mut ramdisk, Minfs::default()).await;
1006
1007        minfs.format().await.expect("failed to format fxfs");
1008
1009        let fs = minfs.serve().await.expect("failed to serve fxfs");
1010        let file = {
1011            let file = fuchsia_fs::directory::open_file(
1012                fs.root(),
1013                test_file_name,
1014                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_READABLE | fio::PERM_WRITABLE,
1015            )
1016            .await
1017            .unwrap();
1018            fuchsia_fs::file::write(&file, test_content).await.unwrap();
1019            file.close().await.expect("close fidl error").expect("close error");
1020            fuchsia_fs::directory::open_file(fs.root(), test_file_name, fio::PERM_READABLE)
1021                .await
1022                .unwrap()
1023        };
1024
1025        let exposed_dir = fs.take_exposed_dir();
1026
1027        assert_eq!(fuchsia_fs::file::read(&file).await.unwrap(), test_content);
1028
1029        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(&exposed_dir)
1030            .expect("connecting to admin marker")
1031            .shutdown()
1032            .await
1033            .expect("shutdown failed");
1034    }
1035
1036    #[fuchsia::test]
1037    async fn f2fs_format_fsck_success() {
1038        let block_size = 4096;
1039        let mut ramdisk = ramdisk(block_size).await;
1040        let mut f2fs = new_fs(&mut ramdisk, F2fs::default()).await;
1041
1042        f2fs.format().await.expect("failed to format f2fs");
1043        f2fs.fsck().await.expect("failed to fsck f2fs");
1044
1045        ramdisk.destroy().await.expect("failed to destroy ramdisk");
1046    }
1047
1048    #[fuchsia::test]
1049    async fn f2fs_format_serve_write_query_restart_read_shutdown() {
1050        let block_size = 4096;
1051        let mut ramdisk = ramdisk(block_size).await;
1052        let mut f2fs = new_fs(&mut ramdisk, F2fs::default()).await;
1053
1054        f2fs.format().await.expect("failed to format f2fs");
1055        let serving = f2fs.serve().await.expect("failed to serve f2fs the first time");
1056
1057        // snapshot of FilesystemInfo
1058        let fs_info1 =
1059            serving.query().await.expect("failed to query filesystem info after first serving");
1060
1061        let filename = "test_file";
1062        let content = String::from("test content").into_bytes();
1063
1064        {
1065            let test_file = fuchsia_fs::directory::open_file(
1066                serving.root(),
1067                filename,
1068                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_WRITABLE,
1069            )
1070            .await
1071            .expect("failed to create test file");
1072            let _: u64 = test_file
1073                .write(&content)
1074                .await
1075                .expect("failed to write to test file")
1076                .map_err(Status::from_raw)
1077                .expect("write error");
1078        }
1079
1080        // check against the snapshot FilesystemInfo
1081        let fs_info2 = serving.query().await.expect("failed to query filesystem info after write");
1082        // With zx::stream, f2fs doesn't support the inline data feature allowing file
1083        // inode blocks to include small data. This way requires keeping two copies of VMOs
1084        // for the same inline data
1085        // assuming content < 4K and its inode block.
1086        let expected_size2 = fs_info2.block_size * 2;
1087        assert_eq!(fs_info2.used_bytes - fs_info1.used_bytes, expected_size2 as u64);
1088
1089        serving.shutdown().await.expect("failed to shutdown f2fs the first time");
1090        let serving = f2fs.serve().await.expect("failed to serve f2fs the second time");
1091
1092        {
1093            let test_file =
1094                fuchsia_fs::directory::open_file(serving.root(), filename, fio::PERM_READABLE)
1095                    .await
1096                    .expect("failed to open test file");
1097            let read_content =
1098                fuchsia_fs::file::read(&test_file).await.expect("failed to read from test file");
1099            assert_eq!(content, read_content);
1100        }
1101
1102        // once more check against the snapshot FilesystemInfo
1103        let fs_info3 = serving.query().await.expect("failed to query filesystem info after read");
1104        // assuming content < 4K and its inode block.
1105        let expected_size3 = fs_info3.block_size * 2;
1106        assert_eq!(fs_info3.used_bytes - fs_info1.used_bytes, expected_size3 as u64);
1107
1108        serving.shutdown().await.expect("failed to shutdown f2fs the second time");
1109        f2fs.fsck().await.expect("failed to fsck f2fs after shutting down the second time");
1110
1111        ramdisk.destroy().await.expect("failed to destroy ramdisk");
1112    }
1113
1114    #[fuchsia::test]
1115    async fn f2fs_bind_to_path() {
1116        let block_size = 4096;
1117        let test_content = b"test content";
1118        let mut ramdisk = ramdisk(block_size).await;
1119        let mut f2fs = new_fs(&mut ramdisk, F2fs::default()).await;
1120
1121        f2fs.format().await.expect("failed to format f2fs");
1122        let mut serving = f2fs.serve().await.expect("failed to serve f2fs");
1123        serving.bind_to_path("/test-f2fs-path").expect("bind_to_path failed");
1124        let test_path = "/test-f2fs-path/test_file";
1125
1126        {
1127            let mut file = std::fs::File::create(test_path).expect("failed to create test file");
1128            file.write_all(test_content).expect("write bytes");
1129        }
1130
1131        {
1132            let mut file = std::fs::File::open(test_path).expect("failed to open test file");
1133            let mut buf = Vec::new();
1134            file.read_to_end(&mut buf).expect("failed to read test file");
1135            assert_eq!(buf, test_content);
1136        }
1137
1138        serving.shutdown().await.expect("failed to shutdown f2fs");
1139
1140        std::fs::File::open(test_path).expect_err("test file was not unbound");
1141    }
1142
1143    // TODO(https://fxbug.dev/42174810): Re-enable this test; it depends on Fxfs failing repeated calls to
1144    // Start.
1145    #[ignore]
1146    #[fuchsia::test]
1147    async fn fxfs_shutdown_component_when_dropped() {
1148        let block_size = 512;
1149        let mut ramdisk = ramdisk(block_size).await;
1150        let mut fxfs = new_fs(&mut ramdisk, Fxfs::default()).await;
1151
1152        fxfs.format().await.expect("failed to format fxfs");
1153        {
1154            let _fs = fxfs.serve_multi_volume().await.expect("failed to serve fxfs");
1155
1156            // Serve should fail for the second time.
1157            assert!(
1158                fxfs.serve_multi_volume().await.is_err(),
1159                "serving succeeded when already mounted"
1160            );
1161        }
1162
1163        // Fxfs should get shut down when dropped, but it's asynchronous, so we need to loop here.
1164        let mut attempts = 0;
1165        loop {
1166            if let Ok(_) = fxfs.serve_multi_volume().await {
1167                break;
1168            }
1169            attempts += 1;
1170            assert!(attempts < 10);
1171            fasync::Timer::new(Duration::from_secs(1)).await;
1172        }
1173    }
1174
1175    #[fuchsia::test]
1176    async fn fxfs_open_volume() {
1177        let block_size = 512;
1178        let mut ramdisk = ramdisk(block_size).await;
1179        let mut fxfs = new_fs(&mut ramdisk, Fxfs::default()).await;
1180
1181        fxfs.format().await.expect("failed to format fxfs");
1182
1183        let mut fs = fxfs.serve_multi_volume().await.expect("failed to serve fxfs");
1184
1185        assert_eq!(fs.has_volume("foo").await.expect("has_volume"), false);
1186        assert!(
1187            fs.open_volume("foo", MountOptions::default()).await.is_err(),
1188            "Opening nonexistent volume should fail"
1189        );
1190
1191        let vol = fs
1192            .create_volume("foo", CreateOptions::default(), MountOptions::default())
1193            .await
1194            .expect("Create volume failed");
1195        vol.query().await.expect("Query volume failed");
1196        // TODO(https://fxbug.dev/42057878) Closing the volume is not synchronous. Immediately reopening the
1197        // volume will race with the asynchronous close and sometimes fail because the volume is
1198        // still mounted.
1199        // fs.open_volume("foo", MountOptions{crypt: None, as_blob: false}).await
1200        //    .expect("Open volume failed");
1201        assert_eq!(fs.has_volume("foo").await.expect("has_volume"), true);
1202    }
1203
1204    #[fuchsia::test]
1205    async fn fxfs_take_exposed_dir_does_not_drop() {
1206        let block_size = 512;
1207        let test_content = b"test content";
1208        let test_file_name = "test-file";
1209        let mut ramdisk = ramdisk(block_size).await;
1210        let mut fxfs = new_fs(&mut ramdisk, Fxfs::default()).await;
1211
1212        fxfs.format().await.expect("failed to format fxfs");
1213
1214        let fs = fxfs.serve_multi_volume().await.expect("failed to serve fxfs");
1215        let file = {
1216            let vol = fs
1217                .create_volume("foo", CreateOptions::default(), MountOptions::default())
1218                .await
1219                .expect("Create volume failed");
1220            let file = fuchsia_fs::directory::open_file(
1221                vol.root(),
1222                test_file_name,
1223                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_READABLE | fio::PERM_WRITABLE,
1224            )
1225            .await
1226            .unwrap();
1227            fuchsia_fs::file::write(&file, test_content).await.unwrap();
1228            file.close().await.expect("close fidl error").expect("close error");
1229            fuchsia_fs::directory::open_file(vol.root(), test_file_name, fio::PERM_READABLE)
1230                .await
1231                .unwrap()
1232        };
1233
1234        let exposed_dir = fs.take_exposed_dir();
1235
1236        assert_eq!(fuchsia_fs::file::read(&file).await.unwrap(), test_content);
1237
1238        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(&exposed_dir)
1239            .expect("connecting to admin marker")
1240            .shutdown()
1241            .await
1242            .expect("shutdown failed");
1243    }
1244}