tegratop 0.2.1

TUI for monitoring Nvidia jetson boards
Documentation
use anyhow::Context;
use anyhow::Result;

use log::error;
use std::fs::File;
use std::io::Read;
use std::io::Seek;

use std::{ffi::CString, fs};

use ratatui::{
    layout::{Constraint, Rect},
    style::{Style, Stylize},
    widgets::{Block, Borders, Padding, Row, Table},
    Frame,
};

#[derive(Debug, Default)]
pub struct Disk {
    device_name: Option<String>,
    space: Option<DiskSpace>,
    io: Option<DiskIO>,
}

#[derive(Debug)]
pub struct DiskIO {
    file: File,
    stats: DiskIOStats,
    total_reads: f64,
    total_writes: f64,
}

#[derive(Debug, Default, Clone)]
pub struct DiskIOStats {
    reads: f64,
    writes: f64,
}

#[derive(Debug, Default)]
pub struct DiskSpace {
    total: f64,
    available: f64,
}

impl DiskIO {
    fn stats(device_name: &str) -> Result<Self> {
        let mut file = File::open("/proc/diskstats").context("Failed to open /proc/diskstats")?;
        let mut buffer = String::new();
        file.read_to_string(&mut buffer)?;

        let mut stats: DiskIOStats = DiskIOStats::default();

        for line in buffer.lines() {
            let fields = line.split_whitespace().collect::<Vec<&str>>();
            if fields[2] == device_name {
                stats = DiskIOStats {
                    reads: fields[5].parse::<f64>()? * 512.0 / 1024.0 / 1024.0,
                    writes: fields[9].parse::<f64>()? * 512.0 / 1024.0 / 1024.0,
                };
                break;
            }
        }

        Ok(Self {
            file,
            stats,
            total_reads: 0.0,
            total_writes: 0.0,
        })
    }

    fn refresh(&mut self, device_name: &str) -> Result<()> {
        self.file.seek(std::io::SeekFrom::Start(0))?;
        let mut buffer = String::new();
        self.file.read_to_string(&mut buffer)?;

        let mut stats: DiskIOStats = self.stats.clone();

        for line in buffer.lines() {
            let fields = line.split_whitespace().collect::<Vec<&str>>();

            if fields[2] == device_name {
                stats = DiskIOStats {
                    reads: fields[5].parse::<f64>()? * 512.0 / 1024.0 / 1024.0,
                    writes: fields[9].parse::<f64>()? * 512.0 / 1024.0 / 1024.0,
                };
                break;
            }
        }

        let total_reads = stats.reads - self.stats.reads;
        let total_writes = stats.writes - self.stats.writes;

        self.stats = stats;
        self.total_reads = total_reads;
        self.total_writes = total_writes;

        Ok(())
    }
}

impl DiskSpace {
    fn stats() -> Result<Option<Self>> {
        let path = CString::new("/").unwrap();

        let mut statvfs: libc::statvfs = unsafe { std::mem::zeroed() };

        if unsafe { libc::statvfs(path.as_ptr(), &mut statvfs) } == 0 {
            let total =
                statvfs.f_blocks as f64 * statvfs.f_frsize as f64 / (1024.0 * 1024.0 * 1024.0);
            let available =
                statvfs.f_bavail as f64 * statvfs.f_frsize as f64 / (1024.0 * 1024.0 * 1024.0);

            return Ok(Some(Self { total, available }));
        }

        Ok(None)
    }
}

impl Disk {
    pub fn new() -> Self {
        match Disk::root_device_name() {
            Ok(Some(device_name)) => {
                let space = match DiskSpace::stats() {
                    Ok(space) => space,
                    Err(e) => {
                        error!("{}", e);
                        None
                    }
                };

                let io = DiskIO::stats(&device_name).map_or_else(
                    |e| {
                        error!("{}", e);
                        None
                    },
                    Some,
                );

                Self {
                    device_name: Some(device_name),
                    space,
                    io,
                }
            }
            Ok(None) => {
                error!("Can not find the root device name");
                Self {
                    device_name: None,
                    space: None,
                    io: None,
                }
            }
            Err(e) => {
                error!("{}", e);

                Self {
                    device_name: None,
                    space: None,
                    io: None,
                }
            }
        }
    }

    pub fn root_device_name() -> Result<Option<String>> {
        let buffer =
            fs::read_to_string("/proc/mounts").context("Failed to read from /proc/mounts")?;
        for line in buffer.lines() {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts[1] == "/" {
                return Ok(Some(parts[0].to_string()));
            }
        }
        Ok(None)
    }

    pub fn refresh(&mut self) {
        if let Ok(stats) = DiskSpace::stats() {
            self.space = stats;
        }

        if let Some(device_name) = &self.device_name {
            if let Some(io) = &mut self.io {
                io.refresh(device_name).ok();
            }
        }
    }

    pub fn render(&self, frame: &mut Frame, block: Rect) {
        let mut rows: Vec<String> = Vec::new();
        let mut space_rows: Vec<String> = Vec::new();
        let mut io_rows: Vec<String> = Vec::new();

        if let Some(space) = &self.space {
            space_rows = vec![
                format!("{:.1}GB", space.total),
                format!("{:.1}GB", space.available),
                format!("{:.1}GB", space.total - space.available),
            ];
        }

        if let Some(io) = &self.io {
            io_rows = vec![
                format!("{:.1}MB/s", io.total_reads),
                format!("{:.1}MB/s", io.total_writes),
            ];
        }

        rows.append(&mut space_rows);
        rows.append(&mut io_rows);

        let rows: [Row; 1] = [Row::new(rows)];

        let widths = [
            Constraint::Length(10),
            Constraint::Length(10),
            Constraint::Length(10),
            Constraint::Length(10),
            Constraint::Length(10),
        ];

        let disk = Table::new(rows, widths)
            .header(
                Row::new(vec!["Total", "Available", "Used", "Reads", "Writes"])
                    .style(Style::new().bold()),
            )
            .block(
                Block::default()
                    .title("Disk")
                    .title_style(Style::new().bold())
                    .padding(Padding::horizontal(1))
                    .borders(Borders::ALL),
            );

        frame.render_widget(disk, block);
    }
}