Preserve open inode identity across workspace renames

This commit is contained in:
ms2k 2026-08-03 01:06:54 +09:00
commit 982395bb28
4 changed files with 77 additions and 14 deletions

View file

@ -346,6 +346,7 @@ impl IgnoredWorkspace {
handle,
Arc::new(OpenFile {
file,
inode,
backing: entry.backing,
path,
access_mode: flags & libc::O_ACCMODE,
@ -448,9 +449,12 @@ impl IgnoredWorkspace {
}
fn attributes(&self, entry: &ResolvedEntry) -> io::Result<FileAttr> {
let stat = entry.stat;
self.attributes_for_inode(self.inode_for(entry), entry.stat)
}
fn attributes_for_inode(&self, inode: INodeNo, stat: rustix::fs::Stat) -> io::Result<FileAttr> {
Ok(FileAttr {
ino: self.inode_for(entry),
ino: inode,
size: u64::try_from(stat.st_size)
.map_err(|_| io::Error::other("negative file size"))?,
blocks: u64::try_from(stat.st_blocks)
@ -485,6 +489,15 @@ impl IgnoredWorkspace {
operation(&open)
}
fn open_file_for_inode(&self, inode: INodeNo) -> Option<Arc<OpenFile>> {
self.files
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.values()
.find(|open| open.inode == inode)
.cloned()
}
fn posix_lock_file(&self, open: &OpenFile, owner: LockOwner) -> io::Result<Arc<File>> {
let stat = fstat(open.file.as_fd())?;
let key = PosixLockKey {

View file

@ -70,6 +70,7 @@ pub(super) struct ResolvedEntry {
pub(super) struct OpenFile {
pub(super) file: File,
pub(super) inode: INodeNo,
pub(super) backing: Classification,
pub(super) path: PathBuf,
pub(super) access_mode: i32,

View file

@ -57,13 +57,12 @@ impl Filesystem for IgnoredWorkspace {
let result = if let Some(handle) = handle {
self.with_file(handle, |open| {
let stat = fstat(open.file.as_fd())?;
let path = self.path_for(inode)?;
self.attributes(&ResolvedEntry {
path,
backing: open.backing,
stat,
})
self.attributes_for_inode(inode, stat)
})
} else if let Some(open) = self.open_file_for_inode(inode) {
fstat(open.file.as_fd())
.map_err(Into::into)
.and_then(|stat| self.attributes_for_inode(inode, stat))
} else {
self.path_for(inode).and_then(|path| {
let snapshot = self.classifier.snapshot_for(&path);
@ -124,12 +123,7 @@ impl Filesystem for IgnoredWorkspace {
)?;
}
self.record_changed(open.backing, &open.path);
let path = self.path_for(inode)?;
self.attributes(&ResolvedEntry {
path,
backing: open.backing,
stat: fstat(open.file.as_fd())?,
})
self.attributes_for_inode(inode, fstat(open.file.as_fd())?)
};
let result = if let Some(handle) = handle {
self.with_file(handle, operation)
@ -914,6 +908,7 @@ impl Filesystem for IgnoredWorkspace {
handle,
Arc::new(OpenFile {
file,
inode: attributes.ino,
backing,
path,
access_mode: flags & libc::O_ACCMODE,

View file

@ -32,6 +32,7 @@ fn posix_lock_owners_use_distinct_backing_lock_domains() {
.write(true)
.open(path)
.unwrap(),
inode: fuser::INodeNo::ROOT,
backing: Classification::Host,
path: std::path::PathBuf::from("ordinary"),
access_mode: libc::O_RDWR,
@ -566,6 +567,59 @@ fn physical_router_replaces_isolated_regular_file_only_toward_isolated_state() {
session.umount_and_join().unwrap();
}
#[test]
#[ignore = "requires a usable host FUSE device"]
fn physical_router_preserves_open_files_across_same_backing_renames() {
let fixture = Fixture::new();
repository(&fixture.workspace);
let filesystem = IgnoredWorkspace::open(&fixture.workspace, &fixture.isolated, None).unwrap();
let mut config = Config::default();
config.mount_options.extend([
MountOption::RW,
MountOption::DefaultPermissions,
MountOption::NoDev,
MountOption::NoSuid,
]);
let session = fuser::spawn_mount2(filesystem, &fixture.mountpoint, &config).unwrap();
fs::write(fixture.mountpoint.join("target"), b"old").unwrap();
let old = OpenOptions::new()
.read(true)
.write(true)
.open(fixture.mountpoint.join("target"))
.unwrap();
fs::write(fixture.mountpoint.join("replacement"), b"replacement").unwrap();
let replacement = OpenOptions::new()
.read(true)
.write(true)
.open(fixture.mountpoint.join("replacement"))
.unwrap();
fs::rename(
fixture.mountpoint.join("target"),
fixture.mountpoint.join("backup"),
)
.unwrap();
fs::rename(
fixture.mountpoint.join("replacement"),
fixture.mountpoint.join("target"),
)
.unwrap();
fs::remove_file(fixture.mountpoint.join("backup")).unwrap();
assert_eq!(old.metadata().unwrap().len(), 3);
assert_eq!(replacement.metadata().unwrap().len(), 11);
assert_eq!(
fs::read(fixture.mountpoint.join("target")).unwrap(),
b"replacement"
);
drop(old);
drop(replacement);
session.umount_and_join().unwrap();
}
fn repository(root: &std::path::Path) {
fs::create_dir_all(root.join(".git/info")).unwrap();
fs::create_dir_all(root.join(".git/hooks")).unwrap();