mod export;
mod registry;
use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use anyhow::{Context, Result, bail};
use arcbox_connect::v1::EnsureMachineExportRequest;
use arcbox_core::Runtime;
use arcbox_core::event::Event;
use arcbox_core::machine::{MachineInfo, MachineState};
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
use self::export::{host_addresses_on_link, release};
use self::registry::Mounts;
use crate::context::DaemonContext;
use crate::host_mount;
const GUEST_OWNER: (u32, u32) = (0, 0);
static MOUNTED: Mutex<Mounts> = Mutex::new(Mounts::new());
const UNMOUNT_RETRY_INTERVAL: Duration = Duration::from_secs(5);
const UNMOUNT_RETRY_BUDGET: Duration = Duration::from_secs(90);
pub fn spawn(ctx: &DaemonContext, runtime: &Arc<Runtime>) {
let events = runtime.event_bus().subscribe();
let shutdown = ctx.shutdown.clone();
let runtime = Arc::clone(runtime);
let root = ctx.host_mounts.machines();
drop(tokio::spawn(async move {
run(&runtime, &root, events, shutdown).await;
}));
}
pub async fn cleanup() {
let entries = mounted().entries();
for (name, path, generation) in entries {
if unmount_path(&name, &path).await {
mounted().forget(&name, generation);
} else {
mounted().mark_stale(&name, generation);
}
}
}
async fn run(
runtime: &Arc<Runtime>,
root: &Path,
mut events: broadcast::Receiver<Event>,
shutdown: CancellationToken,
) {
sync_all(runtime, root).await;
loop {
tokio::select! {
() = shutdown.cancelled() => break,
event = events.recv() => match event {
Ok(Event::MachineStarted { name }) => sync(runtime, root, &name).await,
Ok(
Event::MachineStopping { name }
| Event::MachineStopped { name }
| Event::MachineRemoved { name },
) => unmount_machine(&name).await,
Ok(_) => {}
Err(RecvError::Lagged(_)) => sync_all(runtime, root).await,
Err(RecvError::Closed) => break,
},
}
}
}
async fn sync(runtime: &Arc<Runtime>, root: &Path, name: &str) {
match runtime.machine_manager().get(name) {
Some(machine) if exports_its_root(&machine) => {
if let Err(e) = mount_machine(runtime, root, &machine).await {
warn!(machine = name, error = %e, "could not mount the machine's root on the host");
}
}
_ => unmount_machine(name).await,
}
}
async fn sync_all(runtime: &Arc<Runtime>, root: &Path) {
let mut names: Vec<String> = runtime
.machine_manager()
.list()
.into_iter()
.map(|machine| machine.name)
.collect();
names.extend(mounted().names());
names.sort_unstable();
names.dedup();
for name in names {
sync(runtime, root, &name).await;
}
}
fn exports_its_root(machine: &MachineInfo) -> bool {
machine.state == MachineState::Running
&& machine.distro.is_some()
&& machine.bridge_ip_address.is_some()
}
pub async fn mount_machine(
runtime: &Arc<Runtime>,
root: &Path,
machine: &MachineInfo,
) -> Result<PathBuf> {
let mount_path = root.join(&machine.name);
let already = mounted()
.get(&machine.name)
.is_some_and(|entry| entry.path == mount_path && !entry.stale);
if already
&& host_mount::current_mount_info(&mount_path).is_some_and(|info| info.is_machine_export())
{
return Ok(mount_path);
}
let bridge: Ipv4Addr = machine
.bridge_ip_address
.as_deref()
.context("machine has no bridge address")?
.parse()
.context("machine bridge address")?;
let clients = host_addresses_on_link(bridge);
if clients.is_empty() {
bail!(
"the host has no address on the machine's bridge network ({bridge}), so it could not reach the export"
);
}
match host_mount::current_mount_info(&mount_path) {
None => {}
Some(info) if info.is_machine_export() => {
info!(path = %mount_path.display(), source = %info.source, "replacing a stale machine mount");
release(&mount_path).await?;
}
Some(info) => bail!(
"mount point {} is occupied by {} ({})",
mount_path.display(),
info.source,
info.fstype
),
}
std::fs::create_dir_all(&mount_path)
.with_context(|| format!("creating {}", mount_path.display()))?;
let request = EnsureMachineExportRequest {
client_addresses: clients.iter().map(ToString::to_string).collect(),
host_uid: unsafe { libc::getuid() },
host_gid: unsafe { libc::getgid() },
guest_uid: GUEST_OWNER.0,
guest_gid: GUEST_OWNER.1,
..Default::default()
};
let endpoint = runtime
.ensure_machine_export(&machine.name, request)
.await
.context("the machine's agent did not start the export")?;
let port = u16::try_from(endpoint.port).context("export port out of range")?;
export::mount(&endpoint.address, port, &mount_path).await?;
let hold = runtime.machine_manager().host_hold(&machine.name);
mounted().record(&machine.name, mount_path.clone(), hold);
info!(
machine = %machine.name,
path = %mount_path.display(),
address = %endpoint.address,
port,
"mounted the machine's root on the host (NFSv3, read-write)"
);
Ok(mount_path)
}
pub async fn unmount_machine(name: &str) {
let Some((path, generation)) = mounted()
.get(name)
.map(|entry| (entry.path.clone(), entry.generation))
else {
return;
};
if unmount_path(name, &path).await {
mounted().forget(name, generation);
return;
}
mounted().mark_stale(name, generation);
drop(tokio::spawn(retry_unmount(
name.to_owned(),
path,
generation,
)));
}
async fn retry_unmount(name: String, path: PathBuf, generation: u64) {
let deadline = tokio::time::Instant::now() + UNMOUNT_RETRY_BUDGET;
loop {
tokio::time::sleep(UNMOUNT_RETRY_INTERVAL).await;
if !mounted().is_current(&name, generation) {
return;
}
if unmount_path(&name, &path).await {
mounted().forget(&name, generation);
return;
}
if tokio::time::Instant::now() >= deadline {
warn!(machine = %name, path = %path.display(), "giving up on unmounting the machine's root");
mounted().forget(&name, generation);
return;
}
}
}
async fn unmount_path(name: &str, path: &Path) -> bool {
match host_mount::current_mount_info(path) {
None => {}
Some(info) if info.is_machine_export() => match release(path).await {
Ok(()) => info!(machine = name, path = %path.display(), "unmounted the machine's root"),
Err(e) => {
warn!(machine = name, path = %path.display(), error = %e, "failed to unmount the machine's root");
return false;
}
},
Some(info) => {
warn!(machine = name, path = %path.display(), source = %info.source, "mount point holds a mount this daemon did not create; leaving it");
return true;
}
}
if let Err(e) = std::fs::remove_dir(path)
&& e.kind() != std::io::ErrorKind::NotFound
{
debug!(path = %path.display(), error = %e, "mount point not removed");
}
true
}
fn mounted() -> std::sync::MutexGuard<'static, Mounts> {
MOUNTED.lock().unwrap_or_else(PoisonError::into_inner)
}