style: cargo fmt --all — make codebase rustfmt-clean
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 2m59s
Build and Test / Build Agent (Windows) (push) Has started running
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 2m59s
Build and Test / Build Agent (Windows) (push) Has started running
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
First run of the build-and-test CI gate (cargo fmt --all -- --check) surfaced pre-existing formatting drift across the agent and server crates. Apply rustfmt across the workspace so the codebase meets its own CI gate. Pure formatting; no logic changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,8 @@ use windows_service::{
|
||||
// Service configuration
|
||||
const SERVICE_NAME: &str = "GuruConnectSAS";
|
||||
const SERVICE_DISPLAY_NAME: &str = "GuruConnect SAS Service";
|
||||
const SERVICE_DESCRIPTION: &str = "Handles Secure Attention Sequence (Ctrl+Alt+Del) for GuruConnect remote sessions";
|
||||
const SERVICE_DESCRIPTION: &str =
|
||||
"Handles Secure Attention Sequence (Ctrl+Alt+Del) for GuruConnect remote sessions";
|
||||
const PIPE_NAME: &str = r"\\.\pipe\guruconnect-sas";
|
||||
const INSTALL_DIR: &str = r"C:\Program Files\GuruConnect";
|
||||
|
||||
@@ -360,18 +361,16 @@ fn run_pipe_server() -> Result<()> {
|
||||
tracing::info!("Received command: {}", command);
|
||||
|
||||
let response = match command {
|
||||
"sas" => {
|
||||
match send_sas() {
|
||||
Ok(()) => {
|
||||
tracing::info!("SendSAS executed successfully");
|
||||
"ok\n"
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("SendSAS failed: {}", e);
|
||||
"error\n"
|
||||
}
|
||||
"sas" => match send_sas() {
|
||||
Ok(()) => {
|
||||
tracing::info!("SendSAS executed successfully");
|
||||
"ok\n"
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("SendSAS failed: {}", e);
|
||||
"error\n"
|
||||
}
|
||||
},
|
||||
"ping" => {
|
||||
tracing::info!("Ping received");
|
||||
"pong\n"
|
||||
@@ -432,7 +431,8 @@ fn install_service() -> Result<()> {
|
||||
// Get current executable path
|
||||
let current_exe = std::env::current_exe().context("Failed to get current executable")?;
|
||||
|
||||
let binary_dest = std::path::PathBuf::from(format!(r"{}\\guruconnect-sas-service.exe", INSTALL_DIR));
|
||||
let binary_dest =
|
||||
std::path::PathBuf::from(format!(r"{}\\guruconnect-sas-service.exe", INSTALL_DIR));
|
||||
|
||||
// Create install directory
|
||||
std::fs::create_dir_all(INSTALL_DIR).context("Failed to create install directory")?;
|
||||
@@ -462,7 +462,9 @@ fn install_service() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
service.delete().context("Failed to delete existing service")?;
|
||||
service
|
||||
.delete()
|
||||
.context("Failed to delete existing service")?;
|
||||
drop(service);
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
}
|
||||
@@ -482,7 +484,10 @@ fn install_service() -> Result<()> {
|
||||
};
|
||||
|
||||
let service = manager
|
||||
.create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START)
|
||||
.create_service(
|
||||
&service_info,
|
||||
ServiceAccess::CHANGE_CONFIG | ServiceAccess::START,
|
||||
)
|
||||
.context("Failed to create service")?;
|
||||
|
||||
// Set description
|
||||
@@ -514,13 +519,11 @@ fn install_service() -> Result<()> {
|
||||
fn uninstall_service() -> Result<()> {
|
||||
println!("Uninstalling GuruConnect SAS Service...");
|
||||
|
||||
let binary_path = std::path::PathBuf::from(format!(r"{}\\guruconnect-sas-service.exe", INSTALL_DIR));
|
||||
let binary_path =
|
||||
std::path::PathBuf::from(format!(r"{}\\guruconnect-sas-service.exe", INSTALL_DIR));
|
||||
|
||||
let manager = ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::CONNECT,
|
||||
)
|
||||
.context("Failed to connect to Service Control Manager. Run as Administrator.")?;
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
|
||||
.context("Failed to connect to Service Control Manager. Run as Administrator.")?;
|
||||
|
||||
match manager.open_service(
|
||||
SERVICE_NAME,
|
||||
@@ -558,17 +561,19 @@ fn uninstall_service() -> Result<()> {
|
||||
|
||||
/// Start the service
|
||||
fn start_service() -> Result<()> {
|
||||
let manager = ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::CONNECT,
|
||||
)
|
||||
.context("Failed to connect to Service Control Manager")?;
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
|
||||
.context("Failed to connect to Service Control Manager")?;
|
||||
|
||||
let service = manager
|
||||
.open_service(SERVICE_NAME, ServiceAccess::START | ServiceAccess::QUERY_STATUS)
|
||||
.open_service(
|
||||
SERVICE_NAME,
|
||||
ServiceAccess::START | ServiceAccess::QUERY_STATUS,
|
||||
)
|
||||
.context("Failed to open service. Is it installed?")?;
|
||||
|
||||
service.start::<String>(&[]).context("Failed to start service")?;
|
||||
service
|
||||
.start::<String>(&[])
|
||||
.context("Failed to start service")?;
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
@@ -584,14 +589,14 @@ fn start_service() -> Result<()> {
|
||||
|
||||
/// Stop the service
|
||||
fn stop_service() -> Result<()> {
|
||||
let manager = ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::CONNECT,
|
||||
)
|
||||
.context("Failed to connect to Service Control Manager")?;
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
|
||||
.context("Failed to connect to Service Control Manager")?;
|
||||
|
||||
let service = manager
|
||||
.open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::QUERY_STATUS)
|
||||
.open_service(
|
||||
SERVICE_NAME,
|
||||
ServiceAccess::STOP | ServiceAccess::QUERY_STATUS,
|
||||
)
|
||||
.context("Failed to open service")?;
|
||||
|
||||
service.stop().context("Failed to stop service")?;
|
||||
@@ -610,11 +615,8 @@ fn stop_service() -> Result<()> {
|
||||
|
||||
/// Query service status
|
||||
fn query_status() -> Result<()> {
|
||||
let manager = ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::CONNECT,
|
||||
)
|
||||
.context("Failed to connect to Service Control Manager")?;
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
|
||||
.context("Failed to connect to Service Control Manager")?;
|
||||
|
||||
match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) {
|
||||
Ok(service) => {
|
||||
|
||||
@@ -53,11 +53,11 @@ impl Display {
|
||||
/// Enumerate all connected displays
|
||||
#[cfg(windows)]
|
||||
pub fn enumerate_displays() -> Result<Vec<Display>> {
|
||||
use std::mem;
|
||||
use windows::Win32::Foundation::{BOOL, LPARAM, RECT};
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
EnumDisplayMonitors, GetMonitorInfoW, HMONITOR, MONITORINFOEXW,
|
||||
};
|
||||
use windows::Win32::Foundation::{BOOL, LPARAM, RECT};
|
||||
use std::mem;
|
||||
|
||||
let mut displays = Vec::new();
|
||||
let mut display_id = 0u32;
|
||||
@@ -98,7 +98,11 @@ pub fn enumerate_displays() -> Result<Vec<Display>> {
|
||||
if GetMonitorInfoW(hmonitor, &mut info.monitorInfo as *mut _ as *mut _).as_bool() {
|
||||
let rect = info.monitorInfo.rcMonitor;
|
||||
let name = String::from_utf16_lossy(
|
||||
&info.szDevice[..info.szDevice.iter().position(|&c| c == 0).unwrap_or(info.szDevice.len())]
|
||||
&info.szDevice[..info
|
||||
.szDevice
|
||||
.iter()
|
||||
.position(|&c| c == 0)
|
||||
.unwrap_or(info.szDevice.len())],
|
||||
);
|
||||
|
||||
let is_primary = (info.monitorInfo.dwFlags & 1) != 0; // MONITORINFOF_PRIMARY
|
||||
|
||||
@@ -10,19 +10,18 @@ use anyhow::{Context, Result};
|
||||
use std::ptr;
|
||||
use std::time::Instant;
|
||||
|
||||
use windows::core::Interface;
|
||||
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN;
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
|
||||
D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
|
||||
D3D11_USAGE_STAGING, D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_READ,
|
||||
D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_READ, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
|
||||
D3D11_USAGE_STAGING,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::{
|
||||
CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIOutput, IDXGIOutput1,
|
||||
IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_ACCESS_LOST,
|
||||
DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO,
|
||||
DXGI_RESOURCE_PRIORITY_MAXIMUM,
|
||||
IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_WAIT_TIMEOUT,
|
||||
DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO, DXGI_RESOURCE_PRIORITY_MAXIMUM,
|
||||
};
|
||||
use windows::core::Interface;
|
||||
|
||||
/// DXGI Desktop Duplication capturer
|
||||
pub struct DxgiCapturer {
|
||||
@@ -56,11 +55,16 @@ impl DxgiCapturer {
|
||||
/// Create D3D device and output duplication
|
||||
fn create_duplication(
|
||||
target_display: &Display,
|
||||
) -> Result<(ID3D11Device, ID3D11DeviceContext, IDXGIOutputDuplication, DXGI_OUTDUPL_DESC)> {
|
||||
) -> Result<(
|
||||
ID3D11Device,
|
||||
ID3D11DeviceContext,
|
||||
IDXGIOutputDuplication,
|
||||
DXGI_OUTDUPL_DESC,
|
||||
)> {
|
||||
unsafe {
|
||||
// Create DXGI factory
|
||||
let factory: IDXGIFactory1 = CreateDXGIFactory1()
|
||||
.context("Failed to create DXGI factory")?;
|
||||
let factory: IDXGIFactory1 =
|
||||
CreateDXGIFactory1().context("Failed to create DXGI factory")?;
|
||||
|
||||
// Find the adapter and output for this display
|
||||
let (adapter, output) = Self::find_adapter_output(&factory, target_display)?;
|
||||
@@ -86,11 +90,13 @@ impl DxgiCapturer {
|
||||
let context = context.context("D3D11 context is None")?;
|
||||
|
||||
// Get IDXGIOutput1 interface
|
||||
let output1: IDXGIOutput1 = output.cast()
|
||||
let output1: IDXGIOutput1 = output
|
||||
.cast()
|
||||
.context("Failed to get IDXGIOutput1 interface")?;
|
||||
|
||||
// Create output duplication
|
||||
let duplication = output1.DuplicateOutput(&device)
|
||||
let duplication = output1
|
||||
.DuplicateOutput(&device)
|
||||
.context("Failed to create output duplication")?;
|
||||
|
||||
// Get duplication description
|
||||
@@ -135,7 +141,11 @@ impl DxgiCapturer {
|
||||
let desc = output.GetDesc()?;
|
||||
|
||||
let name = String::from_utf16_lossy(
|
||||
&desc.DeviceName[..desc.DeviceName.iter().position(|&c| c == 0).unwrap_or(desc.DeviceName.len())]
|
||||
&desc.DeviceName[..desc
|
||||
.DeviceName
|
||||
.iter()
|
||||
.position(|&c| c == 0)
|
||||
.unwrap_or(desc.DeviceName.len())],
|
||||
);
|
||||
|
||||
if name == display.name || desc.Monitor.0 as isize == display.handle {
|
||||
@@ -149,10 +159,8 @@ impl DxgiCapturer {
|
||||
}
|
||||
|
||||
// If we didn't find the specific display, use the first one
|
||||
let adapter: IDXGIAdapter1 = factory.EnumAdapters1(0)
|
||||
.context("No adapters found")?;
|
||||
let output: IDXGIOutput = adapter.EnumOutputs(0)
|
||||
.context("No outputs found")?;
|
||||
let adapter: IDXGIAdapter1 = factory.EnumAdapters1(0).context("No adapters found")?;
|
||||
let output: IDXGIOutput = adapter.EnumOutputs(0).context("No outputs found")?;
|
||||
|
||||
Ok((adapter, output))
|
||||
}
|
||||
@@ -171,7 +179,8 @@ impl DxgiCapturer {
|
||||
desc.MiscFlags = Default::default();
|
||||
|
||||
let mut staging: Option<ID3D11Texture2D> = None;
|
||||
self.device.CreateTexture2D(&desc, None, Some(&mut staging))
|
||||
self.device
|
||||
.CreateTexture2D(&desc, None, Some(&mut staging))
|
||||
.context("Failed to create staging texture")?;
|
||||
|
||||
let staging = staging.context("Staging texture is None")?;
|
||||
@@ -188,7 +197,10 @@ impl DxgiCapturer {
|
||||
}
|
||||
|
||||
/// Acquire the next frame from the desktop
|
||||
fn acquire_frame(&mut self, timeout_ms: u32) -> Result<Option<(ID3D11Texture2D, DXGI_OUTDUPL_FRAME_INFO)>> {
|
||||
fn acquire_frame(
|
||||
&mut self,
|
||||
timeout_ms: u32,
|
||||
) -> Result<Option<(ID3D11Texture2D, DXGI_OUTDUPL_FRAME_INFO)>> {
|
||||
unsafe {
|
||||
let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
|
||||
let mut desktop_resource: Option<IDXGIResource> = None;
|
||||
@@ -209,7 +221,8 @@ impl DxgiCapturer {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let texture: ID3D11Texture2D = resource.cast()
|
||||
let texture: ID3D11Texture2D = resource
|
||||
.cast()
|
||||
.context("Failed to cast to ID3D11Texture2D")?;
|
||||
|
||||
Ok(Some((texture, frame_info)))
|
||||
@@ -223,9 +236,7 @@ impl DxgiCapturer {
|
||||
tracing::warn!("Desktop duplication access lost, will need to recreate");
|
||||
Err(anyhow::anyhow!("Access lost"))
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e).context("Failed to acquire frame")
|
||||
}
|
||||
Err(e) => Err(e).context("Failed to acquire frame"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@ use super::{CapturedFrame, Capturer, Display};
|
||||
use anyhow::Result;
|
||||
use std::time::Instant;
|
||||
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject,
|
||||
GetDIBits, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS,
|
||||
SRCCOPY, GetDC, ReleaseDC,
|
||||
};
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetDIBits,
|
||||
ReleaseDC, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, SRCCOPY,
|
||||
};
|
||||
|
||||
/// GDI-based screen capturer
|
||||
pub struct GdiCapturer {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
//! Provides DXGI Desktop Duplication for high-performance screen capture on Windows 8+,
|
||||
//! with GDI fallback for legacy systems or edge cases.
|
||||
|
||||
mod display;
|
||||
#[cfg(windows)]
|
||||
mod dxgi;
|
||||
#[cfg(windows)]
|
||||
mod gdi;
|
||||
mod display;
|
||||
|
||||
pub use display::{Display, DisplayInfo};
|
||||
|
||||
@@ -61,7 +61,11 @@ pub trait Capturer: Send {
|
||||
|
||||
/// Create a capturer for the specified display
|
||||
#[cfg(windows)]
|
||||
pub fn create_capturer(display: Display, use_dxgi: bool, gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
|
||||
pub fn create_capturer(
|
||||
display: Display,
|
||||
use_dxgi: bool,
|
||||
gdi_fallback: bool,
|
||||
) -> Result<Box<dyn Capturer>> {
|
||||
if use_dxgi {
|
||||
match dxgi::DxgiCapturer::new(display.clone()) {
|
||||
Ok(capturer) => {
|
||||
@@ -83,7 +87,11 @@ pub fn create_capturer(display: Display, use_dxgi: bool, gdi_fallback: bool) ->
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn create_capturer(_display: Display, _use_dxgi: bool, _gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
|
||||
pub fn create_capturer(
|
||||
_display: Display,
|
||||
_use_dxgi: bool,
|
||||
_gdi_fallback: bool,
|
||||
) -> Result<Box<dyn Capturer>> {
|
||||
anyhow::bail!("Screen capture only supported on Windows")
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use tracing::{info, warn, error};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::WindowsAndMessaging::*;
|
||||
use windows::core::PCWSTR;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::Foundation::*;
|
||||
#[cfg(windows)]
|
||||
@@ -17,7 +17,7 @@ use windows::Win32::Graphics::Gdi::*;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
#[cfg(windows)]
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::UI::WindowsAndMessaging::*;
|
||||
|
||||
/// A chat message
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -221,11 +221,10 @@ impl Config {
|
||||
|
||||
/// Read embedded configuration from the executable
|
||||
pub fn read_embedded_config() -> Result<EmbeddedConfig> {
|
||||
let exe_path = std::env::current_exe()
|
||||
.context("Failed to get current executable path")?;
|
||||
let exe_path = std::env::current_exe().context("Failed to get current executable path")?;
|
||||
|
||||
let mut file = std::fs::File::open(&exe_path)
|
||||
.context("Failed to open executable for reading")?;
|
||||
let mut file =
|
||||
std::fs::File::open(&exe_path).context("Failed to open executable for reading")?;
|
||||
|
||||
let file_size = file.metadata()?.len();
|
||||
if file_size < (MAGIC_MARKER.len() + 4) as u64 {
|
||||
@@ -245,7 +244,8 @@ impl Config {
|
||||
file.read_exact(&mut buffer)?;
|
||||
|
||||
// Find magic marker
|
||||
let marker_pos = buffer.windows(MAGIC_MARKER.len())
|
||||
let marker_pos = buffer
|
||||
.windows(MAGIC_MARKER.len())
|
||||
.rposition(|window| window == MAGIC_MARKER)
|
||||
.ok_or_else(|| anyhow!("Magic marker not found"))?;
|
||||
|
||||
@@ -269,11 +269,13 @@ impl Config {
|
||||
}
|
||||
|
||||
let config_bytes = &buffer[config_start..config_start + config_length];
|
||||
let config: EmbeddedConfig = serde_json::from_slice(config_bytes)
|
||||
.context("Failed to parse embedded config JSON")?;
|
||||
let config: EmbeddedConfig =
|
||||
serde_json::from_slice(config_bytes).context("Failed to parse embedded config JSON")?;
|
||||
|
||||
info!("Loaded embedded config: server={}, company={:?}",
|
||||
config.server_url, config.company);
|
||||
info!(
|
||||
"Loaded embedded config: server={}, company={:?}",
|
||||
config.server_url, config.company
|
||||
);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
@@ -338,8 +340,8 @@ impl Config {
|
||||
let contents = std::fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("Failed to read config from {:?}", config_path))?;
|
||||
|
||||
let mut config: Config = toml::from_str(&contents)
|
||||
.with_context(|| "Failed to parse config file")?;
|
||||
let mut config: Config =
|
||||
toml::from_str(&contents).with_context(|| "Failed to parse config file")?;
|
||||
|
||||
// Ensure agent_id is set and saved
|
||||
if config.agent_id.is_empty() {
|
||||
@@ -357,11 +359,11 @@ impl Config {
|
||||
let server_url = std::env::var("GURUCONNECT_SERVER_URL")
|
||||
.unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());
|
||||
|
||||
let api_key = std::env::var("GURUCONNECT_API_KEY")
|
||||
.unwrap_or_else(|_| "dev-key".to_string());
|
||||
let api_key =
|
||||
std::env::var("GURUCONNECT_API_KEY").unwrap_or_else(|_| "dev-key".to_string());
|
||||
|
||||
let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
|
||||
.unwrap_or_else(|_| generate_agent_id());
|
||||
let agent_id =
|
||||
std::env::var("GURUCONNECT_AGENT_ID").unwrap_or_else(|_| generate_agent_id());
|
||||
|
||||
let config = Config {
|
||||
server_url,
|
||||
@@ -409,13 +411,11 @@ impl Config {
|
||||
|
||||
/// Get the hostname to use
|
||||
pub fn hostname(&self) -> String {
|
||||
self.hostname_override
|
||||
.clone()
|
||||
.unwrap_or_else(|| {
|
||||
hostname::get()
|
||||
.map(|h| h.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
})
|
||||
self.hostname_override.clone().unwrap_or_else(|| {
|
||||
hostname::get()
|
||||
.map(|h| h.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Save current configuration to file
|
||||
|
||||
@@ -10,7 +10,7 @@ mod raw;
|
||||
pub use raw::RawEncoder;
|
||||
|
||||
use crate::capture::CapturedFrame;
|
||||
use crate::proto::{VideoFrame, RawFrame, DirtyRect as ProtoDirtyRect};
|
||||
use crate::proto::{DirtyRect as ProtoDirtyRect, RawFrame, VideoFrame};
|
||||
use anyhow::Result;
|
||||
|
||||
/// Encoded frame ready for transmission
|
||||
|
||||
@@ -122,12 +122,7 @@ impl RawEncoder {
|
||||
}
|
||||
|
||||
/// Extract pixels for dirty rectangles only
|
||||
fn extract_dirty_pixels(
|
||||
&self,
|
||||
data: &[u8],
|
||||
width: u32,
|
||||
dirty_rects: &[DirtyRect],
|
||||
) -> Vec<u8> {
|
||||
fn extract_dirty_pixels(&self, data: &[u8], width: u32, dirty_rects: &[DirtyRect]) -> Vec<u8> {
|
||||
let stride = (width * 4) as usize;
|
||||
let mut pixels = Vec::new();
|
||||
|
||||
@@ -174,7 +169,8 @@ impl Encoder for RawEncoder {
|
||||
if dirty_rects.len() > 50 {
|
||||
(frame.data.clone(), Vec::new(), true)
|
||||
} else {
|
||||
let dirty_pixels = self.extract_dirty_pixels(&frame.data, frame.width, &dirty_rects);
|
||||
let dirty_pixels =
|
||||
self.extract_dirty_pixels(&frame.data, frame.width, &dirty_rects);
|
||||
(dirty_pixels, dirty_rects, false)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -4,9 +4,9 @@ use anyhow::Result;
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYEVENTF_EXTENDEDKEY,
|
||||
KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, KEYEVENTF_UNICODE, KEYBDINPUT,
|
||||
MapVirtualKeyW, MAPVK_VK_TO_VSC_EX,
|
||||
MapVirtualKeyW, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYBD_EVENT_FLAGS,
|
||||
KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, KEYEVENTF_UNICODE,
|
||||
MAPVK_VK_TO_VSC_EX,
|
||||
};
|
||||
|
||||
/// Keyboard input controller
|
||||
@@ -144,8 +144,8 @@ impl KeyboardController {
|
||||
tracing::info!("SAS service not available, trying direct sas.dll...");
|
||||
|
||||
// Tier 2: Try using the sas.dll directly (requires SYSTEM privileges)
|
||||
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
|
||||
|
||||
unsafe {
|
||||
let dll_name: Vec<u16> = "sas.dll\0".encode_utf16().collect();
|
||||
@@ -195,7 +195,7 @@ impl KeyboardController {
|
||||
0x5D | // Applications key
|
||||
0x6F | // Numpad Divide
|
||||
0x90 | // Num Lock
|
||||
0x91 // Scroll Lock
|
||||
0x91 // Scroll Lock
|
||||
)
|
||||
}
|
||||
|
||||
@@ -205,11 +205,7 @@ impl KeyboardController {
|
||||
let sent = unsafe { SendInput(inputs, std::mem::size_of::<INPUT>() as i32) };
|
||||
|
||||
if sent as usize != inputs.len() {
|
||||
anyhow::bail!(
|
||||
"SendInput failed: sent {} of {} inputs",
|
||||
sent,
|
||||
inputs.len()
|
||||
);
|
||||
anyhow::bail!("SendInput failed: sent {} of {} inputs", sent, inputs.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -250,7 +246,7 @@ pub mod vk {
|
||||
pub const ESCAPE: u16 = 0x1B;
|
||||
pub const SPACE: u16 = 0x20;
|
||||
pub const PRIOR: u16 = 0x21; // Page Up
|
||||
pub const NEXT: u16 = 0x22; // Page Down
|
||||
pub const NEXT: u16 = 0x22; // Page Down
|
||||
pub const END: u16 = 0x23;
|
||||
pub const HOME: u16 = 0x24;
|
||||
pub const LEFT: u16 = 0x25;
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
//!
|
||||
//! Handles mouse and keyboard input simulation using Windows SendInput API.
|
||||
|
||||
mod mouse;
|
||||
mod keyboard;
|
||||
mod mouse;
|
||||
|
||||
pub use mouse::MouseController;
|
||||
pub use keyboard::KeyboardController;
|
||||
pub use mouse::MouseController;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ const XBUTTON2: u32 = 0x0002;
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
|
||||
SM_YVIRTUALSCREEN,
|
||||
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
|
||||
};
|
||||
|
||||
/// Mouse input controller
|
||||
@@ -190,9 +189,7 @@ impl MouseController {
|
||||
/// Send input events
|
||||
#[cfg(windows)]
|
||||
fn send_input(&self, inputs: &[INPUT]) -> Result<()> {
|
||||
let sent = unsafe {
|
||||
SendInput(inputs, std::mem::size_of::<INPUT>() as i32)
|
||||
};
|
||||
let sent = unsafe { SendInput(inputs, std::mem::size_of::<INPUT>() as i32) };
|
||||
|
||||
if sent as usize != inputs.len() {
|
||||
anyhow::bail!("SendInput failed: sent {} of {} inputs", sent, inputs.len());
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
//! - UAC elevation with graceful fallback
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use tracing::{info, warn, error};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::{
|
||||
core::PCWSTR,
|
||||
Win32::Foundation::HANDLE,
|
||||
Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
|
||||
Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
|
||||
Win32::System::Registry::{
|
||||
RegCreateKeyExW, RegSetValueExW, RegCloseKey, HKEY, HKEY_CLASSES_ROOT,
|
||||
HKEY_CURRENT_USER, KEY_WRITE, REG_SZ, REG_OPTION_NON_VOLATILE,
|
||||
RegCloseKey, RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER,
|
||||
KEY_WRITE, REG_OPTION_NON_VOLATILE, REG_SZ,
|
||||
},
|
||||
Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
|
||||
Win32::UI::Shell::ShellExecuteW,
|
||||
Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
|
||||
};
|
||||
@@ -67,11 +67,10 @@ pub fn get_install_path(elevated: bool) -> std::path::PathBuf {
|
||||
if elevated {
|
||||
std::path::PathBuf::from(SYSTEM_INSTALL_PATH)
|
||||
} else {
|
||||
let local_app_data = std::env::var("LOCALAPPDATA")
|
||||
.unwrap_or_else(|_| {
|
||||
let home = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string());
|
||||
format!(r"{}\AppData\Local", home)
|
||||
});
|
||||
let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| {
|
||||
let home = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string());
|
||||
format!(r"{}\AppData\Local", home)
|
||||
});
|
||||
std::path::PathBuf::from(local_app_data).join(USER_INSTALL_PATH)
|
||||
}
|
||||
}
|
||||
@@ -305,7 +304,7 @@ pub fn install(force_user_install: bool) -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
pub fn is_protocol_handler_registered() -> bool {
|
||||
use windows::Win32::System::Registry::{
|
||||
RegOpenKeyExW, RegCloseKey, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, KEY_READ,
|
||||
RegCloseKey, RegOpenKeyExW, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, KEY_READ,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
@@ -318,7 +317,9 @@ pub fn is_protocol_handler_registered() -> bool {
|
||||
0,
|
||||
KEY_READ,
|
||||
&mut key,
|
||||
).is_ok() {
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
let _ = RegCloseKey(key);
|
||||
return true;
|
||||
}
|
||||
@@ -331,7 +332,9 @@ pub fn is_protocol_handler_registered() -> bool {
|
||||
0,
|
||||
KEY_READ,
|
||||
&mut key,
|
||||
).is_ok() {
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
let _ = RegCloseKey(key);
|
||||
return true;
|
||||
}
|
||||
@@ -355,22 +358,25 @@ pub fn parse_protocol_url(url_str: &str) -> Result<(String, String, Option<Strin
|
||||
//
|
||||
// Note: In URL parsing, "view" becomes the host, SESSION_ID is the path
|
||||
|
||||
let url = url::Url::parse(url_str)
|
||||
.map_err(|e| anyhow!("Invalid URL: {}", e))?;
|
||||
let url = url::Url::parse(url_str).map_err(|e| anyhow!("Invalid URL: {}", e))?;
|
||||
|
||||
if url.scheme() != "guruconnect" {
|
||||
return Err(anyhow!("Invalid scheme: expected guruconnect://"));
|
||||
}
|
||||
|
||||
// The "action" (view/connect) is parsed as the host
|
||||
let action = url.host_str()
|
||||
let action = url
|
||||
.host_str()
|
||||
.ok_or_else(|| anyhow!("Missing action in URL"))?;
|
||||
|
||||
// The session ID is the first path segment
|
||||
let path = url.path().trim_start_matches('/');
|
||||
info!("URL path: '{}', host: '{:?}'", path, url.host_str());
|
||||
let session_id = if path.is_empty() {
|
||||
return Err(anyhow!("Invalid URL: Missing session ID (path was empty, full URL: {})", url_str));
|
||||
return Err(anyhow!(
|
||||
"Invalid URL: Missing session ID (path was empty, full URL: {})",
|
||||
url_str
|
||||
));
|
||||
} else {
|
||||
path.split('/').next().unwrap_or("").to_string()
|
||||
};
|
||||
@@ -411,7 +417,5 @@ fn to_wide(s: &str) -> Vec<u16> {
|
||||
|
||||
#[cfg(windows)]
|
||||
fn description_to_bytes(wide: &[u16]) -> Vec<u8> {
|
||||
wide.iter()
|
||||
.flat_map(|w| w.to_le_bytes())
|
||||
.collect()
|
||||
wide.iter().flat_map(|w| w.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
@@ -92,16 +92,18 @@ pub mod build_info {
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing::{info, error, warn, Level};
|
||||
use tracing::{error, info, warn, Level};
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, MB_ICONERROR};
|
||||
#[cfg(windows)]
|
||||
use windows::core::PCWSTR;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
MessageBoxW, MB_ICONERROR, MB_ICONINFORMATION, MB_OK,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};
|
||||
|
||||
/// GuruConnect Remote Desktop
|
||||
@@ -140,7 +142,11 @@ enum Commands {
|
||||
session_id: String,
|
||||
|
||||
/// Server URL
|
||||
#[arg(short, long, default_value = "wss://connect.azcomputerguru.com/ws/viewer")]
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value = "wss://connect.azcomputerguru.com/ws/viewer"
|
||||
)]
|
||||
server: String,
|
||||
|
||||
/// API key for authentication
|
||||
@@ -177,15 +183,27 @@ fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize logging
|
||||
let level = if cli.verbose { Level::DEBUG } else { Level::INFO };
|
||||
let level = if cli.verbose {
|
||||
Level::DEBUG
|
||||
} else {
|
||||
Level::INFO
|
||||
};
|
||||
FmtSubscriber::builder()
|
||||
.with_max_level(level)
|
||||
.with_target(true)
|
||||
.with_thread_ids(true)
|
||||
.init();
|
||||
|
||||
info!("GuruConnect {} ({})", build_info::short_version(), build_info::BUILD_TARGET);
|
||||
info!("Built: {} | Commit: {}", build_info::BUILD_TIMESTAMP, build_info::GIT_COMMIT_DATE);
|
||||
info!(
|
||||
"GuruConnect {} ({})",
|
||||
build_info::short_version(),
|
||||
build_info::BUILD_TARGET
|
||||
);
|
||||
info!(
|
||||
"Built: {} | Commit: {}",
|
||||
build_info::BUILD_TIMESTAMP,
|
||||
build_info::GIT_COMMIT_DATE
|
||||
);
|
||||
|
||||
// Handle post-update cleanup
|
||||
if cli.post_update {
|
||||
@@ -194,21 +212,18 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
match cli.command {
|
||||
Some(Commands::Agent { code }) => {
|
||||
run_agent_mode(code)
|
||||
}
|
||||
Some(Commands::View { session_id, server, api_key }) => {
|
||||
run_viewer_mode(&server, &session_id, &api_key)
|
||||
}
|
||||
Some(Commands::Install { user_only, elevated }) => {
|
||||
run_install(user_only || elevated)
|
||||
}
|
||||
Some(Commands::Uninstall) => {
|
||||
run_uninstall()
|
||||
}
|
||||
Some(Commands::Launch { url }) => {
|
||||
run_launch(&url)
|
||||
}
|
||||
Some(Commands::Agent { code }) => run_agent_mode(code),
|
||||
Some(Commands::View {
|
||||
session_id,
|
||||
server,
|
||||
api_key,
|
||||
}) => run_viewer_mode(&server, &session_id, &api_key),
|
||||
Some(Commands::Install {
|
||||
user_only,
|
||||
elevated,
|
||||
}) => run_install(user_only || elevated),
|
||||
Some(Commands::Uninstall) => run_uninstall(),
|
||||
Some(Commands::Launch { url }) => run_launch(&url),
|
||||
Some(Commands::VersionInfo) => {
|
||||
// Show detailed version info (allocate console on Windows for visibility)
|
||||
#[cfg(windows)]
|
||||
@@ -341,7 +356,10 @@ fn run_install(force_user_install: bool) -> Result<()> {
|
||||
|
||||
match install::install(force_user_install) {
|
||||
Ok(()) => {
|
||||
show_message_box("GuruConnect", "Installation complete!\n\nYou can now use guruconnect:// links.");
|
||||
show_message_box(
|
||||
"GuruConnect",
|
||||
"Installation complete!\n\nYou can now use guruconnect:// links.",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -467,7 +485,11 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
}
|
||||
|
||||
// Create tray icon
|
||||
let tray = match tray::TrayController::new(&hostname, config.support_code.as_deref(), is_support_session) {
|
||||
let tray = match tray::TrayController::new(
|
||||
&hostname,
|
||||
config.support_code.as_deref(),
|
||||
is_support_session,
|
||||
) {
|
||||
Ok(t) => {
|
||||
info!("Tray icon created");
|
||||
Some(t)
|
||||
@@ -503,7 +525,10 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
t.update_status("Status: Connected");
|
||||
}
|
||||
|
||||
if let Err(e) = session.run_with_tray(tray.as_ref(), chat_ctrl.as_ref()).await {
|
||||
if let Err(e) = session
|
||||
.run_with_tray(tray.as_ref(), chat_ctrl.as_ref())
|
||||
.await
|
||||
{
|
||||
let error_msg = e.to_string();
|
||||
|
||||
if error_msg.contains("USER_EXIT") {
|
||||
@@ -515,7 +540,10 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
if error_msg.contains("SESSION_CANCELLED") {
|
||||
info!("Session was cancelled by technician");
|
||||
cleanup_on_exit();
|
||||
show_message_box("Support Session Ended", "The support session was cancelled.");
|
||||
show_message_box(
|
||||
"Support Session Ended",
|
||||
"The support session was cancelled.",
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -524,7 +552,10 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
if let Err(e) = startup::uninstall() {
|
||||
warn!("Uninstall failed: {}", e);
|
||||
}
|
||||
show_message_box("Remote Session Ended", "The session was ended by the administrator.");
|
||||
show_message_box(
|
||||
"Remote Session Ended",
|
||||
"The session was ended by the administrator.",
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -533,7 +564,10 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
if let Err(e) = startup::uninstall() {
|
||||
warn!("Uninstall failed: {}", e);
|
||||
}
|
||||
show_message_box("GuruConnect Removed", "This computer has been removed from remote management.");
|
||||
show_message_box(
|
||||
"GuruConnect Removed",
|
||||
"This computer has been removed from remote management.",
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -551,7 +585,10 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
if error_msg.contains("cancelled") {
|
||||
info!("Support code was cancelled");
|
||||
cleanup_on_exit();
|
||||
show_message_box("Support Session Cancelled", "This support session has been cancelled.");
|
||||
show_message_box(
|
||||
"Support Session Cancelled",
|
||||
"This support session has been cancelled.",
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,7 @@ pub fn request_sas() -> Result<()> {
|
||||
info!("Requesting SAS via service pipe...");
|
||||
|
||||
// Try to connect to the pipe
|
||||
let mut pipe = match OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(PIPE_NAME)
|
||||
{
|
||||
let mut pipe = match OpenOptions::new().read(true).write(true).open(PIPE_NAME) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
warn!("Failed to connect to SAS service pipe: {}", e);
|
||||
@@ -40,7 +36,8 @@ pub fn request_sas() -> Result<()> {
|
||||
|
||||
// Read the response
|
||||
let mut response = [0u8; 64];
|
||||
let n = pipe.read(&mut response)
|
||||
let n = pipe
|
||||
.read(&mut response)
|
||||
.context("Failed to read response from SAS service")?;
|
||||
|
||||
let response_str = String::from_utf8_lossy(&response[..n]);
|
||||
@@ -59,7 +56,10 @@ pub fn request_sas() -> Result<()> {
|
||||
}
|
||||
_ => {
|
||||
error!("Unexpected response from SAS service: {}", response_str);
|
||||
Err(anyhow::anyhow!("Unexpected SAS service response: {}", response_str))
|
||||
Err(anyhow::anyhow!(
|
||||
"Unexpected SAS service response: {}",
|
||||
response_str
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,11 +67,7 @@ pub fn request_sas() -> Result<()> {
|
||||
/// Check if the SAS service is available
|
||||
pub fn is_service_available() -> bool {
|
||||
// Try to open the pipe
|
||||
if let Ok(mut pipe) = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(PIPE_NAME)
|
||||
{
|
||||
if let Ok(mut pipe) = OpenOptions::new().read(true).write(true).open(PIPE_NAME) {
|
||||
// Send a ping command
|
||||
if pipe.write_all(b"ping\n").is_ok() {
|
||||
let mut response = [0u8; 64];
|
||||
|
||||
@@ -37,9 +37,9 @@ fn show_debug_console() {
|
||||
// No-op on non-Windows platforms
|
||||
}
|
||||
|
||||
use crate::proto::{Message, message, ChatMessage, AgentStatus, Heartbeat, HeartbeatAck};
|
||||
use crate::proto::{message, AgentStatus, ChatMessage, Heartbeat, HeartbeatAck, Message};
|
||||
use crate::transport::WebSocketTransport;
|
||||
use crate::tray::{TrayController, TrayAction};
|
||||
use crate::tray::{TrayAction, TrayController};
|
||||
use anyhow::Result;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -71,8 +71,8 @@ pub struct SessionManager {
|
||||
enum SessionState {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Idle, // Connected but not streaming - minimal resource usage
|
||||
Streaming, // Actively capturing and sending frames
|
||||
Idle, // Connected but not streaming - minimal resource usage
|
||||
Streaming, // Actively capturing and sending frames
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
@@ -103,10 +103,11 @@ impl SessionManager {
|
||||
&self.config.api_key,
|
||||
Some(&self.hostname),
|
||||
self.config.support_code.as_deref(),
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.transport = Some(transport);
|
||||
self.state = SessionState::Idle; // Start in idle mode
|
||||
self.state = SessionState::Idle; // Start in idle mode
|
||||
|
||||
tracing::info!("Connected to server, entering idle mode");
|
||||
|
||||
@@ -120,8 +121,12 @@ impl SessionManager {
|
||||
}
|
||||
|
||||
tracing::info!("Initializing streaming resources...");
|
||||
tracing::info!("Capture config: use_dxgi={}, gdi_fallback={}, fps={}",
|
||||
self.config.capture.use_dxgi, self.config.capture.gdi_fallback, self.config.capture.fps);
|
||||
tracing::info!(
|
||||
"Capture config: use_dxgi={}, gdi_fallback={}, fps={}",
|
||||
self.config.capture.use_dxgi,
|
||||
self.config.capture.gdi_fallback,
|
||||
self.config.capture.fps
|
||||
);
|
||||
|
||||
// Get primary display with panic protection
|
||||
tracing::debug!("Enumerating displays...");
|
||||
@@ -132,12 +137,19 @@ impl SessionManager {
|
||||
return Err(anyhow::anyhow!("Display enumeration panicked"));
|
||||
}
|
||||
};
|
||||
tracing::info!("Using display: {} ({}x{})",
|
||||
primary_display.name, primary_display.width, primary_display.height);
|
||||
tracing::info!(
|
||||
"Using display: {} ({}x{})",
|
||||
primary_display.name,
|
||||
primary_display.width,
|
||||
primary_display.height
|
||||
);
|
||||
|
||||
// Create capturer with panic protection
|
||||
// Force GDI mode if DXGI fails or panics
|
||||
tracing::debug!("Creating capturer (DXGI={})...", self.config.capture.use_dxgi);
|
||||
tracing::debug!(
|
||||
"Creating capturer (DXGI={})...",
|
||||
self.config.capture.use_dxgi
|
||||
);
|
||||
let capturer = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
capture::create_capturer(
|
||||
primary_display.clone(),
|
||||
@@ -157,13 +169,13 @@ impl SessionManager {
|
||||
tracing::info!("Capturer created successfully");
|
||||
|
||||
// Create encoder with panic protection
|
||||
tracing::debug!("Creating encoder (codec={}, quality={})...",
|
||||
self.config.encoding.codec, self.config.encoding.quality);
|
||||
tracing::debug!(
|
||||
"Creating encoder (codec={}, quality={})...",
|
||||
self.config.encoding.codec,
|
||||
self.config.encoding.quality
|
||||
);
|
||||
let encoder = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
encoder::create_encoder(
|
||||
&self.config.encoding.codec,
|
||||
self.config.encoding.quality,
|
||||
)
|
||||
encoder::create_encoder(&self.config.encoding.codec, self.config.encoding.quality)
|
||||
})) {
|
||||
Ok(result) => result?,
|
||||
Err(e) => {
|
||||
@@ -202,7 +214,9 @@ impl SessionManager {
|
||||
|
||||
/// Get display count for status reports
|
||||
fn get_display_count(&self) -> i32 {
|
||||
capture::enumerate_displays().map(|d| d.len() as i32).unwrap_or(1)
|
||||
capture::enumerate_displays()
|
||||
.map(|d| d.len() as i32)
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
/// Send agent status to server
|
||||
@@ -249,7 +263,11 @@ impl SessionManager {
|
||||
}
|
||||
|
||||
/// Run the session main loop with tray and chat event processing
|
||||
pub async fn run_with_tray(&mut self, tray: Option<&TrayController>, chat: Option<&ChatController>) -> Result<()> {
|
||||
pub async fn run_with_tray(
|
||||
&mut self,
|
||||
tray: Option<&TrayController>,
|
||||
chat: Option<&ChatController>,
|
||||
) -> Result<()> {
|
||||
if self.transport.is_none() {
|
||||
anyhow::bail!("Not connected");
|
||||
}
|
||||
@@ -395,12 +413,23 @@ impl SessionManager {
|
||||
}
|
||||
|
||||
// Periodic update check (only for persistent agents, not support sessions)
|
||||
if self.config.support_code.is_none() && last_update_check.elapsed() >= UPDATE_CHECK_INTERVAL {
|
||||
if self.config.support_code.is_none()
|
||||
&& last_update_check.elapsed() >= UPDATE_CHECK_INTERVAL
|
||||
{
|
||||
last_update_check = Instant::now();
|
||||
let server_url = self.config.server_url.replace("/ws/agent", "").replace("wss://", "https://").replace("ws://", "http://");
|
||||
let server_url = self
|
||||
.config
|
||||
.server_url
|
||||
.replace("/ws/agent", "")
|
||||
.replace("wss://", "https://")
|
||||
.replace("ws://", "http://");
|
||||
match crate::update::check_for_update(&server_url).await {
|
||||
Ok(Some(version_info)) => {
|
||||
tracing::info!("Update available: {} -> {}", crate::build_info::VERSION, version_info.latest_version);
|
||||
tracing::info!(
|
||||
"Update available: {} -> {}",
|
||||
crate::build_info::VERSION,
|
||||
version_info.latest_version
|
||||
);
|
||||
if let Err(e) = crate::update::perform_update(&version_info).await {
|
||||
tracing::error!("Auto-update failed: {}", e);
|
||||
}
|
||||
@@ -429,7 +458,9 @@ impl SessionManager {
|
||||
if let Ok(encoded) = encoder.encode(&frame) {
|
||||
if encoded.size > 0 {
|
||||
let msg = Message {
|
||||
payload: Some(message::Payload::VideoFrame(encoded.frame)),
|
||||
payload: Some(message::Payload::VideoFrame(
|
||||
encoded.frame,
|
||||
)),
|
||||
};
|
||||
let transport = self.transport.as_mut().unwrap();
|
||||
if let Err(e) = transport.send(msg).await {
|
||||
@@ -472,26 +503,40 @@ impl SessionManager {
|
||||
match msg.payload {
|
||||
Some(message::Payload::MouseEvent(mouse)) => {
|
||||
if let Some(input) = self.input.as_mut() {
|
||||
use crate::proto::MouseEventType;
|
||||
use crate::input::MouseButton;
|
||||
use crate::proto::MouseEventType;
|
||||
|
||||
match MouseEventType::try_from(mouse.event_type).unwrap_or(MouseEventType::MouseMove) {
|
||||
match MouseEventType::try_from(mouse.event_type)
|
||||
.unwrap_or(MouseEventType::MouseMove)
|
||||
{
|
||||
MouseEventType::MouseMove => {
|
||||
input.mouse_move(mouse.x, mouse.y)?;
|
||||
}
|
||||
MouseEventType::MouseDown => {
|
||||
input.mouse_move(mouse.x, mouse.y)?;
|
||||
if let Some(ref buttons) = mouse.buttons {
|
||||
if buttons.left { input.mouse_click(MouseButton::Left, true)?; }
|
||||
if buttons.right { input.mouse_click(MouseButton::Right, true)?; }
|
||||
if buttons.middle { input.mouse_click(MouseButton::Middle, true)?; }
|
||||
if buttons.left {
|
||||
input.mouse_click(MouseButton::Left, true)?;
|
||||
}
|
||||
if buttons.right {
|
||||
input.mouse_click(MouseButton::Right, true)?;
|
||||
}
|
||||
if buttons.middle {
|
||||
input.mouse_click(MouseButton::Middle, true)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseEventType::MouseUp => {
|
||||
if let Some(ref buttons) = mouse.buttons {
|
||||
if buttons.left { input.mouse_click(MouseButton::Left, false)?; }
|
||||
if buttons.right { input.mouse_click(MouseButton::Right, false)?; }
|
||||
if buttons.middle { input.mouse_click(MouseButton::Middle, false)?; }
|
||||
if buttons.left {
|
||||
input.mouse_click(MouseButton::Left, false)?;
|
||||
}
|
||||
if buttons.right {
|
||||
input.mouse_click(MouseButton::Right, false)?;
|
||||
}
|
||||
if buttons.middle {
|
||||
input.mouse_click(MouseButton::Middle, false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseEventType::MouseWheel => {
|
||||
@@ -538,10 +583,19 @@ impl SessionManager {
|
||||
tracing::info!("Update command received from server: {}", cmd.reason);
|
||||
// Trigger update check and perform update if available
|
||||
// The server URL is derived from the config
|
||||
let server_url = self.config.server_url.replace("/ws/agent", "").replace("wss://", "https://").replace("ws://", "http://");
|
||||
let server_url = self
|
||||
.config
|
||||
.server_url
|
||||
.replace("/ws/agent", "")
|
||||
.replace("wss://", "https://")
|
||||
.replace("ws://", "http://");
|
||||
match crate::update::check_for_update(&server_url).await {
|
||||
Ok(Some(version_info)) => {
|
||||
tracing::info!("Update available: {} -> {}", crate::build_info::VERSION, version_info.latest_version);
|
||||
tracing::info!(
|
||||
"Update available: {} -> {}",
|
||||
crate::build_info::VERSION,
|
||||
version_info.latest_version
|
||||
);
|
||||
if let Err(e) = crate::update::perform_update(&version_info).await {
|
||||
tracing::error!("Update failed: {}", e);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
//! Handles adding/removing the agent from Windows startup.
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::{info, warn, error};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::System::Registry::{
|
||||
RegOpenKeyExW, RegSetValueExW, RegDeleteValueW, RegCloseKey,
|
||||
HKEY_CURRENT_USER, KEY_WRITE, REG_SZ,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use windows::core::PCWSTR;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::System::Registry::{
|
||||
RegCloseKey, RegDeleteValueW, RegOpenKeyExW, RegSetValueExW, HKEY_CURRENT_USER, KEY_WRITE,
|
||||
REG_SZ,
|
||||
};
|
||||
|
||||
const STARTUP_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
const STARTUP_VALUE_NAME: &str = "GuruConnect";
|
||||
@@ -61,10 +61,8 @@ pub fn add_to_startup() -> Result<()> {
|
||||
let hkey_raw = std::mem::transmute::<_, windows::Win32::System::Registry::HKEY>(hkey);
|
||||
|
||||
// Set the value
|
||||
let data_bytes = std::slice::from_raw_parts(
|
||||
value_data.as_ptr() as *const u8,
|
||||
value_data.len() * 2,
|
||||
);
|
||||
let data_bytes =
|
||||
std::slice::from_raw_parts(value_data.as_ptr() as *const u8, value_data.len() * 2);
|
||||
|
||||
let set_result = RegSetValueExW(
|
||||
hkey_raw,
|
||||
@@ -168,7 +166,10 @@ pub fn uninstall() -> Result<()> {
|
||||
);
|
||||
|
||||
if result.is_err() {
|
||||
warn!("Failed to schedule file deletion: {:?}. File may need manual removal.", result);
|
||||
warn!(
|
||||
"Failed to schedule file deletion: {:?}. File may need manual removal.",
|
||||
result
|
||||
);
|
||||
} else {
|
||||
info!("Executable scheduled for deletion on reboot");
|
||||
}
|
||||
@@ -185,12 +186,15 @@ pub fn install_sas_service() -> Result<()> {
|
||||
|
||||
// Check if the SAS service binary exists alongside the agent
|
||||
let exe_path = std::env::current_exe()?;
|
||||
let exe_dir = exe_path.parent().ok_or_else(|| anyhow::anyhow!("No parent directory"))?;
|
||||
let exe_dir = exe_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("No parent directory"))?;
|
||||
let sas_binary = exe_dir.join("guruconnect-sas-service.exe");
|
||||
|
||||
if !sas_binary.exists() {
|
||||
// Also check in Program Files
|
||||
let program_files = std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe");
|
||||
let program_files =
|
||||
std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe");
|
||||
if !program_files.exists() {
|
||||
warn!("SAS service binary not found");
|
||||
return Ok(());
|
||||
@@ -232,16 +236,18 @@ pub fn uninstall_sas_service() -> Result<()> {
|
||||
|
||||
// Try to find and run the uninstall command
|
||||
let paths = [
|
||||
std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.join("guruconnect-sas-service.exe"))),
|
||||
Some(std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe")),
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|d| d.join("guruconnect-sas-service.exe"))),
|
||||
Some(std::path::PathBuf::from(
|
||||
r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe",
|
||||
)),
|
||||
];
|
||||
|
||||
for path_opt in paths.iter() {
|
||||
if let Some(ref path) = path_opt {
|
||||
if path.exists() {
|
||||
let output = std::process::Command::new(path)
|
||||
.arg("uninstall")
|
||||
.output();
|
||||
let output = std::process::Command::new(path).arg("uninstall").output();
|
||||
|
||||
if let Ok(result) = output {
|
||||
if result.status.success() {
|
||||
|
||||
@@ -103,11 +103,7 @@ impl WebSocketTransport {
|
||||
let mut stream = stream.lock().await;
|
||||
|
||||
// Use try_next for non-blocking receive
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1),
|
||||
stream.next(),
|
||||
)
|
||||
.await
|
||||
match tokio::time::timeout(std::time::Duration::from_millis(1), stream.next()).await
|
||||
{
|
||||
Ok(Some(Ok(ws_msg))) => Ok(Some(ws_msg)),
|
||||
Ok(Some(Err(e))) => Err(anyhow::anyhow!("WebSocket error: {}", e)),
|
||||
|
||||
@@ -9,12 +9,12 @@ use anyhow::Result;
|
||||
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tray_icon::{Icon, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||
use tracing::{info, warn};
|
||||
use tray_icon::{Icon, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
PeekMessageW, TranslateMessage, DispatchMessageW, MSG, PM_REMOVE,
|
||||
DispatchMessageW, PeekMessageW, TranslateMessage, MSG, PM_REMOVE,
|
||||
};
|
||||
|
||||
/// Events that can be triggered from the tray menu
|
||||
@@ -38,7 +38,11 @@ pub struct TrayController {
|
||||
impl TrayController {
|
||||
/// Create a new tray controller
|
||||
/// `allow_end_session` - If true, show "End Session" menu item (only for support sessions)
|
||||
pub fn new(machine_name: &str, support_code: Option<&str>, allow_end_session: bool) -> Result<Self> {
|
||||
pub fn new(
|
||||
machine_name: &str,
|
||||
support_code: Option<&str>,
|
||||
allow_end_session: bool,
|
||||
) -> Result<Self> {
|
||||
// Create menu items
|
||||
let status_text = if let Some(code) = support_code {
|
||||
format!("Support Session: {}", code)
|
||||
@@ -166,9 +170,9 @@ fn create_default_icon() -> Result<Icon> {
|
||||
|
||||
if dist <= radius {
|
||||
// Green circle
|
||||
rgba[idx] = 76; // R
|
||||
rgba[idx] = 76; // R
|
||||
rgba[idx + 1] = 175; // G
|
||||
rgba[idx + 2] = 80; // B
|
||||
rgba[idx + 2] = 80; // B
|
||||
rgba[idx + 3] = 255; // A
|
||||
} else if dist <= radius + 1.0 {
|
||||
// Anti-aliased edge
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
//! in-place binary replacement with restart.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use sha2::{Sha256, Digest};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{info, warn, error};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::build_info;
|
||||
|
||||
@@ -38,7 +38,7 @@ pub async fn check_for_update(server_base_url: &str) -> Result<Option<VersionInf
|
||||
info!("Checking for updates at {}", url);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true) // For self-signed certs in dev
|
||||
.danger_accept_invalid_certs(true) // For self-signed certs in dev
|
||||
.build()?;
|
||||
|
||||
let response = client
|
||||
@@ -79,11 +79,8 @@ fn is_newer_version(available: &str, current: &str) -> bool {
|
||||
let available_clean = available.split('-').next().unwrap_or(available);
|
||||
let current_clean = current.split('-').next().unwrap_or(current);
|
||||
|
||||
let parse_version = |s: &str| -> Vec<u32> {
|
||||
s.split('.')
|
||||
.filter_map(|p| p.parse().ok())
|
||||
.collect()
|
||||
};
|
||||
let parse_version =
|
||||
|s: &str| -> Vec<u32> { s.split('.').filter_map(|p| p.parse().ok()).collect() };
|
||||
|
||||
let av = parse_version(available_clean);
|
||||
let cv = parse_version(current_clean);
|
||||
@@ -112,7 +109,7 @@ pub async fn download_update(version_info: &VersionInfo) -> Result<PathBuf> {
|
||||
|
||||
let response = client
|
||||
.get(&version_info.download_url)
|
||||
.timeout(std::time::Duration::from_secs(300)) // 5 minutes for large files
|
||||
.timeout(std::time::Duration::from_secs(300)) // 5 minutes for large files
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -147,7 +144,10 @@ pub fn verify_checksum(file_path: &PathBuf, expected_sha256: &str) -> Result<boo
|
||||
if matches {
|
||||
info!("Checksum verified: {}", computed);
|
||||
} else {
|
||||
error!("Checksum mismatch! Expected: {}, Got: {}", expected_sha256, computed);
|
||||
error!(
|
||||
"Checksum mismatch! Expected: {}, Got: {}",
|
||||
expected_sha256, computed
|
||||
);
|
||||
}
|
||||
|
||||
Ok(matches)
|
||||
@@ -160,7 +160,8 @@ pub fn install_update(temp_path: &PathBuf) -> Result<PathBuf> {
|
||||
|
||||
// Get current executable path
|
||||
let current_exe = std::env::current_exe()?;
|
||||
let exe_dir = current_exe.parent()
|
||||
let exe_dir = current_exe
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Cannot get executable directory"))?;
|
||||
|
||||
// Create paths for backup and new executable
|
||||
@@ -257,10 +258,11 @@ pub fn cleanup_post_update() {
|
||||
#[cfg(windows)]
|
||||
fn schedule_delete_on_reboot(path: &PathBuf) {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows::Win32::Storage::FileSystem::{MoveFileExW, MOVEFILE_DELAY_UNTIL_REBOOT};
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Storage::FileSystem::{MoveFileExW, MOVEFILE_DELAY_UNTIL_REBOOT};
|
||||
|
||||
let path_wide: Vec<u16> = path.as_os_str()
|
||||
let path_wide: Vec<u16> = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
@@ -37,11 +37,11 @@ mod vk {
|
||||
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_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
|
||||
pub const VK_SNAPSHOT: u32 = 0x2C; // Print Screen
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -53,15 +53,12 @@ pub struct KeyboardHook {
|
||||
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"))?;
|
||||
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,
|
||||
)?;
|
||||
let hook = SetWindowsHookExW(WH_KEYBOARD_LL, Some(keyboard_hook_proc), None, 0)?;
|
||||
|
||||
HOOK_HANDLE = hook;
|
||||
Ok(Self { _hook: hook })
|
||||
@@ -82,11 +79,7 @@ impl Drop for KeyboardHook {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
unsafe extern "system" fn keyboard_hook_proc(
|
||||
code: i32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
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;
|
||||
@@ -97,10 +90,7 @@ unsafe extern "system" fn keyboard_hook_proc(
|
||||
|
||||
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
|
||||
);
|
||||
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() {
|
||||
@@ -114,7 +104,12 @@ unsafe extern "system" fn keyboard_hook_proc(
|
||||
};
|
||||
|
||||
let _ = tx.try_send(InputEvent::Key(event));
|
||||
trace!("Key hook: vk={:#x} scan={} down={}", vk_code, scan_code, is_down);
|
||||
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
|
||||
@@ -133,12 +128,12 @@ fn get_current_modifiers() -> proto::Modifiers {
|
||||
|
||||
unsafe {
|
||||
proto::Modifiers {
|
||||
ctrl: GetAsyncKeyState(0x11) < 0, // VK_CONTROL
|
||||
alt: GetAsyncKeyState(0x12) < 0, // VK_MENU
|
||||
shift: GetAsyncKeyState(0x10) < 0, // VK_SHIFT
|
||||
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
|
||||
num_lock: GetAsyncKeyState(0x90) & 1 != 0, // VK_NUMLOCK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::proto;
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tracing::{info, error, warn};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ViewerEvent {
|
||||
@@ -93,16 +93,18 @@ pub async fn run(server_url: &str, session_id: &str, api_key: &str) -> Result<()
|
||||
}
|
||||
}
|
||||
Some(proto::message::Payload::CursorPosition(pos)) => {
|
||||
let _ = viewer_tx_recv.send(ViewerEvent::CursorPosition(
|
||||
pos.x, pos.y, pos.visible
|
||||
)).await;
|
||||
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;
|
||||
let _ = viewer_tx_recv
|
||||
.send(ViewerEvent::Disconnected(d.reason))
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Window rendering and frame display
|
||||
|
||||
use super::{ViewerEvent, InputEvent};
|
||||
use crate::proto;
|
||||
#[cfg(windows)]
|
||||
use super::input;
|
||||
use super::{InputEvent, ViewerEvent};
|
||||
use crate::proto;
|
||||
use anyhow::Result;
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::Arc;
|
||||
@@ -43,10 +43,7 @@ struct ViewerApp {
|
||||
}
|
||||
|
||||
impl ViewerApp {
|
||||
fn new(
|
||||
viewer_rx: mpsc::Receiver<ViewerEvent>,
|
||||
input_tx: mpsc::Sender<InputEvent>,
|
||||
) -> Self {
|
||||
fn new(viewer_rx: mpsc::Receiver<ViewerEvent>, input_tx: mpsc::Sender<InputEvent>) -> Self {
|
||||
Self {
|
||||
window: None,
|
||||
surface: None,
|
||||
@@ -112,7 +109,9 @@ impl ViewerApp {
|
||||
}
|
||||
|
||||
fn render(&mut self) {
|
||||
let Some(surface) = &mut self.surface else { return };
|
||||
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 {
|
||||
|
||||
@@ -6,23 +6,17 @@ use bytes::Bytes;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use prost::Message as ProstMessage;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_tungstenite::{
|
||||
connect_async,
|
||||
tungstenite::protocol::Message as WsMessage,
|
||||
MaybeTlsStream, WebSocketStream,
|
||||
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 WsSender =
|
||||
futures_util::stream::SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, WsMessage>;
|
||||
|
||||
pub type WsReceiver = futures_util::stream::SplitStream<
|
||||
WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
>;
|
||||
pub type WsReceiver = futures_util::stream::SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
|
||||
|
||||
/// Receiver wrapper that parses protobuf messages
|
||||
pub struct MessageReceiver {
|
||||
@@ -88,10 +82,7 @@ pub async fn connect(url: &str, token: &str) -> Result<(WsSender, MessageReceive
|
||||
}
|
||||
|
||||
/// Send a protobuf message over the WebSocket
|
||||
pub async fn send_message(
|
||||
sender: &Arc<Mutex<WsSender>>,
|
||||
msg: &proto::Message,
|
||||
) -> Result<()> {
|
||||
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)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user