diff --git a/Cargo.toml b/Cargo.toml index f2e079b8..7042a6fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,10 @@ log = "0.4.6" users = "0.11.0" page_size = "0.4.2" serde = {version = "1.0.102", features = ["std", "derive"], optional = true} +pool = "0.1.4" +tokio = {version = "1.1.0", features = ["fs", "net", "macros", "rt-multi-thread", "sync", "time"]} +async-trait = "0.1.42" +futures = "0.3.12" zerocopy = "0.3" [dev-dependencies] @@ -29,6 +33,7 @@ env_logger = "0.8" clap = "2.32" bincode = "1.3.1" serde = {version = "1.0.102", features=["std", "derive"]} +dashmap = "4.0.2" [build-dependencies] pkg-config = {version = "0.3.14", optional = true } diff --git a/deny.toml b/deny.toml index 77496c2f..7ab358a1 100644 --- a/deny.toml +++ b/deny.toml @@ -34,7 +34,7 @@ targets = [ # The path where the advisory database is cloned/fetched into db-path = "~/.cargo/advisory-db" # The url of the advisory database to use -db-url = "https://github.com/rustsec/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] # The lint level for security vulnerabilities vulnerability = "deny" # The lint level for unmaintained crates diff --git a/examples/hello.rs b/examples/hello.rs index 968f21cb..8cd3a050 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,3 +1,5 @@ +#![allow(clippy::too_many_arguments)] + use fuser::{ FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, Request, @@ -51,26 +53,27 @@ const HELLO_TXT_ATTR: FileAttr = FileAttr { struct HelloFS; +#[async_trait::async_trait] impl Filesystem for HelloFS { - fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { + async fn lookup(&self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { if parent == 1 && name.to_str() == Some("hello.txt") { - reply.entry(&TTL, &HELLO_TXT_ATTR, 0); + reply.entry(&TTL, &HELLO_TXT_ATTR, 0).await; } else { - reply.error(ENOENT); + reply.error(ENOENT).await; } } - fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { + async fn getattr(&self, _req: &Request<'_>, ino: u64, reply: ReplyAttr) { match ino { - 1 => reply.attr(&TTL, &HELLO_DIR_ATTR), - 2 => reply.attr(&TTL, &HELLO_TXT_ATTR), - _ => reply.error(ENOENT), + 1 => reply.attr(&TTL, &HELLO_DIR_ATTR).await, + 2 => reply.attr(&TTL, &HELLO_TXT_ATTR).await, + _ => reply.error(ENOENT).await, } } - fn read( - &mut self, - _req: &Request, + async fn read( + &self, + _req: &Request<'_>, ino: u64, _fh: u64, offset: i64, @@ -80,22 +83,24 @@ impl Filesystem for HelloFS { reply: ReplyData, ) { if ino == 2 { - reply.data(&HELLO_TXT_CONTENT.as_bytes()[offset as usize..]); + reply + .data(&HELLO_TXT_CONTENT.as_bytes()[offset as usize..]) + .await; } else { - reply.error(ENOENT); + reply.error(ENOENT).await; } } - fn readdir( - &mut self, - _req: &Request, + async fn readdir( + &self, + _req: &Request<'_>, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory, ) { if ino != 1 { - reply.error(ENOENT); + reply.error(ENOENT).await; return; } @@ -111,11 +116,12 @@ impl Filesystem for HelloFS { break; } } - reply.ok(); + reply.ok().await; } } -fn main() { +#[tokio::main] +async fn main() { env_logger::init(); let mountpoint = env::args_os().nth(1).unwrap(); let mut options = vec![MountOption::RO, MountOption::FSName("hello".to_string())]; @@ -124,5 +130,7 @@ fn main() { options.push(MountOption::AutoUnmount); } } - fuser::mount2(HelloFS, mountpoint, &options).unwrap(); + fuser::mount2(HelloFS, 5, mountpoint, &options) + .await + .unwrap(); } diff --git a/examples/null.rs b/examples/null.rs index 6b4feecd..208f3fd9 100644 --- a/examples/null.rs +++ b/examples/null.rs @@ -5,8 +5,11 @@ struct NullFS; impl Filesystem for NullFS {} -fn main() { +#[tokio::main] +async fn main() { env_logger::init(); let mountpoint = env::args_os().nth(1).unwrap(); - fuser::mount2(NullFS, mountpoint, &[MountOption::AutoUnmount]).unwrap(); + fuser::mount2(NullFS, 5, mountpoint, &[MountOption::AutoUnmount]) + .await + .unwrap(); } diff --git a/examples/simple.rs b/examples/simple.rs index 6c3eb73c..9a4141ee 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,4 +1,4 @@ -#![allow(clippy::needless_return)] +#![allow(clippy::needless_return, clippy::too_many_arguments)] use clap::{crate_version, App, Arg}; use fuser::consts::FOPEN_DIRECT_IO; @@ -41,7 +41,7 @@ type Inode = u64; type DirectoryDescriptor = BTreeMap, (Inode, FileKind)>; -#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] enum FileKind { File, Directory, @@ -175,7 +175,7 @@ fn time_from_system_time(system_time: &SystemTime) -> (i64, u32) { } } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] struct InodeAttributes { pub inode: Inode, pub open_file_handles: u64, // Ref count of open file handles to this inode @@ -390,7 +390,7 @@ impl SimpleFS { fn insert_link( &self, - req: &Request, + req: &Request<'_>, parent: u64, name: &OsStr, inode: u64, @@ -424,8 +424,9 @@ impl SimpleFS { } } +#[async_trait::async_trait] impl Filesystem for SimpleFS { - fn init(&mut self, _req: &Request, _config: &mut KernelConfig) -> Result<(), c_int> { + async fn init(&self, _req: &Request<'_>, _config: &mut KernelConfig) -> Result<(), c_int> { fs::create_dir_all(Path::new(&self.data_dir).join("inodes")).unwrap(); fs::create_dir_all(Path::new(&self.data_dir).join("contents")).unwrap(); if self.get_inode(FUSE_ROOT_ID).is_err() { @@ -452,11 +453,9 @@ impl Filesystem for SimpleFS { Ok(()) } - fn destroy(&mut self, _req: &Request) {} - - fn lookup(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { + async fn lookup(&self, req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { if name.len() > MAX_NAME_LENGTH as usize { - reply.error(libc::ENAMETOOLONG); + reply.error(libc::ENAMETOOLONG).await; return; } let parent_attrs = self.get_inode(parent).unwrap(); @@ -468,28 +467,28 @@ impl Filesystem for SimpleFS { req.gid(), libc::X_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } match self.lookup_name(parent, name) { - Ok(attrs) => reply.entry(&Duration::new(0, 0), &attrs.into(), 0), - Err(error_code) => reply.error(error_code), + Ok(attrs) => reply.entry(&Duration::new(0, 0), &attrs.into(), 0).await, + Err(error_code) => reply.error(error_code).await, } } - fn forget(&mut self, _req: &Request, _ino: u64, _nlookup: u64) {} + async fn forget(&self, _req: &Request<'_>, _ino: u64, _nlookup: u64) {} - fn getattr(&mut self, _req: &Request, inode: u64, reply: ReplyAttr) { + async fn getattr(&self, _req: &Request<'_>, inode: u64, reply: ReplyAttr) { match self.get_inode(inode) { - Ok(attrs) => reply.attr(&Duration::new(0, 0), &attrs.into()), - Err(error_code) => reply.error(error_code), + Ok(attrs) => reply.attr(&Duration::new(0, 0), &attrs.into()).await, + Err(error_code) => reply.error(error_code).await, } } - fn setattr( - &mut self, - req: &Request, + async fn setattr( + &self, + req: &Request<'_>, inode: u64, mode: Option, uid: Option, @@ -508,7 +507,7 @@ impl Filesystem for SimpleFS { let mut attrs = match self.get_inode(inode) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -516,13 +515,13 @@ impl Filesystem for SimpleFS { if let Some(mode) = mode { debug!("chmod() called with {:?}, {:o}", inode, mode); if req.uid() != 0 && req.uid() != attrs.uid { - reply.error(libc::EPERM); + reply.error(libc::EPERM).await; return; } attrs.mode = mode as u16; attrs.last_metadata_changed = time_now(); self.write_inode(&attrs); - reply.attr(&Duration::new(0, 0), &attrs.into()); + reply.attr(&Duration::new(0, 0), &attrs.into()).await; return; } @@ -531,7 +530,7 @@ impl Filesystem for SimpleFS { if let Some(gid) = gid { // Non-root users can only change gid to a group they're in if req.uid() != 0 && !get_groups(req.pid()).contains(&gid) { - reply.error(libc::EPERM); + reply.error(libc::EPERM).await; return; } } @@ -540,13 +539,13 @@ impl Filesystem for SimpleFS { // but no-op changes by the owner are not an error && !(uid == attrs.uid && req.uid() == attrs.uid) { - reply.error(libc::EPERM); + reply.error(libc::EPERM).await; return; } } // Only owner may change the group if gid.is_some() && req.uid() != 0 && req.uid() != attrs.uid { - reply.error(libc::EPERM); + reply.error(libc::EPERM).await; return; } @@ -558,7 +557,7 @@ impl Filesystem for SimpleFS { } attrs.last_metadata_changed = time_now(); self.write_inode(&attrs); - reply.attr(&Duration::new(0, 0), &attrs.into()); + reply.attr(&Duration::new(0, 0), &attrs.into()).await; return; } @@ -571,15 +570,15 @@ impl Filesystem for SimpleFS { // chmod'ed if self.check_file_handle_write(handle) { if let Err(error_code) = self.truncate(inode, size, 0, 0) { - reply.error(error_code); + reply.error(error_code).await; return; } } else { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } } else if let Err(error_code) = self.truncate(inode, size, req.uid(), req.gid()) { - reply.error(error_code); + reply.error(error_code).await; return; } } @@ -589,7 +588,7 @@ impl Filesystem for SimpleFS { debug!("utimens() called with {:?}, atime={:?}", inode, atime); if attrs.uid != req.uid() && req.uid() != 0 && atime != Now { - reply.error(libc::EPERM); + reply.error(libc::EPERM).await; return; } @@ -603,7 +602,7 @@ impl Filesystem for SimpleFS { libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -618,7 +617,7 @@ impl Filesystem for SimpleFS { debug!("utimens() called with {:?}, mtime={:?}", inode, mtime); if attrs.uid != req.uid() && req.uid() != 0 && mtime != Now { - reply.error(libc::EPERM); + reply.error(libc::EPERM).await; return; } @@ -632,7 +631,7 @@ impl Filesystem for SimpleFS { libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -645,26 +644,26 @@ impl Filesystem for SimpleFS { } let attrs = self.get_inode(inode).unwrap(); - reply.attr(&Duration::new(0, 0), &attrs.into()); + reply.attr(&Duration::new(0, 0), &attrs.into()).await; return; } - fn readlink(&mut self, _req: &Request, inode: u64, reply: ReplyData) { + async fn readlink(&self, _req: &Request<'_>, inode: u64, reply: ReplyData) { debug!("readlink() called on {:?}", inode); let path = self.content_path(inode); if let Ok(mut file) = File::open(&path) { let file_size = file.metadata().unwrap().len(); let mut buffer = vec![0; file_size as usize]; file.read_exact(&mut buffer).unwrap(); - reply.data(&buffer); + reply.data(&buffer).await; } else { - reply.error(libc::ENOENT); + reply.error(libc::ENOENT).await; } } - fn mknod( - &mut self, - req: &Request, + async fn mknod( + &self, + req: &Request<'_>, parent: u64, name: &OsStr, mode: u32, @@ -680,19 +679,19 @@ impl Filesystem for SimpleFS { { // TODO warn!("mknod() implementation is incomplete. Only supports regular files, symlinks, and directories. Got {:o}", mode); - reply.error(libc::ENOSYS); + reply.error(libc::ENOSYS).await; return; } if self.lookup_name(parent, name).is_ok() { - reply.error(libc::EEXIST); + reply.error(libc::EEXIST).await; return; } let mut parent_attrs = match self.get_inode(parent) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -705,7 +704,7 @@ impl Filesystem for SimpleFS { req.gid(), libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } parent_attrs.last_modified = time_now(); @@ -743,12 +742,12 @@ impl Filesystem for SimpleFS { self.write_directory_content(parent, entries); // TODO: implement flags - reply.entry(&Duration::new(0, 0), &attrs.into(), 0); + reply.entry(&Duration::new(0, 0), &attrs.into(), 0).await; } - fn mkdir( - &mut self, - req: &Request, + async fn mkdir( + &self, + req: &Request<'_>, parent: u64, name: &OsStr, mode: u32, @@ -757,14 +756,14 @@ impl Filesystem for SimpleFS { ) { debug!("mkdir() called with {:?} {:?} {:o}", parent, name, mode); if self.lookup_name(parent, name).is_ok() { - reply.error(libc::EEXIST); + reply.error(libc::EEXIST).await; return; } let mut parent_attrs = match self.get_inode(parent) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -777,7 +776,7 @@ impl Filesystem for SimpleFS { req.gid(), libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } parent_attrs.last_modified = time_now(); @@ -811,15 +810,15 @@ impl Filesystem for SimpleFS { entries.insert(name.as_bytes().to_vec(), (inode, FileKind::Directory)); self.write_directory_content(parent, entries); - reply.entry(&Duration::new(0, 0), &attrs.into(), 0); + reply.entry(&Duration::new(0, 0), &attrs.into(), 0).await; } - fn unlink(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { + async fn unlink(&self, req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { debug!("unlink() called with {:?} {:?}", parent, name); let mut attrs = match self.lookup_name(parent, name) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -827,7 +826,7 @@ impl Filesystem for SimpleFS { let mut parent_attrs = match self.get_inode(parent) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -840,7 +839,7 @@ impl Filesystem for SimpleFS { req.gid(), libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -851,7 +850,7 @@ impl Filesystem for SimpleFS { && uid != parent_attrs.uid && uid != attrs.uid { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -868,15 +867,15 @@ impl Filesystem for SimpleFS { entries.remove(name.as_bytes()); self.write_directory_content(parent, entries); - reply.ok(); + reply.ok().await; } - fn rmdir(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { + async fn rmdir(&self, req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { debug!("rmdir() called with {:?} {:?}", parent, name); let mut attrs = match self.lookup_name(parent, name) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -884,14 +883,14 @@ impl Filesystem for SimpleFS { let mut parent_attrs = match self.get_inode(parent) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; // Directories always have a self and parent link if self.get_directory_content(attrs.inode).unwrap().len() > 2 { - reply.error(libc::ENOTEMPTY); + reply.error(libc::ENOTEMPTY).await; return; } if !check_access( @@ -902,7 +901,7 @@ impl Filesystem for SimpleFS { req.gid(), libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -912,7 +911,7 @@ impl Filesystem for SimpleFS { && req.uid() != parent_attrs.uid && req.uid() != attrs.uid { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -929,12 +928,12 @@ impl Filesystem for SimpleFS { entries.remove(name.as_bytes()); self.write_directory_content(parent, entries); - reply.ok(); + reply.ok().await; } - fn symlink( - &mut self, - req: &Request, + async fn symlink( + &self, + req: &Request<'_>, parent: u64, name: &OsStr, link: &Path, @@ -958,7 +957,7 @@ impl Filesystem for SimpleFS { }; if let Err(error_code) = self.insert_link(req, parent, name, inode, FileKind::Symlink) { - reply.error(error_code); + reply.error(error_code).await; return; } self.write_inode(&attrs); @@ -972,12 +971,12 @@ impl Filesystem for SimpleFS { .unwrap(); file.write_all(link.as_os_str().as_bytes()).unwrap(); - reply.entry(&Duration::new(0, 0), &attrs.into(), 0); + reply.entry(&Duration::new(0, 0), &attrs.into(), 0).await; } - fn rename( - &mut self, - req: &Request, + async fn rename( + &self, + req: &Request<'_>, parent: u64, name: &OsStr, new_parent: u64, @@ -988,7 +987,7 @@ impl Filesystem for SimpleFS { let mut inode_attrs = match self.lookup_name(parent, name) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -996,7 +995,7 @@ impl Filesystem for SimpleFS { let mut parent_attrs = match self.get_inode(parent) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -1009,7 +1008,7 @@ impl Filesystem for SimpleFS { req.gid(), libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -1019,14 +1018,14 @@ impl Filesystem for SimpleFS { && req.uid() != parent_attrs.uid && req.uid() != inode_attrs.uid { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } let mut new_parent_attrs = match self.get_inode(new_parent) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -1039,7 +1038,7 @@ impl Filesystem for SimpleFS { req.gid(), libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -1050,7 +1049,7 @@ impl Filesystem for SimpleFS { && req.uid() != new_parent_attrs.uid && req.uid() != existing_attrs.uid { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } } @@ -1065,7 +1064,7 @@ impl Filesystem for SimpleFS { .len() > 2 { - reply.error(libc::ENOTEMPTY); + reply.error(libc::ENOTEMPTY).await; return; } } @@ -1083,7 +1082,7 @@ impl Filesystem for SimpleFS { libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -1129,12 +1128,12 @@ impl Filesystem for SimpleFS { self.write_directory_content(inode_attrs.inode, entries); } - reply.ok(); + reply.ok().await; } - fn link( - &mut self, - req: &Request, + async fn link( + &self, + req: &Request<'_>, inode: u64, new_parent: u64, new_name: &OsStr, @@ -1147,27 +1146,27 @@ impl Filesystem for SimpleFS { let mut attrs = match self.get_inode(inode) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; if let Err(error_code) = self.insert_link(&req, new_parent, new_name, inode, attrs.kind) { - reply.error(error_code); + reply.error(error_code).await; } else { attrs.hardlinks += 1; attrs.last_metadata_changed = time_now(); self.write_inode(&attrs); - reply.entry(&Duration::new(0, 0), &attrs.into(), 0); + reply.entry(&Duration::new(0, 0), &attrs.into(), 0).await; } } - fn open(&mut self, req: &Request, inode: u64, flags: i32, reply: ReplyOpen) { + async fn open(&self, req: &Request<'_>, inode: u64, flags: i32, reply: ReplyOpen) { debug!("open() called for {:?}", inode); let (access_mask, read, write) = match flags & libc::O_ACCMODE { libc::O_RDONLY => { // Behavior is undefined, but most filesystems return EACCES if flags & libc::O_TRUNC != 0 { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } if flags & FMODE_EXEC != 0 { @@ -1181,7 +1180,7 @@ impl Filesystem for SimpleFS { libc::O_RDWR => (libc::R_OK | libc::W_OK, true, true), // Exactly one access mode flag must be specified _ => { - reply.error(libc::EINVAL); + reply.error(libc::EINVAL).await; return; } }; @@ -1199,20 +1198,22 @@ impl Filesystem for SimpleFS { attr.open_file_handles += 1; self.write_inode(&attr); let open_flags = if self.direct_io { FOPEN_DIRECT_IO } else { 0 }; - reply.opened(self.allocate_next_file_handle(read, write), open_flags); + reply + .opened(self.allocate_next_file_handle(read, write), open_flags) + .await; return; } else { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } } - Err(error_code) => reply.error(error_code), + Err(error_code) => reply.error(error_code).await, } } - fn read( - &mut self, - _req: &Request, + async fn read( + &self, + _req: &Request<'_>, inode: u64, fh: u64, offset: i64, @@ -1227,7 +1228,7 @@ impl Filesystem for SimpleFS { ); assert!(offset >= 0); if !self.check_file_handle_read(fh) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -1239,15 +1240,15 @@ impl Filesystem for SimpleFS { let mut buffer = vec![0; read_size as usize]; file.read_exact_at(&mut buffer, offset as u64).unwrap(); - reply.data(&buffer); + reply.data(&buffer).await; } else { - reply.error(libc::ENOENT); + reply.error(libc::ENOENT).await; } } - fn write( - &mut self, - _req: &Request, + async fn write( + &self, + _req: &Request<'_>, inode: u64, fh: u64, offset: i64, @@ -1260,7 +1261,7 @@ impl Filesystem for SimpleFS { debug!("write() called with {:?} size={:?}", inode, data.len()); assert!(offset >= 0); if !self.check_file_handle_write(fh) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -1277,14 +1278,14 @@ impl Filesystem for SimpleFS { } self.write_inode(&attrs); - reply.written(data.len() as u32); + reply.written(data.len() as u32).await; } else { - reply.error(libc::EBADF); + reply.error(libc::EBADF).await; } } - fn release( - &mut self, + async fn release( + &self, _req: &Request<'_>, inode: u64, _fh: u64, @@ -1296,16 +1297,16 @@ impl Filesystem for SimpleFS { if let Ok(mut attrs) = self.get_inode(inode) { attrs.open_file_handles -= 1; } - reply.ok(); + reply.ok().await; } - fn opendir(&mut self, req: &Request, inode: u64, flags: i32, reply: ReplyOpen) { + async fn opendir(&self, req: &Request<'_>, inode: u64, flags: i32, reply: ReplyOpen) { debug!("opendir() called on {:?}", inode); let (access_mask, read, write) = match flags & libc::O_ACCMODE { libc::O_RDONLY => { // Behavior is undefined, but most filesystems return EACCES if flags & libc::O_TRUNC != 0 { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } (libc::R_OK, true, false) @@ -1314,7 +1315,7 @@ impl Filesystem for SimpleFS { libc::O_RDWR => (libc::R_OK | libc::W_OK, true, true), // Exactly one access mode flag must be specified _ => { - reply.error(libc::EINVAL); + reply.error(libc::EINVAL).await; return; } }; @@ -1332,20 +1333,22 @@ impl Filesystem for SimpleFS { attr.open_file_handles += 1; self.write_inode(&attr); let open_flags = if self.direct_io { FOPEN_DIRECT_IO } else { 0 }; - reply.opened(self.allocate_next_file_handle(read, write), open_flags); + reply + .opened(self.allocate_next_file_handle(read, write), open_flags) + .await; return; } else { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } } - Err(error_code) => reply.error(error_code), + Err(error_code) => reply.error(error_code).await, } } - fn readdir( - &mut self, - _req: &Request, + async fn readdir( + &self, + _req: &Request<'_>, inode: u64, _fh: u64, offset: i64, @@ -1356,7 +1359,7 @@ impl Filesystem for SimpleFS { let entries = match self.get_directory_content(inode) { Ok(entries) => entries, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -1376,11 +1379,11 @@ impl Filesystem for SimpleFS { } } - reply.ok(); + reply.ok().await; } - fn releasedir( - &mut self, + async fn releasedir( + &self, _req: &Request<'_>, inode: u64, _fh: u64, @@ -1390,26 +1393,28 @@ impl Filesystem for SimpleFS { if let Ok(mut attrs) = self.get_inode(inode) { attrs.open_file_handles -= 1; } - reply.ok(); + reply.ok().await; } - fn statfs(&mut self, _req: &Request, _ino: u64, reply: ReplyStatfs) { + async fn statfs(&self, _req: &Request<'_>, _ino: u64, reply: ReplyStatfs) { warn!("statfs() implementation is a stub"); // TODO: real implementation of this - reply.statfs( - 10, - 10, - 10, - 1, - 10, - BLOCK_SIZE as u32, - MAX_NAME_LENGTH, - BLOCK_SIZE as u32, - ); + reply + .statfs( + 10, + 10, + 10, + 1, + 10, + BLOCK_SIZE as u32, + MAX_NAME_LENGTH, + BLOCK_SIZE as u32, + ) + .await; } - fn setxattr( - &mut self, + async fn setxattr( + &self, request: &Request<'_>, inode: u64, key: &OsStr, @@ -1420,21 +1425,21 @@ impl Filesystem for SimpleFS { ) { if let Ok(mut attrs) = self.get_inode(inode) { if let Err(error) = xattr_access_check(key.as_bytes(), libc::W_OK, &attrs, request) { - reply.error(error); + reply.error(error).await; return; } attrs.xattrs.insert(key.as_bytes().to_vec(), value.to_vec()); attrs.last_metadata_changed = time_now(); self.write_inode(&attrs); - reply.ok(); + reply.ok().await; } else { - reply.error(libc::EBADF); + reply.error(libc::EBADF).await; } } - fn getxattr( - &mut self, + async fn getxattr( + &self, request: &Request<'_>, inode: u64, key: &OsStr, @@ -1443,30 +1448,30 @@ impl Filesystem for SimpleFS { ) { if let Ok(attrs) = self.get_inode(inode) { if let Err(error) = xattr_access_check(key.as_bytes(), libc::R_OK, &attrs, request) { - reply.error(error); + reply.error(error).await; return; } if let Some(data) = attrs.xattrs.get(key.as_bytes()) { if size == 0 { - reply.size(data.len() as u32); + reply.size(data.len() as u32).await; } else if data.len() <= size as usize { - reply.data(&data); + reply.data(&data).await; } else { - reply.error(libc::ERANGE); + reply.error(libc::ERANGE).await; } } else { #[cfg(target_os = "linux")] - reply.error(libc::ENODATA); + reply.error(libc::ENODATA).await; #[cfg(not(target_os = "linux"))] - reply.error(libc::ENOATTR); + reply.error(libc::ENOATTR).await; } } else { - reply.error(libc::EBADF); + reply.error(libc::EBADF).await; } } - fn listxattr(&mut self, _req: &Request<'_>, inode: u64, size: u32, reply: ReplyXattr) { + async fn listxattr(&self, _req: &Request<'_>, inode: u64, size: u32, reply: ReplyXattr) { if let Ok(attrs) = self.get_inode(inode) { let mut bytes = vec![]; // Convert to concatenated null-terminated strings @@ -1475,56 +1480,56 @@ impl Filesystem for SimpleFS { bytes.push(0); } if size == 0 { - reply.size(bytes.len() as u32); + reply.size(bytes.len() as u32).await; } else if bytes.len() <= size as usize { - reply.data(&bytes); + reply.data(&bytes).await; } else { - reply.error(libc::ERANGE); + reply.error(libc::ERANGE).await; } } else { - reply.error(libc::EBADF); + reply.error(libc::EBADF).await; } } - fn removexattr(&mut self, request: &Request<'_>, inode: u64, key: &OsStr, reply: ReplyEmpty) { + async fn removexattr(&self, request: &Request<'_>, inode: u64, key: &OsStr, reply: ReplyEmpty) { if let Ok(mut attrs) = self.get_inode(inode) { if let Err(error) = xattr_access_check(key.as_bytes(), libc::W_OK, &attrs, request) { - reply.error(error); + reply.error(error).await; return; } if attrs.xattrs.remove(key.as_bytes()).is_none() { #[cfg(target_os = "linux")] - reply.error(libc::ENODATA); + reply.error(libc::ENODATA).await; #[cfg(not(target_os = "linux"))] - reply.error(libc::ENOATTR); + reply.error(libc::ENOATTR).await; return; } attrs.last_metadata_changed = time_now(); self.write_inode(&attrs); - reply.ok(); + reply.ok().await; } else { - reply.error(libc::EBADF); + reply.error(libc::EBADF).await; } } - fn access(&mut self, req: &Request, inode: u64, mask: i32, reply: ReplyEmpty) { + async fn access(&self, req: &Request<'_>, inode: u64, mask: i32, reply: ReplyEmpty) { debug!("access() called with {:?} {:?}", inode, mask); match self.get_inode(inode) { Ok(attr) => { if check_access(attr.uid, attr.gid, attr.mode, req.uid(), req.gid(), mask) { - reply.ok(); + reply.ok().await; } else { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; } } - Err(error_code) => reply.error(error_code), + Err(error_code) => reply.error(error_code).await, } } - fn create( - &mut self, - req: &Request, + async fn create( + &self, + req: &Request<'_>, parent: u64, name: &OsStr, mode: u32, @@ -1534,7 +1539,7 @@ impl Filesystem for SimpleFS { ) { debug!("create() called with {:?} {:?}", parent, name); if self.lookup_name(parent, name).is_ok() { - reply.error(libc::EEXIST); + reply.error(libc::EEXIST).await; return; } @@ -1544,7 +1549,7 @@ impl Filesystem for SimpleFS { libc::O_RDWR => (true, true), // Exactly one access mode flag must be specified _ => { - reply.error(libc::EINVAL); + reply.error(libc::EINVAL).await; return; } }; @@ -1552,7 +1557,7 @@ impl Filesystem for SimpleFS { let mut parent_attrs = match self.get_inode(parent) { Ok(attrs) => attrs, Err(error_code) => { - reply.error(error_code); + reply.error(error_code).await; return; } }; @@ -1565,7 +1570,7 @@ impl Filesystem for SimpleFS { req.gid(), libc::W_OK, ) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } parent_attrs.last_modified = time_now(); @@ -1575,7 +1580,7 @@ impl Filesystem for SimpleFS { let inode = self.allocate_next_inode(); let attrs = InodeAttributes { inode, - open_file_handles: 0, + open_file_handles: 1, size: 0, last_accessed: time_now(), last_modified: time_now(), @@ -1603,18 +1608,20 @@ impl Filesystem for SimpleFS { self.write_directory_content(parent, entries); // TODO: implement flags - reply.created( - &Duration::new(0, 0), - &attrs.into(), - 0, - self.allocate_next_file_handle(read, write), - 0, - ); + reply + .created( + &Duration::new(0, 0), + &attrs.into(), + 0, + self.allocate_next_file_handle(read, write), + 0, + ) + .await; } #[cfg(target_os = "linux")] - fn fallocate( - &mut self, + async fn fallocate( + &self, _req: &Request<'_>, inode: u64, _fh: u64, @@ -1637,14 +1644,14 @@ impl Filesystem for SimpleFS { } self.write_inode(&attrs); } - reply.ok(); + reply.ok().await; } else { - reply.error(libc::ENOENT); + reply.error(libc::ENOENT).await; } } - fn copy_file_range( - &mut self, + async fn copy_file_range( + &self, _req: &Request<'_>, src_inode: u64, src_fh: u64, @@ -1661,11 +1668,11 @@ impl Filesystem for SimpleFS { src_fh, src_inode, src_offset, dest_fh, dest_inode, dest_offset, size ); if !self.check_file_handle_read(src_fh) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } if !self.check_file_handle_write(dest_fh) { - reply.error(libc::EACCES); + reply.error(libc::EACCES).await; return; } @@ -1691,12 +1698,12 @@ impl Filesystem for SimpleFS { } self.write_inode(&attrs); - reply.written(data.len() as u32); + reply.written(data.len() as u32).await; } else { - reply.error(libc::EBADF); + reply.error(libc::EBADF).await; } } else { - reply.error(libc::ENOENT); + reply.error(libc::ENOENT).await; } } } @@ -1777,7 +1784,8 @@ fn fuse_allow_other_enabled() -> io::Result { Ok(false) } -fn main() { +#[tokio::main] +async fn main() { let matches = App::new("Fuser") .version(crate_version!()) .author("Christopher Berner") @@ -1829,10 +1837,7 @@ fn main() { .filter_level(log_level) .init(); - let mut options = vec![ - MountOption::FSName("fuser".to_string()), - MountOption::AutoUnmount, - ]; + let mut options = vec![MountOption::FSName("fuser".to_string())]; if let Ok(enabled) = fuse_allow_other_enabled() { if enabled { options.push(MountOption::AllowOther); @@ -1848,10 +1853,13 @@ fn main() { .unwrap_or_default() .to_string(); + // wrap the FS in a mutex lock such that we don't need to concern ourselves about concurrent updates. fuser::mount2( SimpleFS::new(data_dir, matches.is_present("direct-io")), + 0, mountpoint, &options, ) + .await .unwrap(); } diff --git a/examples/xmp.rs b/examples/xmp.rs new file mode 100644 index 00000000..99d7d09a --- /dev/null +++ b/examples/xmp.rs @@ -0,0 +1,1012 @@ +//! Analogue of fusexmp +//! +//! See also a more high-level example: https://github.com/wfraser/fuse-mt/tree/master/example +#![allow(clippy::too_many_arguments)] + +use fuser::{ + FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, + ReplyEntry, ReplyOpen, ReplyWrite, Request, TimeOrNow, +}; +use libc::c_int; +use libc::{EINVAL, EIO, ENOENT, ENOSYS, EPERM}; +use libc::{O_ACCMODE, O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY}; +use std::{ + convert::TryInto, + env, + fs::{File, OpenOptions}, + sync::Arc, +}; +use std::{ + ffi::{OsStr, OsString}, + os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd}, +}; +use std::{ + path::PathBuf, + time::{Duration, UNIX_EPOCH}, +}; +use tokio::sync::Mutex; + +use async_trait::async_trait; +use dashmap::DashMap; +use log::error; +use std::collections::HashMap; +use std::io::ErrorKind; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt}; +use std::path::Path; +use std::time::SystemTime; + +const TTL: Duration = Duration::from_secs(1); // 1 second + +struct DirInfo { + ino: u64, + name: OsString, + kind: FileType, +} + +struct XmpFS { + /// I don't want to include `slab` in dev-dependencies, so using a counter instead. + /// This provides a source of new inodes and filehandles + counter: std::sync::atomic::AtomicU64, + mount_src: OsString, + inode_to_physical_path: dashmap::DashMap, + mounted_path_to_inode: DashMap, + opened_directories: Arc>>>, + opened_files: DashMap, +} + +fn read_blocking(f: File, size: usize, offset: i64) -> std::io::Result> { + let mut b = vec![0; size]; + + use std::os::unix::fs::FileExt; + + f.read_at(&mut b[..], offset as u64)?; + f.into_raw_fd(); + + Ok(b) +} + +impl XmpFS { + pub fn new(mount_src: &OsString) -> XmpFS { + XmpFS { + counter: std::sync::atomic::AtomicU64::new(1), + mount_src: mount_src.to_owned(), + inode_to_physical_path: DashMap::with_capacity(1024), + mounted_path_to_inode: DashMap::with_capacity(1024), + opened_directories: Arc::new(Mutex::new(HashMap::with_capacity(2))), + opened_files: DashMap::new(), + } + } + + pub async fn populate_root_dir(&mut self) { + let rootino = self + .add_inode(OsStr::from_bytes(b"/"), &self.mount_src) + .await; + assert_eq!(rootino, 1); + } + + pub async fn add_inode(&self, mounted_path: &OsStr, physical_path: &OsStr) -> u64 { + let ino = self + .counter + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + self.mounted_path_to_inode + .insert(mounted_path.to_os_string(), ino); + self.inode_to_physical_path + .insert(ino, physical_path.to_os_string()); + ino + } + + fn mounted_path_to_physical_path(&self, mounted_path: &Path) -> std::path::PathBuf { + let mount_root = Path::new(&self.mount_src); + mount_root.join(mounted_path) + } + + pub async fn add_or_create_inode(&self, mounted_path: impl AsRef) -> u64 { + if let Some(x) = self + .mounted_path_to_inode + .get(mounted_path.as_ref().as_os_str()) + { + return *x; + } + + let mounted_path_ref: &Path = mounted_path.as_ref(); + + self.add_inode( + mounted_path_ref.as_os_str(), + self.mounted_path_to_physical_path(mounted_path_ref) + .as_os_str(), + ) + .await + } + pub async fn get_inode(&self, path: impl AsRef) -> Option { + self.mounted_path_to_inode + .get(path.as_ref().as_os_str()) + .map(|x| *x) + } + + pub async fn unregister_ino(&self, ino: u64) { + if !self.inode_to_physical_path.contains_key(&ino) { + return; + } + self.mounted_path_to_inode + .remove(&*self.inode_to_physical_path.get(&ino).unwrap()); + self.inode_to_physical_path.remove(&ino); + } + + fn entry_path_from_parentino_and_name(&self, parent: u64, name: &OsStr) -> Option { + match self.inode_to_physical_path.get(&parent) { + None => None, + Some(parent) => { + let parent_path = Path::new(parent.value()); + Some(parent_path.join(name)) + } + } + } +} + +fn ft2ft(t: std::fs::FileType) -> FileType { + match t { + x if x.is_symlink() => FileType::Symlink, + x if x.is_dir() => FileType::Directory, + x if x.is_file() => FileType::RegularFile, + x if x.is_fifo() => FileType::NamedPipe, + x if x.is_char_device() => FileType::CharDevice, + x if x.is_block_device() => FileType::BlockDevice, + x if x.is_socket() => FileType::Socket, + _ => FileType::RegularFile, + } +} + +fn meta2attr(m: &std::fs::Metadata, ino: u64) -> FileAttr { + FileAttr { + ino, + size: m.size(), + blocks: m.blocks(), + atime: m.accessed().unwrap_or(UNIX_EPOCH), + mtime: m.modified().unwrap_or(UNIX_EPOCH), + ctime: UNIX_EPOCH + Duration::from_secs(m.ctime().try_into().unwrap_or(0)), + crtime: m.created().unwrap_or(UNIX_EPOCH), + kind: ft2ft(m.file_type()), + perm: m.permissions().mode() as u16, + nlink: m.nlink() as u32, + uid: m.uid(), + gid: m.gid(), + rdev: m.rdev() as u32, + flags: 0, + blksize: m.blksize() as u32, + padding: 0, + } +} + +async fn errhandle( + e: std::io::Error, + not_found: impl FnOnce() -> Option, +) -> libc::c_int { + match e.kind() { + ErrorKind::PermissionDenied => EPERM, + ErrorKind::NotFound => { + if let Some(f) = not_found() { + f.await; + } + ENOENT + } + e => { + error!("{:?}", e); + EIO + } + } +} + +async fn errhandle_no_cleanup(e: std::io::Error) -> libc::c_int { + match e.kind() { + ErrorKind::PermissionDenied => EPERM, + ErrorKind::NotFound => ENOENT, + e => { + error!("{:?}", e); + EIO + } + } +} + +#[async_trait] +impl Filesystem for XmpFS { + async fn init( + &self, + _req: &Request<'_>, + config: &mut fuser::KernelConfig, + ) -> Result<(), c_int> { + config.set_max_write(16 * 1024 * 1024).unwrap(); + #[cfg(feature = "abi-7-13")] + config.set_max_background(512).unwrap(); + Ok(()) + } + async fn lookup(&self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { + if !self.inode_to_physical_path.contains_key(&parent) { + return reply.error(ENOENT).await; + } + + let entry_path = { + let tmp_v = self.inode_to_physical_path.get(&parent).unwrap(); + let parent_path = Path::new(&*tmp_v); + parent_path.join(name).to_owned() + }; + let entry_inode = self.get_inode(&entry_path).await; + + match std::fs::symlink_metadata(&entry_path) { + Err(e) => { + reply + .error( + errhandle(e, || { + // if not found: + if let Some(ino) = entry_inode { + Some(self.unregister_ino(ino)) + } else { + None + } + }) + .await, + ) + .await + } + Ok(m) => { + let ino = match entry_inode { + Some(x) => x, + None => self.add_or_create_inode(entry_path).await, + }; + + let attr: FileAttr = meta2attr(&m, ino); + + return reply.entry(&TTL, &attr, 1).await; + } + } + } + + async fn getattr(&self, _req: &Request<'_>, ino: u64, reply: ReplyAttr) { + if !self.inode_to_physical_path.contains_key(&ino) { + return reply.error(ENOENT).await; + } + + let metadata = { + let tmp_ref = self.inode_to_physical_path.get(&ino).unwrap(); + let entry_path = Path::new(&*tmp_ref); + + std::fs::symlink_metadata(entry_path) + }; + match metadata { + Err(e) => { + reply + .error( + errhandle(e, || { + // if not found: + + Some(self.unregister_ino(ino)) + }) + .await, + ) + .await; + } + Ok(m) => { + let attr: FileAttr = meta2attr(&m, ino); + reply.attr(&TTL, &attr).await; + } + } + } + + async fn open(&self, _req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) { + if !self.inode_to_physical_path.contains_key(&ino) { + return reply.error(ENOENT).await; + } + + let mut oo = OpenOptions::new(); + + let fl = flags as c_int; + match fl & O_ACCMODE { + O_RDONLY => { + oo.read(true); + oo.write(false); + } + O_WRONLY => { + oo.read(false); + oo.write(true); + } + O_RDWR => { + oo.read(true); + oo.write(true); + } + _ => return reply.error(EINVAL).await, + } + + oo.create(false); + if fl & (O_EXCL | O_CREAT) != 0 { + error!("Wrong flags on open"); + return reply.error(EIO).await; + } + + oo.append(fl & O_APPEND == O_APPEND); + oo.truncate(fl & O_TRUNC == O_TRUNC); + + let p = self.inode_to_physical_path.get(&ino).unwrap().clone(); + let entry_path = Path::new(&p); + + match oo.open(entry_path) { + Err(e) => { + reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await + } + Ok(f) => { + let ino = self + .counter + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + self.opened_files.insert(ino, f); + reply.opened(ino, 0).await; + } + } + } + + async fn create( + &self, + _req: &Request<'_>, + parent: u64, + name: &OsStr, + mode: u32, + _umask: u32, + + flags: i32, + reply: ReplyCreate, + ) { + let parent_path = match self.inode_to_physical_path.get(&parent) { + None => return reply.error(ENOENT).await, + Some(parent_path) => parent_path.to_owned(), + }; + + let parent_path = Path::new(&parent_path); + let entry_path = parent_path.join(name); + + let ino = self.add_or_create_inode(&entry_path).await; + + let mut oo = OpenOptions::new(); + + let fl = flags as c_int; + match fl & O_ACCMODE { + O_RDONLY => { + oo.read(true); + oo.write(false); + } + O_WRONLY => { + oo.read(false); + oo.write(true); + } + O_RDWR => { + oo.read(true); + oo.write(true); + } + _ => return reply.error(EINVAL).await, + } + + oo.create(fl & O_CREAT == O_CREAT); + oo.create_new(fl & O_EXCL == O_EXCL); + oo.append(fl & O_APPEND == O_APPEND); + oo.truncate(fl & O_TRUNC == O_TRUNC); + oo.mode(mode); + + match oo.open(&entry_path) { + Err(e) => { + return reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await + } + Ok(f) => { + let meta = match std::fs::symlink_metadata(entry_path) { + Err(e) => { + return reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await; + } + Ok(m) => meta2attr(&m, ino), + }; + let fh = self + .counter + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + self.opened_files.insert(fh, f); + reply.created(&TTL, &meta, 1, fh, 0).await + } + } + } + + async fn read( + &self, + _req: &Request<'_>, + _ino: u64, + fh: u64, + offset: i64, + size: u32, + _flags: i32, + _lock_owner: Option, + + reply: ReplyData, + ) { + let file_opt = self.opened_files.get(&fh); //;.map(|e| e.try_clone().unwrap()); + let f = match file_opt { + None => { + return reply.error(EIO).await; + } + Some(f) => unsafe { File::from_raw_fd(f.as_raw_fd()) }, + }; + + let size = size as usize; + + let b = tokio::task::spawn_blocking(move || read_blocking(f, size, offset)) + .await + .unwrap(); + + match b { + Ok(b) => { + reply.data(&b[..]).await; + } + Err(e) => match e.kind() { + std::io::ErrorKind::UnexpectedEof => { + reply.data(&[]).await; + } + _ => { + return reply.error(errhandle_no_cleanup(e).await).await; + } + }, + } + } + + async fn write( + &self, + _req: &Request<'_>, + _ino: u64, + fh: u64, + offset: i64, + data: &[u8], + _write_flags: u32, + _flags: i32, + _lock_owner: Option, + reply: ReplyWrite, + ) { + let file_opt = self.opened_files.get(&fh); + let f = match file_opt { + None => { + return reply.error(EIO).await; + } + Some(f) => unsafe { File::from_raw_fd(f.as_raw_fd()) }, + }; + + use std::os::unix::fs::FileExt; + match f.write_all_at(data, offset as u64) { + Err(e) => { + f.into_raw_fd(); + return reply.error(errhandle_no_cleanup(e).await).await; + } + Ok(()) => { + f.into_raw_fd(); + reply.written(data.len() as u32).await; + } + }; + } + + async fn fsync( + &self, + _req: &Request<'_>, + _ino: u64, + fh: u64, + datasync: bool, + reply: ReplyEmpty, + ) { + if !self.opened_files.contains_key(&fh) { + reply.error(EIO).await; + return; + } + + let f = self.opened_files.get_mut(&fh).unwrap(); + + match if datasync { + f.sync_data() + } else { + f.sync_all() + } { + Err(e) => { + reply.error(errhandle_no_cleanup(e).await).await; + return; + } + Ok(()) => { + reply.ok().await; + } + } + } + + async fn fsyncdir( + &self, + _req: &Request<'_>, + _ino: u64, + _fh: u64, + _datasync: bool, + reply: ReplyEmpty, + ) { + // I'm not sure how to do I with libstd + reply.ok().await; + } + + async fn release( + &self, + _req: &Request<'_>, + _ino: u64, + fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: ReplyEmpty, + ) { + match self.opened_files.remove(&fh) { + Some(_) => { + reply.ok().await; + } + None => { + reply.error(EIO).await; + } + } + } + + async fn opendir(&self, _req: &Request<'_>, ino: u64, _flags: i32, reply: ReplyOpen) { + if !self.inode_to_physical_path.contains_key(&ino) { + return reply.error(ENOENT).await; + } + + let entry_path = Path::new(&*self.inode_to_physical_path.get(&ino).unwrap()).to_owned(); + + match std::fs::read_dir(&entry_path) { + Err(e) => { + reply.error(errhandle_no_cleanup(e).await).await; + } + Ok(x) => { + let mut v: Vec = Vec::with_capacity(x.size_hint().0); + + let parent_ino: u64 = if ino == 1 { + 1 + } else { + match entry_path.parent() { + None => ino, + Some(x) => self + .mounted_path_to_inode + .get(x.as_os_str()) + .map(|e| *e.value()) + .unwrap_or(ino), + } + }; + + v.push(DirInfo { + ino, + kind: FileType::Directory, + name: OsStr::from_bytes(b".").to_os_string(), + }); + v.push(DirInfo { + ino: parent_ino, + kind: FileType::Directory, + name: OsStr::from_bytes(b"..").to_os_string(), + }); + + for dee in x { + match dee { + Err(e) => { + reply.error(errhandle_no_cleanup(e).await).await; + return; + } + Ok(de) => { + let name = de.file_name().to_os_string(); + let kind = de.file_type().map(ft2ft).unwrap_or(FileType::RegularFile); + let jp = entry_path.join(&name); + let ino = self.add_or_create_inode(jp).await; + + v.push(DirInfo { ino, kind, name }); + } + } + } + let fh = self + .counter + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + self.opened_directories.lock().await.insert(fh, v); + reply.opened(fh, 0).await; + } + } + } + + async fn readdir( + &self, + _req: &Request<'_>, + _ino: u64, + fh: u64, + offset: i64, + mut reply: ReplyDirectory, + ) { + let handle = self.opened_directories.lock().await; + if !handle.contains_key(&fh) { + error!("no fh {} for readdir", fh); + return reply.error(EIO).await; + } + + let entries = &handle[&fh]; + + for (i, entry) in entries.iter().enumerate().skip(offset as usize) { + // i + 1 means the index of the next entry + if reply.add(entry.ino, (i + 1) as i64, entry.kind, &entry.name) { + break; + } + } + reply.ok().await + } + + async fn releasedir( + &self, + _req: &Request<'_>, + _ino: u64, + fh: u64, + _flags: i32, + reply: ReplyEmpty, + ) { + let mut handle = self.opened_directories.lock().await; + if !handle.contains_key(&fh) { + reply.error(EIO).await; + return; + } + + handle.remove(&fh); + reply.ok().await; + } + + async fn readlink(&self, _req: &Request<'_>, ino: u64, reply: ReplyData) { + if let Some(p) = self.inode_to_physical_path.get(&ino) { + let entry_path = Path::new(p.value()); + match std::fs::read_link(entry_path) { + Err(e) => { + reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await + } + Ok(x) => { + reply.data(x.as_os_str().as_bytes()).await; + } + } + } else { + return reply.error(ENOENT).await; + }; + } + + async fn mkdir( + &self, + _req: &Request<'_>, + parent: u64, + name: &OsStr, + _mode: u32, + _umask: u32, + reply: ReplyEntry, + ) { + let entry_path = if let Some(p) = self.entry_path_from_parentino_and_name(parent, name) { + p + } else { + return reply.error(ENOENT).await; + }; + + let ino = self.add_or_create_inode(&entry_path).await; + match std::fs::create_dir(&entry_path) { + Err(e) => reply.error(errhandle_no_cleanup(e).await).await, + Ok(()) => { + let attr = match std::fs::symlink_metadata(entry_path) { + Err(e) => { + return reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await; + } + Ok(m) => meta2attr(&m, ino), + }; + + reply.entry(&TTL, &attr, 1).await; + } + } + } + + async fn unlink(&self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { + match self.inode_to_physical_path.get(&parent) { + None => reply.error(ENOENT).await, + Some(p) => { + let parent_path = Path::new(&*p); + let entry_path = parent_path.join(name); + + match std::fs::remove_file(entry_path) { + Err(e) => reply.error(errhandle_no_cleanup(e).await).await, + Ok(()) => reply.ok().await, + } + } + } + } + + async fn rmdir(&self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) { + if !self.inode_to_physical_path.contains_key(&parent) { + return reply.error(ENOENT).await; + } + + let parent_path = + Path::new(&self.inode_to_physical_path.get(&parent).unwrap().value()).to_owned(); + let entry_path = parent_path.join(name); + + match std::fs::remove_dir(entry_path) { + Err(e) => reply.error(errhandle_no_cleanup(e).await).await, + Ok(()) => { + reply.ok().await; + } + } + } + + async fn symlink( + &self, + _req: &Request<'_>, + parent: u64, + name: &OsStr, + link: &Path, + reply: ReplyEntry, + ) { + if !self.inode_to_physical_path.contains_key(&parent) { + return reply.error(ENOENT).await; + } + + let parent_path = + Path::new(&self.inode_to_physical_path.get(&parent).unwrap().value()).to_owned(); + let entry_path = parent_path.join(name); + let ino = self.add_or_create_inode(&entry_path).await; + + match std::os::unix::fs::symlink(&entry_path, link) { + Err(e) => { + reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await + } + Ok(()) => { + let attr = match std::fs::symlink_metadata(entry_path) { + Err(e) => { + return reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await; + } + Ok(m) => meta2attr(&m, ino), + }; + + reply.entry(&TTL, &attr, 1).await; + } + } + } + + async fn rename( + &self, + _req: &Request<'_>, + parent: u64, + name: &OsStr, + newparent: u64, + newname: &OsStr, + _flags: u32, + + reply: ReplyEmpty, + ) { + if !self.inode_to_physical_path.contains_key(&parent) { + return reply.error(ENOENT).await; + } + if !self.inode_to_physical_path.contains_key(&newparent) { + return reply.error(ENOENT).await; + } + + let parent_path = + Path::new(&self.inode_to_physical_path.get(&parent).unwrap().value()).to_owned(); + let newparent_path = + Path::new(&self.inode_to_physical_path.get(&newparent).unwrap().value()).to_owned(); + + let entry_path = parent_path.join(name); + let newentry_path = newparent_path.join(newname); + + if entry_path == newentry_path { + return reply.ok().await; + } + + let ino = self.add_or_create_inode(&entry_path).await; + + match std::fs::rename(&entry_path, &newentry_path) { + Err(e) => { + reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await + } + Ok(()) => { + self.inode_to_physical_path + .insert(ino, newentry_path.as_os_str().to_os_string()); + self.mounted_path_to_inode.remove(entry_path.as_os_str()); + self.mounted_path_to_inode + .insert(newentry_path.as_os_str().to_os_string(), ino); + reply.ok().await; + } + } + } + + async fn link( + &self, + _req: &Request<'_>, + ino: u64, + newparent: u64, + newname: &OsStr, + reply: ReplyEntry, + ) { + // Not a true hardlink: new inode + if !self.inode_to_physical_path.contains_key(&ino) { + return reply.error(ENOENT).await; + } + if !self.inode_to_physical_path.contains_key(&newparent) { + return reply.error(ENOENT).await; + } + + let entry_path = + Path::new(&self.inode_to_physical_path.get(&ino).unwrap().value()).to_owned(); + let newparent_path = + Path::new(&self.inode_to_physical_path.get(&newparent).unwrap().value()).to_owned(); + let newentry_path = newparent_path.join(newname); + + let newino = self.add_or_create_inode(&newentry_path).await; + + match std::fs::hard_link(&entry_path, &newentry_path) { + Err(e) => { + reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await + } + Ok(()) => { + let attr = match std::fs::symlink_metadata(newentry_path) { + Err(e) => { + return reply + .error(errhandle(e, || Some(self.unregister_ino(newino))).await) + .await; + } + Ok(m) => meta2attr(&m, newino), + }; + + reply.entry(&TTL, &attr, 1).await; + } + } + } + + async fn mknod( + &self, + _req: &Request<'_>, + _parent: u64, + _name: &OsStr, + _mode: u32, + _umask: u32, + _rdev: u32, + reply: ReplyEntry, + ) { + // no mknod lib libstd + reply.error(ENOSYS).await; + } + + async fn setattr( + &self, + _req: &Request<'_>, + ino: u64, + mode: Option, + _uid: Option, + _gid: Option, + size: Option, + _atime: Option, + _mtime: Option, + _ctime: Option, + fh: Option, + _crtime: Option, + _chgtime: Option, + _bkuptime: Option, + _flags: Option, + reply: ReplyAttr, + ) { + // Limited to setting file length only + + let (fh, sz) = match (fh, size) { + (Some(x), Some(y)) => (x, y), + _ => { + // only partial for chmod +x, and not the good one + + let entry_path = + Path::new(&self.inode_to_physical_path.get(&ino).unwrap().value()).to_owned(); + + if let Some(mode) = mode { + use std::fs::Permissions; + + let perm = Permissions::from_mode(mode); + match std::fs::set_permissions(&entry_path, perm) { + Err(e) => { + return reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await + } + Ok(()) => { + let attr = match std::fs::symlink_metadata(entry_path) { + Err(e) => { + return reply + .error( + errhandle(e, || Some(self.unregister_ino(ino))).await, + ) + .await; + } + Ok(m) => meta2attr(&m, ino), + }; + + return reply.attr(&TTL, &attr).await; + } + } + } else { + // Just try to do nothing, successfully. + let attr = match std::fs::symlink_metadata(entry_path) { + Err(e) => { + return reply + .error(errhandle(e, || Some(self.unregister_ino(ino))).await) + .await; + } + Ok(m) => meta2attr(&m, ino), + }; + + return reply.attr(&TTL, &attr).await; + } + } + }; + + if !self.opened_files.contains_key(&fh) { + return reply.error(EIO).await; + } + + let f = self.opened_files.get_mut(&fh).unwrap(); + + match f.set_len(sz) { + Err(e) => reply.error(errhandle_no_cleanup(e).await).await, + Ok(()) => { + // pull regular file metadata out of thin air + + let attr = FileAttr { + ino, + size: sz, + blocks: 1, + atime: UNIX_EPOCH, + mtime: UNIX_EPOCH, + ctime: UNIX_EPOCH, + crtime: UNIX_EPOCH, + kind: FileType::RegularFile, + perm: 0o644, + nlink: 1, + uid: 0, + gid: 0, + rdev: 0, + flags: 0, + blksize: sz as u32, + padding: 0, + }; + + reply.attr(&TTL, &attr).await; + } + } + } +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + env_logger::init(); + let mount_src = env::args_os().nth(1).unwrap(); + let mount_dest = env::args_os().nth(2).unwrap(); + println!("About to mount {:?} onto {:?}", mount_src, mount_dest); + + let options = [ + fuser::MountOption::RW, + fuser::MountOption::Async, + fuser::MountOption::FSName(String::from("xmp")), + fuser::MountOption::AutoUnmount, + ]; + let mut xmp = XmpFS::new(&mount_src); + xmp.populate_root_dir().await; + fuser::mount2(xmp, 16, mount_dest, &options).await.unwrap(); +} diff --git a/src/channel.rs b/src/channel.rs index 05769745..a075d771 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -13,19 +13,38 @@ use crate::fuse_sys::{ fuse_session_destroy, fuse_session_fd, fuse_session_mount, fuse_session_new, fuse_session_unmount, }; -use libc::{self, c_int, c_void, size_t}; + +use libc::{self, c_int, c_void}; use log::error; +use log::warn; #[cfg(any(feature = "libfuse", test))] use std::ffi::OsStr; -use std::ffi::{CStr, CString}; +use std::os::unix::io::IntoRawFd; + +use crate::io_ops::{ArcSubChannel, FileDescriptorRawHandle, SubChannel}; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; +use std::{ + ffi::{CStr, CString}, + sync::Arc, +}; use std::{io, ptr}; -use crate::reply::ReplySender; #[cfg(not(feature = "libfuse"))] use crate::MountOption; +/// Flag to tell OS for fuse to clone the underlying handle so we can have more than one reference to a session. +#[cfg(target_os = "macos")] +pub const FUSE_DEV_IOC_CLONE: u64 = 0x_80_04_e5_00; // = _IOR(229, 0, uint32_t) + +/// Flag to tell OS for fuse to clone the underlying handle so we can have more than one reference to a session. +#[cfg(target_os = "linux")] +pub const FUSE_DEV_IOC_CLONE: u64 = 0x_80_04_e5_00; // = _IOR(229, 0, uint32_t) + +/// Flag to tell OS for fuse to clone the underlying handle so we can have more than one reference to a session. +#[cfg(target_os = "freebsd")] +pub const FUSE_DEV_IOC_CLONE: u64 = 0x_40_04_e5_00; // = _IOR(229, 0, uint32_t) + /// Helper function to provide options as a fuse_args struct /// (which contains an argc count and an argv pointer) #[cfg(any(feature = "libfuse", test))] @@ -44,35 +63,148 @@ fn with_fuse_args T>(options: &[&OsStr], f: F) -> T #[derive(Debug)] pub struct Channel { mountpoint: PathBuf, - pub(in crate) fd: c_int, + pub(in crate) session_fd: ArcSubChannel, + pub(in crate) sub_channels: Vec, pub(in crate) fuse_session: *mut c_void, } +/// This is required since the fuse_sesion is an opaque ptr to the session +/// so rust is unable to infer that it is safe for send. +unsafe impl Send for Channel {} + impl Channel { + /// This allows file systems to work concurrently over several buffers/descriptors for concurrent operation. + /// More detailed description of the protocol is at: + /// https://john-millikin.com/the-fuse-protocol#multi-threading + /// + #[cfg(not(target_os = "macos"))] + fn create_worker(root_fd: &ArcSubChannel) -> io::Result { + let fuse_device_name = "/dev/fuse"; + + let fd = match std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(fuse_device_name) + { + Ok(file) => file.into_raw_fd(), + Err(error) => { + if error.kind() == io::ErrorKind::NotFound { + error!("{} not found. Try 'modprobe fuse'", fuse_device_name); + } + return Err(error); + } + }; + + let code = unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) }; + if code == -1 { + error!("fcntl command failed with {}", code); + return Err(io::Error::last_os_error()); + } + + let code = unsafe { libc::ioctl(fd, FUSE_DEV_IOC_CLONE, root_fd.as_raw_fd()) }; + if code == -1 { + error!("Clone command failed with {}", code); + return Err(io::Error::last_os_error()); + } + + Ok(ArcSubChannel(Arc::new(SubChannel::new( + FileDescriptorRawHandle::new(fd), + )?))) + } + + // mac fuse seems to just re-use the root fd relying onthe atomic semantics setup in the driver + // This will have lowerthroughput than the linux approach. + #[cfg(target_os = "macos")] + fn create_worker(root_fd: &ArcSubChannel) -> io::Result { + Ok(root_fd.clone()) + } + /// + /// Create worker fd's takes the root/session file descriptor and makes several clones + /// This allows file systems to work concurrently over several buffers/descriptors for concurrent operation. + /// More detailed description of the protocol is at: + /// https://john-millikin.com/the-fuse-protocol#multi-threading + /// + fn create_sub_channels( + mountpoint: PathBuf, + worker_channel_count: usize, + root_fd: FileDescriptorRawHandle, + fuse_session: *mut c_void, + ) -> io::Result { + let mut worker_channels = Vec::default(); + + let root_sub_channel = ArcSubChannel(Arc::new(SubChannel::new(root_fd)?)); + worker_channels.push(root_sub_channel.clone()); + + for _ in 0..worker_channel_count { + worker_channels.push(Channel::create_worker(&root_sub_channel)?); + } + + Ok(Channel { + mountpoint, + sub_channels: worker_channels, + session_fd: root_sub_channel, + fuse_session, + }) + } + + /// This is separated out here since the one method we call has multiple error exit points + /// given any exit on error from the inner method we will do an unmount/cleanup step here. + fn new_from_session_and_fd( + mountpoint: &Path, + worker_channel_count: usize, + fd: FileDescriptorRawHandle, + fuse_session: *mut c_void, + ) -> io::Result { + // make a copy here for error handling if we lost it in attempting to construct the channel. + let tmp_root_fd = fd.fd; + match Channel::create_sub_channels( + mountpoint.to_owned(), + worker_channel_count, + fd, + fuse_session, + ) { + Ok(r) => Ok(r), + Err(err) => { + if let Err(e) = unmount(mountpoint, fuse_session, tmp_root_fd) { + warn!("When shutting down on error, attempted to unmount failed with error {:?}. Given failure to mount this maybe be fine.", e); + }; + Err(err) + } + } + } /// Create a new communication channel to the kernel driver by mounting the /// given path. The kernel driver will delegate filesystem operations of /// the given path to the channel. If the channel is dropped, the path is /// unmounted. #[cfg(feature = "libfuse2")] - pub fn new(mountpoint: &Path, options: &[&OsStr]) -> io::Result { + pub fn new( + mountpoint: &Path, + worker_channel_count: usize, + options: &[&OsStr], + ) -> io::Result { let mountpoint = mountpoint.canonicalize()?; + with_fuse_args(options, |args| { let mnt = CString::new(mountpoint.as_os_str().as_bytes())?; let fd = unsafe { fuse_mount_compat25(mnt.as_ptr(), args) }; if fd < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(Channel { - mountpoint, - fd, - fuse_session: ptr::null_mut(), - }) + return Err(io::Error::last_os_error()); } + Channel::new_from_session_and_fd( + &mountpoint, + worker_channel_count, + FileDescriptorRawHandle::new(fd), + ptr::null_mut(), + ) }) } #[cfg(feature = "libfuse3")] - pub fn new(mountpoint: &Path, options: &[&OsStr]) -> io::Result { + pub fn new( + mountpoint: &Path, + worker_channel_count: usize, + options: &[&OsStr], + ) -> io::Result { let mountpoint = mountpoint.canonicalize()?; with_fuse_args(options, |args| { let mnt = CString::new(mountpoint.as_os_str().as_bytes())?; @@ -86,30 +218,32 @@ impl Channel { } let fd = unsafe { fuse_session_fd(fuse_session) }; if fd < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(Channel { - mountpoint, - fd, - fuse_session, - }) + return Err(io::Error::last_os_error()); } + Channel::new_from_session_and_fd( + &mountpoint, + worker_channel_count, + FileDescriptorRawHandle::new(fd), + fuse_session, + ) }) } #[cfg(not(feature = "libfuse"))] - pub fn new2(mountpoint: &Path, options: &[MountOption]) -> io::Result { + pub fn new2( + mountpoint: &Path, + worker_channel_count: usize, + options: &[MountOption], + ) -> io::Result { let mountpoint = mountpoint.canonicalize()?; + let fd = fuse_mount_pure(mountpoint.as_os_str(), options)?; - if fd < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(Channel { - mountpoint, - fd, - fuse_session: ptr::null_mut(), - }) - } + Channel::new_from_session_and_fd( + &mountpoint, + worker_channel_count, + FileDescriptorRawHandle::new(fd), + ptr::null_mut(), + ) } /// Return path of the mounted filesystem @@ -117,79 +251,35 @@ impl Channel { &self.mountpoint } - /// Receives data up to the capacity of the given buffer (can block). - pub fn receive(&self, buffer: &mut [u8]) -> io::Result { - let rc = unsafe { - libc::read( - self.fd, - buffer.as_ptr() as *mut c_void, - buffer.len() as size_t, - ) - }; - if rc < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(rc as usize) + pub(in crate) async fn receive<'a, 'b>( + sub_channel: &'a ArcSubChannel, + terminated: &mut tokio::sync::oneshot::Receiver<()>, + buffer: &'b mut [u8], + ) -> io::Result> { + tokio::select! { + _ = terminated => { + Ok(None) + } + result = sub_channel.do_receive(buffer) => { + result + } } } - - /// Returns a sender object for this channel. The sender object can be - /// used to send to the channel. Multiple sender objects can be used - /// and they can safely be sent to other threads. - pub fn sender(&self) -> ChannelSender { - // Since write/writev syscalls are threadsafe, we can simply create - // a sender by using the same fd and use it in other threads. Only - // the channel closes the fd when dropped. If any sender is used after - // dropping the channel, it'll return an EBADF error. - ChannelSender { fd: self.fd } - } } -unsafe impl Send for Channel {} - impl Drop for Channel { fn drop(&mut self) { // TODO: send ioctl FUSEDEVIOCSETDAEMONDEAD on macOS before closing the fd // Close the communication channel to the kernel driver // (closing it before unnmount prevents sync unmount deadlock) - unsafe { - libc::close(self.fd); - } - // Unmount this channel's mount point - let _ = unmount(&self.mountpoint, self.fuse_session, self.fd); - self.fuse_session = ptr::null_mut(); // unmount frees this pointer - } -} - -#[derive(Clone, Copy, Debug)] -pub struct ChannelSender { - fd: c_int, -} -impl ChannelSender { - /// Send all data in the slice of slice of bytes in a single write (can block). - pub fn send(&self, buffer: &[&[u8]]) -> io::Result<()> { - let iovecs: Vec<_> = buffer - .iter() - .map(|d| libc::iovec { - iov_base: d.as_ptr() as *mut c_void, - iov_len: d.len() as size_t, - }) - .collect(); - let rc = unsafe { libc::writev(self.fd, iovecs.as_ptr(), iovecs.len() as c_int) }; - if rc < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) + // Close all the channel/file handles. This will include the session fd. + for sub_channel in self.sub_channels.iter() { + sub_channel.0.close() } - } -} -impl ReplySender for ChannelSender { - fn send(&self, data: &[&[u8]]) { - if let Err(err) = ChannelSender::send(self, data) { - error!("Failed to send FUSE reply: {}", err); - } + // Unmount this channel's mount point + self.fuse_session = ptr::null_mut(); // unmount frees this pointer } } @@ -256,6 +346,7 @@ pub fn unmount(mountpoint: &Path, fuse_session: *mut c_void, fd: c_int) -> io::R let mnt = CString::new(mountpoint.as_os_str().as_bytes())?; let rc = libc_umount(&mnt, fuse_session, fd); + if rc < 0 { Err(io::Error::last_os_error()) } else { diff --git a/src/io_ops/blocking_io.rs b/src/io_ops/blocking_io.rs new file mode 100644 index 00000000..f8da7a62 --- /dev/null +++ b/src/io_ops/blocking_io.rs @@ -0,0 +1,45 @@ +use super::FileDescriptorRawHandle; +use async_trait::async_trait; +use libc::{self, c_int, c_void, size_t}; +use log::error; +use std::{io, sync::Arc}; + +#[derive(Debug, Clone)] +pub struct SubChannel { + fd: Arc, +} + +impl SubChannel { + pub fn as_raw_fd(&self) -> &FileDescriptorRawHandle { + &self.fd + } + + pub fn new(fd: FileDescriptorRawHandle) -> io::Result { + Ok(SubChannel { fd: Arc::new(fd) }) + } + + /// Send all data in the slice of slice of bytes in a single write (can block). + pub async fn send(&self, buffer: &[&[u8]]) -> io::Result<()> { + let iovecs: Vec<_> = buffer + .iter() + .map(|d| libc::iovec { + iov_base: d.as_ptr() as *mut c_void, + iov_len: d.len() as size_t, + }) + .collect(); + let rc = unsafe { libc::writev(self.fd.fd, iovecs.as_ptr(), iovecs.len() as c_int) }; + if rc < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + + pub fn close(&self) { + self.fd.close() + } + + pub async fn do_receive(&self, buffer: &'_ mut Vec) -> io::Result> { + tokio::task::block_in_place(|| super::blocking_receive(&self.fd, buffer)) + } +} diff --git a/src/io_ops/mod.rs b/src/io_ops/mod.rs new file mode 100644 index 00000000..612eaa27 --- /dev/null +++ b/src/io_ops/mod.rs @@ -0,0 +1,103 @@ +use libc::{self, c_void, size_t}; +use log::error; + +use std::os::unix::prelude::AsRawFd; +use std::{ + ops::Deref, + os::unix::io::RawFd, + sync::{atomic::AtomicBool, Arc}, +}; + +use std::io; + +#[derive(Debug, Clone)] +pub struct ArcSubChannel(pub(crate) Arc); + +impl ArcSubChannel { + pub fn as_raw_fd(&self) -> &FileDescriptorRawHandle { + self.0.as_ref().as_raw_fd() + } +} + +impl Deref for ArcSubChannel { + type Target = SubChannel; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} +#[async_trait::async_trait] +impl crate::reply::ReplySender for ArcSubChannel { + async fn send(&self, data: &[&[u8]]) { + if let Err(err) = SubChannel::send(self.0.as_ref(), data).await { + error!("Failed to send FUSE reply: {}", err); + } + } +} + +/// In the latest version of rust this isn't required since RawFd implements AsRawFD +/// but until pretty recently that didn't work. So including this wrapper is cheap and allows +/// us better compatibility. +#[derive(Debug)] +pub struct FileDescriptorRawHandle { + pub(in crate) fd: RawFd, + is_closed: AtomicBool, +} + +impl FileDescriptorRawHandle { + pub fn new(fd: RawFd) -> Self { + Self { + fd, + is_closed: AtomicBool::default(), + } + } + pub fn close(&self) { + let already_closed = self + .is_closed + .swap(true, std::sync::atomic::Ordering::SeqCst); + if !already_closed { + unsafe { + libc::close(self.fd); + } + } + } +} +impl Drop for FileDescriptorRawHandle { + fn drop(&mut self) { + self.close() + } +} + +impl AsRawFd for FileDescriptorRawHandle { + fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +/// Receives data up to the capacity of the given buffer (can block). +fn blocking_receive(fd: &FileDescriptorRawHandle, buffer: &mut [u8]) -> io::Result> { + let rc = unsafe { + libc::read( + fd.fd, + buffer.as_ptr() as *mut c_void, + buffer.len() as size_t, + ) + }; + if rc < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(Some(rc as usize)) + } +} + +#[cfg(target_os = "macos")] +pub mod blocking_io; + +#[cfg(target_os = "macos")] +pub(crate) use blocking_io::SubChannel; + +#[cfg(not(target_os = "macos"))] +pub mod nonblocking_io; + +#[cfg(not(target_os = "macos"))] +pub(crate) use nonblocking_io::SubChannel; diff --git a/src/io_ops/nonblocking_io.rs b/src/io_ops/nonblocking_io.rs new file mode 100644 index 00000000..3cbe58c9 --- /dev/null +++ b/src/io_ops/nonblocking_io.rs @@ -0,0 +1,91 @@ +use super::FileDescriptorRawHandle; +use libc::O_NONBLOCK; +use libc::{self, c_int, c_void, size_t}; +use log::error; +use std::io; +use std::sync::Arc; +use tokio::io::unix::AsyncFd; + +#[derive(Debug, Clone)] +pub struct SubChannel { + fd: Arc>, +} +impl SubChannel { + pub fn as_raw_fd(&self) -> &FileDescriptorRawHandle { + self.fd.as_ref().get_ref() + } + + pub fn new(fd: FileDescriptorRawHandle) -> io::Result { + let code = unsafe { libc::fcntl(fd.fd, libc::F_SETFL, O_NONBLOCK) }; + if code == -1 { + error!( + "fcntl set file handle to O_NONBLOCK command failed with {}", + code + ); + return Err(io::Error::last_os_error()); + } + + Ok(SubChannel { + fd: Arc::new(AsyncFd::new(fd)?), + }) + } + /// Send all data in the slice of slice of bytes in a single write (can block). + pub async fn send(&self, buffer: &[&[u8]]) -> io::Result<()> { + loop { + let mut guard = self.fd.writable().await?; + + match guard.try_io(|inner| { + let iovecs: Vec<_> = buffer + .iter() + .map(|d| libc::iovec { + iov_base: d.as_ptr() as *mut c_void, + iov_len: d.len() as size_t, + }) + .collect(); + let rc = unsafe { + libc::writev(inner.get_ref().fd, iovecs.as_ptr(), iovecs.len() as c_int) + }; + if rc < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + }) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } + + pub fn close(&self) { + self.fd.get_ref().close() + } + + pub async fn do_receive(&self, buffer: &'_ mut [u8]) -> io::Result> { + use std::time::Duration; + use tokio::time::timeout; + loop { + if let Ok(guard_result) = timeout(Duration::from_millis(1000), self.fd.readable()).await + { + let mut guard = guard_result?; + match guard.try_io(|inner| super::blocking_receive(inner.get_ref(), buffer)) { + Ok(result) => return result, + Err(_would_block) => { + return Ok(None); + } + } + } else { + // some termination states when it comes to fuse in the kernel(umount sometimes..), do not trigger readable. + // so after a timeout/every so often we need to just try do the read manually. + match super::blocking_receive(self.fd.get_ref(), buffer) { + Ok(r) => return Ok(r), + Err(e) => { + if e.kind() != io::ErrorKind::WouldBlock { + return Err(e); + } + } + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a731c94d..7b8412c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ use crate::mount_options::check_option_conflicts; #[cfg(feature = "libfuse")] use crate::mount_options::option_to_string; use crate::session::MAX_WRITE_SIZE; +use async_trait::async_trait; pub use mount_options::MountOption; #[cfg(target_os = "macos")] pub use reply::ReplyXTimes; @@ -35,6 +36,7 @@ pub use reply::{ }; pub use request::Request; pub use session::{BackgroundSession, Session}; + #[cfg(feature = "abi-7-28")] use std::cmp::max; #[cfg(feature = "abi-7-13")] @@ -43,6 +45,7 @@ use std::cmp::min; mod channel; mod fuse_abi; mod fuse_sys; +mod io_ops; mod ll; mod mount_options; mod reply; @@ -297,21 +300,22 @@ pub enum TimeOrNow { /// implementations are provided here to get a mountable filesystem that does /// nothing. #[allow(clippy::too_many_arguments)] -pub trait Filesystem { +#[async_trait] +pub trait Filesystem: Send + Sync + 'static { /// Initialize filesystem. /// Called before any other filesystem method. /// The kernel module connection can be configured using the KernelConfig object - fn init(&mut self, _req: &Request<'_>, _config: &mut KernelConfig) -> Result<(), c_int> { + async fn init(&self, _req: &Request<'_>, _config: &mut KernelConfig) -> Result<(), c_int> { Ok(()) } /// Clean up filesystem. /// Called on filesystem exit. - fn destroy(&mut self, _req: &Request<'_>) {} + async fn destroy(&mut self) {} /// Look up a directory entry by name and get its attributes. - fn lookup(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEntry) { - reply.error(ENOSYS); + async fn lookup(&self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEntry) { + reply.error(ENOSYS).await } /// Forget about an inode. @@ -321,25 +325,25 @@ pub trait Filesystem { /// each forget. The filesystem may ignore forget calls, if the inodes don't need to /// have a limited lifetime. On unmount it is not guaranteed, that all referenced /// inodes will receive a forget message. - fn forget(&mut self, _req: &Request<'_>, _ino: u64, _nlookup: u64) {} + async fn forget(&self, _req: &Request<'_>, _ino: u64, _nlookup: u64) {} /// Like forget, but take multiple forget requests at once for performance. The default /// implementation will fallback to forget. #[cfg(feature = "abi-7-16")] - fn batch_forget(&mut self, req: &Request<'_>, nodes: &[fuse_abi::fuse_forget_one]) { + async fn batch_forget(&self, req: &Request<'_>, nodes: &[fuse_abi::fuse_forget_one]) { for node in nodes { - self.forget(req, node.nodeid, node.nlookup); + self.forget(req, node.nodeid, node.nlookup).await; } } /// Get file attributes. - fn getattr(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyAttr) { - reply.error(ENOSYS); + async fn getattr(&self, _req: &Request<'_>, _ino: u64, reply: ReplyAttr) { + reply.error(ENOSYS).await; } /// Set file attributes. - fn setattr( - &mut self, + async fn setattr( + &self, _req: &Request<'_>, _ino: u64, _mode: Option, @@ -356,18 +360,18 @@ pub trait Filesystem { _flags: Option, reply: ReplyAttr, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Read symbolic link. - fn readlink(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyData) { - reply.error(ENOSYS); + async fn readlink(&self, _req: &Request<'_>, _ino: u64, reply: ReplyData) { + reply.error(ENOSYS).await } /// Create file node. /// Create a regular file, character device, block device, fifo or socket node. - fn mknod( - &mut self, + async fn mknod( + &self, _req: &Request<'_>, _parent: u64, _name: &OsStr, @@ -376,12 +380,12 @@ pub trait Filesystem { _rdev: u32, reply: ReplyEntry, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await } /// Create a directory. - fn mkdir( - &mut self, + async fn mkdir( + &self, _req: &Request<'_>, _parent: u64, _name: &OsStr, @@ -389,34 +393,34 @@ pub trait Filesystem { _umask: u32, reply: ReplyEntry, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Remove a file. - fn unlink(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEmpty) { - reply.error(ENOSYS); + async fn unlink(&self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEmpty) { + reply.error(ENOSYS).await; } /// Remove a directory. - fn rmdir(&mut self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEmpty) { - reply.error(ENOSYS); + async fn rmdir(&self, _req: &Request<'_>, _parent: u64, _name: &OsStr, reply: ReplyEmpty) { + reply.error(ENOSYS).await; } /// Create a symbolic link. - fn symlink( - &mut self, + async fn symlink( + &self, _req: &Request<'_>, _parent: u64, _name: &OsStr, _link: &Path, reply: ReplyEntry, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Rename a file. - fn rename( - &mut self, + async fn rename( + &self, _req: &Request<'_>, _parent: u64, _name: &OsStr, @@ -425,19 +429,19 @@ pub trait Filesystem { _flags: u32, reply: ReplyEmpty, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Create a hard link. - fn link( - &mut self, + async fn link( + &self, _req: &Request<'_>, _ino: u64, _newparent: u64, _newname: &OsStr, reply: ReplyEntry, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Open a file. @@ -448,8 +452,8 @@ pub trait Filesystem { /// anything in fh. There are also some flags (direct_io, keep_cache) which the /// filesystem may set, to change the way the file is opened. See fuse_file_info /// structure in for more details. - fn open(&mut self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: ReplyOpen) { - reply.opened(0, 0); + async fn open(&self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: ReplyOpen) { + reply.opened(0, 0).await } /// Read data. @@ -462,8 +466,8 @@ pub trait Filesystem { /// /// flags: these are the file flags, such as O_SYNC. Only supported with ABI >= 7.9 /// lock_owner: only supported with ABI >= 7.9 - fn read( - &mut self, + async fn read( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, @@ -473,7 +477,7 @@ pub trait Filesystem { _lock_owner: Option, reply: ReplyData, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await } /// Write data. @@ -488,8 +492,8 @@ pub trait Filesystem { /// is disabled /// flags: these are the file flags, such as O_SYNC. Only supported with ABI >= 7.9 /// lock_owner: only supported with ABI >= 7.9 - fn write( - &mut self, + async fn write( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, @@ -500,7 +504,7 @@ pub trait Filesystem { _lock_owner: Option, reply: ReplyWrite, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Flush method. @@ -513,15 +517,15 @@ pub trait Filesystem { /// is not forced to flush pending writes. One reason to flush data, is if the /// filesystem wants to return write errors. If the filesystem supports file locking /// operations (setlk, getlk) it should remove all locks belonging to 'lock_owner'. - fn flush( - &mut self, + async fn flush( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, _lock_owner: u64, reply: ReplyEmpty, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Release an open file. @@ -532,8 +536,8 @@ pub trait Filesystem { /// the release. fh will contain the value set by the open method, or will be undefined /// if the open method didn't set any value. flags will contain the same flags as for /// open. - fn release( - &mut self, + async fn release( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, @@ -542,21 +546,21 @@ pub trait Filesystem { _flush: bool, reply: ReplyEmpty, ) { - reply.ok(); + reply.ok().await; } /// Synchronize file contents. /// If the datasync parameter is non-zero, then only the user data should be flushed, /// not the meta data. - fn fsync( - &mut self, + async fn fsync( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, _datasync: bool, reply: ReplyEmpty, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Open a directory. @@ -566,8 +570,8 @@ pub trait Filesystem { /// anything in fh, though that makes it impossible to implement standard conforming /// directory stream operations in case the contents of the directory can change /// between opendir and releasedir. - fn opendir(&mut self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: ReplyOpen) { - reply.opened(0, 0); + async fn opendir(&self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: ReplyOpen) { + reply.opened(0, 0).await; } /// Read directory. @@ -575,15 +579,15 @@ pub trait Filesystem { /// requested size. Send an empty buffer on end of stream. fh will contain the /// value set by the opendir method, or will be undefined if the opendir method /// didn't set any value. - fn readdir( - &mut self, + async fn readdir( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, _offset: i64, reply: ReplyDirectory, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Read directory. @@ -591,55 +595,55 @@ pub trait Filesystem { /// requested size. Send an empty buffer on end of stream. fh will contain the /// value set by the opendir method, or will be undefined if the opendir method /// didn't set any value. - fn readdirplus( - &mut self, + async fn readdirplus( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, _offset: i64, reply: ReplyDirectoryPlus, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Release an open directory. /// For every opendir call there will be exactly one releasedir call. fh will /// contain the value set by the opendir method, or will be undefined if the /// opendir method didn't set any value. - fn releasedir( - &mut self, + async fn releasedir( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, _flags: i32, reply: ReplyEmpty, ) { - reply.ok(); + reply.ok().await; } /// Synchronize directory contents. /// If the datasync parameter is set, then only the directory contents should /// be flushed, not the meta data. fh will contain the value set by the opendir /// method, or will be undefined if the opendir method didn't set any value. - fn fsyncdir( - &mut self, + async fn fsyncdir( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, _datasync: bool, reply: ReplyEmpty, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Get file system statistics. - fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyStatfs) { - reply.statfs(0, 0, 0, 0, 0, 512, 255, 0); + async fn statfs(&self, _req: &Request<'_>, _ino: u64, reply: ReplyStatfs) { + reply.statfs(0, 0, 0, 0, 0, 512, 255, 0).await; } /// Set an extended attribute. - fn setxattr( - &mut self, + async fn setxattr( + &self, _req: &Request<'_>, _ino: u64, _name: &OsStr, @@ -648,43 +652,43 @@ pub trait Filesystem { _position: u32, reply: ReplyEmpty, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Get an extended attribute. /// If `size` is 0, the size of the value should be sent with `reply.size()`. /// If `size` is not 0, and the value fits, send it with `reply.data()`, or /// `reply.error(ERANGE)` if it doesn't. - fn getxattr( - &mut self, + async fn getxattr( + &self, _req: &Request<'_>, _ino: u64, _name: &OsStr, _size: u32, reply: ReplyXattr, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// List extended attribute names. /// If `size` is 0, the size of the value should be sent with `reply.size()`. /// If `size` is not 0, and the value fits, send it with `reply.data()`, or /// `reply.error(ERANGE)` if it doesn't. - fn listxattr(&mut self, _req: &Request<'_>, _ino: u64, _size: u32, reply: ReplyXattr) { - reply.error(ENOSYS); + async fn listxattr(&self, _req: &Request<'_>, _ino: u64, _size: u32, reply: ReplyXattr) { + reply.error(ENOSYS).await; } /// Remove an extended attribute. - fn removexattr(&mut self, _req: &Request<'_>, _ino: u64, _name: &OsStr, reply: ReplyEmpty) { - reply.error(ENOSYS); + async fn removexattr(&self, _req: &Request<'_>, _ino: u64, _name: &OsStr, reply: ReplyEmpty) { + reply.error(ENOSYS).await; } /// Check file access permissions. /// This will be called for the access() system call. If the 'default_permissions' /// mount option is given, this method is not called. This method is not called /// under Linux kernel versions 2.4.x - fn access(&mut self, _req: &Request<'_>, _ino: u64, _mask: i32, reply: ReplyEmpty) { - reply.error(ENOSYS); + async fn access(&self, _req: &Request<'_>, _ino: u64, _mask: i32, reply: ReplyEmpty) { + reply.error(ENOSYS).await; } /// Create and open a file. @@ -697,8 +701,8 @@ pub trait Filesystem { /// structure in for more details. If this method is not /// implemented or under Linux kernel versions earlier than 2.6.15, the mknod() /// and open() methods will be called instead. - fn create( - &mut self, + async fn create( + &self, _req: &Request<'_>, _parent: u64, _name: &OsStr, @@ -707,12 +711,12 @@ pub trait Filesystem { _flags: i32, reply: ReplyCreate, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Test for a POSIX file lock. - fn getlk( - &mut self, + async fn getlk( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, @@ -723,7 +727,7 @@ pub trait Filesystem { _pid: u32, reply: ReplyLock, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Acquire, modify or release a POSIX file lock. @@ -733,8 +737,8 @@ pub trait Filesystem { /// used to fill in this field in getlk(). Note: if the locking methods are not /// implemented, the kernel will still allow file locking to work locally. /// Hence these are only interesting for network filesystems and similar. - fn setlk( - &mut self, + async fn setlk( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, @@ -746,26 +750,26 @@ pub trait Filesystem { _sleep: bool, reply: ReplyEmpty, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Map block index within file to block index within device. /// Note: This makes sense only for block device backed filesystems mounted /// with the 'blkdev' option - fn bmap( - &mut self, + async fn bmap( + &self, _req: &Request<'_>, _ino: u64, _blocksize: u32, _idx: u64, reply: ReplyBmap, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// control device - fn ioctl( - &mut self, + async fn ioctl( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, @@ -775,12 +779,12 @@ pub trait Filesystem { _out_size: u32, reply: ReplyIoctl, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Preallocate or deallocate space to a file - fn fallocate( - &mut self, + async fn fallocate( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, @@ -789,12 +793,12 @@ pub trait Filesystem { _mode: i32, reply: ReplyEmpty, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Reposition read/write file offset - fn lseek( - &mut self, + async fn lseek( + &self, _req: &Request<'_>, _ino: u64, _fh: u64, @@ -802,12 +806,12 @@ pub trait Filesystem { _whence: i32, reply: ReplyLseek, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// Copy the specified range from the source inode to the destination inode - fn copy_file_range( - &mut self, + async fn copy_file_range( + &self, _req: &Request<'_>, _ino_in: u64, _fh_in: u64, @@ -819,20 +823,20 @@ pub trait Filesystem { _flags: u32, reply: ReplyWrite, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// macOS only: Rename the volume. Set fuse_init_out.flags during init to /// FUSE_VOL_RENAME to enable #[cfg(target_os = "macos")] - fn setvolname(&mut self, _req: &Request<'_>, _name: &OsStr, reply: ReplyEmpty) { - reply.error(ENOSYS); + async fn setvolname(&self, _req: &Request<'_>, _name: &OsStr, reply: ReplyEmpty) { + reply.error(ENOSYS).await; } /// macOS only (undocumented) #[cfg(target_os = "macos")] - fn exchange( - &mut self, + async fn exchange( + &self, _req: &Request<'_>, _parent: u64, _name: &OsStr, @@ -841,14 +845,14 @@ pub trait Filesystem { _options: u64, reply: ReplyEmpty, ) { - reply.error(ENOSYS); + reply.error(ENOSYS).await; } /// macOS only: Query extended times (bkuptime and crtime). Set fuse_init_out.flags /// during init to FUSE_XTIMES to enable #[cfg(target_os = "macos")] - fn getxtimes(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyXTimes) { - reply.error(ENOSYS); + async fn getxtimes(&self, _req: &Request<'_>, _ino: u64, reply: ReplyXTimes) { + reply.error(ENOSYS).await; } } @@ -858,12 +862,20 @@ pub trait Filesystem { /// Note that you need to lead each option with a separate `"-o"` string. See /// `examples/hello.rs`. #[cfg(feature = "libfuse")] -pub fn mount>( +pub async fn mount>( filesystem: FS, + worker_channel_count: usize, + mountpoint: P, options: &[&OsStr], ) -> io::Result<()> { - Session::new(filesystem, mountpoint.as_ref(), options).and_then(|mut se| se.run()) + let session: Session = Session::new( + filesystem, + worker_channel_count, + mountpoint.as_ref(), + options, + )?; + session.run().await } /// Mount the given filesystem to the given mountpoint. This function will @@ -871,13 +883,21 @@ pub fn mount>( /// /// NOTE: This will eventually replace mount(), once the API is stable #[cfg(not(feature = "libfuse"))] -pub fn mount2>( +pub async fn mount2>( filesystem: FS, + worker_channel_count: usize, mountpoint: P, options: &[MountOption], ) -> io::Result<()> { check_option_conflicts(options)?; - Session::new2(filesystem, mountpoint.as_ref(), options).and_then(|mut se| se.run()) + let session = Session::new2( + filesystem, + worker_channel_count, + mountpoint.as_ref(), + options, + )?; + + session.run().await } /// Mount the given filesystem to the given mountpoint. This function will @@ -885,8 +905,9 @@ pub fn mount2>( /// /// NOTE: This will eventually replace mount(), once the API is stable #[cfg(feature = "libfuse")] -pub fn mount2>( +pub async fn mount2>( filesystem: FS, + worker_channel_count: usize, mountpoint: P, options: &[MountOption], ) -> io::Result<()> { @@ -894,7 +915,8 @@ pub fn mount2>( let options: Vec = options.iter().map(|x| option_to_string(x)).collect(); let option_str = options.join(","); let args = vec![OsStr::new("-o"), OsStr::new(&option_str)]; - Session::new(filesystem, mountpoint.as_ref(), &args).and_then(|mut se| se.run()) + let session = Session::new(filesystem, worker_channel_count, mountpoint.as_ref(), &args)?; + session.run().await } /// Mount the given filesystem to the given mountpoint. This function spawns @@ -908,10 +930,18 @@ pub fn mount2>( /// This interface is inherently unsafe if the BackgroundSession is allowed to leak without being /// dropped. See rust-lang/rust#24292 for more details. #[cfg(feature = "libfuse")] -pub fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef>( +pub async fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef>( filesystem: FS, + worker_channel_count: usize, + mountpoint: P, options: &[&OsStr], ) -> io::Result { - Session::new(filesystem, mountpoint.as_ref(), options).and_then(|se| se.spawn()) + let se = Session::new( + filesystem, + worker_channel_count, + mountpoint.as_ref(), + options, + )?; + se.spawn().await } diff --git a/src/reply.rs b/src/reply.rs index 57423c1a..c827c162 100644 --- a/src/reply.rs +++ b/src/reply.rs @@ -15,6 +15,8 @@ use crate::fuse_abi::{ }; use crate::fuse_abi::{fuse_create_out, fuse_getxattr_out}; use crate::fuse_abi::{fuse_dirent, fuse_direntplus, fuse_out_header}; +use crate::{FileAttr, FileType}; +use async_trait::async_trait; use libc::{c_int, EIO, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFREG, S_IFSOCK}; use log::warn; use std::convert::{AsRef, TryInto}; @@ -26,12 +28,11 @@ use std::os::unix::ffi::OsStrExt; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use zerocopy::AsBytes; -use crate::{FileAttr, FileType}; - /// Generic reply callback to send data -pub trait ReplySender: Send + 'static { +#[async_trait] +pub trait ReplySender: Send + Sync + 'static { /// Send data. - fn send(&self, data: &[&[u8]]); + async fn send(&self, data: &[&[u8]]); } impl fmt::Debug for Box { @@ -164,7 +165,7 @@ impl Reply for ReplyRaw { impl ReplyRaw { /// Reply to a request with the given error code and data. Must be called /// only once (the `ok` and `error` methods ensure this by consuming `self`) - fn send(&mut self, err: c_int, bytes: &[&[u8]]) { + async fn send(&mut self, err: c_int, bytes: &[&[u8]]) { assert!(self.sender.is_some()); let len = bytes.iter().fold(0, |l, b| l + b.len()); let header = fuse_out_header { @@ -175,17 +176,17 @@ impl ReplyRaw { let sender = self.sender.take().unwrap(); let mut sendbytes = [header.as_bytes()].to_vec(); sendbytes.extend(bytes); - sender.send(sendbytes.as_ref()); + sender.send(&sendbytes).await; } /// Reply to a request with the given type - pub fn ok(mut self, data: &T) { - self.send(0, &[data.as_bytes()]) + pub async fn ok(mut self, data: &T) { + self.send(0, &[data.as_bytes()]).await; } /// Reply to a request with the given error code - pub fn error(mut self, err: c_int) { - self.send(err, &[]); + pub async fn error(mut self, err: c_int) { + self.send(err, &[]).await; } } @@ -196,7 +197,7 @@ impl Drop for ReplyRaw { "Reply not sent for operation {}, replying with I/O error", self.unique ); - self.send(EIO, &[]); + tokio::task::block_in_place(move || futures::executor::block_on(self.send(EIO, &[]))); } } } @@ -219,13 +220,13 @@ impl Reply for ReplyEmpty { impl ReplyEmpty { /// Reply to a request with nothing - pub fn ok(mut self) { - self.reply.send(0, &[]); + pub async fn ok(mut self) { + self.reply.send(0, &[]).await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -247,13 +248,13 @@ impl Reply for ReplyData { impl ReplyData { /// Reply to a request with the given data - pub fn data(mut self, data: &[u8]) { - self.reply.send(0, &[data]); + pub async fn data(mut self, data: &[u8]) { + self.reply.send(0, &[data]).await } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await } } @@ -275,21 +276,23 @@ impl Reply for ReplyEntry { impl ReplyEntry { /// Reply to a request with the given entry - pub fn entry(self, ttl: &Duration, attr: &FileAttr, generation: u64) { - self.reply.ok(&fuse_entry_out { - nodeid: attr.ino, - generation, - entry_valid: ttl.as_secs(), - attr_valid: ttl.as_secs(), - entry_valid_nsec: ttl.subsec_nanos(), - attr_valid_nsec: ttl.subsec_nanos(), - attr: fuse_attr_from_attr(attr), - }); + pub async fn entry(self, ttl: &Duration, attr: &FileAttr, generation: u64) { + self.reply + .ok(&fuse_entry_out { + nodeid: attr.ino, + generation, + entry_valid: ttl.as_secs(), + attr_valid: ttl.as_secs(), + entry_valid_nsec: ttl.subsec_nanos(), + attr_valid_nsec: ttl.subsec_nanos(), + attr: fuse_attr_from_attr(attr), + }) + .await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await } } @@ -311,18 +314,20 @@ impl Reply for ReplyAttr { impl ReplyAttr { /// Reply to a request with the given attribute - pub fn attr(self, ttl: &Duration, attr: &FileAttr) { - self.reply.ok(&fuse_attr_out { - attr_valid: ttl.as_secs(), - attr_valid_nsec: ttl.subsec_nanos(), - dummy: 0, - attr: fuse_attr_from_attr(attr), - }); + pub async fn attr(self, ttl: &Duration, attr: &FileAttr) { + self.reply + .ok(&fuse_attr_out { + attr_valid: ttl.as_secs(), + attr_valid_nsec: ttl.subsec_nanos(), + dummy: 0, + attr: fuse_attr_from_attr(attr), + }) + .await } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await } } @@ -347,20 +352,22 @@ impl Reply for ReplyXTimes { #[cfg(target_os = "macos")] impl ReplyXTimes { /// Reply to a request with the given xtimes - pub fn xtimes(self, bkuptime: SystemTime, crtime: SystemTime) { + pub async fn xtimes(self, bkuptime: SystemTime, crtime: SystemTime) { let (bkuptime_secs, bkuptime_nanos) = time_from_system_time(&bkuptime); let (crtime_secs, crtime_nanos) = time_from_system_time(&crtime); - self.reply.ok(&fuse_getxtimes_out { - bkuptime: bkuptime_secs as u64, - crtime: crtime_secs as u64, - bkuptimensec: bkuptime_nanos, - crtimensec: crtime_nanos, - }); + self.reply + .ok(&fuse_getxtimes_out { + bkuptime: bkuptime_secs as u64, + crtime: crtime_secs as u64, + bkuptimensec: bkuptime_nanos, + crtimensec: crtime_nanos, + }) + .await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -382,17 +389,19 @@ impl Reply for ReplyOpen { impl ReplyOpen { /// Reply to a request with the given open result - pub fn opened(self, fh: u64, flags: u32) { - self.reply.ok(&fuse_open_out { - fh, - open_flags: flags, - padding: 0, - }); + pub async fn opened(self, fh: u64, flags: u32) { + self.reply + .ok(&fuse_open_out { + fh, + open_flags: flags, + padding: 0, + }) + .await } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await } } @@ -414,13 +423,13 @@ impl Reply for ReplyWrite { impl ReplyWrite { /// Reply to a request with the given open result - pub fn written(self, size: u32) { - self.reply.ok(&fuse_write_out { size, padding: 0 }); + pub async fn written(self, size: u32) { + self.reply.ok(&fuse_write_out { size, padding: 0 }).await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -443,7 +452,7 @@ impl Reply for ReplyStatfs { impl ReplyStatfs { /// Reply to a request with the given open result #[allow(clippy::too_many_arguments)] - pub fn statfs( + pub async fn statfs( self, blocks: u64, bfree: u64, @@ -454,25 +463,27 @@ impl ReplyStatfs { namelen: u32, frsize: u32, ) { - self.reply.ok(&fuse_statfs_out { - st: fuse_kstatfs { - blocks, - bfree, - bavail, - files, - ffree, - bsize, - namelen, - frsize, - padding: 0, - spare: [0; 6], - }, - }); + self.reply + .ok(&fuse_statfs_out { + st: fuse_kstatfs { + blocks, + bfree, + bavail, + files, + ffree, + bsize, + namelen, + frsize, + padding: 0, + spare: [0; 6], + }, + }) + .await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -494,28 +505,37 @@ impl Reply for ReplyCreate { impl ReplyCreate { /// Reply to a request with the given entry - pub fn created(self, ttl: &Duration, attr: &FileAttr, generation: u64, fh: u64, flags: u32) { - self.reply.ok(&fuse_create_out( - fuse_entry_out { - nodeid: attr.ino, - generation, - entry_valid: ttl.as_secs(), - attr_valid: ttl.as_secs(), - entry_valid_nsec: ttl.subsec_nanos(), - attr_valid_nsec: ttl.subsec_nanos(), - attr: fuse_attr_from_attr(attr), - }, - fuse_open_out { - fh, - open_flags: flags, - padding: 0, - }, - )); + pub async fn created( + self, + ttl: &Duration, + attr: &FileAttr, + generation: u64, + fh: u64, + flags: u32, + ) { + self.reply + .ok(&fuse_create_out( + fuse_entry_out { + nodeid: attr.ino, + generation, + entry_valid: ttl.as_secs(), + attr_valid: ttl.as_secs(), + entry_valid_nsec: ttl.subsec_nanos(), + attr_valid_nsec: ttl.subsec_nanos(), + attr: fuse_attr_from_attr(attr), + }, + fuse_open_out { + fh, + open_flags: flags, + padding: 0, + }, + )) + .await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -537,20 +557,22 @@ impl Reply for ReplyLock { impl ReplyLock { /// Reply to a request with the given open result - pub fn locked(self, start: u64, end: u64, typ: i32, pid: u32) { - self.reply.ok(&fuse_lk_out { - lk: fuse_file_lock { - start, - end, - typ, - pid, - }, - }); + pub async fn locked(self, start: u64, end: u64, typ: i32, pid: u32) { + self.reply + .ok(&fuse_lk_out { + lk: fuse_file_lock { + start, + end, + typ, + pid, + }, + }) + .await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -572,13 +594,13 @@ impl Reply for ReplyBmap { impl ReplyBmap { /// Reply to a request with the given open result - pub fn bmap(self, block: u64) { - self.reply.ok(&fuse_bmap_out { block }); + pub async fn bmap(self, block: u64) { + self.reply.ok(&fuse_bmap_out { block }).await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -600,7 +622,7 @@ impl Reply for ReplyIoctl { impl ReplyIoctl { /// Reply to a request with the given open result - pub fn ioctl(mut self, result: i32, data: &[u8]) { + pub async fn ioctl(mut self, result: i32, data: &[u8]) { let header = fuse_ioctl_out { result, // these fields are only needed for unrestricted ioctls @@ -610,15 +632,15 @@ impl ReplyIoctl { }; if !data.is_empty() { - self.reply.send(0, &[header.as_bytes(), data]); + self.reply.send(0, &[header.as_bytes(), data]).await; } else { - self.reply.send(0, &[header.as_bytes()]); + self.reply.send(0, &[header.as_bytes()]).await; } } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -665,13 +687,13 @@ impl ReplyDirectory { } /// Reply to a request with the filled directory buffer - pub fn ok(mut self) { - self.reply.send(0, &[&self.data]); + pub async fn ok(mut self) { + self.reply.send(0, &[&self.data]).await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -736,13 +758,13 @@ impl ReplyDirectoryPlus { } /// Reply to a request with the filled directory buffer - pub fn ok(mut self) { - self.reply.send(0, &[&self.data]); + pub async fn ok(mut self) { + self.reply.send(0, &[&self.data]).await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -764,18 +786,18 @@ impl Reply for ReplyXattr { impl ReplyXattr { /// Reply to a request with the size of the xattr. - pub fn size(self, size: u32) { - self.reply.ok(&fuse_getxattr_out { size, padding: 0 }); + pub async fn size(self, size: u32) { + self.reply.ok(&fuse_getxattr_out { size, padding: 0 }).await; } /// Reply to a request with the data in the xattr. - pub fn data(mut self, data: &[u8]) { - self.reply.send(0, &[data]); + pub async fn data(mut self, data: &[u8]) { + self.reply.send(0, &[data]).await; } /// Reply to a request with the given error code. - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } @@ -797,27 +819,28 @@ impl Reply for ReplyLseek { impl ReplyLseek { /// Reply to a request with seeked offset - pub fn offset(self, offset: i64) { - self.reply.ok(&fuse_lseek_out { offset }); + pub async fn offset(self, offset: i64) { + self.reply.ok(&fuse_lseek_out { offset }).await; } /// Reply to a request with the given error code - pub fn error(self, err: c_int) { - self.reply.error(err); + pub async fn error(self, err: c_int) { + self.reply.error(err).await; } } #[cfg(test)] mod test { + use futures::channel::mpsc::{channel, Sender}; + use super::AsBytes; + #[cfg(target_os = "macos")] use super::ReplyXTimes; use super::ReplyXattr; use super::{Reply, ReplyAttr, ReplyData, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyRaw}; use super::{ReplyBmap, ReplyCreate, ReplyDirectory, ReplyLock, ReplyStatfs, ReplyWrite}; use crate::{FileAttr, FileType}; - use std::sync::mpsc::{channel, Sender}; - use std::thread; use std::time::{Duration, UNIX_EPOCH}; #[allow(dead_code)] @@ -854,14 +877,15 @@ mod test { expected: Vec>, } + #[async_trait::async_trait] impl super::ReplySender for AssertSender { - fn send(&self, data: &[&[u8]]) { + async fn send(&self, data: &[&[u8]]) { assert_eq!(self.expected, data); } } - #[test] - fn reply_raw() { + #[tokio::test] + async fn reply_raw() { let data = Data { a: 0x12, b: 0x34, @@ -877,11 +901,11 @@ mod test { ], }; let reply: ReplyRaw = Reply::new(0xdeadbeef, sender); - reply.ok(&data); + reply.ok(&data).await; } - #[test] - fn reply_error() { + #[tokio::test] + async fn reply_error() { let sender = AssertSender { expected: vec![vec![ 0x10, 0x00, 0x00, 0x00, 0xbe, 0xff, 0xff, 0xff, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, @@ -889,11 +913,11 @@ mod test { ]], }; let reply: ReplyRaw = Reply::new(0xdeadbeef, sender); - reply.error(66); + reply.error(66).await; } - #[test] - fn reply_empty() { + #[tokio::test] + async fn reply_empty() { let sender = AssertSender { expected: vec![vec![ 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x00, 0x00, @@ -901,11 +925,11 @@ mod test { ]], }; let reply: ReplyEmpty = Reply::new(0xdeadbeef, sender); - reply.ok(); + reply.ok().await; } - #[test] - fn reply_data() { + #[tokio::test] + async fn reply_data() { let sender = AssertSender { expected: vec![ vec![ @@ -916,11 +940,11 @@ mod test { ], }; let reply: ReplyData = Reply::new(0xdeadbeef, sender); - reply.data(&[0xde, 0xad, 0xbe, 0xef]); + reply.data(&[0xde, 0xad, 0xbe, 0xef]).await; } - #[test] - fn reply_entry() { + #[tokio::test] + async fn reply_entry() { let mut expected = if cfg!(target_os = "macos") { vec![ vec![ @@ -989,11 +1013,11 @@ mod test { blksize: 0xbb, padding: 0xcc, }; - reply.entry(&ttl, &attr, 0xaa); + reply.entry(&ttl, &attr, 0xaa).await; } - #[test] - fn reply_attr() { + #[tokio::test] + async fn reply_attr() { let mut expected = if cfg!(target_os = "macos") { vec![ vec![ @@ -1058,12 +1082,12 @@ mod test { blksize: 0xbb, padding: 0xcc, }; - reply.attr(&ttl, &attr); + reply.attr(&ttl, &attr).await; } - #[test] + #[tokio::test] #[cfg(target_os = "macos")] - fn reply_xtimes() { + async fn reply_xtimes() { let sender = AssertSender { expected: vec![ vec![ @@ -1078,11 +1102,11 @@ mod test { }; let reply: ReplyXTimes = Reply::new(0xdeadbeef, sender); let time = UNIX_EPOCH + Duration::new(0x1234, 0x5678); - reply.xtimes(time, time); + reply.xtimes(time, time).await; } - #[test] - fn reply_open() { + #[tokio::test] + async fn reply_open() { let sender = AssertSender { expected: vec![ vec![ @@ -1096,11 +1120,11 @@ mod test { ], }; let reply: ReplyOpen = Reply::new(0xdeadbeef, sender); - reply.opened(0x1122, 0x33); + reply.opened(0x1122, 0x33).await; } - #[test] - fn reply_write() { + #[tokio::test] + async fn reply_write() { let sender = AssertSender { expected: vec![ vec![ @@ -1111,11 +1135,11 @@ mod test { ], }; let reply: ReplyWrite = Reply::new(0xdeadbeef, sender); - reply.written(0x1122); + reply.written(0x1122).await; } - #[test] - fn reply_statfs() { + #[tokio::test] + async fn reply_statfs() { let sender = AssertSender { expected: vec![ vec![ @@ -1134,11 +1158,13 @@ mod test { ], }; let reply: ReplyStatfs = Reply::new(0xdeadbeef, sender); - reply.statfs(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88); + reply + .statfs(0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88) + .await; } - #[test] - fn reply_create() { + #[tokio::test] + async fn reply_create() { let mut expected = if cfg!(target_os = "macos") { vec![ vec![ @@ -1213,11 +1239,11 @@ mod test { blksize: 0xdd, padding: 0xee, }; - reply.created(&ttl, &attr, 0xaa, 0xbb, 0xcc); + reply.created(&ttl, &attr, 0xaa, 0xbb, 0xcc).await; } - #[test] - fn reply_lock() { + #[tokio::test] + async fn reply_lock() { let sender = AssertSender { expected: vec![ vec![ @@ -1231,11 +1257,11 @@ mod test { ], }; let reply: ReplyLock = Reply::new(0xdeadbeef, sender); - reply.locked(0x11, 0x22, 0x33, 0x44); + reply.locked(0x11, 0x22, 0x33, 0x44).await; } - #[test] - fn reply_bmap() { + #[tokio::test] + async fn reply_bmap() { let sender = AssertSender { expected: vec![ vec![ @@ -1246,11 +1272,11 @@ mod test { ], }; let reply: ReplyBmap = Reply::new(0xdeadbeef, sender); - reply.bmap(0x1234); + reply.bmap(0x1234).await; } - #[test] - fn reply_directory() { + #[tokio::test] + async fn reply_directory() { let sender = AssertSender { expected: vec![ vec![ @@ -1269,17 +1295,11 @@ mod test { let mut reply = ReplyDirectory::new(0xdeadbeef, sender, 4096); assert!(!reply.add(0xaabb, 1, FileType::Directory, "hello")); assert!(!reply.add(0xccdd, 2, FileType::RegularFile, "world.rs")); - reply.ok(); + reply.ok().await; } - impl super::ReplySender for Sender<()> { - fn send(&self, _: &[&[u8]]) { - Sender::send(self, ()).unwrap() - } - } - - #[test] - fn reply_xattr_size() { + #[tokio::test] + async fn reply_xattr_size() { let sender = AssertSender { expected: vec![ vec![ @@ -1290,11 +1310,11 @@ mod test { ], }; let reply = ReplyXattr::new(0xdeadbeef, sender); - reply.size(0x12345678); + reply.size(0x12345678).await; } - #[test] - fn reply_xattr_data() { + #[tokio::test] + async fn reply_xattr_data() { let sender = AssertSender { expected: vec![ vec![ @@ -1305,16 +1325,32 @@ mod test { ], }; let reply = ReplyXattr::new(0xdeadbeef, sender); - reply.data(&[0x11, 0x22, 0x33, 0x44]); + reply.data(&[0x11, 0x22, 0x33, 0x44]).await; } - #[test] - fn async_reply() { - let (tx, rx) = channel::<()>(); - let reply: ReplyEmpty = Reply::new(0xdeadbeef, tx); - thread::spawn(move || { - reply.ok(); + struct SenderWrapper(std::sync::Arc>>); + #[async_trait::async_trait] + impl super::ReplySender for SenderWrapper { + async fn send(&self, _: &[&[u8]]) { + use futures::sink::SinkExt; + + let mut inner = self.0.lock().await; + let r: &mut Sender<()> = &mut inner; + SinkExt::send(r, ()).await.unwrap() + } + } + + #[tokio::test] + async fn async_reply() { + use futures::stream::StreamExt; + let (tx, mut rx) = channel::<()>(50); + let reply: ReplyEmpty = Reply::new( + 0xdeadbeef, + SenderWrapper(std::sync::Arc::new(tokio::sync::Mutex::new(tx))), + ); + tokio::task::spawn(async move { + reply.ok().await; }); - rx.recv().unwrap(); + rx.next().await.unwrap(); } } diff --git a/src/request.rs b/src/request.rs index e62760c3..5855749c 100644 --- a/src/request.rs +++ b/src/request.rs @@ -5,19 +5,17 @@ //! //! TODO: This module is meant to go away soon in favor of `ll::Request`. -use crate::fuse_abi::consts::*; use crate::fuse_abi::*; +use crate::{fuse_abi::consts::*, io_ops::ArcSubChannel, session::ActiveSession}; use libc::{EIO, ENOSYS, EPROTO}; use log::{debug, error, warn}; -use std::convert::TryFrom; use std::path::Path; use std::time::{Duration, SystemTime}; +use std::{convert::TryFrom, sync::Arc}; -use crate::channel::ChannelSender; #[cfg(feature = "abi-7-21")] use crate::reply::ReplyDirectoryPlus; use crate::reply::{Reply, ReplyDirectory, ReplyEmpty, ReplyRaw}; -use crate::session::Session; use crate::Filesystem; use crate::TimeOrNow::{Now, SpecificTime}; use crate::{ll, KernelConfig}; @@ -33,17 +31,15 @@ fn system_time_from_time(secs: i64, nsecs: u32) -> SystemTime { /// Request data structure #[derive(Debug)] pub struct Request<'a> { - /// Channel sender for sending the reply - ch: ChannelSender, /// Request raw data - data: &'a [u8], + pub data: &'a [u8], /// Parsed request - request: ll::Request<'a>, + pub request: ll::Request<'a>, } impl<'a> Request<'a> { /// Create a new request from the given data - pub fn new(ch: ChannelSender, data: &'a [u8]) -> Option> { + pub fn new(data: &'a [u8]) -> Option> { let request = match ll::Request::try_from(data) { Ok(request) => request, Err(err) => { @@ -53,34 +49,39 @@ impl<'a> Request<'a> { } }; - Some(Self { ch, data, request }) + Some(Self { data, request }) } - /// Dispatch request to the given filesystem. - /// This calls the appropriate filesystem operation method for the - /// request and sends back the returned reply to the kernel - pub fn dispatch(&self, se: &mut Session) { + pub(crate) async fn dispatch_init( + &self, + se: &Arc, + filesystem: &Arc, + ch: ArcSubChannel, + ) { debug!("{}", self.request); - match self.request.operation() { // Filesystem initialization ll::Operation::Init { arg } => { - let reply: ReplyRaw = self.reply(); + let reply: ReplyRaw = self.reply(&ch); // We don't support ABI versions before 7.6 if arg.major < 7 || (arg.major == 7 && arg.minor < 6) { error!("Unsupported FUSE ABI version {}.{}", arg.major, arg.minor); - reply.error(EPROTO); + reply.error(EPROTO).await; + se.destroy().await; return; } + + let mut cfg = se.session_configuration.lock().await; // Remember ABI version supported by kernel - se.proto_major = arg.major; - se.proto_minor = arg.minor; + cfg.proto_major = arg.major; + cfg.proto_minor = arg.minor; let mut config = KernelConfig::new(arg.flags, arg.max_readahead); // Call filesystem init method and give it a chance to return an error - let res = se.filesystem.init(self, &mut config); + let res = filesystem.init(self, &mut config).await; if let Err(err) = res { - reply.error(err); + reply.error(err).await; + se.destroy().await; return; } // Reply with our desired version and settings. If the kernel supports a @@ -113,42 +114,58 @@ impl<'a> Request<'a> { "INIT response: ABI {}.{}, flags {:#x}, max readahead {}, max write {}", init.major, init.minor, init.flags, init.max_readahead, init.max_write ); - se.initialized = true; - reply.ok(&init); + se.initialized + .store(true, std::sync::atomic::Ordering::Relaxed); + reply.ok(&init).await; } // Any operation is invalid before initialization - _ if !se.initialized => { + _ => { warn!("Ignoring FUSE operation before init: {}", self.request); - self.reply::().error(EIO); + self.reply::(&ch).error(EIO).await; } - // Filesystem destroyed - ll::Operation::Destroy => { - se.filesystem.destroy(self); - se.destroyed = true; - self.reply::().ok(); - } - // Any operation is invalid after destroy - _ if se.destroyed => { - warn!("Ignoring FUSE operation after destroy: {}", self.request); - self.reply::().error(EIO); + } + } + + /// Dispatch request to the given filesystem. + /// This calls the appropriate filesystem operation method for the + /// request and sends back the returned reply to the kernel + pub(crate) async fn dispatch( + &self, + active_session: &Arc, + filesystem: Arc, + ch: ArcSubChannel, + ) -> std::io::Result<()> { + debug!("{}", self.request); + + match self.request.operation() { + // Filesystem initialization + ll::Operation::Init { arg: _arg } => { + warn!( + "Already initialized, got init after init init: {}", + self.request + ); + self.reply::(&ch).error(EIO).await; } ll::Operation::Interrupt { .. } => { // TODO: handle FUSE_INTERRUPT - self.reply::().error(ENOSYS); + self.reply::(&ch).error(ENOSYS).await; } ll::Operation::Lookup { name } => { - se.filesystem - .lookup(self, self.request.nodeid(), &name, self.reply()); + filesystem + .lookup(self, self.request.nodeid(), &name, self.reply(&ch)) + .await } ll::Operation::Forget { arg } => { - se.filesystem - .forget(self, self.request.nodeid(), arg.nlookup); // no reply + filesystem + .forget(self, self.request.nodeid(), arg.nlookup) + .await; // no reply } ll::Operation::GetAttr => { - se.filesystem - .getattr(self, self.request.nodeid(), self.reply()); + filesystem + .getattr(self, self.request.nodeid(), self.reply(&ch)) + .await; } ll::Operation::SetAttr { arg } => { let mode = match arg.valid & FATTR_MODE { @@ -241,242 +258,296 @@ impl<'a> Request<'a> { (None, None, None, None) } let (crtime, chgtime, bkuptime, flags) = get_macos_setattr(arg); - se.filesystem.setattr( - self, - self.request.nodeid(), - mode, - uid, - gid, - size, - atime, - mtime, - ctime, - fh, - crtime, - chgtime, - bkuptime, - flags, - self.reply(), - ); + filesystem + .setattr( + self, + self.request.nodeid(), + mode, + uid, + gid, + size, + atime, + mtime, + ctime, + fh, + crtime, + chgtime, + bkuptime, + flags, + self.reply(&ch), + ) + .await; } ll::Operation::ReadLink => { - se.filesystem - .readlink(self, self.request.nodeid(), self.reply()); + filesystem + .readlink(self, self.request.nodeid(), self.reply(&ch)) + .await; } ll::Operation::MkNod { arg, name } => { #[cfg(not(feature = "abi-7-12"))] - se.filesystem.mknod( - self, - self.request.nodeid(), - &name, - arg.mode, - 0, - arg.rdev, - self.reply(), - ); + filesystem + .mknod( + self, + self.request.nodeid(), + &name, + arg.mode, + 0, + arg.rdev, + self.reply(&ch), + ) + .await; #[cfg(feature = "abi-7-12")] - se.filesystem.mknod( - self, - self.request.nodeid(), - &name, - arg.mode, - arg.umask, - arg.rdev, - self.reply(), - ); + filesystem + .mknod( + self, + self.request.nodeid(), + &name, + arg.mode, + arg.umask, + arg.rdev, + self.reply(&ch), + ) + .await; } ll::Operation::MkDir { arg, name } => { #[cfg(not(feature = "abi-7-12"))] - se.filesystem.mkdir( - self, - self.request.nodeid(), - &name, - arg.mode, - 0, - self.reply(), - ); + filesystem + .mkdir( + self, + self.request.nodeid(), + &name, + arg.mode, + 0, + self.reply(&ch), + ) + .await; #[cfg(feature = "abi-7-12")] - se.filesystem.mkdir( - self, - self.request.nodeid(), - &name, - arg.mode, - arg.umask, - self.reply(), - ); + filesystem + .mkdir( + self, + self.request.nodeid(), + &name, + arg.mode, + arg.umask, + self.reply(&ch), + ) + .await; } ll::Operation::Unlink { name } => { - se.filesystem - .unlink(self, self.request.nodeid(), &name, self.reply()); + filesystem + .unlink(self, self.request.nodeid(), &name, self.reply(&ch)) + .await; } ll::Operation::RmDir { name } => { - se.filesystem - .rmdir(self, self.request.nodeid(), &name, self.reply()); + filesystem + .rmdir(self, self.request.nodeid(), &name, self.reply(&ch)) + .await; } ll::Operation::SymLink { name, link } => { - se.filesystem.symlink( - self, - self.request.nodeid(), - &name, - &Path::new(link), - self.reply(), - ); + filesystem + .symlink( + self, + self.request.nodeid(), + &name, + &Path::new(link), + self.reply(&ch), + ) + .await; } ll::Operation::Rename { arg, name, newname } => { - se.filesystem.rename( - self, - self.request.nodeid(), - &name, - arg.newdir, - &newname, - 0, - self.reply(), - ); + filesystem + .rename( + self, + self.request.nodeid(), + &name, + arg.newdir, + &newname, + 0, + self.reply(&ch), + ) + .await; } ll::Operation::Link { arg, name } => { - se.filesystem.link( - self, - arg.oldnodeid, - self.request.nodeid(), - &name, - self.reply(), - ); + filesystem + .link( + self, + arg.oldnodeid, + self.request.nodeid(), + &name, + self.reply(&ch), + ) + .await; } ll::Operation::Open { arg } => { - se.filesystem - .open(self, self.request.nodeid(), arg.flags, self.reply()); + filesystem + .open(self, self.request.nodeid(), arg.flags, self.reply(&ch)) + .await; } ll::Operation::Read { arg } => { #[cfg(not(feature = "abi-7-9"))] - se.filesystem.read( - self, - self.request.nodeid(), - arg.fh, - arg.offset as i64, - arg.size, - 0, - None, - self.reply(), - ); + filesystem + .read( + self, + self.request.nodeid(), + arg.fh, + arg.offset as i64, + arg.size, + 0, + None, + self.reply(&ch), + ) + .await; #[cfg(feature = "abi-7-9")] - se.filesystem.read( - self, - self.request.nodeid(), - arg.fh, - arg.offset as i64, - arg.size, - arg.flags, - if arg.read_flags & FUSE_READ_LOCKOWNER != 0 { - Some(arg.lock_owner) - } else { - None - }, - self.reply(), - ); + filesystem + .read( + self, + self.request.nodeid(), + arg.fh, + arg.offset as i64, + arg.size, + arg.flags, + if arg.read_flags & FUSE_READ_LOCKOWNER != 0 { + Some(arg.lock_owner) + } else { + None + }, + self.reply(&ch), + ) + .await; } ll::Operation::Write { arg, data } => { assert!(data.len() == arg.size as usize); #[cfg(not(feature = "abi-7-9"))] - se.filesystem.write( - self, - self.request.nodeid(), - arg.fh, - arg.offset as i64, - data, - arg.write_flags, - 0, - None, - self.reply(), - ); + filesystem + .write( + self, + self.request.nodeid(), + arg.fh, + arg.offset as i64, + data, + arg.write_flags, + 0, + None, + self.reply(&ch), + ) + .await; #[cfg(feature = "abi-7-9")] - se.filesystem.write( - self, - self.request.nodeid(), - arg.fh, - arg.offset as i64, - data, - arg.write_flags, - arg.flags, - if arg.write_flags & FUSE_WRITE_LOCKOWNER != 0 { - Some(arg.lock_owner) - } else { - None - }, - self.reply(), - ); + filesystem + .write( + self, + self.request.nodeid(), + arg.fh, + arg.offset as i64, + data, + arg.write_flags, + arg.flags, + if arg.write_flags & FUSE_WRITE_LOCKOWNER != 0 { + Some(arg.lock_owner) + } else { + None + }, + self.reply(&ch), + ) + .await; } ll::Operation::Flush { arg } => { - se.filesystem.flush( - self, - self.request.nodeid(), - arg.fh, - arg.lock_owner, - self.reply(), - ); + filesystem + .flush( + self, + self.request.nodeid(), + arg.fh, + arg.lock_owner, + self.reply(&ch), + ) + .await; } ll::Operation::Release { arg } => { let flush = !matches!(arg.release_flags & FUSE_RELEASE_FLUSH, 0); #[cfg(not(feature = "abi-7-17"))] - se.filesystem.release( - self, - self.request.nodeid(), - arg.fh, - arg.flags, - Some(arg.lock_owner), - flush, - self.reply(), - ); + filesystem + .release( + self, + self.request.nodeid(), + arg.fh, + arg.flags, + Some(arg.lock_owner), + flush, + self.reply(&ch), + ) + .await; #[cfg(feature = "abi-7-17")] - se.filesystem.release( - self, - self.request.nodeid(), - arg.fh, - arg.flags, - if arg.release_flags & FUSE_RELEASE_FLOCK_UNLOCK != 0 { - Some(arg.lock_owner) - } else { - None - }, - flush, - self.reply(), - ); + filesystem + .release( + self, + self.request.nodeid(), + arg.fh, + arg.flags, + if arg.release_flags & FUSE_RELEASE_FLOCK_UNLOCK != 0 { + Some(arg.lock_owner) + } else { + None + }, + flush, + self.reply(&ch), + ) + .await; } ll::Operation::FSync { arg } => { let datasync = !matches!(arg.fsync_flags & 1, 0); - se.filesystem - .fsync(self, self.request.nodeid(), arg.fh, datasync, self.reply()); + filesystem + .fsync( + self, + self.request.nodeid(), + arg.fh, + datasync, + self.reply(&ch), + ) + .await; } ll::Operation::OpenDir { arg } => { - se.filesystem - .opendir(self, self.request.nodeid(), arg.flags, self.reply()); + filesystem + .opendir(self, self.request.nodeid(), arg.flags, self.reply(&ch)) + .await; } ll::Operation::ReadDir { arg } => { - se.filesystem.readdir( - self, - self.request.nodeid(), - arg.fh, - arg.offset as i64, - ReplyDirectory::new(self.request.unique(), self.ch, arg.size as usize), - ); + filesystem + .readdir( + self, + self.request.nodeid(), + arg.fh, + arg.offset as i64, + ReplyDirectory::new(self.request.unique(), ch.clone(), arg.size as usize), + ) + .await; } ll::Operation::ReleaseDir { arg } => { - se.filesystem.releasedir( - self, - self.request.nodeid(), - arg.fh, - arg.flags, - self.reply(), - ); + filesystem + .releasedir( + self, + self.request.nodeid(), + arg.fh, + arg.flags, + self.reply(&ch), + ) + .await; } ll::Operation::FSyncDir { arg } => { let datasync = !matches!(arg.fsync_flags & 1, 0); - se.filesystem - .fsyncdir(self, self.request.nodeid(), arg.fh, datasync, self.reply()); + filesystem + .fsyncdir( + self, + self.request.nodeid(), + arg.fh, + datasync, + self.reply(&ch), + ) + .await; } ll::Operation::StatFs => { - se.filesystem - .statfs(self, self.request.nodeid(), self.reply()); + filesystem + .statfs(self, self.request.nodeid(), self.reply(&ch)) + .await; } ll::Operation::SetXAttr { arg, name, value } => { assert!(value.len() == arg.size as usize); @@ -490,205 +561,236 @@ impl<'a> Request<'a> { fn get_position(_arg: &fuse_setxattr_in) -> u32 { 0 } - se.filesystem.setxattr( - self, - self.request.nodeid(), - name, - value, - arg.flags, - get_position(arg), - self.reply(), - ); + filesystem + .setxattr( + self, + self.request.nodeid(), + name, + value, + arg.flags, + get_position(arg), + self.reply(&ch), + ) + .await; } ll::Operation::GetXAttr { arg, name } => { - se.filesystem - .getxattr(self, self.request.nodeid(), name, arg.size, self.reply()); + filesystem + .getxattr(self, self.request.nodeid(), name, arg.size, self.reply(&ch)) + .await; } ll::Operation::ListXAttr { arg } => { - se.filesystem - .listxattr(self, self.request.nodeid(), arg.size, self.reply()); + filesystem + .listxattr(self, self.request.nodeid(), arg.size, self.reply(&ch)) + .await; } ll::Operation::RemoveXAttr { name } => { - se.filesystem - .removexattr(self, self.request.nodeid(), name, self.reply()); + filesystem + .removexattr(self, self.request.nodeid(), name, self.reply(&ch)) + .await; } ll::Operation::Access { arg } => { - se.filesystem - .access(self, self.request.nodeid(), arg.mask, self.reply()); + filesystem + .access(self, self.request.nodeid(), arg.mask, self.reply(&ch)) + .await; } ll::Operation::Create { arg, name } => { #[cfg(not(feature = "abi-7-12"))] - se.filesystem.create( - self, - self.request.nodeid(), - &name, - arg.mode, - 0, - arg.flags, - self.reply(), - ); + filesystem + .create( + self, + self.request.nodeid(), + &name, + arg.mode, + 0, + arg.flags, + self.reply(&ch), + ) + .await; #[cfg(feature = "abi-7-12")] - se.filesystem.create( - self, - self.request.nodeid(), - &name, - arg.mode, - arg.umask, - arg.flags, - self.reply(), - ); + filesystem + .create( + self, + self.request.nodeid(), + &name, + arg.mode, + arg.umask, + arg.flags, + self.reply(&ch), + ) + .await; } ll::Operation::GetLk { arg } => { - se.filesystem.getlk( - self, - self.request.nodeid(), - arg.fh, - arg.owner, - arg.lk.start, - arg.lk.end, - arg.lk.typ, - arg.lk.pid, - self.reply(), - ); + filesystem + .getlk( + self, + self.request.nodeid(), + arg.fh, + arg.owner, + arg.lk.start, + arg.lk.end, + arg.lk.typ, + arg.lk.pid, + self.reply(&ch), + ) + .await; } ll::Operation::SetLk { arg } => { - se.filesystem.setlk( - self, - self.request.nodeid(), - arg.fh, - arg.owner, - arg.lk.start, - arg.lk.end, - arg.lk.typ, - arg.lk.pid, - false, - self.reply(), - ); + filesystem + .setlk( + self, + self.request.nodeid(), + arg.fh, + arg.owner, + arg.lk.start, + arg.lk.end, + arg.lk.typ, + arg.lk.pid, + false, + self.reply(&ch), + ) + .await; } ll::Operation::SetLkW { arg } => { - se.filesystem.setlk( - self, - self.request.nodeid(), - arg.fh, - arg.owner, - arg.lk.start, - arg.lk.end, - arg.lk.typ, - arg.lk.pid, - true, - self.reply(), - ); + filesystem + .setlk( + self, + self.request.nodeid(), + arg.fh, + arg.owner, + arg.lk.start, + arg.lk.end, + arg.lk.typ, + arg.lk.pid, + true, + self.reply(&ch), + ) + .await; } ll::Operation::BMap { arg } => { - se.filesystem.bmap( - self, - self.request.nodeid(), - arg.blocksize, - arg.block, - self.reply(), - ); + filesystem + .bmap( + self, + self.request.nodeid(), + arg.blocksize, + arg.block, + self.reply(&ch), + ) + .await; } #[cfg(feature = "abi-7-11")] ll::Operation::IoCtl { arg, data } => { let in_data = &data[..arg.in_size as usize]; if (arg.flags & FUSE_IOCTL_UNRESTRICTED) > 0 { - self.reply::().error(ENOSYS); + self.reply::(&ch).error(ENOSYS).await; } else { - se.filesystem.ioctl( - self, - self.request.nodeid(), - arg.fh, - arg.flags, - arg.cmd, - in_data, - arg.out_size, - self.reply(), - ); + filesystem + .ioctl( + self, + self.request.nodeid(), + arg.fh, + arg.flags, + arg.cmd, + in_data, + arg.out_size, + self.reply(&ch), + ) + .await; } } #[cfg(feature = "abi-7-11")] ll::Operation::Poll { arg: _ } => { // TODO: handle FUSE_POLL - self.reply::().error(ENOSYS); + self.reply::(&ch).error(ENOSYS).await; } #[cfg(feature = "abi-7-15")] ll::Operation::NotifyReply { data: _ } => { // TODO: handle FUSE_NOTIFY_REPLY - self.reply::().error(ENOSYS); + self.reply::(&ch).error(ENOSYS).await; } #[cfg(feature = "abi-7-16")] ll::Operation::BatchForget { arg: _, nodes } => { - se.filesystem.batch_forget(self, nodes); // no reply + filesystem.batch_forget(self, nodes).await; // no reply } #[cfg(feature = "abi-7-19")] ll::Operation::FAllocate { arg } => { - se.filesystem.fallocate( - self, - self.request.nodeid(), - arg.fh, - arg.offset, - arg.length, - arg.mode, - self.reply(), - ); + filesystem + .fallocate( + self, + self.request.nodeid(), + arg.fh, + arg.offset, + arg.length, + arg.mode, + self.reply(&ch), + ) + .await; } #[cfg(feature = "abi-7-21")] ll::Operation::ReadDirPlus { arg } => { - se.filesystem.readdirplus( - self, - self.request.nodeid(), - arg.fh, - arg.offset, - ReplyDirectoryPlus::new(self.request.unique(), self.ch, arg.size as usize), - ); + filesystem + .readdirplus( + self, + self.request.nodeid(), + arg.fh, + arg.offset, + ReplyDirectoryPlus::new(self.request.unique(), ch, arg.size as usize), + ) + .await; } #[cfg(feature = "abi-7-23")] ll::Operation::Rename2 { arg, name, newname } => { - se.filesystem.rename( - self, - self.request.nodeid(), - name, - arg.newdir, - newname, - arg.flags, - self.reply(), - ); + filesystem + .rename( + self, + self.request.nodeid(), + name, + arg.newdir, + newname, + arg.flags, + self.reply(&ch), + ) + .await; } #[cfg(feature = "abi-7-24")] ll::Operation::Lseek { arg } => { - se.filesystem.lseek( - self, - self.request.nodeid(), - arg.fh, - arg.offset, - arg.whence, - self.reply(), - ); + filesystem + .lseek( + self, + self.request.nodeid(), + arg.fh, + arg.offset, + arg.whence, + self.reply(&ch), + ) + .await; } #[cfg(feature = "abi-7-28")] ll::Operation::CopyFileRange { arg } => { - se.filesystem.copy_file_range( - self, - self.request.nodeid(), - arg.fh_in, - arg.off_in, - arg.nodeid_out, - arg.fh_out, - arg.off_out, - arg.len, - arg.flags as u32, - self.reply(), - ); + filesystem + .copy_file_range( + self, + self.request.nodeid(), + arg.fh_in, + arg.off_in, + arg.nodeid_out, + arg.fh_out, + arg.off_out, + arg.len, + arg.flags as u32, + self.reply(&ch), + ) + .await; } #[cfg(target_os = "macos")] ll::Operation::SetVolName { name } => { - se.filesystem.setvolname(self, name, self.reply()); + filesystem.setvolname(self, name, self.reply(&ch)).await; } #[cfg(target_os = "macos")] ll::Operation::GetXTimes => { - se.filesystem - .getxtimes(self, self.request.nodeid(), self.reply()); + filesystem + .getxtimes(self, self.request.nodeid(), self.reply(&ch)) + .await; } #[cfg(target_os = "macos")] ll::Operation::Exchange { @@ -696,29 +798,37 @@ impl<'a> Request<'a> { oldname, newname, } => { - se.filesystem.exchange( - self, - arg.olddir, - &oldname, - arg.newdir, - &newname, - arg.options, - self.reply(), - ); + filesystem + .exchange( + self, + arg.olddir, + &oldname, + arg.newdir, + &newname, + arg.options, + self.reply(&ch), + ) + .await; } #[cfg(feature = "abi-7-12")] ll::Operation::CuseInit { arg: _ } => { // TODO: handle CUSE_INIT - self.reply::().error(ENOSYS); + self.reply::(&ch).error(ENOSYS).await; + } + + ll::Operation::Destroy => { + active_session.destroy().await; + self.reply::(&ch).ok().await; } } + Ok(()) } /// Create a reply object for this request that can be passed to the filesystem /// implementation and makes sure that a request is replied exactly once - fn reply(&self) -> T { - Reply::new(self.request.unique(), self.ch) + fn reply(&self, ch: &ArcSubChannel) -> T { + Reply::new(self.request.unique(), ch.clone()) } /// Returns the unique identifier of this request diff --git a/src/session.rs b/src/session.rs index a0bfd606..8cd0ebf1 100644 --- a/src/session.rs +++ b/src/session.rs @@ -5,21 +5,26 @@ //! filesystem is mounted, the session loop receives, dispatches and replies to kernel requests //! for filesystem operations under its mount point. +use futures::future::join_all; use libc::{EAGAIN, EINTR, ENODEV, ENOENT}; -use log::{error, info}; +use log::{error, info, warn}; #[cfg(feature = "libfuse")] use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use std::thread::{self, JoinHandle}; use std::{fmt, ptr}; -use std::{io, ops::DerefMut}; +use std::{ + io, + sync::{atomic::AtomicBool, Arc}, +}; +use tokio::{sync::Mutex, task::JoinHandle}; use crate::channel::{self, Channel}; use crate::fuse_abi as abi; -use crate::request::Request; use crate::Filesystem; #[cfg(not(feature = "libfuse"))] use crate::MountOption; +use crate::{io_ops::ArcSubChannel, request::Request}; +use std::ops::DerefMut; /// The max size of write requests from the kernel. The absolute minimum is 4k, /// FUSE recommends at least 128k, max 16M. The FUSE default is 16M on macOS @@ -29,6 +34,13 @@ pub const MAX_WRITE_SIZE: usize = 16 * 1024 * 1024; /// Size of the buffer for reading a request from the kernel. Since the kernel may send /// up to MAX_WRITE_SIZE bytes in a write request, we use that value plus some extra space. const BUFFER_SIZE: usize = MAX_WRITE_SIZE + 4096; +#[derive(Debug, Default)] +pub struct SessionConfiguration { + /// FUSE protocol major version + pub proto_major: u32, + /// FUSE protocol minor version + pub proto_minor: u32, +} /// The session data structure #[derive(Debug)] @@ -37,59 +49,121 @@ pub struct Session { pub filesystem: FS, /// Communication channel to the kernel driver ch: Channel, - /// FUSE protocol major version - pub proto_major: u32, - /// FUSE protocol minor version - pub proto_minor: u32, +} + +#[derive(Debug)] +pub(crate) struct ActiveSession { + pub session_configuration: Arc>, /// True if the filesystem is initialized (init operation done) - pub initialized: bool, + pub initialized: AtomicBool, /// True if the filesystem was destroyed (destroy operation done) - pub destroyed: bool, + is_destroyed: AtomicBool, + /// Pipes to inform all of the child channels/interested parties we are shutting down + pub destroy_signals: Arc>>>, +} + +impl ActiveSession { + pub(in crate::session) async fn register_destroy( + &self, + sender: tokio::sync::oneshot::Sender<()>, + ) { + let mut guard = self.destroy_signals.lock().await; + guard.push(sender) + } + + pub(in crate) fn destroyed(&self) -> bool { + self.is_destroyed.load(std::sync::atomic::Ordering::Relaxed) + } + + pub(in crate) async fn destroy(&self) { + self.is_destroyed + .store(true, std::sync::atomic::Ordering::SeqCst); + let mut guard = self.destroy_signals.lock().await; + + for e in guard.drain(..) { + if let Err(e) = e.send(()) { + warn!("Unable to send a shutdown signal: {:?}", e); + } + } + } +} +impl Default for ActiveSession { + fn default() -> Self { + Self { + session_configuration: Arc::new(Mutex::new(Default::default())), + initialized: AtomicBool::new(false), + is_destroyed: AtomicBool::new(false), + destroy_signals: Arc::new(Mutex::new(Vec::default())), + } + } } impl Session { /// Create a new session by mounting the given filesystem to the given mountpoint #[cfg(feature = "libfuse")] - pub fn new(filesystem: FS, mountpoint: &Path, options: &[&OsStr]) -> io::Result> { - info!("Mounting {}", mountpoint.display()); - Channel::new(mountpoint, options).map(|ch| Session { - filesystem, - ch, - proto_major: 0, - proto_minor: 0, - initialized: false, - destroyed: false, - }) + pub fn new( + filesystem: FS, + worker_channel_count: usize, + mountpoint: &Path, + options: &[&OsStr], + ) -> io::Result> { + let ch = Channel::new(mountpoint, worker_channel_count, options)?; + Ok(Session { filesystem, ch }) } /// Create a new session by mounting the given filesystem to the given mountpoint #[cfg(not(feature = "libfuse"))] pub fn new2( filesystem: FS, + worker_channel_count: usize, mountpoint: &Path, options: &[MountOption], ) -> io::Result> { - info!("Mounting {}", mountpoint.display()); - Channel::new2(mountpoint, options).map(|ch| Session { - filesystem, - ch, - proto_major: 0, - proto_minor: 0, - initialized: false, - destroyed: false, - }) + Channel::new2(mountpoint, worker_channel_count, options) + .map(|ch| Session { filesystem, ch }) } /// Return path of the mounted filesystem - pub fn mountpoint(&self) -> &Path { - &self.ch.mountpoint() + pub fn mountpoint(&self) -> PathBuf { + self.ch.mountpoint().to_owned() } - /// Run the session loop that receives kernel requests and dispatches them to method - /// calls into the filesystem. This read-dispatch-loop is non-concurrent to prevent - /// having multiple buffers (which take up much memory), but the filesystem methods - /// may run concurrent by spawning threads. - pub fn run(&mut self) -> io::Result<()> { + async fn read_single_request<'a, 'b>( + ch: &ArcSubChannel, + terminated: &mut tokio::sync::oneshot::Receiver<()>, + buffer: &'b mut [u8], + ) -> Option>> { + match Channel::receive(ch, terminated, buffer).await { + Err(err) => match err.raw_os_error() { + // Operation interrupted. Accordingly to FUSE, this is safe to retry + Some(ENOENT) => None, + // Interrupted system call, retry + Some(EINTR) => None, + // Explicitly try again + Some(EAGAIN) => None, + // Filesystem was unmounted, quit the loop + Some(ENODEV) => Some(Err(err)), + // Unhandled error + _ => Some(Err(err)), + }, + Ok(Some(size)) => { + if let Some(req) = Request::new(&buffer[..size]) { + Some(Ok(req)) + } else { + None + } + } + Ok(None) => None, + } + } + + async fn main_request_loop( + active_session: &Arc, + ch: &ArcSubChannel, + terminated: &mut tokio::sync::oneshot::Receiver<()>, + filesystem: &Arc, + _worker_idx: usize, + ) -> io::Result<()> { // Buffer for receiving requests from the kernel. Only one is allocated and // it is reused immediately after dispatching to conserve memory and allocations. let mut buffer = vec![0; BUFFER_SIZE]; @@ -97,31 +171,168 @@ impl Session { buffer.deref_mut(), std::mem::align_of::(), ); + + let sender = ch.clone(); + loop { - // Read the next request from the given channel to kernel driver - // The kernel driver makes sure that we get exactly one request per read - match self.ch.receive(buf) { - Ok(size) => match Request::new(self.ch.sender(), &buf[..size]) { - // Dispatch request - Some(req) => req.dispatch(self), - // Quit loop on illegal request - None => break, - }, - Err(err) => match err.raw_os_error() { - // Operation interrupted. Accordingly to FUSE, this is safe to retry - Some(ENOENT) => continue, - // Interrupted system call, retry - Some(EINTR) => continue, - // Explicitly try again - Some(EAGAIN) => continue, - // Filesystem was unmounted, quit the loop - Some(ENODEV) => break, - // Unhandled error - _ => return Err(err), - }, + if active_session.destroyed() { + return Ok(()); + } + + if let Some(req_or_err) = Session::::read_single_request(&ch, terminated, buf).await + { + let req = req_or_err?; + let filesystem = filesystem.clone(); + let sender = sender.clone(); + + match req.dispatch(&active_session, filesystem, sender).await { + Ok(_) => {} + Err(e) => { + warn!("I/O failure in dispatch paths: {:#?}", e); + } + }; + } + } + } + + /// Spin around in the state waiting to ensure we are initialized. + /// There is a possbile race/blocking condition here in that one channel may get an init, and another channel may then + /// get a valid message. So while we won't process messages _before_ an init, a single channel if it gets its first message + /// after a different channel got the init we will need to process that as if we were in the main loop. + async fn wait_for_init( + active_session: &Arc, + ch: &ArcSubChannel, + terminated: &mut tokio::sync::oneshot::Receiver<()>, + filesystem: &Arc, + ) -> io::Result<()> { + let sender = ch.clone(); + loop { + let mut buffer = vec![0; BUFFER_SIZE]; + let buf = aligned_sub_buf( + buffer.deref_mut(), + std::mem::align_of::(), + ); + + if active_session.destroyed() { + return Ok(()); + } + + if let Some(req_or_err) = Session::::read_single_request(&ch, terminated, buf).await + { + let req = req_or_err?; + if !active_session + .initialized + .load(std::sync::atomic::Ordering::Relaxed) + { + req.dispatch_init(&active_session, &filesystem, sender.clone()) + .await; + } else { + let filesystem = filesystem.clone(); + let sender = sender.clone(); + + match req.dispatch(&active_session, filesystem, sender).await { + Ok(_) => {} + Err(e) => { + warn!("I/O failure in dispatch paths: {:#?}", e); + } + }; + } + + if active_session + .initialized + .load(std::sync::atomic::Ordering::Relaxed) + { + return Ok(()); + } } } - Ok(()) + } + + pub(crate) async fn spawn_worker_loop( + active_session: Arc, + ch: ArcSubChannel, + mut terminated: tokio::sync::oneshot::Receiver<()>, + filesystem: Arc, + worker_idx: usize, + ) -> io::Result<()> { + Session::wait_for_init(&active_session, &ch, &mut terminated, &filesystem).await?; + Session::main_request_loop( + &active_session, + &ch, + &mut terminated, + &filesystem, + worker_idx, + ) + .await + } + + async fn driver_evt_loop( + _active_session: Arc, + join_handles: Vec>>, + terminated: tokio::sync::oneshot::Receiver<()>, + mut filesystem: Arc, + channel: Channel, + ) -> io::Result<()> { + let _ = terminated.await; + loop { + if let Some(fs) = Arc::get_mut(&mut filesystem) { + fs.destroy().await; + break; + } + } + drop(channel); + + for ret in join_all(join_handles).await { + if let Err(e) = ret { + warn!("Error joining worker of {:?}", e); + } + } + return Ok(()); + } + + /// Run the session loop that receives kernel requests and dispatches them to method + /// calls into the filesystem. This spawns as a task in tokio returning that task + pub async fn spawn_run(self) -> io::Result>> { + let Session { + ch: channel, + filesystem, + } = self; + + let active_session = Arc::new(ActiveSession::default()); + let filesystem = Arc::new(filesystem); + let (sender, driver_receiver) = tokio::sync::oneshot::channel(); + active_session.register_destroy(sender).await; + let mut join_handles: Vec>> = Vec::default(); + for (idx, ch) in channel.sub_channels.iter().enumerate() { + let ch = ch.clone(); + let active_session = Arc::clone(&active_session); + let filesystem = Arc::clone(&filesystem); + let finalizer_active_session = active_session.clone(); + let (sender, receiver) = tokio::sync::oneshot::channel(); + + active_session.register_destroy(sender).await; + join_handles.push(tokio::spawn(async move { + let r = + Session::spawn_worker_loop(active_session, ch, receiver, filesystem, idx).await; + // once any worker finishes/exits, then then the entire session shout be shut down. + finalizer_active_session.destroy().await; + r + })); + } + + Ok(tokio::task::spawn(Session::driver_evt_loop( + active_session, + join_handles, + driver_receiver, + filesystem, + channel, + ))) + } + + /// Run the session loop that receives kernel requests and dispatches them to method + /// calls into the filesystem. This async method will not return until the system is shut down. + pub async fn run(self) -> io::Result<()> { + self.spawn_run().await?.await? } } @@ -136,14 +347,8 @@ fn aligned_sub_buf(buf: &mut [u8], alignment: usize) -> &mut [u8] { impl Session { /// Run the session loop in a background thread - pub fn spawn(self) -> io::Result { - BackgroundSession::new(self) - } -} - -impl Drop for Session { - fn drop(&mut self) { - info!("Unmounted {}", self.mountpoint().display()); + pub async fn spawn(self) -> io::Result { + BackgroundSession::new(self).await } } @@ -154,25 +359,22 @@ pub struct BackgroundSession { /// Thread guard of the background session pub guard: JoinHandle>, fuse_session: *mut libc::c_void, - fd: libc::c_int, + fd: ArcSubChannel, } impl BackgroundSession { /// Create a new background session for the given session by running its /// session loop in a background thread. If the returned handle is dropped, /// the filesystem is unmounted and the given session ends. - pub fn new( + pub async fn new( mut se: Session, ) -> io::Result { let mountpoint = se.mountpoint().to_path_buf(); // Take the fuse_session, so that we can unmount it let fuse_session = se.ch.fuse_session; - let fd = se.ch.fd; + let fd = se.ch.session_fd.clone(); se.ch.fuse_session = ptr::null_mut(); - let guard = thread::spawn(move || { - let mut se = se; - se.run() - }); + let guard = se.spawn_run().await?; Ok(BackgroundSession { mountpoint, guard, @@ -187,7 +389,7 @@ impl Drop for BackgroundSession { info!("Unmounting {}", self.mountpoint.display()); // Unmounting the filesystem will eventually end the session loop, // drop the session and hence end the background thread. - match channel::unmount(&self.mountpoint, self.fuse_session, self.fd) { + match channel::unmount(&self.mountpoint, self.fuse_session, self.fd.as_raw_fd().fd) { Ok(()) => (), Err(err) => error!("Failed to unmount {}: {}", self.mountpoint.display(), err), }