coretempd: implement per-CPU die temperature daemon
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "coretempd"
|
||||
version = "0.2.5"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "coretempd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
redox-scheme = { path = "../../../../../local/sources/redox-scheme" }
|
||||
syscall = { path = "../../../../../local/sources/syscall", package = "redox_syscall", features = ["std"] }
|
||||
|
||||
[patch.crates-io]
|
||||
redox-scheme = { path = "../../../../../local/sources/redox-scheme" }
|
||||
redox_syscall = { path = "../../../../../local/sources/syscall" }
|
||||
@@ -0,0 +1,348 @@
|
||||
// coretempd — CPU core temperature monitoring daemon
|
||||
//
|
||||
// Reads the IA32_THERM_STATUS MSR (0x19C) for each logical CPU via the
|
||||
// kernel's /scheme/sys/msr/ scheme and exposes per-CPU die temperatures at
|
||||
// /scheme/coretemp/{cpu}/temperature. A summary file lists all CPUs.
|
||||
//
|
||||
// Cross-referenced with Linux drivers/hwmon/coretemp.c:
|
||||
// MSR 0x19C bits 16-22 = digital readout (offset from TjMax)
|
||||
// MSR 0x1A2 bits 16-23 = temperature target (TjMax)
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{info, LevelFilter, Metadata, Record};
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use log::{error, warn};
|
||||
#[cfg(target_os = "redox")]
|
||||
use redox_scheme::{
|
||||
scheme::{SchemeState, SchemeSync},
|
||||
CallerCtx, OpenResult, SignalBehavior, Socket,
|
||||
};
|
||||
#[cfg(target_os = "redox")]
|
||||
use syscall::flag::{MODE_DIR, MODE_FILE};
|
||||
#[cfg(target_os = "redox")]
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
#[cfg(target_os = "redox")]
|
||||
use syscall::{
|
||||
error::{Error as SysError, Result as SysResult, EBADF, EINVAL, ENOENT},
|
||||
Stat,
|
||||
};
|
||||
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(2);
|
||||
const MSR_THERM_STATUS: u32 = 0x19C;
|
||||
const MSR_TEMPERATURE_TARGET: u32 = 0x1A2;
|
||||
const TJMAX_DEFAULT_C: f64 = 100.0;
|
||||
|
||||
struct StderrLogger;
|
||||
|
||||
impl log::Log for StderrLogger {
|
||||
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
|
||||
metadata.level() <= LevelFilter::Info
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record<'_>) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let _ = writeln!(io::stderr().lock(), "[{}] coretempd: {}", record.level(), record.args());
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
fn read_msr(cpu: u32, msr: u32) -> Option<u64> {
|
||||
let path = format!("/scheme/sys/msr/{}/0x{:x}", cpu, msr);
|
||||
let mut file = fs::File::open(&path).ok()?;
|
||||
let mut buf = [0u8; 8];
|
||||
use std::io::Read;
|
||||
file.read_exact(&mut buf).ok()?;
|
||||
Some(u64::from_le_bytes(buf))
|
||||
}
|
||||
|
||||
fn cpu_count() -> u32 {
|
||||
if let Ok(data) = fs::read_to_string("/scheme/sys/cpu") {
|
||||
for line in data.lines() {
|
||||
if let Some(rest) = line.strip_prefix("CPUs: ") {
|
||||
if let Ok(n) = rest.trim().parse::<u32>() {
|
||||
return n.max(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn read_cpu_temperature(cpu: u32) -> Option<f64> {
|
||||
let tjmax = read_msr(cpu, MSR_TEMPERATURE_TARGET)
|
||||
.map(|v| ((v >> 16) & 0xFF) as f64)
|
||||
.unwrap_or(TJMAX_DEFAULT_C);
|
||||
let status = read_msr(cpu, MSR_THERM_STATUS)?;
|
||||
if status & (1 << 31) == 0 {
|
||||
return None;
|
||||
}
|
||||
let digital_readout = ((status >> 16) & 0x7F) as f64;
|
||||
Some(tjmax - digital_readout)
|
||||
}
|
||||
|
||||
fn refresh_temps(count: u32) -> Vec<(u32, f64)> {
|
||||
let mut temps = Vec::new();
|
||||
for cpu in 0..count {
|
||||
if let Some(temp) = read_cpu_temperature(cpu) {
|
||||
temps.push((cpu, temp));
|
||||
}
|
||||
}
|
||||
temps
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
const SCHEME_ROOT_ID: usize = 1;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Debug)]
|
||||
enum HandleKind {
|
||||
Root,
|
||||
CpuDir(u32),
|
||||
Temperature(u32),
|
||||
Summary,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct CoretempScheme {
|
||||
temps: Arc<RwLock<Vec<(u32, f64)>>>,
|
||||
next_id: usize,
|
||||
handles: BTreeMap<usize, HandleKind>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl CoretempScheme {
|
||||
fn new(temps: Arc<RwLock<Vec<(u32, f64)>>>) -> Self {
|
||||
Self {
|
||||
temps,
|
||||
next_id: SCHEME_ROOT_ID + 1,
|
||||
handles: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn alloc_handle(&mut self, kind: HandleKind) -> usize {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.handles.insert(id, kind);
|
||||
id
|
||||
}
|
||||
|
||||
fn handle(&self, id: usize) -> SysResult<&HandleKind> {
|
||||
self.handles.get(&id).ok_or(SysError::new(EBADF))
|
||||
}
|
||||
|
||||
fn is_dir(kind: &HandleKind) -> bool {
|
||||
matches!(kind, HandleKind::Root | HandleKind::CpuDir(_))
|
||||
}
|
||||
|
||||
fn read_file(&self, kind: &HandleKind) -> Option<String> {
|
||||
let temps = self.temps.read().ok()?;
|
||||
match kind {
|
||||
HandleKind::Temperature(cpu) => temps
|
||||
.iter()
|
||||
.find(|(c, _)| *c == *cpu)
|
||||
.map(|(_, t)| format!("{:.1}\n", t)),
|
||||
HandleKind::Summary => {
|
||||
let mut out = String::new();
|
||||
for (cpu, t) in temps.iter() {
|
||||
out.push_str(&format!("cpu{}={:.1}\n", cpu, t));
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(&self, path: &str) -> SysResult<HandleKind> {
|
||||
let trimmed = path.trim_matches('/');
|
||||
if trimmed.is_empty() {
|
||||
return Ok(HandleKind::Root);
|
||||
}
|
||||
let parts: Vec<&str> = trimmed.split('/').filter(|p| !p.is_empty()).collect();
|
||||
match parts.as_slice() {
|
||||
["summary"] => Ok(HandleKind::Summary),
|
||||
[cpu_str] => {
|
||||
let cpu = cpu_str.parse::<u32>().map_err(|_| SysError::new(EINVAL))?;
|
||||
if self.temps.read().map(|t| t.iter().any(|(c, _)| *c == cpu)).unwrap_or(false) {
|
||||
Ok(HandleKind::CpuDir(cpu))
|
||||
} else {
|
||||
Err(SysError::new(ENOENT))
|
||||
}
|
||||
}
|
||||
[cpu_str, "temperature"] => {
|
||||
let cpu = cpu_str.parse::<u32>().map_err(|_| SysError::new(EINVAL))?;
|
||||
if self.temps.read().map(|t| t.iter().any(|(c, _)| *c == cpu)).unwrap_or(false) {
|
||||
Ok(HandleKind::Temperature(cpu))
|
||||
} else {
|
||||
Err(SysError::new(ENOENT))
|
||||
}
|
||||
}
|
||||
_ => Err(SysError::new(ENOENT)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl SchemeSync for CoretempScheme {
|
||||
fn scheme_root(&mut self) -> SysResult<usize> {
|
||||
Ok(SCHEME_ROOT_ID)
|
||||
}
|
||||
|
||||
fn openat(
|
||||
&mut self,
|
||||
dirfd: usize,
|
||||
path: &str,
|
||||
_flags: usize,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> SysResult<OpenResult> {
|
||||
let kind = if dirfd == SCHEME_ROOT_ID {
|
||||
self.resolve(path)?
|
||||
} else {
|
||||
let parent = self.handle(dirfd)?.clone();
|
||||
match parent {
|
||||
HandleKind::Root => self.resolve(path)?,
|
||||
HandleKind::CpuDir(cpu) => {
|
||||
if path.trim_matches('/') == "temperature" {
|
||||
HandleKind::Temperature(cpu)
|
||||
} else {
|
||||
return Err(SysError::new(ENOENT));
|
||||
}
|
||||
}
|
||||
_ => return Err(SysError::new(EINVAL)),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(OpenResult::ThisScheme {
|
||||
number: self.alloc_handle(kind),
|
||||
flags: NewFdFlags::POSITIONED,
|
||||
})
|
||||
}
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> SysResult<()> {
|
||||
let kind = if id == SCHEME_ROOT_ID {
|
||||
HandleKind::Root
|
||||
} else {
|
||||
self.handle(id)?.clone()
|
||||
};
|
||||
stat.st_mode = if Self::is_dir(&kind) { MODE_DIR } else { MODE_FILE };
|
||||
stat.st_size = match self.read_file(&kind) {
|
||||
Some(content) => content.len() as u64,
|
||||
None => 0,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(
|
||||
&mut self,
|
||||
id: usize,
|
||||
buf: &mut [u8],
|
||||
offset: u64,
|
||||
_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> SysResult<usize> {
|
||||
let kind = self.handle(id)?.clone();
|
||||
if Self::is_dir(&kind) {
|
||||
return Err(SysError::new(EINVAL));
|
||||
}
|
||||
let Some(content) = self.read_file(&kind) else {
|
||||
return Err(SysError::new(ENOENT));
|
||||
};
|
||||
let bytes = content.as_bytes();
|
||||
let offset = usize::try_from(offset).map_err(|_| SysError::new(EINVAL))?;
|
||||
if offset >= bytes.len() {
|
||||
return Ok(0);
|
||||
}
|
||||
let count = (bytes.len() - offset).min(buf.len());
|
||||
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn on_close(&mut self, id: usize) {
|
||||
self.handles.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run_scheme(temps: Arc<RwLock<Vec<(u32, f64)>>>) {
|
||||
let socket = match Socket::create() {
|
||||
Ok(socket) => socket,
|
||||
Err(error) => {
|
||||
error!("failed to create scheme:coretemp socket: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut scheme = CoretempScheme::new(temps);
|
||||
let mut state = SchemeState::new();
|
||||
|
||||
match libredox::call::setrens(0, 0) {
|
||||
Ok(_) => info!("/scheme/coretemp ready"),
|
||||
Err(error) => {
|
||||
error!("failed to enter null namespace for scheme:coretemp: {error}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let request = match socket.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(request)) => request,
|
||||
Ok(None) => {
|
||||
warn!("scheme:coretemp socket closed; stopping coretemp scheme server");
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
error!("failed to read scheme:coretemp request: {error}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if let redox_scheme::RequestKind::Call(request) = request.kind() {
|
||||
let response = request.handle_sync(&mut scheme, &mut state);
|
||||
if let Err(error) = socket.write_response(response, SignalBehavior::Restart) {
|
||||
error!("failed to write scheme:coretemp response: {error}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn run_scheme(_temps: Arc<RwLock<Vec<(u32, f64)>>>) {
|
||||
info!("host build: scheme:coretemp serving is disabled outside Redox");
|
||||
}
|
||||
|
||||
fn monitor_loop(temps: Arc<RwLock<Vec<(u32, f64)>>>) -> ! {
|
||||
let count = cpu_count();
|
||||
info!("monitoring {} CPU core(s)", count);
|
||||
loop {
|
||||
let new_temps = refresh_temps(count);
|
||||
if let Ok(mut guard) = temps.write() {
|
||||
*guard = new_temps;
|
||||
}
|
||||
thread::sleep(POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
log::set_logger(&StderrLogger).ok();
|
||||
log::set_max_level(LevelFilter::Info);
|
||||
|
||||
info!("core temperature daemon starting");
|
||||
|
||||
let temps = Arc::new(RwLock::new(Vec::new()));
|
||||
let scheme_temps = Arc::clone(&temps);
|
||||
|
||||
let _scheme_thread = thread::spawn(move || run_scheme(scheme_temps));
|
||||
monitor_loop(temps);
|
||||
}
|
||||
Reference in New Issue
Block a user