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 {
3030
     recovery_active: Arc<AtomicU64>,
3131
     /// The direction to watch for in recovery mode (encoded as u8: 1=Up, 2=Down, 3=Left, 4=Right, 0=none)
3232
     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>,
3336
     event_rx: mpsc::Receiver<GrabEvent>,
3437
     _thread: thread::JoinHandle<()>,
3538
 }
@@ -43,13 +46,15 @@ impl EvdevGrabber {
4346
         let recovery_clone = recovery_active.clone();
4447
         let recovery_direction = Arc::new(AtomicU64::new(0));
4548
         let recovery_dir_clone = recovery_direction.clone();
49
+        let has_devices = Arc::new(AtomicBool::new(false));
50
+        let has_devices_clone = has_devices.clone();
4651
 
4752
         let (event_tx, event_rx) = mpsc::channel();
4853
 
4954
         let thread = thread::Builder::new()
5055
             .name("evdev-grabber".to_string())
5156
             .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) {
5358
                     tracing::error!("Evdev grabber error: {}", e);
5459
                 }
5560
             })
@@ -59,6 +64,7 @@ impl EvdevGrabber {
5964
             active,
6065
             recovery_active,
6166
             recovery_direction,
67
+            has_devices,
6268
             event_rx,
6369
             _thread: thread,
6470
         })
@@ -101,6 +107,13 @@ impl EvdevGrabber {
101107
         self.active.load(Ordering::SeqCst)
102108
     }
103109
 
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
+
104117
     /// Try to receive a grab event (non-blocking)
105118
     pub fn try_recv(&self) -> Option<GrabEvent> {
106119
         self.event_rx.try_recv().ok()
@@ -184,15 +197,20 @@ fn run_evdev_grabber(
184197
     active: Arc<AtomicBool>,
185198
     recovery_active: Arc<AtomicU64>,
186199
     recovery_direction: Arc<AtomicU64>,
200
+    has_devices: Arc<AtomicBool>,
187201
     event_tx: mpsc::Sender<GrabEvent>,
188202
 ) -> Result<(), EvdevGrabError> {
189203
     let device_paths = find_input_devices();
190204
 
191205
     if device_paths.is_empty() {
206
+        tracing::warn!("No input devices found - this machine cannot initiate transfers");
207
+        // Leave has_devices as false (default)
192208
         return Err(EvdevGrabError::NoDevices);
193209
     }
194210
 
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());
196214
     for path in &device_paths {
197215
         tracing::debug!("  {}", path.display());
198216
     }
hyprkvm-daemon/src/main.rsmodified
@@ -967,6 +967,10 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
967967
 
968968
                                 if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
969969
                                     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");
970974
                                 } else {
971975
                                     info!("RECOVERY HOTKEY: At edge with peer, initiating transfer to {:?}", direction);
972976
                                     if let Err(e) = transfer_manager.initiate_transfer(
@@ -1076,6 +1080,14 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
10761080
                                 cursor_pos.0,
10771081
                                 cursor_pos.1
10781082
                             );
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
+                            );
10791091
                         } else {
10801092
                             info!(
10811093
                                 "EDGE: {:?} at ({}, {}) - initiating transfer",
@@ -1220,6 +1232,12 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
12201232
                                                             "CURSOR EDGE: {:?} at ({}, {}) - barrier enabled, blocking",
12211233
                                                             edge_dir, cx, cy
12221234
                                                         );
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
+                                                        );
12231241
                                                     } else {
12241242
                                                         info!(
12251243
                                                             "CURSOR EDGE: {:?} at ({}, {}) - initiating transfer",
@@ -1836,6 +1854,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
18361854
                                     // At edge with peer but received control from different direction
18371855
                                     if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
18381856
                                         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() }
18391859
                                     } else {
18401860
                                         // Initiate new transfer
18411861
                                         let cursor_pos = hypr_client.cursor_pos().await
@@ -1879,6 +1899,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
18791899
                                     }
18801900
                                 } else if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
18811901
                                     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() }
18821904
                                 } else {
18831905
                                     // Initiate new transfer
18841906
                                     let cursor_pos = hypr_client.cursor_pos().await
@@ -2070,25 +2092,32 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
20702092
                                 if peers_guard.get(&dir).is_some() {
20712093
                                     drop(peers_guard);
20722094
 
2073
-                                    // Get cursor position (use center of total screen)
2074
-                                    let cursor_pos = hypr_client.cursor_pos().await
2075
-                                        .map(|c| (c.x, c.y))
2076
-                                        .unwrap_or(((screen_min_x + screen_max_x) / 2, (screen_min_y + screen_max_y) / 2));
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 }
2095
+                                    // Check if we have input devices before initiating
2096
+                                    if !input_grabber.has_devices() {
2097
+                                        IpcResponse::Error {
2098
+                                            message: "No input devices - cannot initiate transfer".to_string(),
20892099
                                         }
2090
-                                        Err(e) => IpcResponse::Error {
2091
-                                            message: format!("Transfer failed: {}", e),
2100
+                                    } else {
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
+                                            }
20922121
                                         }
20932122
                                     }
20942123
                                 } else {