use std::ffi::c_void;
use std::net::TcpStream;
use std::os::unix::io::FromRawFd;
use block2::RcBlock;
use crossbeam_channel::{bounded, Receiver, Sender};
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::{define_class, msg_send, AnyThread};
use objc2_foundation::{
NSKeyValueObservingOptions, NSObject, NSObjectNSKeyValueObserverRegistration, NSString,
};
use objc2_virtualization::{
VZVirtioSocketConnection, VZVirtioSocketDevice, VZVirtualMachine, VZVirtualMachineState,
};
use crate::configuration::VirtualMachineConfiguration;
use crate::error::{Result, VzError};
use crate::sys::queue::{Queue, QueueAttribute};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VmState {
Stopped = 0,
Running = 1,
Paused = 2,
Error = 3,
Starting = 4,
Pausing = 5,
Resuming = 6,
Stopping = 7,
Unknown = -1,
}
#[derive(Debug)]
struct ThreadSafe<T>(T);
unsafe impl<T> Send for ThreadSafe<T> {}
unsafe impl<T> Sync for ThreadSafe<T> {}
impl<T> std::ops::Deref for ThreadSafe<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
#[derive(Debug)]
struct ObserverContext {
machine: ThreadSafe<Retained<VZVirtualMachine>>,
notifier: Sender<VmState>,
state_notifications: Receiver<VmState>,
}
impl ObserverContext {
fn state(&self) -> VmState {
let vz_state = unsafe { self.machine.state() };
match vz_state {
s if s == VZVirtualMachineState::Stopped => VmState::Stopped,
s if s == VZVirtualMachineState::Running => VmState::Running,
s if s == VZVirtualMachineState::Paused => VmState::Paused,
s if s == VZVirtualMachineState::Error => VmState::Error,
s if s == VZVirtualMachineState::Starting => VmState::Starting,
s if s == VZVirtualMachineState::Pausing => VmState::Pausing,
s if s == VZVirtualMachineState::Resuming => VmState::Resuming,
s if s == VZVirtualMachineState::Stopping => VmState::Stopping,
_ => VmState::Unknown,
}
}
}
define_class!(
#[unsafe(super(NSObject))]
#[name = "VirtualMachineStateObserver"]
#[derive(Debug)]
struct VirtualMachineStateObserver;
impl VirtualMachineStateObserver {
#[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
fn observe_value_for_key_path(
&self,
key_path: Option<&NSString>,
_object: Option<&AnyObject>,
_change: Option<&AnyObject>,
context: *mut c_void,
) {
if let Some(msg) = key_path {
let key = msg.to_string();
if key == "state" {
let ctx: &ObserverContext =
unsafe { &*(context as *const ObserverContext) };
let _ = ctx.state_notifications.try_recv();
let _ = ctx.notifier.send(ctx.state());
}
}
}
}
);
unsafe impl Send for VirtualMachineStateObserver {}
unsafe impl Sync for VirtualMachineStateObserver {}
impl VirtualMachineStateObserver {
fn new() -> Retained<Self> {
let this = Self::alloc().set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
#[derive(Debug)]
pub struct VirtualMachine {
ctx: Box<ObserverContext>,
queue: Queue,
observer: Retained<VirtualMachineStateObserver>,
}
impl VirtualMachine {
pub fn new(config: &VirtualMachineConfiguration) -> Self {
unsafe {
let queue = Queue::create("com.virt.fwk.rs", QueueAttribute::Serial);
let dispatch_queue = queue.as_dispatch2();
let machine = VZVirtualMachine::initWithConfiguration_queue(
VZVirtualMachine::alloc(),
&config.inner,
dispatch_queue,
);
let (sender, receiver) = bounded(1);
let observer = VirtualMachineStateObserver::new();
let ctx = Box::new(ObserverContext {
machine: ThreadSafe(machine),
notifier: sender,
state_notifications: receiver,
});
let ctx_ptr: *const ObserverContext = &*ctx;
let key = NSString::from_str("state");
ctx.machine.addObserver_forKeyPath_options_context(
&observer,
&key,
NSKeyValueObservingOptions::New,
ctx_ptr as *mut c_void,
);
VirtualMachine {
ctx,
queue,
observer,
}
}
}
pub fn state_channel(&self) -> Receiver<VmState> {
self.ctx.state_notifications.clone()
}
pub fn supported() -> bool {
unsafe { VZVirtualMachine::isSupported() }
}
pub fn start(&self) -> Result<()> {
let (tx, rx) = std::sync::mpsc::channel();
let machine = self.ctx.machine.0.clone();
let machine = ThreadSafe(machine);
let dispatch_block = RcBlock::new(move || {
let inner_tx = tx.clone();
let completion_handler = RcBlock::new(move |err: *mut objc2_foundation::NSError| {
if err.is_null() {
inner_tx.send(Ok(())).unwrap();
} else {
inner_tx
.send(Err(unsafe { VzError::from_ns_error(&*err) }))
.unwrap();
}
});
unsafe {
machine.startWithCompletionHandler(&completion_handler);
}
});
self.queue.exec_block_async(&dispatch_block);
rx.recv()
.map_err(|_| VzError::new("VM start channel closed"))?
}
pub fn stop(&self) -> Result<()> {
let (tx, rx) = std::sync::mpsc::channel();
let machine = self.ctx.machine.0.clone();
let machine = ThreadSafe(machine);
let dispatch_block = RcBlock::new(move || {
let inner_tx = tx.clone();
let completion_handler = RcBlock::new(move |err: *mut objc2_foundation::NSError| {
if err.is_null() {
inner_tx.send(Ok(())).unwrap();
} else {
inner_tx
.send(Err(unsafe { VzError::from_ns_error(&*err) }))
.unwrap();
}
});
unsafe {
machine.stopWithCompletionHandler(&completion_handler);
}
});
self.queue.exec_block_async(&dispatch_block);
rx.recv()
.map_err(|_| VzError::new("VM stop channel closed"))?
}
pub fn can_start(&self) -> bool {
self.queue
.exec_sync(move || unsafe { self.ctx.machine.canStart() })
}
pub fn can_stop(&self) -> bool {
self.queue
.exec_sync(move || unsafe { self.ctx.machine.canRequestStop() })
}
pub fn can_pause(&self) -> bool {
self.queue
.exec_sync(move || unsafe { self.ctx.machine.canPause() })
}
pub fn can_resume(&self) -> bool {
self.queue
.exec_sync(move || unsafe { self.ctx.machine.canResume() })
}
pub fn can_request_stop(&self) -> bool {
self.queue
.exec_sync(move || unsafe { self.ctx.machine.canRequestStop() })
}
pub fn connect_to_vsock_port(&self, port: u32) -> Result<TcpStream> {
let (tx, rx) = std::sync::mpsc::channel::<Result<TcpStream>>();
let machine = self.ctx.machine.0.clone();
let machine = ThreadSafe(machine);
let dispatch_block = RcBlock::new(move || {
let devices = unsafe { machine.socketDevices() };
let count = devices.len();
if count == 0 {
tx.send(Err(VzError::new("No socket devices found on the VM")))
.ok();
return;
}
let device_obj = devices.objectAtIndex(0);
let device: &VZVirtioSocketDevice =
unsafe { &*(&*device_obj as *const _ as *const VZVirtioSocketDevice) };
let inner_tx = tx.clone();
let completion_handler = RcBlock::new(
move |conn: *mut VZVirtioSocketConnection, err: *mut objc2_foundation::NSError| {
if !err.is_null() {
let error = unsafe { VzError::from_ns_error(&*err) };
inner_tx.send(Err(error)).ok();
} else if conn.is_null() {
inner_tx
.send(Err(VzError::new("vsock connection returned null")))
.ok();
} else {
let fd = unsafe { (*conn).fileDescriptor() };
let duped = unsafe { libc::dup(fd) };
if duped < 0 {
inner_tx
.send(Err(VzError::new("failed to dup vsock fd")))
.ok();
} else {
let stream = unsafe { TcpStream::from_raw_fd(duped) };
inner_tx.send(Ok(stream)).ok();
}
}
},
);
unsafe {
device.connectToPort_completionHandler(port, &completion_handler);
}
});
self.queue.exec_block_async(&dispatch_block);
rx.recv()
.map_err(|_| VzError::new("vsock connection channel closed"))?
}
pub fn state(&self) -> VmState {
self.ctx.state()
}
}
impl Drop for VirtualMachine {
fn drop(&mut self) {
let key_path = NSString::from_str("state");
let ctx_ptr: *const ObserverContext = &*self.ctx;
unsafe {
self.ctx.machine.removeObserver_forKeyPath_context(
&self.observer,
&key_path,
ctx_ptr as *mut c_void,
);
}
}
}