pub use crate::shm::CLOCKBOUND_SHM_CLIENT_DEFAULT_PATH;
pub use crate::shm::ClockStatus;
use crate::shm::ShmReader;
use crate::shm::{ClockBoundNowResult, ClockBoundSnapshot, ClockErrorBound, ShmError};
pub use crate::vmclock::shm::VMCLOCK_SHM_DEFAULT_PATH;
use crate::vmclock::shm_reader::VMClockShmReader;
use errno::Errno;
use std::ffi::CString;
use std::path::Path;
pub struct ClockBoundClient {
clockbound_shm: ClockBoundSHM,
vmclock_shm: VMClockSHM,
}
impl ClockBoundClient {
pub fn new() -> Result<ClockBoundClient, ClockBoundError> {
Self::new_with_path(CLOCKBOUND_SHM_CLIENT_DEFAULT_PATH)
}
pub fn new_with_path(clockbound_shm_path: &str) -> Result<ClockBoundClient, ClockBoundError> {
Self::new_with_paths(clockbound_shm_path, VMCLOCK_SHM_DEFAULT_PATH)
}
pub fn new_with_paths(
clockbound_shm_path: &str,
vmclock_shm_path: &str,
) -> Result<ClockBoundClient, ClockBoundError> {
let mut clockbound_shm = ClockBoundSHM::new(clockbound_shm_path)?;
let cb_snapshot = clockbound_shm.snapshot()?;
let vmclock_shm = VMClockSHM::new(
vmclock_shm_path,
cb_snapshot.clock_disruption_support_enabled(),
)?;
Ok(ClockBoundClient {
clockbound_shm,
vmclock_shm,
})
}
pub fn now(&mut self) -> Result<ClockBoundNowResult, ClockBoundError> {
let cb_snap = self.clockbound_shm.snapshot()?;
let mut clock_bound_now_result = cb_snap.now()?;
if self.vmclock_shm.vmclock_shm_reader.is_none()
&& cb_snap.clock_disruption_support_enabled()
{
self.vmclock_shm.vmclock_shm_reader = Some(VMClockShmReader::new(
self.vmclock_shm.vmclock_shm_path.as_str(),
)?);
}
let is_disrupted = match self.vmclock_shm.disruption_marker()? {
Some(marker) => marker != cb_snap.disruption_marker(),
None => false,
};
if is_disrupted {
clock_bound_now_result.clock_status = ClockStatus::Disrupted;
}
Ok(clock_bound_now_result)
}
}
struct ClockBoundSHM {
#[expect(dead_code)]
clockbound_shm_path: String,
clockbound_shm_reader: ShmReader,
}
impl ClockBoundSHM {
fn new(clockbound_shm_path: &str) -> Result<ClockBoundSHM, ClockBoundError> {
if !Path::new(clockbound_shm_path).exists() {
let detail = format!(
"Path to clockbound daemon shared memory segment does not exist: {clockbound_shm_path}"
);
let error = ClockBoundError {
kind: ClockBoundErrorKind::SegmentNotInitialized,
detail,
errno: Errno(0),
};
return Err(error);
}
let shm_path = CString::new(clockbound_shm_path).expect("CString::new failed");
let shm_reader = ShmReader::new(shm_path.as_c_str())?;
Ok(ClockBoundSHM {
clockbound_shm_path: String::from(clockbound_shm_path),
clockbound_shm_reader: shm_reader,
})
}
fn snapshot(&mut self) -> Result<&ClockErrorBound, ShmError> {
self.clockbound_shm_reader.snapshot()
}
}
struct VMClockSHM {
vmclock_shm_path: String,
vmclock_shm_reader: Option<VMClockShmReader>,
}
impl VMClockSHM {
fn new(
vmclock_shm_path: &str,
clock_disruption_support_enabled: bool,
) -> Result<VMClockSHM, ClockBoundError> {
let mut vmclock_shm_reader: Option<VMClockShmReader> = None;
if clock_disruption_support_enabled {
if !Path::new(vmclock_shm_path).exists() {
let detail = format!(
"Path to VMClock device shared memory segment does not exist: {vmclock_shm_path}"
);
let error = ClockBoundError {
kind: ClockBoundErrorKind::SegmentNotInitialized,
detail,
errno: Errno(0),
};
return Err(error);
}
vmclock_shm_reader = Some(VMClockShmReader::new(vmclock_shm_path)?);
}
Ok(VMClockSHM {
vmclock_shm_path: String::from(vmclock_shm_path),
vmclock_shm_reader,
})
}
fn disruption_marker(&mut self) -> Result<Option<u64>, ShmError> {
if let Some(ref mut vmclock_shm_reader) = self.vmclock_shm_reader {
let snap = vmclock_shm_reader.snapshot()?;
return Ok(Some(snap.disruption_marker));
}
Ok(None)
}
}
#[derive(Debug)]
pub struct ClockBoundError {
pub kind: ClockBoundErrorKind,
pub errno: Errno,
pub detail: String,
}
impl From<ShmError> for ClockBoundError {
fn from(value: ShmError) -> Self {
let (kind, detail, errno) = match value {
ShmError::SyscallError(detail, errno) => (ClockBoundErrorKind::Syscall, detail, errno),
ShmError::SegmentNotInitialized(detail) => {
(ClockBoundErrorKind::SegmentNotInitialized, detail, Errno(0))
}
ShmError::SegmentMalformed(detail) => {
(ClockBoundErrorKind::SegmentMalformed, detail, Errno(0))
}
ShmError::CausalityBreach(detail) => {
(ClockBoundErrorKind::CausalityBreach, detail, Errno(0))
}
ShmError::SegmentVersionNotSupported(detail) => (
ClockBoundErrorKind::SegmentVersionNotSupported,
detail,
Errno(0),
),
};
ClockBoundError {
kind,
errno,
detail,
}
}
}
#[derive(Hash, PartialEq, Eq, Clone, Debug)]
pub enum ClockBoundErrorKind {
Syscall,
SegmentNotInitialized,
SegmentMalformed,
CausalityBreach,
SegmentVersionNotSupported,
}
#[cfg(test)]
mod lib_tests {
use super::*;
use crate::shm::{ClockErrorBound, ShmWrite, ShmWriter};
use crate::shm::{ClockErrorBoundGeneric, ClockErrorBoundLayoutVersion};
use crate::vmclock::shm::{VMClockClockStatus, VMClockShmBody};
use crate::vmclock::shm_writer::{VMClockShmWrite, VMClockShmWriter};
use byteorder::{NativeEndian, WriteBytesExt};
use nix::sys::time::TimeSpec;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use tempfile::NamedTempFile;
macro_rules! write_clockbound_memory_segment {
($file:ident,
$magic_0:literal,
$magic_1:literal,
$segsize:literal,
$version:literal,
$generation:literal) => {
let ceb = ClockErrorBoundGeneric::builder()
.clock_disruption_support_enabled(true)
.build(ClockErrorBoundLayoutVersion::V2);
let slice = unsafe {
::core::slice::from_raw_parts(
(&ceb as *const ClockErrorBound) as *const u8,
::core::mem::size_of::<ClockErrorBound>(),
)
};
$file
.write_u32::<NativeEndian>($magic_0)
.expect("Write failed magic_0");
$file
.write_u32::<NativeEndian>($magic_1)
.expect("Write failed magic_1");
$file
.write_u32::<NativeEndian>($segsize)
.expect("Write failed segsize");
$file
.write_u16::<NativeEndian>($version)
.expect("Write failed version");
$file
.write_u16::<NativeEndian>($generation)
.expect("Write failed generation");
$file
.write_all(slice)
.expect("Write failed ClockErrorBound");
$file.sync_all().expect("Sync to disk failed");
};
}
macro_rules! vmclockshmbody {
() => {
VMClockShmBody {
disruption_marker: 10,
flags: 0_u64,
_padding: [0x00, 0x00],
clock_status: VMClockClockStatus::Unknown,
leap_second_smearing_hint: 0,
tai_offset_sec: 37_i16,
leap_indicator: 0,
counter_period_shift: 0,
counter_value: 0,
counter_period_frac_sec: 0,
counter_period_esterror_rate_frac_sec: 0,
counter_period_maxerror_rate_frac_sec: 0,
time_sec: 0,
time_frac_sec: 0,
time_esterror_nanosec: 0,
time_maxerror_nanosec: 0,
}
};
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq)]
struct VMClockContent {
magic: u32,
size: u32,
version: u16,
counter_id: u8,
time_type: u8,
seq_count: u32,
disruption_marker: u64,
flags: u64,
_padding: [u8; 2],
clock_status: VMClockClockStatus,
leap_second_smearing_hint: u8,
tai_offset_sec: i16,
leap_indicator: u8,
counter_period_shift: u8,
counter_value: u64,
counter_period_frac_sec: u64,
counter_period_esterror_rate_frac_sec: u64,
counter_period_maxerror_rate_frac_sec: u64,
time_sec: u64,
time_frac_sec: u64,
time_esterror_nanosec: u64,
time_maxerror_nanosec: u64,
}
fn write_vmclock_content(file: &mut File, vmclock_content: &VMClockContent) {
let slice = unsafe {
::core::slice::from_raw_parts(
(vmclock_content as *const VMClockContent) as *const u8,
::core::mem::size_of::<VMClockContent>(),
)
};
file.write_all(slice).expect("Write failed VMClockContent");
file.sync_all().expect("Sync to disk failed");
}
fn remove_file_or_directory(path: &str) {
let p = Path::new(&path);
while p.exists() {
if p.is_dir() {
std::fs::remove_dir_all(&path).expect("failed to remove file");
} else {
std::fs::remove_file(&path).expect("failed to remove file");
}
}
}
#[test]
fn test_vmclock_now_with_clock_disruption_support_enabled_success() {
let vmclock_shm_tempfile = NamedTempFile::new().expect("create vmclock file failed");
let vmclock_shm_temppath = vmclock_shm_tempfile.into_temp_path();
let vmclock_shm_path = vmclock_shm_temppath.to_str().unwrap();
remove_file_or_directory(&vmclock_shm_path);
let vmclock_shm_body = vmclockshmbody!();
let mut vmclock_shm_writer = VMClockShmWriter::new(Path::new(&vmclock_shm_path))
.expect("Failed to create a VMClockShmWriter");
vmclock_shm_writer.write(&vmclock_shm_body);
let vmclock_new_result = VMClockSHM::new(&vmclock_shm_path, true);
match vmclock_new_result {
Ok(mut vmclock) => {
let marker_result = vmclock.disruption_marker();
assert!(marker_result.is_ok());
assert!(marker_result.unwrap() == Some(10_u64));
}
Err(_) => {
assert!(false);
}
}
}
#[test]
fn test_vmclock_now_with_clock_disruption_support_enabled_failure() {
let vmclock_shm_tempfile = NamedTempFile::new().expect("create vmclock file failed");
let vmclock_shm_temppath = vmclock_shm_tempfile.into_temp_path();
let vmclock_shm_path = vmclock_shm_temppath.to_str().unwrap();
remove_file_or_directory(&vmclock_shm_path);
let vmclock_new_result = VMClockSHM::new(&vmclock_shm_path, true);
assert!(vmclock_new_result.is_err());
}
#[test]
fn test_vmclock_now_with_clock_disruption_support_not_enabled() {
let vmclock_shm_tempfile = NamedTempFile::new().expect("create vmclock file failed");
let vmclock_shm_temppath = vmclock_shm_tempfile.into_temp_path();
let vmclock_shm_path = vmclock_shm_temppath.to_str().unwrap();
remove_file_or_directory(&vmclock_shm_path);
let vmclock_new_result = VMClockSHM::new(&vmclock_shm_path, false);
match vmclock_new_result {
Ok(mut vmclock) => {
let marker_result = vmclock.disruption_marker();
assert!(marker_result.is_ok());
assert!(marker_result.unwrap() == None)
}
Err(_) => {
assert!(false);
}
}
}
#[test]
fn test_new_with_path_does_not_exist() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
remove_file_or_directory(clockbound_shm_path);
let result = ClockBoundClient::new_with_path(clockbound_shm_path);
assert!(result.is_err());
}
#[test]
fn test_new_with_paths_sanity_check() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
let mut clockbound_shm_file = OpenOptions::new()
.write(true)
.open(clockbound_shm_path)
.expect("open clockbound file failed");
write_clockbound_memory_segment!(
clockbound_shm_file,
0x414D5A4E,
0x43420200,
800,
0x0303,
10
);
let vmclock_shm_tempfile = NamedTempFile::new().expect("create vmclock file failed");
let vmclock_shm_temppath = vmclock_shm_tempfile.into_temp_path();
let vmclock_shm_path = vmclock_shm_temppath.to_str().unwrap();
let mut vmclock_shm_file = OpenOptions::new()
.write(true)
.open(vmclock_shm_path)
.expect("open vmclock file failed");
let vmclock_content = VMClockContent {
magic: 0x4B4C4356,
size: 104_u32,
version: 1_u16,
counter_id: 1_u8,
time_type: 0_u8,
seq_count: 10_u32,
disruption_marker: 888888_u64,
flags: 0_u64,
_padding: [0x00, 0x00],
clock_status: VMClockClockStatus::Synchronized,
leap_second_smearing_hint: 0_u8,
tai_offset_sec: 0_i16,
leap_indicator: 0_u8,
counter_period_shift: 0_u8,
counter_value: 123456_u64,
counter_period_frac_sec: 0_u64,
counter_period_esterror_rate_frac_sec: 0_u64,
counter_period_maxerror_rate_frac_sec: 0_u64,
time_sec: 0_u64,
time_frac_sec: 0_u64,
time_esterror_nanosec: 0_u64,
time_maxerror_nanosec: 0_u64,
};
write_vmclock_content(&mut vmclock_shm_file, &vmclock_content);
let mut clockbound =
match ClockBoundClient::new_with_paths(clockbound_shm_path, vmclock_shm_path) {
Ok(c) => c,
Err(e) => {
eprintln!("{:?}", e);
panic!("ClockBoundClient::new_with_paths() failed");
}
};
let now_result = match clockbound.now() {
Ok(result) => result,
Err(e) => {
eprintln!("{:?}", e);
panic!("ClockBoundClient::now() failed");
}
};
assert_eq!(now_result.clock_status, ClockStatus::Disrupted);
}
#[test]
fn test_new_with_paths_does_not_exist() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
remove_file_or_directory(clockbound_shm_path);
let vmclock_shm_tempfile = NamedTempFile::new().expect("create vmclock file failed");
let vmclock_shm_temppath = vmclock_shm_tempfile.into_temp_path();
let vmclock_shm_path = vmclock_shm_temppath.to_str().unwrap();
remove_file_or_directory(vmclock_shm_path);
let result = ClockBoundClient::new_with_paths(clockbound_shm_path, vmclock_shm_path);
assert!(result.is_err());
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
let mut clockbound_shm_file = OpenOptions::new()
.write(true)
.open(clockbound_shm_path)
.expect("open clockbound file failed");
write_clockbound_memory_segment!(clockbound_shm_file, 0x414D5A4E, 0x43420200, 800, 2, 10);
let vmclock_shm_tempfile = NamedTempFile::new().expect("create vmclock file failed");
let vmclock_shm_temppath = vmclock_shm_tempfile.into_temp_path();
let vmclock_shm_path = vmclock_shm_temppath.to_str().unwrap();
remove_file_or_directory(vmclock_shm_path);
let result = ClockBoundClient::new_with_paths(clockbound_shm_path, vmclock_shm_path);
assert!(result.is_err());
remove_file_or_directory(clockbound_shm_path);
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
remove_file_or_directory(clockbound_shm_path);
let vmclock_shm_tempfile = NamedTempFile::new().expect("create vmclock file failed");
let vmclock_shm_temppath = vmclock_shm_tempfile.into_temp_path();
let vmclock_shm_path = vmclock_shm_temppath.to_str().unwrap();
let mut vmclock_shm_file = OpenOptions::new()
.write(true)
.open(vmclock_shm_path)
.expect("open vmclock file failed");
let vmclock_content = VMClockContent {
magic: 0x4B4C4356,
size: 104_u32,
version: 1_u16,
counter_id: 1_u8,
time_type: 0_u8,
seq_count: 10_u32,
disruption_marker: 888888_u64,
flags: 0_u64,
_padding: [0x00, 0x00],
clock_status: VMClockClockStatus::Synchronized,
leap_second_smearing_hint: 0_u8,
tai_offset_sec: 0_i16,
leap_indicator: 0_u8,
counter_period_shift: 0_u8,
counter_value: 123456_u64,
counter_period_frac_sec: 0_u64,
counter_period_esterror_rate_frac_sec: 0_u64,
counter_period_maxerror_rate_frac_sec: 0_u64,
time_sec: 0_u64,
time_frac_sec: 0_u64,
time_esterror_nanosec: 0_u64,
time_maxerror_nanosec: 0_u64,
};
write_vmclock_content(&mut vmclock_shm_file, &vmclock_content);
let result = ClockBoundClient::new_with_paths(clockbound_shm_path, vmclock_shm_path);
assert!(result.is_err());
}
#[test]
#[ignore = "can fail if daemon has run previously with root privs"]
fn test_new_sanity_check() {
let result = ClockBoundClient::new();
if Path::new(CLOCKBOUND_SHM_CLIENT_DEFAULT_PATH).exists() {
assert!(result.is_ok());
} else {
assert!(result.is_err());
}
}
#[test]
#[ignore = "daemon version mismatch"]
fn test_now_clock_error_bound_now_error() {
let clockbound_shm_tempfile = NamedTempFile::new().expect("create clockbound file failed");
let clockbound_shm_temppath = clockbound_shm_tempfile.into_temp_path();
let clockbound_shm_path = clockbound_shm_temppath.to_str().unwrap();
let mut clockbound_shm_file = OpenOptions::new()
.write(true)
.open(clockbound_shm_path)
.expect("open clockbound file failed");
write_clockbound_memory_segment!(
clockbound_shm_file,
0x414D5A4E,
0x43420200,
800,
0x0002,
10
);
let vmclock_shm_tempfile = NamedTempFile::new().expect("create vmclock file failed");
let vmclock_shm_temppath = vmclock_shm_tempfile.into_temp_path();
let vmclock_shm_path = vmclock_shm_temppath.to_str().unwrap();
let mut vmclock_shm_file = OpenOptions::new()
.write(true)
.open(vmclock_shm_path)
.expect("open vmclock file failed");
let vmclock_content = VMClockContent {
magic: 0x4B4C4356,
size: 104_u32,
version: 1_u16,
counter_id: 1_u8,
time_type: 0_u8,
seq_count: 10_u32,
disruption_marker: 888888_u64,
flags: 0_u64,
_padding: [0x00, 0x00],
clock_status: VMClockClockStatus::Synchronized,
leap_second_smearing_hint: 0_u8,
tai_offset_sec: 0_i16,
leap_indicator: 0_u8,
counter_period_shift: 0_u8,
counter_value: 123456_u64,
counter_period_frac_sec: 0_u64,
counter_period_esterror_rate_frac_sec: 0_u64,
counter_period_maxerror_rate_frac_sec: 0_u64,
time_sec: 0_u64,
time_frac_sec: 0_u64,
time_esterror_nanosec: 0_u64,
time_maxerror_nanosec: 0_u64,
};
write_vmclock_content(&mut vmclock_shm_file, &vmclock_content);
let mut writer = ShmWriter::new(
Path::new(clockbound_shm_path),
ClockErrorBoundLayoutVersion::V2,
ClockErrorBoundLayoutVersion::V2,
)
.expect("Failed to create a writer");
let ceb = ClockErrorBoundGeneric::builder().build(ClockErrorBoundLayoutVersion::V3);
writer.write(&ceb);
let mut clockbound =
match ClockBoundClient::new_with_paths(clockbound_shm_path, vmclock_shm_path) {
Ok(c) => c,
Err(e) => {
eprintln!("{:?}", e);
panic!("ClockBoundClient::new_with_paths() failed");
}
};
let now_result = clockbound.now();
assert!(now_result.is_ok());
let ceb = ClockErrorBoundGeneric::builder()
.as_of(TimeSpec::new(100, 0))
.void_after(TimeSpec::new(10, 0))
.max_drift_ppb(1_000_000_000)
.clock_status(ClockStatus::Synchronized)
.clock_disruption_support_enabled(true)
.build(ClockErrorBoundLayoutVersion::V3);
writer.write(&ceb);
let now_result = clockbound.now();
assert!(now_result.is_err());
}
#[test]
fn test_shmerror_clockbounderror_conversion_syscallerror() {
let errno = Errno(1);
let detail = String::from("test detail");
let shm_error = ShmError::SyscallError(detail.clone(), errno);
let clockbounderror = ClockBoundError::from(shm_error);
assert_eq!(ClockBoundErrorKind::Syscall, clockbounderror.kind);
assert_eq!(errno, clockbounderror.errno);
assert_eq!(detail, clockbounderror.detail);
}
#[test]
fn test_shmerror_clockbounderror_conversion_segmentnotinitialized() {
let detail = String::from("test detail");
let shm_error = ShmError::SegmentNotInitialized(detail.clone());
let clockbounderror = ClockBoundError::from(shm_error);
assert_eq!(
ClockBoundErrorKind::SegmentNotInitialized,
clockbounderror.kind
);
assert_eq!(Errno(0), clockbounderror.errno);
assert_eq!(detail, clockbounderror.detail);
}
#[test]
fn test_shmerror_clockbounderror_conversion_segmentmalformed() {
let detail = String::from("test detail");
let shm_error = ShmError::SegmentMalformed(detail.clone());
let clockbounderror = ClockBoundError::from(shm_error);
assert_eq!(ClockBoundErrorKind::SegmentMalformed, clockbounderror.kind);
assert_eq!(Errno(0), clockbounderror.errno);
assert_eq!(detail, clockbounderror.detail);
}
#[test]
fn test_shmerror_clockbounderror_conversion_causalitybreach() {
let detail = String::from("test detail");
let shm_error = ShmError::CausalityBreach(detail.clone());
let clockbounderror = ClockBoundError::from(shm_error);
assert_eq!(ClockBoundErrorKind::CausalityBreach, clockbounderror.kind);
assert_eq!(Errno(0), clockbounderror.errno);
assert_eq!(detail, clockbounderror.detail);
}
}