Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ base64 = "0.22"
[target.'cfg(unix)'.dependencies]
libc = "0.2"

# Linux-only dependencies for FUSE and NFS functionality
# Linux-only dependencies for FUSE and sandbox functionality
[target.'cfg(target_os = "linux")'.dependencies]
fuser = { version = "0.15", default-features = false, features = ["abi-7-29"] }
uuid = { version = "1", features = ["v4"] }
# NFS server for userspace filesystem (used by `agentfs run` and `agentfs serve nfs`)
nfsserve = "0.10"
Expand All @@ -55,6 +54,13 @@ agentfs-sandbox = { path = "../sandbox", optional = true }
reverie = { git = "https://github.com/facebookexperimental/reverie", optional = true }
reverie-ptrace = { git = "https://github.com/facebookexperimental/reverie", optional = true }
reverie-process = { git = "https://github.com/facebookexperimental/reverie", optional = true }
# Vendored fuser dependencies (pure-Rust FUSE implementation)
log = "0.4"
memchr = "2.7"
page_size = "0.6"
smallvec = "1.6"
zerocopy = { version = "0.8", features = ["derive"] }
nix = { version = "0.29", features = ["fs", "user"] }

# macOS dependencies for NFS functionality (no FUSE - uses nfsserve instead)
[target.'cfg(target_os = "macos")'.dependencies]
Expand Down
65 changes: 21 additions & 44 deletions cli/src/cmd/mount.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
use agentfs_sdk::{get_mounts, AgentFSOptions, FileSystem, HostFS, OverlayFS};
use agentfs_sdk::get_mounts;
use anyhow::Result;
use std::{io::Write, os::unix::fs::MetadataExt, path::PathBuf, sync::Arc};
use std::{io::Write, os::unix::fs::MetadataExt, path::PathBuf};

#[cfg(target_os = "linux")]
use std::sync::Arc;

#[cfg(target_os = "linux")]
use agentfs_sdk::{AgentFSOptions, FileSystem, HostFS, OverlayFS};
#[cfg(target_os = "linux")]
use turso::value::Value;

#[cfg(target_os = "linux")]
use crate::{cmd::init::open_agentfs, fuse::FuseMountOptions};

/// Arguments for the mount command.
Expand All @@ -25,21 +33,8 @@ pub struct MountArgs {
}

/// Mount the agent filesystem using FUSE.
#[cfg(target_os = "linux")]
pub fn mount(args: MountArgs) -> Result<()> {
if !supports_fuse() {
#[cfg(target_os = "macos")]
{
anyhow::bail!(
"macFUSE is not installed. Please install it from https://osxfuse.github.io/ \n\
or via Homebrew: brew install --cask macfuse"
);
}
#[cfg(not(target_os = "macos"))]
{
anyhow::bail!("FUSE is not available on this system");
}
}

let opts = AgentFSOptions::resolve(&args.id_or_path)?;

let fsname = format!(
Expand Down Expand Up @@ -135,7 +130,17 @@ pub fn mount(args: MountArgs) -> Result<()> {
}
}

/// Mount the agent filesystem using FUSE (macOS - not supported).
#[cfg(target_os = "macos")]
pub fn mount(_args: MountArgs) -> Result<()> {
anyhow::bail!(
"FUSE mounting is not supported on macOS in this version.\n\
Use `agentfs nfs` to mount via NFS instead."
);
}

/// Check if a path is a mountpoint by comparing device IDs
#[cfg(target_os = "linux")]
fn is_mounted(path: &std::path::Path) -> bool {
let path_meta = match std::fs::metadata(path) {
Ok(m) => m,
Expand All @@ -156,34 +161,6 @@ fn is_mounted(path: &std::path::Path) -> bool {
path_meta.dev() != parent_meta.dev()
}

/// Check if macOS system supports FUSE.
///
/// The `libfuse` dynamic library is weakly linked so that users who don't have
/// macFUSE installed can still run the other commands.
#[cfg(target_os = "macos")]
fn supports_fuse() -> bool {
for lib_name in &[
c"/usr/local/lib/libfuse.2.dylib",
c"/usr/local/lib/libfuse.dylib",
] {
let handle = unsafe { libc::dlopen(lib_name.as_ptr(), libc::RTLD_LAZY) };
if !handle.is_null() {
unsafe { libc::dlclose(handle) };
return true;
}
}
false
}

/// Check if Linux system supports FUSE.
///
/// The `fuser` crate does not even need `libfuse` so technically it always support FUSE.
/// Of course, if FUSE is disabled in the kernel, we'll get an error, but that's life.
#[cfg(target_os = "linux")]
fn supports_fuse() -> bool {
true
}

/// List all currently mounted agentfs filesystems
pub fn list_mounts<W: Write>(out: &mut W) {
let mounts = get_mounts();
Expand Down
10 changes: 5 additions & 5 deletions cli/src/fuse.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use agentfs_sdk::{BoxedFile, FileSystem, FsError, Stats};
use fuser::{
use crate::fuser::{
consts::{
FUSE_ASYNC_READ, FUSE_CACHE_SYMLINKS, FUSE_NO_OPENDIR_SUPPORT, FUSE_PARALLEL_DIROPS,
FUSE_WRITEBACK_CACHE,
Expand All @@ -8,6 +7,7 @@ use fuser::{
ReplyDirectory, ReplyDirectoryPlus, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyStatfs, ReplyWrite,
Request,
};
use agentfs_sdk::{BoxedFile, FileSystem, FsError, Stats};
use parking_lot::Mutex;
use std::{
collections::HashMap,
Expand Down Expand Up @@ -186,8 +186,8 @@ impl Filesystem for AgentFSFuse {
_uid: Option<u32>,
_gid: Option<u32>,
size: Option<u64>,
_atime: Option<fuser::TimeOrNow>,
_mtime: Option<fuser::TimeOrNow>,
_atime: Option<crate::fuser::TimeOrNow>,
_mtime: Option<crate::fuser::TimeOrNow>,
_ctime: Option<SystemTime>,
fh: Option<u64>,
_crtime: Option<SystemTime>,
Expand Down Expand Up @@ -1287,7 +1287,7 @@ pub fn mount(
mount_opts.push(MountOption::AllowRoot);
}

fuser::mount2(fs, &opts.mountpoint, &mount_opts)?;
crate::fuser::mount2(fs, &opts.mountpoint, &mount_opts)?;

Ok(())
}
78 changes: 78 additions & 0 deletions cli/src/fuser/channel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use std::{
fs::File,
io,
os::{
fd::{AsFd, BorrowedFd},
unix::prelude::AsRawFd,
},
sync::Arc,
};

use libc::{c_int, c_void, size_t};

use super::reply::ReplySender;

/// A raw communication channel to the FUSE kernel driver
#[derive(Debug)]
pub struct Channel(Arc<File>);

impl AsFd for Channel {
fn as_fd(&self) -> BorrowedFd<'_> {
self.0.as_fd()
}
}

impl Channel {
/// 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.
pub(crate) fn new(device: Arc<File>) -> Self {
Self(device)
}

/// Receives data up to the capacity of the given buffer (can block).
pub fn receive(&self, buffer: &mut [u8]) -> io::Result<usize> {
let rc = unsafe {
libc::read(
self.0.as_raw_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)
}
}

/// 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 file and use it in other threads.
ChannelSender(self.0.clone())
}
}

#[derive(Clone, Debug)]
pub struct ChannelSender(Arc<File>);

impl ReplySender for ChannelSender {
fn send(&self, bufs: &[io::IoSlice<'_>]) -> io::Result<()> {
let rc = unsafe {
libc::writev(
self.0.as_raw_fd(),
bufs.as_ptr() as *const libc::iovec,
bufs.len() as c_int,
)
};
if rc < 0 {
Err(io::Error::last_os_error())
} else {
debug_assert_eq!(bufs.iter().map(|b| b.len()).sum::<usize>(), rc as usize);
Ok(())
}
}
}
Loading