use std::path::Path;
use std::time::Duration;
use arcbox_vz::{
MacAuxiliaryStorage, MacGraphicsDeviceConfiguration, MacHardwareModel, MacMachineIdentifier,
MacOSBootLoader, MacPlatform, NetworkDeviceConfiguration, StorageDeviceConfiguration,
VirtualMachine, VirtualMachineConfiguration, VirtualMachineState, min_cpu_count,
min_memory_size,
};
use crate::error::{CoreError, Result};
const READINESS_POLL_INTERVAL: Duration = Duration::from_millis(250);
const READINESS_MAX_POLLS: u32 = 240;
pub struct MacVm {
vm: VirtualMachine,
}
impl MacVm {
#[allow(
clippy::future_not_send,
reason = "drives Virtualization.framework through ObjC pointers and the VM's dispatch queue, which are inherently !Send and held across await; the caller drives it on a single thread"
)]
pub async fn boot(
disk: &Path,
aux: &Path,
hardware_model: &[u8],
machine_id: &[u8],
mac_address: &str,
cpus: u32,
memory_mib: u64,
) -> Result<Self> {
let hardware_model = MacHardwareModel::from_data(hardware_model)?;
let machine_id = MacMachineIdentifier::from_data(machine_id)?;
let aux = MacAuxiliaryStorage::open(aux)?;
let platform = MacPlatform::new(&hardware_model, &machine_id, &aux)?;
let cpu_count = usize::try_from(u64::from(cpus).max(min_cpu_count())).unwrap_or(1);
let memory = (memory_mib * 1024 * 1024).max(min_memory_size());
let mut config = VirtualMachineConfiguration::new()?;
config
.set_cpu_count(cpu_count)
.set_memory_size(memory)
.set_platform(platform)
.set_boot_loader(MacOSBootLoader::new()?)
.add_storage_device(StorageDeviceConfiguration::disk_image(disk, false)?)
.add_network_device(NetworkDeviceConfiguration::nat_with_mac(mac_address)?)
.add_graphics_device(MacGraphicsDeviceConfiguration::new(1920, 1080, 80)?);
config.validate()?;
let vm = config.build()?;
vm.start().await?;
let mut polls = 0;
while vm.state() != VirtualMachineState::Running && polls < READINESS_MAX_POLLS {
tokio::time::sleep(READINESS_POLL_INTERVAL).await;
polls += 1;
}
if vm.state() != VirtualMachineState::Running {
return Err(CoreError::macos(format!(
"macOS VM did not reach Running (state = {:?})",
vm.state()
)));
}
Ok(Self { vm })
}
#[must_use]
pub fn state(&self) -> VirtualMachineState {
self.vm.state()
}
#[must_use]
pub fn is_running(&self) -> bool {
self.vm.state() == VirtualMachineState::Running
}
pub fn request_stop(&self) -> Result<()> {
Ok(self.vm.request_stop()?)
}
#[allow(
clippy::future_not_send,
reason = "Virtualization.framework VM handles are !Send; the caller drives stop on a single thread"
)]
pub async fn stop(&self) -> Result<()> {
Ok(self.vm.stop().await?)
}
}