Created comprehensive VPN setup tooling for Peaceful Spirit L2TP/IPsec connection and enhanced agent documentation framework. VPN Configuration (PST-NW-VPN): - Setup-PST-L2TP-VPN.ps1: Automated L2TP/IPsec setup with split-tunnel and DNS - Connect-PST-VPN.ps1: Connection helper with PPP adapter detection, DNS (192.168.0.2), and route config (192.168.0.0/24) - Connect-PST-VPN-Standalone.ps1: Self-contained connection script for remote deployment - Fix-PST-VPN-Auth.ps1: Authentication troubleshooting for CHAP/MSChapv2 - Diagnose-VPN-Interface.ps1: Comprehensive VPN interface and routing diagnostic - Quick-Test-VPN.ps1: Fast connectivity verification (DNS/router/routes) - Add-PST-VPN-Route-Manual.ps1: Manual route configuration helper - vpn-connect.bat, vpn-disconnect.bat: Simple batch file shortcuts - OpenVPN config files (Windows-compatible, abandoned for L2TP) Key VPN Implementation Details: - L2TP creates PPP adapter with connection name as interface description - UniFi auto-configures DNS (192.168.0.2) but requires manual route to 192.168.0.0/24 - Split-tunnel enabled (only remote traffic through VPN) - All-user connection for pre-login auto-connect via scheduled task - Authentication: CHAP + MSChapv2 for UniFi compatibility Agent Documentation: - AGENT_QUICK_REFERENCE.md: Quick reference for all specialized agents - documentation-squire.md: Documentation and task management specialist agent - Updated all agent markdown files with standardized formatting Project Organization: - Moved conversation logs to dedicated directories (guru-connect-conversation-logs, guru-rmm-conversation-logs) - Cleaned up old session JSONL files from projects/msp-tools/ - Added guru-connect infrastructure (agent, dashboard, proto, scripts, .gitea workflows) - Added guru-rmm server components and deployment configs Technical Notes: - VPN IP pool: 192.168.4.x (client gets 192.168.4.6) - Remote network: 192.168.0.0/24 (router at 192.168.0.10) - PSK: rrClvnmUeXEFo90Ol+z7tfsAZHeSK6w7 - Credentials: pst-admin / 24Hearts$ Files: 15 VPN scripts, 2 agent docs, conversation log reorganization, guru-connect/guru-rmm infrastructure additions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
509 lines
16 KiB
Rust
509 lines
16 KiB
Rust
//! Window rendering and frame display
|
|
|
|
use super::{ViewerEvent, InputEvent};
|
|
use crate::proto;
|
|
#[cfg(windows)]
|
|
use super::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,
|
|
}
|
|
}
|