blob: eb6cd0cb93079b2eb9df2af4c9f43759021183ac [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! A safe rust wrapper for creating and using ramdisks.
#![deny(missing_docs)]
use anyhow::{Context as _, Error, anyhow};
use fidl::endpoints::{DiscoverableProtocolMarker as _, Proxy as _};
use fidl_fuchsia_device::{ControllerMarker, ControllerProxy, ControllerSynchronousProxy};
use fidl_fuchsia_hardware_ramdisk::{Guid, RamdiskControllerMarker};
use fs_management::filesystem::{BlockConnector, DirBasedBlockConnector};
use fuchsia_component_client::{Service, connect_to_named_protocol_at_dir_root};
use {
fidl_fuchsia_hardware_ramdisk as framdisk, fidl_fuchsia_io as fio,
fidl_fuchsia_storage_block as fblock,
};
const GUID_LEN: usize = 16;
const DEV_PATH: &str = "/dev";
const RAMCTL_PATH: &str = "sys/platform/ram-disk/ramctl";
const BLOCK_EXTENSION: &str = "block";
/// A type to help construct a [`RamdeviceClient`] optionally from a VMO.
pub struct RamdiskClientBuilder {
ramdisk_source: RamdiskSource,
block_size: u64,
max_transfer_blocks: Option<u32>,
dev_root: Option<fio::DirectoryProxy>,
guid: Option<[u8; GUID_LEN]>,
use_v2: bool,
ramdisk_service: Option<fio::DirectoryProxy>,
device_flags: Option<fblock::DeviceFlag>,
// Whether to publish this ramdisk as a fuchsia.hardware.block.volume.Service service. This
// only works for the v2 driver.
publish: bool,
}
enum RamdiskSource {
Vmo { vmo: zx::Vmo },
Size { block_count: u64 },
}
impl RamdiskClientBuilder {
/// Create a new ramdisk builder
pub fn new(block_size: u64, block_count: u64) -> Self {
Self {
ramdisk_source: RamdiskSource::Size { block_count },
block_size,
max_transfer_blocks: None,
guid: None,
dev_root: None,
use_v2: false,
ramdisk_service: None,
publish: false,
device_flags: None,
}
}
/// Create a new ramdisk builder with a vmo
pub fn new_with_vmo(vmo: zx::Vmo, block_size: Option<u64>) -> Self {
Self {
ramdisk_source: RamdiskSource::Vmo { vmo },
block_size: block_size.unwrap_or(0),
max_transfer_blocks: None,
guid: None,
dev_root: None,
use_v2: false,
ramdisk_service: None,
publish: false,
device_flags: None,
}
}
/// Use the given directory as "/dev" instead of opening "/dev" from the environment.
pub fn dev_root(mut self, dev_root: fio::DirectoryProxy) -> Self {
self.dev_root = Some(dev_root);
self
}
/// Initialize the ramdisk with the given GUID, which can be queried from the ramdisk instance.
pub fn guid(mut self, guid: [u8; GUID_LEN]) -> Self {
self.guid = Some(guid);
self
}
/// Sets the maximum transfer size.
pub fn max_transfer_blocks(mut self, value: u32) -> Self {
self.max_transfer_blocks = Some(value);
self
}
/// Use the V2 ramdisk driver.
pub fn use_v2(mut self) -> Self {
self.use_v2 = true;
self
}
/// Specifies the ramdisk service.
pub fn ramdisk_service(mut self, service: fio::DirectoryProxy) -> Self {
self.ramdisk_service = Some(service);
self
}
/// Publish this ramdisk as a fuchsia.hardware.block.volume.Service service.
pub fn publish(mut self) -> Self {
self.publish = true;
self
}
/// Use the provided device flags.
pub fn device_flags(mut self, device_flags: fblock::DeviceFlag) -> Self {
self.device_flags = Some(device_flags);
self
}
/// Create the ramdisk.
pub async fn build(self) -> Result<RamdiskClient, Error> {
let Self {
ramdisk_source,
block_size,
max_transfer_blocks,
guid,
dev_root,
use_v2,
ramdisk_service,
publish,
device_flags,
} = self;
if use_v2 {
// Pick the first service instance we find.
let service = match ramdisk_service {
Some(s) => {
Service::from_service_dir_proxy(s, fidl_fuchsia_hardware_ramdisk::ServiceMarker)
}
None => Service::open(fidl_fuchsia_hardware_ramdisk::ServiceMarker)?,
};
let ramdisk_controller = service.watch_for_any().await?.connect_to_controller()?;
let type_guid = guid.map(|guid| Guid { value: guid });
let options = match ramdisk_source {
RamdiskSource::Vmo { vmo } => framdisk::Options {
vmo: Some(vmo),
block_size: if block_size == 0 {
None
} else {
Some(block_size.try_into().unwrap())
},
type_guid,
publish: Some(publish),
max_transfer_blocks,
device_flags,
..Default::default()
},
RamdiskSource::Size { block_count } => framdisk::Options {
block_count: Some(block_count),
block_size: Some(block_size.try_into().unwrap()),
type_guid,
publish: Some(publish),
max_transfer_blocks,
device_flags,
..Default::default()
},
};
let (outgoing, event) =
ramdisk_controller.create(options).await?.map_err(|s| zx::Status::from_raw(s))?;
RamdiskClient::new_v2(outgoing.into_proxy(), event)
} else {
let dev_root = if let Some(dev_root) = dev_root {
dev_root
} else {
fuchsia_fs::directory::open_in_namespace(DEV_PATH, fio::PERM_READABLE)
.with_context(|| format!("open {}", DEV_PATH))?
};
let ramdisk_controller = device_watcher::recursive_wait_and_open::<
RamdiskControllerMarker,
>(&dev_root, RAMCTL_PATH)
.await
.with_context(|| format!("waiting for {}", RAMCTL_PATH))?;
let type_guid = guid.map(|guid| Guid { value: guid });
let name = match ramdisk_source {
RamdiskSource::Vmo { vmo } => ramdisk_controller
.create_from_vmo_with_params(vmo, block_size, type_guid.as_ref())
.await?
.map_err(zx::Status::from_raw)
.context("creating ramdisk from vmo")?,
RamdiskSource::Size { block_count } => ramdisk_controller
.create(block_size, block_count, type_guid.as_ref())
.await?
.map_err(zx::Status::from_raw)
.with_context(|| format!("creating ramdisk with {} blocks", block_count))?,
};
let name = name.ok_or_else(|| anyhow!("Failed to get instance name"))?;
RamdiskClient::new(dev_root, &name).await
}
}
}
/// A client for managing a ramdisk. This can be created with the [`RamdiskClient::create`]
/// function or through the type returned by [`RamdiskClient::builder`] to specify additional
/// options.
pub enum RamdiskClient {
/// V1
V1 {
/// The directory backing the block driver.
block_dir: fio::DirectoryProxy,
/// The device controller for the block device.
block_controller: ControllerProxy,
/// The device controller for the ramdisk.
ramdisk_controller: Option<ControllerProxy>,
},
/// V2
V2 {
/// The outgoing directory for the ram-disk.
outgoing: fio::DirectoryProxy,
/// The event that keeps the ramdisk alive.
_event: zx::EventPair,
},
}
impl RamdiskClient {
async fn new(dev_root: fio::DirectoryProxy, instance_name: &str) -> Result<Self, Error> {
let ramdisk_path = format!("{RAMCTL_PATH}/{instance_name}");
let ramdisk_controller_path = format!("{ramdisk_path}/device_controller");
let block_path = format!("{ramdisk_path}/{BLOCK_EXTENSION}");
// Wait for ramdisk path to appear
let ramdisk_controller = device_watcher::recursive_wait_and_open::<ControllerMarker>(
&dev_root,
&ramdisk_controller_path,
)
.await
.with_context(|| format!("waiting for {}", &ramdisk_controller_path))?;
// Wait for the block path to appear
let block_dir = device_watcher::recursive_wait_and_open_directory(&dev_root, &block_path)
.await
.with_context(|| format!("waiting for {}", &block_path))?;
let block_controller = connect_to_named_protocol_at_dir_root::<ControllerMarker>(
&block_dir,
"device_controller",
)
.with_context(|| {
format!("opening block controller at {}/device_controller", &block_path)
})?;
Ok(Self::V1 { block_dir, block_controller, ramdisk_controller: Some(ramdisk_controller) })
}
fn new_v2(outgoing: fio::DirectoryProxy, event: zx::EventPair) -> Result<Self, Error> {
Ok(Self::V2 { outgoing, _event: event })
}
/// Create a new ramdisk builder with the given block_size and block_count.
pub fn builder(block_size: u64, block_count: u64) -> RamdiskClientBuilder {
RamdiskClientBuilder::new(block_size, block_count)
}
/// Create a new ramdisk.
pub async fn create(block_size: u64, block_count: u64) -> Result<Self, Error> {
Self::builder(block_size, block_count).build().await
}
/// Get a reference to the block controller.
pub fn as_controller(&self) -> Option<&ControllerProxy> {
match self {
Self::V1 { block_controller, .. } => Some(block_controller),
Self::V2 { .. } => None,
}
}
/// Get a reference to the block directory proxy.
pub fn as_dir(&self) -> Option<&fio::DirectoryProxy> {
match self {
Self::V1 { block_dir, .. } => Some(block_dir),
Self::V2 { .. } => None,
}
}
/// Get an open channel to the underlying ramdevice.
pub fn open(&self) -> Result<fidl::endpoints::ClientEnd<fblock::BlockMarker>, Error> {
let (client, server_end) = fidl::endpoints::create_endpoints();
self.connect(server_end)?;
Ok(client)
}
/// Gets a connector for the Block protocol of the ramdisk.
pub fn connector(&self) -> Result<Box<dyn BlockConnector>, Error> {
match self {
Self::V1 { .. } => {
// At this point, we have already waited on the block path to appear so we can
// directly open a connection to the ramdevice.
// TODO(https://fxbug.dev/42063787): In order to allow multiplexing to be removed,
// use connect_to_device_fidl to connect to the BlockProxy instead of
// connect_to_.._dir_root. Requires downstream work.
let block_dir = fuchsia_fs::directory::clone(
self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?,
)?;
Ok(Box::new(DirBasedBlockConnector::new(block_dir, ".".to_string())))
}
Self::V2 { outgoing, .. } => {
let block_dir = fuchsia_fs::directory::clone(outgoing)?;
Ok(Box::new(DirBasedBlockConnector::new(
block_dir,
format!("svc/{}", fidl_fuchsia_storage_block::BlockMarker::PROTOCOL_NAME),
)))
}
}
}
/// Get an open channel to the underlying ramdevice.
pub fn connect(
&self,
server_end: fidl::endpoints::ServerEnd<fblock::BlockMarker>,
) -> Result<(), Error> {
match self {
Self::V1 { .. } => {
let block_dir = self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?;
Ok(block_dir.open(
".",
fio::Flags::empty(),
&fio::Options::default(),
server_end.into_channel(),
)?)
}
Self::V2 { outgoing, .. } => Ok(outgoing.open(
&format!("svc/{}", fidl_fuchsia_storage_block::BlockMarker::PROTOCOL_NAME),
fio::Flags::empty(),
&fio::Options::default(),
server_end.into_channel(),
)?),
}
}
/// Get an open channel to the underlying ramdevice's controller.
pub fn open_controller(&self) -> Result<ControllerProxy, Error> {
match self {
Self::V1 { .. } => {
let block_dir = self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?;
let controller_proxy = connect_to_named_protocol_at_dir_root::<ControllerMarker>(
block_dir,
"device_controller",
)
.context("opening block controller")?;
Ok(controller_proxy)
}
Self::V2 { .. } => Err(anyhow!("Not supported")),
}
}
/// Get an open channel to the Ramdisk protocol.
pub fn open_ramdisk(&self) -> Result<framdisk::RamdiskProxy, Error> {
match self {
Self::V1 { .. } => Err(anyhow!("Not supported")),
Self::V2 { outgoing, .. } => {
let (client, server) = fidl::endpoints::create_proxy::<framdisk::RamdiskMarker>();
outgoing.open(
&format!("svc/{}", framdisk::RamdiskMarker::PROTOCOL_NAME),
fio::Flags::empty(),
&fio::Options::default(),
server.into_channel(),
)?;
Ok(client)
}
}
}
/// Starts unbinding the underlying ramdisk and returns before the device is removed. This
/// deallocates all resources for this ramdisk, which will remove all data written to the
/// associated ramdisk.
pub async fn destroy(mut self) -> Result<(), Error> {
match &mut self {
Self::V1 { ramdisk_controller, .. } => {
let ramdisk_controller = ramdisk_controller
.take()
.ok_or_else(|| anyhow!("ramdisk controller is invalid"))?;
let () = ramdisk_controller
.schedule_unbind()
.await
.context("unbind transport")?
.map_err(zx::Status::from_raw)
.context("unbind response")?;
}
Self::V2 { .. } => {} // Dropping the event will destroy the device.
}
Ok(())
}
/// Unbinds the underlying ramdisk and waits for the device and all child devices to be removed.
/// This deallocates all resources for this ramdisk, which will remove all data written to the
/// associated ramdisk.
pub async fn destroy_and_wait_for_removal(mut self) -> Result<(), Error> {
match &mut self {
Self::V1 { block_controller, ramdisk_controller, .. } => {
// Calling `schedule_unbind` on the ramdisk controller initiates the unbind process
// but doesn't wait for anything to complete. The unbinding process starts at the
// ramdisk and propagates down through the child devices. FIDL connections are
// closed during the unbind process so the ramdisk controller connection will be
// closed before connections to the child block device. After unbinding, the drivers
// are removed starting at the children and ending at the ramdisk.
let ramdisk_controller = ramdisk_controller
.take()
.ok_or_else(|| anyhow!("ramdisk controller is invalid"))?;
let () = ramdisk_controller
.schedule_unbind()
.await
.context("unbind transport")?
.map_err(zx::Status::from_raw)
.context("unbind response")?;
let _: (zx::Signals, zx::Signals) = futures::future::try_join(
block_controller.on_closed(),
ramdisk_controller.on_closed(),
)
.await
.context("on closed")?;
}
Self::V2 { .. } => {}
}
Ok(())
}
/// Consume the RamdiskClient without destroying the underlying ramdisk. The caller must
/// manually destroy the ramdisk device after calling this function.
///
/// This should be used instead of `std::mem::forget`, as the latter will leak memory.
pub fn forget(mut self) -> Result<(), Error> {
match &mut self {
Self::V1 { ramdisk_controller, .. } => {
let _ = ramdisk_controller.take();
Ok(())
}
Self::V2 { .. } => Err(anyhow!("Not supported")),
}
}
}
impl BlockConnector for RamdiskClient {
fn connect_channel_to_block(
&self,
server_end: fidl::endpoints::ServerEnd<fidl_fuchsia_storage_block::BlockMarker>,
) -> Result<(), Error> {
self.connect(server_end.into_channel().into())
}
}
impl Drop for RamdiskClient {
fn drop(&mut self) {
if let Self::V1 { ramdisk_controller, .. } = self {
if let Some(ramdisk_controller) = ramdisk_controller.take() {
let _: Result<Result<(), _>, _> = ControllerSynchronousProxy::new(
ramdisk_controller.into_channel().unwrap().into(),
)
.schedule_unbind(zx::MonotonicInstant::INFINITE);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
// Note that if these tests flake, all downstream tests that depend on this crate may too.
const TEST_GUID: [u8; GUID_LEN] = [
0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
0x10,
];
#[fuchsia::test]
async fn create_get_dir_proxy_destroy() {
// just make sure all the functions are hooked up properly.
let ramdisk =
RamdiskClient::builder(512, 2048).build().await.expect("failed to create ramdisk");
let ramdisk_dir = ramdisk.as_dir().expect("directory is invalid");
fuchsia_fs::directory::readdir(ramdisk_dir).await.expect("failed to readdir");
ramdisk.destroy().await.expect("failed to destroy the ramdisk");
}
#[fuchsia::test]
async fn create_with_dev_root_and_guid_get_dir_proxy_destroy() {
let dev_root = fuchsia_fs::directory::open_in_namespace(DEV_PATH, fio::PERM_READABLE)
.with_context(|| format!("open {}", DEV_PATH))
.expect("failed to create directory proxy");
let ramdisk = RamdiskClient::builder(512, 2048)
.dev_root(dev_root)
.guid(TEST_GUID)
.build()
.await
.expect("failed to create ramdisk");
let ramdisk_dir = ramdisk.as_dir().expect("directory is invalid");
fuchsia_fs::directory::readdir(ramdisk_dir).await.expect("failed to readdir");
ramdisk.destroy().await.expect("failed to destroy the ramdisk");
}
#[fuchsia::test]
async fn create_with_guid_get_dir_proxy_destroy() {
let ramdisk = RamdiskClient::builder(512, 2048)
.guid(TEST_GUID)
.build()
.await
.expect("failed to create ramdisk");
let ramdisk_dir = ramdisk.as_dir().expect("invalid directory proxy");
fuchsia_fs::directory::readdir(ramdisk_dir).await.expect("failed to readdir");
ramdisk.destroy().await.expect("failed to destroy the ramdisk");
}
#[fuchsia::test]
async fn create_open_destroy() {
let ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
let client = ramdisk.open().unwrap().into_proxy();
client.get_info().await.expect("get_info failed").unwrap();
ramdisk.destroy().await.expect("failed to destroy the ramdisk");
// The ramdisk will be scheduled to be unbound, so `client` may be valid for some time.
}
#[fuchsia::test]
async fn create_open_forget() {
let ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
let client = ramdisk.open().unwrap().into_proxy();
client.get_info().await.expect("get_info failed").unwrap();
assert!(ramdisk.forget().is_ok());
// We should succeed calling `get_info` as the ramdisk should still exist.
client.get_info().await.expect("get_info failed").unwrap();
}
#[fuchsia::test]
async fn destroy_and_wait_for_removal() {
let ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
let dir = fuchsia_fs::directory::clone(ramdisk.as_dir().unwrap()).unwrap();
assert_matches!(
fuchsia_fs::directory::readdir(&dir).await.unwrap().as_slice(),
[
fuchsia_fs::directory::DirEntry {
name: name1,
kind: fuchsia_fs::directory::DirentKind::File,
},
fuchsia_fs::directory::DirEntry {
name: name2,
kind: fuchsia_fs::directory::DirentKind::File,
},
fuchsia_fs::directory::DirEntry {
name: name3,
kind: fuchsia_fs::directory::DirentKind::File,
},
] if [name1, name2, name3] == [
fidl_fuchsia_device_fs::DEVICE_CONTROLLER_NAME,
fidl_fuchsia_device_fs::DEVICE_PROTOCOL_NAME,
fidl_fuchsia_device_fs::DEVICE_TOPOLOGY_NAME,
]
);
let () = ramdisk.destroy_and_wait_for_removal().await.unwrap();
assert_matches!(fuchsia_fs::directory::readdir(&dir).await.unwrap().as_slice(), []);
}
}