wraith/src-tauri/src/commands/rdp_commands.rs
Vantz Stockwell 8c431d3d12 fix: SSH input deadlock — output loop held channel mutex across await
Root cause: The output reader loop held Arc<TokioMutex<Channel>> while
calling ch.wait().await. After the initial prompt rendered and the server
went idle, wait() blocked indefinitely holding the lock. ssh_write()
could never acquire the mutex to send keystrokes. Permanent deadlock.

Fix: Separated read/write paths. The output loop now owns the Channel
exclusively via tokio::select!, receiving resize/shutdown commands through
an mpsc channel. Writes go through Handle::data(channel_id, data) which
bypasses the Channel entirely — no shared mutex, no deadlock.

Also killed all compiler warnings (unused imports in rdp module).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:28:09 -04:00

104 lines
2.8 KiB
Rust

//! Tauri commands for RDP session management.
//!
//! Mirrors the pattern used by `ssh_commands.rs` — thin command wrappers that
//! delegate to the `RdpService` via `State<AppState>`.
use tauri::State;
use crate::rdp::{RdpConfig, RdpSessionInfo};
use crate::AppState;
/// Connect to an RDP server.
///
/// Performs the full connection handshake (TCP -> TLS -> CredSSP -> RDP) and
/// starts streaming frame updates in the background.
///
/// Returns the session UUID.
#[tauri::command]
pub fn connect_rdp(
config: RdpConfig,
state: State<'_, AppState>,
) -> Result<String, String> {
state.rdp.connect(config)
}
/// Get the current frame buffer as a base64-encoded RGBA string.
///
/// The frontend decodes this and draws it onto a `<canvas>` element.
/// Pixel format: RGBA, 4 bytes per pixel, row-major, top-left origin.
#[tauri::command]
pub async fn rdp_get_frame(
session_id: String,
state: State<'_, AppState>,
) -> Result<String, String> {
state.rdp.get_frame(&session_id).await
}
/// Send a mouse event to an RDP session.
///
/// `flags` uses MS-RDPBCGR mouse event flags:
/// - 0x0800 = move
/// - 0x1000 = left button
/// - 0x2000 = right button
/// - 0x4000 = middle button
/// - 0x8000 = button pressed (absence = released)
/// - 0x0200 = vertical wheel
/// - 0x0100 = negative wheel direction
/// - 0x0400 = horizontal wheel
#[tauri::command]
pub async fn rdp_send_mouse(
session_id: String,
x: u16,
y: u16,
flags: u32,
state: State<'_, AppState>,
) -> Result<(), String> {
state.rdp.send_mouse(&session_id, x, y, flags)
}
/// Send a keyboard event to an RDP session.
///
/// `scancode` is the RDP hardware scancode (from the scancode map in
/// `rdp::input`). For extended keys (e.g. arrows, numpad enter), the high
/// byte is 0xE0.
///
/// `pressed` is `true` for key-down, `false` for key-up.
#[tauri::command]
pub async fn rdp_send_key(
session_id: String,
scancode: u16,
pressed: bool,
state: State<'_, AppState>,
) -> Result<(), String> {
state.rdp.send_key(&session_id, scancode, pressed)
}
/// Send clipboard text to an RDP session by simulating keystrokes.
#[tauri::command]
pub async fn rdp_send_clipboard(
session_id: String,
text: String,
state: State<'_, AppState>,
) -> Result<(), String> {
state.rdp.send_clipboard(&session_id, &text)
}
/// Disconnect an RDP session.
///
/// Sends a graceful shutdown to the RDP server and removes the session.
#[tauri::command]
pub async fn disconnect_rdp(
session_id: String,
state: State<'_, AppState>,
) -> Result<(), String> {
state.rdp.disconnect(&session_id)
}
/// List all active RDP sessions (metadata only).
#[tauri::command]
pub async fn list_rdp_sessions(
state: State<'_, AppState>,
) -> Result<Vec<RdpSessionInfo>, String> {
Ok(state.rdp.list_sessions())
}