Integrate routed Git workspaces with persistent state

This commit is contained in:
ms2k 2026-07-18 02:08:14 +09:00
commit 917409fcc6

View file

@ -44,10 +44,21 @@ pub struct MountConfiguration {
pub threads: usize,
}
pub(crate) struct RouterConfiguration {
pub workspace_label: PathBuf,
pub workspace: File,
pub isolated_state: File,
pub mountpoint: PathBuf,
pub global_excludes: Option<PathBuf>,
pub isolate_ignored: bool,
pub threads: usize,
}
pub struct IgnoredWorkspace {
host: BackingRoot,
isolated: BackingRoot,
classifier: GitClassifier,
isolate_ignored: bool,
nodes: Mutex<NodeTable>,
files: Mutex<HashMap<u64, Arc<OpenFile>>>,
directories: Mutex<HashMap<u64, DirectoryStream>>,
@ -55,6 +66,21 @@ pub struct IgnoredWorkspace {
next_handle: AtomicU64,
}
fn mount_config(threads: usize) -> Config {
let mut config = Config::default();
config.mount_options.extend([
MountOption::FSName("lazaret-ignored-workspace".to_owned()),
MountOption::RW,
MountOption::Exec,
MountOption::NoSuid,
MountOption::NoDev,
MountOption::DefaultPermissions,
]);
config.n_threads = Some(threads.max(1));
config.clone_fd = true;
config
}
impl IgnoredWorkspace {
/// # Errors
/// Refuses roots that are not distinct, descriptor-openable directories in a Git workspace.
@ -86,11 +112,41 @@ impl IgnoredWorkspace {
Self::from_roots(workspace_label, host, isolated, global_excludes)
}
/// # Errors
/// Refuses aliased or non-directory authorities and unreadable Git policy inputs.
pub fn from_authorities_with_policy(
workspace_label: PathBuf,
workspace: File,
isolated_state: File,
global_excludes: Option<PathBuf>,
isolate_ignored: bool,
) -> io::Result<Self> {
let host = BackingRoot::from_file(workspace)?;
let isolated = BackingRoot::from_file(isolated_state)?;
Self::from_roots_with_policy(
workspace_label,
host,
isolated,
global_excludes,
isolate_ignored,
)
}
fn from_roots(
workspace_label: PathBuf,
host: BackingRoot,
isolated: BackingRoot,
global_excludes: Option<PathBuf>,
) -> io::Result<Self> {
Self::from_roots_with_policy(workspace_label, host, isolated, global_excludes, true)
}
fn from_roots_with_policy(
workspace_label: PathBuf,
host: BackingRoot,
isolated: BackingRoot,
global_excludes: Option<PathBuf>,
isolate_ignored: bool,
) -> io::Result<Self> {
if host.identity()? == isolated.identity()? {
return Err(io::Error::other("workspace and isolated authorities alias"));
@ -102,6 +158,7 @@ impl IgnoredWorkspace {
host,
isolated,
classifier,
isolate_ignored,
nodes: Mutex::new(NodeTable::new()),
files: Mutex::new(HashMap::new()),
directories: Mutex::new(HashMap::new()),
@ -118,20 +175,36 @@ impl IgnoredWorkspace {
&configuration.isolated_state,
configuration.global_excludes,
)?;
let mut config = Config::default();
config.mount_options.extend([
MountOption::FSName("lazaret-ignored-workspace".to_owned()),
MountOption::RW,
MountOption::Exec,
MountOption::NoSuid,
MountOption::NoDev,
MountOption::DefaultPermissions,
]);
config.n_threads = Some(configuration.threads.max(1));
config.clone_fd = true;
let config = mount_config(configuration.threads);
fuser::mount2(filesystem, configuration.mountpoint, &config)
}
/// # Errors
/// Returns mount, readiness, control-channel, unmount, or daemon I/O failures.
pub(crate) fn serve_from_authorities(
configuration: RouterConfiguration,
ready: impl FnOnce() -> io::Result<()>,
) -> io::Result<()> {
let filesystem = Self::from_authorities_with_policy(
configuration.workspace_label,
configuration.workspace,
configuration.isolated_state,
configuration.global_excludes,
configuration.isolate_ignored,
)?;
let session = fuser::spawn_mount2(
filesystem,
configuration.mountpoint,
&mount_config(configuration.threads),
)?;
if let Err(error) = ready() {
let _ = session.umount_and_join();
return Err(error);
}
std::io::copy(&mut std::io::stdin().lock(), &mut std::io::sink())?;
session.umount_and_join()
}
fn root(&self, backing: Classification) -> &BackingRoot {
match backing {
Classification::Host => &self.host,
@ -171,7 +244,7 @@ impl IgnoredWorkspace {
.as_ref()
.or(host.as_ref())
.is_some_and(|stat| stat.st_mode & libc::S_IFMT == libc::S_IFDIR);
let backing = snapshot.classify(path, directory);
let backing = self.classify(snapshot, path, directory);
if snapshot.is_tracked(path) {
if let (Some(host), Some(isolated)) = (&host, &isolated)
&& host.st_mode & libc::S_IFMT != isolated.st_mode & libc::S_IFMT
@ -200,13 +273,26 @@ impl IgnoredWorkspace {
path: &Path,
directory: bool,
) -> io::Result<Classification> {
let backing = snapshot.classify(path, directory);
let backing = self.classify(snapshot, path, directory);
if backing == Classification::Isolated {
self.isolated.ensure_parent_directories(path)?;
}
Ok(backing)
}
fn classify(
&self,
snapshot: &ClassificationSnapshot,
path: &Path,
directory: bool,
) -> Classification {
if self.isolate_ignored {
snapshot.classify(path, directory)
} else {
Classification::Host
}
}
fn open_file(&self, inode: INodeNo, flags: i32) -> io::Result<u64> {
let path = self.path_for(inode)?;
let snapshot = self.classifier.snapshot_for(&path);
@ -262,7 +348,7 @@ impl IgnoredWorkspace {
.resolve(&snapshot, path)
.and_then(|entry| self.attributes(&entry))
{
Ok(attributes) => reply.entry(&TTL, &attributes, Generation(snapshot.generation())),
Ok(attributes) => reply.entry(&TTL, &attributes, Generation(0)),
Err(error) => reply.error(errno(&error)),
}
}
@ -643,7 +729,8 @@ impl Filesystem for IgnoredWorkspace {
let destination = self.child_path(new_parent, new_name)?;
let snapshot = self.classifier.snapshot_for_paths(&[&source, &destination]);
let source_entry = self.resolve(&snapshot, &source)?;
let destination_backing = snapshot.classify(
let destination_backing = self.classify(
&snapshot,
&destination,
source_entry.stat.st_mode & libc::S_IFMT == libc::S_IFDIR,
);
@ -673,7 +760,7 @@ impl Filesystem for IgnoredWorkspace {
let destination = self.child_path(new_parent, new_name)?;
let snapshot = self.classifier.snapshot_for_paths(&[&source, &destination]);
let source_entry = self.resolve(&snapshot, &source)?;
let destination_backing = snapshot.classify(&destination, false);
let destination_backing = self.classify(&snapshot, &destination, false);
if source_entry.backing != destination_backing {
return Err(io::Error::from_raw_os_error(libc::EXDEV));
}
@ -1105,13 +1192,13 @@ impl Filesystem for IgnoredWorkspace {
access_mode: flags & libc::O_ACCMODE,
}),
);
Ok((attributes, handle, snapshot.generation()))
Ok((attributes, handle))
})();
match result {
Ok((attributes, handle, generation)) => reply.created(
Ok((attributes, handle)) => reply.created(
&TTL,
&attributes,
Generation(generation),
Generation(0),
FileHandle(handle),
FopenFlags::empty(),
),
@ -1552,6 +1639,27 @@ mod tests {
}
}
#[test]
fn disabled_isolation_routes_ignored_paths_to_the_host_backing() {
let fixture = Fixture::new();
repository(&fixture.workspace);
fs::write(fixture.workspace.join(".gitignore"), b"ignored/\n").unwrap();
let filesystem = IgnoredWorkspace::from_authorities_with_policy(
fixture.workspace.clone(),
fs::File::open(&fixture.workspace).unwrap(),
fs::File::open(&fixture.isolated).unwrap(),
None,
false,
)
.unwrap();
let path = std::path::Path::new("ignored/item");
let snapshot = filesystem.classifier.snapshot_for(path);
assert_eq!(
filesystem.classify(&snapshot, path, false),
Classification::Host
);
}
#[test]
#[ignore = "requires a usable host FUSE device"]
fn physical_router_preserves_lock_domains_across_transition() {