use std::time::Duration;
use arcbox_vm_driver::Vsock;
use tracing::info;
use super::{MSG_CLOCK_SYNC, MSG_EXIT, connect_to_agent, read_frame, write_frame};
use crate::agent::ClockSync;
use crate::error::{ComputerError, Result};
pub async fn sync_clock(vsock: &dyn Vsock) -> Result<ClockSync> {
let started = std::time::Instant::now();
let mut stream = connect_to_agent(vsock).await?;
let connected = std::time::Instant::now();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| ComputerError::Vsock(format!("system time error: {e}")))?;
let secs = i64::try_from(now.as_secs())
.map_err(|e| ComputerError::Vsock(format!("unix timestamp overflow: {e}")))?;
let nanos = now.subsec_nanos();
let result = sync_clock_on_stream(&mut stream, secs, nanos).await;
info!(
connect_ms = connected.duration_since(started).as_millis() as u64,
rpc_ms = connected.elapsed().as_millis() as u64,
"clock sync"
);
result
}
async fn sync_clock_on_stream<S: tokio::io::AsyncReadExt + tokio::io::AsyncWriteExt + Unpin>(
stream: &mut S,
secs: i64,
nanos: u32,
) -> Result<ClockSync> {
let mut payload = [0u8; 12];
payload[..8].copy_from_slice(&secs.to_le_bytes());
payload[8..].copy_from_slice(&nanos.to_le_bytes());
write_frame(stream, MSG_CLOCK_SYNC, &payload)
.await
.map_err(|e| ComputerError::Vsock(format!("write MSG_CLOCK_SYNC: {e}")))?;
let (msg_type, payload) = tokio::time::timeout(Duration::from_secs(5), read_frame(stream))
.await
.map_err(|_| ComputerError::Vsock("clock sync: timed out waiting for response".into()))?
.map_err(|e| ComputerError::Vsock(format!("read clock sync response: {e}")))?;
if msg_type != MSG_EXIT {
return Err(ComputerError::Vsock(format!(
"clock sync: unexpected response type 0x{msg_type:02x}"
)));
}
if payload.len() < 4 {
return Err(ComputerError::Vsock(format!(
"clock sync: payload too short ({} bytes, expected 4)",
payload.len()
)));
}
let code = i32::from_le_bytes(payload[..4].try_into().unwrap());
if code != 0 {
return Ok(ClockSync::AgentError(code));
}
Ok(ClockSync::Synced)
}
#[cfg(test)]
mod tests {
use super::super::MSG_STDOUT;
use super::*;
#[tokio::test]
async fn test_sync_clock_success() {
let (mut agent, mut host) = tokio::io::duplex(256);
let agent_handle = tokio::spawn(async move {
let (ty, payload) = read_frame(&mut agent).await.unwrap();
assert_eq!(ty, MSG_CLOCK_SYNC);
assert_eq!(payload.len(), 12);
let secs = i64::from_le_bytes(payload[..8].try_into().unwrap());
let nanos = u32::from_le_bytes(payload[8..12].try_into().unwrap());
assert_eq!(secs, 1_700_000_000);
assert_eq!(nanos, 123_456_789);
write_frame(&mut agent, MSG_EXIT, &0i32.to_le_bytes())
.await
.unwrap();
});
let result = sync_clock_on_stream(&mut host, 1_700_000_000, 123_456_789).await;
assert_eq!(result.unwrap(), ClockSync::Synced);
agent_handle.await.unwrap();
}
#[tokio::test]
async fn test_sync_clock_agent_error() {
let (mut agent, mut host) = tokio::io::duplex(256);
let agent_handle = tokio::spawn(async move {
let _ = read_frame(&mut agent).await.unwrap();
write_frame(&mut agent, MSG_EXIT, &(-1i32).to_le_bytes())
.await
.unwrap();
});
let result = sync_clock_on_stream(&mut host, 1_700_000_000, 0).await;
assert_eq!(result.unwrap(), ClockSync::AgentError(-1));
agent_handle.await.unwrap();
}
#[tokio::test]
async fn test_sync_clock_short_payload() {
let (mut agent, mut host) = tokio::io::duplex(256);
let agent_handle = tokio::spawn(async move {
let _ = read_frame(&mut agent).await.unwrap();
write_frame(&mut agent, MSG_EXIT, &[0u8; 2]).await.unwrap();
});
let result = sync_clock_on_stream(&mut host, 1_700_000_000, 0).await;
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("too short"), "unexpected error: {msg}");
agent_handle.await.unwrap();
}
#[tokio::test]
async fn test_sync_clock_unexpected_frame() {
let (mut agent, mut host) = tokio::io::duplex(256);
let agent_handle = tokio::spawn(async move {
let _ = read_frame(&mut agent).await.unwrap();
write_frame(&mut agent, MSG_STDOUT, b"oops").await.unwrap();
});
let result = sync_clock_on_stream(&mut host, 1_700_000_000, 0).await;
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("unexpected response type"),
"unexpected error: {msg}"
);
agent_handle.await.unwrap();
}
}