joularcore 0.1.0

Joular Core is a platform to measure power and energy across all systems, OSes and devices
Documentation
/*
 * Copyright (c) 2025-2026, Adel Noureddine.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the
 * GNU Lesser General Public License v3.0 only (LGPL-3.0-only)
 * which accompanies this distribution, and is available at
 * https://www.gnu.org/licenses/lgpl-3.0.en.html
 *
 * Author : Adel Noureddine
 */

#[cfg(target_family = "unix")]
use {
    memmap2::MmapMut,
    std::fs::OpenOptions,
    std::io,
    std::mem::size_of,
    std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt},
    std::path::Path,
    std::sync::{Arc, Mutex},
};

#[cfg(target_os = "windows")]
use {
    std::io,
    std::mem::size_of,
    windows::Win32::Foundation::INVALID_HANDLE_VALUE,
    windows::Win32::System::Memory::{
        CreateFileMappingW, FILE_MAP_ALL_ACCESS, MapViewOfFile, PAGE_READWRITE,
    },
    windows::core::PCWSTR,
};

const BUFFER_SIZE: usize = 5;

/// Ring buffer struct to store in the ring buffer.
/// `timestamp` is the Unix epoch (seconds) at which this sample was produced;
/// consumers can use it to detect stale entries.
#[repr(C)]
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "api", derive(serde::Serialize))]
pub struct RingBufferStruct {
    pub timestamp: u64,
    pub cpu_power: f64,
    pub gpu_power: f64,
    pub total_power: f64,
    pub cpu_usage: f64,
    pub pid_app_power: f64,
}

impl From<&crate::monitor::MonitorSample> for RingBufferStruct {
    fn from(sample: &crate::monitor::MonitorSample) -> Self {
        Self {
            timestamp: sample.timestamp,
            cpu_power: sample.cpu_power,
            gpu_power: sample.gpu_power,
            total_power: sample.total_power,
            cpu_usage: sample.cpu_usage,
            pid_app_power: sample.pid_app_power(),
        }
    }
}

/// RingBuffer writer
pub struct RingBufferWriter {
    enabled: bool,
    #[cfg(target_family = "unix")]
    mmap: Option<Arc<Mutex<MmapMut>>>,
    #[cfg(target_os = "windows")]
    ptr: *mut u8,
    #[cfg(target_os = "windows")]
    handle: Option<windows::Win32::Foundation::HANDLE>,
}

impl RingBufferWriter {
    /// Platform-specific path / name of the shared memory object.
    pub fn shared_path() -> &'static str {
        if cfg!(target_os = "macos") {
            "/tmp/joularcorering"
        } else if cfg!(target_os = "windows") {
            "Local\\JoularCoreRing"
        } else {
            "/dev/shm/joularcorering"
        }
    }

    /// Create a new ring buffer writer
    pub fn new(enabled: bool) -> io::Result<Self> {
        if !enabled {
            return Ok(Self {
                enabled: false,
                #[cfg(target_family = "unix")]
                mmap: None,
                #[cfg(target_os = "windows")]
                ptr: std::ptr::null_mut(),
                #[cfg(target_os = "windows")]
                handle: None,
            });
        }

        #[cfg(target_family = "unix")]
        {
            let path = if cfg!(target_os = "macos") {
                "/tmp/joularcorering"
            } else {
                "/dev/shm/joularcorering"
            };

            let entry_size = size_of::<RingBufferStruct>();
            let file_size = 8 + BUFFER_SIZE * entry_size;

            // If the file already exists and is owned by another user, refuse
            // to share it rather than silently co-mingle data. The path lives
            // in a world-writable sticky-bit directory, so another user can
            // create the file first; in that case, bail with a clear error.
            if let Ok(meta) = std::fs::metadata(path) {
                let our_uid = unsafe { libc::geteuid() };
                if meta.uid() != our_uid {
                    return Err(io::Error::other(format!(
                        "Ring buffer file {path} is owned by another user; \
                         remove it or run as the owner"
                    )));
                }
            }

            // Try creating and opening the ring buffer file. Mode 0o600
            // means same-UID consumers (which is the IPC use case) can still
            // mmap it, while other local users on the box cannot read live
            // power telemetry.
            let file = OpenOptions::new()
                .read(true)
                .write(true)
                .create(true)
                .truncate(true)
                .mode(0o600)
                .open(Path::new(path))
                .or_else(|_| {
                    // Fallback: reopen without create/truncate (we already
                    // verified ownership above).
                    OpenOptions::new()
                        .read(true)
                        .write(true)
                        .open(Path::new(path))
                })
                .map_err(|e| io::Error::other(format!("Failed to open shared memory file: {e}")))?;

            // Belt-and-braces: also re-set the mode in case the file already
            // existed from an older Joular Core build with the default umask.
            let _ = file.set_permissions(std::fs::Permissions::from_mode(0o600));

            file.set_len(file_size as u64)?;
            let mmap = unsafe {
                MmapMut::map_mut(&file)
                    .map_err(|e| io::Error::other(format!("Failed to mmap shared memory: {e}")))?
            };
            let mmap = Arc::new(Mutex::new(mmap));

            Ok(Self {
                enabled,
                mmap: Some(mmap),
            })
        }

        #[cfg(target_os = "windows")]
        {
            let entry_size = size_of::<RingBufferStruct>();
            let file_size = 8 + BUFFER_SIZE * entry_size;

            // UTF-16 name for the mapping object (null-terminated)
            let local_name: Vec<u16> = "Local\\JoularCoreRing"
                .encode_utf16()
                .chain(std::iter::once(0))
                .collect();

            unsafe {
                // Create the shared memory
                let mapping = match CreateFileMappingW(
                    INVALID_HANDLE_VALUE,
                    None,
                    PAGE_READWRITE,
                    0,
                    file_size as u32,
                    PCWSTR::from_raw(local_name.as_ptr()),
                ) {
                    Ok(h) => h,
                    Err(e) => {
                        return Err(io::Error::other(format!("CreateFileMappingW failed: {e}")));
                    }
                };

                // Map into address space
                let result_ptr = MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);

                if result_ptr.Value.is_null() {
                    let _ = windows::Win32::Foundation::CloseHandle(mapping);
                    return Err(io::Error::other("MapViewOfFile failed"));
                }

                let ringbuffer_ptr = result_ptr.Value as *mut u8;

                Ok(Self {
                    enabled: true,
                    ptr: ringbuffer_ptr,
                    handle: Some(mapping),
                })
            }
        }
    }
}

impl Drop for RingBufferWriter {
    #[allow(clippy::needless_return)]
    fn drop(&mut self) {
        if !self.enabled {
            return;
        }

        #[cfg(target_os = "windows")]
        unsafe {
            if !self.ptr.is_null() {
                let _ = windows::Win32::System::Memory::UnmapViewOfFile(
                    windows::Win32::System::Memory::MEMORY_MAPPED_VIEW_ADDRESS {
                        Value: self.ptr as *mut _,
                    },
                );
            }
            if let Some(handle) = self.handle {
                let _ = windows::Win32::Foundation::CloseHandle(handle);
            }
        }
    }
}
impl RingBufferWriter {
    /// Write a ring buffer struct to the ring buffer
    pub fn write(&self, ringbufferstruct: RingBufferStruct) {
        if !self.enabled {
            return;
        }

        #[cfg(target_family = "unix")]
        if let Some(ref mmap_mutex) = self.mmap {
            let mut mmap = mmap_mutex.lock().unwrap_or_else(|e| e.into_inner());
            let entry_size = size_of::<RingBufferStruct>();
            let required = 8 + BUFFER_SIZE * entry_size;
            if mmap.len() < required {
                crate::logging::print_error(
                    "Ring buffer mmap too small; skipping write (buffer may be corrupted)",
                );
                return;
            }
            let head_bytes: [u8; 8] = match mmap[0..8].try_into() {
                Ok(b) => b,
                Err(_) => return,
            };
            let head = u64::from_ne_bytes(head_bytes);
            let idx = (head as usize) % BUFFER_SIZE;
            let offset = 8 + idx * entry_size;
            let bytes: &[u8] = unsafe {
                std::slice::from_raw_parts(
                    &ringbufferstruct as *const RingBufferStruct as *const u8,
                    entry_size,
                )
            };
            mmap[offset..offset + bytes.len()].copy_from_slice(bytes);
            mmap[0..8].copy_from_slice(&head.wrapping_add(1).to_ne_bytes());
        }

        #[cfg(target_os = "windows")]
        unsafe {
            let head_ptr = self.ptr as *mut u64;
            let head = *head_ptr;
            let idx = (head as usize) % BUFFER_SIZE;
            let offset = 8 + idx * std::mem::size_of::<RingBufferStruct>();
            let ringbufferstruct_ptr = self.ptr.add(offset) as *mut RingBufferStruct;
            *ringbufferstruct_ptr = ringbufferstruct;
            *head_ptr = head + 1;
        }
    }
}