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