greggd 1.0.5

Lightweight Linux, macOS, and Windows metrics daemon that exposes a read-only JSON API for the gregg client.
Documentation
//! Linux mountinfo parsing, filtering, and native capacity collection.

use std::collections::HashMap;
use std::path::Path;

use crate::collector::drives::{normalize, DriveCandidate};
use crate::collector::error::CollectError;
use crate::collector::linux::source::{ProcSource, RawStatvfs};

#[derive(Debug, Clone, PartialEq, Eq)]
struct MountRecord {
    device: String,
    root: String,
    mount_point: String,
    filesystem_type: String,
    source: String,
}

const EXCLUDED_FILESYSTEMS: &[&str] = &[
    "proc",
    "sysfs",
    "devpts",
    "cgroup",
    "cgroup2",
    "securityfs",
    "debugfs",
    "tracefs",
    "configfs",
    "pstore",
    "efivarfs",
    "mqueue",
    "hugetlbfs",
    "bpf",
    "fusectl",
    "tmpfs",
    "devtmpfs",
    "ramfs",
    "overlay",
    "nfs",
    "nfs4",
    "cifs",
    "smb3",
    "sshfs",
    "fuse.sshfs",
    "9p",
    "ceph",
    "glusterfs",
    "afs",
];

fn decode_mountinfo(value: &str) -> Option<String> {
    let mut result = String::with_capacity(value.len());
    let mut chars = value.chars();
    while let Some(ch) = chars.next() {
        if ch != '\\' {
            result.push(ch);
            continue;
        }
        let code: String = chars.by_ref().take(3).collect();
        let decoded = match code.as_str() {
            "040" => ' ',
            "011" => '\t',
            "012" => '\n',
            "134" => '\\',
            _ => return None,
        };
        result.push(decoded);
    }
    Some(result)
}

fn parse_mountinfo_line(line: &str) -> Option<MountRecord> {
    let (left, right) = line.split_once(" - ")?;
    let left: Vec<_> = left.split_whitespace().collect();
    let right: Vec<_> = right.split_whitespace().collect();
    if left.len() < 6 || right.len() < 2 {
        return None;
    }
    let device = left[2].to_string();
    let root = decode_mountinfo(left[3])?;
    let mount_point = decode_mountinfo(left[4])?;
    let filesystem_type = right[0].to_string();
    let source = decode_mountinfo(right[1])?;
    Some(MountRecord {
        device,
        root,
        mount_point,
        filesystem_type,
        source,
    })
}

fn eligible(record: &MountRecord) -> bool {
    !EXCLUDED_FILESYSTEMS.contains(&record.filesystem_type.as_str())
        && !record.mount_point.is_empty()
        && !record.mount_point.starts_with("/proc/")
}

fn preferred(left: &MountRecord, right: &MountRecord) -> bool {
    let left_rank = (left.mount_point != "/", left.root != "/");
    let right_rank = (right.mount_point != "/", right.root != "/");
    left_rank < right_rank
        || (left_rank == right_rank
            && (left.mount_point.len(), left.mount_point.as_str())
                < (right.mount_point.len(), right.mount_point.as_str()))
}

fn identity(record: &MountRecord) -> String {
    if record.device != "0:0" {
        return record.device.clone();
    }
    format!(
        "synthetic:{}:{}:{}:{}",
        record.filesystem_type, record.source, record.root, record.mount_point
    )
}

fn capacity(stats: RawStatvfs) -> Option<(u64, u64, u64)> {
    let unit = (stats.fragment_size != 0)
        .then_some(stats.fragment_size)
        .or_else(|| (stats.block_size != 0).then_some(stats.block_size))?;
    let total = stats.blocks.checked_mul(unit)?;
    let free = stats.free_blocks.checked_mul(unit)?;
    let available = stats.available_blocks.checked_mul(unit)?;
    (total > 0 && free <= total && available <= total).then_some((total, free, available))
}

pub(crate) fn collect(
    source: &ProcSource,
) -> Result<Vec<gregg_protocol::v2::DriveMetrics>, CollectError> {
    let raw = source.read_mountinfo()?;
    let mut selected: HashMap<String, MountRecord> = HashMap::new();
    for line in raw.lines() {
        let Some(record) = parse_mountinfo_line(line) else {
            continue;
        };
        if !eligible(&record) {
            continue;
        }
        let key = identity(&record);
        match selected.get(&key) {
            Some(current) if !preferred(&record, current) => {}
            _ => {
                selected.insert(key, record);
            }
        }
    }

    let mut candidates = Vec::new();
    for (key, record) in selected {
        let path = Path::new(&record.mount_point);
        let Ok(stats) = source.statvfs(path) else {
            continue;
        };
        let Some((total, free, available)) = capacity(stats) else {
            continue;
        };
        candidates.push(DriveCandidate {
            identity: key,
            name: record.mount_point,
            total_bytes: total,
            total_free_bytes: free,
            available_bytes: available,
        });
    }
    Ok(normalize(candidates))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::collector::linux::source::{MemorySource, RawStatvfs};
    use std::sync::Arc;

    const MOUNTS: &str = "42 1 8:1 / / rw - ext4 /dev/sda1 rw\n43 1 8:1 / /mnt/root rw - ext4 /dev/sda1 rw\n44 1 8:2 /home /home rw - xfs /dev/sda2 rw\n45 1 0:0 / /tmp/foo\\040bar rw - tmpfs tmpfs rw\n46 1 8:3 / /net rw - nfs server:/x rw\n";

    #[test]
    fn parses_filters_deduplicates_and_sorts() {
        let source = MemorySource::new()
            .with_file("/proc/self/mountinfo", MOUNTS)
            .with_logical_cores(2);
        let mut source = ProcSource::for_source(Arc::new(source));
        source.memory_source_mut().unwrap().add_statvfs(
            "/",
            RawStatvfs {
                blocks: 10,
                free_blocks: 3,
                available_blocks: 3,
                fragment_size: 100,
                block_size: 100,
            },
        );
        source.memory_source_mut().unwrap().add_statvfs(
            "/home",
            RawStatvfs {
                blocks: 20,
                free_blocks: 4,
                available_blocks: 4,
                fragment_size: 100,
                block_size: 100,
            },
        );
        let drives = collect(&source).unwrap();
        assert_eq!(
            drives
                .iter()
                .map(|drive| drive.name.as_str())
                .collect::<Vec<_>>(),
            ["/", "/home"]
        );
        assert_eq!(drives[0].used_bytes, 700);
    }

    #[test]
    fn malformed_lines_are_skipped_and_capacity_is_checked() {
        let source = MemorySource::new()
            .with_file(
                "/proc/self/mountinfo",
                "bad\n1 1 8:1 / / rw - ext4 /dev/sda rw\n",
            )
            .with_logical_cores(1);
        let mut source = ProcSource::for_source(Arc::new(source));
        source.memory_source_mut().unwrap().add_statvfs(
            "/",
            RawStatvfs {
                blocks: u64::MAX,
                free_blocks: 0,
                available_blocks: 0,
                fragment_size: 2,
                block_size: 1,
            },
        );
        assert!(collect(&source).unwrap().is_empty());
    }

    #[test]
    fn preserves_total_free_and_caller_available_separately() {
        let source = MemorySource::new()
            .with_file("/proc/self/mountinfo", MOUNTS)
            .with_logical_cores(1);
        let mut source = ProcSource::for_source(Arc::new(source));
        source.memory_source_mut().unwrap().add_statvfs(
            "/",
            RawStatvfs {
                blocks: 100,
                free_blocks: 40,
                available_blocks: 25,
                fragment_size: 1,
                block_size: 1,
            },
        );

        let drive = collect(&source).unwrap().into_iter().next().unwrap();
        assert_eq!(drive.used_bytes, 60);
        assert_eq!(drive.total_bytes, 100);
        assert_eq!(drive.available_bytes, Some(25));
    }
}