sysfuss 0.3.0

sysfs wrapper for convenience
Documentation
use std::path::{Path, PathBuf};
use std::convert::AsRef;
use std::io::Result as IoResult;

const HWMON_DIR_PATH: &'static str = "sys/class/hwmon/";

use crate::SysEntityAttributesExt;

/// Attribute representation.
/// Attribute files are usually in the format `<type><number>_<item>`.
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "derive", derive(Debug))]
pub struct HwMonAttribute {
   inner: HwMonAttributeInner,
}

#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "derive", derive(Debug))]
enum HwMonAttributeInner {
    Name,
    Standard {
        ty: HwMonAttributeType,
        number: u64,
        item: HwMonAttributeItem,
    },
    Uevent,
    Custom(&'static str),
}

impl std::str::FromStr for HwMonAttributeInner {
    type Err = String;
    
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "name" => Ok(Self::Name),
            "uevent" => Ok(Self::Uevent),
            s => {
                let start = HwMonAttributeType::from_attr_start(s)?;
                let end = HwMonAttributeItem::from_attr_end(s)?;
                let trimmed = s.trim_start_matches(start.to_attr_str()).trim_end_matches(end.to_attr_str());
                if trimmed.ends_with("_") {
                    let index = trimmed.trim_end_matches('_').parse().map_err(|e| format!("Invalid number in attribute name {}: {}", s, e))?;
                    Ok(Self::Standard { ty: start, number: index, item: end })
                } else {
                    Err(format!("Missing underscore in attribute name {}", s))
                }
            }
        }
    }
}

/// Attribute type in the format `<type><number>_<item>`
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "derive", derive(Debug))]
pub enum HwMonAttributeType {
    /// Voltage
    In,
    /// Temperature
    Temp,
    /// Fan
    Fan,
    /// Current
    Curr,
}

impl HwMonAttributeType {
    const fn to_attr_str(&self) -> &str {
        match self {
            Self::In => "in",
            Self::Temp => "out",
            Self::Fan => "fan",
            Self::Curr => "curr",
        }
    }
    
    fn from_attr_start(s: &str) -> Result<Self, String> {
        if s.starts_with(Self::In.to_attr_str()) {
            Ok(Self::In)
        } else if s.starts_with(Self::Temp.to_attr_str()) {
            Ok(Self::Temp)
        } else if s.starts_with(Self::Fan.to_attr_str()) {
            Ok(Self::Fan)
        } else if s.starts_with(Self::Curr.to_attr_str()) {
            Ok(Self::Curr)
        } else {
            Err(format!("Unrecognised hwmon attribute type in name {}", s))
        }
    }
}

/// Attribute item in the format `<type><number>_<item>`
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "derive", derive(Debug))]
pub enum HwMonAttributeItem {
    /// Input
    Input,
    /// Minimum
    Min,
    /// Maximum
    Max,
    /// Readable name for another attribute
    Label,
}

impl HwMonAttributeItem {
    const fn to_attr_str(&self) -> &str {
        match self {
            Self::Input => "input",
            Self::Min => "min",
            Self::Max => "max",
            Self::Label => "label"
        }
    }
    
    fn from_attr_end(s: &str) -> Result<Self, String> {
        if s.ends_with(Self::Input.to_attr_str()) {
            Ok(Self::Input)
        } else if s.ends_with(Self::Min.to_attr_str()) {
            Ok(Self::Min)
        } else if s.ends_with(Self::Max.to_attr_str()) {
            Ok(Self::Max)
        } else {
            Err(format!("Unrecognised hwmon attribute type in name {}", s))
        }
    }
}

impl HwMonAttribute {
    /// `name` attribute
    pub const fn name() -> Self {
        Self {
            inner: HwMonAttributeInner::Name
        }
    }
    
    /// Custom attribute in standard format
    pub const fn new(ty: HwMonAttributeType, number: u64, item: HwMonAttributeItem) -> Self {
        Self {
            inner: HwMonAttributeInner::Standard { ty, number, item }
        }
    }
    
    /// `uevent` attribute
    pub const fn uevent() -> Self {
        Self {
            inner: HwMonAttributeInner::Uevent
        }
    }

    /// Custom entity attribute
    pub const fn custom(attr: &'static str) -> Self {
        Self {
            inner: HwMonAttributeInner::Custom(attr)
        }
    }
    
    fn to_attr_str(&self) -> String {
        match self.inner {
            HwMonAttributeInner::Name => "name".to_string(),
            HwMonAttributeInner::Standard { ty, number, item } => format!("{}{}_{}", ty.to_attr_str(), number, item.to_attr_str()),
            HwMonAttributeInner::Uevent => "uevent".to_string(),
            HwMonAttributeInner::Custom(s) => s.to_owned(),
        }
    }
}

impl crate::SysAttribute for HwMonAttribute {
    fn filename(&self) -> PathBuf {
        PathBuf::from(self.to_attr_str())
    }
}

/// hwmon<number>/ directory
#[cfg_attr(feature = "derive", derive(Debug, Clone))]
pub struct HwMonPath {
    path: PathBuf
}

impl HwMonPath {
    pub(crate) fn all(root: impl AsRef<Path>) -> IoResult<impl Iterator<Item=IoResult<Self>>> {
        let hwmon_dir_path = root.as_ref().join(HWMON_DIR_PATH);
        hwmon_dir_path.read_dir()
            .map(
                |iter| iter.filter_map(
                    |entry| entry.map(
                        |entry| if crate::os_str_util::starts_with(&entry.file_name(), "hwmon".as_ref()) {
                            Some(Self {
                                path: entry.path(),
                            })
                        } else {
                            None
                        }).transpose()))
    }
    
    /// Get a hwmon entry by index. 
    /// This does not check if it exists.
    pub(crate) fn entry(root: &crate::SysPath, i: u64) -> Self {
        Self {
            path: root.as_ref().join(HWMON_DIR_PATH).join(format!("hwmon{}", i)),
        }
    }
    
    /// Get a hwmon entry by name, if it exists
    pub(crate) fn name(root: &crate::SysPath, name: &str) -> IoResult<Option<Self>> {
        for entry in Self::all(root)? {
            let entry = entry?;
            let value: String = entry.attribute::<String, _>(HwMonAttribute::name()).map_err(|e| match e {
                crate::EitherErr2::First(e) => e,
                crate::EitherErr2::Second(_e) => panic!("Infallible"),
            })?;
            if value == name {
                return Ok(Some(entry))
            }
        }
        Ok(None)
    }
    
    /// Get a hwmon attribute (file contents) by attribute filename.
    /// It is recommended to use HwMon.attribute unless the attribute has a non-standard name
    pub fn attribute_str(&self, name: &str) -> IoResult<String> {
        std::fs::read_to_string(self.path.join(name))
    }
    
    /// Get the path to a hwmon attribute.
    /// Use `HwMonPath.as_ref().join(attribute_str)` for non-standard attributes
    pub fn path_to(&self, attr: HwMonAttribute) -> PathBuf {
        self.path.join(&attr.to_attr_str())
    }
}

impl AsRef<Path> for HwMonPath {
    fn as_ref(&self) -> &Path {
        self.path.as_path()
    }
}

impl crate::SysEntity for HwMonPath {
    fn to_entity_path(self) -> crate::EntityPath {
        crate::EntityPath::HwMon(self)
    }
    
    fn name(&self) -> IoResult<String> {
        self.attribute(HwMonAttribute::name()).map_err(|e| e.map_infallible_second())
    }
}

impl crate::SysEntityAttributes<HwMonAttribute> for HwMonPath {
    fn capabilities(&self) -> Vec<HwMonAttribute> {
        if let Ok(dir_iter) = self.path.read_dir() {
            dir_iter.filter_map(
                |entry| entry.ok().filter(|entry| entry.path().is_file()).and_then(|entry| entry.file_name().into_string().ok()).and_then(|s| s.parse::<HwMonAttributeInner>().ok())
            ).map(|inner| HwMonAttribute {inner}).collect()
        } else {
            Vec::with_capacity(0)
        }
    }
}


#[cfg(test)]
mod tests {
    use super::*;
    use crate::SysEntityAttributes;

    #[test]
    fn hwmon_all() -> std::io::Result<()> {
        let sys = crate::SysPath::default();
        let all_hwmon: Vec<_> = HwMonPath::all(sys)?.collect();
        assert!(!all_hwmon.is_empty());
        for hwmon in all_hwmon.into_iter() {
            let hwmon = hwmon?;
            assert!(hwmon.attribute::<String, _>(HwMonAttribute::name()).map_err(|e| e.map_infallible_second())? != "");
            assert!(!hwmon.attribute::<String, _>(HwMonAttribute::name()).map_err(|e| e.map_infallible_second())?.ends_with("\n"));
            assert!(!hwmon.capabilities().is_empty());
            assert!(hwmon.capabilities().contains(&HwMonAttribute::name()))
        }
        Ok(())
    }

    #[test]
    fn hwmon_capabilities() -> std::io::Result<()> {
        let sys = crate::SysPath::default();
        if !sys.hwmon_by_name("amdgpu").is_ok() {
            // skip if system has no AMD GPU
            eprintln!("hwmon test skipped since amdgpu does not exist (maybe running on a laptop PC?)");
            return Ok(())
        }
        let hwmon = sys.hwmon(crate::capability::attributes([
            HwMonAttribute::name(),
            HwMonAttribute::new(HwMonAttributeType::Fan, 1, HwMonAttributeItem::Input),
            HwMonAttribute::new(HwMonAttributeType::Fan, 1, HwMonAttributeItem::Min),
            HwMonAttribute::new(HwMonAttributeType::Fan, 1, HwMonAttributeItem::Max)
        ].into_iter()))?.next().expect("Missing capable amdgpu");
        assert_eq!(hwmon.attribute::<String, _>(HwMonAttribute::name()).expect("name capable but also incapable"), "amdgpu");
        Ok(())
    }
}