use crate::energy::{CPUEnergy, GPUEnergy};
use std::fs;
use std::io;
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())
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PowerFormat {
PowerJoular,
Watts,
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)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum JoularCorePowerType {
Cpu,
Gpu,
}
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()))
}
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()))
}
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()
),
));
}
let target_columns: &[&str] = match power_type {
JoularCorePowerType::Cpu => &["App Power (W)", "Process Power (W)", "CPU Power (W)"],
JoularCorePowerType::Gpu => &["GPU Power (W)"],
};
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),
)
})
}
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)
}
pub struct VmCpu {
file_path: String,
power_format: PowerFormat,
}
impl VmCpu {
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,
})
}
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)
}
}
pub struct VmGpu {
file_path: String,
power_format: PowerFormat,
}
impl VmGpu {
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,
})
}
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)
}
}