Skip to content

Commit

Permalink
FUSE: unmount if app did not finish cleanly
Browse files Browse the repository at this point in the history
  • Loading branch information
inetic committed Nov 8, 2024
2 parents 9da8a85 + 6a90531 commit 45678fe
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 10 deletions.
2 changes: 1 addition & 1 deletion vfs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ tracing = { workspace = true }
thiserror = { workspace = true }

[target.'cfg(any(target_os = "linux"))'.dependencies]
fuser = "0.14.0"
fuser = "0.15.0"
libc = "0.2.139"
bitflags = "2.6.0"

Expand Down
7 changes: 6 additions & 1 deletion vfs/src/fuse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ pub fn mount(
repository: Arc<Repository>,
mount_point: impl AsRef<Path>,
) -> Result<MountGuard, io::Error> {
// TODO: Would be great if we could use MountOption::AutoUnmount, but the documentation
// say it can't be used without MountOption::AllowOther or MountOption::AllowRoot. However
// the two latter options require modifications to /etc/fuse.conf. It's not clear to me
// whether this is a limitation of fuser or libfuse. Fuser has an open ticket for it here
// https://github.com/cberner/fuser/issues/230
let session = fuser::spawn_mount2(
VirtualFilesystem::new(runtime_handle, repository),
mount_point,
Expand Down Expand Up @@ -159,7 +164,7 @@ impl fuser::Filesystem for VirtualFilesystem {
self.inner.forget(inode, lookups)
}

fn getattr(&mut self, _req: &Request, inode: Inode, reply: ReplyAttr) {
fn getattr(&mut self, _req: &Request, inode: Inode, _fh: Option<u64>, reply: ReplyAttr) {
let attr = try_request!(self.rt.block_on(self.inner.getattr(inode)), reply);
reply.attr(&TTL, &attr)
}
Expand Down
67 changes: 59 additions & 8 deletions vfs/src/fuse/multi_repo_vfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,23 @@ use tokio::runtime::Handle as RuntimeHandle;

pub struct MultiRepoVFS {
runtime_handle: RuntimeHandle,
mount_point: PathBuf,
mount_root: PathBuf,
repositories: Mutex<HashMap<PathBuf, Mount>>,
}

impl MultiRepoMount for MultiRepoVFS {
fn create(
mount_point: impl AsRef<Path>,
mount_root: impl AsRef<Path>,
) -> Pin<Box<dyn Future<Output = Result<Self, MountError>> + Send>> {
Box::pin(future::ready(Ok(Self {
runtime_handle: RuntimeHandle::current(),
mount_point: mount_point.as_ref().to_path_buf(),
mount_root: mount_root.as_ref().to_path_buf(),
repositories: Mutex::new(HashMap::default()),
})))
}

fn insert(&self, store_path: PathBuf, repo: Arc<Repository>) -> Result<(), io::Error> {
let mount_point = extract_mount_point(&store_path)?;
let mount_point = self.mount_point.join(mount_point);

// TODO: should this be async?
fs::create_dir_all(&mount_point)?;
let mount_point = prepare_mountpoint(&store_path, &self.mount_root)?;

let mount_guard = super::mount(self.runtime_handle.clone(), repo, &mount_point)?;

Expand Down Expand Up @@ -80,3 +76,58 @@ fn extract_mount_point(store_path: &Path) -> Result<&OsStr, io::Error> {
)
})
}

// TODO: should this be async?
fn prepare_mountpoint(store_path: &PathBuf, mount_root: &PathBuf) -> Result<PathBuf, io::Error> {
let mount_point = extract_mount_point(&store_path)?;
let mount_point = mount_root.join(mount_point);

let create_dir_error = match fs::create_dir_all(&mount_point) {
Ok(()) => return Ok(mount_point),
Err(error) => error,
};

if create_dir_error.kind() != io::ErrorKind::AlreadyExists {
return Err(create_dir_error);
}

// At this point the `mount_point` exists, now check if we can use it.

let mut read_dir = match mount_point.read_dir() {
Ok(read_dir) => read_dir,
Err(read_dir_error) => {
if read_dir_error.kind() == io::ErrorKind::NotConnected {
// Most likely a previous Ouisync process did not exit cleanly or otherwise failed
// to unmount repositories. So let's try to unmount it first. One disadvantage of
// doing this is that we could accidentally unmount user's directories. However
// MultiRepoVFS mounts everything under `mount_root` which is a dedicated to
// Ouisync, so in practice this shouldn't be a problem.

tracing::warn!(
"Mount point {mount_point:?} is not connected, attempting to unmount it"
);

std::process::Command::new("fusermount")
// Unwrap should be OK given that we're on a POSIX file system.
.args(["-u", mount_point.to_str().unwrap()])
.output()?;

mount_point.read_dir()?
} else {
return Err(read_dir_error);
}
}
};

let dir_is_empty = read_dir.next().is_none();

if !dir_is_empty {
return Err(io::Error::new(
// TODO: io::ErrorKind::DirectoryNotEmpty would have been better, but it's unstable
io::ErrorKind::InvalidInput,
format!("Mount directory {mount_point:?} is not empty"),
));
}

Ok(mount_point)
}

0 comments on commit 45678fe

Please sign in to comment.