i3status_rs/blocks/privacy/
v4l.rs1use 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}