In short: fuser calls a JjFuse method on its session thread. The method clones
what it needs, and the reply_async! macro spawns a task on the tokio runtime and returns
straight away. The task resolves the inode to a path with InodeMap::get_path, awaits the
matching VirtualFilesystem call, and replies. Errors become errnos: ENOENT,
ENOTDIR, EISDIR, EINVAL, or EIO for everything else.
Every method JjFuse doesn't implement falls back to fuser's default. The mount
is read-only (MountOption::RO).
This page covers src/fuse.rs and
src/jj_error.rs. The layer beneath,
VirtualFilesystem and PathMappedVfs, is on
vfs-layer. How inode numbers are handed out is on
inodes.
JjFuse
JjFuse<FS: VirtualFilesystem> holds three
things. fs is the virtual file system behind an Arc. inode_map
is an Arc<InodeMap>, created fresh in new, so inode numbers start over at
every mount. rt_handle is a tokio::runtime::Handle. main.rs passes
the runtime's handle and an Arc of the PathMappedVfs
(source).
Implemented Filesystem methods
| Method | What it does | VirtualFilesystem call | Reply | Source |
|---|---|---|---|---|
init | Asks the kernel for FUSE_PARALLEL_DIROPS and FUSE_ASYNC_READ, ignoring the result. Runs synchronously. | — | Ok(()) | L70 |
lookup | Rejects non-UTF-8 names (InvalidPath), assigns or reuses the child's inode with get_ino(parent, name), appends the name to the parent's path, and fetches attributes. | get_attributes(parent/name) | reply.entry(TTL, attr, Generation(0)) | L78 |
getattr | Attributes for an inode's path. The file handle is ignored. | get_attributes(path) | reply.attr(TTL, attr) | L98 |
read | Reads size bytes at offset. The file handle, flags and lock owner are ignored. | read(path, offset, size) | reply.data(bytes) | L110 |
readdir | Emits . (offset 1) and .. (offset 2), then the listing with offsets 3, 4, …, skipping entries already sent. It assigns each child an inode, and stops when the reply buffer is full. | read_directory(path) | reply.ok() | L132 |
readlink | The symlink target's raw bytes. | read_link(path) | reply.data(bytes) | L176 |
The list above is FUSE_OPS in jj_vfs_docs.py, which is refreshed whenever the
pin moves. readdir lists the whole directory on every call and then skips to
offset, so a large directory read in several batches is listed several times. It also
answers .. of the root with the root itself, since InodeMap::new records the root as its own parent
(source, get_parent_ino).
Methods left to fuser's defaults
Everything else uses the default bodies in fuser 0.18.0's Filesystem trait
(source). The ones a reader of this mount actually hits:
| Method | Default behaviour | Effect here | Source |
|---|---|---|---|
open, opendir | Reply opened(FileHandle(0), empty flags). | Opening always succeeds with handle 0. read and readdir ignore the handle, so this is stateless I/O. | L576, L706 |
release, releasedir | Reply ok. | Closing always succeeds. | L671, L748 |
forget, batch_forget | Do nothing. | Inodes are never freed (see namespace). | L425 |
statfs | Reply all zeros, with block size 512 and a 255-byte name limit. | df shows an empty file system. | L776 |
flush, fsync, fsyncdir, access, readdirplus, xattrs, locks and the rest | Log [Not Implemented] and reply ENOSYS. | Callers get "function not implemented". | L651 |
setattr, mknod, mkdir, unlink, write, create, … | ENOSYS as above. | The mount is RO (source), so writes are refused before they reach the file system. | L442 |
The async bridge and reply_async!
fuser's methods are synchronous and take &self, while the
VirtualFilesystem is async. reply_async!
bridges the two. Its arguments are self, the reply object, the inode, a
path => async body pair and a value => success body pair. It then:
- clones the inode map and runtime handle, and
spawns a task on the runtime. The FUSE method returns immediately, and the reply object moves into the task. - In the task, resolves the inode with
inode_map.get_path(ino). On failure it logs aterrorlevel and replies with the errno. - Awaits the body with the path bound mutably (
lookuppushes the child's name onto it). - On
Ok, runs the success body, which sends the reply. OnErr, logs atdebuglevel with the path, and replies witherr.into().
Because replies are sent from tokio tasks, several requests can be in flight at once.
init requests FUSE_ASYNC_READ and FUSE_PARALLEL_DIROPS so the kernel
will send them that way (source). Every method also carries
#[tracing::instrument(level = "debug")].
One read, end to end
How PathMappedVfs::read serves an offset, by reading and discarding the first
offset bytes every time, is on vfs-layer
(source). Before any read, the kernel has already
looked the path up and opened it with fuser's default open.
FileAttributes → FileAttr
FileAttributes has four fields: size,
file_type, created and modified.
to_fuse(ino) fills in every FileAttr
field:
FileAttr field | Value |
|---|---|
ino | The inode passed in. |
size | size |
blocks | size.div_ceil(512) |
atime | SystemTime::now(), with a TODO: properly set timestamps. |
mtime, ctime | modified |
crtime | created |
kind | file_type converted as below. |
perm | 0o755 for directories, 0o644 otherwise, including symlinks. Nothing is ever executable. |
nlink | 2 for directories, 1 otherwise. |
uid, gid | Hard-coded 1000, whoever mounted it. |
rdev, flags | 0 |
blksize | 4096 |
Both entry and attribute replies use TTL = 1 s (source), so
the kernel re-asks after a second. lookup always reports generation 0.
FileType conversion
virtual_file::FileType | fuser::FileType |
|---|---|
File | RegularFile |
Directory | Directory |
Symlink | Symlink |
impl From<FileType> for fuser::FileType
JjError → errno
JjError variant | Message | errno |
|---|---|---|
NotFound | Not found | ENOENT |
NotADirectory | Expected a directory | ENOTDIR |
NotAFile | Expected a file | EISDIR |
NotASymlink | Expected a symlink | EINVAL |
InvalidPath | Invalid path | EIO |
IO(std::io::Error) | the I/O error's text | EIO |
JjLibBackendError(BackendError) | Underlying jj-lib error: … | EIO |
(enum, mapping). The fallback
arm sends every other variant to EIO, including InvalidPath, so a non-UTF-8
name in lookup reports an I/O error rather than "no such file". It also discards the
wrapped I/O error's own errno.