use std::ffi::c_void;
use std::future::Future;
use std::sync::Arc;
use arcbox_core::Runtime;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
const WAKE_SYNC_ATTEMPTS: usize = 3;
const WAKE_SYNC_RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(2);
pub fn spawn_wake_clock_sync(runtime: &Arc<Runtime>, shutdown: &CancellationToken) {
#[cfg(target_os = "macos")]
{
let (tx, rx) = mpsc::channel(1);
if let Err(error) = imp::spawn_observer(tx, shutdown.clone()) {
warn!(%error, "host wake observer unavailable; guest clock will not re-sync after sleep");
return;
}
info!("host wake observer registered; guest clock re-syncs after sleep");
let runtime = Arc::clone(runtime);
let shutdown = shutdown.clone();
drop(tokio::spawn(async move {
wake_sync_loop(rx, runtime, shutdown).await;
}));
}
#[cfg(not(target_os = "macos"))]
{
let _ = (runtime, shutdown);
}
}
async fn wake_sync_loop(
mut wakes: mpsc::Receiver<()>,
runtime: Arc<Runtime>,
shutdown: CancellationToken,
) {
loop {
tokio::select! {
() = shutdown.cancelled() => break,
wake = wakes.recv() => {
if wake.is_none() {
break;
}
let runtime = Arc::clone(&runtime);
sync_after_wake(
|| {
let runtime = Arc::clone(&runtime);
async move { runtime.sync_system_vm_clock().await }
},
&shutdown,
)
.await;
}
}
}
}
async fn sync_after_wake<F, Fut>(sync: F, shutdown: &CancellationToken)
where
F: Fn() -> Fut,
Fut: Future<Output = arcbox_core::Result<bool>>,
{
for attempt in 1..=WAKE_SYNC_ATTEMPTS {
match sync().await {
Ok(true) => {
info!("guest wall clock re-synced after host wake");
return;
}
Ok(false) => {
debug!("host wake: System VM not ready, nothing to re-sync");
return;
}
Err(error) if attempt == WAKE_SYNC_ATTEMPTS => {
warn!(%error, "guest clock re-sync after host wake failed");
return;
}
Err(error) => {
debug!(%error, attempt, "guest clock re-sync failed; retrying");
}
}
tokio::select! {
() = shutdown.cancelled() => return,
() = tokio::time::sleep(WAKE_SYNC_RETRY_DELAY) => {}
}
}
}
#[cfg(test)]
mod tests {
use super::{WAKE_SYNC_ATTEMPTS, sync_after_wake};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio_util::sync::CancellationToken;
fn transient_error() -> arcbox_core::CoreError {
arcbox_core::CoreError::Vm("agent not reachable yet".to_owned())
}
#[tokio::test(start_paused = true)]
async fn a_transient_failure_is_retried_until_the_clock_lands() {
let calls = Arc::new(AtomicUsize::new(0));
let seen = Arc::clone(&calls);
sync_after_wake(
|| {
let seen = Arc::clone(&seen);
async move {
if seen.fetch_add(1, Ordering::Relaxed) == 0 {
Err(transient_error())
} else {
Ok(true)
}
}
},
&CancellationToken::new(),
)
.await;
assert_eq!(calls.load(Ordering::Relaxed), 2, "the retry must have run");
}
#[tokio::test(start_paused = true)]
async fn a_vm_that_is_not_ready_ends_the_attempt_immediately() {
let calls = Arc::new(AtomicUsize::new(0));
let seen = Arc::clone(&calls);
sync_after_wake(
|| {
let seen = Arc::clone(&seen);
async move {
seen.fetch_add(1, Ordering::Relaxed);
Ok(false)
}
},
&CancellationToken::new(),
)
.await;
assert_eq!(calls.load(Ordering::Relaxed), 1);
}
#[tokio::test(start_paused = true)]
async fn a_persistent_failure_gives_up_after_the_budget() {
let calls = Arc::new(AtomicUsize::new(0));
let seen = Arc::clone(&calls);
sync_after_wake(
|| {
let seen = Arc::clone(&seen);
async move {
seen.fetch_add(1, Ordering::Relaxed);
Err(transient_error())
}
},
&CancellationToken::new(),
)
.await;
assert_eq!(calls.load(Ordering::Relaxed), WAKE_SYNC_ATTEMPTS);
}
#[tokio::test(start_paused = true)]
async fn shutdown_preempts_the_retry_backoff() {
let calls = Arc::new(AtomicUsize::new(0));
let seen = Arc::clone(&calls);
let shutdown = CancellationToken::new();
shutdown.cancel();
let elapsed = tokio::time::Instant::now();
sync_after_wake(
|| {
let seen = Arc::clone(&seen);
async move {
seen.fetch_add(1, Ordering::Relaxed);
Err(transient_error())
}
},
&shutdown,
)
.await;
assert_eq!(calls.load(Ordering::Relaxed), 1);
assert!(elapsed.elapsed() < Duration::from_secs(1));
}
}
#[cfg(target_os = "macos")]
mod imp {
use super::{CancellationToken, c_void, debug, mpsc};
const K_IO_MESSAGE_CAN_SYSTEM_SLEEP: u32 = 0xE000_0270;
const K_IO_MESSAGE_SYSTEM_WILL_SLEEP: u32 = 0xE000_0280;
const K_IO_MESSAGE_SYSTEM_HAS_POWERED_ON: u32 = 0xE000_0300;
type IoConnectT = u32;
type IoObjectT = u32;
type IoServiceT = u32;
type IoReturnT = i32;
type IoNotificationPortRef = *mut c_void;
type CfRunLoopSourceRef = *mut c_void;
type CfRunLoopRef = *mut c_void;
type CfStringRef = *const c_void;
type IoServiceInterestCallback =
unsafe extern "C" fn(refcon: *mut c_void, service: IoServiceT, msg: u32, arg: *mut c_void);
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn IORegisterForSystemPower(
refcon: *mut c_void,
port: *mut IoNotificationPortRef,
callback: IoServiceInterestCallback,
notifier: *mut IoObjectT,
) -> IoConnectT;
fn IODeregisterForSystemPower(notifier: *mut IoObjectT) -> IoReturnT;
fn IONotificationPortGetRunLoopSource(port: IoNotificationPortRef) -> CfRunLoopSourceRef;
fn IONotificationPortDestroy(port: IoNotificationPortRef);
fn IOAllowPowerChange(connect: IoConnectT, arg: isize) -> IoReturnT;
fn IOServiceClose(connect: IoConnectT) -> IoReturnT;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
static kCFRunLoopDefaultMode: CfStringRef;
fn CFRunLoopGetCurrent() -> CfRunLoopRef;
fn CFRunLoopAddSource(rl: CfRunLoopRef, source: CfRunLoopSourceRef, mode: CfStringRef);
fn CFRunLoopRemoveSource(rl: CfRunLoopRef, source: CfRunLoopSourceRef, mode: CfStringRef);
fn CFRunLoopRun();
fn CFRunLoopStop(rl: CfRunLoopRef);
}
#[derive(Clone, Copy)]
struct RunLoopHandle(CfRunLoopRef);
unsafe impl Send for RunLoopHandle {}
impl RunLoopHandle {
fn stop(self) {
unsafe { CFRunLoopStop(self.0) };
}
}
struct Observer {
root_port: IoConnectT,
wakes: mpsc::Sender<()>,
}
unsafe extern "C" fn on_power_message(
refcon: *mut c_void,
_service: IoServiceT,
msg: u32,
arg: *mut c_void,
) {
let observer = unsafe { &*refcon.cast::<Observer>() };
match msg {
K_IO_MESSAGE_CAN_SYSTEM_SLEEP | K_IO_MESSAGE_SYSTEM_WILL_SLEEP => {
unsafe { IOAllowPowerChange(observer.root_port, arg as isize) };
}
K_IO_MESSAGE_SYSTEM_HAS_POWERED_ON if observer.wakes.try_send(()).is_err() => {
debug!("host wake: sync already pending");
}
_ => {}
}
}
pub(super) fn spawn_observer(
wakes: mpsc::Sender<()>,
shutdown: CancellationToken,
) -> std::io::Result<()> {
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
std::thread::Builder::new()
.name("arcbox-power".to_owned())
.spawn(move || run_observer(&wakes, &ready_tx))?;
let run_loop = match ready_rx.recv() {
Ok(Ok(handle)) => handle,
Ok(Err(e)) => return Err(e),
Err(_) => {
return Err(std::io::Error::other(
"power observer thread exited before registering",
));
}
};
drop(tokio::spawn(async move {
shutdown.cancelled().await;
run_loop.stop();
}));
Ok(())
}
fn run_observer(
wakes: &mpsc::Sender<()>,
ready: &std::sync::mpsc::Sender<std::io::Result<RunLoopHandle>>,
) {
let mut port: IoNotificationPortRef = std::ptr::null_mut();
let mut notifier: IoObjectT = 0;
let mut observer = Box::new(Observer {
root_port: 0,
wakes: wakes.clone(),
});
let refcon = (&raw mut *observer).cast::<c_void>();
let root_port = unsafe {
IORegisterForSystemPower(refcon, &raw mut port, on_power_message, &raw mut notifier)
};
if root_port == 0 || port.is_null() {
let _ = ready.send(Err(std::io::Error::other(
"IORegisterForSystemPower failed",
)));
return;
}
observer.root_port = root_port;
let source = unsafe { IONotificationPortGetRunLoopSource(port) };
let run_loop = unsafe { CFRunLoopGetCurrent() };
unsafe { CFRunLoopAddSource(run_loop, source, kCFRunLoopDefaultMode) };
let _ = ready.send(Ok(RunLoopHandle(run_loop)));
unsafe { CFRunLoopRun() };
unsafe {
IODeregisterForSystemPower(&raw mut notifier);
CFRunLoopRemoveSource(run_loop, source, kCFRunLoopDefaultMode);
IONotificationPortDestroy(port);
IOServiceClose(root_port);
}
drop(observer);
}
}