diff --git a/Cargo.lock b/Cargo.lock index ddc85f6..16714c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,21 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "cc" version = "1.0.83" @@ -138,6 +153,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -169,6 +194,17 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", +] + [[package]] name = "either" version = "1.13.0" @@ -234,9 +270,11 @@ version = "1.1.1" dependencies = [ "anyhow", "clap", + "core-foundation", "env_logger", "log", "mac_address", + "objc2-io-kit", "regex", "sysinfo", ] @@ -284,7 +322,7 @@ version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if", "libc", @@ -300,6 +338,46 @@ dependencies = [ "winapi", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "bitflags 2.10.0", + "block2", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "portable-atomic" version = "1.11.0" diff --git a/Cargo.toml b/Cargo.toml index d7f480b..3d1419c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,5 @@ sysinfo = "0.31.4" log = "0.4.0" env_logger = "0.11.8" regex = "1.11.1" +core-foundation = "0.10.1" +objc2-io-kit = "0.3.2" \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md index cf8d528..8b2b18a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -25,6 +25,21 @@ The URI (address) of the RESTful service. If not specified, defaults to `none:// Specify a path in which krunkit will write the PID to. The option does not provide any form of locking. +- `--timesync` + +Specify a Vsock port on which the host will send the current time to +the `qemu-guest-agent` process running inside the guest. + +#### Example + +```bash +--timesync 1234 +``` +On guest: +```bash + /usr/bin/qemu-ga --method=vsock-listen --path=3:1234 --logfile=/var/log/qemu-ga.log -v +``` + ### Virtual Machine Resources - `--cpus` diff --git a/src/cmdline.rs b/src/cmdline.rs index bd81e85..44d3ed4 100644 --- a/src/cmdline.rs +++ b/src/cmdline.rs @@ -63,6 +63,10 @@ pub struct Args { /// Firmware path. #[arg(long, short)] pub firmware_path: Option, + + /// Vsock port for timesync + #[arg(long = "timesync")] + pub timesync: Option, } /// Parse the input string into a hash map of key value pairs, associating the argument with its diff --git a/src/context.rs b/src/context.rs index 028617d..65c72b2 100644 --- a/src/context.rs +++ b/src/context.rs @@ -16,6 +16,8 @@ use std::{ io, }; +use crate::timesync::timesync_listener; +use crate::virtio::{VsockAction, VsockConfig}; use anyhow::{anyhow, Context}; use env_logger::{Builder, Env, Target}; @@ -191,6 +193,19 @@ impl TryFrom for KrunContext { } } + if let Some(timesync_port) = args.timesync { + let vsock_config = VsockConfig { + port: timesync_port, + socket_url: PathBuf::from(format!( + "/tmp/krunkit_timesync_{}.sock", + std::process::id() + )), + action: VsockAction::Connect, + }; + unsafe { vsock_config.krun_ctx_set(id)? } + thread::spawn(move || timesync_listener(vsock_config)); + } + Ok(Self { id, args }) } } diff --git a/src/main.rs b/src/main.rs index a367887..96e3170 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod cmdline; mod context; mod status; +mod timesync; mod virtio; use cmdline::Args; diff --git a/src/timesync.rs b/src/timesync.rs new file mode 100644 index 0000000..b2cf6b6 --- /dev/null +++ b/src/timesync.rs @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::virtio::VsockConfig; +use std::io::{BufRead, BufReader, Write}; +use std::os::unix::net::UnixStream; +use std::path::PathBuf; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +use core_foundation::runloop::{ + kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetCurrent, CFRunLoopRun, __CFRunLoopSource, +}; +use objc2_io_kit::{ + io_object_t, io_service_t, kIOMessageSystemHasPoweredOn, kIOMessageSystemWillPowerOn, + kIOMessageSystemWillSleep, IONotificationPort, IORegisterForSystemPower, +}; +use std::{ + ffi::c_void, + sync::mpsc::{channel, Receiver, Sender}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Activity { + Sleep, + Wake, +} + +#[allow(non_upper_case_globals)] +extern "C-unwind" fn power_callback( + refcon: *mut c_void, + _service: io_service_t, + message_type: u32, + _message_argument: *mut c_void, +) { + let tx = unsafe { &*(refcon as *mut Sender) }; + log::debug!("Power callback called: {:X?}", message_type); + let activity = match message_type { + kIOMessageSystemWillSleep => Some(Activity::Sleep), + kIOMessageSystemWillPowerOn | kIOMessageSystemHasPoweredOn => Some(Activity::Wake), + _ => { + log::debug!("Unknown message type: {:X?}", message_type); + None + } + }; + if let Some(activity) = activity { + if let Err(e) = tx.send(activity) { + log::error!("Failed to send activity: {e}"); + } + } +} + +pub fn start_power_monitor() -> Receiver { + let (tx, rx) = channel::(); + std::thread::spawn(move || unsafe { + let tx_ptr = Box::into_raw(Box::new(tx)); + let mut notifier_port: *mut IONotificationPort = std::ptr::null_mut(); + let mut notifier_object: io_object_t = 0; + + let root_port = IORegisterForSystemPower( + tx_ptr as *mut c_void, + &mut notifier_port, + Some(power_callback), + &mut notifier_object, + ); + if root_port == 0 { + log::error!("Failed to register for system power notifications"); + return; + } + let run_loop_source = IONotificationPort::run_loop_source(notifier_port).unwrap(); + CFRunLoopAddSource( + CFRunLoopGetCurrent(), + std::ptr::from_ref(&*run_loop_source) as *mut __CFRunLoopSource, + kCFRunLoopCommonModes, + ); + CFRunLoopRun(); + }); + rx +} + +fn sync_time(socket_url: PathBuf) { + let mut stream = match UnixStream::connect(&socket_url) { + Ok(stream) => stream, + Err(e) => { + log::error!( + "Failed to connect to timesync socket {:?}: {}", + socket_url, + e + ); + return; + } + }; + + let time_ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let cmd = + format!("{{\"execute\": \"guest-set-time\", \"arguments\":{{\"time\": {time_ns}}}}}\n"); + + if let Err(e) = stream.write_all(cmd.as_bytes()) { + log::error!("Failed to write to timesync socket: {}", e); + return; + } + + let mut reader = BufReader::new(&stream); + let mut response = String::new(); + match reader.read_line(&mut response) { + Ok(_) => { + log::info!("Time synced to {time_ns}"); + } + Err(e) => { + log::error!("Failed to read qemu-guest-agent response: {e}"); + } + } +} + +pub fn timesync_listener(vsock_config: VsockConfig) { + let rx = start_power_monitor(); + for activity in rx { + match activity { + Activity::Sleep => { + log::debug!("System is going to sleep"); + } + Activity::Wake => { + log::debug!("System is waking up. Syncing time..."); + sync_time(vsock_config.socket_url.clone()); + } + } + } +}