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