Unify agent and viewer into single guruconnect binary
- Renamed package from guruconnect-agent to guruconnect - Added CLI subcommands: agent, view, install, uninstall, launch - Moved viewer code into agent/src/viewer module - Added install module with: - UAC elevation attempt with user-install fallback - Protocol handler registration (guruconnect://) - System-wide install to Program Files or user install to LocalAppData - Single binary now handles both receiving and initiating connections - Protocol URL format: guruconnect://view/SESSION_ID?token=API_KEY Usage: guruconnect agent - Run as background agent guruconnect view <session_id> - View a remote session guruconnect install - Install and register protocol guruconnect launch <url> - Handle guruconnect:// URL 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
914
Cargo.lock
generated
914
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ resolver = "2"
|
||||
members = [
|
||||
"agent",
|
||||
"server",
|
||||
"viewer",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
[package]
|
||||
name = "guruconnect-agent"
|
||||
name = "guruconnect"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["AZ Computer Guru"]
|
||||
description = "GuruConnect Remote Desktop Agent"
|
||||
description = "GuruConnect Remote Desktop - Agent and Viewer"
|
||||
|
||||
[dependencies]
|
||||
# CLI
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread", "macros"] }
|
||||
|
||||
@@ -13,6 +16,11 @@ tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread",
|
||||
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
||||
futures-util = "0.3"
|
||||
|
||||
# Windowing (for viewer)
|
||||
winit = { version = "0.30", features = ["rwh_06"] }
|
||||
softbuffer = "0.4"
|
||||
raw-window-handle = "0.6"
|
||||
|
||||
# Compression
|
||||
zstd = "0.13"
|
||||
|
||||
@@ -58,8 +66,11 @@ muda = "0.15" # Menu for tray icon
|
||||
# Image handling for tray icon
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
|
||||
# URL parsing
|
||||
url = "2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
# Windows APIs for screen capture and input
|
||||
# Windows APIs for screen capture, input, and shell operations
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
@@ -69,10 +80,12 @@ windows = { version = "0.58", features = [
|
||||
"Win32_Graphics_Direct3D11",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Registry",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_Environment",
|
||||
"Win32_Security",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Pipes",
|
||||
@@ -88,16 +101,9 @@ prost-build = "0.13"
|
||||
winres = "0.1"
|
||||
|
||||
[[bin]]
|
||||
name = "guruconnect-agent"
|
||||
name = "guruconnect"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "guruconnect-sas-service"
|
||||
path = "src/bin/sas_service.rs"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = "z"
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
||||
364
agent/src/install.rs
Normal file
364
agent/src/install.rs
Normal file
@@ -0,0 +1,364 @@
|
||||
//! Installation and protocol handler registration
|
||||
//!
|
||||
//! Handles:
|
||||
//! - Self-installation to Program Files (with UAC) or LocalAppData (fallback)
|
||||
//! - Protocol handler registration (guruconnect://)
|
||||
//! - UAC elevation with graceful fallback
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use tracing::{info, warn, error};
|
||||
|
||||
#[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,
|
||||
},
|
||||
Win32::UI::Shell::ShellExecuteW,
|
||||
Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::ffi::OsStr;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
/// Install locations
|
||||
pub const SYSTEM_INSTALL_PATH: &str = r"C:\Program Files\GuruConnect";
|
||||
pub const USER_INSTALL_PATH: &str = r"GuruConnect"; // Relative to %LOCALAPPDATA%
|
||||
|
||||
/// Check if running with elevated privileges
|
||||
#[cfg(windows)]
|
||||
pub fn is_elevated() -> bool {
|
||||
unsafe {
|
||||
let mut token_handle = HANDLE::default();
|
||||
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut elevation = TOKEN_ELEVATION::default();
|
||||
let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;
|
||||
|
||||
let result = GetTokenInformation(
|
||||
token_handle,
|
||||
TokenElevation,
|
||||
Some(&mut elevation as *mut _ as *mut _),
|
||||
size,
|
||||
&mut size,
|
||||
);
|
||||
|
||||
let _ = windows::Win32::Foundation::CloseHandle(token_handle);
|
||||
|
||||
result.is_ok() && elevation.TokenIsElevated != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn is_elevated() -> bool {
|
||||
unsafe { libc::geteuid() == 0 }
|
||||
}
|
||||
|
||||
/// Get the install path based on elevation status
|
||||
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)
|
||||
});
|
||||
std::path::PathBuf::from(local_app_data).join(USER_INSTALL_PATH)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the executable path
|
||||
pub fn get_exe_path(install_path: &std::path::Path) -> std::path::PathBuf {
|
||||
install_path.join("guruconnect.exe")
|
||||
}
|
||||
|
||||
/// Attempt to elevate and re-run with install command
|
||||
#[cfg(windows)]
|
||||
pub fn try_elevate_and_install() -> Result<bool> {
|
||||
let exe_path = std::env::current_exe()?;
|
||||
let exe_path_wide: Vec<u16> = OsStr::new(exe_path.as_os_str())
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
let verb: Vec<u16> = OsStr::new("runas")
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
let params: Vec<u16> = OsStr::new("install --elevated")
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
unsafe {
|
||||
let result = ShellExecuteW(
|
||||
None,
|
||||
PCWSTR(verb.as_ptr()),
|
||||
PCWSTR(exe_path_wide.as_ptr()),
|
||||
PCWSTR(params.as_ptr()),
|
||||
PCWSTR::null(),
|
||||
SW_SHOWNORMAL,
|
||||
);
|
||||
|
||||
// ShellExecuteW returns > 32 on success
|
||||
if result.0 as usize > 32 {
|
||||
info!("UAC elevation requested");
|
||||
Ok(true)
|
||||
} else {
|
||||
warn!("UAC elevation denied or failed");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn try_elevate_and_install() -> Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Register the guruconnect:// protocol handler
|
||||
#[cfg(windows)]
|
||||
pub fn register_protocol_handler(elevated: bool) -> Result<()> {
|
||||
let install_path = get_install_path(elevated);
|
||||
let exe_path = get_exe_path(&install_path);
|
||||
let exe_path_str = exe_path.to_string_lossy();
|
||||
|
||||
// Command to execute: "C:\...\guruconnect.exe" "launch" "%1"
|
||||
let command = format!("\"{}\" launch \"%1\"", exe_path_str);
|
||||
|
||||
// Choose registry root based on elevation
|
||||
let root_key = if elevated {
|
||||
HKEY_CLASSES_ROOT
|
||||
} else {
|
||||
// User-level registration under Software\Classes
|
||||
HKEY_CURRENT_USER
|
||||
};
|
||||
|
||||
let base_path = if elevated {
|
||||
"guruconnect"
|
||||
} else {
|
||||
r"Software\Classes\guruconnect"
|
||||
};
|
||||
|
||||
unsafe {
|
||||
// Create guruconnect key
|
||||
let mut protocol_key = HKEY::default();
|
||||
let key_path = to_wide(base_path);
|
||||
let result = RegCreateKeyExW(
|
||||
root_key,
|
||||
PCWSTR(key_path.as_ptr()),
|
||||
0,
|
||||
PCWSTR::null(),
|
||||
REG_OPTION_NON_VOLATILE,
|
||||
KEY_WRITE,
|
||||
None,
|
||||
&mut protocol_key,
|
||||
None,
|
||||
);
|
||||
if result.is_err() {
|
||||
return Err(anyhow!("Failed to create protocol key: {:?}", result));
|
||||
}
|
||||
|
||||
// Set default value (protocol description)
|
||||
let description = to_wide("GuruConnect Protocol");
|
||||
let result = RegSetValueExW(
|
||||
protocol_key,
|
||||
PCWSTR::null(),
|
||||
0,
|
||||
REG_SZ,
|
||||
Some(&description_to_bytes(&description)),
|
||||
);
|
||||
if result.is_err() {
|
||||
let _ = RegCloseKey(protocol_key);
|
||||
return Err(anyhow!("Failed to set protocol description: {:?}", result));
|
||||
}
|
||||
|
||||
// Set URL Protocol (empty string indicates this is a protocol handler)
|
||||
let url_protocol = to_wide("URL Protocol");
|
||||
let empty = to_wide("");
|
||||
let result = RegSetValueExW(
|
||||
protocol_key,
|
||||
PCWSTR(url_protocol.as_ptr()),
|
||||
0,
|
||||
REG_SZ,
|
||||
Some(&description_to_bytes(&empty)),
|
||||
);
|
||||
if result.is_err() {
|
||||
let _ = RegCloseKey(protocol_key);
|
||||
return Err(anyhow!("Failed to set URL Protocol: {:?}", result));
|
||||
}
|
||||
|
||||
let _ = RegCloseKey(protocol_key);
|
||||
|
||||
// Create shell\open\command key
|
||||
let command_path = if elevated {
|
||||
r"guruconnect\shell\open\command"
|
||||
} else {
|
||||
r"Software\Classes\guruconnect\shell\open\command"
|
||||
};
|
||||
let command_key_path = to_wide(command_path);
|
||||
let mut command_key = HKEY::default();
|
||||
let result = RegCreateKeyExW(
|
||||
root_key,
|
||||
PCWSTR(command_key_path.as_ptr()),
|
||||
0,
|
||||
PCWSTR::null(),
|
||||
REG_OPTION_NON_VOLATILE,
|
||||
KEY_WRITE,
|
||||
None,
|
||||
&mut command_key,
|
||||
None,
|
||||
);
|
||||
if result.is_err() {
|
||||
return Err(anyhow!("Failed to create command key: {:?}", result));
|
||||
}
|
||||
|
||||
// Set the command
|
||||
let command_wide = to_wide(&command);
|
||||
let result = RegSetValueExW(
|
||||
command_key,
|
||||
PCWSTR::null(),
|
||||
0,
|
||||
REG_SZ,
|
||||
Some(&description_to_bytes(&command_wide)),
|
||||
);
|
||||
if result.is_err() {
|
||||
let _ = RegCloseKey(command_key);
|
||||
return Err(anyhow!("Failed to set command: {:?}", result));
|
||||
}
|
||||
|
||||
let _ = RegCloseKey(command_key);
|
||||
}
|
||||
|
||||
info!("Protocol handler registered: guruconnect://");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn register_protocol_handler(_elevated: bool) -> Result<()> {
|
||||
warn!("Protocol handler registration not supported on this platform");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install the application
|
||||
pub fn install(force_user_install: bool) -> Result<()> {
|
||||
let elevated = is_elevated();
|
||||
|
||||
// If not elevated and not forcing user install, try to elevate
|
||||
if !elevated && !force_user_install {
|
||||
info!("Attempting UAC elevation for system-wide install...");
|
||||
match try_elevate_and_install() {
|
||||
Ok(true) => {
|
||||
// Elevation was requested, exit this instance
|
||||
// The elevated instance will continue the install
|
||||
info!("Elevated process started, exiting current instance");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Ok(false) => {
|
||||
info!("UAC denied, falling back to user install");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Elevation failed: {}, falling back to user install", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let install_path = get_install_path(elevated);
|
||||
let exe_path = get_exe_path(&install_path);
|
||||
|
||||
info!("Installing to: {}", install_path.display());
|
||||
|
||||
// Create install directory
|
||||
std::fs::create_dir_all(&install_path)?;
|
||||
|
||||
// Copy ourselves to install location
|
||||
let current_exe = std::env::current_exe()?;
|
||||
if current_exe != exe_path {
|
||||
std::fs::copy(¤t_exe, &exe_path)?;
|
||||
info!("Copied executable to: {}", exe_path.display());
|
||||
}
|
||||
|
||||
// Register protocol handler
|
||||
register_protocol_handler(elevated)?;
|
||||
|
||||
info!("Installation complete!");
|
||||
if elevated {
|
||||
info!("Installed system-wide to: {}", install_path.display());
|
||||
} else {
|
||||
info!("Installed for current user to: {}", install_path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a guruconnect:// URL and extract session parameters
|
||||
pub fn parse_protocol_url(url: &str) -> Result<(String, String, Option<String>)> {
|
||||
// Expected formats:
|
||||
// guruconnect://view/SESSION_ID
|
||||
// guruconnect://view/SESSION_ID?token=API_KEY
|
||||
// guruconnect://connect/SESSION_ID?server=wss://...&token=API_KEY
|
||||
|
||||
let url = url::Url::parse(url)
|
||||
.map_err(|e| anyhow!("Invalid URL: {}", e))?;
|
||||
|
||||
if url.scheme() != "guruconnect" {
|
||||
return Err(anyhow!("Invalid scheme: expected guruconnect://"));
|
||||
}
|
||||
|
||||
let path = url.path().trim_start_matches('/');
|
||||
let parts: Vec<&str> = path.split('/').collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
return Err(anyhow!("Missing action in URL"));
|
||||
}
|
||||
|
||||
let action = parts[0];
|
||||
let session_id = parts.get(1).map(|s| s.to_string())
|
||||
.ok_or_else(|| anyhow!("Missing session ID"))?;
|
||||
|
||||
// Extract query parameters
|
||||
let mut server = None;
|
||||
let mut token = None;
|
||||
|
||||
for (key, value) in url.query_pairs() {
|
||||
match key.as_ref() {
|
||||
"server" => server = Some(value.to_string()),
|
||||
"token" | "api_key" => token = Some(value.to_string()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Default server if not specified
|
||||
let server = server.unwrap_or_else(|| "wss://connect.azcomputerguru.com/ws/viewer".to_string());
|
||||
|
||||
match action {
|
||||
"view" | "connect" => Ok((server, session_id, token)),
|
||||
_ => Err(anyhow!("Unknown action: {}", action)),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for Windows registry operations
|
||||
#[cfg(windows)]
|
||||
fn to_wide(s: &str) -> Vec<u16> {
|
||||
OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn description_to_bytes(wide: &[u16]) -> Vec<u8> {
|
||||
wide.iter()
|
||||
.flat_map(|w| w.to_le_bytes())
|
||||
.collect()
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
//! GuruConnect Agent - Remote Desktop Agent for Windows
|
||||
//! GuruConnect - Remote Desktop Agent and Viewer
|
||||
//!
|
||||
//! Provides screen capture, input injection, and remote control capabilities.
|
||||
//! Single binary for both agent (receiving connections) and viewer (initiating connections).
|
||||
//!
|
||||
//! Usage:
|
||||
//! guruconnect-agent.exe [support_code]
|
||||
//!
|
||||
//! If a support code is provided, the agent will connect using that code
|
||||
//! for a one-time support session.
|
||||
//! guruconnect agent - Run as background agent
|
||||
//! guruconnect view <session_id> - View a remote session
|
||||
//! guruconnect install - Install and register protocol handler
|
||||
//! guruconnect launch <url> - 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")]
|
||||
@@ -16,42 +17,227 @@ 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"));
|
||||
}
|
||||
|
||||
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};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, MB_ICONERROR};
|
||||
#[cfg(windows)]
|
||||
use windows::core::PCWSTR;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY};
|
||||
use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::System::Console::{AllocConsole, FreeConsole, GetConsoleWindow};
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW, SW_HIDE};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};
|
||||
|
||||
/// Extract a 6-digit support code from the executable's filename.
|
||||
/// Looks for patterns like "GuruConnect-123456.exe" or "123456.exe"
|
||||
/// GuruConnect Remote Desktop
|
||||
#[derive(Parser)]
|
||||
#[command(name = "guruconnect")]
|
||||
#[command(version, about = "Remote desktop agent and viewer")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
|
||||
/// Support code for legacy mode (runs agent with code)
|
||||
#[arg(value_name = "SUPPORT_CODE")]
|
||||
support_code: Option<String>,
|
||||
|
||||
/// 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<String>,
|
||||
},
|
||||
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
|
||||
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 v{}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
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)
|
||||
}
|
||||
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 we should auto-detect mode
|
||||
// For now, default to agent mode
|
||||
run_agent_mode(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run in agent mode (receive remote connections)
|
||||
fn run_agent_mode(support_code: Option<String>) -> 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<String> {
|
||||
// Get the path to the current executable
|
||||
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
|
||||
// Try common patterns: "Name-123456", "Name_123456", "123456"
|
||||
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()) {
|
||||
@@ -59,7 +245,7 @@ fn extract_code_from_filename() -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if the last 6 characters are digits (e.g., "GuruConnect123456")
|
||||
// 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()) {
|
||||
@@ -70,45 +256,12 @@ fn extract_code_from_filename() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if the process is running with elevated privileges (Windows only)
|
||||
#[cfg(windows)]
|
||||
fn is_elevated() -> bool {
|
||||
unsafe {
|
||||
let mut token_handle = windows::Win32::Foundation::HANDLE::default();
|
||||
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut elevation = TOKEN_ELEVATION::default();
|
||||
let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;
|
||||
|
||||
let result = GetTokenInformation(
|
||||
token_handle,
|
||||
TokenElevation,
|
||||
Some(&mut elevation as *mut _ as *mut _),
|
||||
size,
|
||||
&mut size,
|
||||
);
|
||||
|
||||
let _ = windows::Win32::Foundation::CloseHandle(token_handle);
|
||||
|
||||
result.is_ok() && elevation.TokenIsElevated != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn is_elevated() -> bool {
|
||||
// On non-Windows, check if running as root
|
||||
unsafe { libc::geteuid() == 0 }
|
||||
}
|
||||
|
||||
/// Show a message box to the user (Windows only)
|
||||
/// 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;
|
||||
|
||||
// Convert strings to wide strings for Windows API
|
||||
let title_wide: Vec<u16> = OsStr::new(title)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
@@ -129,74 +282,59 @@ fn show_message_box(title: &str, message: &str) {
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn show_message_box(_title: &str, _message: &str) {
|
||||
// No-op on non-Windows platforms
|
||||
fn show_message_box(_title: &str, message: &str) {
|
||||
println!("{}", message);
|
||||
}
|
||||
|
||||
/// Show the debug console window (Windows only)
|
||||
/// 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<u16> = OsStr::new(title)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let message_wide: Vec<u16> = 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 {
|
||||
// Check if we already have a console
|
||||
let hwnd = GetConsoleWindow();
|
||||
if hwnd.0 == std::ptr::null_mut() {
|
||||
// No console, allocate one
|
||||
let _ = AllocConsole();
|
||||
info!("Debug console window opened");
|
||||
} else {
|
||||
// Console exists, make sure it's visible
|
||||
let _ = ShowWindow(hwnd, SW_SHOW);
|
||||
info!("Debug console window shown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn show_debug_console() {
|
||||
// No-op on non-Windows platforms
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn show_debug_console() {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(Level::INFO)
|
||||
.with_target(true)
|
||||
.with_thread_ids(true)
|
||||
.init();
|
||||
|
||||
info!("GuruConnect Agent v{}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
// Check and log elevation status
|
||||
if is_elevated() {
|
||||
info!("Running with elevated (administrator) privileges");
|
||||
} else {
|
||||
info!("Running with standard user privileges");
|
||||
}
|
||||
|
||||
// Extract support code from executable filename
|
||||
// e.g., GuruConnect-123456.exe -> 123456
|
||||
let support_code = extract_code_from_filename();
|
||||
if let Some(ref code) = support_code {
|
||||
info!("Support code from filename: {}", code);
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
let mut config = config::Config::load()?;
|
||||
config.support_code = support_code;
|
||||
info!("Loaded configuration for server: {}", config.server_url);
|
||||
|
||||
// Run the agent
|
||||
if let Err(e) = run_agent(config).await {
|
||||
error!("Agent error: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clean up before exiting (remove from startup, etc.)
|
||||
/// Called when user explicitly ends session or support session completes
|
||||
/// Clean up before exiting
|
||||
fn cleanup_on_exit() {
|
||||
info!("Cleaning up before exit");
|
||||
if let Err(e) = startup::remove_from_startup() {
|
||||
@@ -204,44 +342,37 @@ fn cleanup_on_exit() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the agent main loop
|
||||
async fn run_agent(config: config::Config) -> Result<()> {
|
||||
// Create session manager with elevation status
|
||||
let elevated = is_elevated();
|
||||
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 so we reconnect after reboot
|
||||
// Persistent agents (no support code) should ALWAYS be in startup
|
||||
// Support sessions only need startup temporarily while active
|
||||
// Add to startup
|
||||
if let Err(e) = startup::add_to_startup() {
|
||||
warn!("Failed to add to startup: {}. Agent won't persist through reboot.", e);
|
||||
warn!("Failed to add to startup: {}", e);
|
||||
}
|
||||
|
||||
// Create tray icon
|
||||
// Only support sessions can be ended by user - persistent agents are admin-managed
|
||||
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: {}. Continuing without tray.", e);
|
||||
warn!("Failed to create tray icon: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Create chat controller
|
||||
let chat_ctrl = chat::ChatController::new();
|
||||
if chat_ctrl.is_some() {
|
||||
info!("Chat controller created");
|
||||
}
|
||||
|
||||
// Connect to server and run main loop
|
||||
loop {
|
||||
info!("Connecting to server...");
|
||||
|
||||
// Check if user requested exit via tray before connecting (support sessions only)
|
||||
if is_support_session {
|
||||
if let Some(ref t) = tray {
|
||||
if t.exit_requested() {
|
||||
@@ -256,44 +387,32 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
Ok(_) => {
|
||||
info!("Connected to server");
|
||||
|
||||
// Update tray status
|
||||
if let Some(ref t) = tray {
|
||||
t.update_status("Status: Connected");
|
||||
}
|
||||
|
||||
// Run session until disconnect, passing tray and chat for event processing
|
||||
if let Err(e) = session.run_with_tray(tray.as_ref(), chat_ctrl.as_ref()).await {
|
||||
let error_msg = e.to_string();
|
||||
|
||||
// Check if this is a user-initiated exit
|
||||
if error_msg.contains("USER_EXIT") {
|
||||
info!("Session ended by user");
|
||||
cleanup_on_exit();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if this is a cancellation
|
||||
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 by the technician.\n\nThis window will close automatically.",
|
||||
);
|
||||
// Exit cleanly without reconnecting
|
||||
show_message_box("Support Session Ended", "The support session was cancelled.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if this is an admin disconnect (uninstall)
|
||||
if error_msg.contains("ADMIN_DISCONNECT") {
|
||||
info!("Session was disconnected by administrator - uninstalling");
|
||||
info!("Session disconnected by administrator - uninstalling");
|
||||
if let Err(e) = startup::uninstall() {
|
||||
warn!("Uninstall failed: {}", e);
|
||||
}
|
||||
show_message_box(
|
||||
"Remote Session Ended",
|
||||
"The remote support session has been ended by the administrator.\n\nThe agent will be removed from this computer.",
|
||||
);
|
||||
show_message_box("Remote Session Ended", "The session was ended by the administrator.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -303,15 +422,10 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
|
||||
// Check if connection was rejected due to cancelled code
|
||||
if error_msg.contains("cancelled") {
|
||||
info!("Support code was cancelled before connection");
|
||||
info!("Support code was cancelled");
|
||||
cleanup_on_exit();
|
||||
show_message_box(
|
||||
"Support Session Cancelled",
|
||||
"This support session has been cancelled.\n\nPlease contact your technician for a new support code.",
|
||||
);
|
||||
// Exit cleanly without reconnecting
|
||||
show_message_box("Support Session Cancelled", "This support session has been cancelled.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -319,14 +433,12 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// For support sessions, don't reconnect if something goes wrong
|
||||
if is_support_session {
|
||||
info!("Support session ended, not reconnecting");
|
||||
cleanup_on_exit();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Wait before reconnecting (persistent agents only - support sessions already exited above)
|
||||
info!("Reconnecting in 5 seconds...");
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Low-level keyboard hook for capturing all keys including Win key
|
||||
|
||||
use crate::InputEvent;
|
||||
use super::InputEvent;
|
||||
#[cfg(windows)]
|
||||
use crate::proto;
|
||||
use anyhow::Result;
|
||||
@@ -1,42 +1,17 @@
|
||||
//! GuruConnect Native Viewer
|
||||
//! Viewer module - Native remote desktop viewer with full keyboard capture
|
||||
//!
|
||||
//! Native remote desktop viewer with full keyboard capture including Win key.
|
||||
//! This module provides the viewer functionality for connecting to remote
|
||||
//! GuruConnect sessions with low-level keyboard hooks for Win key capture.
|
||||
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod proto;
|
||||
mod transport;
|
||||
mod render;
|
||||
mod input;
|
||||
mod render;
|
||||
mod transport;
|
||||
|
||||
use crate::proto;
|
||||
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,
|
||||
}
|
||||
use tracing::{info, error, warn};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ViewerEvent {
|
||||
@@ -54,43 +29,21 @@ pub enum InputEvent {
|
||||
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();
|
||||
|
||||
/// Run the viewer to connect to a remote session
|
||||
pub async fn run(server_url: &str, session_id: &str, api_key: &str) -> Result<()> {
|
||||
info!("GuruConnect Viewer starting");
|
||||
info!("Server: {}", args.server);
|
||||
info!("Session: {}", args.session_id);
|
||||
info!("Server: {}", server_url);
|
||||
info!("Session: {}", 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);
|
||||
let ws_url = format!("{}?session_id={}", server_url, session_id);
|
||||
info!("Connecting to {}", ws_url);
|
||||
|
||||
let (ws_sender, mut ws_receiver) = transport::connect(&ws_url, &args.api_key).await?;
|
||||
let (ws_sender, mut ws_receiver) = transport::connect(&ws_url, api_key).await?;
|
||||
let ws_sender = Arc::new(Mutex::new(ws_sender));
|
||||
|
||||
info!("Connected to server");
|
||||
@@ -100,6 +53,7 @@ async fn run_viewer(
|
||||
let ws_sender_input = ws_sender.clone();
|
||||
|
||||
// Spawn task to forward input events to server
|
||||
let mut input_rx = input_rx;
|
||||
let input_task = tokio::spawn(async move {
|
||||
while let Some(event) = input_rx.recv().await {
|
||||
let msg = match event {
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Window rendering and frame display
|
||||
|
||||
use crate::{ViewerEvent, InputEvent, proto};
|
||||
use super::{ViewerEvent, InputEvent};
|
||||
use crate::proto;
|
||||
#[cfg(windows)]
|
||||
use crate::input;
|
||||
use super::input;
|
||||
use anyhow::Result;
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::Arc;
|
||||
@@ -1,66 +0,0 @@
|
||||
[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"
|
||||
@@ -1,9 +0,0 @@
|
||||
use std::io::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
println!("cargo:rerun-if-changed=../proto/guruconnect.proto");
|
||||
|
||||
prost_build::compile_protos(&["../proto/guruconnect.proto"], &["../proto/"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
//! Protocol buffer definitions
|
||||
|
||||
// Include generated protobuf code
|
||||
include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
|
||||
Reference in New Issue
Block a user