Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
15 changes: 15 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 4 additions & 0 deletions src/cmdline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ pub struct Args {
/// Firmware path.
#[arg(long, short)]
pub firmware_path: Option<PathBuf>,

/// Vsock port for timesync
#[arg(long = "timesync")]
pub timesync: Option<u32>,
}

/// Parse the input string into a hash map of key value pairs, associating the argument with its
Expand Down
15 changes: 15 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -191,6 +193,19 @@ impl TryFrom<Args> 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 })
}
}
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
mod cmdline;
mod context;
mod status;
mod timesync;
mod virtio;

use cmdline::Args;
Expand Down
130 changes: 130 additions & 0 deletions src/timesync.rs
Original file line number Diff line number Diff line change
@@ -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<Activity>) };
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<Activity> {
let (tx, rx) = channel::<Activity>();
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());
}
}
}
}
Loading