use std::{
ffi::{CStr, CString, OsStr, c_char, c_int, c_long, c_void},
io,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
thread::JoinHandle,
time::{Duration, Instant},
};
use super::super::{
claim::{Claim, claim, reclaim_abandoned},
table::{mounts_under, resolved, unmount_under},
};
use crate::fs::{
FileSystem, Mount, Posix, SetAttr, Stat,
filesystem::posix::{
BLOCK_SIZE, NAME_MAX, OpenFlagBits, TOTAL_BLOCKS, TOTAL_INODES, attr_for,
decode_open_flags, host_errno, mode_for, unix_time,
},
};
const HOST_OPEN_FLAGS: OpenFlagBits = OpenFlagBits {
truncate: libc::O_TRUNC,
create: libc::O_CREAT,
create_new: libc::O_EXCL,
};
#[repr(C)]
#[derive(Default)]
struct VirtxStat {
ino: u64,
size: u64,
blocks: u64,
mode: u32,
nlink: u32,
blksize: u32,
_pad: u32,
mtime: i64,
mtime_nsec: i64,
atime: i64,
atime_nsec: i64,
ctime: i64,
ctime_nsec: i64,
}
fn to_virtx_stat(inode: u64, stat: &Stat) -> VirtxStat {
let attr = attr_for(stat);
let (mtime, mtime_nsec) = unix_time(attr.mtime);
let (atime, atime_nsec) = unix_time(attr.atime);
let (ctime, ctime_nsec) = unix_time(attr.ctime);
VirtxStat {
ino: inode,
size: attr.size,
blocks: attr.blocks,
mode: attr.mode,
nlink: attr.nlink,
blksize: attr.blksize,
_pad: 0,
mtime,
mtime_nsec,
atime,
atime_nsec,
ctime,
ctime_nsec,
}
}
type DirentSink = unsafe extern "C" fn(*mut c_void, u64, *const c_char, u32, u64) -> c_int;
#[repr(C)]
struct Ops {
lookup:
unsafe extern "C" fn(*mut c_void, u64, *const c_char, *mut u64, *mut VirtxStat) -> c_int,
getattr: unsafe extern "C" fn(*mut c_void, u64, *mut VirtxStat) -> c_int,
setattr:
unsafe extern "C" fn(*mut c_void, u64, u64, c_int, u64, c_int, *mut VirtxStat) -> c_int,
open: unsafe extern "C" fn(*mut c_void, u64, c_int, *mut u64) -> c_int,
create: unsafe extern "C" fn(
*mut c_void,
u64,
*const c_char,
c_int,
*mut u64,
*mut u64,
*mut VirtxStat,
) -> c_int,
read: unsafe extern "C" fn(*mut c_void, u64, u64, u64, *mut c_char) -> c_long,
write: unsafe extern "C" fn(*mut c_void, u64, u64, u64, *const c_char) -> c_long,
flush: unsafe extern "C" fn(*mut c_void, u64) -> c_int,
release: unsafe extern "C" fn(*mut c_void, u64) -> c_int,
mkdir:
unsafe extern "C" fn(*mut c_void, u64, *const c_char, *mut u64, *mut VirtxStat) -> c_int,
unlink: unsafe extern "C" fn(*mut c_void, u64, *const c_char) -> c_int,
rmdir: unsafe extern "C" fn(*mut c_void, u64, *const c_char) -> c_int,
rename: unsafe extern "C" fn(*mut c_void, u64, *const c_char, u64, *const c_char) -> c_int,
readdir: unsafe extern "C" fn(*mut c_void, u64, u64, *mut c_void, DirentSink) -> c_int,
forget: unsafe extern "C" fn(*mut c_void, u64, u64),
total_blocks: u64,
total_inodes: u64,
block_size: u32,
name_max: u32,
}
unsafe extern "C" {
fn virtx_fuse_t_mount(
mountpoint: *const c_char,
fsname: *const c_char,
backend: *const c_char,
fs: *mut c_void,
ops: *const Ops,
) -> *mut c_void;
fn virtx_fuse_t_loop(session: *mut c_void) -> c_int;
fn virtx_fuse_t_stop(session: *mut c_void);
fn virtx_fuse_t_destroy(session: *mut c_void);
}
unsafe fn recover<'a, T: FileSystem>(fs: *mut c_void) -> &'a Posix<T> {
unsafe { &*(fs as *const Posix<T>) }
}
fn code(result: io::Result<()>) -> c_int {
match result {
Ok(()) => 0,
Err(err) => -host_errno(&err),
}
}
unsafe extern "C" fn lookup<T: FileSystem>(
fs: *mut c_void,
parent: u64,
name: *const c_char,
out_inode: *mut u64,
out: *mut VirtxStat,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
let name = OsStr::from_bytes(unsafe { CStr::from_ptr(name) }.to_bytes());
code(
super::block_on(fs.lookup_child(parent, name)).map(|(inode, stat)| unsafe {
*out_inode = inode;
*out = to_virtx_stat(inode, &stat);
}),
)
}
unsafe extern "C" fn getattr<T: FileSystem>(
fs: *mut c_void,
inode: u64,
out: *mut VirtxStat,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
code(super::block_on(fs.stat_inode(inode)).map(|stat| unsafe {
*out = to_virtx_stat(inode, &stat);
}))
}
unsafe extern "C" fn setattr<T: FileSystem>(
fs: *mut c_void,
inode: u64,
_fh: u64,
_has_fh: c_int,
size: u64,
has_size: c_int,
out: *mut VirtxStat,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
let want = SetAttr {
size: (has_size != 0).then_some(size),
..Default::default()
};
code(
super::block_on(fs.setattr_inode(inode, want)).map(|stat| unsafe {
*out = to_virtx_stat(inode, &stat);
}),
)
}
unsafe extern "C" fn open<T: FileSystem>(
fs: *mut c_void,
inode: u64,
flags: c_int,
out_fh: *mut u64,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
let options = match decode_open_flags(flags, &HOST_OPEN_FLAGS) {
Ok(options) => options,
Err(err) => return -host_errno(&err),
};
code(
super::block_on(fs.open_inode(inode, options)).map(|fh| unsafe {
*out_fh = fh;
}),
)
}
unsafe extern "C" fn create<T: FileSystem>(
fs: *mut c_void,
parent: u64,
name: *const c_char,
flags: c_int,
out_inode: *mut u64,
out_fh: *mut u64,
out: *mut VirtxStat,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
let name = OsStr::from_bytes(unsafe { CStr::from_ptr(name) }.to_bytes());
let options = match decode_open_flags(flags, &HOST_OPEN_FLAGS) {
Ok(options) => options.create(true),
Err(err) => return -host_errno(&err),
};
code(
super::block_on(fs.create_child(parent, name, options)).map(|(inode, stat, fh)| unsafe {
*out_inode = inode;
*out_fh = fh;
*out = to_virtx_stat(inode, &stat);
}),
)
}
unsafe extern "C" fn read<T: FileSystem>(
fs: *mut c_void,
fh: u64,
offset: u64,
size: u64,
buf: *mut c_char,
) -> c_long {
let fs = unsafe { recover::<T>(fs) };
match super::block_on(fs.read_handle(fh, offset, size as u32)) {
Ok(data) => {
unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), buf as *mut u8, data.len()) };
data.len() as c_long
}
Err(err) => -host_errno(&err) as c_long,
}
}
unsafe extern "C" fn write<T: FileSystem>(
fs: *mut c_void,
fh: u64,
offset: u64,
size: u64,
buf: *const c_char,
) -> c_long {
let fs = unsafe { recover::<T>(fs) };
let data = unsafe { std::slice::from_raw_parts(buf as *const u8, size as usize) };
match super::block_on(fs.write_handle(fh, offset, data)) {
Ok(written) => written as c_long,
Err(err) => -host_errno(&err) as c_long,
}
}
unsafe extern "C" fn flush<T: FileSystem>(fs: *mut c_void, fh: u64) -> c_int {
let fs = unsafe { recover::<T>(fs) };
code(super::block_on(fs.flush_handle(fh)))
}
unsafe extern "C" fn release<T: FileSystem>(fs: *mut c_void, fh: u64) -> c_int {
let fs = unsafe { recover::<T>(fs) };
code(super::block_on(fs.release_handle(fh)))
}
unsafe extern "C" fn mkdir<T: FileSystem>(
fs: *mut c_void,
parent: u64,
name: *const c_char,
out_inode: *mut u64,
out: *mut VirtxStat,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
let name = OsStr::from_bytes(unsafe { CStr::from_ptr(name) }.to_bytes());
code(
super::block_on(fs.mkdir_child(parent, name)).map(|(inode, stat)| unsafe {
*out_inode = inode;
*out = to_virtx_stat(inode, &stat);
}),
)
}
unsafe extern "C" fn unlink<T: FileSystem>(
fs: *mut c_void,
parent: u64,
name: *const c_char,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
let name = OsStr::from_bytes(unsafe { CStr::from_ptr(name) }.to_bytes());
code(super::block_on(fs.unlink_child(parent, name)))
}
unsafe extern "C" fn rmdir<T: FileSystem>(
fs: *mut c_void,
parent: u64,
name: *const c_char,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
let name = OsStr::from_bytes(unsafe { CStr::from_ptr(name) }.to_bytes());
code(super::block_on(fs.rmdir_child(parent, name)))
}
unsafe extern "C" fn rename<T: FileSystem>(
fs: *mut c_void,
parent: u64,
name: *const c_char,
newparent: u64,
newname: *const c_char,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
let name = OsStr::from_bytes(unsafe { CStr::from_ptr(name) }.to_bytes());
let newname = OsStr::from_bytes(unsafe { CStr::from_ptr(newname) }.to_bytes());
code(super::block_on(
fs.rename_child(parent, name, newparent, newname),
))
}
unsafe extern "C" fn readdir<T: FileSystem>(
fs: *mut c_void,
inode: u64,
offset: u64,
sink: *mut c_void,
emit: DirentSink,
) -> c_int {
let fs = unsafe { recover::<T>(fs) };
code(super::block_on(fs.for_each_dirent(
inode,
offset,
|child_inode, child, cursor| {
let name = CString::new(child.name.as_bytes())
.map_err(|_| io::Error::from(io::ErrorKind::InvalidFilename))?;
let stop = unsafe {
emit(
sink,
child_inode,
name.as_ptr(),
mode_for(child.kind),
cursor,
)
};
Ok(stop != 0)
},
)))
}
unsafe extern "C" fn forget<T: FileSystem>(fs: *mut c_void, inode: u64, nlookup: u64) {
let fs = unsafe { recover::<T>(fs) };
fs.forget_inode(inode, nlookup);
}
fn ops_for<T: FileSystem>() -> Ops {
Ops {
lookup: lookup::<T>,
getattr: getattr::<T>,
setattr: setattr::<T>,
open: open::<T>,
create: create::<T>,
read: read::<T>,
write: write::<T>,
flush: flush::<T>,
release: release::<T>,
mkdir: mkdir::<T>,
unlink: unlink::<T>,
rmdir: rmdir::<T>,
rename: rename::<T>,
readdir: readdir::<T>,
forget: forget::<T>,
total_blocks: TOTAL_BLOCKS,
total_inodes: TOTAL_INODES,
block_size: BLOCK_SIZE as u32,
name_max: NAME_MAX,
}
}
const FSNAME: &CStr = c"virtx";
const MOUNT_TIMEOUT: Duration = Duration::from_secs(10);
const POLL_INTERVAL: Duration = Duration::from_millis(20);
const UNMOUNT_SETTLE: Duration = Duration::from_secs(3);
const LOOP_EXIT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FuseTBackend {
Nfs,
FsKit,
Smb,
}
impl FuseTBackend {
fn option(self) -> &'static CStr {
match self {
FuseTBackend::Nfs => c"nfs",
FuseTBackend::FsKit => c"fskit",
FuseTBackend::Smb => c"smb",
}
}
}
struct SessionPtr(*mut c_void);
unsafe impl Send for SessionPtr {}
impl SessionPtr {
fn get(&self) -> *mut c_void {
self.0
}
}
pub struct FuseTMount {
session: *mut c_void,
thread: Option<JoinHandle<c_int>>,
mountpoint: PathBuf,
_fs: Option<Box<dyn Send + Sync>>,
_claim: Claim,
}
impl FuseTMount {
pub fn try_new<T: FileSystem + 'static>(fs: T, mountpoint: &Path) -> io::Result<Self> {
Self::mount(fs, mountpoint, std::ptr::null())
}
pub fn try_new_with<T: FileSystem + 'static>(
fs: T,
mountpoint: &Path,
backend: FuseTBackend,
) -> io::Result<Self> {
Self::mount(fs, mountpoint, backend.option().as_ptr())
}
fn mount<T: FileSystem + 'static>(
fs: T,
mountpoint: &Path,
backend: *const c_char,
) -> io::Result<Self> {
crate::fs::mount_support()?;
reclaim_abandoned();
let c_mountpoint = CString::new(mountpoint.as_os_str().as_bytes())
.map_err(|_| io::Error::from(io::ErrorKind::InvalidFilename))?;
let fs: Box<Posix<T>> = Box::new(Posix::new(fs));
let fs_ptr = &*fs as *const Posix<T> as *mut c_void;
let ops = ops_for::<T>();
let session = unsafe {
virtx_fuse_t_mount(
c_mountpoint.as_ptr(),
FSNAME.as_ptr(),
backend,
fs_ptr,
&ops,
)
};
if session.is_null() {
return Err(io::Error::other(format!(
"FUSE-T could not mount {}: is fuse-t installed, and does the mount point \
exist and is it empty?",
mountpoint.display()
)));
}
let sendable = SessionPtr(session);
let thread = std::thread::Builder::new()
.name("virtx-fuse-t".into())
.spawn(move || unsafe { virtx_fuse_t_loop(sendable.get()) })
.inspect_err(|_| {
unsafe { virtx_fuse_t_destroy(session) };
})?;
let mount = FuseTMount {
session,
thread: Some(thread),
mountpoint: mountpoint.to_path_buf(),
_fs: Some(fs),
_claim: claim(mountpoint),
};
if !mount.wait_until_mounted(MOUNT_TIMEOUT) {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!(
"FUSE-T did not finish mounting {} within {MOUNT_TIMEOUT:?}",
mountpoint.display()
),
));
}
Ok(mount)
}
pub fn join(mut self) -> io::Result<()> {
let Some(thread) = self.thread.take() else {
return Ok(());
};
match thread.join() {
Ok(0) => Ok(()),
Ok(code) => Err(io::Error::other(format!(
"the FUSE-T session for {} ended with {code}",
self.mountpoint.display()
))),
Err(_) => Err(io::Error::other(format!(
"the thread serving {} panicked",
self.mountpoint.display()
))),
}
}
fn wait_until_mounted(&self, timeout: Duration) -> bool {
use std::os::unix::fs::MetadataExt;
let Some(parent) = self.mountpoint.parent() else {
return false;
};
let deadline = Instant::now() + timeout;
loop {
match (
std::fs::metadata(&self.mountpoint),
std::fs::metadata(parent),
) {
(Ok(here), Ok(above)) if here.dev() != above.dev() => return true,
_ => {}
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(POLL_INTERVAL);
}
}
}
impl Mount for FuseTMount {
fn mountpoint(&self) -> &Path {
&self.mountpoint
}
}
unsafe impl Send for FuseTMount {}
unsafe impl Sync for FuseTMount {}
impl Drop for FuseTMount {
fn drop(&mut self) {
if self.session.is_null() {
return;
}
let session = std::mem::replace(&mut self.session, std::ptr::null_mut());
let mountpoint = resolved(&self.mountpoint);
let was_busy = !unmount_under(&mountpoint);
unsafe { virtx_fuse_t_stop(session) };
let collected = match self.thread.take() {
Some(thread) if thread_ends(&thread, LOOP_EXIT_TIMEOUT) => {
let _ = thread.join();
true
}
Some(_) => false,
None => true,
};
let left = if was_busy {
unmount_under(&mountpoint);
settle(&mountpoint, UNMOUNT_SETTLE)
} else {
Vec::new()
};
for survivor in left {
eprintln!(
"virtx: {} would not unmount — take it down by hand",
survivor.display()
);
}
if !collected {
eprintln!(
"virtx: the thread serving {} did not stop within {LOOP_EXIT_TIMEOUT:?}; \
leaving its session and filesystem allocated",
self.mountpoint.display()
);
std::mem::forget(self._fs.take());
return;
}
unsafe { virtx_fuse_t_destroy(session) };
}
}
fn settle(mountpoint: &Path, timeout: Duration) -> Vec<PathBuf> {
let deadline = Instant::now() + timeout;
loop {
let left = mounts_under(mountpoint);
if left.is_empty() || Instant::now() >= deadline {
return left;
}
std::thread::sleep(POLL_INTERVAL);
}
}
fn thread_ends(thread: &JoinHandle<c_int>, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while !thread.is_finished() {
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(POLL_INTERVAL);
}
true
}