tenseleyflow/hyprkvm / a27c4a6

Browse files

feat: add relay mode for deviceless machines

When a machine without physical input devices (e.g., hatake) is in
ReceivedControl and initiates a transfer to another direction, it now
enters Relaying mode instead of trying to grab local devices.

In Relaying mode, the machine forwards InputEvents from the original
controller to the new target, acting as a transparent relay. This
enables full multi-machine KVM chains where only one machine has
physical input devices.

New states and events:
- TransferState::Relaying { from, to } - receiving from one, forwarding to another
- TransferEvent::StartRelay { from, to } - begin relay mode
- TransferEvent::StopRelay - end relay mode

When the relay target returns control (Leave), the relay machine
resumes ReceivedControl from the original source.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
a27c4a6b574ea1070c2127c9a9775428786a3e4b
Parents
a657504
Tree
7d25812

2 changed files

StatusFile+-
M hyprkvm-daemon/src/main.rs 29 1
M hyprkvm-daemon/src/transfer/manager.rs 148 27
hyprkvm-daemon/src/main.rsmodified
@@ -311,6 +311,8 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
311311
 
312312
     // Track which direction we're capturing for
313313
     let mut capture_direction: Option<Direction> = None;
314
+    // Track relay mode: (from, to) when forwarding input from one peer to another
315
+    let mut relay_mode: Option<(Direction, Direction)> = None;
314316
     let mut input_sequence: u64 = 0;
315317
 
316318
     // Escape key detection
@@ -1348,7 +1350,24 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
13481350
                                         // Transfer complete
13491351
                                     }
13501352
                                     Message::InputEvent(input_payload) => {
1351
-                                        // Inject input via emulation module
1353
+                                        // Check if we're in relay mode and this is from the relay source
1354
+                                        if let Some((relay_from, relay_to)) = relay_mode {
1355
+                                            if direction == relay_from {
1356
+                                                // Forward to relay target instead of injecting locally
1357
+                                                tracing::trace!("Relaying input from {:?} to {:?}", relay_from, relay_to);
1358
+                                                // Need to drop current borrow and get relay target
1359
+                                                drop(peers);
1360
+                                                let mut peers_guard = peers_arc.write().await;
1361
+                                                if let Some(target_peer) = peers_guard.get_mut(&relay_to) {
1362
+                                                    if let Err(e) = target_peer.send(&Message::InputEvent(input_payload)).await {
1363
+                                                        tracing::error!("Failed to relay input to {:?}: {}", relay_to, e);
1364
+                                                    }
1365
+                                                }
1366
+                                                continue; // Skip local injection
1367
+                                            }
1368
+                                        }
1369
+
1370
+                                        // Normal case: inject input via emulation module
13521371
                                         if let Some(ref mut emu) = input_emulator {
13531372
                                             use hyprkvm_common::protocol::InputEventType;
13541373
                                             match input_payload.event {
@@ -1638,6 +1657,15 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
16381657
                             emu.keyboard.reset_all_keys();
16391658
                         }
16401659
                     }
1660
+                    transfer::TransferEvent::StartRelay { from, to } => {
1661
+                        info!("Starting input relay: {:?} -> {:?}", from, to);
1662
+                        relay_mode = Some((from, to));
1663
+                        // No local device grabbing needed - we're forwarding received input
1664
+                    }
1665
+                    transfer::TransferEvent::StopRelay => {
1666
+                        info!("Stopping input relay");
1667
+                        relay_mode = None;
1668
+                    }
16411669
                     transfer::TransferEvent::SyncClipboardOutgoing { direction } => {
16421670
                         // Sync clipboard to the peer in the given direction
16431671
                         // Check if clipboard sync is enabled and appropriate for this event
hyprkvm-daemon/src/transfer/manager.rsmodified
@@ -25,9 +25,11 @@ pub enum TransferState {
2525
         started_at: Instant,
2626
         /// True if transfer was triggered via keyboard (Super+Arrow)
2727
         keyboard_initiated: bool,
28
+        /// If we initiated from ReceivedControl, this is the original source
29
+        relay_from: Option<Direction>,
2830
     },
2931
 
30
-    /// We sent control away, forwarding input
32
+    /// We sent control away, forwarding input (from local devices)
3133
     RemoteActive {
3234
         target: Direction,
3335
         transfer_id: u64,
@@ -36,6 +38,14 @@ pub enum TransferState {
3638
         keyboard_initiated: bool,
3739
     },
3840
 
41
+    /// We are relaying input from one machine to another (no local devices)
42
+    Relaying {
43
+        from: Direction,
44
+        to: Direction,
45
+        transfer_id: u64,
46
+        entered_at: Instant,
47
+    },
48
+
3949
     /// We received control from another machine
4050
     ReceivedControl {
4151
         from: Direction,
@@ -53,6 +63,10 @@ impl TransferState {
5363
         matches!(self, TransferState::RemoteActive { .. })
5464
     }
5565
 
66
+    pub fn is_relaying(&self) -> bool {
67
+        matches!(self, TransferState::Relaying { .. })
68
+    }
69
+
5670
     pub fn is_receiving(&self) -> bool {
5771
         matches!(self, TransferState::ReceivedControl { .. })
5872
     }
@@ -61,12 +75,17 @@ impl TransferState {
6175
 /// Events from the transfer manager
6276
 #[derive(Debug, Clone)]
6377
 pub enum TransferEvent {
64
-    /// Start capturing and forwarding input
78
+    /// Start capturing and forwarding input (from local devices)
6579
     /// `keyboard_initiated` is true if the transfer was triggered via keyboard (Super+Arrow),
6680
     /// false if triggered via CLI or other non-keyboard means
6781
     StartCapture { direction: Direction, keyboard_initiated: bool },
6882
     /// Stop capturing, return to local
6983
     StopCapture,
84
+    /// Start relaying input from one direction to another (no local devices needed)
85
+    /// Used when a deviceless machine needs to forward input through
86
+    StartRelay { from: Direction, to: Direction },
87
+    /// Stop relaying
88
+    StopRelay,
7089
     /// Start injecting received input
7190
     StartInjection { from: Direction },
7291
     /// Stop injecting
@@ -124,9 +143,10 @@ impl TransferManager {
124143
     ) -> Result<(), TransferError> {
125144
         let mut state = self.state.write().await;
126145
 
127
-        // Only transfer from local or received state
128
-        match &*state {
129
-            TransferState::Local | TransferState::ReceivedControl { .. } => {}
146
+        // Track if we're initiating from ReceivedControl (for relay mode)
147
+        let relay_from = match &*state {
148
+            TransferState::Local => None,
149
+            TransferState::ReceivedControl { from, .. } => Some(*from),
130150
             TransferState::Initiating { .. } => {
131151
                 return Err(TransferError::AlreadyTransferring);
132152
             }
@@ -135,7 +155,12 @@ impl TransferManager {
135155
                     "Already in remote active state".to_string(),
136156
                 ));
137157
             }
138
-        }
158
+            TransferState::Relaying { .. } => {
159
+                return Err(TransferError::InvalidState(
160
+                    "Already relaying".to_string(),
161
+                ));
162
+            }
163
+        };
139164
 
140165
         let transfer_id = self.next_transfer_id();
141166
 
@@ -166,6 +191,7 @@ impl TransferManager {
166191
             transfer_id,
167192
             started_at: Instant::now(),
168193
             keyboard_initiated,
194
+            relay_from,
169195
         };
170196
 
171197
         // Send Enter message
@@ -196,6 +222,7 @@ impl TransferManager {
196222
                 target,
197223
                 transfer_id,
198224
                 keyboard_initiated,
225
+                relay_from,
199226
                 ..
200227
             } => {
201228
                 if *transfer_id != ack.transfer_id {
@@ -209,34 +236,67 @@ impl TransferManager {
209236
 
210237
                 if !ack.success {
211238
                     tracing::warn!("EnterAck rejected: {:?}", ack.error);
212
-                    *state = TransferState::Local;
239
+                    // If we were relaying, go back to ReceivedControl
240
+                    if let Some(from) = relay_from {
241
+                        *state = TransferState::ReceivedControl {
242
+                            from: *from,
243
+                            transfer_id: *transfer_id,
244
+                            entered_at: Instant::now(),
245
+                        };
246
+                    } else {
247
+                        *state = TransferState::Local;
248
+                    }
213249
                     return Err(TransferError::Rejected(
214250
                         ack.error.unwrap_or_else(|| "Unknown".to_string()),
215251
                     ));
216252
                 }
217253
 
218
-                tracing::info!(
219
-                    "Transfer accepted, cursor at {:?}, keyboard_initiated={}",
220
-                    ack.actual_cursor_pos,
221
-                    keyboard_initiated
222
-                );
223
-
224254
                 let direction = *target;
225255
                 let tid = *transfer_id;
226256
                 let kbd_init = *keyboard_initiated;
257
+                let from_dir = *relay_from;
227258
 
228
-                *state = TransferState::RemoteActive {
229
-                    target: direction,
230
-                    transfer_id: tid,
231
-                    entered_at: Instant::now(),
232
-                    keyboard_initiated: kbd_init,
233
-                };
259
+                // Check if this is a relay (initiated from ReceivedControl) or direct transfer
260
+                if let Some(from) = from_dir {
261
+                    tracing::info!(
262
+                        "Relay transfer accepted: {:?} -> {:?}, cursor at {:?}",
263
+                        from,
264
+                        direction,
265
+                        ack.actual_cursor_pos
266
+                    );
234267
 
235
-                // Start capturing input
236
-                self.event_tx
237
-                    .send(TransferEvent::StartCapture { direction, keyboard_initiated: kbd_init })
238
-                    .await
239
-                    .map_err(|_| TransferError::ChannelClosed)?;
268
+                    *state = TransferState::Relaying {
269
+                        from,
270
+                        to: direction,
271
+                        transfer_id: tid,
272
+                        entered_at: Instant::now(),
273
+                    };
274
+
275
+                    // Start relaying input (don't grab local devices, forward from source)
276
+                    self.event_tx
277
+                        .send(TransferEvent::StartRelay { from, to: direction })
278
+                        .await
279
+                        .map_err(|_| TransferError::ChannelClosed)?;
280
+                } else {
281
+                    tracing::info!(
282
+                        "Transfer accepted, cursor at {:?}, keyboard_initiated={}",
283
+                        ack.actual_cursor_pos,
284
+                        keyboard_initiated
285
+                    );
286
+
287
+                    *state = TransferState::RemoteActive {
288
+                        target: direction,
289
+                        transfer_id: tid,
290
+                        entered_at: Instant::now(),
291
+                        keyboard_initiated: kbd_init,
292
+                    };
293
+
294
+                    // Start capturing input from local devices
295
+                    self.event_tx
296
+                        .send(TransferEvent::StartCapture { direction, keyboard_initiated: kbd_init })
297
+                        .await
298
+                        .map_err(|_| TransferError::ChannelClosed)?;
299
+                }
240300
 
241301
                 // Trigger clipboard sync (if enabled, handled by main loop)
242302
                 self.event_tx
@@ -283,6 +343,11 @@ impl TransferManager {
283343
                 // This shouldn't happen normally - they should send Leave, not Enter
284344
                 tracing::warn!("Received Enter while in RemoteActive - unusual but accepting");
285345
             }
346
+            TransferState::Relaying { .. } => {
347
+                // We're relaying input, but receiving a new Enter
348
+                // This is unusual but we'll accept it
349
+                tracing::warn!("Received Enter while Relaying - unusual but accepting");
350
+            }
286351
         }
287352
 
288353
         // Calculate actual cursor position using proper screen bounds
@@ -440,8 +505,45 @@ impl TransferManager {
440505
                 *state = TransferState::Local;
441506
                 Ok(())
442507
             }
508
+            TransferState::Relaying { from, transfer_id, .. } => {
509
+                if *transfer_id != payload.transfer_id {
510
+                    tracing::warn!("Leave transfer_id mismatch (relay)");
511
+                }
512
+
513
+                let from_dir = *from;
514
+                tracing::info!("Relay target returned control, resuming ReceivedControl from {:?}", from_dir);
515
+
516
+                // Stop relaying
517
+                self.event_tx
518
+                    .send(TransferEvent::StopRelay)
519
+                    .await
520
+                    .map_err(|_| TransferError::ChannelClosed)?;
521
+
522
+                // Send LeaveAck to the target
523
+                let direction = payload.to_direction.opposite();
524
+                self.event_tx
525
+                    .send(TransferEvent::SendMessage {
526
+                        direction,
527
+                        message: Message::LeaveAck,
528
+                    })
529
+                    .await
530
+                    .map_err(|_| TransferError::ChannelClosed)?;
531
+
532
+                // Resume injection from original source
533
+                self.event_tx
534
+                    .send(TransferEvent::StartInjection { from: from_dir })
535
+                    .await
536
+                    .map_err(|_| TransferError::ChannelClosed)?;
537
+
538
+                *state = TransferState::ReceivedControl {
539
+                    from: from_dir,
540
+                    transfer_id: *transfer_id,
541
+                    entered_at: Instant::now(),
542
+                };
543
+                Ok(())
544
+            }
443545
             _ => Err(TransferError::InvalidState(
444
-                "Not in RemoteActive state".to_string(),
546
+                "Not in RemoteActive or Relaying state".to_string(),
445547
             )),
446548
         }
447549
     }
@@ -451,15 +553,34 @@ impl TransferManager {
451553
         let mut state = self.state.write().await;
452554
 
453555
         match &*state {
454
-            TransferState::Initiating { .. } => {
556
+            TransferState::Initiating { relay_from, .. } => {
455557
                 tracing::warn!("Aborting pending transfer");
456
-                *state = TransferState::Local;
558
+                // If we were initiating from ReceivedControl, go back there
559
+                if let Some(from) = relay_from {
560
+                    *state = TransferState::ReceivedControl {
561
+                        from: *from,
562
+                        transfer_id: 0,
563
+                        entered_at: Instant::now(),
564
+                    };
565
+                } else {
566
+                    *state = TransferState::Local;
567
+                }
457568
             }
458569
             TransferState::RemoteActive { .. } => {
459570
                 tracing::warn!("Aborting remote active state");
460571
                 let _ = self.event_tx.send(TransferEvent::StopCapture).await;
461572
                 *state = TransferState::Local;
462573
             }
574
+            TransferState::Relaying { from, .. } => {
575
+                tracing::warn!("Aborting relay state");
576
+                let _ = self.event_tx.send(TransferEvent::StopRelay).await;
577
+                // Go back to receiving from original source
578
+                *state = TransferState::ReceivedControl {
579
+                    from: *from,
580
+                    transfer_id: 0,
581
+                    entered_at: Instant::now(),
582
+                };
583
+            }
463584
             TransferState::ReceivedControl { .. } => {
464585
                 tracing::warn!("Aborting received control state");
465586
                 let _ = self.event_tx.send(TransferEvent::StopInjection).await;