use std::path::Path;
use crate::lfs;
use std::io::BufRead;
use crate::run::RunOptions;
pub fn generate_vsock_cid() -> u32 {
let pid = std::process::id();
pid + 3
}
fn detect_host_cpu_model() -> Option<String> {
use std::fs::File;
let file = File::open("/proc/cpuinfo").ok()?;
let reader = std::io::BufReader::new(file);
for line in reader.lines() {
let line = line.ok()?;
if line.starts_with("model name") || line.starts_with("CPU model") {
if let Some(pos) = line.find(':') {
let model = line[pos + 1..].trim().to_string();
return Some(model);
}
}
}
None
}
fn detect_cpu_implementer() -> Option<u32> {
use std::fs::File;
let file = File::open("/proc/cpuinfo").ok()?;
let reader = std::io::BufReader::new(file);
for line in reader.lines() {
let line = line.ok()?;
if line.starts_with("CPU implementer") {
if let Some(pos) = line.find(':') {
let value = line[pos + 1..].trim();
if let Some(hex_str) = value.strip_prefix("0x") {
return u32::from_str_radix(hex_str, 16).ok();
}
return value.parse().ok();
}
}
}
None
}
fn detect_cpu_part() -> Option<u32> {
use std::fs::File;
let file = File::open("/proc/cpuinfo").ok()?;
let reader = std::io::BufReader::new(file);
for line in reader.lines() {
let line = line.ok()?;
if line.starts_with("CPU part") {
if let Some(pos) = line.find(':') {
let value = line[pos + 1..].trim();
if let Some(hex_str) = value.strip_prefix("0x") {
return u32::from_str_radix(hex_str, 16).ok();
}
return value.parse().ok();
}
}
}
None
}
const HISILICON_IMPLEMENTER: u32 = 0x48;
const KUNPENG_920_PART: u32 = 0xd01;
const KUNPENG_930_PART: u32 = 0xd02;
fn is_kunpeng_920() -> bool {
if let Some(model) = detect_host_cpu_model() {
if model.contains("Kunpeng") && model.contains("920") {
return true;
}
}
if let Some(implementer) = detect_cpu_implementer() {
if let Some(part) = detect_cpu_part() {
return implementer == HISILICON_IMPLEMENTER && part == KUNPENG_920_PART;
}
}
false
}
fn is_kunpeng_930() -> bool {
if let Some(model) = detect_host_cpu_model() {
if model.contains("Kunpeng") && model.contains("930") {
return true;
}
}
if let Some(implementer) = detect_cpu_implementer() {
if let Some(part) = detect_cpu_part() {
return implementer == HISILICON_IMPLEMENTER && part == KUNPENG_930_PART;
}
}
false
}
fn qemu_supports_cpu_model(qemu_bin: &str, cpu_model: &str) -> bool {
if std::env::consts::ARCH != "aarch64" {
return false;
}
use std::process::Command;
let output = Command::new(qemu_bin)
.arg("-cpu")
.arg("help")
.output();
match output {
Ok(output) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.lines().any(|line| line.trim() == cpu_model)
}
_ => false,
}
}
fn get_qemu_cpu_model(qemu_bin: &str) -> &'static str {
if std::env::consts::ARCH == "aarch64" {
if is_kunpeng_930() && qemu_supports_cpu_model(qemu_bin, "Kunpeng-930") {
"Kunpeng-930"
} else if is_kunpeng_920() && qemu_supports_cpu_model(qemu_bin, "Kunpeng-920") {
"Kunpeng-920"
} else {
"max"
}
} else {
"host"
}
}
use crate::vm::client;
use color_eyre::eyre;
use color_eyre::Result;
use crate::models::dirs;
fn find_qemu_binary() -> Result<String> {
let arch = std::env::consts::ARCH;
let candidates = [
format!("qemu-system-{}", arch),
format!("qemu-{}", arch),
"qemu".to_string(),
];
for candidate in &candidates {
if let Some(path) = crate::utils::find_command_in_paths(candidate) {
return Ok(path.to_string_lossy().to_string());
}
}
Err(eyre::eyre!(
"QEMU binary not found. Tried: {} in PATH and common system paths. \
Please install QEMU or set EPKG_VM_QEMU environment variable.",
candidates.join(", ")
))
}
fn find_kernel_image() -> Result<String> {
let uname = crate::posix::posix_uname()
.map_err(|e| eyre::eyre!("Failed to get kernel release: {:?}", e))?;
let release = uname.release;
let candidates = [
format!("/boot/vmlinuz-{}", release),
"/boot/vmlinuz".to_string(),
format!("/boot/kernel-{}", release),
"/boot/kernel".to_string(),
format!("/boot/bzImage-{}", release),
"/boot/bzImage".to_string(),
format!("/boot/Image-{}", release),
"/boot/Image".to_string(),
format!("/boot/vmlinux-{}", release),
"/boot/vmlinux".to_string(),
];
for candidate in &candidates {
if lfs::metadata_on_host(candidate).is_ok() {
return Ok(candidate.clone());
}
}
Err(eyre::eyre!(
"No kernel image found in /boot/. Tried: {}. \
Use '--kernel /path/to/kernel' to specify a guest kernel image.",
candidates.join(", ")
))
}
pub fn resolve_vm_kernel_path(run_options: &RunOptions) -> Result<String> {
let kernel = run_options
.kernel
.clone()
.or_else(crate::init::default_kernel_path_if_exists)
.or_else(|| find_kernel_image().ok())
.ok_or_else(|| {
eyre::eyre!(
"No kernel image for VM. Use '--kernel /path/to/kernel', run 'epkg self install', or ensure a kernel exists in /boot."
)
})?;
if !lfs::exists_on_host(Path::new(&kernel)) {
return Err(eyre::eyre!("Kernel image not found at {}", kernel));
}
Ok(kernel)
}
fn parse_vmm_config(run_options: &RunOptions) -> Result<(String, Option<String>, String, Option<String>, String)> {
let kernel = resolve_vm_kernel_path(run_options)?;
let initrd = run_options.initrd.clone().or_else(|| std::env::var("EPKG_VM_INITRD").ok());
let qemu_bin = match std::env::var("EPKG_VM_QEMU") {
Ok(p) => crate::utils::find_command_in_paths(&p)
.map(|path| path.to_string_lossy().to_string())
.unwrap_or(p),
Err(_) => find_qemu_binary()?,
};
let virtiofsd_bin = std::env::var("EPKG_VM_VIRTIOFSD")
.unwrap_or_else(|_| "virtiofsd".to_string());
let virtiofsd_path = crate::utils::find_command_in_paths(&virtiofsd_bin);
let virtiofsd_bin = virtiofsd_path
.map(|p| p.to_string_lossy().to_string());
let env_extra = std::env::var("EPKG_QEMU_EXTRA_ARGS")
.or_else(|_| std::env::var("EPKG_VM_EXTRA_ARGS"))
.unwrap_or_default();
let extra_qemu_args = if let Some(cli_args) = &run_options.kernel_args {
if env_extra.is_empty() {
cli_args.clone()
} else {
format!("{} {}", env_extra, cli_args)
}
} else {
env_extra
};
Ok((kernel, initrd, qemu_bin, virtiofsd_bin, extra_qemu_args))
}
pub(crate) fn ensure_vmm_log_dir() -> Result<()> {
let base_log_dir = dirs().epkg_cache.join("vmm-logs");
lfs::create_dir_all(&base_log_dir)
.map_err(|e| eyre::eyre!("Failed to create VMM log directory: {}", e))?;
Ok(())
}
fn create_pid_log_with_symlink(log_name: &str) -> Result<std::path::PathBuf> {
let base_log_dir = dirs().epkg_cache.join("vmm-logs");
lfs::create_dir_all(&base_log_dir)
.map_err(|e| eyre::eyre!("Failed to create VMM log directory: {}", e))?;
let pid = std::process::id();
let log_path = base_log_dir.join(format!("{}-{}.log", log_name, pid));
let latest_log = base_log_dir.join(format!("latest-{}.log", log_name));
let _ = lfs::remove_file(&latest_log);
if let Err(e) = lfs::symlink_file_for_native(&log_path, &latest_log) {
log::warn!("Failed to create symlink {} -> {}: {}", latest_log.display(), log_path.display(), e);
}
Ok(log_path)
}
pub fn setup_vmm_logs() -> Result<(std::path::PathBuf, std::path::PathBuf)> {
let qemu_log_path = create_pid_log_with_symlink("qemu")?;
let virtiofsd_log_path = create_pid_log_with_symlink("virtiofsd")?;
log::info!("VMM logs: QEMU={} virtiofsd={}", qemu_log_path.display(), virtiofsd_log_path.display());
Ok((qemu_log_path, virtiofsd_log_path))
}
pub fn build_guest_command(cmd_path: &Path, args: &[String]) -> Result<(Vec<String>, String)> {
let mut cmd_parts: Vec<String> = Vec::new();
cmd_parts.push(cmd_path.to_string_lossy().to_string());
cmd_parts.extend(args.iter().cloned());
let raw_cmd = shlex::try_join(cmd_parts.iter().map(|s| s.as_str()))
.map_err(|e| eyre::eyre!("Failed to join command parts: {}", e))?;
let init_cmd = percent_encode(&raw_cmd);
Ok((cmd_parts, init_cmd))
}
fn start_virtiofsd_at(
shared_dir: &Path,
virtiofsd_bin: &str,
virtiofsd_log_path: &Path,
is_root: bool,
translate_uid: &[String],
translate_gid: &[String],
reuse_session: bool,
) -> Result<(tempfile::TempDir, std::process::Child, std::path::PathBuf)> {
use std::fs::File;
use std::process::{Command, Stdio};
let tmpdir = tempfile::Builder::new()
.prefix("epkg-vmm-")
.tempdir()
.map_err(|e| eyre::eyre!("Failed to create temporary directory for VMM: {}", e))?;
let socket_path = tmpdir.path().join("vhostqemu.sock");
let mut virtiofsd_cmd = Command::new(virtiofsd_bin);
virtiofsd_cmd
.arg("--shared-dir")
.arg(shared_dir.display().to_string())
.arg("--socket-path")
.arg(socket_path.display().to_string())
.arg("--cache")
.arg("auto")
.arg("--sandbox").arg("none")
.arg("--rlimit-nofile").arg("0");
if is_root {
virtiofsd_cmd.arg("--inode-file-handles=prefer");
}
virtiofsd_cmd.arg("--announce-submounts");
for spec in translate_uid {
virtiofsd_cmd.arg("--translate-uid").arg(spec);
}
for spec in translate_gid {
virtiofsd_cmd.arg("--translate-gid").arg(spec);
}
let log_file = File::create(virtiofsd_log_path)
.map_err(|e| eyre::eyre!("Failed to create virtiofsd log {}: {}", virtiofsd_log_path.display(), e))?;
virtiofsd_cmd
.stdout(Stdio::from(log_file.try_clone().map_err(|e| eyre::eyre!("Failed to dup virtiofsd log: {}", e))?))
.stderr(Stdio::from(log_file));
log::debug!("virtiofsd command: {} {}",
virtiofsd_bin,
virtiofsd_cmd.get_args()
.map(|s| {
let owned = s.to_string_lossy().into_owned();
shlex::try_quote(&owned)
.map(|cow| cow.into_owned())
.unwrap_or_else(|_| owned)
})
.collect::<Vec<_>>()
.join(" "));
if reuse_session {
#[cfg(target_os = "linux")]
{
use std::os::unix::process::CommandExt;
use nix::unistd::setsid;
unsafe {
virtiofsd_cmd.pre_exec(|| {
setsid().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(())
});
}
}
}
let mut virtiofsd_child = virtiofsd_cmd
.spawn()
.map_err(|e| eyre::eyre!("Failed to spawn virtiofsd ({}): {}", virtiofsd_bin, e))?;
wait_for_virtiofsd_socket(&mut virtiofsd_child, &socket_path)?;
Ok((tmpdir, virtiofsd_child, socket_path))
}
fn wait_for_virtiofsd_socket(
virtiofsd_child: &mut std::process::Child,
socket_path: &Path,
) -> Result<()> {
const SOCKET_WAIT_TIMEOUT_MS: u64 = 500;
const SOCKET_POLL_INTERVAL_MS: u64 = 5;
let start = std::time::Instant::now();
loop {
match virtiofsd_child.try_wait() {
Ok(Some(status)) => {
return Err(eyre::eyre!("virtiofsd exited early with status: {}", status));
}
Ok(None) => {
if socket_path.exists() {
log::debug!("virtiofsd socket created after {:?}", start.elapsed());
return Ok(());
}
}
Err(e) => {
return Err(eyre::eyre!("Failed to check virtiofsd status: {}", e));
}
}
if start.elapsed().as_millis() as u64 > SOCKET_WAIT_TIMEOUT_MS {
let _ = virtiofsd_child.kill();
let _ = virtiofsd_child.wait();
return Err(eyre::eyre!(
"virtiofsd socket not created at {} after {}ms timeout",
socket_path.display(),
SOCKET_WAIT_TIMEOUT_MS
));
}
std::thread::sleep(std::time::Duration::from_millis(SOCKET_POLL_INTERVAL_MS));
}
}
fn build_qemu_command(
kernel: &str,
initrd: &Option<String>,
qemu_bin: &str,
rootfs_mode: &RootFsMode,
env_root: &Path,
mount_tag: &str,
use_vsock: bool,
guest_cid: u32,
extra_qemu_args: &str,
serial_log_path: &std::path::Path,
vm_cpus: u8,
vm_memory_mb: u32,
init_cmd: Option<&str>,
init_user: Option<&str>,
) -> std::process::Command {
use std::process::Command;
let mut qemu_cmd = Command::new(qemu_bin);
if std::env::consts::ARCH == "aarch64" {
qemu_cmd.arg("-machine").arg("virt,highmem=on");
}
qemu_cmd.arg("-enable-kvm");
let cpu_model = get_qemu_cpu_model(qemu_bin);
qemu_cmd.arg("-cpu").arg(cpu_model)
.arg("-m").arg(vm_memory_mb.to_string())
.arg("-smp").arg(vm_cpus.to_string())
.arg("-no-reboot")
.arg("-display").arg("none")
.arg("-serial").arg(format!("file:{}", serial_log_path.display()))
.arg("-monitor").arg("none")
.arg("-kernel").arg(kernel);
if let Some(ref initrd_path) = initrd {
qemu_cmd.arg("-initrd").arg(initrd_path);
}
match rootfs_mode {
RootFsMode::Virtiofs(_, socket_path) => {
qemu_cmd
.arg("-object")
.arg(format!("memory-backend-file,id=mem,size={}M,mem-path=/dev/shm,share=on", vm_memory_mb))
.arg("-numa")
.arg("node,memdev=mem");
qemu_cmd
.arg("-chardev")
.arg(format!("socket,id=char0,path={}", socket_path.display()))
.arg("-device")
.arg(if std::env::consts::ARCH == "aarch64" {
format!("vhost-user-fs-device,queue-size=1024,chardev=char0,tag={}", mount_tag)
} else {
format!("vhost-user-fs-pci,queue-size=1024,chardev=char0,tag={}", mount_tag)
});
}
RootFsMode::Plan9 => {
qemu_cmd
.arg("-fsdev")
.arg(format!("local,id=fsdev0,path={},security_model=none", env_root.display()))
.arg("-device")
.arg(if std::env::consts::ARCH == "aarch64" {
format!("virtio-9p-device,fsdev=fsdev0,mount_tag={}", mount_tag)
} else {
format!("virtio-9p-pci,fsdev=fsdev0,mount_tag={}", mount_tag)
});
}
}
qemu_cmd
.arg("-netdev")
.arg("user,id=net0")
.arg("-device")
.arg("virtio-net-pci,netdev=net0,romfile=");
if use_vsock {
qemu_cmd
.arg("-device")
.arg(if std::env::consts::ARCH == "aarch64" {
format!("vhost-vsock-device,guest-cid={}", guest_cid)
} else {
format!("vhost-vsock-pci,guest-cid={}", guest_cid)
});
}
let rootfstype = match rootfs_mode {
RootFsMode::Virtiofs(_, _) => "virtiofs",
RootFsMode::Plan9 => "9p",
};
let console_dev = if std::env::consts::ARCH == "aarch64" { "ttyAMA0" } else { "ttyS0" };
let mut append_args = if std::env::consts::ARCH == "aarch64" {
format!(
"console={} debug earlycon=pl011,0x9000000 panic=1 root={} rootfstype={} init=/usr/bin/init sysctl.fs.file-max=1048576 loglevel=8 epkg.tsi=0",
console_dev, mount_tag, rootfstype
)
} else {
format!(
"console={} debug panic=1 root={} rootfstype={} init=/usr/bin/init sysctl.fs.file-max=1048576 loglevel=8 epkg.tsi=0",
console_dev, mount_tag, rootfstype
)
};
if let Ok(rust_log) = std::env::var("RUST_LOG") {
if !rust_log.is_empty() {
append_args.push_str(&format!(" epkg.rust_log={}", percent_encode(&rust_log)));
}
}
append_args.push_str(" epkg.var.EPKG_HOST_OS=Linux");
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
append_args.push_str(&format!(" epkg.var.EPKG_HOME={}", percent_encode(&home)));
}
}
if let Ok(user) = std::env::var("USER") {
if !user.is_empty() {
append_args.push_str(&format!(" epkg.var.EPKG_USER={}", percent_encode(&user)));
}
}
if let Some(cmd) = init_cmd {
if !cmd.is_empty() {
append_args.push_str(&format!(" epkg.init_cmd={}", cmd));
}
}
if let Some(user) = init_user {
if !user.is_empty() {
append_args.push_str(&format!(" epkg.init_user={}", percent_encode(user)));
}
}
if let Ok(pwd) = std::env::var("PWD") {
if !pwd.is_empty() && pwd != "/" {
append_args.push_str(&format!(" epkg.init_pwd={}", percent_encode(&pwd)));
}
}
if !extra_qemu_args.is_empty() {
append_args.push(' ');
append_args.push_str(extra_qemu_args);
}
qemu_cmd.arg("-append").arg(append_args);
qemu_cmd
}
type VirtiofsdGuard = Option<(tempfile::TempDir, std::process::Child)>;
enum RootFsMode {
Virtiofs(VirtiofsdGuard, std::path::PathBuf),
Plan9,
}
fn setup_rootfs_mode(
env_root: &Path,
existing_socket_path: Option<&Path>,
virtiofsd_bin: Option<&String>,
virtiofsd_log_path: &Path,
is_root: bool,
translate_uid: &[String],
translate_gid: &[String],
reuse_session: bool,
) -> Result<RootFsMode> {
if let Ok(rootfs_choice) = std::env::var("EPKG_VM_ROOTFS") {
match rootfs_choice.as_str() {
"9p" | "plan9" => {
log::info!("EPKG_VM_ROOTFS={} forcing 9p rootfs", rootfs_choice);
return Ok(RootFsMode::Plan9);
}
"virtiofs" => {
log::info!("EPKG_VM_ROOTFS=virtiofs forcing virtiofs rootfs");
}
_ => {
log::warn!("EPKG_VM_ROOTFS={} unknown, using default selection", rootfs_choice);
}
}
}
if let Some(path) = existing_socket_path {
return Ok(RootFsMode::Virtiofs(None, path.to_path_buf()));
}
if let Some(virtiofsd_bin) = virtiofsd_bin {
match start_virtiofsd_at(env_root, virtiofsd_bin, virtiofsd_log_path, is_root, translate_uid, translate_gid, reuse_session) {
Ok((tmpdir, child, path)) => {
return Ok(RootFsMode::Virtiofs(Some((tmpdir, child)), path));
}
Err(e) => {
if std::env::var("EPKG_VM_ROOTFS").as_deref() == Ok("virtiofs") {
return Err(eyre::eyre!("EPKG_VM_ROOTFS=virtiofs forced but virtiofsd failed: {}", e));
}
log::warn!("virtiofsd failed to start ({}), falling back to 9p", e);
}
}
}
log::info!("Using 9p filesystem for VM root (virtiofsd not available)");
Ok(RootFsMode::Plan9)
}
fn spawn_qemu(
kernel: &str,
initrd: &Option<String>,
qemu_bin: &str,
rootfs_mode: &RootFsMode,
env_root: &Path,
mount_tag: &str,
use_vsock: bool,
guest_cid: u32,
extra_qemu_args: &str,
qemu_log_path: &Path,
vm_cpus: u8,
vm_memory_mb: u32,
init_cmd: Option<&str>,
init_user: Option<&str>,
reuse_session: bool,
) -> Result<std::process::Child> {
use std::process::Stdio;
let mut qemu_cmd = build_qemu_command(
kernel,
initrd,
qemu_bin,
rootfs_mode,
env_root,
mount_tag,
use_vsock,
guest_cid,
extra_qemu_args,
qemu_log_path,
vm_cpus,
vm_memory_mb,
init_cmd,
init_user,
);
if reuse_session {
#[cfg(target_os = "linux")]
{
use std::os::unix::process::CommandExt;
use nix::unistd::setsid;
unsafe {
qemu_cmd.pre_exec(|| {
setsid().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(())
});
}
}
}
let log_qemu_output = log::log_enabled!(log::Level::Debug);
let need_stderr_for_error_detection = use_vsock;
if log_qemu_output {
use std::fs::File;
let stdout_log = File::create(qemu_log_path.with_extension("stdout.log"))
.map(Stdio::from)
.unwrap_or_else(|e| {
log::warn!("Failed to create QEMU stdout log: {}", e);
Stdio::null()
});
let stderr_log = File::create(qemu_log_path.with_extension("stderr.log"))
.map(Stdio::from)
.unwrap_or_else(|e| {
log::warn!("Failed to create QEMU stderr log: {}", e);
Stdio::null()
});
qemu_cmd.stdin(Stdio::null()).stdout(stdout_log).stderr(stderr_log);
} else if need_stderr_for_error_detection {
use std::fs::File;
let stderr_log = File::create(qemu_log_path.with_extension("stderr.log"))
.map(Stdio::from)
.unwrap_or_else(|e| {
log::warn!("Failed to create QEMU stderr log: {}", e);
Stdio::null()
});
qemu_cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(stderr_log);
} else {
qemu_cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
}
log::debug!("qemu command: {} {}",
qemu_bin,
qemu_cmd.get_args()
.map(|s| {
let owned = s.to_string_lossy().into_owned();
shlex::try_quote(&owned)
.map(|cow| cow.into_owned())
.unwrap_or_else(|_| owned)
})
.collect::<Vec<_>>()
.join(" "));
qemu_cmd
.spawn()
.map_err(|e| eyre::eyre!("Failed to spawn QEMU ({}): {}", qemu_bin, e))
}
fn handle_guest_execution(
qemu_child: &mut std::process::Child,
use_control_channel: bool,
use_vsock: bool,
guest_cid: u32,
cmd_parts: &[String],
io_mode: crate::models::IoMode,
env_root: &Path,
qemu_log_path: &std::path::Path,
vm_cpus: u8,
vm_memory_mb: u32,
vm_keep_timeout: Option<u32>,
user: Option<&str>,
vm_daemon: bool,
vm_daemon_ready_fd: Option<&std::os::fd::OwnedFd>,
) -> Result<i32> {
if use_vsock && vm_daemon {
let qemu_stderr_path = qemu_log_path.with_extension("stderr.log");
let reuse_session = vm_keep_timeout.is_some();
client::wait_for_guest_ready(guest_cid, Some(qemu_child), Some(&qemu_stderr_path))?;
log::info!("qemu: guest is ready, registering session");
let env_name = &crate::models::config().common.env_name;
let socket_path = std::path::PathBuf::from(format!("vsock:{}", guest_cid));
let daemon_pid = qemu_child.id();
let config = crate::vm::VmConfig {
timeout: vm_keep_timeout,
extend: 10,
cpus: vm_cpus as u32,
memory_mib: vm_memory_mb,
backend: "qemu".to_string(),
};
crate::vm::register_vm_session(env_root, env_name, &socket_path, "qemu", &config, daemon_pid)?;
log::info!("qemu: registered VM session for {} (CID={}, PID={})", env_name, guest_cid, daemon_pid);
match client::send_command_via_vsock_simple(
cmd_parts,
io_mode,
guest_cid,
10000,
reuse_session,
vm_keep_timeout,
user,
) {
Ok(_cmd_exit_code) => {
log::info!("qemu: daemon mode - dummy command returned");
if let Some(fd) = vm_daemon_ready_fd {
let signal = b"READY\n";
match nix::unistd::write(fd, signal) {
Ok(n) if n == signal.len() => {
log::info!("qemu: daemon mode - signaled VM ready to parent via pipe");
}
Ok(n) => {
log::warn!("qemu: daemon mode - partial write to ready pipe: {} bytes", n);
}
Err(e) => {
log::error!("qemu: daemon mode - failed to write to ready pipe: {}", e);
}
}
}
return Ok(0);
}
Err(e) => {
if let Err(kill_err) = qemu_child.kill() {
log::debug!("Failed to kill QEMU process: {}", kill_err);
}
if let Err(wait_err) = qemu_child.wait() {
log::debug!("Failed to wait for QEMU process: {}", wait_err);
}
return Err(e);
}
}
}
if use_vsock {
let qemu_stderr_path = qemu_log_path.with_extension("stderr.log");
let reuse_session = vm_keep_timeout.is_some();
client::wait_for_guest_ready(guest_cid, Some(qemu_child), Some(&qemu_stderr_path))?;
log::info!("qemu: guest is ready");
let env_name = &crate::models::config().common.env_name;
let socket_path = std::path::PathBuf::from(format!("vsock:{}", guest_cid));
let daemon_pid = qemu_child.id();
let config = crate::vm::VmConfig {
timeout: vm_keep_timeout,
extend: 10,
cpus: vm_cpus as u32,
memory_mib: vm_memory_mb,
backend: "qemu".to_string(),
};
crate::vm::register_vm_session(env_root, env_name, &socket_path, "qemu", &config, daemon_pid)?;
log::info!("qemu: registered VM session for {} (CID={}, PID={})", env_name, guest_cid, daemon_pid);
match client::send_command_via_vsock_simple(
cmd_parts,
io_mode,
guest_cid,
10000,
reuse_session,
vm_keep_timeout,
user,
) {
Ok(cmd_exit_code) => {
log::debug!("qemu: command completed with exit code {}", cmd_exit_code);
if reuse_session {
return Ok(cmd_exit_code);
}
let _ = qemu_child
.wait()
.map_err(|e| eyre::eyre!("Failed to wait for QEMU process: {}", e))?;
log::debug!("qemu: QEMU process exited");
Ok(cmd_exit_code)
}
Err(e) => {
if let Err(kill_err) = qemu_child.kill() {
log::debug!("Failed to kill QEMU process: {}", kill_err);
}
if let Err(wait_err) = qemu_child.wait() {
log::debug!("Failed to wait for QEMU process: {}", wait_err);
}
Err(e)
}
}
} else if use_control_channel {
match client::send_command_via_tcp(cmd_parts, io_mode) {
Ok(cmd_exit_code) => {
let _ = qemu_child
.wait()
.map_err(|e| eyre::eyre!("Failed to wait for QEMU process: {}", e))?;
Ok(cmd_exit_code)
}
Err(e) => {
if let Err(kill_err) = qemu_child.kill() {
log::debug!("Failed to kill QEMU process: {}", kill_err);
}
if let Err(wait_err) = qemu_child.wait() {
log::debug!("Failed to wait for QEMU process: {}", wait_err);
}
Err(e)
}
}
} else {
let qemu_status = qemu_child
.wait()
.map_err(|e| eyre::eyre!("Failed to wait for QEMU process: {}", e))?;
Ok(qemu_status.code().unwrap_or(1))
}
}
fn cleanup_virtiofsd_child(rootfs_mode: RootFsMode) {
if let RootFsMode::Virtiofs(Some((_, mut virtiofsd_child)), _) = rootfs_mode {
let _ = virtiofsd_child.kill();
let _ = virtiofsd_child.wait();
}
}
struct VmSetup {
kernel: String,
initrd: Option<String>,
qemu_bin: String,
extra_qemu_args: String,
rootfs_mode: RootFsMode,
mount_tag: String,
qemu_log_path: std::path::PathBuf,
}
fn setup_qemu_vm(
env_root: &Path,
run_options: &RunOptions,
existing_socket_path: Option<&Path>,
) -> Result<VmSetup> {
crate::run::ensure_linux_kvm_ready_for_vm()?;
let (kernel, initrd, qemu_bin, virtiofsd_bin, extra_qemu_args) = parse_vmm_config(run_options)?;
let (qemu_log_path, virtiofsd_log_path) = setup_vmm_logs()?;
let host_uid = users::get_current_uid();
let host_gid = users::get_current_gid();
let (auto_uid, auto_gid) = if crate::auto_idmap::should_auto_map(host_uid) {
crate::auto_idmap::auto_idmap_specs(host_uid, host_gid, run_options.user.as_deref())
} else {
(vec![], vec![])
};
let translate_uid = crate::auto_idmap::merge_idmap_specs(auto_uid, run_options.translate_uid.clone());
let translate_gid = crate::auto_idmap::merge_idmap_specs(auto_gid, run_options.translate_gid.clone());
let reuse_session = run_options.vm_keep_timeout.is_some();
let rootfs_mode = setup_rootfs_mode(
env_root,
existing_socket_path,
virtiofsd_bin.as_ref(),
&virtiofsd_log_path,
host_uid == 0,
&translate_uid,
&translate_gid,
reuse_session,
)?;
Ok(VmSetup {
kernel,
initrd,
qemu_bin,
extra_qemu_args,
rootfs_mode,
mount_tag: "epkg_env".to_string(),
qemu_log_path,
})
}
pub fn run_command_in_qemu(
env_root: &Path,
run_options: &RunOptions,
guest_cmd_path: &Path,
existing_socket_path: Option<&Path>,
vm_daemon_ready_fd: Option<&std::os::fd::OwnedFd>,
) -> Result<()> {
let setup = setup_qemu_vm(env_root, run_options, existing_socket_path)?;
let (cmd_parts, init_cmd) = build_guest_command(guest_cmd_path, &run_options.args)?;
let use_cmdline_mode = std::env::var("EPKG_VM_NO_DAEMON").is_ok();
let use_vsock = !use_cmdline_mode;
let use_control_channel = false;
let vm_cpus = crate::run::resolve_vm_cpus(run_options);
let vm_memory_mb = crate::run::resolve_vm_memory_mib(run_options);
let guest_cid = generate_vsock_cid();
log::info!("qemu: generated vsock CID {} for VM", guest_cid);
let init_cmd_append = if use_cmdline_mode { Some(init_cmd.as_str()) } else { None };
let init_user_append = if use_cmdline_mode { run_options.user.as_deref() } else { None };
let reuse_session = run_options.vm_keep_timeout.is_some();
let mut qemu_child = match spawn_qemu(
&setup.kernel,
&setup.initrd,
&setup.qemu_bin,
&setup.rootfs_mode,
env_root,
&setup.mount_tag,
use_vsock,
guest_cid,
&setup.extra_qemu_args,
&setup.qemu_log_path,
vm_cpus,
vm_memory_mb,
init_cmd_append,
init_user_append,
reuse_session,
) {
Ok(child) => child,
Err(e) => {
cleanup_virtiofsd_child(setup.rootfs_mode);
return Err(e);
}
};
let exit_code = match handle_guest_execution(
&mut qemu_child,
use_control_channel,
use_vsock,
guest_cid,
&cmd_parts,
run_options.io_mode,
env_root,
&setup.qemu_log_path,
vm_cpus,
vm_memory_mb,
run_options.vm_keep_timeout,
run_options.user.as_deref(),
run_options.vm_daemon,
vm_daemon_ready_fd,
) {
Ok(code) => code,
Err(e) => {
cleanup_virtiofsd_child(setup.rootfs_mode);
return Err(e);
}
};
if run_options.vm_daemon {
log::info!("qemu: daemon mode - waiting for QEMU to complete (VM timeout)");
let status = qemu_child.wait()
.map_err(|e| eyre::eyre!("Failed to wait for QEMU process: {}", e))?;
log::info!("qemu: VM daemon exited with status {:?}", status);
cleanup_virtiofsd_child(setup.rootfs_mode);
std::process::exit(0);
}
if reuse_session {
log::info!("qemu: reuse_session mode - VM continues running (timeout {}s), exiting with code {}",
run_options.vm_keep_timeout.unwrap_or(0), exit_code);
if let RootFsMode::Virtiofs(Some((tmpdir, child)), _) = setup.rootfs_mode {
std::mem::forget(tmpdir);
std::mem::forget(child);
log::debug!("qemu: forgot virtiofsd guard for reuse session");
}
std::process::exit(exit_code);
}
cleanup_virtiofsd_child(setup.rootfs_mode);
std::process::exit(exit_code);
}
pub fn percent_encode(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
' ' => result.push_str("%20"),
'=' => result.push_str("%3D"),
'"' => result.push_str("%22"),
'\'' => result.push_str("%27"),
'\\' => result.push_str("%5C"),
'%' => result.push_str("%25"),
c => result.push(c),
}
}
result
}