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 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}