All checks were successful
CI never ran clippy on the agent crate (the build-server clippy job is Linux-only and can't compile the Windows agent; build-agent only runs cargo build), so 77 clippy -D-warnings errors had accumulated. Behavior-preserving cleanup, code-reviewed APPROVED, locally verified (cargo clippy --workspace --all-targets --all-features -- -D warnings exits 0; cargo test --workspace = 57 passed). - let _ = on Win32 resource-teardown BOOL returns (gdi.rs); fallible BitBlt/GetDIBits stay error-handled - removed unused imports/vars; idiom fixes (div_ceil, is_null, transmute annotations, match collapsing, useless_conversion) - #[allow(dead_code)] + comment on genuine Task-6/7 scaffolding (vk consts, SpecialKey emission, SAS mgmt API, modifier tracking, GDI frame-diff fields) - Cargo.lock: cargo pruned ~147 stale transitive entries (no version changes) Follow-up: add cargo clippy -D warnings to the build-agent CI job so the agent crate stays clippy-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
422 lines
12 KiB
Rust
422 lines
12 KiB
Rust
//! 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};
|
|
|
|
#[cfg(windows)]
|
|
use windows::{
|
|
core::PCWSTR,
|
|
Win32::Foundation::HANDLE,
|
|
Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
|
|
Win32::System::Registry::{
|
|
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,
|
|
};
|
|
|
|
#[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(())
|
|
}
|
|
|
|
/// Check if the guruconnect:// protocol handler is registered
|
|
#[cfg(windows)]
|
|
pub fn is_protocol_handler_registered() -> bool {
|
|
use windows::Win32::System::Registry::{
|
|
RegCloseKey, RegOpenKeyExW, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, KEY_READ,
|
|
};
|
|
|
|
unsafe {
|
|
// Check system-wide registration (HKCR\guruconnect)
|
|
let mut key = HKEY::default();
|
|
let key_path = to_wide("guruconnect");
|
|
if RegOpenKeyExW(
|
|
HKEY_CLASSES_ROOT,
|
|
PCWSTR(key_path.as_ptr()),
|
|
0,
|
|
KEY_READ,
|
|
&mut key,
|
|
)
|
|
.is_ok()
|
|
{
|
|
let _ = RegCloseKey(key);
|
|
return true;
|
|
}
|
|
|
|
// Check user-level registration (HKCU\Software\Classes\guruconnect)
|
|
let key_path = to_wide(r"Software\Classes\guruconnect");
|
|
if RegOpenKeyExW(
|
|
HKEY_CURRENT_USER,
|
|
PCWSTR(key_path.as_ptr()),
|
|
0,
|
|
KEY_READ,
|
|
&mut key,
|
|
)
|
|
.is_ok()
|
|
{
|
|
let _ = RegCloseKey(key);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn is_protocol_handler_registered() -> bool {
|
|
// On non-Windows, assume not registered (or check ~/.local/share/applications)
|
|
false
|
|
}
|
|
|
|
/// Parse a guruconnect:// URL and extract session parameters
|
|
pub fn parse_protocol_url(url_str: &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
|
|
//
|
|
// 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))?;
|
|
|
|
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()
|
|
.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
|
|
));
|
|
} else {
|
|
path.split('/').next().unwrap_or("").to_string()
|
|
};
|
|
|
|
if session_id.is_empty() {
|
|
return Err(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()
|
|
}
|