Restaged repo, allocator and runtime implemented, ioring-backed async fs/net/channel/timer primitives
This commit is contained in:
552
lib/runtime/src/fs.rs
Normal file
552
lib/runtime/src/fs.rs
Normal file
@@ -0,0 +1,552 @@
|
||||
//! Portable async filesystem API.
|
||||
//!
|
||||
//! Cancellation semantics:
|
||||
//! - Dropping an I/O future cancels interest in the result.
|
||||
//! - The runtime issues best-effort kernel cancellation where supported.
|
||||
//! - The underlying OS operation may still complete after the future is dropped.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::os::fd::{AsRawFd, OwnedFd};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::op::fs::{
|
||||
FileType as RawFileType, FsOp, MetadataTarget, OpenOptions as OpOpenOptions,
|
||||
RawDirEntry as OpDirEntry, RawMetadata,
|
||||
};
|
||||
use crate::sys::linux::fs as sys_fs;
|
||||
|
||||
struct FileInner {
|
||||
fd: OwnedFd,
|
||||
}
|
||||
|
||||
pub struct File {
|
||||
inner: Arc<FileInner>,
|
||||
}
|
||||
|
||||
pub struct OpenOptions {
|
||||
inner: OpOpenOptions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Metadata {
|
||||
inner: RawMetadata,
|
||||
}
|
||||
|
||||
pub struct ReadDir {
|
||||
inner: sys_fs::ReadDirStream,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DirEntry {
|
||||
inner: OpDirEntry,
|
||||
}
|
||||
|
||||
impl File {
|
||||
pub async fn open(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||
OpenOptions::new().read(true).open(path).await
|
||||
}
|
||||
|
||||
pub async fn create(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.read_impl(None, buf).await
|
||||
}
|
||||
|
||||
pub async fn read_exact(&mut self, mut buf: &mut [u8]) -> io::Result<()> {
|
||||
while !buf.is_empty() {
|
||||
let read = self.read(buf).await?;
|
||||
if read == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"failed to fill whole buffer",
|
||||
));
|
||||
}
|
||||
buf = &mut buf[read..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.write_impl(None, buf).await
|
||||
}
|
||||
|
||||
pub async fn write_all(&mut self, mut buf: &[u8]) -> io::Result<()> {
|
||||
while !buf.is_empty() {
|
||||
let written = self.write(buf).await?;
|
||||
if written == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::WriteZero,
|
||||
"failed to write whole buffer",
|
||||
));
|
||||
}
|
||||
buf = &buf[written..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn sync_all(&self) -> io::Result<()> {
|
||||
sys_fs::sync_all(FsOp::SyncAll { fd: self.raw_fd() }).await
|
||||
}
|
||||
|
||||
pub async fn sync_data(&self) -> io::Result<()> {
|
||||
sys_fs::sync_data(FsOp::SyncData { fd: self.raw_fd() }).await
|
||||
}
|
||||
|
||||
pub async fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.read_impl(Some(offset), buf).await
|
||||
}
|
||||
|
||||
pub async fn read_exact_at(&self, mut offset: u64, mut buf: &mut [u8]) -> io::Result<()> {
|
||||
while !buf.is_empty() {
|
||||
let read = self.read_at(offset, buf).await?;
|
||||
if read == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"failed to fill whole buffer",
|
||||
));
|
||||
}
|
||||
offset = offset.saturating_add(read as u64);
|
||||
buf = &mut buf[read..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write_at(&self, offset: u64, buf: &[u8]) -> io::Result<usize> {
|
||||
self.write_impl(Some(offset), buf).await
|
||||
}
|
||||
|
||||
pub async fn write_all_at(&self, mut offset: u64, mut buf: &[u8]) -> io::Result<()> {
|
||||
while !buf.is_empty() {
|
||||
let written = self.write_at(offset, buf).await?;
|
||||
if written == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::WriteZero,
|
||||
"failed to write whole buffer",
|
||||
));
|
||||
}
|
||||
offset = offset.saturating_add(written as u64);
|
||||
buf = &buf[written..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn metadata(&self) -> io::Result<Metadata> {
|
||||
sys_fs::metadata(FsOp::Metadata {
|
||||
target: MetadataTarget::File(self.raw_fd()),
|
||||
follow_symlinks: true,
|
||||
})
|
||||
.await
|
||||
.map(Metadata::from_raw)
|
||||
}
|
||||
|
||||
pub async fn set_len(&self, len: u64) -> io::Result<()> {
|
||||
sys_fs::set_len(FsOp::SetLen {
|
||||
fd: self.raw_fd(),
|
||||
len,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn try_clone(&self) -> io::Result<Self> {
|
||||
sys_fs::try_clone(FsOp::Duplicate { fd: self.raw_fd() })
|
||||
.await
|
||||
.map(File::from_owned_fd)
|
||||
}
|
||||
|
||||
fn from_owned_fd(fd: OwnedFd) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(FileInner { fd }),
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_fd(&self) -> i32 {
|
||||
self.inner.fd.as_raw_fd()
|
||||
}
|
||||
|
||||
async fn read_impl(&self, offset: Option<u64>, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let data = sys_fs::read(FsOp::Read {
|
||||
fd: self.raw_fd(),
|
||||
offset,
|
||||
len: buf.len(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let read = data.len();
|
||||
buf[..read].copy_from_slice(&data);
|
||||
Ok(read)
|
||||
}
|
||||
|
||||
async fn write_impl(&self, offset: Option<u64>, buf: &[u8]) -> io::Result<usize> {
|
||||
sys_fs::write(FsOp::Write {
|
||||
fd: self.raw_fd(),
|
||||
offset,
|
||||
data: buf.to_vec(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenOptions {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: OpOpenOptions::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self, value: bool) -> &mut Self {
|
||||
self.inner.read = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn write(&mut self, value: bool) -> &mut Self {
|
||||
self.inner.write = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn append(&mut self, value: bool) -> &mut Self {
|
||||
self.inner.append = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn truncate(&mut self, value: bool) -> &mut Self {
|
||||
self.inner.truncate = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn create(&mut self, value: bool) -> &mut Self {
|
||||
self.inner.create = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn create_new(&mut self, value: bool) -> &mut Self {
|
||||
self.inner.create_new = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
|
||||
sys_fs::open(FsOp::Open {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
options: self.inner.clone(),
|
||||
})
|
||||
.await
|
||||
.map(File::from_owned_fd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OpenOptions {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
fn from_raw(inner: RawMetadata) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> u64 {
|
||||
self.inner.len
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn is_file(&self) -> bool {
|
||||
self.inner.file_type == RawFileType::File
|
||||
}
|
||||
|
||||
pub fn is_dir(&self) -> bool {
|
||||
self.inner.file_type == RawFileType::Directory
|
||||
}
|
||||
|
||||
pub fn is_symlink(&self) -> bool {
|
||||
self.inner.file_type == RawFileType::Symlink
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> u16 {
|
||||
self.inner.mode
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadDir {
|
||||
pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> {
|
||||
self.inner
|
||||
.next_entry()
|
||||
.await
|
||||
.map(|entry| entry.map(|inner| DirEntry { inner }))
|
||||
}
|
||||
}
|
||||
|
||||
impl DirEntry {
|
||||
pub fn path(&self) -> PathBuf {
|
||||
self.inner.path.clone()
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> &OsStr {
|
||||
self.inner.file_name.as_os_str()
|
||||
}
|
||||
|
||||
pub async fn metadata(&self) -> io::Result<Metadata> {
|
||||
metadata(self.path()).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
|
||||
let mut file = File::open(path.as_ref()).await?;
|
||||
let mut output = Vec::new();
|
||||
let mut chunk = vec![0; 8192];
|
||||
|
||||
loop {
|
||||
let read = file.read(&mut chunk).await?;
|
||||
if read == 0 {
|
||||
return Ok(output);
|
||||
}
|
||||
output.extend_from_slice(&chunk[..read]);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
|
||||
let bytes = read(path).await?;
|
||||
String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
|
||||
}
|
||||
|
||||
pub async fn write(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> io::Result<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.await?;
|
||||
file.write_all(data.as_ref()).await
|
||||
}
|
||||
|
||||
pub async fn metadata(path: impl AsRef<Path>) -> io::Result<Metadata> {
|
||||
sys_fs::metadata(FsOp::Metadata {
|
||||
target: MetadataTarget::Path(path.as_ref().to_path_buf()),
|
||||
follow_symlinks: true,
|
||||
})
|
||||
.await
|
||||
.map(Metadata::from_raw)
|
||||
}
|
||||
|
||||
pub async fn create_dir(path: impl AsRef<Path>) -> io::Result<()> {
|
||||
sys_fs::create_dir(FsOp::CreateDir {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
recursive: false,
|
||||
mode: 0o777,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let mut current = PathBuf::new();
|
||||
|
||||
for component in path.components() {
|
||||
current.push(component.as_os_str());
|
||||
if current.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match create_dir(¤t).await {
|
||||
Ok(()) => {}
|
||||
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_file(path: impl AsRef<Path>) -> io::Result<()> {
|
||||
sys_fs::remove_file(FsOp::RemoveFile {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove_dir(path: impl AsRef<Path>) -> io::Result<()> {
|
||||
sys_fs::remove_dir(FsOp::RemoveDir {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
|
||||
sys_fs::rename(FsOp::Rename {
|
||||
from: from.as_ref().to_path_buf(),
|
||||
to: to.as_ref().to_path_buf(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn read_dir(path: impl AsRef<Path>) -> io::Result<ReadDir> {
|
||||
sys_fs::read_dir(FsOp::ReadDir {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
})
|
||||
.map(|inner| ReadDir { inner })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
OpenOptions, create_dir_all, metadata, read, read_dir, read_to_string, remove_dir,
|
||||
remove_file, rename, write,
|
||||
};
|
||||
use crate::queue_future;
|
||||
use crate::{queue_task, run};
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn test_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
fn unique_path(label: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system time should be after epoch")
|
||||
.as_nanos();
|
||||
std::env::temp_dir().join(format!("ruin-runtime-{label}-{}-{nanos}", process::id()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_fs_round_trip() {
|
||||
let _guard = test_lock().lock().unwrap();
|
||||
let root = unique_path("fs-round-trip");
|
||||
let nested = root.join("nested");
|
||||
let file_path = nested.join("hello.txt");
|
||||
let renamed_path = nested.join("renamed.txt");
|
||||
let output = Arc::new(Mutex::new(None::<String>));
|
||||
|
||||
{
|
||||
let output = Arc::clone(&output);
|
||||
queue_task(move || {
|
||||
queue_future(async move {
|
||||
create_dir_all(&nested)
|
||||
.await
|
||||
.expect("dir creation should succeed");
|
||||
write(&file_path, b"hello world")
|
||||
.await
|
||||
.expect("initial write should succeed");
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&file_path)
|
||||
.await
|
||||
.expect("open should succeed");
|
||||
file.write_at(6, b"runtime")
|
||||
.await
|
||||
.expect("positioned write should succeed");
|
||||
file.sync_all().await.expect("sync should succeed");
|
||||
|
||||
let mut prefix = [0u8; 5];
|
||||
file.read_exact_at(0, &mut prefix)
|
||||
.await
|
||||
.expect("positioned read should succeed");
|
||||
assert_eq!(&prefix, b"hello");
|
||||
|
||||
let meta = file.metadata().await.expect("metadata should succeed");
|
||||
assert!(meta.is_file());
|
||||
assert!(meta.len() >= 13);
|
||||
|
||||
let cloned = file.try_clone().await.expect("clone should succeed");
|
||||
cloned.set_len(13).await.expect("truncate should succeed");
|
||||
|
||||
rename(&file_path, &renamed_path)
|
||||
.await
|
||||
.expect("rename should succeed");
|
||||
let text = read_to_string(&renamed_path)
|
||||
.await
|
||||
.expect("read_to_string should succeed");
|
||||
assert_eq!(text, "hello runtime");
|
||||
|
||||
let bytes = read(&renamed_path).await.expect("read should succeed");
|
||||
assert_eq!(bytes, b"hello runtime");
|
||||
|
||||
let path_meta = metadata(&renamed_path)
|
||||
.await
|
||||
.expect("path metadata should work");
|
||||
assert!(path_meta.is_file());
|
||||
|
||||
*output.lock().unwrap() = Some(text);
|
||||
|
||||
remove_file(&renamed_path)
|
||||
.await
|
||||
.expect("remove_file should succeed");
|
||||
remove_dir(&nested)
|
||||
.await
|
||||
.expect("remove nested dir should succeed");
|
||||
remove_dir(&root)
|
||||
.await
|
||||
.expect("remove root dir should succeed");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
assert_eq!(output.lock().unwrap().as_deref(), Some("hello runtime"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_read_dir_streams_entries() {
|
||||
let _guard = test_lock().lock().unwrap();
|
||||
let root = unique_path("fs-read-dir");
|
||||
let one = root.join("one.txt");
|
||||
let two = root.join("two.txt");
|
||||
let seen: Arc<Mutex<BTreeSet<OsString>>> = Arc::new(Mutex::new(BTreeSet::new()));
|
||||
|
||||
{
|
||||
let seen = Arc::clone(&seen);
|
||||
queue_task(move || {
|
||||
queue_future(async move {
|
||||
create_dir_all(&root)
|
||||
.await
|
||||
.expect("dir creation should succeed");
|
||||
write(&one, b"1").await.expect("write one should succeed");
|
||||
write(&two, b"2").await.expect("write two should succeed");
|
||||
|
||||
let mut dir = read_dir(&root).await.expect("read_dir should succeed");
|
||||
while let Some(entry) = dir.next_entry().await.expect("stream should succeed") {
|
||||
seen.lock()
|
||||
.unwrap()
|
||||
.insert(entry.file_name().to_os_string());
|
||||
}
|
||||
|
||||
remove_file(&one).await.expect("remove one should succeed");
|
||||
remove_file(&two).await.expect("remove two should succeed");
|
||||
remove_dir(&root).await.expect("remove root should succeed");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
let seen = seen.lock().unwrap();
|
||||
assert!(seen.contains(&OsString::from("one.txt")));
|
||||
assert!(seen.contains(&OsString::from("two.txt")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user