use crate::monitor::PowerRecord;
use crate::{Error, Result};
#[cfg(unix)]
use {
memmap2::MmapMut,
std::fs::{File, OpenOptions},
std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt},
std::path::Path,
std::sync::Mutex,
};
#[cfg(windows)]
use {
std::os::windows::ffi::OsStrExt,
std::path::Path,
std::sync::Mutex,
windows::Win32::Foundation::{ERROR_ALREADY_EXISTS, GetLastError, INVALID_HANDLE_VALUE},
windows::Win32::System::Memory::{
CreateFileMappingW, FILE_MAP_ALL_ACCESS, MapViewOfFile, PAGE_READWRITE,
},
windows::core::PCWSTR,
};
use std::mem::size_of;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
pub const BUFFER_SIZE: usize = 5;
const HEADER_SIZE: usize = size_of::<u64>();
const ENTRY_SIZE: usize = size_of::<PowerRecord>();
const _: () = assert!(ENTRY_SIZE == 48, "PowerRecord layout changed");
const REGION_SIZE: usize = HEADER_SIZE + BUFFER_SIZE * ENTRY_SIZE;
pub struct RingBufferWriter {
#[cfg(unix)]
mmap: Mutex<MmapMut>,
#[cfg(windows)]
mapping: Mutex<WindowsMapping>,
}
impl RingBufferWriter {
#[must_use]
pub fn default_path() -> PathBuf {
PathBuf::from(if cfg!(target_os = "macos") {
"/tmp/joularcorering"
} else if cfg!(windows) {
r"Local\JoularCoreRing"
} else {
"/dev/shm/joularcorering"
})
}
pub fn new() -> Result<Self> {
Self::with_path(Self::default_path())
}
pub fn with_path(path: impl Into<PathBuf>) -> Result<Self> {
let path = path.into();
#[cfg(unix)]
{
let file = open_exclusive(&path)?;
file.set_len(REGION_SIZE as u64)?;
let mmap = unsafe { MmapMut::map_mut(&file) }
.map_err(|e| Error::config(format!("failed to map {}: {e}", path.display())))?;
if mmap.len() < REGION_SIZE {
return Err(Error::config(format!(
"mapped {} bytes but {REGION_SIZE} are required",
mmap.len()
)));
}
Ok(Self {
mmap: Mutex::new(mmap),
})
}
#[cfg(windows)]
{
Ok(Self {
mapping: Mutex::new(WindowsMapping::create(&path)?),
})
}
}
pub fn write(&self, entry: PowerRecord) {
#[cfg(unix)]
{
let mut mmap = self.mmap.lock().unwrap_or_else(|e| e.into_inner());
unsafe { write_entry(mmap.as_mut_ptr(), entry) };
}
#[cfg(windows)]
{
let mapping = self.mapping.lock().unwrap_or_else(|e| e.into_inner());
unsafe { write_entry(mapping.ptr, entry) };
}
}
}
impl crate::output::OutputSink for RingBufferWriter {
fn send(&mut self, sample: &crate::monitor::MonitorSample) -> crate::Result<()> {
self.write(sample.into());
Ok(())
}
}
unsafe fn write_entry(base: *mut u8, entry: PowerRecord) {
let head = unsafe { AtomicU64::from_ptr(base.cast::<u64>()) };
let index = (head.load(Ordering::Relaxed) as usize) % BUFFER_SIZE;
unsafe {
base.add(HEADER_SIZE + index * ENTRY_SIZE)
.cast::<PowerRecord>()
.write_unaligned(entry);
}
head.fetch_add(1, Ordering::Release);
}
#[cfg(unix)]
fn open_exclusive(path: &Path) -> Result<File> {
let mut options = OpenOptions::new();
options
.read(true)
.write(true)
.mode(0o600)
.custom_flags(libc::O_NOFOLLOW);
let file = match options.clone().create_new(true).open(path) {
Ok(file) => return Ok(file),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => options
.open(path)
.map_err(|e| Error::config(format!("failed to open {}: {e}", path.display())))?,
Err(e) => {
return Err(Error::config(format!(
"failed to create {}: {e}",
path.display()
)));
}
};
let meta = file.metadata()?;
let euid = unsafe { libc::geteuid() };
if !meta.is_file() {
return Err(Error::config(format!(
"{} is not a regular file; refusing to use it",
path.display()
)));
}
if meta.uid() != euid {
return Err(Error::config(format!(
"{} is owned by uid {} but this process runs as uid {euid}; \
remove it or run as the owner",
path.display(),
meta.uid()
)));
}
if meta.nlink() != 1 {
return Err(Error::config(format!(
"{} has {} hard links; refusing to use it",
path.display(),
meta.nlink()
)));
}
file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
Ok(file)
}
#[cfg(windows)]
struct WindowsMapping {
handle: windows::Win32::Foundation::HANDLE,
ptr: *mut u8,
}
#[cfg(windows)]
unsafe impl Send for WindowsMapping {}
#[cfg(windows)]
impl WindowsMapping {
fn create(name: &Path) -> Result<Self> {
let wide: Vec<u16> = name
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let handle = unsafe {
CreateFileMappingW(
INVALID_HANDLE_VALUE,
None,
PAGE_READWRITE,
0,
REGION_SIZE as u32,
PCWSTR::from_raw(wide.as_ptr()),
)
}
.map_err(|e| Error::config(format!("CreateFileMappingW failed: {e}")))?;
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
let _ = unsafe { windows::Win32::Foundation::CloseHandle(handle) };
return Err(Error::config(format!(
"a shared memory section named {} already exists; \
another Joular Core instance may be running",
name.display()
)));
}
let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, REGION_SIZE) };
if view.Value.is_null() {
let _ = unsafe { windows::Win32::Foundation::CloseHandle(handle) };
return Err(Error::config("MapViewOfFile failed"));
}
Ok(Self {
handle,
ptr: view.Value.cast::<u8>(),
})
}
}
#[cfg(windows)]
impl Drop for WindowsMapping {
fn drop(&mut self) {
unsafe {
let _ = windows::Win32::System::Memory::UnmapViewOfFile(
windows::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS {
Value: self.ptr.cast(),
},
);
let _ = windows::Win32::Foundation::CloseHandle(self.handle);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(timestamp: u64) -> PowerRecord {
PowerRecord {
timestamp,
cpu_power: 1.0,
gpu_power: 2.0,
total_power: 3.0,
cpu_usage: 4.0,
pid_or_app_power: 5.0,
}
}
fn read_slot(region: &[u8], index: usize) -> PowerRecord {
let start = HEADER_SIZE + index * ENTRY_SIZE;
let mut bytes = [0u8; ENTRY_SIZE];
bytes.copy_from_slice(®ion[start..start + ENTRY_SIZE]);
unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast()) }
}
fn head_of(region: &[u8]) -> u64 {
u64::from_ne_bytes(region[..HEADER_SIZE].try_into().unwrap())
}
#[test]
fn the_wire_format_is_unchanged() {
let record = PowerRecord {
timestamp: 1_700_000_000,
cpu_power: 1.0,
gpu_power: 2.0,
total_power: 3.0,
cpu_usage: 4.0,
pid_or_app_power: 5.0,
};
let mut backing: Vec<u64> = vec![0; REGION_SIZE.div_ceil(size_of::<u64>())];
let base = backing.as_mut_ptr().cast::<u8>();
unsafe { write_entry(base, record) };
let region = unsafe { std::slice::from_raw_parts(base, REGION_SIZE) };
assert_eq!(ENTRY_SIZE, 48);
assert_eq!(HEADER_SIZE, 8);
let slot = ®ion[HEADER_SIZE..HEADER_SIZE + ENTRY_SIZE];
let u64_at =
|offset: usize| u64::from_ne_bytes(slot[offset..offset + 8].try_into().unwrap());
let f64_at =
|offset: usize| f64::from_ne_bytes(slot[offset..offset + 8].try_into().unwrap());
assert_eq!(u64_at(0), 1_700_000_000, "timestamp at offset 0");
assert_eq!(f64_at(8), 1.0, "cpu_power at offset 8");
assert_eq!(f64_at(16), 2.0, "gpu_power at offset 16");
assert_eq!(f64_at(24), 3.0, "total_power at offset 24");
assert_eq!(f64_at(32), 4.0, "cpu_usage at offset 32");
assert_eq!(f64_at(40), 5.0, "pid_or_app_power at offset 40");
}
#[test]
fn entries_land_in_successive_slots_and_wrap() {
let mut backing: Vec<u64> = vec![0; REGION_SIZE.div_ceil(size_of::<u64>())];
let base = backing.as_mut_ptr().cast::<u8>();
for i in 0..(BUFFER_SIZE as u64 + 2) {
unsafe { write_entry(base, sample(i)) };
}
let region = unsafe { std::slice::from_raw_parts(base, REGION_SIZE) };
assert_eq!(head_of(region), BUFFER_SIZE as u64 + 2);
assert_eq!(read_slot(region, 0).timestamp, 5);
assert_eq!(read_slot(region, 1).timestamp, 6);
assert_eq!(read_slot(region, 2).timestamp, 2);
}
}