all-smi 0.26.2

Command-line utility for monitoring GPU hardware. It provides a real-time view of GPU utilization, memory usage, temperature, power consumption, and other metrics.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
// Copyright 2025 Lablup Inc. and Jeongkyu Shin
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

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;

/// Manages the hl-smi subprocess lifecycle
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 {
    /// Create a new 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,
        }
    }

    /// Start the hl-smi process and monitoring thread
    pub fn start(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let (command_tx, command_rx) = mpsc::channel();
        self.command_tx = Some(command_tx);

        // Set up panic handler to cleanup on panic
        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| {
            // Cleanup subprocess on panic
            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);
        }));

        // Start the hl-smi process
        self.start_hlsmi_process(command_rx)?;

        // Start monitoring thread
        self.start_monitor_thread();

        Ok(())
    }

    /// Start the hl-smi subprocess with stdout piping
    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());

        // On Unix, create a new process group so we can kill all children
        #[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")?;

        // Start reader thread with panic catching
        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(())
    }

    /// Reader thread that processes stdout from hl-smi
    /// hl-smi outputs CSV lines continuously, we accumulate a complete snapshot
    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; // 0 means we don't know yet
        let mut lines_in_snapshot = 0;

        for line in reader.lines() {
            // Check for shutdown command
            if let Ok(ReaderCommand::Shutdown) = command_rx.try_recv() {
                break;
            }

            let line = match line {
                Ok(l) => l,
                Err(_) => break, // Pipe broken, process died
            };

            let line = line.trim();
            if line.is_empty() {
                continue;
            }

            // Parse the device index (first field in CSV)
            let device_index = line
                .split(',')
                .next()
                .and_then(|s| s.trim().parse::<usize>().ok())
                .unwrap_or(usize::MAX);

            // If we see index 0 and we already have data, this marks the start of a new snapshot
            if device_index == 0 && lines_in_snapshot > 0 {
                // Store the complete snapshot we just finished
                if !current_snapshot.is_empty() {
                    let mut buffer = data_buffer.lock().unwrap();
                    if buffer.len() >= buffer_capacity {
                        buffer.pop_front(); // Remove oldest
                    }
                    buffer.push_back(std::mem::take(&mut current_snapshot));
                    current_snapshot.reserve(4096);
                }

                // If we didn't know device count, now we do
                if device_count == 0 {
                    device_count = lines_in_snapshot;
                }

                // Reset for new snapshot
                lines_in_snapshot = 0;
            }

            // Add line to current snapshot
            let _ = writeln!(current_snapshot, "{line}");
            lines_in_snapshot += 1;

            // If we know device count and have collected all devices, store it immediately
            // This handles cases where output comes in batches
            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;
            }
        }
    }

    /// Start a background thread to monitor the process
    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(_)) => {
                                // Process has exited, need to restart
                                #[cfg(debug_assertions)]
                                eprintln!("hl-smi process died, restarting...");
                                true
                            }
                            Ok(None) => false, // Still running
                            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; // Manager is shutting down
                    }

                    // Create new channel for the restarted process
                    let (_new_tx, new_rx) = mpsc::channel();

                    // Restart hl-smi
                    if let Err(_e) = Self::restart_hlsmi(&process_arc, &store_arc, new_rx, &config)
                    {
                        #[cfg(debug_assertions)]
                        eprintln!("Failed to restart hl-smi: {_e}");
                    }
                }
            }
        });
    }

    /// Restart the hl-smi process
    fn restart_hlsmi(
        process_arc: &Arc<Mutex<Option<Child>>>,
        store: &Arc<MetricsStore>,
        command_rx: Receiver<ReaderCommand>,
        config: &HlsmiConfig,
    ) -> Result<(), Box<dyn std::error::Error>> {
        // Kill existing process if any
        {
            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());

        // On Unix, create a new process group so we can kill all children
        #[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")?;

        // Start new reader thread with panic catching
        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(())
    }

    /// Shutdown the process manager
    pub fn shutdown(&mut self) {
        // Mark as not running
        {
            let mut is_running = self.is_running.lock().unwrap();
            *is_running = false;
        }

        // Send shutdown command to reader thread
        if let Some(tx) = &self.command_tx {
            let _ = tx.send(ReaderCommand::Shutdown);
        }

        // Kill only the process we started
        {
            let mut process_guard = self.process.lock().unwrap();
            if let Some(mut child) = process_guard.take() {
                #[cfg(unix)]
                {
                    // Kill the process group we created
                    let pid = child.id() as i32;
                    unsafe {
                        // Since we set process_group(0) when spawning,
                        // the child is the leader of its own process group
                        let _ = libc::killpg(pid, libc::SIGTERM);
                        thread::sleep(Duration::from_millis(100));

                        // If still running, force kill
                        let _ = libc::killpg(pid, libc::SIGKILL);
                    }
                }

                // Also try to kill via the Child handle
                let _ = child.kill();
                let _ = child.wait();
            }
        }
    }

    /// Check if the process is running (test use only)
    #[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>();

        // Spawn a thread simulating the reader
        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() {
                // Check for shutdown
                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);
            }
        });

        // Let it read some lines
        thread::sleep(Duration::from_millis(50));

        // Send shutdown command
        let _ = tx.send(ReaderCommand::Shutdown);

        // Wait for thread to finish
        let _ = handle.join();

        // The thread completed
        // No assertions needed - test passes if no panic
    }
}