Skip to main content

fuchsia_fatfs/
component.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::{Disk, FatDirectory, FatFs, fatfs_error_to_status};
6use anyhow::{Context, Error, bail};
7use block_client::RemoteBlockClientSync;
8use fidl::endpoints::{ClientEnd, DiscoverableProtocolMarker, RequestStream, ServerEnd};
9use fidl_fuchsia_fs::{AdminMarker, AdminRequest, AdminRequestStream};
10use fidl_fuchsia_fs_startup::{
11    CheckOptions, FormatOptions, StartOptions, StartupMarker, StartupRequest, StartupRequestStream,
12};
13use fidl_fuchsia_io as fio;
14use fidl_fuchsia_process_lifecycle::{LifecycleRequest, LifecycleRequestStream};
15use fidl_fuchsia_storage_block::BlockMarker;
16use fragile::Fragile;
17use fuchsia_async as fasync;
18use futures::TryStreamExt;
19use log::{error, info, warn};
20use std::cell::RefCell;
21use std::rc::Rc;
22use std::sync::Arc;
23use vfs::directory::helper::DirectlyMutable;
24use vfs::execution_scope::ExecutionScope;
25use vfs::node::Node as _;
26
27fn map_to_raw_status(e: Error) -> zx::sys::zx_status_t {
28    map_to_status(e).into_raw()
29}
30
31fn map_to_status(error: Error) -> zx::Status {
32    match error.downcast::<zx::Status>() {
33        Ok(status) => status,
34        Err(error) => match error.downcast::<std::io::Error>() {
35            Ok(io_error) => fatfs_error_to_status(io_error),
36            Err(error) => {
37                // Print the internal error if we re-map it because we will lose any context after
38                // this.
39                warn!(error:?; "Internal error");
40                zx::Status::INTERNAL
41            }
42        },
43    }
44}
45
46enum State {
47    ComponentStarted,
48    Running(RunningState),
49}
50
51struct RunningState {
52    // We have to wrap this in an Arc, even though it itself basically just wraps an Arc, so that
53    // FsInspectTree can reference `fs` as a Weak<dyn FsInspect>`.
54    fs: FatFs,
55}
56
57impl State {
58    /// Disconnects the running filesystem by removing its "root" entry from the
59    /// outgoing directory and calling `close` on it to decrement its reference count.
60    /// Returns the `FatFs` instance if it was running.
61    fn disconnect(&mut self, outgoing_dir: &vfs::directory::immutable::Simple) -> Option<FatFs> {
62        if let State::Running(RunningState { fs }) =
63            std::mem::replace(self, State::ComponentStarted)
64        {
65            if let Ok(Some(entry)) =
66                outgoing_dir.remove_entry("root", /* must_be_directory: */ false)
67            {
68                let _ = entry.into_any().downcast::<FatDirectory>().unwrap().close();
69            }
70            Some(fs)
71        } else {
72            None
73        }
74    }
75}
76
77pub struct Component {
78    state: RefCell<State>,
79
80    /// The execution scope of the pseudo filesystem (data plane). All VFS connections
81    /// and background flush tasks run on this scope.
82    scope: ExecutionScope,
83
84    /// The execution scope for admin and lifecycle services (control plane). This is
85    /// kept separate from `scope` so that control connections (like Admin or Lifecycle)
86    /// can remain active and process requests even while the filesystem is restarting
87    /// and `scope` is being shut down.
88    admin_scope: ExecutionScope,
89
90    /// The root of the pseudo filesystem for the component.
91    outgoing_dir: Arc<vfs::directory::immutable::Simple>,
92}
93
94impl Component {
95    pub fn new() -> Rc<Self> {
96        Rc::new(Self {
97            state: RefCell::new(State::ComponentStarted),
98            scope: ExecutionScope::new(),
99            admin_scope: ExecutionScope::new(),
100            outgoing_dir: vfs::directory::immutable::simple(),
101        })
102    }
103
104    /// Runs Fatfs as a component.
105    pub async fn run(
106        self: Rc<Self>,
107        outgoing_dir: zx::Channel,
108        lifecycle_channel: Option<zx::Channel>,
109    ) -> Result<(), Error> {
110        let svc_dir = vfs::directory::immutable::simple();
111        self.outgoing_dir.add_entry("svc", svc_dir.clone()).expect("Unable to create svc dir");
112        let weak = Fragile::new(Rc::downgrade(&self));
113        let weak_startup = weak.clone();
114        svc_dir.add_entry(
115            StartupMarker::PROTOCOL_NAME,
116            vfs::service::endpoint(move |_scope, channel| {
117                let weak = weak_startup.clone();
118                let requests = StartupRequestStream::from_channel(channel);
119                if let Some(me) = weak.get().upgrade() {
120                    let weak_task = weak.clone();
121                    me.admin_scope.spawn_local(async move {
122                        if let Some(me) = weak_task.get().upgrade() {
123                            let _ = me.handle_startup_requests(requests).await;
124                        }
125                    });
126                }
127            }),
128        )?;
129
130        let weak_admin = weak.clone();
131        svc_dir.add_entry(
132            AdminMarker::PROTOCOL_NAME,
133            vfs::service::endpoint(move |_scope, channel| {
134                let weak = weak_admin.clone();
135                let requests = AdminRequestStream::from_channel(channel);
136                if let Some(me) = weak.get().upgrade() {
137                    let weak_task = weak.clone();
138                    me.admin_scope.spawn_local(async move {
139                        if let Some(me) = weak_task.get().upgrade() {
140                            let _ = me.handle_admin_requests(requests).await;
141                        }
142                    });
143                }
144            }),
145        )?;
146
147        vfs::directory::serve_on(
148            self.outgoing_dir.clone(),
149            fio::PERM_READABLE | fio::PERM_WRITABLE,
150            self.admin_scope.clone(),
151            ServerEnd::new(outgoing_dir),
152        );
153
154        if let Some(channel) = lifecycle_channel {
155            let weak = Fragile::new(Rc::downgrade(&self));
156            self.admin_scope.spawn_local(async move {
157                if let Some(me) = weak.get().upgrade() {
158                    if let Err(error) = me.handle_lifecycle_requests(channel).await {
159                        warn!(error:?; "handle_lifecycle_requests");
160                    }
161                }
162            });
163        }
164
165        // Wait for the admin scope to finish first. Once the admin scope has finished,
166        // no new VFS connections can be created (as the control/startup/admin channels
167        // are hosted on it). If we waited for the VFS scope first, new connections
168        // could be spawned on it while we were waiting.
169        self.admin_scope.wait().await;
170        self.scope.wait().await;
171        self.stop_filesystem().await;
172
173        Ok(())
174    }
175
176    async fn handle_startup_requests(&self, mut stream: StartupRequestStream) -> Result<(), Error> {
177        while let Some(request) = stream.try_next().await? {
178            match request {
179                StartupRequest::Start { responder, device, options } => {
180                    responder.send(self.handle_start(device, options).await.map_err(|e| {
181                        error!(e:?; "handle_start failed");
182                        map_to_raw_status(e)
183                    }))?
184                }
185                StartupRequest::Format { responder, device, options } => {
186                    responder.send(self.handle_format(device, options).await.map_err(|e| {
187                        error!(e:?; "handle_format failed");
188                        map_to_raw_status(e)
189                    }))?
190                }
191                StartupRequest::Check { responder, device, options } => {
192                    responder.send(self.handle_check(device, options).await.map_err(|e| {
193                        error!(e:?; "handle_check failed");
194                        map_to_raw_status(e)
195                    }))?
196                }
197            }
198        }
199        Ok(())
200    }
201
202    async fn start_with_disk(&self, disk: Box<dyn Disk>) -> Result<(), Error> {
203        self.stop_filesystem().await;
204
205        // Resurrect the VFS scope so it can be reused to spawn connections for the new disk.
206        self.scope.resurrect();
207
208        let fs = FatFs::new(disk, self.scope.clone()).map_err(|_| zx::Status::IO)?;
209        let root = fs.get_root()?;
210
211        self.outgoing_dir.add_entry("root", root)?;
212
213        *self.state.borrow_mut() = State::Running(RunningState { fs });
214
215        Ok(())
216    }
217
218    async fn handle_start(
219        &self,
220        device: ClientEnd<BlockMarker>,
221        options: StartOptions,
222    ) -> Result<(), Error> {
223        info!(options:?; "Received start request");
224
225        let remote_block_client = RemoteBlockClientSync::new(device)?;
226        let device = block_client::Cache::new(remote_block_client)?;
227
228        self.start_with_disk(Box::new(device)).await?;
229
230        info!("Mounted");
231        Ok(())
232    }
233
234    async fn handle_format(
235        &self,
236        device: ClientEnd<BlockMarker>,
237        options: FormatOptions,
238    ) -> Result<(), Error> {
239        let args: Box<dyn Iterator<Item = _> + Send> =
240            if let Some(spc) = options.sectors_per_cluster {
241                Box::new(["-c".to_string(), format!("{spc}")].into_iter())
242            } else {
243                Box::new(std::iter::empty())
244            };
245        if block_adapter::run(device.into_proxy(), "/pkg/bin/mkfs-msdosfs", args).await? == 0 {
246            Ok(())
247        } else {
248            bail!(zx::Status::IO)
249        }
250    }
251
252    async fn handle_check(
253        &self,
254        device: ClientEnd<BlockMarker>,
255        _options: CheckOptions,
256    ) -> Result<(), Error> {
257        // Pass the '-n' flag so that it never modifies which remains consistent with other
258        // filesystems.
259        if block_adapter::run(
260            device.into_proxy(),
261            "/pkg/bin/fsck-msdosfs",
262            ["-n".to_string()].into_iter(),
263        )
264        .await?
265            == 0
266        {
267            Ok(())
268        } else {
269            bail!(zx::Status::IO)
270        }
271    }
272
273    async fn handle_admin_requests(&self, mut stream: AdminRequestStream) -> Result<(), Error> {
274        while let Some(request) = stream.try_next().await.context("Reading request")? {
275            if self.handle_admin(request).await? {
276                break;
277            }
278        }
279        Ok(())
280    }
281
282    // Returns true if we should close the connection.
283    async fn handle_admin(&self, req: AdminRequest) -> Result<bool, Error> {
284        match req {
285            AdminRequest::Shutdown { responder } => {
286                info!("Received shutdown request");
287                self.stop_filesystem().await;
288                responder
289                    .send()
290                    .unwrap_or_else(|e| warn!("Failed to send shutdown response: {}", e));
291                return Ok(true);
292            }
293        }
294    }
295
296    async fn stop_filesystem(&self) {
297        info!("Stopping fatfs runtime; remaining connections will be forcibly closed");
298
299        // Disconnect the filesystem's root directory from the outgoing directory first.
300        // This prevents new connections from being established via the outgoing directory
301        // while we are waiting for existing ones to drain.
302        let maybe_fs = self.state.borrow_mut().disconnect(&self.outgoing_dir);
303
304        self.scope.shutdown();
305        self.scope.wait().await;
306
307        // Cleanly shut down the filesystem. This is guaranteed to succeed (meaning
308        // Rc::into_inner will succeed) because all connection references to it
309        // have been dropped during scope wait.
310        if let Some(fs) = maybe_fs {
311            if let Err(error) = fs.shut_down() {
312                error!(error:?; "Failed to shutdown fatfs");
313            } else {
314                info!("Filesystem terminated");
315            }
316        }
317    }
318
319    async fn handle_lifecycle_requests(&self, lifecycle_channel: zx::Channel) -> Result<(), Error> {
320        let mut stream =
321            LifecycleRequestStream::from_channel(fasync::Channel::from_channel(lifecycle_channel));
322        match stream.try_next().await.context("Reading request")? {
323            Some(LifecycleRequest::Stop { .. }) => {
324                info!("Received Lifecycle::Stop request");
325                self.stop_filesystem().await;
326            }
327            None => {}
328        }
329        Ok(())
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use fidl::endpoints::{Proxy, create_proxy};
337    use fuchsia_async as fasync;
338
339    #[fuchsia::test]
340    async fn test_component_lifecycle_with_open_connections() {
341        let component = Component::new();
342
343        // Create a 2MB fatfs formatted in-memory disk.
344        let mut buffer = vec![0u8; 2048 << 10];
345        let cursor = std::io::Cursor::new(buffer.as_mut_slice());
346        fatfs::format_volume(cursor, fatfs::FormatVolumeOptions::new()).unwrap();
347        let disk: Box<dyn Disk> = Box::new(std::io::Cursor::new(buffer));
348
349        component.start_with_disk(disk).await.unwrap();
350
351        let (outgoing_dir_client, outgoing_dir_server) = create_proxy::<fio::DirectoryMarker>();
352        let component_clone = component.clone();
353        let run_task = fasync::Task::local(async move {
354            component_clone.run(outgoing_dir_server.into_channel(), None).await.unwrap();
355        });
356
357        // Open the root directory via outgoing_dir.
358        let (root_client, root_server) = create_proxy::<fio::NodeMarker>();
359        outgoing_dir_client
360            .open(
361                "root",
362                fio::PERM_READABLE | fio::PERM_WRITABLE,
363                &Default::default(),
364                root_server.into_channel(),
365            )
366            .unwrap();
367        let root_client = fio::DirectoryProxy::new(root_client.into_channel().unwrap());
368
369        // Guarantee "open" has been processed by making a round-trip call.
370        let _ = root_client.query().await.unwrap();
371
372        // Close outgoing_dir_client. admin_scope should become empty, but VFS scope still has root_client.
373        drop(outgoing_dir_client);
374
375        // Wait a bit and ensure the component is still running.
376        use futures::FutureExt;
377        let mut run_task = run_task.fuse();
378        let mut delay = Box::pin(fasync::Timer::new(fasync::MonotonicInstant::after(
379            zx::MonotonicDuration::from_millis(100),
380        )))
381        .fuse();
382        futures::select! {
383            _ = run_task => panic!("Component exited prematurely!"),
384            _ = delay => {},
385        }
386
387        // Now close the root connection. The component should exit.
388        drop(root_client);
389
390        // Await run_task to ensure it exits.
391        run_task.await;
392    }
393
394    #[fuchsia::test]
395    async fn test_component_restart_with_open_connections() {
396        let component = Component::new();
397
398        // Helper to create a formatted disk.
399        let create_disk = || {
400            let mut buffer = vec![0u8; 2048 << 10];
401            let cursor = std::io::Cursor::new(buffer.as_mut_slice());
402            fatfs::format_volume(cursor, fatfs::FormatVolumeOptions::new()).unwrap();
403            Box::new(std::io::Cursor::new(buffer)) as Box<dyn Disk>
404        };
405
406        // Start with disk 1.
407        component.start_with_disk(create_disk()).await.unwrap();
408
409        let (outgoing_dir_client, outgoing_dir_server) = create_proxy::<fio::DirectoryMarker>();
410        let component_clone = component.clone();
411        let _run_task = fasync::Task::local(async move {
412            component_clone.run(outgoing_dir_server.into_channel(), None).await.unwrap();
413        });
414
415        // Open the root directory.
416        let (root_client1, root_server1) = create_proxy::<fio::NodeMarker>();
417        outgoing_dir_client
418            .open(
419                "root",
420                fio::PERM_READABLE | fio::PERM_WRITABLE,
421                &Default::default(),
422                root_server1.into_channel(),
423            )
424            .unwrap();
425        let root_client1 = fio::DirectoryProxy::new(root_client1.into_channel().unwrap());
426
427        // Guarantee "open" has been processed.
428        let _ = root_client1.query().await.unwrap();
429
430        // Restart with disk 2. This should shut down the scope, closing root_client1.
431        component.start_with_disk(create_disk()).await.unwrap();
432
433        // Verify root_client1 is closed (calls to it should fail).
434        assert!(root_client1.query().await.is_err());
435
436        // We should be able to open root again, and it should work (talking to disk 2).
437        let (root_client2, root_server2) = create_proxy::<fio::NodeMarker>();
438        outgoing_dir_client
439            .open(
440                "root",
441                fio::PERM_READABLE | fio::PERM_WRITABLE,
442                &Default::default(),
443                root_server2.into_channel(),
444            )
445            .unwrap();
446        let root_client2 = fio::DirectoryProxy::new(root_client2.into_channel().unwrap());
447
448        // This should succeed.
449        let _ = root_client2.query().await.unwrap();
450
451        // Cleanup: close connections and wait for run task to exit so disk 2 is shut down cleanly.
452        drop(root_client2);
453        drop(outgoing_dir_client);
454        _run_task.await;
455    }
456}