//! GuruConnect - Remote Desktop Agent and Viewer //! //! Single binary for both agent (receiving connections) and viewer (initiating connections). //! //! Usage: //! guruconnect agent - Run as background agent //! guruconnect view - View a remote session //! guruconnect install - Install and register protocol handler //! guruconnect launch - Handle guruconnect:// URL //! guruconnect [support_code] - Legacy: run agent with support code // Hide console window by default on Windows (release builds) #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod capture; mod chat; mod config; mod encoder; mod input; mod install; mod sas_client; mod session; mod startup; mod transport; mod tray; mod viewer; pub mod proto { include!(concat!(env!("OUT_DIR"), "/guruconnect.rs")); } /// Build information embedded at compile time pub mod build_info { /// Cargo package version (from Cargo.toml) pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Git commit hash (short, 8 chars) pub const GIT_HASH: &str = env!("GIT_HASH"); /// Git commit hash (full) pub const GIT_HASH_FULL: &str = env!("GIT_HASH_FULL"); /// Git branch name pub const GIT_BRANCH: &str = env!("GIT_BRANCH"); /// Git dirty state ("clean" or "dirty") pub const GIT_DIRTY: &str = env!("GIT_DIRTY"); /// Git commit date pub const GIT_COMMIT_DATE: &str = env!("GIT_COMMIT_DATE"); /// Build timestamp (UTC) pub const BUILD_TIMESTAMP: &str = env!("BUILD_TIMESTAMP"); /// Build profile (debug/release) pub const BUILD_PROFILE: &str = env!("BUILD_PROFILE"); /// Target triple (e.g., x86_64-pc-windows-msvc) pub const BUILD_TARGET: &str = env!("BUILD_TARGET"); /// Short version string for display (version + git hash) pub fn short_version() -> String { if GIT_DIRTY == "dirty" { format!("{}-{}-dirty", VERSION, GIT_HASH) } else { format!("{}-{}", VERSION, GIT_HASH) } } /// Full version string with all details pub fn full_version() -> String { format!( "GuruConnect v{}\n\ Git: {} ({})\n\ Branch: {}\n\ Commit: {}\n\ Built: {}\n\ Profile: {}\n\ Target: {}", VERSION, GIT_HASH, GIT_DIRTY, GIT_BRANCH, GIT_COMMIT_DATE, BUILD_TIMESTAMP, BUILD_PROFILE, BUILD_TARGET ) } } use anyhow::Result; use clap::{Parser, Subcommand}; use tracing::{info, error, 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::{ShowWindow, SW_SHOW}; /// GuruConnect Remote Desktop #[derive(Parser)] #[command(name = "guruconnect")] #[command(version = concat!(env!("CARGO_PKG_VERSION"), "-", env!("GIT_HASH")), about = "Remote desktop agent and viewer")] struct Cli { #[command(subcommand)] command: Option, /// Support code for legacy mode (runs agent with code) #[arg(value_name = "SUPPORT_CODE")] support_code: Option, /// Enable verbose logging #[arg(short, long, global = true)] verbose: bool, } #[derive(Subcommand)] enum Commands { /// Run as background agent (receive remote connections) Agent { /// Support code for one-time session #[arg(short, long)] code: Option, }, /// View a remote session (connect to an agent) View { /// Session ID to connect to session_id: String, /// Server URL #[arg(short, long, default_value = "wss://connect.azcomputerguru.com/ws/viewer")] server: String, /// API key for authentication #[arg(short, long, default_value = "")] api_key: String, }, /// Install GuruConnect and register protocol handler Install { /// Skip UAC elevation, install for current user only #[arg(long)] user_only: bool, /// Called internally when running elevated #[arg(long, hide = true)] elevated: bool, }, /// Uninstall GuruConnect Uninstall, /// Handle a guruconnect:// protocol URL Launch { /// The guruconnect:// URL to handle url: String, }, /// Show detailed version and build information #[command(name = "version-info")] VersionInfo, } fn main() -> Result<()> { let cli = Cli::parse(); // Initialize logging 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); 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::VersionInfo) => { // Show detailed version info (allocate console on Windows for visibility) #[cfg(windows)] show_debug_console(); println!("{}", build_info::full_version()); Ok(()) } None => { // Legacy mode: if a support code was provided, run as agent if let Some(code) = cli.support_code { run_agent_mode(Some(code)) } else { // No args: check if protocol handler is installed // If not, run install mode (user likely downloaded from web) if !install::is_protocol_handler_registered() { info!("Protocol handler not registered, running installer"); run_install(false) } else { // Protocol handler exists, run as agent run_agent_mode(None) } } } } } /// Run in agent mode (receive remote connections) fn run_agent_mode(support_code: Option) -> Result<()> { info!("Running in agent mode"); // Check elevation status if install::is_elevated() { info!("Running with elevated (administrator) privileges"); } else { info!("Running with standard user privileges"); } // Also check for support code in filename (legacy compatibility) let code = support_code.or_else(extract_code_from_filename); if let Some(ref c) = code { info!("Support code: {}", c); } // Load configuration let mut config = config::Config::load()?; config.support_code = code; info!("Server: {}", config.server_url); // Run the agent let rt = tokio::runtime::Runtime::new()?; rt.block_on(run_agent(config)) } /// Run in viewer mode (connect to remote session) fn run_viewer_mode(server: &str, session_id: &str, api_key: &str) -> Result<()> { info!("Running in viewer mode"); info!("Connecting to session: {}", session_id); let rt = tokio::runtime::Runtime::new()?; rt.block_on(viewer::run(server, session_id, api_key)) } /// Handle guruconnect:// URL launch fn run_launch(url: &str) -> Result<()> { info!("Handling protocol URL: {}", url); match install::parse_protocol_url(url) { Ok((server, session_id, token)) => { let api_key = token.unwrap_or_default(); run_viewer_mode(&server, &session_id, &api_key) } Err(e) => { error!("Failed to parse URL: {}", e); show_error_box("GuruConnect", &format!("Invalid URL: {}", e)); Err(e) } } } /// Install GuruConnect fn run_install(force_user_install: bool) -> Result<()> { info!("Installing GuruConnect..."); match install::install(force_user_install) { Ok(()) => { show_message_box("GuruConnect", "Installation complete!\n\nYou can now use guruconnect:// links."); Ok(()) } Err(e) => { error!("Installation failed: {}", e); show_error_box("GuruConnect", &format!("Installation failed: {}", e)); Err(e) } } } /// Uninstall GuruConnect fn run_uninstall() -> Result<()> { info!("Uninstalling GuruConnect..."); // Remove from startup if let Err(e) = startup::remove_from_startup() { warn!("Failed to remove from startup: {}", e); } // TODO: Remove registry keys for protocol handler // TODO: Remove install directory show_message_box("GuruConnect", "Uninstall complete."); Ok(()) } /// Extract a 6-digit support code from the executable's filename fn extract_code_from_filename() -> Option { let exe_path = std::env::current_exe().ok()?; let filename = exe_path.file_stem()?.to_str()?; // Look for a 6-digit number in the filename for part in filename.split(|c| c == '-' || c == '_' || c == '.') { let trimmed = part.trim(); if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) { return Some(trimmed.to_string()); } } // Check if the last 6 characters are digits if filename.len() >= 6 { let last_six = &filename[filename.len() - 6..]; if last_six.chars().all(|c| c.is_ascii_digit()) { return Some(last_six.to_string()); } } None } /// Show a message box (Windows only) #[cfg(windows)] fn show_message_box(title: &str, message: &str) { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; let title_wide: Vec = OsStr::new(title) .encode_wide() .chain(std::iter::once(0)) .collect(); let message_wide: Vec = OsStr::new(message) .encode_wide() .chain(std::iter::once(0)) .collect(); unsafe { MessageBoxW( None, PCWSTR(message_wide.as_ptr()), PCWSTR(title_wide.as_ptr()), MB_OK | MB_ICONINFORMATION, ); } } #[cfg(not(windows))] fn show_message_box(_title: &str, message: &str) { println!("{}", message); } /// Show an error message box (Windows only) #[cfg(windows)] fn show_error_box(title: &str, message: &str) { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; let title_wide: Vec = OsStr::new(title) .encode_wide() .chain(std::iter::once(0)) .collect(); let message_wide: Vec = OsStr::new(message) .encode_wide() .chain(std::iter::once(0)) .collect(); unsafe { MessageBoxW( None, PCWSTR(message_wide.as_ptr()), PCWSTR(title_wide.as_ptr()), MB_OK | MB_ICONERROR, ); } } #[cfg(not(windows))] fn show_error_box(_title: &str, message: &str) { eprintln!("ERROR: {}", message); } /// Show debug console window (Windows only) #[cfg(windows)] #[allow(dead_code)] fn show_debug_console() { unsafe { let hwnd = GetConsoleWindow(); if hwnd.0 == std::ptr::null_mut() { let _ = AllocConsole(); } else { let _ = ShowWindow(hwnd, SW_SHOW); } } } #[cfg(not(windows))] #[allow(dead_code)] fn show_debug_console() {} /// Clean up before exiting fn cleanup_on_exit() { info!("Cleaning up before exit"); if let Err(e) = startup::remove_from_startup() { warn!("Failed to remove from startup: {}", e); } } /// Run the agent main loop async fn run_agent(config: config::Config) -> Result<()> { let elevated = install::is_elevated(); let mut session = session::SessionManager::new(config.clone(), elevated); let is_support_session = config.support_code.is_some(); let hostname = config.hostname(); // Add to startup if let Err(e) = startup::add_to_startup() { warn!("Failed to add to startup: {}", e); } // Create tray icon let tray = match tray::TrayController::new(&hostname, config.support_code.as_deref(), is_support_session) { Ok(t) => { info!("Tray icon created"); Some(t) } Err(e) => { warn!("Failed to create tray icon: {}", e); None } }; // Create chat controller let chat_ctrl = chat::ChatController::new(); // Connect to server and run main loop loop { info!("Connecting to server..."); if is_support_session { if let Some(ref t) = tray { if t.exit_requested() { info!("Exit requested by user"); cleanup_on_exit(); return Ok(()); } } } match session.connect().await { Ok(_) => { info!("Connected to server"); if let Some(ref t) = tray { t.update_status("Status: Connected"); } 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") { info!("Session ended by user"); cleanup_on_exit(); return Ok(()); } 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."); return Ok(()); } if error_msg.contains("ADMIN_DISCONNECT") { info!("Session disconnected by administrator - uninstalling"); if let Err(e) = startup::uninstall() { warn!("Uninstall failed: {}", e); } show_message_box("Remote Session Ended", "The session was ended by the administrator."); return Ok(()); } if error_msg.contains("ADMIN_UNINSTALL") { info!("Uninstall command received from server - uninstalling"); if let Err(e) = startup::uninstall() { warn!("Uninstall failed: {}", e); } show_message_box("GuruConnect Removed", "This computer has been removed from remote management."); return Ok(()); } if error_msg.contains("ADMIN_RESTART") { info!("Restart command received - will reconnect"); // Don't exit, just let the loop continue to reconnect } else { error!("Session error: {}", e); } } } Err(e) => { let error_msg = e.to_string(); 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."); return Ok(()); } error!("Connection failed: {}", e); } } if is_support_session { info!("Support session ended, not reconnecting"); cleanup_on_exit(); return Ok(()); } info!("Reconnecting in 5 seconds..."); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; } }