i3status_rs/blocks/privacy/
v4l.rs

1use inotify::{EventStream, Inotify, WatchDescriptor, WatchMask, Watches};
2use tokio::fs::{File, read_dir};
3use tokio::time::{Interval, interval};
4
5use std::path::PathBuf;
6
7use super::*;
8
9#[derive(Deserialize, Debug, SmartDefault)]
10#[serde(rename_all = "lowercase", deny_unknown_fields, default)]
11pub struct Config {
12    exclude_device: Vec<PathBuf>,
13    #[default(vec!["pipewire".into(), "wireplumber".into()])]
14    exclude_consumer: Vec<String>,
15}
16
17pub(super) struct Monitor<'a> {
18    config: &'a Config,
19    devices: HashMap<PathBuf, WatchDescriptor>,
20    interval: Interval,
21    watches: Watches,
22    stream: EventStream<[u8; 1024]>,
23}
24
25impl<'a> Monitor<'a> {
26    pub(super) async fn new(config: &'a Config, duration: Duration) -> Result<Self> {
27        let notify = Inotify::init().error("Failed to start inotify")?;
28        let watches = notify.watches();
29
30        let stream = notify
31            .into_event_stream([0; 1024])
32            .error("Failed to create event stream")?;
33
34        let mut s = Self {
35            config,
36            devices: HashMap::new(),
37            interval: interval(duration),
38            watches,
39            stream,
40        };
41        s.update_devices().await?;
42
43        Ok(s)
44    }
45
46    async fn update_devices(&mut self) -> Result<bool> {
47        let mut changes = false;
48        let mut devices_to_remove: HashMap<PathBuf, WatchDescriptor> = self.devices.clone();
49        let mut sysfs_paths = read_dir("/dev").await.error("Unable to read /dev")?;
50        while let Some(entry) = sysfs_paths
51            .next_entry()
52            .await
53            .error("Unable to get next device in /dev")?
54        {
55            if let Some(file_name) = entry.file_name().to_str()
56                && !file_name.starts_with("video")
57            {
58                continue;
59            }
60
61            let sysfs_path = entry.path();
62
63            if self.config.exclude_device.contains(&sysfs_path) {
64                debug!("ignoring {:?}", sysfs_path);
65                continue;
66            }
67
68            if self.devices.contains_key(&sysfs_path) {
69                devices_to_remove.remove(&sysfs_path);
70            } else {
71                debug!("adding watch {:?}", sysfs_path);
72                self.devices.insert(
73                    sysfs_path.clone(),
74                    self.watches
75                        .add(&sysfs_path, WatchMask::OPEN | WatchMask::CLOSE)
76                        .error("Failed to watch data location")?,
77                );
78                changes = true;
79            }
80        }
81        for (sysfs_path, wd) in devices_to_remove {
82            debug!("removing watch {:?}", sysfs_path);
83            self.devices.remove(&sysfs_path);
84            self.watches
85                .remove(wd)
86                .error("Failed to unwatch data location")?;
87            changes = true;
88        }
89
90        Ok(changes)
91    }
92}
93
94#[async_trait]
95impl PrivacyMonitor for Monitor<'_> {
96    async fn get_info(&mut self) -> Result<PrivacyInfo> {
97        let mut mapping: PrivacyInfo = PrivacyInfo::new();
98
99        let mut proc_paths = read_dir("/proc").await.error("Unable to read /proc")?;
100        while let Some(proc_path) = proc_paths
101            .next_entry()
102            .await
103            .error("Unable to get next device in /proc")?
104        {
105            let proc_path = proc_path.path();
106            let fd_path = proc_path.join("fd");
107            let Ok(mut fd_paths) = read_dir(fd_path).await else {
108                continue;
109            };
110            while let Ok(Some(fd_path)) = fd_paths.next_entry().await {
111                let mut contents = String::new();
112                if let Ok(link_path) = fd_path.path().read_link()
113                    && self.devices.contains_key(&link_path)
114                    && let Ok(mut file) = File::open(proc_path.join("comm")).await
115                    && file.read_to_string(&mut contents).await.is_ok()
116                {
117                    let reader = contents.trim_end().to_string();
118                    if self.config.exclude_consumer.contains(&reader) {
119                        continue;
120                    }
121                    debug!("{} {:?}", reader, link_path);
122                    *mapping
123                        .entry(Type::Webcam)
124                        .or_default()
125                        .entry(link_path.to_string_lossy().to_string())
126                        .or_default()
127                        .entry(reader)
128                        .or_default() += 1;
129                    debug!("{:?}", mapping);
130                }
131            }
132        }
133        Ok(mapping)
134    }
135
136    async fn wait_for_change(&mut self) -> Result<()> {
137        loop {
138            select! {
139                _ = self.interval.tick() => {
140                    if self.update_devices().await? {
141                        break;
142                    }
143                },
144                _ = self.stream.next_debounced() => break
145            }
146        }
147        Ok(())
148    }
149}