Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ jobs:
target/
key: ${{ runner.os }}-cargo-compile-v1-${{ matrix.libfuse }}-${{ matrix.features }}-${{ hashFiles('**/Cargo.toml', '.github/workflows/*.yml') }}

- name: Configure system policies
id: system-policies
run: |
echo 'user_allow_other' | sudo tee -a /etc/fuse.conf

- name: Install packages
run: |
sudo apt update
Expand All @@ -52,6 +57,8 @@ jobs:
cargo build --target=x86_64-unknown-linux-musl
cargo test --all --features=libfuse,${{ matrix.features }}
cargo test --all --features=${{ matrix.features }}
cargo test --config "target.'cfg(test)'.runner = 'sudo -E'" --all --features=libfuse,${{ matrix.features }}
cargo test --config "target.'cfg(test)'.runner = 'sudo -E'" --all --features=${{ matrix.features }}
cargo doc --all --no-deps --features=${{ matrix.features }}
make test_passthrough

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ bincode = "1.3.1"
serde = { version = "1.0.102", features = ["std", "derive"] }
tempfile = "3.10.1"
nix = { version = "0.30.0", features = ["poll", "fs", "ioctl"] }
test-log = "0.2.19"

[build-dependencies]
pkg-config = "0.3.14"
Expand Down
140 changes: 140 additions & 0 deletions tests/fixtures/hello_fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use std::ffi::OsStr;
use std::path::PathBuf;
use std::time::Duration;
use std::time::UNIX_EPOCH;

use clap::Parser;
use fuser::Errno;
use fuser::FileAttr;
use fuser::FileHandle;
use fuser::FileType;
use fuser::Filesystem;
use fuser::INodeNo;
use fuser::LockOwner;
use fuser::OpenFlags;
use fuser::ReplyAttr;
use fuser::ReplyData;
use fuser::ReplyDirectory;
use fuser::ReplyEntry;
use fuser::Request;

#[derive(Parser)]
#[command(version, author = "Christopher Berner")]
struct Args {
/// Act as a client, and mount FUSE at given path
mount_point: PathBuf,

/// Automatically unmount on process exit
#[clap(long)]
auto_unmount: bool,

/// Allow root user to access filesystem
#[clap(long)]
allow_root: bool,
}

const TTL: Duration = Duration::from_secs(1); // 1 second

const HELLO_DIR_ATTR: FileAttr = FileAttr {
ino: INodeNo::ROOT,
size: 0,
blocks: 0,
atime: UNIX_EPOCH, // 1970-01-01 00:00:00
mtime: UNIX_EPOCH,
ctime: UNIX_EPOCH,
crtime: UNIX_EPOCH,
kind: FileType::Directory,
perm: 0o755,
nlink: 2,
uid: 501,
gid: 20,
rdev: 0,
flags: 0,
blksize: 512,
};

const HELLO_TXT_CONTENT: &str = "Hello World!\n";

const HELLO_TXT_ATTR: FileAttr = FileAttr {
ino: INodeNo(2),
size: 13,
blocks: 1,
atime: UNIX_EPOCH, // 1970-01-01 00:00:00
mtime: UNIX_EPOCH,
ctime: UNIX_EPOCH,
crtime: UNIX_EPOCH,
kind: FileType::RegularFile,
perm: 0o644,
nlink: 1,
uid: 501,
gid: 20,
rdev: 0,
flags: 0,
blksize: 512,
};

pub struct HelloFS;

impl Filesystem for HelloFS {
fn lookup(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) {
if u64::from(parent) == 1 && name.to_str() == Some("hello.txt") {
reply.entry(&TTL, &HELLO_TXT_ATTR, fuser::Generation(0));
} else {
reply.error(Errno::ENOENT);
}
}

fn getattr(&self, _req: &Request, ino: INodeNo, _fh: Option<FileHandle>, reply: ReplyAttr) {
match u64::from(ino) {
1 => reply.attr(&TTL, &HELLO_DIR_ATTR),
2 => reply.attr(&TTL, &HELLO_TXT_ATTR),
_ => reply.error(Errno::ENOENT),
}
}

fn read(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
offset: u64,
_size: u32,
_flags: OpenFlags,
_lock_owner: Option<LockOwner>,
reply: ReplyData,
) {
if u64::from(ino) == 2 {
reply.data(&HELLO_TXT_CONTENT.as_bytes()[offset as usize..]);
} else {
reply.error(Errno::ENOENT);
}
}

fn readdir(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
offset: u64,
mut reply: ReplyDirectory,
) {
if u64::from(ino) != 1 {
reply.error(Errno::ENOENT);
return;
}

let entries = vec![
(1, FileType::Directory, "."),
(1, FileType::Directory, ".."),
(2, FileType::RegularFile, "hello.txt"),
];

for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) {
// i + 1 means the index of the next entry
if reply.add(INodeNo(entry.0), (i + 1) as u64, entry.1, entry.2) {
break;
}
}
reply.ok();
}
}
1 change: 1 addition & 0 deletions tests/fixtures/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod hello_fs;
112 changes: 112 additions & 0 deletions tests/unmount.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
mod fixtures;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the fixtures module used by this integration test

The new test file declares mod fixtures;, which requires either tests/fixtures.rs or tests/fixtures/mod.rs, but this commit does not add either file. That causes the unmount integration test target to fail compilation with an unresolved module error before any test logic can run.

Useful? React with 👍 / 👎.


use std::io::Read;
use std::time::Duration;

use fixtures::hello_fs::HelloFS;
use fuser::Config;
use fuser::MountOption;
use fuser::SessionACL;

#[test_log::test]
fn should_prompt_unmount_retry_while_file_is_open_without_autounmount() {
let mut mountpoint = tempfile::tempdir().unwrap();
mountpoint.disable_cleanup(true);
let mut cfg = Config::default();
cfg.acl = SessionACL::RootAndOwner;
cfg.n_threads = Some(2);
let session = fuser::spawn_mount(HelloFS, &mountpoint, &cfg).unwrap();
let hello_file = mountpoint.path().join("hello.txt");

let (handle_open_done_tx, handle_open_done_rx) = std::sync::mpsc::channel::<()>();
let (umount_completed_tx, unmount_completed_rx) = std::sync::mpsc::channel::<()>();

let main_thread = std::thread::spawn(move || {
// Attempt to unmount while the file is open
handle_open_done_rx.recv().expect("recv handle open done");
// TODO: the outstanding handle must be closed for the unmount to finish, for which the thread owning the outstanding
// handle must receive a message from umount_and_join that it would fail with EBUSY (otherwise, the outstanding
// handle might be closed before an unmount is attempted, which causes the unmount to succeed immediately and
// makes the test flaky). The only way to mitigate the flakiness without non-blocking cooperation from umount_and_join
// is to make the handle thread wait for some time.

// In previous candidate PRs, this is done by creating an interface that does not perform a lazy/detach
// unmount, which returns EBUSY, allowing the thread to send a signal to the thread owning the outstanding
// handle to inform it that it cannot unmount.
session.umount_and_join().expect("unmount should succeed");
umount_completed_tx
.send(())
.expect("send unmount completed");
});
let hello_thread = std::thread::spawn(move || {
let mut file = std::fs::File::open(hello_file).expect("open hello file");
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).expect("read hello file");
// Notify the main thread that the file is opened - the session should try to unmount while the handle is open
handle_open_done_tx.send(()).expect("send handle open done");
// FIXME: this part should have waited for the main thread to send a busy/blocking error
std::thread::sleep(Duration::from_secs_f64(0.25));
drop(file);
});

let res = unmount_completed_rx.recv_timeout(Duration::from_secs(5));
if let Err(e) = res {
let _ = main_thread.join();
let _ = hello_thread.join();
panic!("unmount completed rx error: {:?}", e);
}
main_thread.join().expect("join main thread");
hello_thread.join().expect("join hello thread");
}

#[test_log::test]
fn should_prompt_unmount_retry_while_file_is_open_with_autounmount() {
let mut mountpoint = tempfile::tempdir().unwrap();
mountpoint.disable_cleanup(true);
let mut cfg = Config::default();
cfg.acl = SessionACL::RootAndOwner;
cfg.n_threads = Some(2);
cfg.mount_options.push(MountOption::AutoUnmount);
let session = fuser::spawn_mount(HelloFS, &mountpoint, &cfg).unwrap();
let hello_file = mountpoint.path().join("hello.txt");

let (handle_open_done_tx, handle_open_done_rx) = std::sync::mpsc::channel::<()>();
let (umount_completed_tx, unmount_completed_rx) = std::sync::mpsc::channel::<()>();

let main_thread = std::thread::spawn(move || {
// Attempt to unmount while the file is open
handle_open_done_rx.recv().expect("recv handle open done");
// TODO: the outstanding handle must be closed for the unmount to finish, for which the thread owning the outstanding
// handle must receive a message from umount_and_join that it would fail with EBUSY (otherwise, the outstanding
// handle might be closed before an unmount is attempted, which causes the unmount to succeed immediately and
// makes the test flaky). The only way to mitigate the flakiness without non-blocking cooperation from umount_and_join
// is to make the handle thread wait for some time.

// In previous candidate PRs, this is done by creating an interface that does not perform a lazy/detach
// unmount, which returns EBUSY, allowing the thread to send a signal to the thread owning the outstanding
// handle to inform it that it cannot unmount.
session.umount_and_join().expect("unmount should succeed");
umount_completed_tx
.send(())
.expect("send unmount completed");
});
let hello_thread = std::thread::spawn(move || {
let mut file = std::fs::File::open(hello_file).expect("open hello file");
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).expect("read hello file");
// Notify the main thread that the file is opened - the session should try to unmount while the handle is open
handle_open_done_tx.send(()).expect("send handle open done");
// FIXME: this part should have waited for the main thread to send a busy/blocking error
std::thread::sleep(Duration::from_secs_f64(0.25));
drop(file);
});

let res = unmount_completed_rx.recv_timeout(Duration::from_secs(5));
if let Err(e) = res {
let _ = main_thread.join();
let _ = hello_thread.join();
panic!("unmount completed rx error: {:?}", e);
}
main_thread.join().expect("join main thread");
hello_thread.join().expect("join hello thread");
}
Loading