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                if !file_name.starts_with("video") {
57                    continue;
58                }
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 Ok(link_path) = fd_path.path().read_link() else {
112                    continue;
113                };
114                if self.devices.contains_key(&link_path) {
115                    let Ok(mut file) = File::open(proc_path.join("comm")).await else {
116                        continue;
117                    };
118                    let mut contents = String::new();
119                    if file.read_to_string(&mut contents).await.is_ok() {
120                        let reader = contents.trim_end().to_string();
121                        if self.config.exclude_consumer.contains(&reader) {
122                            continue;
123                        }
124                        debug!("{} {:?}", reader, link_path);
125                        *mapping
126                            .entry(Type::Webcam)
127                            .or_default()
128                            .entry(link_path.to_string_lossy().to_string())
129                            .or_default()
130                            .entry(reader)
131                            .or_default() += 1;
132                        debug!("{:?}", mapping);
133                    }
134                }
135            }
136        }
137        Ok(mapping)
138    }
139
140    async fn wait_for_change(&mut self) -> Result<()> {
141        loop {
142            select! {
143                _ = self.interval.tick() => {
144                    if self.update_devices().await? {
145                        break;
146                    }
147                },
148                _ = self.stream.next_debounced() => break
149            }
150        }
151        Ok(())
152    }
153}