use std::os::fd::AsRawFd as _;
use std::path::Path;
use arcbox_connect::v1::DiskTrimResponse;
use arcbox_constants::cmdline::MACHINE_DATA_KEY;
use super::btrfs::BTRFS_TEMP_MOUNT;
use super::cmdline::cmdline_value;
use super::metadata_volume::METADATA_MOUNT;
use crate::agent::Guest;
use crate::rpc::{ErrorResponse, RpcResponse};
const MACHINE_TRIM_MOUNT: &str = "/run/arcbox/trim";
#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
struct FstrimRange {
start: u64,
len: u64,
minlen: u64,
}
nix::ioctl_readwrite!(fitrim, b'X', 121, FstrimRange);
pub(super) async fn handle_disk_trim(guest: Guest) -> RpcResponse {
let (tx, rx) = tokio::sync::oneshot::channel();
let spawned = std::thread::Builder::new()
.name("disk-trim".into())
.spawn(move || {
let _ = tx.send(trim_guest(guest));
});
let outcome = match spawned {
Ok(_) => rx
.await
.map_err(|_| "disk trim thread exited without a result".to_owned()),
Err(e) => Err(format!("spawn disk trim thread: {e}")),
};
match outcome.and_then(|trimmed| trimmed) {
Ok(trimmed) => RpcResponse::DiskTrim(DiskTrimResponse {
result: trimmed.summary(),
bytes_trimmed: trimmed.bytes,
..Default::default()
}),
Err(reason) => RpcResponse::Error(ErrorResponse::new(
500,
format!("disk trim failed: {reason}"),
)),
}
}
struct Trimmed {
per_fs: Vec<(String, u64)>,
bytes: u64,
}
impl Trimmed {
fn summary(&self) -> String {
self.per_fs
.iter()
.map(|(name, bytes)| format!("{name}: {bytes} bytes trimmed"))
.collect::<Vec<_>>()
.join("; ")
}
}
fn trim_guest(guest: Guest) -> Result<Trimmed, String> {
match guest {
Guest::SystemVm => trim_mounts(&[BTRFS_TEMP_MOUNT, METADATA_MOUNT]),
Guest::DistroMachine => trim_machine_data_disk(),
}
}
fn trim_mounts(mounts: &[&str]) -> Result<Trimmed, String> {
let mut per_fs = Vec::new();
for mount in mounts {
if !crate::mount::is_mounted(mount) {
tracing::debug!(mount, "not mounted; nothing to trim");
continue;
}
let bytes = trim_mount(Path::new(mount)).map_err(|e| format!("{mount}: {e}"))?;
tracing::info!(mount, bytes, "trimmed");
per_fs.push(((*mount).to_owned(), bytes));
}
let bytes = per_fs.iter().map(|(_, b)| *b).sum();
Ok(Trimmed { per_fs, bytes })
}
fn trim_machine_data_disk() -> Result<Trimmed, String> {
let device = cmdline_value(MACHINE_DATA_KEY)
.ok_or_else(|| format!("{MACHINE_DATA_KEY} missing from the kernel cmdline"))?;
enter_private_mount_namespace()?;
std::fs::create_dir_all(MACHINE_TRIM_MOUNT)
.map_err(|e| format!("create {MACHINE_TRIM_MOUNT}: {e}"))?;
nix::mount::mount(
Some(device.as_str()),
MACHINE_TRIM_MOUNT,
Some("btrfs"),
nix::mount::MsFlags::empty(),
None::<&str>,
)
.map_err(|e| format!("mount {device} on {MACHINE_TRIM_MOUNT}: {e}"))?;
let bytes = trim_mount(Path::new(MACHINE_TRIM_MOUNT)).map_err(|e| format!("{device}: {e}"))?;
tracing::info!(device, bytes, "trimmed the machine data disk");
Ok(Trimmed {
per_fs: vec![(device, bytes)],
bytes,
})
}
fn enter_private_mount_namespace() -> Result<(), String> {
use nix::mount::MsFlags;
use nix::sched::{CloneFlags, unshare};
unshare(CloneFlags::CLONE_NEWNS).map_err(|e| format!("unshare mount namespace: {e}"))?;
nix::mount::mount(
None::<&str>,
"/",
None::<&str>,
MsFlags::MS_REC | MsFlags::MS_PRIVATE,
None::<&str>,
)
.map_err(|e| format!("make mounts private: {e}"))
}
fn trim_mount(mount: &Path) -> std::io::Result<u64> {
let dir = std::fs::File::open(mount)?;
let mut range = FstrimRange {
start: 0,
len: u64::MAX,
minlen: 0,
};
unsafe { fitrim(dir.as_raw_fd(), &raw mut range) }?;
Ok(range.len)
}
#[cfg(test)]
mod tests {
use super::Trimmed;
#[test]
fn summary_lists_each_filesystem() {
let trimmed = Trimmed {
per_fs: vec![
("/run/arcbox/data".into(), 4096),
("/run/arcbox/metadata".into(), 0),
],
bytes: 4096,
};
assert_eq!(
trimmed.summary(),
"/run/arcbox/data: 4096 bytes trimmed; /run/arcbox/metadata: 0 bytes trimmed"
);
}
}