In short: jjfsd <MOUNTPOINT> <WORKSPACE_ROOT> sets up logging, builds a
read-only mount configuration named jjfs, starts a tokio runtime, loads the workspace and
the repository at its current operation, and wraps it in the only path mapper there is. It mounts with
fuser::spawn_mount, which serves requests on a background thread, and blocks the main
thread until SIGINT, SIGTERM or SIGHUP. Returning from main drops the session handle, and
that drop is what unmounts. Every failure is a panic, and the repository is never reloaded: the mount
shows the repo as it was when it started.
This page covers src/main.rs, the crate's only binary. What
it builds is described in the namespace,
the virtual file layer and
the FUSE adapter.
The sequence
| Step | Call | On failure | Source |
|---|---|---|---|
| Logging | tracing_subscriber::fmt().with_env_filter(…).init() | Panics if a global subscriber is already set. | L12 |
| Arguments | std::env::args() | Prints usage, exits 1. | L20 |
| Mount options | fuser::Config::default(), then mount_options | Cannot fail. | L28 |
| Runtime | tokio::runtime::Runtime::new(), rt.enter() | Panics: "Failed to create tokio runtime". | L34 |
| Settings | UserSettings::from_config(StackedConfig::with_defaults()) | Panics: "Failed to load user settings". | L37 |
| Workspace | Workspace::load(…) | Panics: "Failed to load workspace". | L40 |
| Repo at head | rt.block_on(repo_loader.load_at_head()) | Panics: "Failed to load repo at head". | L48 |
| File system | AllCommitsPathMapper::new, PathMappedVfs::new | Cannot fail. | L53 |
| Mount | fuser::spawn_mount(JjFuse::new(…), mountpoint, &config) | Panics: "Failed to mount filesystem". | L56 |
| Wait | rt.block_on(wait_for_shutdown()) | Panics if a signal handler can't be installed. | L64, L67 |
Arguments
The arguments are positional and read straight from std::env::args(); there is no
argument-parsing crate and no flags. With fewer than two, it prints
Usage: <program> <MOUNTPOINT> <WORKSPACE_ROOT> to stderr and exits with
status 1. Extra arguments are ignored. The mount point stays a &String; the workspace
root becomes a Path (main.rs). Neither is checked
here: a missing directory surfaces later as a workspace or mount panic.
Logging
tracing_subscriber's formatting subscriber writes to stdout. Its filter comes from the
RUST_LOG environment variable, which EnvFilter::builder() reads by default; the
default directive is INFO, and from_env_lossy skips directives it can't parse
instead of failing (main.rs). The FUSE methods carry
#[tracing::instrument(level = "debug", …)] (for example
fuse.rs), so RUST_LOG=debug logs every kernel request.
The two println! lines, "Mounting JjVfs at …" and "Received …, unmounting...", bypass
tracing.
Mount configuration
fuser::Config is #[non_exhaustive], so it can't be written as a struct literal
outside fuser; main takes the Default and assigns one field
(mount_options.rs).
| Item | Type/Signature | Meaning | Source |
|---|---|---|---|
mount_options | Vec<MountOption> | Set to [RO, FSName("jjfs")]; the default is empty. | main.rs, fuser |
acl | SessionACL | Left at the default, Owner: only the user who mounted can use the mount, FUSE's normal mode. No allow_other. | session.rs |
n_threads | Option<usize> | Left at None: one thread reads kernel requests. JjFuse spawns each request's work onto tokio, so this thread only dispatches. | mount_options.rs, session.rs |
clone_fd | bool | Left false; it only matters with more than one thread. | mount_options.rs |
MountOption::RO | kernel flag | The kernel mounts read-only, so writes fail with EROFS before reaching jjfsd, which implements no write methods anyway. | mount_options.rs, L187 |
MountOption::FSName("jjfs") | fusermount option | The source name in the mount table, in place of /dev/fuse; it's what mount and df show. Note it's jjfs, not the crate's name. | mount_options.rs, fuse_pure.rs |
Not set, and so off: AutoUnmount (fusermount unmounts if the process dies),
DefaultPermissions (the kernel checks mode bits), Subtype, and every other
flag (the full list). JjFuse also
asks for two capabilities in its init callback, FUSE_PARALLEL_DIROPS and
FUSE_ASYNC_READ, ignoring whether the kernel grants them
(fuse.rs).
The tokio runtime
Runtime::new() builds tokio's default multi-threaded runtime (the crate enables
rt-multi-thread, signal, macros and time).
rt.enter() makes it the current runtime for the main thread for as long as
_guard lives, which is all of main. The runtime is used three ways: to block on
loading the repo, to block on the signal wait, and, through the Handle cloned into
JjFuse, to run every FUSE request (main.rs,
fuse.rs; see
the async bridge).
Loading the repository
| Item | Type/Signature | Meaning | Source |
|---|---|---|---|
StackedConfig::with_defaults | fn with_defaults() -> Self | One layer: jj-lib's built-in config/misc.toml. No user config, repo config or environment is read, so ~/.config/jj has no effect on the mount. | config.rs, L910 |
UserSettings::from_config | fn from_config(config: StackedConfig) -> Result<Self, ConfigGetError> | Reads the user name, e-mail, operation host and user, and other settings from that config. | settings.rs |
Workspace::load | fn load(&UserSettings, &Path, &StoreFactories, &WorkingCopyFactories) -> Result<Self, WorkspaceLoadError> | Opens the .jj directory at the workspace root. The default factories are jj-lib's built-in backends (Git included, since jj-lib's default git feature is on) and the local working copy. | workspace.rs, L630, repo.rs |
RepoLoader::load_at_head | async fn load_at_head(&self) -> Result<Arc<ReadonlyRepo>, RepoLoaderError> | Resolves the operation heads to one operation and loads the repo as of it. | repo.rs |
If the operation log has divergent heads, load_at_head merges them in a transaction and
writes the merged operation to the op store without publishing it as a new head
(repo.rs). That's the only way mounting can write to the
repository. jjfsd never snapshots the working copy, so files edited in a workspace but not
yet snapshotted by a jj command don't appear in its @ commit.
The resulting Arc<ReadonlyRepo> goes into
AllCommitsPathMapper::new, the only
PathMapper implementation in the crate, and that into
PathMappedVfs::new. There is no option to choose another
mapper; changing the layout means changing this line (main.rs).
spawn_mount
spawn_mount runs Session::new and then Session::spawn
(lib.rs). Session::new mounts
(session.rs). On Linux, without fuser's
libfuse features, which this crate doesn't enable, fuser uses its pure-Rust mount code
(build.rs): it tries the mount system call first and,
on EPERM (the usual case for a non-root user), runs the setuid fusermount3 or
fusermount helper (fuse_pure.rs,
L493). spawn then starts a thread named
fuser-bg that runs the request loop, and returns a BackgroundSession holding
that thread's handle and the mount (session.rs,
L560). main keeps it in
_session; the underscore keeps it alive (a bare _ would drop it at once).
Threads after mounting
Signals and unmounting
wait_for_shutdown installs tokio handlers for SIGINT, SIGTERM and SIGHUP and waits in
tokio::select! for the first, printing which one arrived
(main.rs). It is #[cfg(unix)] with no other
definition, so the binary only builds on Unix. When it returns, so does main, and locals
drop in reverse order: _session first, while the runtime is still alive.
Dropping the BackgroundSession drops its Mount, whose Drop
unmounts (mod.rs). The pure-Rust unmount skips a mount that
is already gone, calls umount, and on EPERM falls back to
fusermount -u; a failure is only logged as a warning
(fuse_pure.rs). main doesn't call
umount_and_join, so the fuser-bg thread isn't joined; the process just exits.
Without AutoUnmount, nothing unmounts if the process dies without running destructors, as
with SIGKILL or any signal other than the three handled. The mount point is then left for the user to
clear with fusermount -u.
Limitations
- The repository is loaded once. New commits and operations after the mount starts never appear; see the namespace's snapshot.
- Only jj-lib's default config is used, so user and repo settings are ignored.
- Every error is a panic with a fixed message; there are no exit codes beyond the usage error's 1.
- The layout, mount options, thread count and access mode are all fixed in code; there are no flags.
- Unix only, and no automatic unmount if the process is killed.
mainitself has no test.test_vfs_mountrepeats its middle steps, mounting with the same options (namedjjfs_test) over a temporary repo and reading through the mount, then unmounts by dropping the session; it needs FUSE on the machine running the tests (vfs_integration.rs).