use std::{
ffi::OsString,
fs, io,
os::unix::{
ffi::OsStringExt,
fs::{DirBuilderExt, MetadataExt},
},
path::PathBuf,
sync::Mutex,
};
#[cfg(feature = "mount")]
use {
super::table::resolved,
std::{
os::unix::ffi::OsStrExt,
path::Path,
sync::atomic::{AtomicU64, Ordering},
},
};
use super::table::{mounts_under, unmount_under};
const DIR: &str = "virtx-mounts";
const BUDGET: usize = 4;
#[cfg(feature = "mount")]
static NEXT: AtomicU64 = AtomicU64::new(0);
static LIVE: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
pub(crate) fn live() -> Vec<PathBuf> {
LIVE.lock().map(|live| live.clone()).unwrap_or_default()
}
#[cfg(feature = "mount")]
pub(crate) struct Claim {
mountpoint: PathBuf,
record: Option<PathBuf>,
}
#[cfg(feature = "mount")]
pub(crate) fn claim(mountpoint: &Path) -> Claim {
let mountpoint = resolved(mountpoint);
if let Ok(mut live) = LIVE.lock() {
live.push(mountpoint.clone());
}
let record = registry().map(|dir| {
watch(&dir);
let name = format!(
"{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
);
let path = dir.join(name);
let _ = fs::write(&path, mountpoint.as_os_str().as_bytes());
path
});
Claim { mountpoint, record }
}
#[cfg(feature = "mount")]
const WATCHDOG_SCRIPT: &str = r#"
trap '' INT TERM HUP QUIT PIPE
cat >/dev/null
for record in "$VIRTX_MOUNT_REGISTRY/$VIRTX_MOUNT_OWNER"-*; do
[ -f "$record" ] || continue
mountpoint=$(cat "$record") || continue
if [ -z "$mountpoint" ]; then rm -f "$record"; continue; fi
if [ "$(uname)" = Darwin ]; then
umount "$mountpoint" 2>/dev/null || diskutil unmount force "$mountpoint" >/dev/null 2>&1
else
umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null
fi && rm -f "$record"
done
"#;
#[cfg(feature = "mount")]
fn watch(registry: &Path) {
use std::{
os::unix::process::CommandExt,
process::{ChildStdin, Command, Stdio},
};
static WATCHDOG: Mutex<Option<(u32, ChildStdin)>> = Mutex::new(None);
let pid = std::process::id();
let Ok(mut watchdog) = WATCHDOG.lock() else {
return;
};
if watchdog.as_ref().is_some_and(|(owner, _)| *owner == pid) {
return;
}
let spawned = Command::new("/bin/sh")
.args(["-c", WATCHDOG_SCRIPT, "virtx-mount-watchdog"])
.env("VIRTX_MOUNT_OWNER", pid.to_string())
.env("VIRTX_MOUNT_REGISTRY", registry)
.current_dir("/")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.process_group(0)
.spawn();
if let Ok(mut child) = spawned
&& let Some(stdin) = child.stdin.take()
{
*watchdog = Some((pid, stdin));
}
}
#[cfg(feature = "mount")]
impl Drop for Claim {
fn drop(&mut self) {
if let Ok(mut live) = LIVE.lock()
&& let Some(at) = live.iter().position(|p| p == &self.mountpoint)
{
live.swap_remove(at);
}
if let Some(record) = &self.record
&& mounts_under(&self.mountpoint).is_empty()
{
let _ = fs::remove_file(record);
}
}
}
fn registry() -> Option<PathBuf> {
let dir = std::env::temp_dir().join(DIR);
fs::DirBuilder::new()
.mode(0o700)
.recursive(true)
.create(&dir)
.ok()?;
let meta = fs::metadata(&dir).ok()?;
if meta.uid() != unsafe { libc::geteuid() } {
return None;
}
if meta.mode() & 0o022 != 0 {
return None;
}
Some(dir)
}
fn gone(pid: libc::pid_t) -> bool {
if pid <= 0 {
return false;
}
let answer = unsafe { libc::kill(pid, 0) };
answer == -1 && io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
}
pub fn reclaim_abandoned() -> Vec<PathBuf> {
let mut left = Vec::new();
let Some(dir) = registry() else {
return left;
};
let Ok(entries) = fs::read_dir(&dir) else {
return left;
};
let mut budget = BUDGET;
for entry in entries.flatten() {
let record = entry.path();
let named = entry
.file_name()
.to_str()
.and_then(|name| name.split('-').next()?.parse::<libc::pid_t>().ok());
let Some(pid) = named.filter(|pid| *pid > 0) else {
let _ = fs::remove_file(&record);
continue;
};
if !gone(pid) {
continue;
}
let Ok(bytes) = fs::read(&record) else {
let _ = fs::remove_file(&record);
continue;
};
if bytes.is_empty() {
let _ = fs::remove_file(&record);
continue;
}
let mountpoint = PathBuf::from(OsString::from_vec(bytes));
if mounts_under(&mountpoint).is_empty() {
let _ = fs::remove_file(&record);
continue;
}
if budget == 0 {
break;
}
budget -= 1;
if unmount_under(&mountpoint) {
let _ = fs::remove_file(&record);
} else {
left.push(mountpoint);
}
}
left
}
#[cfg(all(test, feature = "mount"))]
mod tests {
use super::*;
#[test]
fn a_claim_lasts_exactly_as_long_as_it_is_held() {
let path = std::env::temp_dir().join("virtx-claim-probe");
let resolved = resolved(&path);
let held = claim(&path);
assert!(live().contains(&resolved), "the register takes the path");
let record = held.record.clone().expect("a record was written");
assert!(
record.exists(),
"the record is on disk while the claim is held"
);
drop(held);
assert!(!live().contains(&resolved), "the register lets the path go");
assert!(!record.exists(), "the record goes with the claim");
}
#[test]
fn a_live_process_keeps_its_own_records() {
let path = std::env::temp_dir().join("virtx-claim-live-probe");
let held = claim(&path);
let record = held.record.clone().expect("a record was written");
reclaim_abandoned();
assert!(
record.exists(),
"a record whose owner is running is not somebody else's to clear"
);
drop(held);
}
#[test]
#[ignore = "mounts a real filesystem"]
fn a_record_outlives_a_claim_dropped_over_a_live_mount() {
#[cfg(not(target_os = "macos"))]
use crate::fs::FuseMount as HostMount;
#[cfg(target_os = "macos")]
use crate::fs::FuseTMount as HostMount;
use crate::fs::{FileSystem, InMemFs};
let path =
std::env::temp_dir().join(format!("virtx-claim-live-mount-{}", std::process::id()));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(&path).expect("temp dir is writable");
let volume = InMemFs::new();
let rt = tokio::runtime::Runtime::new().expect("a runtime to seed the volume");
rt.block_on(async { volume.create(Path::new("greeting.txt")).await })
.expect("fresh store");
let mount = HostMount::try_new(volume, &path).expect("the volume mounts");
let held = claim(&path);
let record = held.record.clone().expect("a record was written");
drop(held);
assert!(
record.exists(),
"a record whose mount is still up is what a later run reclaims"
);
drop(mount);
let _ = fs::remove_file(&record);
let _ = fs::remove_dir_all(&path);
}
#[test]
fn a_record_from_a_dead_owner_is_cleared() {
let Some(dir) = registry() else {
panic!("the registry is usable under $TMPDIR");
};
let record = dir.join(format!("{}-virtx-test", libc::pid_t::MAX));
let mountpoint = std::env::temp_dir().join("virtx-claim-dead-probe");
fs::create_dir_all(&mountpoint).unwrap();
fs::write(&record, mountpoint.as_os_str().as_bytes()).unwrap();
reclaim_abandoned();
assert!(!record.exists(), "an abandoned record is cleared");
fs::remove_dir_all(&mountpoint).ok();
}
}