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
 */

use crate::energy::{CPUEnergy, GPUEnergy};
use std::fs;
use std::io;

/// Validate that `file_path` points to an existing regular file that we can open,
/// returning the canonicalized path so subsequent reads follow the same target.
fn validate_power_file(file_path: &str, var_name: &str) -> Result<String, String> {
    if file_path.is_empty() {
        return Err(format!("{} is set but empty", var_name));
    }

    let canonical = fs::canonicalize(file_path).map_err(|e| {
        format!(
            "{} points to {:?} but the file is unreadable: {}",
            var_name, file_path, e
        )
    })?;

    let meta = fs::metadata(&canonical).map_err(|e| {
        format!(
            "{} points to {:?} but metadata cannot be read: {}",
            var_name, canonical, e
        )
    })?;

    if !meta.is_file() {
        return Err(format!(
            "{} points to {:?} which is not a regular file",
            var_name, canonical
        ));
    }

    Ok(canonical.to_string_lossy().into_owned())
}

/// Power format types supported by VM power monitor
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PowerFormat {
    /// PowerJoular format (CSV with 3 columns, power in 3rd column)
    PowerJoular,
    /// Simple watts format (single value)
    Watts,
    /// JoularCore format (CSV with variable columns)
    JoularCore,
}

impl std::str::FromStr for PowerFormat {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "powerjoular" => Ok(PowerFormat::PowerJoular),
            "watts" => Ok(PowerFormat::Watts),
            "joularcore" => Ok(PowerFormat::JoularCore),
            _ => Err(format!("Unsupported power format: {}", s)),
        }
    }
}

/// Power type to extract from JoularCore CSV
#[derive(Debug, Clone, Copy, PartialEq)]
enum JoularCorePowerType {
    Cpu,
    Gpu,
}

/// Read power from PowerJoular format (CSV with 3 columns)
fn read_powerjoular(content: &str) -> Result<f64, io::Error> {
    let first_line = content.lines().next().unwrap_or("");

    if first_line.is_empty() {
        return Ok(0.0);
    }

    let parts: Vec<&str> = first_line.split(',').collect();

    if parts.len() < 3 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("Expected at least 3 columns, found {}", parts.len()),
        ));
    }

    parts[2]
        .trim()
        .parse::<f64>()
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
}

/// Read power from simple watts format (single value)
fn read_watts(content: &str) -> Result<f64, io::Error> {
    let first_line = content.lines().next().unwrap_or("");
    let trimmed = first_line.trim();

    if trimmed.is_empty() {
        return Ok(0.0);
    }

    trimmed
        .parse::<f64>()
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
}

/// Parse JoularCore CSV and extract the appropriate power value
fn parse_joularcore(content: &str, power_type: JoularCorePowerType) -> Result<f64, io::Error> {
    let mut lines = content.lines().filter(|line| !line.trim().is_empty());

    let header = lines.next().unwrap_or("");
    let data = lines.next_back().unwrap_or("");

    if header.is_empty() || data.is_empty() {
        return Ok(0.0);
    }

    let headers: Vec<&str> = header.split(',').map(str::trim).collect();
    let values: Vec<&str> = data.split(',').map(str::trim).collect();

    if headers.len() != values.len() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "Header and data column count mismatch: {} vs {}",
                headers.len(),
                values.len()
            ),
        ));
    }

    // Priority order for column selection
    let target_columns: &[&str] = match power_type {
        JoularCorePowerType::Cpu => &["App Power (W)", "Process Power (W)", "CPU Power (W)"],
        JoularCorePowerType::Gpu => &["GPU Power (W)"],
    };

    // Find first matching column and parse its value
    target_columns
        .iter()
        .find_map(|&col| {
            headers
                .iter()
                .position(|&h| h == col)
                .and_then(|idx| values[idx].parse::<f64>().ok())
        })
        .ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                format!("No valid power column found for {:?}", power_type),
            )
        })
}

/// Read power based on format and power type
fn read_power_from_file(
    file_path: &str,
    power_format: PowerFormat,
    power_type: JoularCorePowerType,
) -> f64 {
    let content = match fs::read_to_string(file_path) {
        Ok(c) => c,
        Err(_) => return 0.0,
    };

    let result = match power_format {
        PowerFormat::PowerJoular => read_powerjoular(&content),
        PowerFormat::Watts => read_watts(&content),
        PowerFormat::JoularCore => parse_joularcore(&content, power_type),
    };

    result.unwrap_or(0.0)
}

/// Virtual Machine CPU power reader
pub struct VmCpu {
    file_path: String,
    power_format: PowerFormat,
}

impl VmCpu {
    /// Create a new VM CPU power monitor
    pub fn new(file_path: String, power_format: PowerFormat) -> Result<Self, String> {
        let file_path = validate_power_file(&file_path, "VM_CPU_POWER_FILE")?;

        Ok(Self {
            file_path,
            power_format,
        })
    }

    /// Create from environment variables
    pub fn from_env() -> Result<Self, String> {
        let file_path = std::env::var("VM_CPU_POWER_FILE")
            .map_err(|_| "VM_CPU_POWER_FILE environment variable not set".to_string())?;

        let format_str =
            std::env::var("VM_CPU_POWER_FORMAT").unwrap_or_else(|_| "watts".to_string());

        let power_format = format_str.parse::<PowerFormat>()?;

        Self::new(file_path, power_format)
    }
}

impl CPUEnergy for VmCpu {
    fn get_power(&self) -> f64 {
        read_power_from_file(&self.file_path, self.power_format, JoularCorePowerType::Cpu)
    }
}

/// Virtual Machine GPU power reader
pub struct VmGpu {
    file_path: String,
    power_format: PowerFormat,
}

impl VmGpu {
    /// Create a new VM GPU power monitor
    pub fn new(file_path: String, power_format: PowerFormat) -> Result<Self, String> {
        let file_path = validate_power_file(&file_path, "VM_GPU_POWER_FILE")?;

        Ok(Self {
            file_path,
            power_format,
        })
    }

    /// Create from environment variables
    pub fn from_env() -> Result<Self, String> {
        let file_path = std::env::var("VM_GPU_POWER_FILE")
            .map_err(|_| "VM_GPU_POWER_FILE environment variable not set".to_string())?;

        let format_str =
            std::env::var("VM_GPU_POWER_FORMAT").unwrap_or_else(|_| "watts".to_string());

        let power_format = format_str.parse::<PowerFormat>()?;

        Self::new(file_path, power_format)
    }
}

impl GPUEnergy for VmGpu {
    fn get_power(&self) -> f64 {
        read_power_from_file(&self.file_path, self.power_format, JoularCorePowerType::Gpu)
    }
}