diff --git a/.cirrus.yml b/.cirrus.yml index 4109512..563f94f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -24,6 +24,16 @@ task: - . $HOME/.cargo/env << : *BUILD +task: + name: MacOS + macos_instance: + image: ghcr.io/cirruslabs/macos-sonoma-base:latest + setup_script: + - curl https://sh.rustup.rs -o rustup.sh + - sh rustup.sh -y --profile=minimal + - . $HOME/.cargo/env + << : *BUILD + task: name: Linux container: @@ -34,6 +44,7 @@ minver_task: depends_on: - FreeBSD - Linux + - MacOS freebsd_instance: image: freebsd-13-2-release-amd64 setup_script: diff --git a/Cargo.lock b/Cargo.lock index 5ad26de..128a70f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,9 +252,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "77e53693616d3075149f4ead59bdeecd204ac6b8192d8969757601b74bddf00f" [[package]] name = "concurrent-queue" @@ -358,7 +358,7 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fuse3" -version = "0.7.2" +version = "0.7.3" dependencies = [ "async-fs", "async-global-executor", @@ -478,9 +478,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "linux-raw-sys" diff --git a/Cargo.toml b/Cargo.toml index 0ebd4b9..bfb839a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,20 +33,17 @@ bincode = "1.3.3" bytes = "1.5" futures-channel = { version = "0.3.30", features = ["sink"] } futures-util = { version = "0.3.30", features = ["sink"] } -libc = "0.2.155" -nix = { version = "0.29.0", default-features = false, features = ["fs", "mount"] } +libc = "0.2.158" +nix = { version = "0.29.0", default-features = false, features = ["fs", "mount", "user"] } serde = { version = "1.0.196", features = ["derive"] } slab = "0.4.9" tracing = "0.1.40" trait-make = "0.1" which = { version = "6", optional = true } -[target.'cfg(target_os = "linux")'.dependencies] -nix = { version = "0.29.0", default-features = false, features = ["user"] } - [dependencies.tokio] version = "1.36" -features = ["fs", "rt", "sync", "net", "macros", "process"] +features = ["fs", "rt", "sync", "net", "macros", "process", "time"] optional = true [package.metadata.docs.rs] diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 2255b5a..711d6c2 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -25,7 +25,7 @@ path = "src/path_memfs/main.rs" [dependencies] fuse3 = { path = "../", features = ["tokio-runtime", "unprivileged"] } -libc = "0.2.155" +libc = "0.2.158" tokio = { version = "1.36", features = ["macros", "rt", "time", "signal"] } futures-util = "0.3.30" mio = { version = "0.8.11", features = ["os-poll"] } diff --git a/src/helper.rs b/src/helper.rs index 5f1e28c..b87f3cc 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -33,7 +33,10 @@ pub const fn mode_from_kind_and_perm(kind: FileType, perm: u16) -> u32 { // Some platforms like Linux x86_64 have mode_t = u32, and lint warns of a trivial_numeric_casts. // But others like macOS x86_64 have mode_t = u16, requiring a typecast. So, just silence lint. -#[cfg(all(not(target_os = "linux"), target_os = "freebsd"))] +#[cfg(all( + not(target_os = "linux"), + any(target_os = "freebsd", target_os = "macos") +))] #[allow(trivial_numeric_casts)] /// returns the mode for a given file kind and permission pub const fn mode_from_kind_and_perm(kind: FileType, perm: u16) -> u32 { diff --git a/src/lib.rs b/src/lib.rs index 62fc890..0ae03b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,10 +20,10 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use std::io::{self, ErrorKind}; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] -use std::path::PathBuf; +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] +use std::path::{PathBuf, Path}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub use errno::Errno; @@ -35,6 +35,9 @@ use raw::abi::{ FATTR_MODE, FATTR_MTIME, FATTR_MTIME_NOW, FATTR_SIZE, FATTR_UID, }; +#[cfg(target_os = "macos")] +use raw::abi::{FATTR_BKUPTIME, FATTR_CHGTIME, FATTR_CRTIME, FATTR_FLAGS}; + mod errno; mod helper; mod mount_options; @@ -169,6 +172,26 @@ impl From<&fuse_setattr_in> for SetAttr { set_attr.ctime = fsai2ts!(setattr_in.ctime, setattr_in.ctimensec); } + #[cfg(target_os = "macos")] + if setattr_in.valid & FATTR_CRTIME > 0 { + set_attr.ctime = fsai2ts!(setattr_in.crtime, setattr_in.crtimensec); + } + + #[cfg(target_os = "macos")] + if setattr_in.valid & FATTR_CHGTIME > 0 { + set_attr.ctime = fsai2ts!(setattr_in.chgtime, setattr_in.chgtimensec); + } + + #[cfg(target_os = "macos")] + if setattr_in.valid & FATTR_BKUPTIME > 0 { + set_attr.ctime = fsai2ts!(setattr_in.bkuptime, setattr_in.bkuptimensec); + } + + #[cfg(target_os = "macos")] + if setattr_in.valid & FATTR_FLAGS > 0 { + set_attr.flags = Some(setattr_in.flags); + } + set_attr } } @@ -214,3 +237,15 @@ fn find_fusermount3() -> io::Result { ) }) } + +#[cfg(target_os = "macos")] +fn find_macfuse_mount() -> io::Result { + if Path::new("/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse").exists() { + Ok(PathBuf::from("/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse")) + } else { + Err(io::Error::new( + ErrorKind::NotFound, + "macfuse mount binary not found, Please install macfuse first.", + )) + } +} diff --git a/src/mount_options.rs b/src/mount_options.rs index 49ac210..08bc456 100644 --- a/src/mount_options.rs +++ b/src/mount_options.rs @@ -1,10 +1,10 @@ use std::ffi::OsString; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] use std::os::unix::io::RawFd; #[cfg(target_os = "freebsd")] use nix::mount::Nmount; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "macos", target_os = "linux"))] use nix::unistd; /// mount options. @@ -22,7 +22,7 @@ pub struct MountOptions { pub(crate) default_permissions: bool, pub(crate) fs_name: Option, pub(crate) gid: Option, - #[cfg(target_os = "freebsd")] + #[cfg(any(target_os = "macos", target_os = "freebsd"))] pub(crate) intr: bool, #[cfg(target_os = "linux")] pub(crate) nodiratime: bool, @@ -47,7 +47,6 @@ pub struct MountOptions { // Other FUSE mount options // default 40000 - #[cfg(target_os = "linux")] pub(crate) rootmode: Option, } @@ -244,6 +243,30 @@ impl MountOptions { options } + #[cfg(target_os = "macos")] + pub(crate) fn build(&self) -> OsString { + let mut opts = vec![ + String::from("-o fsname=ofs"), + ]; + + if self.allow_root { + opts.push("-o allow_root".to_string()); + } + + if self.allow_other { + opts.push("-o allow_other".to_string()); + } + + let mut options = OsString::from(opts.join(" ")); + + if let Some(custom_options) = &self.custom_options { + options.push(" "); + options.push(custom_options); + } + + options + } + #[cfg(all(target_os = "linux", feature = "unprivileged"))] pub(crate) fn build_with_unprivileged(&self) -> OsString { let mut opts = vec![ @@ -314,6 +337,30 @@ impl MountOptions { flags } + #[cfg(target_os = "macos")] + pub(crate) fn flags(&self) -> nix::mount::MntFlags { + use nix::mount::MntFlags; + + let mut flags = MntFlags::empty(); + if self.noatime { + flags.insert(MntFlags::MNT_NOATIME); + } + if self.noexec { + flags.insert(MntFlags::MNT_NOEXEC); + } + if self.nosuid { + flags.insert(MntFlags::MNT_NOSUID); + } + if self.read_only { + flags.insert(MntFlags::MNT_RDONLY); + } + + if self.sync { + flags.insert(MntFlags::MNT_SYNCHRONOUS); + } + flags + } + #[cfg(target_os = "linux")] pub(crate) fn flags(&self) -> nix::mount::MsFlags { use nix::mount::MsFlags; diff --git a/src/path/reply.rs b/src/path/reply.rs index ba6ef8a..724447a 100644 --- a/src/path/reply.rs +++ b/src/path/reply.rs @@ -56,12 +56,16 @@ impl From<(Inode, FileAttr)> for crate::raw::reply::FileAttr { atime: attr.atime.into(), mtime: attr.mtime.into(), ctime: attr.ctime.into(), + #[cfg(target_os = "macos")] + crtime: attr.crtime.into(), kind: attr.kind, perm: attr.perm, nlink: attr.nlink, uid: attr.uid, gid: attr.gid, rdev: attr.rdev, + #[cfg(target_os = "macos")] + flags: attr.flags, blksize: attr.blksize, } } diff --git a/src/raw/abi.rs b/src/raw/abi.rs index 0e20cea..9f412c0 100644 --- a/src/raw/abi.rs +++ b/src/raw/abi.rs @@ -582,6 +582,11 @@ pub const FUSE_RENAME_IN_SIZE: usize = mem::size_of::(); #[allow(non_camel_case_types)] pub struct fuse_rename_in { pub newdir: u64, + // https://github.com/osxfuse/fuse/blob/master/include/fuse_kernel.h#L448 + #[cfg(target_os = "macos")] + pub flags: u32, + #[cfg(target_os = "macos")] + _padding: u32, } pub const FUSE_RENAME2_IN_SIZE: usize = mem::size_of::(); diff --git a/src/raw/connection/async_io.rs b/src/raw/connection/async_io.rs index 9085c41..01f5221 100644 --- a/src/raw/connection/async_io.rs +++ b/src/raw/connection/async_io.rs @@ -1,22 +1,26 @@ -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] use std::fs::File; use std::fs::OpenOptions; use std::io; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] use std::io::Write; use std::io::{IoSlice, IoSliceMut}; use std::ops::{Deref, DerefMut}; #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", + target_os = "macos" ))] use std::os::fd::OwnedFd; -use std::os::fd::{AsFd, BorrowedFd}; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +use std::os::fd::AsFd; +use std::os::fd::BorrowedFd; +use std::os::fd::FromRawFd; +use std::os::unix::io::AsRawFd; +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use std::os::unix::io::RawFd; use std::pin::pin; use std::sync::Arc; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use std::{ffi::OsString, path::Path}; #[cfg(any( @@ -24,27 +28,28 @@ use std::{ffi::OsString, path::Path}; target_os = "freebsd" ))] use async_io::Async; -use async_lock::Mutex; +use async_lock::{futures, Mutex}; use async_notify::Notify; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use async_process::Command; -use futures_util::{select, FutureExt}; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +use futures_util::{select, FutureExt, try_join, join}; +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use nix::sys::socket::{self, AddressFamily, ControlMessageOwned, MsgFlags, SockFlag, SockType}; #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", + target_os = "macos" ))] use nix::sys::uio; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use tracing::debug; #[cfg(all(target_os = "linux", feature = "unprivileged"))] use crate::find_fusermount3; use crate::raw::connection::CompleteIoResult; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use crate::MountOptions; - +use std::env; #[derive(Debug)] pub struct FuseConnection { unmount_notify: Arc, @@ -52,6 +57,7 @@ pub struct FuseConnection { } impl FuseConnection { + #[cfg(any(target_os = "linux",target_os = "freebsd"))] pub fn new(unmount_notify: Arc) -> io::Result { #[cfg(target_os = "freebsd")] { @@ -89,6 +95,21 @@ impl FuseConnection { }) } + #[cfg(target_os = "macos")] + pub async fn new_with_unprivileged( + mount_options: MountOptions, + mount_path: impl AsRef, + unmount_notify: Arc, + ) -> io::Result { + let connection = + BlockFuseConnection::new_with_unprivileged(mount_options, mount_path).await?; + + Ok(Self { + unmount_notify, + mode: ConnectionMode::Block(connection), + }) + } + pub async fn read_vectored + Send + 'static>( &self, header_buf: Vec, @@ -109,7 +130,7 @@ impl FuseConnection { data_buf: T, ) -> CompleteIoResult<(Vec, T), usize> { match &self.mode { - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "macos"))] ConnectionMode::Block(connection) => { connection.read_vectored(header_buf, data_buf).await } @@ -129,7 +150,7 @@ impl FuseConnection { body_extend_data: Option, ) -> CompleteIoResult<(T, Option), usize> { match &self.mode { - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "macos"))] ConnectionMode::Block(connection) => { connection.write_vectored(data, body_extend_data).await } @@ -146,7 +167,7 @@ impl FuseConnection { #[derive(Debug)] enum ConnectionMode { - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "macos"))] Block(BlockFuseConnection), #[cfg(any( all(target_os = "linux", feature = "unprivileged"), @@ -155,7 +176,7 @@ enum ConnectionMode { NonBlock(NonBlockFuseConnection), } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] #[derive(Debug)] struct BlockFuseConnection { file: File, @@ -163,8 +184,9 @@ struct BlockFuseConnection { write: Mutex<()>, } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] impl BlockFuseConnection { + #[cfg(target_os = "linux")] pub fn new() -> io::Result { const DEV_FUSE: &str = "/dev/fuse"; @@ -177,6 +199,113 @@ impl BlockFuseConnection { }) } + #[cfg(target_os = "macos")] + async fn new_with_unprivileged( + mount_options: MountOptions, + mount_path: impl AsRef, + ) -> io::Result { + use std::{thread, time::Duration}; + + use crate::find_macfuse_mount; + + let (sock0, sock1) = match socket::socketpair( + AddressFamily::Unix, + SockType::Stream, + None, + SockFlag::empty(), + ) { + Err(err) => return Err(err.into()), + + Ok((sock0, sock1)) => (sock0, sock1), + }; + + let binary_path = find_macfuse_mount()?; + + const ENV: &str = "_FUSE_COMMFD"; + + let options = mount_options.build(); + + debug!("mount options {:?}", options); + + let exec_path = match env::current_exe() { + Ok(path) => path, + Err(err) => return Err(err) + }; + + let mount_path = mount_path.as_ref().as_os_str().to_os_string(); + async_global_executor::spawn(async move { + debug!("mount_thread start"); + let fd0 = sock0.as_raw_fd(); + let mut binding = Command::new(binary_path); + let mut child = binding + .env(ENV, fd0.to_string()) + .env("_FUSE_CALL_BY_LIB", "1") + .env("_FUSE_COMMVERS", "2") + .env("_FUSE_DAEMON_PATH", exec_path) + .args(vec![ options, mount_path]) + .spawn()?; + if !child.status().await?.success() { + return Err(io::Error::new( + io::ErrorKind::Other, + "fusermount run failed", + )); + } + Ok(()) + }); + + let fd1 = sock1.as_raw_fd(); + let fd = async_global_executor::spawn_blocking(move || { + debug!("wait_thread start"); + // wait for macfuse mount command start + // it seems that socket::recvmsg will not block to wait for the message + // so we need to sleep for a while + thread::sleep(Duration::from_secs(1)); + // let mut buf = vec![0; 10000]; // buf should large enough + let mut buf = vec![]; // it seems 0 len still works well + + let mut cmsg_buf = nix::cmsg_space!([RawFd; 1]); + + let mut bufs = [IoSliceMut::new(&mut buf)]; + + let msg = match socket::recvmsg::<()>( + fd1, + &mut bufs[..], + Some(&mut cmsg_buf), + MsgFlags::empty(), + ) { + Err(err) => return Err(err.into()), + + Ok(msg) => msg, + }; + + let mut cmsgs = match msg.cmsgs() { + Err(err) => return Err(err.into()), + Ok(cmsgs) => cmsgs, + }; + + let fd = if let Some(ControlMessageOwned::ScmRights(fds)) = cmsgs.next() { + if fds.is_empty() { + return Err(io::Error::new(io::ErrorKind::Other, "no fuse fd")); + } + + fds[0] + } else { + return Err(io::Error::new(io::ErrorKind::Other, "get fuse fd failed")); + }; + + Ok(fd) + }).await.unwrap(); + + // Safety: fd is valid + let file = unsafe { File::from_raw_fd(fd) }; + + Ok(Self { + file, + read: Mutex::new(()), + write: Mutex::new(()), + }) + } + async fn read_vectored + Send + 'static>( &self, mut header_buf: Vec, @@ -401,11 +530,11 @@ impl NonBlockFuseConnection { impl AsFd for FuseConnection { fn as_fd(&self) -> BorrowedFd<'_> { match &self.mode { - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "macos"))] ConnectionMode::Block(connection) => { // Safety: we own the File connection.file.as_fd() - } + }, #[cfg(any( all(target_os = "linux", feature = "unprivileged"), diff --git a/src/raw/connection/tokio.rs b/src/raw/connection/tokio.rs index a818a55..c2a0919 100644 --- a/src/raw/connection/tokio.rs +++ b/src/raw/connection/tokio.rs @@ -1,29 +1,37 @@ -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] use std::fs::File; use std::fs::OpenOptions; use std::io; #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", + target_os = "macos", ))] use std::io::ErrorKind; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::io::Read; +#[cfg(any(target_os = "linux", target_os = "macos"))] use std::io::Write; use std::io::{IoSlice, IoSliceMut}; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", + target_os = "macos", ))] use std::os::fd::OwnedFd; use std::os::fd::{AsFd, BorrowedFd}; -#[cfg(target_os = "freebsd")] +#[cfg(any(target_os = "freebsd", target_os = "macos"))] use std::os::unix::fs::OpenOptionsExt; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use std::os::unix::io::RawFd; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::os::unix::io::{AsRawFd, FromRawFd}; use std::pin::pin; use std::sync::Arc; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use std::{ffi::OsString, path::Path}; use async_notify::Notify; @@ -31,32 +39,36 @@ use futures_util::lock::Mutex; use futures_util::{select, FutureExt}; #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", + target_os = "macos", ))] use nix::sys::uio; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use nix::{ fcntl::{FcntlArg, OFlag}, sys::socket::{self, AddressFamily, ControlMessageOwned, MsgFlags, SockFlag, SockType}, }; #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", + target_os = "macos", ))] -use tokio::io::{unix::AsyncFd, Interest}; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +use tokio::io::unix::AsyncFd; +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use tokio::process::Command; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] use tokio::task; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use tracing::debug; -#[cfg(target_os = "freebsd")] +#[cfg(any(target_os = "freebsd", target_os = "macos"))] use tracing::warn; +use std::env; +use tokio::io::Interest; use super::CompleteIoResult; #[cfg(all(target_os = "linux", feature = "unprivileged"))] use crate::find_fusermount3; -#[cfg(all(target_os = "linux", feature = "unprivileged"))] +#[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] use crate::MountOptions; #[derive(Debug)] @@ -66,6 +78,7 @@ pub struct FuseConnection { } impl FuseConnection { + #[cfg(any(target_os = "linux",target_os = "freebsd"))] pub fn new(unmount_notify: Arc) -> io::Result { #[cfg(target_os = "freebsd")] { @@ -103,6 +116,21 @@ impl FuseConnection { }) } + #[cfg(target_os = "macos")] + pub async fn new_with_unprivileged( + mount_options: MountOptions, + mount_path: impl AsRef, + unmount_notify: Arc, + ) -> io::Result { + let connection = + BlockFuseConnection::new_with_unprivileged(mount_options, mount_path).await?; + + Ok(Self { + unmount_notify, + mode: ConnectionMode::Block(connection), + }) + } + pub async fn read_vectored + Send + 'static>( &self, header_buf: Vec, @@ -123,13 +151,13 @@ impl FuseConnection { data_buf: T, ) -> CompleteIoResult<(Vec, T), usize> { match &self.mode { - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "macos"))] ConnectionMode::Block(connection) => { connection.read_vectored(header_buf, data_buf).await } #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", ))] ConnectionMode::NonBlock(connection) => { connection.read_vectored(header_buf, data_buf).await @@ -143,13 +171,13 @@ impl FuseConnection { body_extend_data: Option, ) -> CompleteIoResult<(T, Option), usize> { match &self.mode { - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "macos"))] ConnectionMode::Block(connection) => { connection.write_vectored(data, body_extend_data).await } #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", ))] ConnectionMode::NonBlock(connection) => { connection.write_vectored(data, body_extend_data).await @@ -160,16 +188,16 @@ impl FuseConnection { #[derive(Debug)] enum ConnectionMode { - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "macos"))] Block(BlockFuseConnection), #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", ))] NonBlock(NonBlockFuseConnection), } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] #[derive(Debug)] struct BlockFuseConnection { file: File, @@ -177,8 +205,9 @@ struct BlockFuseConnection { write: Mutex<()>, } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] impl BlockFuseConnection { + #[cfg(target_os = "linux")] pub fn new() -> io::Result { const DEV_FUSE: &str = "/dev/fuse"; @@ -191,6 +220,115 @@ impl BlockFuseConnection { }) } + #[cfg(target_os = "macos")] + async fn new_with_unprivileged( + mount_options: MountOptions, + mount_path: impl AsRef, + ) -> io::Result { + use std::{thread, time::Duration}; + + use tokio::time::sleep; + use crate::find_macfuse_mount; + + let (sock0, sock1) = match socket::socketpair( + AddressFamily::Unix, + SockType::Stream, + None, + SockFlag::empty(), + ) { + Err(err) => return Err(err.into()), + + Ok((sock0, sock1)) => (sock0, sock1), + }; + + let binary_path = find_macfuse_mount()?; + + const ENV: &str = "_FUSE_COMMFD"; + + let options = mount_options.build(); + + debug!("mount options {:?}", options); + + let exec_path = match env::current_exe() { + Ok(path) => path, + Err(err) => return Err(err) + }; + + let mount_path = mount_path.as_ref().as_os_str().to_os_string(); + // macfuse_mound will block until fuse init done, so we can not join it in the current function + tokio::spawn(async move { + debug!("mount_thread start"); + let fd0 = sock0.as_raw_fd(); + let mut binding = Command::new(binary_path); + let child = binding + .env(ENV, fd0.to_string()) + .env("_FUSE_CALL_BY_LIB", "1") + .env("_FUSE_COMMVERS", "2") + .env("_FUSE_DAEMON_PATH", exec_path) + .args(vec![ options, mount_path]); + let status = child.spawn()?.wait().await?; + + if status.success() { + Ok(()) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "fusermount run failed", + )) + } + }); + + let fd1 = sock1.as_raw_fd(); + // wait for macfuse mount + let fd = task::spawn_blocking(move || { + debug!("wait_thread start"); + // wait for macfuse mount command start + // it seems that socket::recvmsg will not block to wait for the message + // so we need to sleep for a while + thread::sleep(Duration::from_secs(1)); + // let mut buf = vec![0; 10000]; // buf should large enough + let mut buf = vec![]; // it seems 0 len still works well + + let mut cmsg_buf = nix::cmsg_space!([RawFd; 1]); + + let mut bufs = [IoSliceMut::new(&mut buf)]; + + let msg = match socket::recvmsg::<()>( + fd1, + &mut bufs[..], + Some(&mut cmsg_buf), + MsgFlags::empty(), + ) { + Err(err) => return Err(err.into()), + + Ok(msg) => msg, + }; + + let mut cmsgs = match msg.cmsgs() { + Err(err) => return Err(err.into()), + Ok(cmsgs) => cmsgs, + }; + let fd = if let Some(ControlMessageOwned::ScmRights(fds)) = cmsgs.next() { + if fds.is_empty() { + return Err(io::Error::new(ErrorKind::Other, "no fuse fd")); + } + + fds[0] + } else { + return Err(io::Error::new(ErrorKind::Other, "get fuse fd failed")); + }; + + Ok(fd) + }).await.unwrap()?; + + let file = unsafe { File::from_raw_fd(fd) }; + Ok(Self { + file, + read: Mutex::new(()), + write: Mutex::new(()), + }) + } + async fn read_vectored + Send + 'static>( &self, mut header_buf: Vec, @@ -250,7 +388,7 @@ impl BlockFuseConnection { #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", ))] #[derive(Debug)] struct NonBlockFuseConnection { @@ -261,11 +399,12 @@ struct NonBlockFuseConnection { #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", ))] impl NonBlockFuseConnection { - #[cfg(target_os = "freebsd")] + #[cfg(any(target_os = "freebsd", target_os = "macos"))] fn new() -> io::Result { + #[cfg(target_os = "freebsd")] const DEV_FUSE: &str = "/dev/fuse"; match OpenOptions::new() @@ -276,8 +415,9 @@ impl NonBlockFuseConnection { { Err(e) => { if e.kind() == ErrorKind::NotFound { - warn!("Cannot open /dev/fuse. Is the module loaded?"); + warn!("Cannot open {}. Is the module loaded?", DEV_FUSE); } + warn!("Cannot open {}. err: {:?}", DEV_FUSE, e); Err(e) } Ok(file) => Ok(Self { @@ -376,12 +516,13 @@ impl NonBlockFuseConnection { }) } - #[cfg(all(target_os = "linux", feature = "unprivileged"))] + #[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] fn set_fd_non_blocking(fd: RawFd) -> io::Result<()> { let flags = nix::fcntl::fcntl(fd, FcntlArg::F_GETFL).map_err(io::Error::from)?; - + debug!("set fd {:?} to non-blocking", OFlag::from_bits_truncate(flags)); let flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK; + debug!("set fd {:?} to non-blocking", flags); nix::fcntl::fcntl(fd, FcntlArg::F_SETFL(flags)).map_err(io::Error::from)?; Ok(()) @@ -447,12 +588,12 @@ impl NonBlockFuseConnection { impl AsFd for FuseConnection { fn as_fd(&self) -> BorrowedFd<'_> { match &self.mode { - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "macos"))] ConnectionMode::Block(connection) => connection.file.as_fd(), #[cfg(any( all(target_os = "linux", feature = "unprivileged"), - target_os = "freebsd" + target_os = "freebsd", ))] ConnectionMode::NonBlock(connection) => connection.fd.as_fd(), } diff --git a/src/raw/reply.rs b/src/raw/reply.rs index 08b16d2..531dd90 100644 --- a/src/raw/reply.rs +++ b/src/raw/reply.rs @@ -63,15 +63,21 @@ impl From for fuse_attr { atime: attr.atime.sec as u64, mtime: attr.mtime.sec as u64, ctime: attr.ctime.sec as u64, + #[cfg(target_os = "macos")] + crtime: attr.crtime.sec as u64, atimensec: attr.atime.nsec, mtimensec: attr.mtime.nsec, ctimensec: attr.ctime.nsec, + #[cfg(target_os = "macos")] + crtimensec: attr.crtime.nsec, mode: mode_from_kind_and_perm(attr.kind, attr.perm), nlink: attr.nlink, uid: attr.uid, gid: attr.gid, rdev: attr.rdev, blksize: attr.blksize, + #[cfg(target_os = "macos")] + flags: attr.flags, _padding: 0, } } diff --git a/src/raw/session.rs b/src/raw/session.rs index 39db389..bde3e64 100644 --- a/src/raw/session.rs +++ b/src/raw/session.rs @@ -36,7 +36,7 @@ use futures_util::select; use futures_util::sink::{Sink, SinkExt}; use futures_util::stream::StreamExt; use nix::mount; -#[cfg(target_os = "freebsd")] +#[cfg(any(target_os = "freebsd", target_os = "macos"))] use nix::mount::MntFlags; #[cfg(all( target_os = "linux", @@ -109,7 +109,7 @@ struct MountHandleInner { task: JoinHandle>, mount_path: PathBuf, destroy_notify: Arc, - #[cfg(all(target_os = "linux", feature = "unprivileged"))] + #[cfg(any(all(target_os = "linux", feature = "unprivileged"), target_os = "macos"))] unprivileged: bool, } @@ -131,6 +131,14 @@ impl MountHandleInner { .await?; } + #[cfg(target_os = "macos")] + { + task::spawn_blocking(move || { + mount::unmount(&self.mount_path, MntFlags::MNT_SYNCHRONOUS) + }) + .await?; + } + #[cfg(target_os = "linux")] { #[cfg(all(target_os = "linux", feature = "unprivileged"))] @@ -167,6 +175,14 @@ impl MountHandleInner { .await .unwrap()?; } + #[cfg(target_os = "macos")] + { + task::spawn_blocking(move || { + mount::unmount(&self.mount_path, MntFlags::MNT_SYNCHRONOUS) + }) + .await + .unwrap()?; + } #[cfg(target_os = "linux")] { @@ -307,6 +323,40 @@ impl Session { self.mount(fs, mount_path).await } + #[cfg(target_os = "macos")] + pub async fn mount_with_unprivileged>( + mut self, + fs: FS, + mount_path: P, + ) -> IoResult { + let mount_path = mount_path.as_ref(); + + self.mount_empty_check(mount_path).await?; + + let notify = Arc::new(async_notify::Notify::new()); + let fuse_connection = FuseConnection::new_with_unprivileged( + self.mount_options.clone(), + mount_path, + notify.clone(), + ) + .await?; + + self.fuse_connection.replace(Arc::new(fuse_connection)); + + self.filesystem.replace(Arc::new(fs)); + + debug!("mount {:?} success", mount_path); + + Ok(MountHandle { + inner: Some(MountHandleInner { + task: task::spawn(self.inner_mount()), + mount_path: mount_path.to_path_buf(), + destroy_notify: notify, + unprivileged: true, + }), + }) + } + /// mount the filesystem without root permission. #[cfg(all(target_os = "linux", feature = "unprivileged"))] pub async fn mount_with_unprivileged>( @@ -434,6 +484,11 @@ impl Session { }) } + #[cfg(target_os = "macos")] + pub async fn mount>(mut self, fs: FS, mount_path: P) -> IoResult { + self.mount_with_unprivileged(fs, mount_path).await + } + async fn inner_mount(mut self) -> IoResult<()> { let fuse_write_connection = self.fuse_connection.as_ref().unwrap().clone(); @@ -1126,6 +1181,41 @@ impl Session { reply_flags |= FUSE_NO_OPENDIR_SUPPORT; } + #[cfg(target_os = "macos")] + if init_in.flags & FUSE_ALLOCATE > 0 { + debug!("enable FUSE_ALLOCATE"); + + reply_flags |= FUSE_ALLOCATE; + } + + #[cfg(target_os = "macos")] + if init_in.flags & FUSE_EXCHANGE_DATA > 0 { + debug!("enable FUSE_EXCHANGE_DATA"); + + reply_flags |= FUSE_EXCHANGE_DATA; + } + + #[cfg(target_os = "macos")] + if init_in.flags & FUSE_CASE_INSENSITIVE > 0 { + debug!("enable FUSE_CASE_INSENSITIVE"); + + reply_flags |= FUSE_CASE_INSENSITIVE; + } + + #[cfg(target_os = "macos")] + if init_in.flags & FUSE_VOL_RENAME > 0 { + debug!("enable FUSE_VOL_RENAME"); + + reply_flags |= FUSE_VOL_RENAME; + } + + #[cfg(target_os = "macos")] + if init_in.flags & FUSE_XTIMES > 0 { + debug!("enable FUSE_XTIMES"); + + reply_flags |= FUSE_XTIMES; + } + // TODO: pass init_in to init, so the file system will know which flags are in use. let reply = match fs.init(request).await { Err(err) => {