1use std::sync::atomic::AtomicBool;
2use std::sync::Arc;
3
4pub fn spawn_monitor(active: Arc<AtomicBool>) {
5 #[cfg(target_os = "macos")]
6 macos::spawn(active);
7 #[cfg(target_os = "windows")]
8 windows::spawn(active);
9 #[cfg(target_os = "linux")]
10 linux::spawn(active);
11 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
12 let _ = active;
13}
14
15#[cfg(target_os = "macos")]
16mod macos {
17 use std::io::{BufRead, BufReader};
18 use std::process::{Command, Stdio};
19 use std::sync::atomic::{AtomicBool, Ordering};
20 use std::sync::Arc;
21 use std::thread;
22
23 pub(super) const LOG_BIN: &str = "/usr/bin/log";
27
28 pub fn spawn(active: Arc<AtomicBool>) {
29 thread::spawn(move || {
30 let Ok(mut child) = Command::new(LOG_BIN)
31 .args([
32 "stream",
33 "--style",
34 "compact",
35 "--predicate",
36 "eventMessage contains \"Post event kCameraStream\"",
37 "--info",
38 ])
39 .stdout(Stdio::piped())
40 .stderr(Stdio::null())
41 .spawn()
42 else {
43 return;
44 };
45 let Some(stdout) = child.stdout.take() else {
46 return;
47 };
48 let reader = BufReader::new(stdout);
49 for line in reader.lines().map_while(Result::ok) {
50 if line.contains("kCameraStreamStart") {
51 active.store(true, Ordering::Relaxed);
52 } else if line.contains("kCameraStreamStop") {
53 active.store(false, Ordering::Relaxed);
54 }
55 }
56 });
57 }
58}
59
60#[cfg(any(target_os = "windows", target_os = "linux"))]
64const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(10);
65
66#[cfg(target_os = "windows")]
67mod windows {
68 use std::sync::atomic::{AtomicBool, Ordering};
69 use std::sync::Arc;
70 use std::thread;
71 use winreg::enums::HKEY_CURRENT_USER;
72 use winreg::RegKey;
73
74 const KEY_PATH: &str =
75 "Software\\Microsoft\\Windows\\CurrentVersion\\CapabilityAccessManager\\ConsentStore\\webcam";
76
77 pub fn spawn(active: Arc<AtomicBool>) {
78 thread::spawn(move || loop {
79 active.store(check(), Ordering::Relaxed);
80 thread::sleep(super::POLL_INTERVAL);
81 });
82 }
83
84 fn check() -> bool {
85 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
86 let Ok(root) = hkcu.open_subkey(KEY_PATH) else {
87 return false;
88 };
89 if any_active_app(&root) {
90 return true;
91 }
92 if let Ok(non_packaged) = root.open_subkey("NonPackaged") {
93 if any_active_app(&non_packaged) {
94 return true;
95 }
96 }
97 false
98 }
99
100 fn any_active_app(key: &RegKey) -> bool {
101 for name in key.enum_keys().filter_map(Result::ok) {
102 if name == "NonPackaged" {
103 continue;
104 }
105 let Ok(app_key) = key.open_subkey(&name) else {
106 continue;
107 };
108 if let Ok(stop) = app_key.get_value::<u64, _>("LastUsedTimeStop") {
109 if stop == 0 {
110 return true;
111 }
112 }
113 }
114 false
115 }
116}
117
118#[cfg(target_os = "linux")]
119mod linux {
120 use std::fs;
121 use std::sync::atomic::{AtomicBool, Ordering};
122 use std::sync::Arc;
123 use std::thread;
124
125 pub fn spawn(active: Arc<AtomicBool>) {
126 thread::spawn(move || loop {
127 active.store(check(), Ordering::Relaxed);
128 thread::sleep(super::POLL_INTERVAL);
129 });
130 }
131
132 fn check() -> bool {
133 let Ok(proc_dir) = fs::read_dir("/proc") else {
134 return false;
135 };
136 for entry in proc_dir.filter_map(Result::ok) {
137 let path = entry.path();
138 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
139 continue;
140 };
141 if !name.bytes().all(|b| b.is_ascii_digit()) {
142 continue;
143 }
144 let Ok(fds) = fs::read_dir(path.join("fd")) else {
145 continue;
146 };
147 for fd in fds.filter_map(Result::ok) {
148 let Ok(target) = fs::read_link(fd.path()) else {
149 continue;
150 };
151 if let Some(target_str) = target.to_str() {
152 if target_str.starts_with("/dev/video") {
153 return true;
154 }
155 }
156 }
157 }
158 false
159 }
160}
161
162#[cfg(all(test, target_os = "macos"))]
163mod macos_bin_tests {
164 use super::macos::LOG_BIN;
165
166 #[test]
167 fn log_bin_is_absolute_and_non_empty() {
168 assert!(!LOG_BIN.is_empty());
169 assert!(
170 LOG_BIN.starts_with('/'),
171 "expected absolute path, got {LOG_BIN}"
172 );
173 }
174}