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::{anyhow, Context as _, Error};
9use fidl::endpoints::{ClientEnd, 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::{connect_to_named_protocol_at_dir_root, Service};
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: Option<fio::DirectoryProxy>,
200
201        /// The device controller for the block device.
202        block_controller: Option<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 {
245            block_dir: Some(block_dir),
246            block_controller: Some(block_controller),
247            ramdisk_controller: Some(ramdisk_controller),
248        })
249    }
250
251    fn new_v2(outgoing: fio::DirectoryProxy, event: zx::EventPair) -> Result<Self, Error> {
252        Ok(Self::V2 { outgoing, _event: event })
253    }
254
255    /// Create a new ramdisk builder with the given block_size and block_count.
256    pub fn builder(block_size: u64, block_count: u64) -> RamdiskClientBuilder {
257        RamdiskClientBuilder::new(block_size, block_count)
258    }
259
260    /// Create a new ramdisk.
261    pub async fn create(block_size: u64, block_count: u64) -> Result<Self, Error> {
262        Self::builder(block_size, block_count).build().await
263    }
264
265    /// Get a reference to the block controller.
266    pub fn as_controller(&self) -> Option<&ControllerProxy> {
267        match self {
268            Self::V1 { block_controller, .. } => block_controller.as_ref(),
269            Self::V2 { .. } => None,
270        }
271    }
272
273    /// Take the block controller.
274    pub fn take_controller(&mut self) -> Option<ControllerProxy> {
275        match self {
276            Self::V1 { block_controller, .. } => block_controller.take(),
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, .. } => block_dir.as_ref(),
285            Self::V2 { .. } => None,
286        }
287    }
288
289    /// Take the block directory proxy.
290    pub fn take_dir(&mut self) -> Option<fio::DirectoryProxy> {
291        match self {
292            Self::V1 { block_dir, .. } => block_dir.take(),
293            Self::V2 { .. } => None,
294        }
295    }
296
297    /// Get an open channel to the underlying ramdevice.
298    pub fn open(&self) -> Result<fidl::endpoints::ClientEnd<fhardware_block::BlockMarker>, Error> {
299        let (client, server_end) = fidl::endpoints::create_endpoints();
300        self.connect(server_end)?;
301        Ok(client)
302    }
303
304    /// Gets a connector for the Block protocol of the ramdisk.
305    pub fn connector(&self) -> Result<Box<dyn BlockConnector>, Error> {
306        match self {
307            Self::V1 { .. } => {
308                // At this point, we have already waited on the block path to appear so we can
309                // directly open a connection to the ramdevice.
310
311                // TODO(https://fxbug.dev/42063787): In order to allow multiplexing to be removed,
312                // use connect_to_device_fidl to connect to the BlockProxy instead of
313                // connect_to_.._dir_root.  Requires downstream work.
314                let block_dir = fuchsia_fs::directory::clone(
315                    self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?,
316                )?;
317                Ok(Box::new(DirBasedBlockConnector::new(block_dir, ".".to_string())))
318            }
319            Self::V2 { outgoing, .. } => {
320                let block_dir = fuchsia_fs::directory::clone(outgoing)?;
321                Ok(Box::new(DirBasedBlockConnector::new(
322                    block_dir,
323                    format!("svc/{}", fvolume::VolumeMarker::PROTOCOL_NAME),
324                )))
325            }
326        }
327    }
328
329    /// Get an open channel to the underlying ramdevice.
330    pub fn connect(
331        &self,
332        server_end: fidl::endpoints::ServerEnd<fhardware_block::BlockMarker>,
333    ) -> Result<(), Error> {
334        match self {
335            Self::V1 { .. } => {
336                let block_dir = self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?;
337                Ok(block_dir.open(
338                    ".",
339                    fio::Flags::empty(),
340                    &fio::Options::default(),
341                    server_end.into_channel(),
342                )?)
343            }
344            Self::V2 { outgoing, .. } => Ok(outgoing.open(
345                &format!("svc/{}", fvolume::VolumeMarker::PROTOCOL_NAME),
346                fio::Flags::empty(),
347                &fio::Options::default(),
348                server_end.into_channel(),
349            )?),
350        }
351    }
352
353    /// Get an open channel to the underlying ramdevice's controller.
354    pub fn open_controller(
355        &self,
356    ) -> Result<fidl::endpoints::ClientEnd<fidl_fuchsia_device::ControllerMarker>, Error> {
357        match self {
358            Self::V1 { .. } => {
359                let block_dir = self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?;
360                let controller_proxy = connect_to_named_protocol_at_dir_root::<
361                    fidl_fuchsia_device::ControllerMarker,
362                >(block_dir, "device_controller")?;
363                Ok(ClientEnd::new(controller_proxy.into_channel().unwrap().into()))
364            }
365            Self::V2 { .. } => Err(anyhow!("Not supported")),
366        }
367    }
368
369    /// Starts unbinding the underlying ramdisk and returns before the device is removed. This
370    /// deallocates all resources for this ramdisk, which will remove all data written to the
371    /// associated ramdisk.
372    pub async fn destroy(mut self) -> Result<(), Error> {
373        match &mut self {
374            Self::V1 { ramdisk_controller, .. } => {
375                let ramdisk_controller = ramdisk_controller
376                    .take()
377                    .ok_or_else(|| anyhow!("ramdisk controller is invalid"))?;
378                let () = ramdisk_controller
379                    .schedule_unbind()
380                    .await
381                    .context("unbind transport")?
382                    .map_err(zx::Status::from_raw)
383                    .context("unbind response")?;
384            }
385            Self::V2 { .. } => {} // Dropping the event will destroy the device.
386        }
387        Ok(())
388    }
389
390    /// Unbinds the underlying ramdisk and waits for the device and all child devices to be removed.
391    /// This deallocates all resources for this ramdisk, which will remove all data written to the
392    /// associated ramdisk.
393    pub async fn destroy_and_wait_for_removal(mut self) -> Result<(), Error> {
394        match &mut self {
395            Self::V1 { block_controller, ramdisk_controller, .. } => {
396                // Calling `schedule_unbind` on the ramdisk controller initiates the unbind process
397                // but doesn't wait for anything to complete. The unbinding process starts at the
398                // ramdisk and propagates down through the child devices. FIDL connections are
399                // closed during the unbind process so the ramdisk controller connection will be
400                // closed before connections to the child block device. After unbinding, the drivers
401                // are removed starting at the children and ending at the ramdisk.
402
403                let block_controller = block_controller
404                    .take()
405                    .ok_or_else(|| anyhow!("block controller is invalid"))?;
406                let ramdisk_controller = ramdisk_controller
407                    .take()
408                    .ok_or_else(|| anyhow!("ramdisk controller is invalid"))?;
409                let () = ramdisk_controller
410                    .schedule_unbind()
411                    .await
412                    .context("unbind transport")?
413                    .map_err(zx::Status::from_raw)
414                    .context("unbind response")?;
415                let _: (zx::Signals, zx::Signals) = futures::future::try_join(
416                    block_controller.on_closed(),
417                    ramdisk_controller.on_closed(),
418                )
419                .await
420                .context("on closed")?;
421            }
422            Self::V2 { .. } => {}
423        }
424        Ok(())
425    }
426
427    /// Consume the RamdiskClient without destroying the underlying ramdisk. The caller must
428    /// manually destroy the ramdisk device after calling this function.
429    ///
430    /// This should be used instead of `std::mem::forget`, as the latter will leak memory.
431    pub fn forget(mut self) -> Result<(), Error> {
432        match &mut self {
433            Self::V1 { ramdisk_controller, .. } => {
434                let _: ControllerProxy = ramdisk_controller
435                    .take()
436                    .ok_or_else(|| anyhow!("ramdisk controller is invalid"))?;
437                Ok(())
438            }
439            Self::V2 { .. } => Err(anyhow!("Not supported")),
440        }
441    }
442}
443
444impl BlockConnector for RamdiskClient {
445    fn connect_channel_to_volume(
446        &self,
447        server_end: fidl::endpoints::ServerEnd<fidl_fuchsia_hardware_block_volume::VolumeMarker>,
448    ) -> Result<(), Error> {
449        self.connect(server_end.into_channel().into())
450    }
451}
452
453impl Drop for RamdiskClient {
454    fn drop(&mut self) {
455        if let Self::V1 { ramdisk_controller, .. } = self {
456            if let Some(ramdisk_controller) = ramdisk_controller.take() {
457                let _: Result<Result<(), _>, _> = ControllerSynchronousProxy::new(
458                    ramdisk_controller.into_channel().unwrap().into(),
459                )
460                .schedule_unbind(zx::MonotonicInstant::INFINITE);
461            }
462        }
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469    use assert_matches::assert_matches;
470
471    // Note that if these tests flake, all downstream tests that depend on this crate may too.
472
473    const TEST_GUID: [u8; GUID_LEN] = [
474        0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
475        0x10,
476    ];
477
478    #[fuchsia::test]
479    async fn create_get_dir_proxy_destroy() {
480        // just make sure all the functions are hooked up properly.
481        let ramdisk =
482            RamdiskClient::builder(512, 2048).build().await.expect("failed to create ramdisk");
483        let ramdisk_dir = ramdisk.as_dir().expect("directory is invalid");
484        fuchsia_fs::directory::readdir(ramdisk_dir).await.expect("failed to readdir");
485        ramdisk.destroy().await.expect("failed to destroy the ramdisk");
486    }
487
488    #[fuchsia::test]
489    async fn create_with_dev_root_and_guid_get_dir_proxy_destroy() {
490        let dev_root = fuchsia_fs::directory::open_in_namespace(DEV_PATH, fio::PERM_READABLE)
491            .with_context(|| format!("open {}", DEV_PATH))
492            .expect("failed to create directory proxy");
493        let ramdisk = RamdiskClient::builder(512, 2048)
494            .dev_root(dev_root)
495            .guid(TEST_GUID)
496            .build()
497            .await
498            .expect("failed to create ramdisk");
499        let ramdisk_dir = ramdisk.as_dir().expect("directory is invalid");
500        fuchsia_fs::directory::readdir(ramdisk_dir).await.expect("failed to readdir");
501        ramdisk.destroy().await.expect("failed to destroy the ramdisk");
502    }
503
504    #[fuchsia::test]
505    async fn create_with_guid_get_dir_proxy_destroy() {
506        let ramdisk = RamdiskClient::builder(512, 2048)
507            .guid(TEST_GUID)
508            .build()
509            .await
510            .expect("failed to create ramdisk");
511        let ramdisk_dir = ramdisk.as_dir().expect("invalid directory proxy");
512        fuchsia_fs::directory::readdir(ramdisk_dir).await.expect("failed to readdir");
513        ramdisk.destroy().await.expect("failed to destroy the ramdisk");
514    }
515
516    #[fuchsia::test]
517    async fn create_open_destroy() {
518        let ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
519        let client = ramdisk.open().unwrap().into_proxy();
520        client.get_info().await.expect("get_info failed").unwrap();
521        ramdisk.destroy().await.expect("failed to destroy the ramdisk");
522        // The ramdisk will be scheduled to be unbound, so `client` may be valid for some time.
523    }
524
525    #[fuchsia::test]
526    async fn create_open_forget() {
527        let ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
528        let client = ramdisk.open().unwrap().into_proxy();
529        client.get_info().await.expect("get_info failed").unwrap();
530        assert!(ramdisk.forget().is_ok());
531        // We should succeed calling `get_info` as the ramdisk should still exist.
532        client.get_info().await.expect("get_info failed").unwrap();
533    }
534
535    #[fuchsia::test]
536    async fn destroy_and_wait_for_removal() {
537        let mut ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
538        let dir = ramdisk.take_dir().unwrap();
539
540        assert_matches!(
541            fuchsia_fs::directory::readdir(&dir).await.unwrap().as_slice(),
542            [
543                fuchsia_fs::directory::DirEntry {
544                    name: name1,
545                    kind: fuchsia_fs::directory::DirentKind::File,
546                },
547                fuchsia_fs::directory::DirEntry {
548                    name: name2,
549                    kind: fuchsia_fs::directory::DirentKind::File,
550                },
551                fuchsia_fs::directory::DirEntry {
552                    name: name3,
553                    kind: fuchsia_fs::directory::DirentKind::File,
554                },
555            ] if [name1, name2, name3] == [
556                fidl_fuchsia_device_fs::DEVICE_CONTROLLER_NAME,
557                fidl_fuchsia_device_fs::DEVICE_PROTOCOL_NAME,
558                fidl_fuchsia_device_fs::DEVICE_TOPOLOGY_NAME,
559              ]
560        );
561
562        let () = ramdisk.destroy_and_wait_for_removal().await.unwrap();
563
564        assert_matches!(fuchsia_fs::directory::readdir(&dir).await.unwrap().as_slice(), []);
565    }
566}