use std::{
ffi::OsString,
path::{Path, PathBuf},
time::{Duration, Instant},
};
const UNMOUNT_DEADLINE: Duration = Duration::from_secs(3);
const POLL_INTERVAL: Duration = Duration::from_millis(50);
#[cfg(target_os = "macos")]
pub(crate) fn mounts_under(root: &Path) -> Vec<PathBuf> {
use std::os::unix::ffi::OsStringExt;
let root = resolved(root);
let count = unsafe { libc::getfsstat(std::ptr::null_mut(), 0, libc::MNT_NOWAIT) };
if count <= 0 {
return Vec::new();
}
let mut entries: Vec<libc::statfs> = Vec::with_capacity(count as usize + 8);
let bytes = (entries.capacity() * size_of::<libc::statfs>()) as libc::c_int;
let written = unsafe { libc::getfsstat(entries.as_mut_ptr(), bytes, libc::MNT_NOWAIT) };
if written <= 0 {
return Vec::new();
}
unsafe { entries.set_len(written as usize) };
entries
.iter()
.filter_map(|entry| {
let name = unsafe { std::ffi::CStr::from_ptr(entry.f_mntonname.as_ptr()) };
let path = PathBuf::from(OsString::from_vec(name.to_bytes().to_vec()));
path.starts_with(&root).then_some(path)
})
.collect()
}
#[cfg(target_os = "linux")]
pub(crate) fn mounts_under(root: &Path) -> Vec<PathBuf> {
use std::os::unix::ffi::OsStringExt;
let root = resolved(root);
let Ok(table) = std::fs::read_to_string("/proc/self/mountinfo") else {
return Vec::new();
};
table
.lines()
.filter_map(|line| line.split_whitespace().nth(4))
.map(|p| PathBuf::from(OsString::from_vec(p.replace("\\040", " ").into_bytes())))
.filter(|p| p.starts_with(&root))
.collect()
}
#[cfg(target_os = "macos")]
const LADDER: [&[&str]; 2] = [&["umount"], &["diskutil", "unmount", "force"]];
#[cfg(target_os = "linux")]
const LADDER: [&[&str]; 2] = [&["umount"], &["umount", "-l"]];
pub(crate) fn unmount_under(root: &Path) -> bool {
for mountpoint in mounts_under(root) {
for rung in LADDER {
let (command, leading) = rung.split_first().expect("every rung names a command");
let spawned = std::process::Command::new(command)
.args(leading)
.arg(&mountpoint)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
if let Ok(mut child) = spawned {
wait_briefly(&mut child);
}
if !mounts_under(&mountpoint).iter().any(|m| m == &mountpoint) {
break;
}
}
}
mounts_under(root).is_empty()
}
fn wait_briefly(child: &mut std::process::Child) {
let deadline = Instant::now() + UNMOUNT_DEADLINE;
loop {
match child.try_wait() {
Ok(Some(_)) => return,
Ok(None) if Instant::now() < deadline => std::thread::sleep(POLL_INTERVAL),
_ => {
let _ = child.kill();
return;
}
}
}
}
pub(crate) fn resolved(path: &Path) -> PathBuf {
let (Some(parent), Some(name)) = (path.parent(), path.file_name()) else {
return path.to_path_buf();
};
match parent.canonicalize() {
Ok(parent) => parent.join(name),
Err(_) => path.to_path_buf(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_table_names_the_root_filesystem() {
assert!(
mounts_under(Path::new("/"))
.iter()
.any(|p| p == Path::new("/")),
"`/` is mounted on every host this runs on"
);
}
#[test]
fn a_directory_with_nothing_mounted_under_it_reports_nothing() {
let dir = std::env::temp_dir().join(format!("virtx-table-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
assert!(mounts_under(&resolved(&dir)).is_empty());
assert!(unmount_under(&resolved(&dir)));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
#[cfg(target_os = "macos")]
fn resolving_a_temp_path_changes_it() {
let raw = std::env::temp_dir().join("virtx-resolve-probe");
assert!(
resolved(&raw).starts_with("/private"),
"macOS `$TMPDIR` resolves under /private; got {}",
resolved(&raw).display()
);
}
}