use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
use anyhow::{Context, Result, bail};
use crate::config::{Arch, Port};
pub struct Launch<'a> {
pub arch: Arch,
pub cpus: u32,
pub memory: &'a str,
pub disk: &'a Path,
pub seed: &'a Path,
pub ssh_port: u16,
pub qmp_port: u16,
pub ports: &'a [Port],
pub console: &'a Path,
pub pidfile: &'a Path,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Accel {
Kvm,
Hvf,
Tcg,
}
pub fn accel(arch: Arch) -> Accel {
if arch != Arch::host() {
return Accel::Tcg;
}
if cfg!(target_os = "macos") {
return Accel::Hvf;
}
let kvm = std::fs::OpenOptions::new().read(true).write(true).open("/dev/kvm");
if cfg!(target_os = "linux") && kvm.is_ok() { Accel::Kvm } else { Accel::Tcg }
}
pub fn create_overlay(base: &Path, overlay: &Path, size: &str) -> Result<()> {
let output = Command::new("qemu-img")
.args(["create", "-q", "-f", "qcow2", "-F", "qcow2", "-b"])
.arg(base)
.arg(overlay)
.arg(size)
.output()
.context("running qemu-img (is QEMU installed?)")?;
if !output.status.success() {
bail!("qemu-img create failed: {}", String::from_utf8_lossy(&output.stderr).trim());
}
Ok(())
}
pub fn start(l: &Launch) -> Result<Accel> {
let accel = accel(l.arch);
let (binary, machine) = match l.arch {
Arch::Aarch64 => ("qemu-system-aarch64", "virt"),
_ => ("qemu-system-x86_64", "q35"),
};
let cpu = match accel {
Accel::Kvm | Accel::Hvf => "host",
Accel::Tcg => "max",
};
let accel_name = match accel {
Accel::Kvm => "kvm",
Accel::Hvf => "hvf",
Accel::Tcg => "tcg",
};
let mut netdev = format!("user,id=net0,hostfwd=tcp:127.0.0.1:{}-:22", l.ssh_port);
for port in l.ports {
netdev.push_str(&format!(",hostfwd=tcp:127.0.0.1:{}-:{}", port.host, port.guest));
}
let mut cmd = Command::new(binary);
cmd.args(["-machine", machine, "-accel", accel_name, "-cpu", cpu])
.args(["-smp", &l.cpus.to_string(), "-m", l.memory])
.args(["-display", "none", "-vga", "none"])
.arg("-drive")
.arg(drive(l.disk, "qcow2"))
.arg("-drive")
.arg(drive(l.seed, "raw"))
.args(["-netdev", &netdev, "-device", "virtio-net-pci,netdev=net0"])
.args(["-device", "virtio-rng-pci"])
.arg("-serial")
.arg(format!("file:{}", l.console.display()))
.args(["-qmp", &format!("tcp:127.0.0.1:{},server=on,wait=off", l.qmp_port)])
.arg("-pidfile")
.arg(l.pidfile)
.arg("-daemonize");
if l.arch == Arch::Aarch64 {
cmd.arg("-bios").arg(aarch64_firmware(binary)?);
}
let output = cmd.output().with_context(|| format!("running {binary} (is QEMU installed?)"))?;
if !output.status.success() {
bail!("{binary} failed to start: {}", String::from_utf8_lossy(&output.stderr).trim());
}
Ok(accel)
}
fn drive(path: &Path, format: &str) -> String {
let file = path.to_string_lossy().replace(',', ",,");
format!("file={file},format={format},if=virtio")
}
fn aarch64_firmware(binary: &str) -> Result<PathBuf> {
let mut candidates = Vec::new();
if let Some(prefix) = which(binary).and_then(|p| p.parent()?.parent().map(Path::to_path_buf)) {
candidates.push(prefix.join("share/qemu/edk2-aarch64-code.fd"));
}
for path in [
"/opt/homebrew/share/qemu/edk2-aarch64-code.fd",
"/usr/local/share/qemu/edk2-aarch64-code.fd",
"/usr/share/qemu/edk2-aarch64-code.fd",
"/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
"/usr/share/AAVMF/AAVMF_CODE.fd",
"/usr/share/edk2/aarch64/QEMU_EFI.fd",
] {
candidates.push(path.into());
}
candidates
.into_iter()
.find(|p| p.is_file())
.context("no aarch64 UEFI firmware found; install it (e.g. apt install qemu-efi-aarch64)")
}
fn which(binary: &str) -> Option<PathBuf> {
std::env::split_paths(&std::env::var_os("PATH")?).map(|dir| dir.join(binary)).find(|p| p.is_file())
}
pub fn free_port() -> Result<u16> {
Ok(TcpListener::bind("127.0.0.1:0")?.local_addr()?.port())
}
pub fn running_pid(pidfile: &Path) -> Option<i32> {
let pid: i32 = std::fs::read_to_string(pidfile).ok()?.trim().parse().ok()?;
(unsafe { libc::kill(pid, 0) } == 0).then_some(pid)
}
pub fn qmp(port: u16, command: &str) -> Result<()> {
let stream = TcpStream::connect(("127.0.0.1", port)).context("connecting to the QEMU monitor")?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
let mut reader = BufReader::new(stream.try_clone()?);
let mut writer = stream;
let mut line = String::new();
reader.read_line(&mut line)?; for cmd in ["qmp_capabilities", command] {
writeln!(writer, "{{\"execute\":\"{cmd}\"}}")?;
line.clear();
reader.read_line(&mut line)?;
}
Ok(())
}
pub fn stop(pidfile: &Path, qmp_port: u16, timeout: Duration) -> Result<()> {
let Some(pid) = running_pid(pidfile) else { return Ok(()) };
if qmp(qmp_port, "system_powerdown").is_ok() && wait_exit(pidfile, timeout) {
return Ok(());
}
kill(pid, pidfile)
}
pub fn kill(pid: i32, pidfile: &Path) -> Result<()> {
unsafe { libc::kill(pid, libc::SIGTERM) };
if !wait_exit(pidfile, Duration::from_secs(10)) {
unsafe { libc::kill(pid, libc::SIGKILL) };
wait_exit(pidfile, Duration::from_secs(5));
}
Ok(())
}
fn wait_exit(pidfile: &Path, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if running_pid(pidfile).is_none() {
return true;
}
std::thread::sleep(Duration::from_millis(250));
}
running_pid(pidfile).is_none()
}