ramdevice_client/
lib.rs

1// Copyright 2019 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//! A safe rust wrapper for creating and using ramdisks.
6
7#![deny(missing_docs)]
8use anyhow::{Context as _, Error, anyhow};
9use fidl::endpoints::{DiscoverableProtocolMarker as _, Proxy as _};
10use fidl_fuchsia_device::{ControllerMarker, ControllerProxy, ControllerSynchronousProxy};
11use fidl_fuchsia_hardware_ramdisk::{Guid, RamdiskControllerMarker};
12use fs_management::filesystem::{BlockConnector, DirBasedBlockConnector};
13use fuchsia_component_client::{Service, connect_to_named_protocol_at_dir_root};
14use {
15    fidl_fuchsia_hardware_block as fhardware_block, fidl_fuchsia_hardware_block_volume as fvolume,
16    fidl_fuchsia_hardware_ramdisk as framdisk, fidl_fuchsia_io as fio,
17};
18
19const GUID_LEN: usize = 16;
20const DEV_PATH: &str = "/dev";
21const RAMCTL_PATH: &str = "sys/platform/ram-disk/ramctl";
22const BLOCK_EXTENSION: &str = "block";
23
24/// A type to help construct a [`RamdeviceClient`] optionally from a VMO.
25pub struct RamdiskClientBuilder {
26    ramdisk_source: RamdiskSource,
27    block_size: u64,
28    max_transfer_blocks: Option<u32>,
29    dev_root: Option<fio::DirectoryProxy>,
30    guid: Option<[u8; GUID_LEN]>,
31    use_v2: bool,
32    ramdisk_service: Option<fio::DirectoryProxy>,
33
34    // Whether to publish this ramdisk as a fuchsia.hardware.block.volume.Service service.  This
35    // only works for the v2 driver.
36    publish: bool,
37}
38
39enum RamdiskSource {
40    Vmo { vmo: zx::Vmo },
41    Size { block_count: u64 },
42}
43
44impl RamdiskClientBuilder {
45    /// Create a new ramdisk builder
46    pub fn new(block_size: u64, block_count: u64) -> Self {
47        Self {
48            ramdisk_source: RamdiskSource::Size { block_count },
49            block_size,
50            max_transfer_blocks: None,
51            guid: None,
52            dev_root: None,
53            use_v2: false,
54            ramdisk_service: None,
55            publish: false,
56        }
57    }
58
59    /// Create a new ramdisk builder with a vmo
60    pub fn new_with_vmo(vmo: zx::Vmo, block_size: Option<u64>) -> Self {
61        Self {
62            ramdisk_source: RamdiskSource::Vmo { vmo },
63            block_size: block_size.unwrap_or(0),
64            max_transfer_blocks: None,
65            guid: None,
66            dev_root: None,
67            use_v2: false,
68            ramdisk_service: None,
69            publish: false,
70        }
71    }
72
73    /// Use the given directory as "/dev" instead of opening "/dev" from the environment.
74    pub fn dev_root(mut self, dev_root: fio::DirectoryProxy) -> Self {
75        self.dev_root = Some(dev_root);
76        self
77    }
78
79    /// Initialize the ramdisk with the given GUID, which can be queried from the ramdisk instance.
80    pub fn guid(mut self, guid: [u8; GUID_LEN]) -> Self {
81        self.guid = Some(guid);
82        self
83    }
84
85    /// Sets the maximum transfer size.
86    pub fn max_transfer_blocks(mut self, value: u32) -> Self {
87        self.max_transfer_blocks = Some(value);
88        self
89    }
90
91    /// Use the V2 ramdisk driver.
92    pub fn use_v2(mut self) -> Self {
93        self.use_v2 = true;
94        self
95    }
96
97    /// Specifies the ramdisk service.
98    pub fn ramdisk_service(mut self, service: fio::DirectoryProxy) -> Self {
99        self.ramdisk_service = Some(service);
100        self
101    }
102
103    /// Publish this ramdisk as a fuchsia.hardware.block.volume.Service service.
104    pub fn publish(mut self) -> Self {
105        self.publish = true;
106        self
107    }
108
109    /// Create the ramdisk.
110    pub async fn build(self) -> Result<RamdiskClient, Error> {
111        let Self {
112            ramdisk_source,
113            block_size,
114            max_transfer_blocks,
115            guid,
116            dev_root,
117            use_v2,
118            ramdisk_service,
119            publish,
120        } = self;
121
122        if use_v2 {
123            // Pick the first service instance we find.
124            let service = match ramdisk_service {
125                Some(s) => {
126                    Service::from_service_dir_proxy(s, fidl_fuchsia_hardware_ramdisk::ServiceMarker)
127                }
128                None => Service::open(fidl_fuchsia_hardware_ramdisk::ServiceMarker)?,
129            };
130            let ramdisk_controller = service.watch_for_any().await?.connect_to_controller()?;
131
132            let type_guid = guid.map(|guid| Guid { value: guid });
133
134            let options = match ramdisk_source {
135                RamdiskSource::Vmo { vmo } => framdisk::Options {
136                    vmo: Some(vmo),
137                    block_size: if block_size == 0 {
138                        None
139                    } else {
140                        Some(block_size.try_into().unwrap())
141                    },
142                    type_guid,
143                    publish: Some(publish),
144                    max_transfer_blocks,
145                    ..Default::default()
146                },
147                RamdiskSource::Size { block_count } => framdisk::Options {
148                    block_count: Some(block_count),
149                    block_size: Some(block_size.try_into().unwrap()),
150                    type_guid,
151                    publish: Some(publish),
152                    max_transfer_blocks,
153                    ..Default::default()
154                },
155            };
156
157            let (outgoing, event) =
158                ramdisk_controller.create(options).await?.map_err(|s| zx::Status::from_raw(s))?;
159
160            RamdiskClient::new_v2(outgoing.into_proxy(), event)
161        } else {
162            let dev_root = if let Some(dev_root) = dev_root {
163                dev_root
164            } else {
165                fuchsia_fs::directory::open_in_namespace(DEV_PATH, fio::PERM_READABLE)
166                    .with_context(|| format!("open {}", DEV_PATH))?
167            };
168            let ramdisk_controller = device_watcher::recursive_wait_and_open::<
169                RamdiskControllerMarker,
170            >(&dev_root, RAMCTL_PATH)
171            .await
172            .with_context(|| format!("waiting for {}", RAMCTL_PATH))?;
173            let type_guid = guid.map(|guid| Guid { value: guid });
174            let name = match ramdisk_source {
175                RamdiskSource::Vmo { vmo } => ramdisk_controller
176                    .create_from_vmo_with_params(vmo, block_size, type_guid.as_ref())
177                    .await?
178                    .map_err(zx::Status::from_raw)
179                    .context("creating ramdisk from vmo")?,
180                RamdiskSource::Size { block_count } => ramdisk_controller
181                    .create(block_size, block_count, type_guid.as_ref())
182                    .await?
183                    .map_err(zx::Status::from_raw)
184                    .with_context(|| format!("creating ramdisk with {} blocks", block_count))?,
185            };
186            let name = name.ok_or_else(|| anyhow!("Failed to get instance name"))?;
187            RamdiskClient::new(dev_root, &name).await
188        }
189    }
190}
191
192/// A client for managing a ramdisk. This can be created with the [`RamdiskClient::create`]
193/// function or through the type returned by [`RamdiskClient::builder`] to specify additional
194/// options.
195pub enum RamdiskClient {
196    /// V1
197    V1 {
198        /// The directory backing the block driver.
199        block_dir: fio::DirectoryProxy,
200
201        /// The device controller for the block device.
202        block_controller: ControllerProxy,
203
204        /// The device controller for the ramdisk.
205        ramdisk_controller: Option<ControllerProxy>,
206    },
207    /// V2
208    V2 {
209        /// The outgoing directory for the ram-disk.
210        outgoing: fio::DirectoryProxy,
211
212        /// The event that keeps the ramdisk alive.
213        _event: zx::EventPair,
214    },
215}
216
217impl RamdiskClient {
218    async fn new(dev_root: fio::DirectoryProxy, instance_name: &str) -> Result<Self, Error> {
219        let ramdisk_path = format!("{RAMCTL_PATH}/{instance_name}");
220        let ramdisk_controller_path = format!("{ramdisk_path}/device_controller");
221        let block_path = format!("{ramdisk_path}/{BLOCK_EXTENSION}");
222
223        // Wait for ramdisk path to appear
224        let ramdisk_controller = device_watcher::recursive_wait_and_open::<ControllerMarker>(
225            &dev_root,
226            &ramdisk_controller_path,
227        )
228        .await
229        .with_context(|| format!("waiting for {}", &ramdisk_controller_path))?;
230
231        // Wait for the block path to appear
232        let block_dir = device_watcher::recursive_wait_and_open_directory(&dev_root, &block_path)
233            .await
234            .with_context(|| format!("waiting for {}", &block_path))?;
235
236        let block_controller = connect_to_named_protocol_at_dir_root::<ControllerMarker>(
237            &block_dir,
238            "device_controller",
239        )
240        .with_context(|| {
241            format!("opening block controller at {}/device_controller", &block_path)
242        })?;
243
244        Ok(Self::V1 { block_dir, block_controller, ramdisk_controller: Some(ramdisk_controller) })
245    }
246
247    fn new_v2(outgoing: fio::DirectoryProxy, event: zx::EventPair) -> Result<Self, Error> {
248        Ok(Self::V2 { outgoing, _event: event })
249    }
250
251    /// Create a new ramdisk builder with the given block_size and block_count.
252    pub fn builder(block_size: u64, block_count: u64) -> RamdiskClientBuilder {
253        RamdiskClientBuilder::new(block_size, block_count)
254    }
255
256    /// Create a new ramdisk.
257    pub async fn create(block_size: u64, block_count: u64) -> Result<Self, Error> {
258        Self::builder(block_size, block_count).build().await
259    }
260
261    /// Get a reference to the block controller.
262    pub fn as_controller(&self) -> Option<&ControllerProxy> {
263        match self {
264            Self::V1 { block_controller, .. } => Some(block_controller),
265            Self::V2 { .. } => None,
266        }
267    }
268
269    /// Get a reference to the block directory proxy.
270    pub fn as_dir(&self) -> Option<&fio::DirectoryProxy> {
271        match self {
272            Self::V1 { block_dir, .. } => Some(block_dir),
273            Self::V2 { .. } => None,
274        }
275    }
276
277    /// Get an open channel to the underlying ramdevice.
278    pub fn open(&self) -> Result<fidl::endpoints::ClientEnd<fhardware_block::BlockMarker>, Error> {
279        let (client, server_end) = fidl::endpoints::create_endpoints();
280        self.connect(server_end)?;
281        Ok(client)
282    }
283
284    /// Gets a connector for the Block protocol of the ramdisk.
285    pub fn connector(&self) -> Result<Box<dyn BlockConnector>, Error> {
286        match self {
287            Self::V1 { .. } => {
288                // At this point, we have already waited on the block path to appear so we can
289                // directly open a connection to the ramdevice.
290
291                // TODO(https://fxbug.dev/42063787): In order to allow multiplexing to be removed,
292                // use connect_to_device_fidl to connect to the BlockProxy instead of
293                // connect_to_.._dir_root.  Requires downstream work.
294                let block_dir = fuchsia_fs::directory::clone(
295                    self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?,
296                )?;
297                Ok(Box::new(DirBasedBlockConnector::new(block_dir, ".".to_string())))
298            }
299            Self::V2 { outgoing, .. } => {
300                let block_dir = fuchsia_fs::directory::clone(outgoing)?;
301                Ok(Box::new(DirBasedBlockConnector::new(
302                    block_dir,
303                    format!("svc/{}", fvolume::VolumeMarker::PROTOCOL_NAME),
304                )))
305            }
306        }
307    }
308
309    /// Get an open channel to the underlying ramdevice.
310    pub fn connect(
311        &self,
312        server_end: fidl::endpoints::ServerEnd<fhardware_block::BlockMarker>,
313    ) -> Result<(), Error> {
314        match self {
315            Self::V1 { .. } => {
316                let block_dir = self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?;
317                Ok(block_dir.open(
318                    ".",
319                    fio::Flags::empty(),
320                    &fio::Options::default(),
321                    server_end.into_channel(),
322                )?)
323            }
324            Self::V2 { outgoing, .. } => Ok(outgoing.open(
325                &format!("svc/{}", fvolume::VolumeMarker::PROTOCOL_NAME),
326                fio::Flags::empty(),
327                &fio::Options::default(),
328                server_end.into_channel(),
329            )?),
330        }
331    }
332
333    /// Get an open channel to the underlying ramdevice's controller.
334    pub fn open_controller(&self) -> Result<ControllerProxy, Error> {
335        match self {
336            Self::V1 { .. } => {
337                let block_dir = self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?;
338                let controller_proxy = connect_to_named_protocol_at_dir_root::<ControllerMarker>(
339                    block_dir,
340                    "device_controller",
341                )
342                .context("opening block controller")?;
343                Ok(controller_proxy)
344            }
345            Self::V2 { .. } => Err(anyhow!("Not supported")),
346        }
347    }
348
349    /// Starts unbinding the underlying ramdisk and returns before the device is removed. This
350    /// deallocates all resources for this ramdisk, which will remove all data written to the
351    /// associated ramdisk.
352    pub async fn destroy(mut self) -> Result<(), Error> {
353        match &mut self {
354            Self::V1 { ramdisk_controller, .. } => {
355                let ramdisk_controller = ramdisk_controller
356                    .take()
357                    .ok_or_else(|| anyhow!("ramdisk controller is invalid"))?;
358                let () = ramdisk_controller
359                    .schedule_unbind()
360                    .await
361                    .context("unbind transport")?
362                    .map_err(zx::Status::from_raw)
363                    .context("unbind response")?;
364            }
365            Self::V2 { .. } => {} // Dropping the event will destroy the device.
366        }
367        Ok(())
368    }
369
370    /// Unbinds the underlying ramdisk and waits for the device and all child devices to be removed.
371    /// This deallocates all resources for this ramdisk, which will remove all data written to the
372    /// associated ramdisk.
373    pub async fn destroy_and_wait_for_removal(mut self) -> Result<(), Error> {
374        match &mut self {
375            Self::V1 { block_controller, ramdisk_controller, .. } => {
376                // Calling `schedule_unbind` on the ramdisk controller initiates the unbind process
377                // but doesn't wait for anything to complete. The unbinding process starts at the
378                // ramdisk and propagates down through the child devices. FIDL connections are
379                // closed during the unbind process so the ramdisk controller connection will be
380                // closed before connections to the child block device. After unbinding, the drivers
381                // are removed starting at the children and ending at the ramdisk.
382                let ramdisk_controller = ramdisk_controller
383                    .take()
384                    .ok_or_else(|| anyhow!("ramdisk controller is invalid"))?;
385                let () = ramdisk_controller
386                    .schedule_unbind()
387                    .await
388                    .context("unbind transport")?
389                    .map_err(zx::Status::from_raw)
390                    .context("unbind response")?;
391                let _: (zx::Signals, zx::Signals) = futures::future::try_join(
392                    block_controller.on_closed(),
393                    ramdisk_controller.on_closed(),
394                )
395                .await
396                .context("on closed")?;
397            }
398            Self::V2 { .. } => {}
399        }
400        Ok(())
401    }
402
403    /// Consume the RamdiskClient without destroying the underlying ramdisk. The caller must
404    /// manually destroy the ramdisk device after calling this function.
405    ///
406    /// This should be used instead of `std::mem::forget`, as the latter will leak memory.
407    pub fn forget(mut self) -> Result<(), Error> {
408        match &mut self {
409            Self::V1 { ramdisk_controller, .. } => {
410                let _ = ramdisk_controller.take();
411                Ok(())
412            }
413            Self::V2 { .. } => Err(anyhow!("Not supported")),
414        }
415    }
416}
417
418impl BlockConnector for RamdiskClient {
419    fn connect_channel_to_volume(
420        &self,
421        server_end: fidl::endpoints::ServerEnd<fidl_fuchsia_hardware_block_volume::VolumeMarker>,
422    ) -> Result<(), Error> {
423        self.connect(server_end.into_channel().into())
424    }
425}
426
427impl Drop for RamdiskClient {
428    fn drop(&mut self) {
429        if let Self::V1 { ramdisk_controller, .. } = self {
430            if let Some(ramdisk_controller) = ramdisk_controller.take() {
431                let _: Result<Result<(), _>, _> = ControllerSynchronousProxy::new(
432                    ramdisk_controller.into_channel().unwrap().into(),
433                )
434                .schedule_unbind(zx::MonotonicInstant::INFINITE);
435            }
436        }
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use assert_matches::assert_matches;
444
445    // Note that if these tests flake, all downstream tests that depend on this crate may too.
446
447    const TEST_GUID: [u8; GUID_LEN] = [
448        0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
449        0x10,
450    ];
451
452    #[fuchsia::test]
453    async fn create_get_dir_proxy_destroy() {
454        // just make sure all the functions are hooked up properly.
455        let ramdisk =
456            RamdiskClient::builder(512, 2048).build().await.expect("failed to create ramdisk");
457        let ramdisk_dir = ramdisk.as_dir().expect("directory is invalid");
458        fuchsia_fs::directory::readdir(ramdisk_dir).await.expect("failed to readdir");
459        ramdisk.destroy().await.expect("failed to destroy the ramdisk");
460    }
461
462    #[fuchsia::test]
463    async fn create_with_dev_root_and_guid_get_dir_proxy_destroy() {
464        let dev_root = fuchsia_fs::directory::open_in_namespace(DEV_PATH, fio::PERM_READABLE)
465            .with_context(|| format!("open {}", DEV_PATH))
466            .expect("failed to create directory proxy");
467        let ramdisk = RamdiskClient::builder(512, 2048)
468            .dev_root(dev_root)
469            .guid(TEST_GUID)
470            .build()
471            .await
472            .expect("failed to create ramdisk");
473        let ramdisk_dir = ramdisk.as_dir().expect("directory is invalid");
474        fuchsia_fs::directory::readdir(ramdisk_dir).await.expect("failed to readdir");
475        ramdisk.destroy().await.expect("failed to destroy the ramdisk");
476    }
477
478    #[fuchsia::test]
479    async fn create_with_guid_get_dir_proxy_destroy() {
480        let ramdisk = RamdiskClient::builder(512, 2048)
481            .guid(TEST_GUID)
482            .build()
483            .await
484            .expect("failed to create ramdisk");
485        let ramdisk_dir = ramdisk.as_dir().expect("invalid directory proxy");
486        fuchsia_fs::directory::readdir(ramdisk_dir).await.expect("failed to readdir");
487        ramdisk.destroy().await.expect("failed to destroy the ramdisk");
488    }
489
490    #[fuchsia::test]
491    async fn create_open_destroy() {
492        let ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
493        let client = ramdisk.open().unwrap().into_proxy();
494        client.get_info().await.expect("get_info failed").unwrap();
495        ramdisk.destroy().await.expect("failed to destroy the ramdisk");
496        // The ramdisk will be scheduled to be unbound, so `client` may be valid for some time.
497    }
498
499    #[fuchsia::test]
500    async fn create_open_forget() {
501        let ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
502        let client = ramdisk.open().unwrap().into_proxy();
503        client.get_info().await.expect("get_info failed").unwrap();
504        assert!(ramdisk.forget().is_ok());
505        // We should succeed calling `get_info` as the ramdisk should still exist.
506        client.get_info().await.expect("get_info failed").unwrap();
507    }
508
509    #[fuchsia::test]
510    async fn destroy_and_wait_for_removal() {
511        let ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
512        let dir = fuchsia_fs::directory::clone(ramdisk.as_dir().unwrap()).unwrap();
513
514        assert_matches!(
515            fuchsia_fs::directory::readdir(&dir).await.unwrap().as_slice(),
516            [
517                fuchsia_fs::directory::DirEntry {
518                    name: name1,
519                    kind: fuchsia_fs::directory::DirentKind::File,
520                },
521                fuchsia_fs::directory::DirEntry {
522                    name: name2,
523                    kind: fuchsia_fs::directory::DirentKind::File,
524                },
525                fuchsia_fs::directory::DirEntry {
526                    name: name3,
527                    kind: fuchsia_fs::directory::DirentKind::File,
528                },
529            ] if [name1, name2, name3] == [
530                fidl_fuchsia_device_fs::DEVICE_CONTROLLER_NAME,
531                fidl_fuchsia_device_fs::DEVICE_PROTOCOL_NAME,
532                fidl_fuchsia_device_fs::DEVICE_TOPOLOGY_NAME,
533              ]
534        );
535
536        let () = ramdisk.destroy_and_wait_for_removal().await.unwrap();
537
538        assert_matches!(fuchsia_fs::directory::readdir(&dir).await.unwrap().as_slice(), []);
539    }
540}