use crate::utils::command::new_command;
use std::io::BufRead;
use std::io::BufReader;
use std::panic::{self, AssertUnwindSafe};
use std::process::{Child, Stdio};
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use super::config::{HlsmiConfig, ReaderCommand};
use super::store::MetricsStore;
#[cfg(unix)]
use libc;
pub struct ProcessManager {
process: Arc<Mutex<Option<Child>>>,
command_tx: Option<Sender<ReaderCommand>>,
is_running: Arc<Mutex<bool>>,
config: HlsmiConfig,
store: Arc<MetricsStore>,
}
impl ProcessManager {
pub fn new(config: HlsmiConfig, store: Arc<MetricsStore>) -> Self {
Self {
process: Arc::new(Mutex::new(None)),
command_tx: None,
is_running: Arc::new(Mutex::new(false)),
config,
store,
}
}
pub fn start(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let (command_tx, command_rx) = mpsc::channel();
self.command_tx = Some(command_tx);
let process_clone = self.process.clone();
let is_running_clone = self.is_running.clone();
let old_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
if let Ok(mut guard) = process_clone.lock()
&& let Some(mut child) = guard.take()
{
let _ = child.kill();
let _ = child.wait();
}
if let Ok(mut running) = is_running_clone.lock() {
*running = false;
}
old_hook(panic_info);
}));
self.start_hlsmi_process(command_rx)?;
self.start_monitor_thread();
Ok(())
}
fn start_hlsmi_process(
&self,
command_rx: Receiver<ReaderCommand>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = new_command("hl-smi");
let args = self.config.get_hlsmi_args();
cmd.args(&args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
let mut child = cmd.spawn()?;
let stdout = child.stdout.take().ok_or("Failed to capture stdout")?;
let data_buffer = self.store.get_buffer();
let buffer_capacity = self.config.buffer_capacity;
thread::spawn(move || {
let _ = panic::catch_unwind(AssertUnwindSafe(|| {
Self::reader_thread(stdout, data_buffer, command_rx, buffer_capacity);
}));
});
let mut process_guard = self.process.lock().unwrap();
*process_guard = Some(child);
let mut is_running = self.is_running.lock().unwrap();
*is_running = true;
Ok(())
}
fn reader_thread(
stdout: std::process::ChildStdout,
data_buffer: Arc<Mutex<std::collections::VecDeque<String>>>,
command_rx: Receiver<ReaderCommand>,
buffer_capacity: usize,
) {
use std::fmt::Write;
let reader = BufReader::new(stdout);
let mut current_snapshot = String::with_capacity(4096);
let mut device_count = 0; let mut lines_in_snapshot = 0;
for line in reader.lines() {
if let Ok(ReaderCommand::Shutdown) = command_rx.try_recv() {
break;
}
let line = match line {
Ok(l) => l,
Err(_) => break, };
let line = line.trim();
if line.is_empty() {
continue;
}
let device_index = line
.split(',')
.next()
.and_then(|s| s.trim().parse::<usize>().ok())
.unwrap_or(usize::MAX);
if device_index == 0 && lines_in_snapshot > 0 {
if !current_snapshot.is_empty() {
let mut buffer = data_buffer.lock().unwrap();
if buffer.len() >= buffer_capacity {
buffer.pop_front(); }
buffer.push_back(std::mem::take(&mut current_snapshot));
current_snapshot.reserve(4096);
}
if device_count == 0 {
device_count = lines_in_snapshot;
}
lines_in_snapshot = 0;
}
let _ = writeln!(current_snapshot, "{line}");
lines_in_snapshot += 1;
if device_count > 0 && lines_in_snapshot >= device_count {
let mut buffer = data_buffer.lock().unwrap();
if buffer.len() >= buffer_capacity {
buffer.pop_front();
}
buffer.push_back(std::mem::take(&mut current_snapshot));
current_snapshot.reserve(4096);
lines_in_snapshot = 0;
}
}
}
fn start_monitor_thread(&self) {
let process_arc = self.process.clone();
let store_arc = self.store.clone();
let is_running = self.is_running.clone();
let config = self.config.clone();
thread::spawn(move || {
loop {
thread::sleep(Duration::from_secs(config.monitor_interval_secs));
let should_restart = {
let mut process_guard = process_arc.lock().unwrap();
if let Some(ref mut child) = *process_guard {
match child.try_wait() {
Ok(Some(_)) => {
#[cfg(debug_assertions)]
eprintln!("hl-smi process died, restarting...");
true
}
Ok(None) => false, Err(_e) => {
#[cfg(debug_assertions)]
eprintln!("Error checking hl-smi status: {_e}");
true
}
}
} else {
false
}
};
if should_restart {
if let Ok(running) = is_running.lock()
&& !*running
{
break; }
let (_new_tx, new_rx) = mpsc::channel();
if let Err(_e) = Self::restart_hlsmi(&process_arc, &store_arc, new_rx, &config)
{
#[cfg(debug_assertions)]
eprintln!("Failed to restart hl-smi: {_e}");
}
}
}
});
}
fn restart_hlsmi(
process_arc: &Arc<Mutex<Option<Child>>>,
store: &Arc<MetricsStore>,
command_rx: Receiver<ReaderCommand>,
config: &HlsmiConfig,
) -> Result<(), Box<dyn std::error::Error>> {
{
let mut process_guard = process_arc.lock().unwrap();
if let Some(mut child) = process_guard.take() {
let _ = child.kill();
let _ = child.wait();
}
}
let mut cmd = new_command("hl-smi");
let args = config.get_hlsmi_args();
cmd.args(&args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
let mut child = cmd.spawn()?;
let stdout = child.stdout.take().ok_or("Failed to capture stdout")?;
let data_buffer = store.get_buffer();
let buffer_capacity = config.buffer_capacity;
thread::spawn(move || {
let _ = panic::catch_unwind(AssertUnwindSafe(|| {
Self::reader_thread(stdout, data_buffer, command_rx, buffer_capacity);
}));
});
let mut process_guard = process_arc.lock().unwrap();
*process_guard = Some(child);
Ok(())
}
pub fn shutdown(&mut self) {
{
let mut is_running = self.is_running.lock().unwrap();
*is_running = false;
}
if let Some(tx) = &self.command_tx {
let _ = tx.send(ReaderCommand::Shutdown);
}
{
let mut process_guard = self.process.lock().unwrap();
if let Some(mut child) = process_guard.take() {
#[cfg(unix)]
{
let pid = child.id() as i32;
unsafe {
let _ = libc::killpg(pid, libc::SIGTERM);
thread::sleep(Duration::from_millis(100));
let _ = libc::killpg(pid, libc::SIGKILL);
}
}
let _ = child.kill();
let _ = child.wait();
}
}
}
#[cfg(test)]
pub(super) fn is_running(&self) -> bool {
*self.is_running.lock().unwrap()
}
}
impl Drop for ProcessManager {
fn drop(&mut self) {
self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reader_thread_shutdown() {
use std::io::Cursor;
let test_input = "0, UUID-1, HL-325L, 1.22.1, 131072 MiB, 672 MiB, 130400 MiB, 226 W, 850 W, 36 C, 0 %\n\
1, UUID-2, HL-325L, 1.22.1, 131072 MiB, 672 MiB, 130400 MiB, 230 W, 850 W, 39 C, 0 %\n";
let cursor = Cursor::new(test_input);
let data_buffer = Arc::new(Mutex::new(std::collections::VecDeque::new()));
let (tx, rx) = mpsc::channel::<ReaderCommand>();
let buffer_clone = data_buffer.clone();
let handle = thread::spawn(move || {
let reader = BufReader::new(cursor);
let mut snapshot = String::new();
for line in reader.lines() {
if let Ok(ReaderCommand::Shutdown) = rx.try_recv() {
break;
}
if let Ok(line) = line {
snapshot.push_str(&line);
snapshot.push('\n');
}
thread::sleep(Duration::from_millis(10));
}
if !snapshot.is_empty() {
let mut buffer = buffer_clone.lock().unwrap();
buffer.push_back(snapshot);
}
});
thread::sleep(Duration::from_millis(50));
let _ = tx.send(ReaderCommand::Shutdown);
let _ = handle.join();
}
}