#![deny(missing_docs)]
use anyhow::{anyhow, bail, Context as _, Error};
use fidl::endpoints::{ClientEnd, DiscoverableProtocolMarker as _, Proxy as _, ServiceMarker};
use fidl_fuchsia_device::{ControllerMarker, ControllerProxy, ControllerSynchronousProxy};
use fidl_fuchsia_hardware_ramdisk::{Guid, RamdiskControllerMarker};
use fuchsia_component::client::{
connect_to_instance_in_service_dir, connect_to_named_protocol_at_dir_root,
connect_to_protocol_at_dir_svc,
};
use fuchsia_fs::directory::WatchEvent;
use futures::TryStreamExt;
use {
fidl_fuchsia_hardware_block as fhardware_block, fidl_fuchsia_hardware_block_volume as fvolume,
fidl_fuchsia_hardware_ramdisk as framdisk, fidl_fuchsia_io as fio,
};
const GUID_LEN: usize = 16;
const DEV_PATH: &str = "/dev";
const RAMCTL_PATH: &str = "sys/platform/ram-disk/ramctl";
const BLOCK_EXTENSION: &str = "block";
pub struct RamdiskClientBuilder {
ramdisk_source: RamdiskSource,
block_size: u64,
dev_root: Option<fio::DirectoryProxy>,
guid: Option<[u8; GUID_LEN]>,
use_v2: bool,
ramdisk_service: Option<fio::DirectoryProxy>,
publish: bool,
}
enum RamdiskSource {
Vmo { vmo: zx::Vmo },
Size { block_count: u64 },
}
impl RamdiskClientBuilder {
pub fn new(block_size: u64, block_count: u64) -> Self {
Self {
ramdisk_source: RamdiskSource::Size { block_count },
block_size,
guid: None,
dev_root: None,
use_v2: false,
ramdisk_service: None,
publish: false,
}
}
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),
guid: None,
dev_root: None,
use_v2: false,
ramdisk_service: None,
publish: false,
}
}
pub fn dev_root(mut self, dev_root: fio::DirectoryProxy) -> Self {
self.dev_root = Some(dev_root);
self
}
pub fn guid(mut self, guid: [u8; GUID_LEN]) -> Self {
self.guid = Some(guid);
self
}
pub fn use_v2(mut self) -> Self {
self.use_v2 = true;
self
}
pub fn ramdisk_service(mut self, service: fio::DirectoryProxy) -> Self {
self.ramdisk_service = Some(service);
self
}
pub fn publish(mut self) -> Self {
self.publish = true;
self
}
pub async fn build(self) -> Result<RamdiskClient, Error> {
let Self { ramdisk_source, block_size, guid, dev_root, use_v2, ramdisk_service, publish } =
self;
if use_v2 {
let ramdisk_service_dir = match ramdisk_service {
Some(s) => s,
None => fuchsia_fs::directory::open_in_namespace(
&format!("/svc/{}", fidl_fuchsia_hardware_ramdisk::ServiceMarker::SERVICE_NAME),
fio::Flags::empty(),
)?,
};
let mut watcher = fuchsia_fs::directory::Watcher::new(&ramdisk_service_dir)
.await
.context("Watcher closed")?;
let ramdisk_controller = loop {
let Some(item) = watcher.try_next().await? else {
bail!("Unexpected watcher end");
};
if let WatchEvent::ADD_FILE | WatchEvent::EXISTING = item.event {
let instance = item.filename.to_str().expect("Non UTF8 name!");
if instance == "." {
continue;
}
break connect_to_instance_in_service_dir::<
fidl_fuchsia_hardware_ramdisk::ServiceMarker,
>(&ramdisk_service_dir, instance)?
.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),
..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),
..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
}
}
}
pub enum RamdiskClient {
V1 {
block_dir: Option<fio::DirectoryProxy>,
block_controller: Option<ControllerProxy>,
ramdisk_controller: Option<ControllerProxy>,
},
V2 {
outgoing: fio::DirectoryProxy,
_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}");
let ramdisk_controller = device_watcher::recursive_wait_and_open::<ControllerMarker>(
&dev_root,
&ramdisk_controller_path,
)
.await
.with_context(|| format!("waiting for {}", &ramdisk_controller_path))?;
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: Some(block_dir),
block_controller: Some(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 })
}
pub fn builder(block_size: u64, block_count: u64) -> RamdiskClientBuilder {
RamdiskClientBuilder::new(block_size, block_count)
}
pub async fn create(block_size: u64, block_count: u64) -> Result<Self, Error> {
Self::builder(block_size, block_count).build().await
}
pub fn as_controller(&self) -> Option<&ControllerProxy> {
match self {
Self::V1 { block_controller, .. } => block_controller.as_ref(),
Self::V2 { .. } => None,
}
}
pub fn take_controller(&mut self) -> Option<ControllerProxy> {
match self {
Self::V1 { block_controller, .. } => block_controller.take(),
Self::V2 { .. } => None,
}
}
pub fn as_dir(&self) -> Option<&fio::DirectoryProxy> {
match self {
Self::V1 { block_dir, .. } => block_dir.as_ref(),
Self::V2 { .. } => None,
}
}
pub fn take_dir(&mut self) -> Option<fio::DirectoryProxy> {
match self {
Self::V1 { block_dir, .. } => block_dir.take(),
Self::V2 { .. } => None,
}
}
pub fn open(&self) -> Result<fidl::endpoints::ClientEnd<fhardware_block::BlockMarker>, Error> {
match self {
Self::V1 { .. } => {
let block_dir = self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?;
let block_proxy = connect_to_named_protocol_at_dir_root::<
fhardware_block::BlockMarker,
>(block_dir, ".")?;
let block_client_end = ClientEnd::<fhardware_block::BlockMarker>::new(
block_proxy.into_channel().unwrap().into(),
);
Ok(block_client_end)
}
Self::V2 { outgoing, .. } => {
let block_proxy =
connect_to_protocol_at_dir_svc::<fvolume::VolumeMarker>(outgoing)?;
let block_client_end = ClientEnd::<fhardware_block::BlockMarker>::new(
block_proxy.into_channel().unwrap().into(),
);
Ok(block_client_end)
}
}
}
pub fn connect(
&self,
server_end: fidl::endpoints::ServerEnd<fhardware_block::BlockMarker>,
) -> Result<(), Error> {
match self {
Self::V1 { .. } => {
let block_dir = self.as_dir().ok_or_else(|| anyhow!("directory is invalid"))?;
Ok(block_dir.open3(
".",
fio::Flags::empty(),
&fio::Options::default(),
server_end.into_channel(),
)?)
}
Self::V2 { outgoing, .. } => Ok(outgoing.open3(
&format!("svc/{}", fvolume::VolumeMarker::PROTOCOL_NAME),
fio::Flags::empty(),
&fio::Options::default(),
server_end.into_channel(),
)?),
}
}
pub fn open_controller(
&self,
) -> Result<fidl::endpoints::ClientEnd<fidl_fuchsia_device::ControllerMarker>, 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::<
fidl_fuchsia_device::ControllerMarker,
>(block_dir, "device_controller")?;
Ok(ClientEnd::new(controller_proxy.into_channel().unwrap().into()))
}
Self::V2 { .. } => Err(anyhow!("Not supported")),
}
}
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 { .. } => {} }
Ok(())
}
pub async fn destroy_and_wait_for_removal(mut self) -> Result<(), Error> {
match &mut self {
Self::V1 { block_controller, ramdisk_controller, .. } => {
let block_controller = block_controller
.take()
.ok_or_else(|| anyhow!("block controller is invalid"))?;
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(())
}
pub fn forget(mut self) -> Result<(), Error> {
match &mut self {
Self::V1 { ramdisk_controller, .. } => {
let _: ControllerProxy = ramdisk_controller
.take()
.ok_or_else(|| anyhow!("ramdisk controller is invalid"))?;
Ok(())
}
Self::V2 { .. } => Err(anyhow!("Not supported")),
}
}
}
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;
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() {
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");
}
#[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());
client.get_info().await.expect("get_info failed").unwrap();
}
#[fuchsia::test]
async fn destroy_and_wait_for_removal() {
let mut ramdisk = RamdiskClient::create(512, 2048).await.unwrap();
let dir = ramdisk.take_dir().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,
},
] if [name1, name2] == [
fidl_fuchsia_device_fs::DEVICE_CONTROLLER_NAME,
fidl_fuchsia_device_fs::DEVICE_PROTOCOL_NAME,
]
);
let () = ramdisk.destroy_and_wait_for_removal().await.unwrap();
assert_matches!(fuchsia_fs::directory::readdir(&dir).await.unwrap().as_slice(), []);
}
}