jj-vfs-poc / Starting and mounting

Starting and mounting

Everything jjfsd does before the first kernel request and after the last one lives in one 81-line file: parse two arguments, load the repository once, hand the file system to fuser, then wait for a signal.

On this page
  1. The sequence
  2. Arguments
  3. Logging
  4. Mount configuration
  5. The tokio runtime
  6. Loading the repository
  7. spawn_mount
  8. Threads after mounting
  9. Signals and unmounting
  10. Limitations

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

StepCallOn failureSource
Loggingtracing_subscriber::fmt().with_env_filter(…).init()Panics if a global subscriber is already set.L12
Argumentsstd::env::args()Prints usage, exits 1.L20
Mount optionsfuser::Config::default(), then mount_optionsCannot fail.L28
Runtimetokio::runtime::Runtime::new(), rt.enter()Panics: "Failed to create tokio runtime".L34
SettingsUserSettings::from_config(StackedConfig::with_defaults())Panics: "Failed to load user settings".L37
WorkspaceWorkspace::load(…)Panics: "Failed to load workspace".L40
Repo at headrt.block_on(repo_loader.load_at_head())Panics: "Failed to load repo at head".L48
File systemAllCommitsPathMapper::new, PathMappedVfs::newCannot fail.L53
Mountfuser::spawn_mount(JjFuse::new(…), mountpoint, &config)Panics: "Failed to mount filesystem".L56
Waitrt.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).

ItemType/SignatureMeaningSource
mount_optionsVec<MountOption>Set to [RO, FSName("jjfs")]; the default is empty.main.rs, fuser
aclSessionACLLeft at the default, Owner: only the user who mounted can use the mount, FUSE's normal mode. No allow_other.session.rs
n_threadsOption<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_fdboolLeft false; it only matters with more than one thread.mount_options.rs
MountOption::ROkernel flagThe 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 optionThe 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

ItemType/SignatureMeaningSource
StackedConfig::with_defaultsfn with_defaults() -> SelfOne 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_configfn 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::loadfn 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_headasync 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

jjfsd's threads once mounted The kernel sends requests through /dev/fuse to fuser's fuser-bg thread, which calls JjFuse. JjFuse spawns each request onto the tokio runtime's worker threads, which call PathMappedVfs and AllCommitsPathMapper over the ReadonlyRepo loaded once at start. The main thread is blocked in wait_for_shutdown; when a signal arrives it returns from main, dropping the BackgroundSession, which unmounts. kernel · /dev/fuse RO mount "jjfs" thread fuser-bg reads requests, calls JjFuse JjFuse rt_handle.spawn per request tokio worker threads PathMappedVfs → AllCommitsPathMapper ReadonlyRepo loaded once, at head main thread · rt.block_on(wait_for_shutdown()) SIGINT / SIGTERM / SIGHUP → return → drop _session → unmount

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.
  • main itself has no test. test_vfs_mount repeats its middle steps, mounting with the same options (named jjfs_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).