Skip to main content

planarian_backend_linux/
sysfs.rs

1//! `InventoryBackend` over sysfs: which configured devices are present, which
2//! driver each is bound to, and bind/unbind through the driver interface.
3//!
4//! Devices come from the manifest (`services.planarian.devices.<name>.sysfs`),
5//! e.g. `snd-hda0 → /sys/bus/pci/devices/0000:00:1b.0`.
6
7use crate::admitted;
8use planarian_schema::{DeviceInfo, DriverRef, InventoryBackend, OpId, Outcome, Resources, UnitId};
9use std::collections::BTreeMap;
10use std::path::{Path, PathBuf};
11
12/// Device unit name → its sysfs device directory.
13pub type DeviceMap = BTreeMap<String, PathBuf>;
14
15#[derive(Clone, Debug)]
16pub struct Sysfs {
17    devices: DeviceMap,
18    txn_dir: PathBuf,
19    /// `/sys` (a test tree in tests).
20    root: PathBuf,
21}
22
23/// `/sys/bus/<bus>/devices/<id>` → (bus, id).
24pub fn bus_and_id(dev: &Path) -> Option<(String, String)> {
25    let canonical = std::fs::canonicalize(dev).unwrap_or_else(|_| dev.to_path_buf());
26    let id = canonical.file_name()?.to_string_lossy().into_owned();
27    let subsystem =
28        std::fs::read_link(canonical.join("subsystem")).ok().or_else(|| std::fs::read_link(dev.join("subsystem")).ok());
29    let bus = match subsystem {
30        Some(s) => s.file_name()?.to_string_lossy().into_owned(),
31        None => {
32            // /sys/bus/<bus>/devices/<id> form.
33            let parts: Vec<String> = dev.components().map(|c| c.as_os_str().to_string_lossy().into_owned()).collect();
34            let i = parts.iter().position(|p| p == "bus")?;
35            parts.get(i + 1)?.clone()
36        }
37    };
38    Some((bus, id))
39}
40
41impl Sysfs {
42    pub fn new(devices: DeviceMap, txn_dir: PathBuf) -> Sysfs {
43        Sysfs { devices, txn_dir, root: "/sys".into() }
44    }
45
46    #[cfg(test)]
47    fn at(root: &Path, devices: DeviceMap, txn_dir: PathBuf) -> Sysfs {
48        Sysfs { devices, txn_dir, root: root.into() }
49    }
50
51    fn path(&self, device: &UnitId) -> Option<&PathBuf> {
52        self.devices.get(&device.name)
53    }
54
55    fn driver_of(dev: &Path) -> Option<DriverRef> {
56        let link = std::fs::read_link(dev.join("driver")).ok()?;
57        Some(DriverRef { name: link.file_name()?.to_string_lossy().into_owned(), module: None })
58    }
59
60    fn write(path: &Path, value: &str) -> Outcome {
61        match std::fs::write(path, value) {
62            Ok(()) => Outcome::Succeeded,
63            Err(e) => Outcome::Failed(format!("{}: {e}", path.display())),
64        }
65    }
66}
67
68fn meminfo() -> (u64, u64) {
69    let text = std::fs::read_to_string("/proc/meminfo").unwrap_or_default();
70    let get = |k: &str| {
71        text.lines()
72            .find(|l| l.starts_with(k))
73            .and_then(|l| l.split_whitespace().nth(1))
74            .and_then(|v| v.parse::<u64>().ok())
75            .unwrap_or(0)
76            / 1024
77    };
78    (get("MemTotal:"), get("MemAvailable:"))
79}
80
81impl InventoryBackend for Sysfs {
82    fn devices(&self) -> Result<Vec<DeviceInfo>, Outcome> {
83        Ok(self
84            .devices
85            .iter()
86            .filter(|(_, p)| p.exists())
87            .map(|(name, p)| DeviceInfo { id: UnitId::device(name), driver: Self::driver_of(p) })
88            .collect())
89    }
90
91    fn resources(&self) -> Result<Resources, Outcome> {
92        let (total, available) = meminfo();
93        Ok(Resources { mem_total_mb: total, mem_available_mb: available, fs_free_bytes: BTreeMap::new() })
94    }
95
96    fn bound_driver(&self, device: &UnitId) -> Result<Option<DriverRef>, Outcome> {
97        let p = self.path(device).ok_or_else(|| Outcome::Failed(format!("{device} has no sysfs path")))?;
98        Ok(Self::driver_of(p))
99    }
100
101    fn bind(&self, device: &UnitId, driver: &DriverRef, op: &OpId) -> Outcome {
102        if !admitted(&self.txn_dir, op) {
103            return Outcome::fenced();
104        }
105        let Some(p) = self.path(device) else { return Outcome::Failed(format!("{device} has no sysfs path")) };
106        let Some((bus, id)) = bus_and_id(p) else {
107            return Outcome::Failed(format!("cannot tell the bus of {}", p.display()));
108        };
109        match Self::driver_of(p) {
110            Some(d) if d.name == driver.name => return Outcome::Succeeded,
111            // The kernel refuses to bind a device another driver holds.
112            Some(_) => {
113                if let Outcome::Failed(e) = Self::write(&p.join("driver/unbind"), &id) {
114                    return Outcome::Failed(e);
115                }
116            }
117            None => {}
118        }
119        if let Some(m) = &driver.module {
120            let o = crate::cmd::run(&["modprobe", "-d", &m.0, &driver.name], std::time::Duration::from_secs(30));
121            if !o.ok() {
122                return Outcome::Failed(o.tail());
123            }
124        }
125        // A driver binds through `bind` only to devices in its own ID table;
126        // a generic one (vfio-pci, pci-stub) matches none. `driver_override`
127        // names the driver the device may bind to; it is cleared after, so
128        // the device is left with its binding and no lasting override.
129        let ov = p.join("driver_override");
130        let overriding = ov.exists();
131        if overriding && let Outcome::Failed(e) = Self::write(&ov, &driver.name) {
132            return Outcome::Failed(e);
133        }
134        let o = Self::write(&self.root.join(format!("bus/{bus}/drivers/{}/bind", driver.name)), &id);
135        if overriding {
136            let _ = Self::write(&ov, "\n");
137        }
138        o
139    }
140
141    fn unbind(&self, device: &UnitId, op: &OpId) -> Outcome {
142        if !admitted(&self.txn_dir, op) {
143            return Outcome::fenced();
144        }
145        let Some(p) = self.path(device) else { return Outcome::Failed(format!("{device} has no sysfs path")) };
146        if Self::driver_of(p).is_none() {
147            return Outcome::Succeeded;
148        }
149        let Some((_, id)) = bus_and_id(p) else { return Outcome::Failed("cannot tell the device id".into()) };
150        Self::write(&p.join("driver/unbind"), &id)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn bus_from_path() {
160        assert_eq!(
161            bus_and_id(Path::new("/sys/bus/pci/devices/0000:00:1b.0")),
162            Some(("pci".into(), "0000:00:1b.0".into()))
163        );
164    }
165
166    #[test]
167    fn bind_overrides_the_driver_match_and_clears_it() {
168        let d = tempfile::tempdir().unwrap();
169        let dev = d.path().join("bus/pci/devices/0000:00:1b.0");
170        std::fs::create_dir_all(&dev).unwrap();
171        std::fs::write(dev.join("driver_override"), "(null)\n").unwrap();
172        let drv = d.path().join("bus/pci/drivers/vfio-pci");
173        std::fs::create_dir_all(&drv).unwrap();
174        let held = d.path().join("bus/pci/drivers/snd_hda_intel");
175        std::fs::create_dir_all(&held).unwrap();
176        std::os::unix::fs::symlink(&held, dev.join("driver")).unwrap();
177        let txn = d.path().join("txn");
178        std::fs::create_dir_all(txn.join("7")).unwrap();
179        std::fs::write(txn.join("7/lease"), "1").unwrap();
180        let s = Sysfs::at(d.path(), [("snd-hda0".to_string(), dev.clone())].into(), txn);
181        let op = OpId { txn: planarian_schema::TxnId(7), epoch: 1, seq: 1 };
182        let o = s.bind(&UnitId::device("snd-hda0"), &DriverRef { name: "vfio-pci".into(), module: None }, &op);
183        assert_eq!(o, Outcome::Succeeded);
184        assert_eq!(std::fs::read_to_string(held.join("unbind")).unwrap(), "0000:00:1b.0", "the holder lets go first");
185        assert_eq!(std::fs::read_to_string(drv.join("bind")).unwrap(), "0000:00:1b.0");
186        assert_eq!(std::fs::read_to_string(dev.join("driver_override")).unwrap(), "\n", "cleared after binding");
187    }
188
189    #[test]
190    fn driver_and_presence_from_a_fake_tree() {
191        let d = tempfile::tempdir().unwrap();
192        let dev = d.path().join("bus/pci/devices/0000:00:1b.0");
193        std::fs::create_dir_all(&dev).unwrap();
194        std::fs::create_dir_all(d.path().join("bus/pci/drivers/snd_hda_intel")).unwrap();
195        std::os::unix::fs::symlink(d.path().join("bus/pci/drivers/snd_hda_intel"), dev.join("driver")).unwrap();
196        let s = Sysfs::new(
197            [("snd-hda0".to_string(), dev.clone()), ("gone".to_string(), d.path().join("nope"))].into(),
198            d.path().into(),
199        );
200        let devs = s.devices().unwrap();
201        assert_eq!(devs.len(), 1);
202        assert_eq!(devs[0].driver.as_ref().unwrap().name, "snd_hda_intel");
203    }
204}