tenseleyflow/hyprkvm / ca44876

Browse files

fix: prevent deviceless machines from initiating transfers

Add has_devices flag to EvdevGrabber that tracks whether the machine
has physical input devices. Machines without devices (e.g. headless
servers or remote desktops) can still receive control transfers but
cannot initiate them, since they have no input to capture and forward.

This fixes a deadlock where a deviceless machine would try to initiate
a transfer, enter RemoteActive state with no evdev thread to capture
input, while the peer would wait forever in ReceivedControl for input
that never arrives.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
ca44876ddfd05184dff035359d920e0ca88ee10d
Parents
1e3d833
Tree
c4fb389

2 changed files

StatusFile+-
M hyprkvm-daemon/src/input/evdev_grab.rs 20 2
M hyprkvm-daemon/src/main.rs 47 18
hyprkvm-daemon/src/input/evdev_grab.rsmodified
@@ -30,6 +30,9 @@ pub struct EvdevGrabber {
30
     recovery_active: Arc<AtomicU64>,
30
     recovery_active: Arc<AtomicU64>,
31
     /// The direction to watch for in recovery mode (encoded as u8: 1=Up, 2=Down, 3=Left, 4=Right, 0=none)
31
     /// The direction to watch for in recovery mode (encoded as u8: 1=Up, 2=Down, 3=Left, 4=Right, 0=none)
32
     recovery_direction: Arc<AtomicU64>,
32
     recovery_direction: Arc<AtomicU64>,
33
+    /// Flag indicating whether this grabber has input devices to grab
34
+    /// If false, this machine cannot initiate transfers (but can still receive them)
35
+    has_devices: Arc<AtomicBool>,
33
     event_rx: mpsc::Receiver<GrabEvent>,
36
     event_rx: mpsc::Receiver<GrabEvent>,
34
     _thread: thread::JoinHandle<()>,
37
     _thread: thread::JoinHandle<()>,
35
 }
38
 }
@@ -43,13 +46,15 @@ impl EvdevGrabber {
43
         let recovery_clone = recovery_active.clone();
46
         let recovery_clone = recovery_active.clone();
44
         let recovery_direction = Arc::new(AtomicU64::new(0));
47
         let recovery_direction = Arc::new(AtomicU64::new(0));
45
         let recovery_dir_clone = recovery_direction.clone();
48
         let recovery_dir_clone = recovery_direction.clone();
49
+        let has_devices = Arc::new(AtomicBool::new(false));
50
+        let has_devices_clone = has_devices.clone();
46
 
51
 
47
         let (event_tx, event_rx) = mpsc::channel();
52
         let (event_tx, event_rx) = mpsc::channel();
48
 
53
 
49
         let thread = thread::Builder::new()
54
         let thread = thread::Builder::new()
50
             .name("evdev-grabber".to_string())
55
             .name("evdev-grabber".to_string())
51
             .spawn(move || {
56
             .spawn(move || {
52
-                if let Err(e) = run_evdev_grabber(active_clone, recovery_clone, recovery_dir_clone, event_tx) {
57
+                if let Err(e) = run_evdev_grabber(active_clone, recovery_clone, recovery_dir_clone, has_devices_clone, event_tx) {
53
                     tracing::error!("Evdev grabber error: {}", e);
58
                     tracing::error!("Evdev grabber error: {}", e);
54
                 }
59
                 }
55
             })
60
             })
@@ -59,6 +64,7 @@ impl EvdevGrabber {
59
             active,
64
             active,
60
             recovery_active,
65
             recovery_active,
61
             recovery_direction,
66
             recovery_direction,
67
+            has_devices,
62
             event_rx,
68
             event_rx,
63
             _thread: thread,
69
             _thread: thread,
64
         })
70
         })
@@ -101,6 +107,13 @@ impl EvdevGrabber {
101
         self.active.load(Ordering::SeqCst)
107
         self.active.load(Ordering::SeqCst)
102
     }
108
     }
103
 
109
 
110
+    /// Check if this grabber has input devices available
111
+    /// If false, this machine cannot initiate transfers (no devices to grab)
112
+    /// but can still receive control from other machines
113
+    pub fn has_devices(&self) -> bool {
114
+        self.has_devices.load(Ordering::SeqCst)
115
+    }
116
+
104
     /// Try to receive a grab event (non-blocking)
117
     /// Try to receive a grab event (non-blocking)
105
     pub fn try_recv(&self) -> Option<GrabEvent> {
118
     pub fn try_recv(&self) -> Option<GrabEvent> {
106
         self.event_rx.try_recv().ok()
119
         self.event_rx.try_recv().ok()
@@ -184,15 +197,20 @@ fn run_evdev_grabber(
184
     active: Arc<AtomicBool>,
197
     active: Arc<AtomicBool>,
185
     recovery_active: Arc<AtomicU64>,
198
     recovery_active: Arc<AtomicU64>,
186
     recovery_direction: Arc<AtomicU64>,
199
     recovery_direction: Arc<AtomicU64>,
200
+    has_devices: Arc<AtomicBool>,
187
     event_tx: mpsc::Sender<GrabEvent>,
201
     event_tx: mpsc::Sender<GrabEvent>,
188
 ) -> Result<(), EvdevGrabError> {
202
 ) -> Result<(), EvdevGrabError> {
189
     let device_paths = find_input_devices();
203
     let device_paths = find_input_devices();
190
 
204
 
191
     if device_paths.is_empty() {
205
     if device_paths.is_empty() {
206
+        tracing::warn!("No input devices found - this machine cannot initiate transfers");
207
+        // Leave has_devices as false (default)
192
         return Err(EvdevGrabError::NoDevices);
208
         return Err(EvdevGrabError::NoDevices);
193
     }
209
     }
194
 
210
 
195
-    tracing::info!("Found {} input device paths", device_paths.len());
211
+    // Mark that we have devices - transfers can be initiated from this machine
212
+    has_devices.store(true, Ordering::SeqCst);
213
+    tracing::info!("Found {} input device paths (can initiate transfers)", device_paths.len());
196
     for path in &device_paths {
214
     for path in &device_paths {
197
         tracing::debug!("  {}", path.display());
215
         tracing::debug!("  {}", path.display());
198
     }
216
     }
hyprkvm-daemon/src/main.rsmodified
@@ -967,6 +967,10 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
967
 
967
 
968
                                 if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
968
                                 if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
969
                                     info!("RECOVERY HOTKEY: Barrier enabled, blocking transfer");
969
                                     info!("RECOVERY HOTKEY: Barrier enabled, blocking transfer");
970
+                                } else if !input_grabber.has_devices() {
971
+                                    // Should never happen (no devices = no recovery hotkey events)
972
+                                    // but guard against it anyway
973
+                                    tracing::debug!("RECOVERY HOTKEY: No input devices, cannot initiate");
970
                                 } else {
974
                                 } else {
971
                                     info!("RECOVERY HOTKEY: At edge with peer, initiating transfer to {:?}", direction);
975
                                     info!("RECOVERY HOTKEY: At edge with peer, initiating transfer to {:?}", direction);
972
                                     if let Err(e) = transfer_manager.initiate_transfer(
976
                                     if let Err(e) = transfer_manager.initiate_transfer(
@@ -1076,6 +1080,14 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1076
                                 cursor_pos.0,
1080
                                 cursor_pos.0,
1077
                                 cursor_pos.1
1081
                                 cursor_pos.1
1078
                             );
1082
                             );
1083
+                        } else if !input_grabber.has_devices() {
1084
+                            // No input devices = cannot initiate transfers (deviceless machine)
1085
+                            tracing::debug!(
1086
+                                "EDGE: {:?} at ({}, {}) - no input devices, cannot initiate",
1087
+                                direction,
1088
+                                cursor_pos.0,
1089
+                                cursor_pos.1
1090
+                            );
1079
                         } else {
1091
                         } else {
1080
                             info!(
1092
                             info!(
1081
                                 "EDGE: {:?} at ({}, {}) - initiating transfer",
1093
                                 "EDGE: {:?} at ({}, {}) - initiating transfer",
@@ -1220,6 +1232,12 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1220
                                                             "CURSOR EDGE: {:?} at ({}, {}) - barrier enabled, blocking",
1232
                                                             "CURSOR EDGE: {:?} at ({}, {}) - barrier enabled, blocking",
1221
                                                             edge_dir, cx, cy
1233
                                                             edge_dir, cx, cy
1222
                                                         );
1234
                                                         );
1235
+                                                    } else if !input_grabber.has_devices() {
1236
+                                                        // No input devices = cannot initiate transfers
1237
+                                                        tracing::debug!(
1238
+                                                            "CURSOR EDGE: {:?} at ({}, {}) - no input devices, cannot initiate",
1239
+                                                            edge_dir, cx, cy
1240
+                                                        );
1223
                                                     } else {
1241
                                                     } else {
1224
                                                         info!(
1242
                                                         info!(
1225
                                                             "CURSOR EDGE: {:?} at ({}, {}) - initiating transfer",
1243
                                                             "CURSOR EDGE: {:?} at ({}, {}) - initiating transfer",
@@ -1836,6 +1854,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1836
                                     // At edge with peer but received control from different direction
1854
                                     // At edge with peer but received control from different direction
1837
                                     if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1855
                                     if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1838
                                         IpcResponse::Error { message: "Barrier enabled".to_string() }
1856
                                         IpcResponse::Error { message: "Barrier enabled".to_string() }
1857
+                                    } else if !input_grabber.has_devices() {
1858
+                                        IpcResponse::Error { message: "No input devices - cannot initiate transfer".to_string() }
1839
                                     } else {
1859
                                     } else {
1840
                                         // Initiate new transfer
1860
                                         // Initiate new transfer
1841
                                         let cursor_pos = hypr_client.cursor_pos().await
1861
                                         let cursor_pos = hypr_client.cursor_pos().await
@@ -1879,6 +1899,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
1879
                                     }
1899
                                     }
1880
                                 } else if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1900
                                 } else if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1881
                                     IpcResponse::Error { message: "Barrier enabled".to_string() }
1901
                                     IpcResponse::Error { message: "Barrier enabled".to_string() }
1902
+                                } else if !input_grabber.has_devices() {
1903
+                                    IpcResponse::Error { message: "No input devices - cannot initiate transfer".to_string() }
1882
                                 } else {
1904
                                 } else {
1883
                                     // Initiate new transfer
1905
                                     // Initiate new transfer
1884
                                     let cursor_pos = hypr_client.cursor_pos().await
1906
                                     let cursor_pos = hypr_client.cursor_pos().await
@@ -2070,25 +2092,32 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
2070
                                 if peers_guard.get(&dir).is_some() {
2092
                                 if peers_guard.get(&dir).is_some() {
2071
                                     drop(peers_guard);
2093
                                     drop(peers_guard);
2072
 
2094
 
2073
-                                    // Get cursor position (use center of total screen)
2095
+                                    // Check if we have input devices before initiating
2074
-                                    let cursor_pos = hypr_client.cursor_pos().await
2096
+                                    if !input_grabber.has_devices() {
2075
-                                        .map(|c| (c.x, c.y))
2097
+                                        IpcResponse::Error {
2076
-                                        .unwrap_or(((screen_min_x + screen_max_x) / 2, (screen_min_y + screen_max_y) / 2));
2098
+                                            message: "No input devices - cannot initiate transfer".to_string(),
2077
-
2078
-                                    // Initiate transfer (CLI-initiated, not keyboard)
2079
-                                    info!("IPC Switch: calling initiate_transfer");
2080
-                                    match transfer_manager.initiate_transfer(dir, cursor_pos, screen_min_x, screen_min_y, screen_max_x, screen_max_y, false).await {
2081
-                                        Ok(()) => {
2082
-                                            let machine_name = config.machines.neighbors
2083
-                                                .iter()
2084
-                                                .find(|n| n.direction == dir)
2085
-                                                .map(|n| n.name.clone())
2086
-                                                .unwrap_or_else(|| format!("{:?}", dir));
2087
-                                            info!("IPC Switch: initiate_transfer succeeded, returning response to CLI");
2088
-                                            IpcResponse::Transferred { to_machine: machine_name }
2089
                                         }
2099
                                         }
2090
-                                        Err(e) => IpcResponse::Error {
2100
+                                    } else {
2091
-                                            message: format!("Transfer failed: {}", e),
2101
+                                        // Get cursor position (use center of total screen)
2102
+                                        let cursor_pos = hypr_client.cursor_pos().await
2103
+                                            .map(|c| (c.x, c.y))
2104
+                                            .unwrap_or(((screen_min_x + screen_max_x) / 2, (screen_min_y + screen_max_y) / 2));
2105
+
2106
+                                        // Initiate transfer (CLI-initiated, not keyboard)
2107
+                                        info!("IPC Switch: calling initiate_transfer");
2108
+                                        match transfer_manager.initiate_transfer(dir, cursor_pos, screen_min_x, screen_min_y, screen_max_x, screen_max_y, false).await {
2109
+                                            Ok(()) => {
2110
+                                                let machine_name = config.machines.neighbors
2111
+                                                    .iter()
2112
+                                                    .find(|n| n.direction == dir)
2113
+                                                    .map(|n| n.name.clone())
2114
+                                                    .unwrap_or_else(|| format!("{:?}", dir));
2115
+                                                info!("IPC Switch: initiate_transfer succeeded, returning response to CLI");
2116
+                                                IpcResponse::Transferred { to_machine: machine_name }
2117
+                                            }
2118
+                                            Err(e) => IpcResponse::Error {
2119
+                                                message: format!("Transfer failed: {}", e),
2120
+                                            }
2092
                                         }
2121
                                         }
2093
                                     }
2122
                                     }
2094
                                 } else {
2123
                                 } else {