Add native viewer with low-level keyboard hooks
- New viewer crate for Windows native remote desktop viewing - Implements WH_KEYBOARD_LL hook for Win key, Alt+Tab capture - WebSocket client for server communication - softbuffer rendering for frame display - Zstd decompression for compressed frames - Mouse and keyboard input forwarding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1911
Cargo.lock
generated
1911
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"agent",
|
"agent",
|
||||||
"server",
|
"server",
|
||||||
|
"viewer",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
66
viewer/Cargo.toml
Normal file
66
viewer/Cargo.toml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
[package]
|
||||||
|
name = "guruconnect-viewer"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["AZ Computer Guru"]
|
||||||
|
description = "GuruConnect Native Remote Desktop Viewer"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Async runtime
|
||||||
|
tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
|
# WebSocket
|
||||||
|
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
||||||
|
futures-util = "0.3"
|
||||||
|
url = "2"
|
||||||
|
|
||||||
|
# Windowing
|
||||||
|
winit = { version = "0.30", features = ["rwh_06"] }
|
||||||
|
softbuffer = "0.4"
|
||||||
|
raw-window-handle = "0.6"
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
zstd = "0.13"
|
||||||
|
|
||||||
|
# Protocol (protobuf)
|
||||||
|
prost = "0.13"
|
||||||
|
prost-types = "0.13"
|
||||||
|
bytes = "1"
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
anyhow = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
|
# UUID
|
||||||
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
|
|
||||||
|
# CLI args
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
# Windows APIs for low-level keyboard hooks
|
||||||
|
windows = { version = "0.58", features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
|
"Win32_System_LibraryLoader",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
]}
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
prost-build = "0.13"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
opt-level = "z"
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
9
viewer/build.rs
Normal file
9
viewer/build.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use std::io::Result;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
println!("cargo:rerun-if-changed=../proto/guruconnect.proto");
|
||||||
|
|
||||||
|
prost_build::compile_protos(&["../proto/guruconnect.proto"], &["../proto/"])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
173
viewer/src/input.rs
Normal file
173
viewer/src/input.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
//! Low-level keyboard hook for capturing all keys including Win key
|
||||||
|
|
||||||
|
use crate::InputEvent;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use crate::proto;
|
||||||
|
use anyhow::Result;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use windows::{
|
||||||
|
Win32::Foundation::{LPARAM, LRESULT, WPARAM},
|
||||||
|
Win32::UI::WindowsAndMessaging::{
|
||||||
|
CallNextHookEx, DispatchMessageW, GetMessageW, PeekMessageW, SetWindowsHookExW,
|
||||||
|
TranslateMessage, UnhookWindowsHookEx, HHOOK, KBDLLHOOKSTRUCT, MSG, PM_REMOVE,
|
||||||
|
WH_KEYBOARD_LL, WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
static INPUT_TX: OnceLock<mpsc::Sender<InputEvent>> = OnceLock::new();
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
static mut HOOK_HANDLE: HHOOK = HHOOK(std::ptr::null_mut());
|
||||||
|
|
||||||
|
/// Virtual key codes for special keys
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod vk {
|
||||||
|
pub const VK_LWIN: u32 = 0x5B;
|
||||||
|
pub const VK_RWIN: u32 = 0x5C;
|
||||||
|
pub const VK_APPS: u32 = 0x5D;
|
||||||
|
pub const VK_LSHIFT: u32 = 0xA0;
|
||||||
|
pub const VK_RSHIFT: u32 = 0xA1;
|
||||||
|
pub const VK_LCONTROL: u32 = 0xA2;
|
||||||
|
pub const VK_RCONTROL: u32 = 0xA3;
|
||||||
|
pub const VK_LMENU: u32 = 0xA4; // Left Alt
|
||||||
|
pub const VK_RMENU: u32 = 0xA5; // Right Alt
|
||||||
|
pub const VK_TAB: u32 = 0x09;
|
||||||
|
pub const VK_ESCAPE: u32 = 0x1B;
|
||||||
|
pub const VK_SNAPSHOT: u32 = 0x2C; // Print Screen
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub struct KeyboardHook {
|
||||||
|
_hook: HHOOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
impl KeyboardHook {
|
||||||
|
pub fn new(input_tx: mpsc::Sender<InputEvent>) -> Result<Self> {
|
||||||
|
// Store the sender globally for the hook callback
|
||||||
|
INPUT_TX.set(input_tx).map_err(|_| anyhow::anyhow!("Input TX already set"))?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let hook = SetWindowsHookExW(
|
||||||
|
WH_KEYBOARD_LL,
|
||||||
|
Some(keyboard_hook_proc),
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
HOOK_HANDLE = hook;
|
||||||
|
Ok(Self { _hook: hook })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
impl Drop for KeyboardHook {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
if !HOOK_HANDLE.0.is_null() {
|
||||||
|
let _ = UnhookWindowsHookEx(HOOK_HANDLE);
|
||||||
|
HOOK_HANDLE = HHOOK(std::ptr::null_mut());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
unsafe extern "system" fn keyboard_hook_proc(
|
||||||
|
code: i32,
|
||||||
|
wparam: WPARAM,
|
||||||
|
lparam: LPARAM,
|
||||||
|
) -> LRESULT {
|
||||||
|
if code >= 0 {
|
||||||
|
let kb_struct = &*(lparam.0 as *const KBDLLHOOKSTRUCT);
|
||||||
|
let vk_code = kb_struct.vkCode;
|
||||||
|
let scan_code = kb_struct.scanCode;
|
||||||
|
|
||||||
|
let is_down = wparam.0 as u32 == WM_KEYDOWN || wparam.0 as u32 == WM_SYSKEYDOWN;
|
||||||
|
let is_up = wparam.0 as u32 == WM_KEYUP || wparam.0 as u32 == WM_SYSKEYUP;
|
||||||
|
|
||||||
|
if is_down || is_up {
|
||||||
|
// Check if this is a key we want to intercept (Win key, Alt+Tab, etc.)
|
||||||
|
let should_intercept = matches!(
|
||||||
|
vk_code,
|
||||||
|
vk::VK_LWIN | vk::VK_RWIN | vk::VK_APPS
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send the key event to the remote
|
||||||
|
if let Some(tx) = INPUT_TX.get() {
|
||||||
|
let event = proto::KeyEvent {
|
||||||
|
down: is_down,
|
||||||
|
key_type: proto::KeyEventType::KeyVk as i32,
|
||||||
|
vk_code,
|
||||||
|
scan_code,
|
||||||
|
unicode: String::new(),
|
||||||
|
modifiers: Some(get_current_modifiers()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = tx.try_send(InputEvent::Key(event));
|
||||||
|
trace!("Key hook: vk={:#x} scan={} down={}", vk_code, scan_code, is_down);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Win key, consume the event so it doesn't open Start menu locally
|
||||||
|
if should_intercept {
|
||||||
|
return LRESULT(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CallNextHookEx(HOOK_HANDLE, code, wparam, lparam)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn get_current_modifiers() -> proto::Modifiers {
|
||||||
|
use windows::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
proto::Modifiers {
|
||||||
|
ctrl: GetAsyncKeyState(0x11) < 0, // VK_CONTROL
|
||||||
|
alt: GetAsyncKeyState(0x12) < 0, // VK_MENU
|
||||||
|
shift: GetAsyncKeyState(0x10) < 0, // VK_SHIFT
|
||||||
|
meta: GetAsyncKeyState(0x5B) < 0 || GetAsyncKeyState(0x5C) < 0, // VK_LWIN/RWIN
|
||||||
|
caps_lock: GetAsyncKeyState(0x14) & 1 != 0, // VK_CAPITAL
|
||||||
|
num_lock: GetAsyncKeyState(0x90) & 1 != 0, // VK_NUMLOCK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pump Windows message queue (required for hooks to work)
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn pump_messages() {
|
||||||
|
unsafe {
|
||||||
|
let mut msg = MSG::default();
|
||||||
|
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||||
|
let _ = TranslateMessage(&msg);
|
||||||
|
DispatchMessageW(&msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-Windows stubs
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct KeyboardHook;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl KeyboardHook {
|
||||||
|
pub fn new(_input_tx: mpsc::Sender<InputEvent>) -> Result<Self> {
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn pump_messages() {}
|
||||||
167
viewer/src/main.rs
Normal file
167
viewer/src/main.rs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
//! GuruConnect Native Viewer
|
||||||
|
//!
|
||||||
|
//! Native remote desktop viewer with full keyboard capture including Win key.
|
||||||
|
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
mod proto;
|
||||||
|
mod transport;
|
||||||
|
mod render;
|
||||||
|
mod input;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
use tracing::{info, error, warn, Level};
|
||||||
|
use tracing_subscriber::FmtSubscriber;
|
||||||
|
|
||||||
|
/// GuruConnect Native Viewer
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(name = "guruconnect-viewer")]
|
||||||
|
#[command(about = "Native remote desktop viewer with full keyboard capture")]
|
||||||
|
struct Args {
|
||||||
|
/// Server URL (e.g., wss://connect.azcomputerguru.com/ws/viewer)
|
||||||
|
#[arg(short, long, default_value = "wss://connect.azcomputerguru.com/ws/viewer")]
|
||||||
|
server: String,
|
||||||
|
|
||||||
|
/// Session ID to connect to
|
||||||
|
#[arg(short = 'i', long)]
|
||||||
|
session_id: String,
|
||||||
|
|
||||||
|
/// API key for authentication
|
||||||
|
#[arg(short, long, default_value = "dev-key")]
|
||||||
|
api_key: String,
|
||||||
|
|
||||||
|
/// Enable verbose logging
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ViewerEvent {
|
||||||
|
Connected,
|
||||||
|
Disconnected(String),
|
||||||
|
Frame(render::FrameData),
|
||||||
|
CursorPosition(i32, i32, bool),
|
||||||
|
CursorShape(proto::CursorShape),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum InputEvent {
|
||||||
|
Mouse(proto::MouseEvent),
|
||||||
|
Key(proto::KeyEvent),
|
||||||
|
SpecialKey(proto::SpecialKeyEvent),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
// Initialize logging
|
||||||
|
let level = if args.verbose { Level::DEBUG } else { Level::INFO };
|
||||||
|
FmtSubscriber::builder()
|
||||||
|
.with_max_level(level)
|
||||||
|
.with_target(false)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("GuruConnect Viewer starting");
|
||||||
|
info!("Server: {}", args.server);
|
||||||
|
info!("Session: {}", args.session_id);
|
||||||
|
|
||||||
|
// Create channels for communication between components
|
||||||
|
let (viewer_tx, viewer_rx) = mpsc::channel::<ViewerEvent>(100);
|
||||||
|
let (input_tx, input_rx) = mpsc::channel::<InputEvent>(100);
|
||||||
|
|
||||||
|
// Run the viewer
|
||||||
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
rt.block_on(async {
|
||||||
|
run_viewer(args, viewer_tx, viewer_rx, input_tx, input_rx).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_viewer(
|
||||||
|
args: Args,
|
||||||
|
viewer_tx: mpsc::Sender<ViewerEvent>,
|
||||||
|
viewer_rx: mpsc::Receiver<ViewerEvent>,
|
||||||
|
input_tx: mpsc::Sender<InputEvent>,
|
||||||
|
mut input_rx: mpsc::Receiver<InputEvent>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Connect to server
|
||||||
|
let ws_url = format!("{}?session_id={}", args.server, args.session_id);
|
||||||
|
info!("Connecting to {}", ws_url);
|
||||||
|
|
||||||
|
let (ws_sender, mut ws_receiver) = transport::connect(&ws_url, &args.api_key).await?;
|
||||||
|
let ws_sender = Arc::new(Mutex::new(ws_sender));
|
||||||
|
|
||||||
|
info!("Connected to server");
|
||||||
|
let _ = viewer_tx.send(ViewerEvent::Connected).await;
|
||||||
|
|
||||||
|
// Clone sender for input forwarding
|
||||||
|
let ws_sender_input = ws_sender.clone();
|
||||||
|
|
||||||
|
// Spawn task to forward input events to server
|
||||||
|
let input_task = tokio::spawn(async move {
|
||||||
|
while let Some(event) = input_rx.recv().await {
|
||||||
|
let msg = match event {
|
||||||
|
InputEvent::Mouse(m) => proto::Message {
|
||||||
|
payload: Some(proto::message::Payload::MouseEvent(m)),
|
||||||
|
},
|
||||||
|
InputEvent::Key(k) => proto::Message {
|
||||||
|
payload: Some(proto::message::Payload::KeyEvent(k)),
|
||||||
|
},
|
||||||
|
InputEvent::SpecialKey(s) => proto::Message {
|
||||||
|
payload: Some(proto::message::Payload::SpecialKey(s)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = transport::send_message(&ws_sender_input, &msg).await {
|
||||||
|
error!("Failed to send input: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn task to receive messages from server
|
||||||
|
let viewer_tx_recv = viewer_tx.clone();
|
||||||
|
let receive_task = tokio::spawn(async move {
|
||||||
|
while let Some(msg) = ws_receiver.recv().await {
|
||||||
|
match msg.payload {
|
||||||
|
Some(proto::message::Payload::VideoFrame(frame)) => {
|
||||||
|
if let Some(proto::video_frame::Encoding::Raw(raw)) = frame.encoding {
|
||||||
|
let frame_data = render::FrameData {
|
||||||
|
width: raw.width as u32,
|
||||||
|
height: raw.height as u32,
|
||||||
|
data: raw.data,
|
||||||
|
compressed: raw.compressed,
|
||||||
|
is_keyframe: raw.is_keyframe,
|
||||||
|
};
|
||||||
|
let _ = viewer_tx_recv.send(ViewerEvent::Frame(frame_data)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(proto::message::Payload::CursorPosition(pos)) => {
|
||||||
|
let _ = viewer_tx_recv.send(ViewerEvent::CursorPosition(
|
||||||
|
pos.x, pos.y, pos.visible
|
||||||
|
)).await;
|
||||||
|
}
|
||||||
|
Some(proto::message::Payload::CursorShape(shape)) => {
|
||||||
|
let _ = viewer_tx_recv.send(ViewerEvent::CursorShape(shape)).await;
|
||||||
|
}
|
||||||
|
Some(proto::message::Payload::Disconnect(d)) => {
|
||||||
|
warn!("Server disconnected: {}", d.reason);
|
||||||
|
let _ = viewer_tx_recv.send(ViewerEvent::Disconnected(d.reason)).await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the window (this blocks until window closes)
|
||||||
|
render::run_window(viewer_rx, input_tx).await?;
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
input_task.abort();
|
||||||
|
receive_task.abort();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
4
viewer/src/proto.rs
Normal file
4
viewer/src/proto.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
//! Protocol buffer definitions
|
||||||
|
|
||||||
|
// Include generated protobuf code
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
|
||||||
507
viewer/src/render.rs
Normal file
507
viewer/src/render.rs
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
//! Window rendering and frame display
|
||||||
|
|
||||||
|
use crate::{ViewerEvent, InputEvent, proto};
|
||||||
|
#[cfg(windows)]
|
||||||
|
use crate::input;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
use winit::{
|
||||||
|
application::ApplicationHandler,
|
||||||
|
dpi::LogicalSize,
|
||||||
|
event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent},
|
||||||
|
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||||||
|
keyboard::{KeyCode, PhysicalKey},
|
||||||
|
window::{Window, WindowId},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Frame data received from server
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FrameData {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub compressed: bool,
|
||||||
|
pub is_keyframe: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ViewerApp {
|
||||||
|
window: Option<Arc<Window>>,
|
||||||
|
surface: Option<softbuffer::Surface<Arc<Window>, Arc<Window>>>,
|
||||||
|
frame_buffer: Vec<u32>,
|
||||||
|
frame_width: u32,
|
||||||
|
frame_height: u32,
|
||||||
|
viewer_rx: mpsc::Receiver<ViewerEvent>,
|
||||||
|
input_tx: mpsc::Sender<InputEvent>,
|
||||||
|
mouse_x: i32,
|
||||||
|
mouse_y: i32,
|
||||||
|
#[cfg(windows)]
|
||||||
|
keyboard_hook: Option<input::KeyboardHook>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewerApp {
|
||||||
|
fn new(
|
||||||
|
viewer_rx: mpsc::Receiver<ViewerEvent>,
|
||||||
|
input_tx: mpsc::Sender<InputEvent>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
window: None,
|
||||||
|
surface: None,
|
||||||
|
frame_buffer: Vec::new(),
|
||||||
|
frame_width: 0,
|
||||||
|
frame_height: 0,
|
||||||
|
viewer_rx,
|
||||||
|
input_tx,
|
||||||
|
mouse_x: 0,
|
||||||
|
mouse_y: 0,
|
||||||
|
#[cfg(windows)]
|
||||||
|
keyboard_hook: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_frame(&mut self, frame: FrameData) {
|
||||||
|
let data = if frame.compressed {
|
||||||
|
// Decompress zstd
|
||||||
|
match zstd::decode_all(frame.data.as_slice()) {
|
||||||
|
Ok(decompressed) => decompressed,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to decompress frame: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
frame.data
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert BGRA to ARGB (softbuffer expects 0RGB format on little-endian)
|
||||||
|
let pixel_count = (frame.width * frame.height) as usize;
|
||||||
|
if data.len() < pixel_count * 4 {
|
||||||
|
error!("Frame data too small: {} < {}", data.len(), pixel_count * 4);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize frame buffer if needed
|
||||||
|
if self.frame_width != frame.width || self.frame_height != frame.height {
|
||||||
|
self.frame_width = frame.width;
|
||||||
|
self.frame_height = frame.height;
|
||||||
|
self.frame_buffer.resize(pixel_count, 0);
|
||||||
|
|
||||||
|
// Resize window to match frame
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
let _ = window.request_inner_size(LogicalSize::new(frame.width, frame.height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert BGRA to 0RGB (ignore alpha, swap B and R)
|
||||||
|
for i in 0..pixel_count {
|
||||||
|
let offset = i * 4;
|
||||||
|
let b = data[offset] as u32;
|
||||||
|
let g = data[offset + 1] as u32;
|
||||||
|
let r = data[offset + 2] as u32;
|
||||||
|
// 0RGB format: 0x00RRGGBB
|
||||||
|
self.frame_buffer[i] = (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request redraw
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self) {
|
||||||
|
let Some(surface) = &mut self.surface else { return };
|
||||||
|
let Some(window) = &self.window else { return };
|
||||||
|
|
||||||
|
if self.frame_buffer.is_empty() || self.frame_width == 0 || self.frame_height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = window.inner_size();
|
||||||
|
if size.width == 0 || size.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize surface if needed
|
||||||
|
let width = NonZeroU32::new(size.width).unwrap();
|
||||||
|
let height = NonZeroU32::new(size.height).unwrap();
|
||||||
|
|
||||||
|
if let Err(e) = surface.resize(width, height) {
|
||||||
|
error!("Failed to resize surface: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = match surface.buffer_mut() {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to get surface buffer: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simple nearest-neighbor scaling
|
||||||
|
let scale_x = self.frame_width as f32 / size.width as f32;
|
||||||
|
let scale_y = self.frame_height as f32 / size.height as f32;
|
||||||
|
|
||||||
|
for y in 0..size.height {
|
||||||
|
for x in 0..size.width {
|
||||||
|
let src_x = ((x as f32 * scale_x) as u32).min(self.frame_width - 1);
|
||||||
|
let src_y = ((y as f32 * scale_y) as u32).min(self.frame_height - 1);
|
||||||
|
let src_idx = (src_y * self.frame_width + src_x) as usize;
|
||||||
|
let dst_idx = (y * size.width + x) as usize;
|
||||||
|
|
||||||
|
if src_idx < self.frame_buffer.len() && dst_idx < buffer.len() {
|
||||||
|
buffer[dst_idx] = self.frame_buffer[src_idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = buffer.present() {
|
||||||
|
error!("Failed to present buffer: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_mouse_event(&self, event_type: proto::MouseEventType, x: i32, y: i32) {
|
||||||
|
let event = proto::MouseEvent {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
buttons: Some(proto::MouseButtons::default()),
|
||||||
|
wheel_delta_x: 0,
|
||||||
|
wheel_delta_y: 0,
|
||||||
|
event_type: event_type as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = self.input_tx.try_send(InputEvent::Mouse(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_mouse_button(&self, button: MouseButton, state: ElementState) {
|
||||||
|
let event_type = match state {
|
||||||
|
ElementState::Pressed => proto::MouseEventType::MouseDown,
|
||||||
|
ElementState::Released => proto::MouseEventType::MouseUp,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buttons = proto::MouseButtons::default();
|
||||||
|
match button {
|
||||||
|
MouseButton::Left => buttons.left = true,
|
||||||
|
MouseButton::Right => buttons.right = true,
|
||||||
|
MouseButton::Middle => buttons.middle = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = proto::MouseEvent {
|
||||||
|
x: self.mouse_x,
|
||||||
|
y: self.mouse_y,
|
||||||
|
buttons: Some(buttons),
|
||||||
|
wheel_delta_x: 0,
|
||||||
|
wheel_delta_y: 0,
|
||||||
|
event_type: event_type as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = self.input_tx.try_send(InputEvent::Mouse(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_mouse_wheel(&self, delta_x: i32, delta_y: i32) {
|
||||||
|
let event = proto::MouseEvent {
|
||||||
|
x: self.mouse_x,
|
||||||
|
y: self.mouse_y,
|
||||||
|
buttons: Some(proto::MouseButtons::default()),
|
||||||
|
wheel_delta_x: delta_x,
|
||||||
|
wheel_delta_y: delta_y,
|
||||||
|
event_type: proto::MouseEventType::MouseWheel as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = self.input_tx.try_send(InputEvent::Mouse(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_key_event(&self, key: PhysicalKey, state: ElementState) {
|
||||||
|
let vk_code = match key {
|
||||||
|
PhysicalKey::Code(code) => keycode_to_vk(code),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let event = proto::KeyEvent {
|
||||||
|
down: state == ElementState::Pressed,
|
||||||
|
key_type: proto::KeyEventType::KeyVk as i32,
|
||||||
|
vk_code,
|
||||||
|
scan_code: 0,
|
||||||
|
unicode: String::new(),
|
||||||
|
modifiers: Some(proto::Modifiers::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = self.input_tx.try_send(InputEvent::Key(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn screen_to_frame_coords(&self, x: f64, y: f64) -> (i32, i32) {
|
||||||
|
let Some(window) = &self.window else {
|
||||||
|
return (x as i32, y as i32);
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = window.inner_size();
|
||||||
|
if size.width == 0 || size.height == 0 || self.frame_width == 0 || self.frame_height == 0 {
|
||||||
|
return (x as i32, y as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale from window coordinates to frame coordinates
|
||||||
|
let scale_x = self.frame_width as f64 / size.width as f64;
|
||||||
|
let scale_y = self.frame_height as f64 / size.height as f64;
|
||||||
|
|
||||||
|
let frame_x = (x * scale_x) as i32;
|
||||||
|
let frame_y = (y * scale_y) as i32;
|
||||||
|
|
||||||
|
(frame_x, frame_y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for ViewerApp {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.window.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let window_attrs = Window::default_attributes()
|
||||||
|
.with_title("GuruConnect Viewer")
|
||||||
|
.with_inner_size(LogicalSize::new(1280, 720));
|
||||||
|
|
||||||
|
let window = Arc::new(event_loop.create_window(window_attrs).unwrap());
|
||||||
|
|
||||||
|
// Create software rendering surface
|
||||||
|
let context = softbuffer::Context::new(window.clone()).unwrap();
|
||||||
|
let surface = softbuffer::Surface::new(&context, window.clone()).unwrap();
|
||||||
|
|
||||||
|
self.window = Some(window.clone());
|
||||||
|
self.surface = Some(surface);
|
||||||
|
|
||||||
|
// Install keyboard hook
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let input_tx = self.input_tx.clone();
|
||||||
|
match input::KeyboardHook::new(input_tx) {
|
||||||
|
Ok(hook) => {
|
||||||
|
info!("Keyboard hook installed");
|
||||||
|
self.keyboard_hook = Some(hook);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to install keyboard hook: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Window created");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
|
||||||
|
// Check for incoming viewer events (non-blocking)
|
||||||
|
while let Ok(viewer_event) = self.viewer_rx.try_recv() {
|
||||||
|
match viewer_event {
|
||||||
|
ViewerEvent::Frame(frame) => {
|
||||||
|
self.process_frame(frame);
|
||||||
|
}
|
||||||
|
ViewerEvent::Connected => {
|
||||||
|
info!("Connected to remote session");
|
||||||
|
}
|
||||||
|
ViewerEvent::Disconnected(reason) => {
|
||||||
|
warn!("Disconnected: {}", reason);
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
ViewerEvent::CursorPosition(_x, _y, _visible) => {
|
||||||
|
// Could update cursor display here
|
||||||
|
}
|
||||||
|
ViewerEvent::CursorShape(_shape) => {
|
||||||
|
// Could update cursor shape here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
info!("Window close requested");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
self.render();
|
||||||
|
}
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
debug!("Window resized to {}x{}", size.width, size.height);
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::CursorMoved { position, .. } => {
|
||||||
|
let (x, y) = self.screen_to_frame_coords(position.x, position.y);
|
||||||
|
self.mouse_x = x;
|
||||||
|
self.mouse_y = y;
|
||||||
|
self.send_mouse_event(proto::MouseEventType::MouseMove, x, y);
|
||||||
|
}
|
||||||
|
WindowEvent::MouseInput { state, button, .. } => {
|
||||||
|
self.send_mouse_button(button, state);
|
||||||
|
}
|
||||||
|
WindowEvent::MouseWheel { delta, .. } => {
|
||||||
|
let (dx, dy) = match delta {
|
||||||
|
MouseScrollDelta::LineDelta(x, y) => (x as i32 * 120, y as i32 * 120),
|
||||||
|
MouseScrollDelta::PixelDelta(pos) => (pos.x as i32, pos.y as i32),
|
||||||
|
};
|
||||||
|
self.send_mouse_wheel(dx, dy);
|
||||||
|
}
|
||||||
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
|
// Note: This handles keys that aren't captured by the low-level hook
|
||||||
|
// The hook handles Win key and other special keys
|
||||||
|
if !event.repeat {
|
||||||
|
self.send_key_event(event.physical_key, event.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
// Keep checking for events
|
||||||
|
event_loop.set_control_flow(ControlFlow::Poll);
|
||||||
|
|
||||||
|
// Process Windows messages for keyboard hook
|
||||||
|
#[cfg(windows)]
|
||||||
|
input::pump_messages();
|
||||||
|
|
||||||
|
// Request redraw periodically to check for new frames
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the viewer window
|
||||||
|
pub async fn run_window(
|
||||||
|
viewer_rx: mpsc::Receiver<ViewerEvent>,
|
||||||
|
input_tx: mpsc::Sender<InputEvent>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let event_loop = EventLoop::new()?;
|
||||||
|
let mut app = ViewerApp::new(viewer_rx, input_tx);
|
||||||
|
|
||||||
|
event_loop.run_app(&mut app)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert winit KeyCode to Windows virtual key code
|
||||||
|
fn keycode_to_vk(code: KeyCode) -> u32 {
|
||||||
|
match code {
|
||||||
|
// Letters
|
||||||
|
KeyCode::KeyA => 0x41,
|
||||||
|
KeyCode::KeyB => 0x42,
|
||||||
|
KeyCode::KeyC => 0x43,
|
||||||
|
KeyCode::KeyD => 0x44,
|
||||||
|
KeyCode::KeyE => 0x45,
|
||||||
|
KeyCode::KeyF => 0x46,
|
||||||
|
KeyCode::KeyG => 0x47,
|
||||||
|
KeyCode::KeyH => 0x48,
|
||||||
|
KeyCode::KeyI => 0x49,
|
||||||
|
KeyCode::KeyJ => 0x4A,
|
||||||
|
KeyCode::KeyK => 0x4B,
|
||||||
|
KeyCode::KeyL => 0x4C,
|
||||||
|
KeyCode::KeyM => 0x4D,
|
||||||
|
KeyCode::KeyN => 0x4E,
|
||||||
|
KeyCode::KeyO => 0x4F,
|
||||||
|
KeyCode::KeyP => 0x50,
|
||||||
|
KeyCode::KeyQ => 0x51,
|
||||||
|
KeyCode::KeyR => 0x52,
|
||||||
|
KeyCode::KeyS => 0x53,
|
||||||
|
KeyCode::KeyT => 0x54,
|
||||||
|
KeyCode::KeyU => 0x55,
|
||||||
|
KeyCode::KeyV => 0x56,
|
||||||
|
KeyCode::KeyW => 0x57,
|
||||||
|
KeyCode::KeyX => 0x58,
|
||||||
|
KeyCode::KeyY => 0x59,
|
||||||
|
KeyCode::KeyZ => 0x5A,
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
KeyCode::Digit0 => 0x30,
|
||||||
|
KeyCode::Digit1 => 0x31,
|
||||||
|
KeyCode::Digit2 => 0x32,
|
||||||
|
KeyCode::Digit3 => 0x33,
|
||||||
|
KeyCode::Digit4 => 0x34,
|
||||||
|
KeyCode::Digit5 => 0x35,
|
||||||
|
KeyCode::Digit6 => 0x36,
|
||||||
|
KeyCode::Digit7 => 0x37,
|
||||||
|
KeyCode::Digit8 => 0x38,
|
||||||
|
KeyCode::Digit9 => 0x39,
|
||||||
|
|
||||||
|
// Function keys
|
||||||
|
KeyCode::F1 => 0x70,
|
||||||
|
KeyCode::F2 => 0x71,
|
||||||
|
KeyCode::F3 => 0x72,
|
||||||
|
KeyCode::F4 => 0x73,
|
||||||
|
KeyCode::F5 => 0x74,
|
||||||
|
KeyCode::F6 => 0x75,
|
||||||
|
KeyCode::F7 => 0x76,
|
||||||
|
KeyCode::F8 => 0x77,
|
||||||
|
KeyCode::F9 => 0x78,
|
||||||
|
KeyCode::F10 => 0x79,
|
||||||
|
KeyCode::F11 => 0x7A,
|
||||||
|
KeyCode::F12 => 0x7B,
|
||||||
|
|
||||||
|
// Special keys
|
||||||
|
KeyCode::Escape => 0x1B,
|
||||||
|
KeyCode::Tab => 0x09,
|
||||||
|
KeyCode::CapsLock => 0x14,
|
||||||
|
KeyCode::ShiftLeft => 0x10,
|
||||||
|
KeyCode::ShiftRight => 0x10,
|
||||||
|
KeyCode::ControlLeft => 0x11,
|
||||||
|
KeyCode::ControlRight => 0x11,
|
||||||
|
KeyCode::AltLeft => 0x12,
|
||||||
|
KeyCode::AltRight => 0x12,
|
||||||
|
KeyCode::Space => 0x20,
|
||||||
|
KeyCode::Enter => 0x0D,
|
||||||
|
KeyCode::Backspace => 0x08,
|
||||||
|
KeyCode::Delete => 0x2E,
|
||||||
|
KeyCode::Insert => 0x2D,
|
||||||
|
KeyCode::Home => 0x24,
|
||||||
|
KeyCode::End => 0x23,
|
||||||
|
KeyCode::PageUp => 0x21,
|
||||||
|
KeyCode::PageDown => 0x22,
|
||||||
|
|
||||||
|
// Arrow keys
|
||||||
|
KeyCode::ArrowUp => 0x26,
|
||||||
|
KeyCode::ArrowDown => 0x28,
|
||||||
|
KeyCode::ArrowLeft => 0x25,
|
||||||
|
KeyCode::ArrowRight => 0x27,
|
||||||
|
|
||||||
|
// Numpad
|
||||||
|
KeyCode::NumLock => 0x90,
|
||||||
|
KeyCode::Numpad0 => 0x60,
|
||||||
|
KeyCode::Numpad1 => 0x61,
|
||||||
|
KeyCode::Numpad2 => 0x62,
|
||||||
|
KeyCode::Numpad3 => 0x63,
|
||||||
|
KeyCode::Numpad4 => 0x64,
|
||||||
|
KeyCode::Numpad5 => 0x65,
|
||||||
|
KeyCode::Numpad6 => 0x66,
|
||||||
|
KeyCode::Numpad7 => 0x67,
|
||||||
|
KeyCode::Numpad8 => 0x68,
|
||||||
|
KeyCode::Numpad9 => 0x69,
|
||||||
|
KeyCode::NumpadAdd => 0x6B,
|
||||||
|
KeyCode::NumpadSubtract => 0x6D,
|
||||||
|
KeyCode::NumpadMultiply => 0x6A,
|
||||||
|
KeyCode::NumpadDivide => 0x6F,
|
||||||
|
KeyCode::NumpadDecimal => 0x6E,
|
||||||
|
KeyCode::NumpadEnter => 0x0D,
|
||||||
|
|
||||||
|
// Punctuation
|
||||||
|
KeyCode::Semicolon => 0xBA,
|
||||||
|
KeyCode::Equal => 0xBB,
|
||||||
|
KeyCode::Comma => 0xBC,
|
||||||
|
KeyCode::Minus => 0xBD,
|
||||||
|
KeyCode::Period => 0xBE,
|
||||||
|
KeyCode::Slash => 0xBF,
|
||||||
|
KeyCode::Backquote => 0xC0,
|
||||||
|
KeyCode::BracketLeft => 0xDB,
|
||||||
|
KeyCode::Backslash => 0xDC,
|
||||||
|
KeyCode::BracketRight => 0xDD,
|
||||||
|
KeyCode::Quote => 0xDE,
|
||||||
|
|
||||||
|
// Other
|
||||||
|
KeyCode::PrintScreen => 0x2C,
|
||||||
|
KeyCode::ScrollLock => 0x91,
|
||||||
|
KeyCode::Pause => 0x13,
|
||||||
|
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
100
viewer/src/transport.rs
Normal file
100
viewer/src/transport.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//! WebSocket transport for viewer-server communication
|
||||||
|
|
||||||
|
use crate::proto;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use prost::Message as ProstMessage;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tokio_tungstenite::{
|
||||||
|
connect_async,
|
||||||
|
tungstenite::protocol::Message as WsMessage,
|
||||||
|
MaybeTlsStream, WebSocketStream,
|
||||||
|
};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
|
pub type WsSender = futures_util::stream::SplitSink<
|
||||||
|
WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||||
|
WsMessage,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub type WsReceiver = futures_util::stream::SplitStream<
|
||||||
|
WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
/// Receiver wrapper that parses protobuf messages
|
||||||
|
pub struct MessageReceiver {
|
||||||
|
inner: WsReceiver,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageReceiver {
|
||||||
|
pub async fn recv(&mut self) -> Option<proto::Message> {
|
||||||
|
loop {
|
||||||
|
match self.inner.next().await {
|
||||||
|
Some(Ok(WsMessage::Binary(data))) => {
|
||||||
|
match proto::Message::decode(Bytes::from(data)) {
|
||||||
|
Ok(msg) => return Some(msg),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to decode message: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(WsMessage::Close(_))) => {
|
||||||
|
debug!("WebSocket closed");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Ok(WsMessage::Ping(_))) => {
|
||||||
|
trace!("Received ping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Some(Ok(WsMessage::Pong(_))) => {
|
||||||
|
trace!("Received pong");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Some(Ok(_)) => continue,
|
||||||
|
Some(Err(e)) => {
|
||||||
|
error!("WebSocket error: {}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
None => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to the GuruConnect server
|
||||||
|
pub async fn connect(url: &str, api_key: &str) -> Result<(WsSender, MessageReceiver)> {
|
||||||
|
// Add API key to URL
|
||||||
|
let full_url = if url.contains('?') {
|
||||||
|
format!("{}&api_key={}", url, api_key)
|
||||||
|
} else {
|
||||||
|
format!("{}?api_key={}", url, api_key)
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("Connecting to {}", full_url);
|
||||||
|
|
||||||
|
let (ws_stream, _) = connect_async(&full_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("Failed to connect: {}", e))?;
|
||||||
|
|
||||||
|
let (sender, receiver) = ws_stream.split();
|
||||||
|
|
||||||
|
Ok((sender, MessageReceiver { inner: receiver }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a protobuf message over the WebSocket
|
||||||
|
pub async fn send_message(
|
||||||
|
sender: &Arc<Mutex<WsSender>>,
|
||||||
|
msg: &proto::Message,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut buf = Vec::with_capacity(msg.encoded_len());
|
||||||
|
msg.encode(&mut buf)?;
|
||||||
|
|
||||||
|
let mut sender = sender.lock().await;
|
||||||
|
sender.send(WsMessage::Binary(buf)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user