use std::os::fd::{FromRawFd, IntoRawFd};
use std::sync::mpsc;
use block2::RcBlock;
use dispatch2::{DispatchQueue, DispatchRetained};
use objc2::rc::Retained;
use objc2::{AnyThread, ClassType};
use objc2_foundation::{NSArray, NSError, NSFileHandle, NSString, NSURL};
use objc2_virtualization::{
VZDirectorySharingDeviceConfiguration, VZDiskImageStorageDeviceAttachment,
VZEntropyDeviceConfiguration, VZFileHandleSerialPortAttachment, VZLinuxBootLoader,
VZMemoryBalloonDeviceConfiguration, VZNATNetworkDeviceAttachment, VZNetworkDeviceConfiguration,
VZSerialPortConfiguration, VZSharedDirectory, VZSingleDirectoryShare,
VZSocketDeviceConfiguration, VZStorageDeviceConfiguration, VZVirtioBlockDeviceConfiguration,
VZVirtioConsoleDeviceSerialPortConfiguration, VZVirtioEntropyDeviceConfiguration,
VZVirtioFileSystemDeviceConfiguration, VZVirtioNetworkDeviceConfiguration,
VZVirtioSocketConnection, VZVirtioSocketDevice, VZVirtioSocketDeviceConfiguration,
VZVirtioTraditionalMemoryBalloonDeviceConfiguration, VZVirtualMachine,
VZVirtualMachineConfiguration, VZVirtualMachineState,
};
use crate::vm::{Paths, Resources, SHARE_TAG, VSOCK_PORT};
fn command_line(home: &str) -> String {
format!(
"console=hvc0 root=/dev/vda ro rootfstype=ext4 init=/sbin/ferrodock-agent \
ferrodock.data=/dev/vdb ferrodock.share={SHARE_TAG}:{home}"
)
}
fn url(path: &std::path::Path) -> Retained<NSURL> {
NSURL::fileURLWithPath(&NSString::from_str(&path.to_string_lossy()))
}
fn describe(error: &NSError) -> String {
error.localizedDescription().to_string()
}
pub struct Machine {
queue: DispatchRetained<DispatchQueue>,
vm: Retained<VZVirtualMachine>,
}
unsafe impl Send for Machine {}
unsafe impl Sync for Machine {}
impl Machine {
pub fn new(paths: &Paths, resources: &Resources) -> Result<Self, String> {
let config = configuration(paths, resources)?;
let queue = DispatchQueue::new("dev.ferrodock.engine", None);
let vm = unsafe {
VZVirtualMachine::initWithConfiguration_queue(
VZVirtualMachine::alloc(),
&config,
&queue,
)
};
Ok(Self { queue, vm })
}
fn on_queue<R: Send + 'static>(
&self,
work: impl FnOnce(&VZVirtualMachine) -> R + Send + 'static,
) -> R {
let (tx, rx) = mpsc::channel();
let vm = Shared(self.vm.clone());
self.queue.exec_async(move || {
let vm = vm;
let _ = tx.send(work(&vm.0));
});
rx.recv().expect("the VM queue dropped a result")
}
pub fn start(&self) -> Result<(), String> {
let (tx, rx) = mpsc::channel::<Result<(), String>>();
self.on_queue(move |vm| {
let done = RcBlock::new(move |error: *mut NSError| {
let result = match unsafe { error.as_ref() } {
Some(error) => Err(describe(error)),
None => Ok(()),
};
let _ = tx.send(result);
});
unsafe { vm.startWithCompletionHandler(&done) };
});
rx.recv().map_err(|_| "the VM never answered".to_owned())?
}
pub fn request_stop(&self) {
self.on_queue(|vm| unsafe {
if vm.canRequestStop() {
let _ = vm.requestStopWithError();
}
});
}
pub fn force_stop(&self) {
let (tx, rx) = mpsc::channel::<()>();
self.on_queue(move |vm| {
let stopped = tx.clone();
let done = RcBlock::new(move |_error: *mut NSError| {
let _ = stopped.send(());
});
unsafe {
if vm.canStop() {
vm.stopWithCompletionHandler(&done);
} else {
let _ = tx.send(());
}
}
});
let _ = rx.recv_timeout(std::time::Duration::from_secs(10));
}
pub fn running(&self) -> bool {
self.on_queue(|vm| unsafe { vm.state() } == VZVirtualMachineState::Running)
}
pub fn connect(&self) -> Result<std::os::unix::net::UnixStream, String> {
let (tx, rx) = mpsc::channel::<Result<i32, String>>();
self.on_queue(move |vm| {
let devices = unsafe { vm.socketDevices() };
let Some(device) = devices.firstObject() else {
let _ = tx.send(Err("the VM has no socket device".into()));
return;
};
let device: Retained<VZVirtioSocketDevice> =
unsafe { Retained::cast_unchecked(device) };
let done = RcBlock::new(
move |connection: *mut VZVirtioSocketConnection, error: *mut NSError| {
let result = match unsafe { (connection.as_ref(), error.as_ref()) } {
(Some(connection), _) => {
let fd = unsafe { libc::dup(connection.fileDescriptor()) };
if fd < 0 {
Err(std::io::Error::last_os_error().to_string())
} else {
Ok(fd)
}
}
(None, Some(error)) => Err(describe(error)),
(None, None) => Err("no connection".into()),
};
let _ = tx.send(result);
},
);
unsafe { device.connectToPort_completionHandler(VSOCK_PORT, &done) };
});
let fd = rx
.recv()
.map_err(|_| "the VM never answered".to_owned())??;
Ok(unsafe { std::os::unix::net::UnixStream::from_raw_fd(fd) })
}
}
struct Shared(Retained<VZVirtualMachine>);
unsafe impl Send for Shared {}
fn configuration(
paths: &Paths,
resources: &Resources,
) -> Result<Retained<VZVirtualMachineConfiguration>, String> {
let home = std::env::var("HOME").map_err(|_| "no home folder".to_owned())?;
unsafe {
let config = VZVirtualMachineConfiguration::new();
config.setCPUCount(resources.cpus as usize);
config.setMemorySize(resources.memory_mib * 1024 * 1024);
let boot =
VZLinuxBootLoader::initWithKernelURL(VZLinuxBootLoader::alloc(), &url(&paths.kernel));
boot.setCommandLine(&NSString::from_str(&command_line(&home)));
config.setBootLoader(Some(boot.as_super()));
let mut disks: Vec<Retained<VZStorageDeviceConfiguration>> = Vec::new();
for (path, read_only) in [(&paths.rootfs, true), (&paths.data, false)] {
let attachment = VZDiskImageStorageDeviceAttachment::initWithURL_readOnly_error(
VZDiskImageStorageDeviceAttachment::alloc(),
&url(path),
read_only,
)
.map_err(|e| format!("{}: {}", path.display(), describe(&e)))?;
let disk = VZVirtioBlockDeviceConfiguration::initWithAttachment(
VZVirtioBlockDeviceConfiguration::alloc(),
attachment.as_super(),
);
disks.push(Retained::into_super(disk));
}
config.setStorageDevices(&NSArray::from_retained_slice(&disks));
let network = VZVirtioNetworkDeviceConfiguration::new();
network.setAttachment(Some(VZNATNetworkDeviceAttachment::new().as_super()));
let networks: Vec<Retained<VZNetworkDeviceConfiguration>> =
vec![Retained::into_super(network)];
config.setNetworkDevices(&NSArray::from_retained_slice(&networks));
let log = std::fs::File::create(&paths.console)
.map_err(|e| format!("{}: {e}", paths.console.display()))?;
let handle = NSFileHandle::initWithFileDescriptor_closeOnDealloc(
NSFileHandle::alloc(),
log.into_raw_fd(),
true,
);
let console = VZVirtioConsoleDeviceSerialPortConfiguration::new();
let attachment =
VZFileHandleSerialPortAttachment::initWithFileHandleForReading_fileHandleForWriting(
VZFileHandleSerialPortAttachment::alloc(),
None,
Some(&handle),
);
console.setAttachment(Some(attachment.as_super()));
let ports: Vec<Retained<VZSerialPortConfiguration>> = vec![Retained::into_super(console)];
config.setSerialPorts(&NSArray::from_retained_slice(&ports));
let entropy: Vec<Retained<VZEntropyDeviceConfiguration>> = vec![Retained::into_super(
VZVirtioEntropyDeviceConfiguration::new(),
)];
config.setEntropyDevices(&NSArray::from_retained_slice(&entropy));
let balloon: Vec<Retained<VZMemoryBalloonDeviceConfiguration>> =
vec![Retained::into_super(
VZVirtioTraditionalMemoryBalloonDeviceConfiguration::new(),
)];
config.setMemoryBalloonDevices(&NSArray::from_retained_slice(&balloon));
let sockets: Vec<Retained<VZSocketDeviceConfiguration>> = vec![Retained::into_super(
VZVirtioSocketDeviceConfiguration::new(),
)];
config.setSocketDevices(&NSArray::from_retained_slice(&sockets));
let directory = VZSharedDirectory::initWithURL_readOnly(
VZSharedDirectory::alloc(),
&url(std::path::Path::new(&home)),
false,
);
let share =
VZSingleDirectoryShare::initWithDirectory(VZSingleDirectoryShare::alloc(), &directory);
let fs = VZVirtioFileSystemDeviceConfiguration::initWithTag(
VZVirtioFileSystemDeviceConfiguration::alloc(),
&NSString::from_str(SHARE_TAG),
);
fs.setShare(Some(share.as_super()));
let shares: Vec<Retained<VZDirectorySharingDeviceConfiguration>> =
vec![Retained::into_super(fs)];
config.setDirectorySharingDevices(&NSArray::from_retained_slice(&shares));
config
.validateWithError()
.map_err(|e| format!("The engine's VM setup is invalid: {}", describe(&e)))?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
#[test]
fn tells_the_kernel_where_everything_is() {
let line = super::command_line("/Users/me");
assert!(line.contains("init=/sbin/ferrodock-agent"));
assert!(line.contains("ferrodock.share=home:/Users/me"));
assert!(line.contains("root=/dev/vda ro"));
}
}