use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use eyre::{Context, OptionExt, Result, bail};
use dir_lock::DirLock;
use directories::ProjectDirs;
use tokio::io::AsyncWriteExt;
use tokio::task;
use tracing::{debug, instrument, trace, warn};
use walkdir::WalkDir;
use crate::error::Error;
use crate::vms::CID_FILENAME;
pub(crate) static HEX_ALPHABET: [char; 16] = [
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f',
];
pub(crate) fn ensure_directory(purpose: &str, path: &Path) -> Result<()> {
if !path.exists() {
debug!("{purpose} dir {path:?} doesn't exist yet, creating");
fs::create_dir_all(path).wrap_err(format!("Creating {purpose} dir {path:?}"))?;
}
Ok(())
}
pub(crate) async fn ensure_directory_async(purpose: &str, path: &Path) -> Result<()> {
let purpose = purpose.to_owned();
let path = path.to_owned();
task::spawn_blocking(move || ensure_directory(&purpose, &path)).await?
}
#[derive(Clone, Debug)]
pub struct VmexecDirs {
pub cache_dir: PathBuf,
pub images_dir: PathBuf,
pub secrets_dir: PathBuf,
pub vms_dir: PathBuf,
pub lock_dir: PathBuf,
}
impl VmexecDirs {
pub fn new() -> Result<Self> {
let project_dir =
ProjectDirs::from("", "", "vmexec").ok_or_eyre("Couldn't get project dir")?;
let cache_dir = project_dir.cache_dir().to_path_buf();
ensure_directory("cache", &cache_dir)?;
let images_dir = cache_dir.join("images");
ensure_directory("images", &images_dir)?;
let data_dir = project_dir
.state_dir()
.ok_or_eyre("Couldn't get state dir")?
.to_path_buf();
ensure_directory("data", &data_dir)?;
let secrets_dir = data_dir.join("secrets");
ensure_directory("secrets", &secrets_dir)?;
let vms_dir = data_dir.join("vms");
ensure_directory("vms", &vms_dir)?;
let lock_dir = data_dir.join("lockdir");
Ok(Self {
cache_dir,
images_dir,
secrets_dir,
vms_dir,
lock_dir,
})
}
}
pub(crate) fn escape_path(path: &str) -> String {
let trimmed = path.trim_matches('/');
if trimmed.is_empty() {
return "-".to_string();
}
let mut slash_seq = false;
let parts: Vec<String> = trimmed
.bytes()
.filter(|b| {
let is_slash = *b == b'/';
let res = !(is_slash && slash_seq);
slash_seq = is_slash;
res
})
.enumerate()
.map(|(n, b)| escape_byte(b, n))
.collect();
parts.join("")
}
fn escape_byte(b: u8, index: usize) -> String {
let c = char::from(b);
match c {
'/' => '-'.to_string(),
':' | '_' | '0'..='9' | 'a'..='z' | 'A'..='Z' => c.to_string(),
'.' if index > 0 => c.to_string(),
_ => format!(r#"\x{b:02x}"#),
}
}
#[instrument(skip(_lock))]
pub(crate) fn create_free_cid(_lock: &DirLock, vms_dir: &Path, vm_dir: &Path) -> Result<u32> {
let mut cids = vec![];
for entry in WalkDir::new(vms_dir) {
let entry = entry?;
let filename = entry.file_name();
if filename.to_string_lossy() == CID_FILENAME {
trace!("Found CID file at {:?}", entry.path());
let cid = fs::read_to_string(entry.path())?;
cids.push(cid.parse::<u32>()?);
}
}
cids.sort();
let cid = if let Some(last_cid) = cids.iter().next_back() {
last_cid + 1
} else {
10
};
debug!("Our new CID: {cid}");
fs::write(vm_dir.join(CID_FILENAME), cid.to_string())?;
Ok(cid)
}
pub(crate) fn lock_state_dir(dirs: &VmexecDirs) -> Result<DirLock> {
let lock_dir = &dirs.lock_dir;
if lock_dir.exists() {
warn!("Already locked by another vmexec operation, waiting...");
}
trace!("Trying to lock {lock_dir:?}");
let lock = DirLock::new_sync(lock_dir)?;
Ok(lock)
}
#[derive(Clone, Debug)]
pub struct ExecutablePaths {
pub qemu_path: PathBuf,
pub virtiofsd_path: PathBuf,
pub virt_copy_out_path: PathBuf,
}
pub fn find_required_tools() -> Result<ExecutablePaths, Error> {
let qemu_path = which::which_global("qemu-system-x86_64").map_err(|_| Error::MissingTool {
tool: "qemu-system-x86_64",
searched: "PATH",
})?;
let virtiofsd_path =
which::which_in("virtiofsd", Some("/usr/lib:/usr/libexec"), "/").map_err(|_| {
Error::MissingTool {
tool: "virtiofsd",
searched: "/usr/lib or /usr/libexec",
}
})?;
let virt_copy_out_path =
which::which_global("virt-copy-out").map_err(|_| Error::MissingTool {
tool: "virt-copy-out",
searched: "PATH",
})?;
check_unshare()?;
Ok(ExecutablePaths {
qemu_path,
virtiofsd_path,
virt_copy_out_path,
})
}
fn check_unshare() -> Result<()> {
let unshare_output = Command::new("unshare")
.env("LC_ALL", "C")
.arg("-r")
.arg("id")
.output()?;
let unshare_stdout = std::str::from_utf8(&unshare_output.stdout)?;
let unshare_stderr = std::str::from_utf8(&unshare_output.stderr)?;
if !unshare_output.status.success() {
bail!(
"Test command 'unshare -r id' didn't exit succesfully, stdout: {unshare_stdout}, stderr: {unshare_stderr}"
);
}
if !unshare_stdout.starts_with("uid=0(root) gid=0(root) groups=0(root)") {
bail!(
"Expected output to start with 'unshare -r id' to report 'uid=0(root) gid=0(root) groups=0(root)' but got: {unshare_stdout}"
);
}
Ok(())
}
pub(crate) async fn safe_flush<W: AsyncWriteExt + std::marker::Unpin>(
writer: &mut W,
) -> std::io::Result<()> {
use std::io::ErrorKind;
loop {
match writer.flush().await {
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
tokio::time::sleep(Duration::from_millis(1)).await;
continue;
}
result => return result,
}
}
}