From 838e17f4faae300cd7049d98f01947b7835f828e Mon Sep 17 00:00:00 2001 From: vasilito Date: Wed, 8 Jul 2026 01:14:16 +0300 Subject: [PATCH] coretempd: implement per-CPU die temperature daemon --- .../system/coretempd/source/Cargo.toml | 19 + .../system/coretempd/source/src/main.rs | 348 ++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 local/recipes/system/coretempd/source/Cargo.toml create mode 100644 local/recipes/system/coretempd/source/src/main.rs diff --git a/local/recipes/system/coretempd/source/Cargo.toml b/local/recipes/system/coretempd/source/Cargo.toml new file mode 100644 index 0000000000..1fe40dffab --- /dev/null +++ b/local/recipes/system/coretempd/source/Cargo.toml @@ -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" } diff --git a/local/recipes/system/coretempd/source/src/main.rs b/local/recipes/system/coretempd/source/src/main.rs new file mode 100644 index 0000000000..45e7b2d6e3 --- /dev/null +++ b/local/recipes/system/coretempd/source/src/main.rs @@ -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 { + 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::() { + return n.max(1); + } + } + } + } + 1 +} + +fn read_cpu_temperature(cpu: u32) -> Option { + 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>>, + next_id: usize, + handles: BTreeMap, +} + +#[cfg(target_os = "redox")] +impl CoretempScheme { + fn new(temps: Arc>>) -> 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 { + 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 { + 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::().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::().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 { + Ok(SCHEME_ROOT_ID) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + _flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> SysResult { + 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 { + 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>>) { + 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>>) { + info!("host build: scheme:coretemp serving is disabled outside Redox"); +} + +fn monitor_loop(temps: Arc>>) -> ! { + 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); +}