coretempd: implement per-CPU die temperature daemon

This commit is contained in:
2026-07-08 01:14:16 +03:00
parent 207dddff9e
commit 838e17f4fa
2 changed files with 367 additions and 0 deletions
@@ -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);
}