use anyhow::{Context, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
static RE_NAME: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"-name\s+["'][^"']+["']"#).expect("Invalid regex: RE_NAME"));
static RE_DISPLAY: Lazy<Regex> =
Lazy::new(|| Regex::new(r"-display\s+\S+(,\S+)*").expect("Invalid regex: RE_DISPLAY"));
static RE_VGA: Lazy<Regex> =
Lazy::new(|| Regex::new(r"-vga\s+\S+").expect("Invalid regex: RE_VGA"));
static RE_AUDIODEV: Lazy<Regex> =
Lazy::new(|| Regex::new(r"-audiodev\s+\S+(,\S+)*").expect("Invalid regex: RE_AUDIODEV"));
static RE_SOUNDHW: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"-device\s+(intel-hda|ich9-intel-hda|hda-duplex|hda-micro|hda-output|AC97|sb16)[,\s]?[^\s\\]*")
.expect("Invalid regex: RE_SOUNDHW")
});
static RE_CDROM: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"-cdrom\s+("[^"]+"|'[^']+'|\$\w+|\S+)"#).expect("Invalid regex: RE_CDROM")
});
static RE_DRIVE_CDROM: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"-drive\s+[^\s\\]*media=cdrom[^\s\\]*").expect("Invalid regex: RE_DRIVE_CDROM")
});
static RE_DRIVE_ISO: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"-drive\s+[^\s\\]*file="\$ISO"[^\s\\]*"#).expect("Invalid regex: RE_DRIVE_ISO")
});
static RE_EMPTY_CONT: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\\\n\s*\\\n").expect("Invalid regex: RE_EMPTY_CONT"));
static RE_CPU_HOST: Lazy<Regex> =
Lazy::new(|| Regex::new(r"-cpu\s+host\b").expect("Invalid regex: RE_CPU_HOST"));
static RE_MACHINE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"-machine\s+(\S+)").expect("Invalid regex: RE_MACHINE"));
static RE_BOOT_D: Lazy<Regex> =
Lazy::new(|| Regex::new(r"-boot\s+order=d\b").expect("Invalid regex: RE_BOOT_D"));
use crate::hardware::SingleGpuConfig;
use crate::vm::lifecycle::{load_pci_passthrough, load_usb_passthrough};
use crate::vm::DiscoveredVm;
#[derive(Debug)]
pub struct GeneratedScripts {
pub start_script: PathBuf,
pub restore_script: PathBuf,
}
#[derive(Debug, Default)]
struct LaunchScriptComponents {
disk_var: Option<String>,
iso_var: Option<String>,
ovmf_code: Option<String>,
ovmf_vars: Option<String>,
tpm_dir: Option<String>,
has_tpm: bool,
has_uefi: bool,
smbios_opts: Option<String>,
}
fn parse_launch_script(content: &str) -> LaunchScriptComponents {
let mut components = LaunchScriptComponents::default();
let lines: Vec<&str> = content.lines().collect();
let mut i = 0;
while i < lines.len() {
let trimmed = lines[i].trim();
if trimmed.starts_with("DISK=") {
components.disk_var = Some(trimmed.to_string());
}
if trimmed.starts_with("ISO=") {
components.iso_var = Some(trimmed.to_string());
}
if trimmed.starts_with("OVMF_CODE=") {
if let Some(path) = extract_quoted_value(trimmed, "OVMF_CODE=") {
components.ovmf_code = Some(path);
}
}
if trimmed.starts_with("OVMF_VARS=") {
if let Some(path) = extract_quoted_value(trimmed, "OVMF_VARS=") {
components.ovmf_vars = Some(path);
}
}
if trimmed.starts_with("TPM_DIR=") {
if let Some(path) = extract_quoted_value(trimmed, "TPM_DIR=") {
components.tpm_dir = Some(path);
}
components.has_tpm = true;
}
if trimmed.contains("-tpmdev") || trimmed.contains("swtpm") {
components.has_tpm = true;
}
if trimmed.contains("OVMF") || trimmed.contains("pflash") {
components.has_uefi = true;
}
if trimmed.starts_with("SMBIOS_OPTS=(") {
let mut smbios_block = String::new();
smbios_block.push_str(lines[i]);
smbios_block.push('\n');
if !trimmed.ends_with(')') || trimmed == "SMBIOS_OPTS=(" {
i += 1;
while i < lines.len() {
smbios_block.push_str(lines[i]);
smbios_block.push('\n');
if lines[i].trim().ends_with(')') {
break;
}
i += 1;
}
}
components.smbios_opts = Some(smbios_block);
} else if trimmed.starts_with("SMBIOS_OPTS=") {
components.smbios_opts = Some(trimmed.to_string());
}
i += 1;
}
components
}
fn extract_quoted_value(line: &str, prefix: &str) -> Option<String> {
let rest = line.strip_prefix(prefix)?;
let rest = rest.trim();
if rest.starts_with('"') && rest.len() > 1 {
let end = rest[1..].find('"').map(|i| i + 1)?;
Some(rest[1..end].to_string())
} else if rest.starts_with('\'') && rest.len() > 1 {
let end = rest[1..].find('\'').map(|i| i + 1)?;
Some(rest[1..end].to_string())
} else {
Some(rest.split_whitespace().next()?.to_string())
}
}
pub fn generate_single_gpu_scripts(
vm: &DiscoveredVm,
config: &SingleGpuConfig,
) -> Result<GeneratedScripts> {
let vm_dir = &vm.path;
crate::hardware::save_config(vm_dir, config)
.with_context(|| "Failed to save single-GPU config")?;
let start_content = generate_start_script(vm, config)?;
let start_path = vm_dir.join("single-gpu-start.sh");
write_executable_script(&start_path, &start_content)?;
let restore_content = generate_restore_script(vm, config);
let restore_path = vm_dir.join("single-gpu-restore.sh");
write_executable_script(&restore_path, &restore_content)?;
Ok(GeneratedScripts {
start_script: start_path,
restore_script: restore_path,
})
}
fn write_executable_script(path: &Path, content: &str) -> Result<()> {
fs::write(path, content).with_context(|| format!("Failed to write script: {:?}", path))?;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms)?;
Ok(())
}
fn generate_start_script(vm: &DiscoveredVm, config: &SingleGpuConfig) -> Result<String> {
let gpu_addr = &config.gpu.address;
let audio_addr = config
.audio
.as_ref()
.map(|a| a.address.as_str())
.unwrap_or("");
let original_driver = config.original_driver.module_name();
let display_manager = config.display_manager.service_name();
let vm_dir = vm.path.display();
let vm_name = vm.display_name();
let launch_script = fs::read_to_string(&vm.launch_script)
.with_context(|| format!("Failed to read launch script: {:?}", vm.launch_script))?;
let components = parse_launch_script(&launch_script);
let modules_to_unload = config.original_driver.dependent_modules();
let unload_modules_cmd = if !modules_to_unload.is_empty() {
modules_to_unload
.iter()
.map(|m| format!(" modprobe -r {} 2>/dev/null || true", m))
.collect::<Vec<_>>()
.join("\n")
} else {
" # No additional modules to unload".to_string()
};
let usb_devices = load_usb_passthrough(vm);
let usb_passthrough_args = generate_usb_passthrough_args(&usb_devices);
let pci_passthrough_args = load_pci_passthrough(vm);
let extra_pci_addrs = extract_pci_addresses(&pci_passthrough_args);
let extra_pci_addrs_str = if extra_pci_addrs.is_empty() {
"EXTRA_PCI_ADDRS=()".to_string()
} else {
format!(
"EXTRA_PCI_ADDRS=({})",
extra_pci_addrs
.iter()
.map(|a| format!("\"{}\"", a))
.collect::<Vec<_>>()
.join(" ")
)
};
let qemu_command = extract_qemu_command_for_passthrough(
vm,
config,
&components,
&usb_passthrough_args,
&pci_passthrough_args,
)?;
let variable_defs = generate_variable_definitions(vm, &components);
let tpm_functions = if components.has_tpm {
generate_tpm_functions(&components)
} else {
String::new()
};
let tpm_start = if components.has_tpm {
r#"
# Start TPM emulator
start_tpm
"#
.to_string()
} else {
String::new()
};
let nvidia_module_load = if original_driver == "nvidia" {
r#"
# Load NVIDIA modules in dependency order
modprobe nvidia 2>/dev/null || true
sleep 1
modprobe nvidia_modeset 2>/dev/null || true
sleep 0.5
modprobe nvidia_drm 2>/dev/null || true
modprobe nvidia_uvm 2>/dev/null || true
sleep 1"#
.to_string()
} else {
format!(
r#"
modprobe "{}" 2>/dev/null || true
sleep 1"#,
original_driver
)
};
let script = format!(
r#"#!/bin/bash
# Single GPU Passthrough Start Script
# Generated by vm-curator for: {vm_name}
#
# This script must be run from a TTY (Ctrl+Alt+F3), not from a graphical terminal.
# It will stop your display manager, pass your GPU to the VM, and restore
# the display when the VM exits.
#
# For single-GPU passthrough, the VM's display goes directly to physical monitors.
set -e
# ============================================================================
# Configuration
# ============================================================================
VM_DIR="{vm_dir}"
VM_NAME="{vm_name}"
GPU_ADDR="{gpu_addr}"
AUDIO_ADDR="{audio_addr}"
ORIGINAL_DRIVER="{original_driver}"
DISPLAY_MANAGER="{display_manager}"
{extra_pci_addrs}
{variable_defs}
# ============================================================================
# Safety Checks
# ============================================================================
# Must run as root or with sudo
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root (use sudo)"
exit 1
fi
# Must run from TTY, not graphical terminal
if [[ -n "$DISPLAY" ]] || [[ -n "$WAYLAND_DISPLAY" ]]; then
echo "ERROR: This script must be run from a TTY (Ctrl+Alt+F3), not a graphical terminal"
echo ""
echo "Instructions:"
echo " 1. Press Ctrl+Alt+F3 to switch to TTY3"
echo " 2. Log in with your username"
echo " 3. Run: sudo $0"
exit 1
fi
# Check that VFIO modules are available
if ! modinfo vfio_pci &>/dev/null; then
echo "ERROR: vfio_pci module not available"
echo "Run System Setup from vm-curator Settings > Single GPU Passthrough"
exit 1
fi
{tpm_functions}
# ============================================================================
# Cleanup Function
# ============================================================================
cleanup() {{
local exit_code=$?
echo ""
echo "Cleaning up and restoring display..."
# Kill any lingering QEMU processes for this VM
pkill -f "qemu.*$VM_NAME" 2>/dev/null || true
{tpm_cleanup}
# Unbind from vfio-pci using PCI remove+rescan pattern
if [[ -e "/sys/bus/pci/devices/$GPU_ADDR" ]]; then
echo "Removing GPU from PCI bus..."
echo 1 > /sys/bus/pci/devices/$GPU_ADDR/remove 2>/dev/null || true
fi
if [[ -n "$AUDIO_ADDR" ]] && [[ -e "/sys/bus/pci/devices/$AUDIO_ADDR" ]]; then
echo 1 > /sys/bus/pci/devices/$AUDIO_ADDR/remove 2>/dev/null || true
fi
# Remove extra PCI devices from bus (will be re-bound on rescan)
for addr in "${{EXTRA_PCI_ADDRS[@]}}"; do
if [[ -e "/sys/bus/pci/devices/$addr" ]]; then
echo 1 > /sys/bus/pci/devices/$addr/remove 2>/dev/null || true
fi
done
sleep 2
# Rescan PCI bus
echo "Rescanning PCI bus..."
echo 1 > /sys/bus/pci/rescan
sleep 3
# Unload VFIO modules
modprobe -r vfio_pci 2>/dev/null || true
modprobe -r vfio_iommu_type1 2>/dev/null || true
modprobe -r vfio 2>/dev/null || true
sleep 1
{nvidia_module_load}
# Manual bind fallback if GPU doesn't auto-bind
if [[ -e "/sys/bus/pci/devices/$GPU_ADDR" ]] && [[ ! -e "/sys/bus/pci/devices/$GPU_ADDR/driver" ]]; then
echo "Manual bind to $ORIGINAL_DRIVER..."
echo "$GPU_ADDR" > /sys/bus/pci/drivers/$ORIGINAL_DRIVER/bind 2>/dev/null || true
fi
# Restart display manager
echo "Starting display manager..."
systemctl start "$DISPLAY_MANAGER" 2>/dev/null || true
echo "Display restored."
exit $exit_code
}}
trap cleanup EXIT INT TERM
# ============================================================================
# Stop Display Manager
# ============================================================================
echo "Stopping display manager ($DISPLAY_MANAGER)..."
systemctl stop "$DISPLAY_MANAGER"
sleep 2
# For NVIDIA, stop persistence daemon
if [[ "$ORIGINAL_DRIVER" == "nvidia" ]]; then
systemctl stop nvidia-persistenced 2>/dev/null || true
sleep 1
fi
# ============================================================================
# Unload GPU Driver
# ============================================================================
echo "Unloading GPU driver modules..."
# Kill any processes using the GPU (try common ones)
for proc in Xorg Xwayland gnome-shell kwin_wayland plasmashell sway hyprland; do
pkill -9 "$proc" 2>/dev/null || true
done
sleep 2
# Unload driver modules
{unload_modules_cmd}
# Verify driver is unloaded
if [[ -e "/sys/bus/pci/devices/$GPU_ADDR/driver" ]]; then
current_driver=$(basename $(readlink /sys/bus/pci/devices/$GPU_ADDR/driver))
if [[ "$current_driver" != "vfio-pci" ]]; then
echo "$GPU_ADDR" > /sys/bus/pci/drivers/$current_driver/unbind 2>/dev/null || true
fi
fi
if [[ -n "$AUDIO_ADDR" ]] && [[ -e "/sys/bus/pci/devices/$AUDIO_ADDR/driver" ]]; then
current_driver=$(basename $(readlink /sys/bus/pci/devices/$AUDIO_ADDR/driver))
if [[ "$current_driver" != "vfio-pci" ]]; then
echo "$AUDIO_ADDR" > /sys/bus/pci/drivers/$current_driver/unbind 2>/dev/null || true
fi
fi
# ============================================================================
# Bind to VFIO
# ============================================================================
echo "Binding GPU to vfio-pci..."
modprobe vfio_pci
# Set driver override and bind
echo "vfio-pci" > /sys/bus/pci/devices/$GPU_ADDR/driver_override
echo "$GPU_ADDR" > /sys/bus/pci/drivers/vfio-pci/bind
if [[ -n "$AUDIO_ADDR" ]]; then
echo "vfio-pci" > /sys/bus/pci/devices/$AUDIO_ADDR/driver_override
echo "$AUDIO_ADDR" > /sys/bus/pci/drivers/vfio-pci/bind
fi
# Bind extra PCI devices (network cards, USB controllers, NVMe, etc.)
for addr in "${{EXTRA_PCI_ADDRS[@]}}"; do
echo "Binding $addr to vfio-pci..."
# Unbind from current driver if bound
if [[ -e "/sys/bus/pci/devices/$addr/driver" ]]; then
current_driver=$(basename $(readlink /sys/bus/pci/devices/$addr/driver))
if [[ "$current_driver" != "vfio-pci" ]]; then
echo "$addr" > /sys/bus/pci/drivers/$current_driver/unbind 2>/dev/null || true
fi
fi
echo "vfio-pci" > /sys/bus/pci/devices/$addr/driver_override
echo "$addr" > /sys/bus/pci/drivers/vfio-pci/bind
done
# Verify binding
if [[ ! -e "/sys/bus/pci/drivers/vfio-pci/$GPU_ADDR" ]]; then
echo "ERROR: Failed to bind GPU to vfio-pci"
exit 1
fi
echo "GPU successfully bound to vfio-pci"
{tpm_start}
# ============================================================================
# Start VM
# ============================================================================
echo ""
echo "============================================"
echo "Starting VM: $VM_NAME"
echo "============================================"
echo "Note: Display will appear on your physical monitor(s)"
echo ""
# Run QEMU (no 'exec' so cleanup trap runs)
cd "$VM_DIR"
{qemu_command}
echo ""
echo "VM has exited."
# Cleanup will run via trap
"#,
vm_name = vm_name,
vm_dir = vm_dir,
gpu_addr = gpu_addr,
audio_addr = audio_addr,
original_driver = original_driver,
display_manager = display_manager,
extra_pci_addrs = extra_pci_addrs_str,
variable_defs = variable_defs,
tpm_functions = tpm_functions,
tpm_cleanup = if components.has_tpm {
r#"
# Kill TPM emulator
if [[ -n "$TPM_DIR" ]]; then
pkill -f "swtpm.*$TPM_DIR" 2>/dev/null || true
fi
"#
} else {
""
},
nvidia_module_load = nvidia_module_load,
unload_modules_cmd = unload_modules_cmd,
tpm_start = tpm_start,
qemu_command = qemu_command,
);
Ok(script)
}
fn generate_variable_definitions(vm: &DiscoveredVm, components: &LaunchScriptComponents) -> String {
let mut vars = Vec::new();
if let Some(ref disk_var) = components.disk_var {
vars.push(disk_var.clone());
} else if let Some(disk) = vm.config.disks.first() {
vars.push(format!("DISK=\"{}\"", disk.path.display()));
}
if components.has_uefi {
if let Some(ref ovmf_code) = components.ovmf_code {
vars.push(format!("OVMF_CODE=\"{}\"", ovmf_code));
}
if let Some(ref ovmf_vars) = components.ovmf_vars {
vars.push(format!("OVMF_VARS=\"{}\"", ovmf_vars));
}
}
if components.has_tpm {
if let Some(ref tpm_dir) = components.tpm_dir {
vars.push(format!("TPM_DIR=\"{}\"", tpm_dir));
} else {
vars.push(format!("TPM_DIR=\"{}/tpm\"", vm.path.display()));
}
}
if let Some(ref smbios) = components.smbios_opts {
vars.push(smbios.clone());
}
if vars.is_empty() {
String::new()
} else {
vars.join("\n") + "\n"
}
}
fn generate_tpm_functions(components: &LaunchScriptComponents) -> String {
let tpm_dir = components.tpm_dir.as_deref().unwrap_or("$VM_DIR/tpm");
format!(
r#"
# ============================================================================
# TPM Functions
# ============================================================================
init_tpm() {{
if [[ ! -d "{tpm_dir}" ]]; then
echo "Initializing TPM state directory..."
mkdir -p "{tpm_dir}"
swtpm_setup --tpmstate "{tpm_dir}" --tpm2 --create-ek-cert --create-platform-cert
fi
}}
start_tpm() {{
init_tpm
echo "Starting TPM emulator..."
swtpm socket \
--tpmstate dir="{tpm_dir}" \
--ctrl type=unixio,path="{tpm_dir}/swtpm-sock" \
--tpm2 \
--daemon
sleep 1
}}
"#,
tpm_dir = tpm_dir
)
}
fn extract_pci_addresses(pci_args: &[String]) -> Vec<String> {
pci_args
.iter()
.filter_map(|arg| {
arg.split("host=")
.nth(1)
.map(|s| s.split([',', ' ']).next().unwrap_or(s).to_string())
})
.collect()
}
fn generate_usb_passthrough_args(devices: &[crate::vm::UsbPassthrough]) -> String {
if devices.is_empty() {
return String::new();
}
let mut args = vec!["-usb".to_string()];
let has_usb3 = devices.iter().any(|d| d.is_usb3());
if has_usb3 {
args.push("-device qemu-xhci,id=xhci,p2=8,p3=8".to_string());
}
for dev in devices {
if dev.is_usb3() {
args.push(format!(
"-device usb-host,bus=xhci.0,vendorid=0x{:04x},productid=0x{:04x}",
dev.vendor_id, dev.product_id
));
} else {
args.push(format!(
"-device usb-host,vendorid=0x{:04x},productid=0x{:04x}",
dev.vendor_id, dev.product_id
));
}
}
args.join(" \\\n ")
}
fn generate_restore_script(vm: &DiscoveredVm, config: &SingleGpuConfig) -> String {
let gpu_addr = &config.gpu.address;
let audio_addr = config
.audio
.as_ref()
.map(|a| a.address.as_str())
.unwrap_or("");
let original_driver = config.original_driver.module_name();
let display_manager = config.display_manager.service_name();
let vm_name = vm.display_name();
let vm_dir = vm.path.display();
let launch_script = fs::read_to_string(&vm.launch_script).unwrap_or_default();
let components = parse_launch_script(&launch_script);
let pci_passthrough_args = load_pci_passthrough(vm);
let extra_pci_addrs = extract_pci_addresses(&pci_passthrough_args);
let extra_pci_addrs_str = if extra_pci_addrs.is_empty() {
"EXTRA_PCI_ADDRS=()".to_string()
} else {
format!(
"EXTRA_PCI_ADDRS=({})",
extra_pci_addrs
.iter()
.map(|a| format!("\"{}\"", a))
.collect::<Vec<_>>()
.join(" ")
)
};
let tpm_cleanup = if components.has_tpm {
let tpm_dir = components.tpm_dir.as_deref().unwrap_or("$VM_DIR/tpm");
format!(
r#"
# Kill TPM emulator if running
TPM_DIR="{tpm_dir}"
pkill -f "swtpm.*$TPM_DIR" 2>/dev/null || true
"#,
tpm_dir = tpm_dir
)
} else {
String::new()
};
let nvidia_module_load = if original_driver == "nvidia" {
r#"
# Load NVIDIA modules in dependency order
echo "Loading NVIDIA modules..."
modprobe nvidia 2>/dev/null || true
sleep 1
modprobe nvidia_modeset 2>/dev/null || true
sleep 0.5
modprobe nvidia_drm 2>/dev/null || true
modprobe nvidia_uvm 2>/dev/null || true
sleep 1"#
.to_string()
} else {
format!(
r#"
echo "Loading {} driver..."
modprobe "{}" 2>/dev/null || true
sleep 2"#,
original_driver, original_driver
)
};
format!(
r#"#!/bin/bash
# Single GPU Passthrough Restore Script
# Generated by vm-curator for: {vm_name}
#
# Use this script to restore your display if something goes wrong.
# Can be run from SSH or a recovery boot.
set -e
VM_DIR="{vm_dir}"
VM_NAME="{vm_name}"
GPU_ADDR="{gpu_addr}"
AUDIO_ADDR="{audio_addr}"
ORIGINAL_DRIVER="{original_driver}"
DISPLAY_MANAGER="{display_manager}"
{extra_pci_addrs}
# Must run as root
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root (use sudo)"
exit 1
fi
echo "Restoring display..."
# Kill any lingering QEMU processes
echo "Killing any QEMU processes..."
pkill -f "qemu.*$VM_NAME" 2>/dev/null || true
{tpm_cleanup}
# Remove and rescan PCI devices (more reliable than unbind)
echo "Removing GPU from PCI bus..."
if [[ -e "/sys/bus/pci/devices/$GPU_ADDR" ]]; then
echo 1 > /sys/bus/pci/devices/$GPU_ADDR/remove 2>/dev/null || true
fi
if [[ -n "$AUDIO_ADDR" ]] && [[ -e "/sys/bus/pci/devices/$AUDIO_ADDR" ]]; then
echo 1 > /sys/bus/pci/devices/$AUDIO_ADDR/remove 2>/dev/null || true
fi
# Remove extra PCI devices from bus (will be re-bound on rescan)
for addr in "${{EXTRA_PCI_ADDRS[@]}}"; do
if [[ -e "/sys/bus/pci/devices/$addr" ]]; then
echo "Removing $addr from PCI bus..."
echo 1 > /sys/bus/pci/devices/$addr/remove 2>/dev/null || true
fi
done
sleep 2
# Rescan PCI bus
echo "Rescanning PCI bus..."
echo 1 > /sys/bus/pci/rescan
sleep 3
# Unload VFIO modules
echo "Unloading VFIO modules..."
modprobe -r vfio_pci 2>/dev/null || true
modprobe -r vfio_iommu_type1 2>/dev/null || true
modprobe -r vfio 2>/dev/null || true
sleep 1
# Load original driver
if [[ -n "$ORIGINAL_DRIVER" ]] && [[ "$ORIGINAL_DRIVER" != "vfio-pci" ]]; then
{nvidia_module_load}
# Manual bind fallback if GPU doesn't auto-bind
if [[ -e "/sys/bus/pci/devices/$GPU_ADDR" ]] && [[ ! -e "/sys/bus/pci/devices/$GPU_ADDR/driver" ]]; then
echo "Manual bind to $ORIGINAL_DRIVER..."
echo "$GPU_ADDR" > /sys/bus/pci/drivers/$ORIGINAL_DRIVER/bind 2>/dev/null || true
fi
fi
# Restart display manager
echo "Starting display manager..."
systemctl start "$DISPLAY_MANAGER" 2>/dev/null || true
echo ""
echo "Restore complete!"
echo "If display still doesn't work, try rebooting."
"#,
vm_name = vm_name,
vm_dir = vm_dir,
gpu_addr = gpu_addr,
audio_addr = audio_addr,
original_driver = original_driver,
display_manager = display_manager,
extra_pci_addrs = extra_pci_addrs_str,
tpm_cleanup = tpm_cleanup,
nvidia_module_load = nvidia_module_load,
)
}
#[derive(Debug)]
pub enum SystemSetupResult {
Launched,
NoTerminal,
Error(String),
}
pub fn run_system_setup(gpu_driver: &str) -> SystemSetupResult {
use std::process::Command;
let script_content = generate_interactive_setup_script(gpu_driver);
let script_path = "/tmp/vm-curator-vfio-setup.sh";
if let Err(e) = std::fs::write(script_path, &script_content) {
return SystemSetupResult::Error(format!("Failed to write setup script: {}", e));
}
if let Err(e) = std::fs::set_permissions(
script_path,
std::os::unix::fs::PermissionsExt::from_mode(0o755),
) {
return SystemSetupResult::Error(format!("Failed to make script executable: {}", e));
}
let terminals: &[(&str, &[&str])] = &[
("alacritty", &["-e", "sudo", script_path]),
("kitty", &["sudo", script_path]),
("ghostty", &["-e", "sudo", script_path]),
("gnome-terminal", &["--", "sudo", script_path]),
("konsole", &["-e", "sudo", script_path]),
("xfce4-terminal", &["-x", "sudo", script_path]),
("xterm", &["-e", "sudo", script_path]),
];
for (term, args) in terminals {
if Command::new("which")
.arg(term)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
match Command::new(term).args(*args).spawn() {
Ok(_) => return SystemSetupResult::Launched,
Err(_) => continue,
}
}
}
SystemSetupResult::NoTerminal
}
fn generate_interactive_setup_script(gpu_driver: &str) -> String {
let softdep_line = format!("softdep {} pre: vfio-pci", gpu_driver);
format!(
r#"#!/bin/bash
# Single GPU Passthrough System Setup
# Generated by vm-curator
#
# This script configures your system for single GPU passthrough.
set -e
echo "========================================"
echo " Single GPU Passthrough System Setup"
echo "========================================"
echo ""
echo "This will:"
echo " 1. Create /etc/modules-load.d/vfio.conf"
echo " 2. Create /etc/modprobe.d/vfio.conf"
echo " 3. Regenerate initramfs (if applicable)"
echo ""
echo "Press Enter to continue or Ctrl+C to cancel..."
read
echo ""
echo "[1/3] Creating VFIO modules configuration..."
cat > /etc/modules-load.d/vfio.conf << 'EOF'
# VFIO modules for GPU passthrough
vfio
vfio_iommu_type1
vfio_pci
EOF
echo " Created /etc/modules-load.d/vfio.conf"
echo ""
echo "[2/3] Creating modprobe configuration..."
cat > /etc/modprobe.d/vfio.conf << 'EOF'
# Load VFIO before GPU driver
{softdep_line}
options vfio_pci disable_vga=1
EOF
echo " Created /etc/modprobe.d/vfio.conf"
echo ""
echo "[3/3] Updating boot configuration..."
echo ""
# Detect bootloader and initramfs tool
INITRAMFS_UPDATED=false
if command -v mkinitcpio &>/dev/null; then
echo "Detected mkinitcpio - regenerating initramfs..."
echo ""
mkinitcpio -P
INITRAMFS_UPDATED=true
elif command -v booster &>/dev/null; then
echo "Detected booster - regenerating initramfs..."
echo ""
booster build --force
INITRAMFS_UPDATED=true
elif command -v update-initramfs &>/dev/null; then
echo "Detected update-initramfs - regenerating initramfs..."
echo ""
update-initramfs -u -k all
INITRAMFS_UPDATED=true
elif command -v dracut &>/dev/null; then
echo "Detected dracut - regenerating initramfs..."
echo ""
dracut -f --regenerate-all
INITRAMFS_UPDATED=true
fi
# Check for Limine bootloader
if [[ -f /boot/limine.cfg ]] || [[ -f /boot/limine/limine.cfg ]] || [[ -f /boot/EFI/BOOT/limine.cfg ]]; then
echo ""
echo "Detected Limine bootloader."
if [[ "$INITRAMFS_UPDATED" == "true" ]]; then
echo "Initramfs has been updated. Limine should pick up changes on reboot."
else
echo ""
echo "If you use an initramfs with Limine, please regenerate it manually."
echo "If you boot without an initramfs, the module configs will be loaded"
echo "by systemd-modules-load.service after boot."
fi
fi
# Check for systemd-boot
if [[ -d /boot/loader ]] || bootctl is-installed &>/dev/null 2>&1; then
if [[ "$INITRAMFS_UPDATED" == "false" ]]; then
echo ""
echo "Detected systemd-boot. Please ensure your initramfs is regenerated"
echo "if you use one, or the modules will load via systemd-modules-load."
fi
fi
if [[ "$INITRAMFS_UPDATED" == "false" ]]; then
echo ""
echo "No standard initramfs tool detected."
echo "The VFIO modules will be loaded by systemd-modules-load.service on boot."
echo "This should work for most setups, but if you use a custom initramfs,"
echo "please regenerate it manually to include the VFIO modules."
fi
echo ""
echo "========================================"
echo " Setup Complete!"
echo "========================================"
echo ""
echo "You must REBOOT your system for changes to take effect."
echo ""
echo "After reboot, to use single GPU passthrough:"
echo " 1. Press Ctrl+Alt+F3 to switch to TTY3"
echo " 2. Log in with your username"
echo " 3. Run: sudo ./single-gpu-start.sh (in your VM directory)"
echo ""
echo "Press Enter to close this window..."
read
"#,
softdep_line = softdep_line,
)
}
fn extract_qemu_command_for_passthrough(
vm: &DiscoveredVm,
config: &SingleGpuConfig,
components: &LaunchScriptComponents,
usb_passthrough_args: &str,
pci_passthrough_args: &[String],
) -> Result<String> {
let launch_script = fs::read_to_string(&vm.launch_script)
.with_context(|| format!("Failed to read launch script: {:?}", vm.launch_script))?;
let mut qemu_lines = Vec::new();
let mut in_qemu_command = false;
let mut found_qemu = false;
for line in launch_script.lines() {
let trimmed = line.trim();
if !in_qemu_command {
if (trimmed.starts_with("qemu-system-")
|| trimmed.starts_with("\"$QEMU\"")
|| trimmed.starts_with("$QEMU "))
&& !trimmed.starts_with('#')
{
in_qemu_command = true;
found_qemu = true;
} else if trimmed.starts_with("exec qemu-system-") && !trimmed.starts_with('#') {
in_qemu_command = true;
found_qemu = true;
let without_exec = trimmed.strip_prefix("exec ").unwrap_or(trimmed);
qemu_lines.push(without_exec.to_string());
if !trimmed.ends_with('\\') {
break;
}
continue;
}
}
if in_qemu_command {
qemu_lines.push(line.to_string());
if !trimmed.ends_with('\\') {
break;
}
}
}
if !found_qemu {
return Ok(generate_basic_qemu_command(
vm,
config,
components,
usb_passthrough_args,
pci_passthrough_args,
));
}
let mut qemu_cmd = qemu_lines.join("\n");
qemu_cmd = RE_NAME
.replace_all(&qemu_cmd, r#"-name "$VM_NAME""#)
.to_string();
qemu_cmd = RE_DISPLAY.replace_all(&qemu_cmd, "").to_string();
qemu_cmd = RE_VGA.replace_all(&qemu_cmd, "").to_string();
qemu_cmd = RE_AUDIODEV.replace_all(&qemu_cmd, "").to_string();
qemu_cmd = RE_SOUNDHW.replace_all(&qemu_cmd, "").to_string();
qemu_cmd = RE_CDROM.replace_all(&qemu_cmd, "").to_string();
qemu_cmd = RE_DRIVE_CDROM.replace_all(&qemu_cmd, "").to_string();
qemu_cmd = RE_DRIVE_ISO.replace_all(&qemu_cmd, "").to_string();
while RE_EMPTY_CONT.is_match(&qemu_cmd) {
qemu_cmd = RE_EMPTY_CONT.replace_all(&qemu_cmd, "\\\n").to_string();
}
let mut passthrough_args = Vec::new();
passthrough_args.push(format!(
"-device vfio-pci,host={},multifunction=on",
config.gpu.address
));
if let Some(ref audio) = config.audio {
passthrough_args.push(format!("-device vfio-pci,host={}", audio.address));
}
passthrough_args.push("-display none".to_string());
passthrough_args.push("-vga none".to_string());
passthrough_args.push("-device virtio-rng-pci".to_string());
if !usb_passthrough_args.is_empty() {
passthrough_args.push(usb_passthrough_args.to_string());
}
for pci_arg in pci_passthrough_args {
passthrough_args.push(pci_arg.clone());
}
let nvidia_cpu_flags = if config.gpu.is_nvidia() {
Some("-cpu host,kvm=off,hv_vendor_id=AuthenticAMD,hv_relaxed,hv_spinlocks=0x1fff,hv_vapic,hv_time".to_string())
} else {
None
};
let passthrough_str = passthrough_args.join(" \\\n ");
if let Some(last_backslash) = qemu_cmd.rfind('\\') {
let (before, _) = qemu_cmd.split_at(last_backslash);
qemu_cmd = format!("{} \\\n {}", before.trim_end(), passthrough_str);
} else {
qemu_cmd = format!("{} \\\n {}", qemu_cmd.trim_end(), passthrough_str);
}
if let Some(flags) = nvidia_cpu_flags {
if RE_CPU_HOST.is_match(&qemu_cmd) {
qemu_cmd = RE_CPU_HOST.replace(&qemu_cmd, flags.as_str()).to_string();
}
}
if !qemu_cmd.contains("kernel_irqchip") {
if let Some(caps) = RE_MACHINE.captures(&qemu_cmd) {
let machine_opts = caps.get(1).unwrap().as_str();
if !machine_opts.contains("kernel_irqchip") {
let new_opts = format!("{},kernel_irqchip=on", machine_opts);
qemu_cmd = RE_MACHINE
.replace(&qemu_cmd, format!("-machine {}", new_opts).as_str())
.to_string();
}
}
}
qemu_cmd = RE_BOOT_D.replace(&qemu_cmd, "-boot order=c").to_string();
Ok(qemu_cmd)
}
fn generate_basic_qemu_command(
vm: &DiscoveredVm,
config: &SingleGpuConfig,
components: &LaunchScriptComponents,
usb_passthrough_args: &str,
pci_passthrough_args: &[String],
) -> String {
let qemu_emulator = vm.config.emulator.command();
let memory = vm.config.memory_mb;
let cpu_cores = vm.config.cpu_cores;
let cpu_flags = if config.gpu.is_nvidia() {
"host,kvm=off,hv_vendor_id=AuthenticAMD,hv_relaxed,hv_spinlocks=0x1fff,hv_vapic,hv_time"
} else {
"host"
};
let mut cmd = format!(
r#"{} \
-name "$VM_NAME" \
-machine q35,accel=kvm,kernel_irqchip=on \
-cpu {} \
-m {} \
-smp {} \
-enable-kvm"#,
qemu_emulator, cpu_flags, memory, cpu_cores
);
if components.smbios_opts.is_some() {
cmd.push_str(
r#" \
"${SMBIOS_OPTS[@]}""#,
);
}
if components.has_uefi {
cmd.push_str(
r#" \
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
-drive if=pflash,format=raw,file="$OVMF_VARS""#,
);
}
if components.has_tpm {
cmd.push_str(
r#" \
-chardev socket,id=chrtpm,path="$TPM_DIR/swtpm-sock" \
-tpmdev emulator,id=tpm0,chardev=chrtpm \
-device tpm-tis,tpmdev=tpm0"#,
);
}
if let Some(disk) = vm.config.disks.first() {
let format_str = match &disk.format {
crate::vm::qemu_config::DiskFormat::Qcow2 => "qcow2",
crate::vm::qemu_config::DiskFormat::Raw => "raw",
crate::vm::qemu_config::DiskFormat::Vmdk => "vmdk",
crate::vm::qemu_config::DiskFormat::Vdi => "vdi",
crate::vm::qemu_config::DiskFormat::Other(s) => s.as_str(),
};
cmd.push_str(&format!(
r#" \
-drive file="$DISK",format={},if=virtio"#,
format_str
));
}
cmd.push_str(&format!(
r#" \
-device vfio-pci,host={},multifunction=on"#,
config.gpu.address
));
if let Some(ref audio) = config.audio {
cmd.push_str(&format!(
r#" \
-device vfio-pci,host={}"#,
audio.address
));
}
cmd.push_str(
r#" \
-display none \
-vga none"#,
);
cmd.push_str(
r#" \
-device virtio-rng-pci"#,
);
if !usb_passthrough_args.is_empty() {
cmd.push_str(&format!(
r#" \
{}"#,
usb_passthrough_args
));
} else {
cmd.push_str(
r#" \
# USB passthrough - configure via USB Passthrough in VM Management
-usb"#,
);
}
for pci_arg in pci_passthrough_args {
cmd.push_str(&format!(
r#" \
{}"#,
pci_arg
));
}
cmd.push_str(
r#" \
-boot order=c"#,
);
cmd
}
pub fn delete_scripts(vm_path: &Path) -> Result<()> {
let scripts = ["single-gpu-start.sh", "single-gpu-restore.sh"];
for script in scripts {
let path = vm_path.join(script);
if path.exists() {
fs::remove_file(&path)
.with_context(|| format!("Failed to delete script: {:?}", path))?;
}
}
Ok(())
}
pub fn regenerate_if_exists(vm: &DiscoveredVm, config: &SingleGpuConfig) -> Result<bool> {
use crate::hardware::scripts_exist;
if !scripts_exist(&vm.path) {
return Ok(false);
}
generate_single_gpu_scripts(vm, config)?;
Ok(true)
}
pub fn regenerate_from_saved_config(vm: &DiscoveredVm) -> Result<bool> {
use crate::hardware::{load_config, scripts_exist};
if !scripts_exist(&vm.path) {
return Ok(false);
}
let config =
load_config(&vm.path).ok_or_else(|| anyhow::anyhow!("No saved single-GPU config found"))?;
generate_single_gpu_scripts(vm, &config)?;
Ok(true)
}