use std::path::Path;
use anyhow::{Context, Result, bail};
use arcbox_constants::paths::HostMountLayout;
use tracing::{debug, info, warn};
use crate::context::DaemonContext;
use crate::host_mount::{self, MountInfo};
pub(super) async fn prepare(ctx: &DaemonContext) -> Result<()> {
let layout = &ctx.host_mounts;
release_stale(layout).await;
if let Some(info) = host_mount::current_mount_info(layout.root()) {
bail!(
"the host mount root {} is occupied by {} ({}); unmount it or point {} elsewhere",
layout.root().display(),
info.source,
info.fstype,
arcbox_constants::env::HOST_MOUNT_DIR
);
}
let mut dirs = vec![layout.machines()];
if ctx.mount_nfs {
dirs.push(layout.docker());
}
for dir in dirs {
std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
}
Ok(())
}
async fn release_stale(layout: &HostMountLayout) {
let root = layout.root();
let mut stale = host_mount::mounts_under(root);
if let Some(info) = host_mount::current_mount_info(root) {
stale.push((root.to_path_buf(), info));
}
for (path, info) in stale {
if info.is_docker_export() || info.is_machine_export() {
release(&path, &info).await;
} else {
warn!(
path = %path.display(),
source = %info.source,
fstype = %info.fstype,
"a mount this daemon did not create sits under the host mount root; leaving it"
);
}
}
if host_mount::current_mount_info(root).is_none() {
remove_empty_mount_points(&layout.machines());
}
let legacy = layout.legacy_machines_root();
if !legacy.is_dir() {
return;
}
for (path, info) in host_mount::mounts_under(&legacy) {
if info.is_machine_export() {
release(&path, &info).await;
}
}
remove_empty_mount_points(&legacy);
match std::fs::remove_dir(&legacy) {
Ok(()) => info!(
path = %legacy.display(),
"removed the machine mount root of a daemon from before the single host mount root"
),
Err(e) => warn!(
path = %legacy.display(),
error = %e,
"the pre-single-root machine mount root is not empty; leaving it"
),
}
}
async fn release(path: &Path, info: &MountInfo) {
match host_mount::unmount_force(path).await {
Ok(()) => info!(
path = %path.display(),
source = %info.source,
"released a mount a previous daemon left behind"
),
Err(e) => warn!(
path = %path.display(),
source = %info.source,
error = %e,
"could not release a mount a previous daemon left behind"
),
}
}
fn remove_empty_mount_points(dir: &Path) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
if !entry.file_type().is_ok_and(|kind| kind.is_dir()) {
continue;
}
let path = entry.path();
match std::fs::remove_dir(&path) {
Ok(()) => info!(path = %path.display(), "removed a stale mount point"),
Err(e) => debug!(path = %path.display(), error = %e, "mount point not removed"),
}
}
}
#[cfg(test)]
mod tests {
use arcbox_constants::paths::HostMountLayout;
use super::{release_stale, remove_empty_mount_points};
#[tokio::test]
async fn stale_mount_points_are_swept_and_the_legacy_root_removed() {
let dir = tempfile::tempdir().unwrap();
let layout = HostMountLayout::new(dir.path().join("ArcBox"));
std::fs::create_dir_all(layout.machine("gone")).unwrap();
std::fs::create_dir_all(layout.machine("kept")).unwrap();
std::fs::write(layout.machine("kept").join("notes"), "the user's").unwrap();
std::fs::write(layout.machines().join(".DS_Store"), "").unwrap();
let legacy = layout.legacy_machines_root();
std::fs::create_dir_all(legacy.join("ubuntu")).unwrap();
assert_eq!(legacy, dir.path().join("ArcBoxMachines"));
release_stale(&layout).await;
assert!(!layout.machine("gone").exists());
assert!(layout.machine("kept").join("notes").exists());
assert!(layout.machines().join(".DS_Store").exists());
assert!(!legacy.exists());
}
#[tokio::test]
async fn a_legacy_root_with_the_users_files_stays() {
let dir = tempfile::tempdir().unwrap();
let layout = HostMountLayout::new(dir.path().join("ArcBox"));
let legacy = layout.legacy_machines_root();
std::fs::create_dir_all(legacy.join("ubuntu")).unwrap();
std::fs::write(legacy.join("ubuntu/readme"), "kept").unwrap();
release_stale(&layout).await;
assert!(legacy.join("ubuntu/readme").exists());
}
#[test]
fn sweeping_a_missing_directory_is_a_no_op() {
remove_empty_mount_points(std::path::Path::new("/nonexistent/arcbox-test"));
}
}