Skip to content

Commit d760d16

Browse files
vyasguntylerfanelli
andcommitted
feat: Add --timesync flag for guest clock synchronization
Add a --timesync option which uses vsock to send host time to qemu-guest-agent running in the guest on VM suspend/resume. Uses IOKit's power management APIs to detect system sleep/wake events. Since the official iokit-sys crate is incomplete, a custom fork is required. This also maintains feature parity between krunkit and vfkit. Assisted by: Claude (Anthropic AI) Signed-off-by: Gunjan Vyas <vyasgun20@gmail.com> Co-authored-by: Tyler Fanelli <tfanelli@redhat.com>
1 parent f274d75 commit d760d16

6 files changed

Lines changed: 231 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 79 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ sysinfo = "0.31.4"
1616
log = "0.4.0"
1717
env_logger = "0.11.8"
1818
regex = "1.11.1"
19+
core-foundation = "0.10.1"
20+
objc2-io-kit = "0.3.2"

src/cmdline.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ pub struct Args {
6363
/// Firmware path.
6464
#[arg(long, short)]
6565
pub firmware_path: Option<PathBuf>,
66+
67+
/// Vsock port for timesync
68+
#[arg(long = "timesync")]
69+
pub timesync: Option<u32>,
6670
}
6771

6872
/// Parse the input string into a hash map of key value pairs, associating the argument with its

src/context.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use std::{
1616
io,
1717
};
1818

19+
use crate::timesync::timesync_listener;
20+
use crate::virtio::{VsockAction, VsockConfig};
1921
use anyhow::{anyhow, Context};
2022
use env_logger::{Builder, Env, Target};
2123

@@ -191,6 +193,19 @@ impl TryFrom<Args> for KrunContext {
191193
}
192194
}
193195

196+
if let Some(timesync_port) = args.timesync {
197+
let vsock_config = VsockConfig {
198+
port: timesync_port,
199+
socket_url: PathBuf::from(format!(
200+
"/tmp/krunkit_timesync_{}.sock",
201+
std::process::id()
202+
)),
203+
action: VsockAction::Connect,
204+
};
205+
unsafe { vsock_config.krun_ctx_set(id)? }
206+
thread::spawn(move || timesync_listener(vsock_config));
207+
}
208+
194209
Ok(Self { id, args })
195210
}
196211
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
mod cmdline;
66
mod context;
77
mod status;
8+
mod timesync;
89
mod virtio;
910

1011
use cmdline::Args;

src/timesync.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
use crate::virtio::VsockConfig;
4+
use std::io::{BufRead, BufReader, Write};
5+
use std::os::unix::net::UnixStream;
6+
use std::path::PathBuf;
7+
use std::time::SystemTime;
8+
use std::time::UNIX_EPOCH;
9+
10+
use core_foundation::runloop::{
11+
kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetCurrent, CFRunLoopRun, __CFRunLoopSource,
12+
};
13+
use objc2_io_kit::{
14+
io_object_t, io_service_t, kIOMessageSystemHasPoweredOn, kIOMessageSystemWillPowerOn,
15+
kIOMessageSystemWillSleep, IONotificationPort, IORegisterForSystemPower,
16+
};
17+
use std::{
18+
ffi::c_void,
19+
sync::mpsc::{channel, Receiver, Sender},
20+
};
21+
22+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23+
pub enum Activity {
24+
Sleep,
25+
Wake,
26+
}
27+
28+
#[allow(non_upper_case_globals)]
29+
extern "C-unwind" fn power_callback(
30+
refcon: *mut c_void,
31+
_service: io_service_t,
32+
message_type: u32,
33+
_message_argument: *mut c_void,
34+
) {
35+
let tx = unsafe { &*(refcon as *mut Sender<Activity>) };
36+
log::debug!("Power callback called: {:X?}", message_type);
37+
let activity = match message_type {
38+
kIOMessageSystemWillSleep => Some(Activity::Sleep),
39+
kIOMessageSystemWillPowerOn | kIOMessageSystemHasPoweredOn => Some(Activity::Wake),
40+
_ => {
41+
log::debug!("Unknown message type: {:X?}", message_type);
42+
None
43+
}
44+
};
45+
if let Some(activity) = activity {
46+
if let Err(e) = tx.send(activity) {
47+
log::error!("Failed to send activity: {e}");
48+
}
49+
}
50+
}
51+
52+
pub fn start_power_monitor() -> Receiver<Activity> {
53+
let (tx, rx) = channel::<Activity>();
54+
std::thread::spawn(move || unsafe {
55+
let tx_ptr = Box::into_raw(Box::new(tx));
56+
let mut notifier_port: *mut IONotificationPort = std::ptr::null_mut();
57+
let mut notifier_object: io_object_t = 0;
58+
59+
let root_port = IORegisterForSystemPower(
60+
tx_ptr as *mut c_void,
61+
&mut notifier_port,
62+
Some(power_callback),
63+
&mut notifier_object,
64+
);
65+
if root_port == 0 {
66+
log::error!("Failed to register for system power notifications");
67+
return;
68+
}
69+
let run_loop_source = IONotificationPort::run_loop_source(notifier_port).unwrap();
70+
CFRunLoopAddSource(
71+
CFRunLoopGetCurrent(),
72+
std::ptr::from_ref(&*run_loop_source) as *mut __CFRunLoopSource,
73+
kCFRunLoopCommonModes,
74+
);
75+
CFRunLoopRun();
76+
});
77+
rx
78+
}
79+
80+
fn sync_time(socket_url: PathBuf) {
81+
let mut stream = match UnixStream::connect(&socket_url) {
82+
Ok(stream) => stream,
83+
Err(e) => {
84+
log::error!(
85+
"Failed to connect to timesync socket {:?}: {}",
86+
socket_url,
87+
e
88+
);
89+
return;
90+
}
91+
};
92+
93+
let time_ns = SystemTime::now()
94+
.duration_since(UNIX_EPOCH)
95+
.unwrap()
96+
.as_nanos();
97+
let cmd =
98+
format!("{{\"execute\": \"guest-set-time\", \"arguments\":{{\"time\": {time_ns}}}}}\n");
99+
100+
if let Err(e) = stream.write_all(cmd.as_bytes()) {
101+
log::error!("Failed to write to timesync socket: {}", e);
102+
return;
103+
}
104+
105+
let mut reader = BufReader::new(&stream);
106+
let mut response = String::new();
107+
match reader.read_line(&mut response) {
108+
Ok(_) => {
109+
log::info!("Time synced to {time_ns}");
110+
}
111+
Err(e) => {
112+
log::error!("Failed to read qemu-guest-agent response: {e}");
113+
}
114+
}
115+
}
116+
117+
pub fn timesync_listener(vsock_config: VsockConfig) {
118+
let rx = start_power_monitor();
119+
for activity in rx {
120+
match activity {
121+
Activity::Sleep => {
122+
log::debug!("System is going to sleep");
123+
}
124+
Activity::Wake => {
125+
log::debug!("System is waking up. Syncing time...");
126+
sync_time(vsock_config.socket_url.clone());
127+
}
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)