Skip to main content

usbwatch_rs/watcher/
linux.rs

1#[cfg(target_os = "linux")]
2use crate::device_info::{DeviceEventType, DeviceHandle, UsbDeviceInfo};
3#[cfg(target_os = "linux")]
4use std::collections::HashMap;
5#[cfg(target_os = "linux")]
6use std::path::Path;
7#[cfg(target_os = "linux")]
8use tokio::fs;
9#[cfg(target_os = "linux")]
10use tokio::sync::mpsc;
11
12#[cfg(target_os = "linux")]
13/// Linux-specific USB device watcher implementation.
14///
15/// This watcher uses the Linux sysfs filesystem (`/sys/bus/usb/devices`) to monitor
16/// USB device connections and disconnections. It polls the filesystem periodically
17/// to detect changes and sends events through the provided channel.
18///
19/// Device handles are provided for each detected device, including sysfs path.
20/// Future versions may detect device nodes (e.g., `/dev/ttyUSB0`).
21pub struct LinuxUsbWatcher {
22    tx: mpsc::Sender<UsbDeviceInfo>,
23}
24
25#[cfg(target_os = "linux")]
26impl LinuxUsbWatcher {
27    /// Creates a new Linux USB watcher.
28    ///
29    /// # Arguments
30    ///
31    /// * `tx` - Channel sender for broadcasting USB device events
32    ///
33    /// # Returns
34    ///
35    /// A new `LinuxUsbWatcher` instance
36    pub fn new(tx: mpsc::Sender<UsbDeviceInfo>) -> Self {
37        Self { tx }
38    }
39
40    /// Starts monitoring USB devices on Linux.
41    ///
42    /// This method continuously polls the `/sys/bus/usb/devices` directory
43    /// to detect USB device connection and disconnection events. It maintains
44    /// a map of known devices and compares against current devices to identify
45    /// changes.
46    ///
47    /// # Returns
48    ///
49    /// Returns `Ok(())` if monitoring stops gracefully, or `Err(String)` if
50    /// an error occurs during monitoring.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if:
55    /// - The USB devices directory is not accessible
56    /// - File system operations fail
57    /// - Device information parsing fails
58    pub async fn start_monitoring(&self) -> Result<(), String> {
59        println!("Starting USB device monitoring on Linux...");
60
61        // Simple polling approach - check /sys/bus/usb/devices periodically
62        let mut known_devices: HashMap<String, UsbDeviceInfo> = HashMap::new();
63        let mut poll_interval = tokio::time::Duration::from_secs(1); // Start with 1s, adapt based on activity
64
65        loop {
66            let scan_start = std::time::Instant::now();
67
68            match self.scan_usb_devices().await {
69                Ok(current_devices) => {
70                    let current_map: HashMap<String, UsbDeviceInfo> = current_devices
71                        .into_iter()
72                        .map(|d| {
73                            let key = format!(
74                                "{}:{}:{}",
75                                d.vendor_id,
76                                d.product_id,
77                                d.serial_number.as_deref().unwrap_or("unknown")
78                            );
79                            (key, d)
80                        })
81                        .collect();
82
83                    let mut events_sent = 0;
84
85                    // Check for new devices (connected)
86                    for (key, device) in &current_map {
87                        if !known_devices.contains_key(key) {
88                            let mut device_clone = device.clone();
89                            device_clone.event_type = DeviceEventType::Connected;
90                            if let Err(e) = self.tx.send(device_clone).await {
91                                eprintln!("Failed to send device event: {e}");
92                            } else {
93                                events_sent += 1;
94                            }
95                        }
96                    }
97
98                    // Check for removed devices (disconnected)
99                    for (key, device) in &known_devices {
100                        if !current_map.contains_key(key) {
101                            let mut device_clone = device.clone();
102                            device_clone.event_type = DeviceEventType::Disconnected;
103                            if let Err(e) = self.tx.send(device_clone).await {
104                                eprintln!("Failed to send device event: {e}");
105                            } else {
106                                events_sent += 1;
107                            }
108                        }
109                    }
110
111                    known_devices = current_map;
112
113                    // Adaptive polling: reduce interval if there's activity, increase if idle
114                    if events_sent > 0 {
115                        poll_interval = tokio::time::Duration::from_millis(500);
116                    // Fast polling when active
117                    } else {
118                        poll_interval = std::cmp::min(
119                            poll_interval + tokio::time::Duration::from_millis(100),
120                            tokio::time::Duration::from_secs(5), // Max 5 seconds when idle
121                        );
122                    }
123                }
124                Err(e) => {
125                    eprintln!("Error scanning USB devices: {e}");
126                    // Use exponential backoff on errors
127                    poll_interval =
128                        std::cmp::min(poll_interval * 2, tokio::time::Duration::from_secs(10));
129                }
130            }
131
132            // Ensure we don't scan too frequently
133            let scan_duration = scan_start.elapsed();
134            if scan_duration < poll_interval {
135                tokio::time::sleep(poll_interval - scan_duration).await;
136            }
137        }
138    }
139
140    async fn scan_usb_devices(&self) -> Result<Vec<UsbDeviceInfo>, String> {
141        let mut devices = Vec::new();
142        let usb_devices_path = Path::new("/sys/bus/usb/devices");
143
144        if !usb_devices_path.exists() {
145            return Err(
146                "USB devices path not found. Make sure you're running on Linux with USB support."
147                    .to_string(),
148            );
149        }
150
151        let mut entries = fs::read_dir(usb_devices_path)
152            .await
153            .map_err(|e| e.to_string())?;
154
155        while let Some(entry) = entries.next_entry().await.map_err(|e| e.to_string())? {
156            let path = entry.path();
157
158            // Skip entries that don't look like USB devices (e.g., usb1, usb2, etc.)
159            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
160                // Look for actual USB devices and root hubs, but skip interfaces
161                // USB devices: patterns like "1-1", "1-2", "2-1", etc. (devices connected to ports)
162                // USB root hubs: "usb1", "usb2", etc.
163                // Skip interfaces: "1-0:1.0", "2-0:1.0", etc.
164                let is_device = (name.matches('-').count() == 1 && !name.contains(':'))
165                    || name.starts_with("usb");
166                let is_interface = name.contains(':');
167
168                if is_device && !is_interface {
169                    if let Ok(device_info) = self.parse_usb_device(&path).await {
170                        // Skip devices with all zero VID/PID (typically means no actual device info)
171                        if device_info.vendor_id != "0000" || device_info.product_id != "0000" {
172                            devices.push(device_info);
173                        }
174                    } else {
175                        println!("Failed to parse device: {name}");
176                    }
177                }
178            }
179        }
180
181        Ok(devices)
182    }
183
184    async fn parse_usb_device(&self, device_path: &Path) -> Result<UsbDeviceInfo, String> {
185        let vendor_id = self
186            .read_sys_file(device_path, "idVendor")
187            .await
188            .unwrap_or_else(|| "0000".to_string());
189        let product_id = self
190            .read_sys_file(device_path, "idProduct")
191            .await
192            .unwrap_or_else(|| "0000".to_string());
193
194        let product_name = self
195            .read_sys_file(device_path, "product")
196            .await
197            .unwrap_or_else(|| "Unknown Device".to_string());
198        let manufacturer = self
199            .read_sys_file(device_path, "manufacturer")
200            .await
201            .unwrap_or_default();
202        let serial_number = self.read_sys_file(device_path, "serial").await;
203
204        let device_name = if !manufacturer.is_empty() && !product_name.is_empty() {
205            format!("{manufacturer} {product_name}")
206        } else if !product_name.is_empty() {
207            product_name
208        } else {
209            "Unknown Device".to_string()
210        };
211
212        let device_handle = DeviceHandle::Linux {
213            sysfs_path: device_path.to_string_lossy().to_string(),
214            device_node: None, // Could be enhanced to detect device nodes
215        };
216
217        Ok(UsbDeviceInfo::with_handle(
218            device_name,
219            vendor_id,
220            product_id,
221            serial_number,
222            DeviceEventType::Connected, // Will be updated by caller
223            device_handle,
224        ))
225    }
226    async fn read_sys_file(&self, device_path: &Path, filename: &str) -> Option<String> {
227        let file_path = device_path.join(filename);
228        fs::read_to_string(file_path)
229            .await
230            .ok()
231            .map(|s| s.trim().to_string())
232            .filter(|s| !s.is_empty())
233    }
234}
235
236#[cfg(not(target_os = "linux"))]
237pub struct LinuxUsbWatcher;
238
239#[cfg(not(target_os = "linux"))]
240impl LinuxUsbWatcher {
241    pub fn new(_tx: tokio::sync::mpsc::Sender<crate::device_info::UsbDeviceInfo>) -> Self {
242        Self
243    }
244
245    pub async fn start_monitoring(&self) -> Result<(), String> {
246        Err("Linux USB monitoring not available on this platform".to_string())
247    }
248}