From 68eab236bfb8c20bae958ea6c011c5a5bff7b73b Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Sun, 28 Dec 2025 20:34:41 -0700 Subject: [PATCH] Add SAS Service for Ctrl+Alt+Del support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New guruconnect-sas-service binary (runs as SYSTEM) - Named pipe IPC for agent-to-service communication - Multi-tier SAS approach: service > sas.dll > fallback - Service auto-install/uninstall helpers in startup.rs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- agent/Cargo.toml | 10 + agent/src/bin/sas_service.rs | 587 +++++++++++++++++++++++++++++++++++ agent/src/input/keyboard.rs | 23 +- agent/src/main.rs | 1 + agent/src/sas_client.rs | 106 +++++++ agent/src/startup.rs | 103 ++++++ 6 files changed, 822 insertions(+), 8 deletions(-) create mode 100644 agent/src/bin/sas_service.rs create mode 100644 agent/src/sas_client.rs diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 06fc1a4..25cfbd5 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -75,6 +75,8 @@ windows = { version = "0.58", features = [ "Win32_System_Console", "Win32_Security", "Win32_Storage_FileSystem", + "Win32_System_Pipes", + "Win32_System_SystemServices", ]} # Windows service support @@ -84,6 +86,14 @@ windows-service = "0.7" prost-build = "0.13" winres = "0.1" +[[bin]] +name = "guruconnect-agent" +path = "src/main.rs" + +[[bin]] +name = "guruconnect-sas-service" +path = "src/bin/sas_service.rs" + [profile.release] lto = true codegen-units = 1 diff --git a/agent/src/bin/sas_service.rs b/agent/src/bin/sas_service.rs new file mode 100644 index 0000000..f86aa4f --- /dev/null +++ b/agent/src/bin/sas_service.rs @@ -0,0 +1,587 @@ +//! GuruConnect SAS Service +//! +//! Windows Service running as SYSTEM to handle Ctrl+Alt+Del (Secure Attention Sequence). +//! The agent communicates with this service via named pipe IPC. + +use std::ffi::OsString; +use std::io::{Read, Write}; +use std::sync::mpsc; +use std::time::Duration; + +use anyhow::{Context, Result}; +use windows::core::{s, w, PCSTR}; +use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; +use windows::Win32::Security::{ + InitializeSecurityDescriptor, SetSecurityDescriptorDacl, PSECURITY_DESCRIPTOR, + SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR, +}; +use windows::Win32::Storage::FileSystem::{ + FlushFileBuffers, ReadFile, WriteFile, +}; +use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW}; +use windows::Win32::System::Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, PIPE_ACCESS_DUPLEX, + PIPE_READMODE_MESSAGE, PIPE_TYPE_MESSAGE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT, +}; +use windows::Win32::System::SystemServices::SECURITY_DESCRIPTOR_REVISION; +use windows_service::{ + define_windows_service, + service::{ + ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode, + ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, + service_manager::{ServiceManager, ServiceManagerAccess}, +}; + +// 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 PIPE_NAME: &str = r"\\.\pipe\guruconnect-sas"; +const INSTALL_DIR: &str = r"C:\Program Files\GuruConnect"; + +fn main() { + // Set up logging + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_target(false) + .init(); + + match std::env::args().nth(1).as_deref() { + Some("install") => { + if let Err(e) = install_service() { + eprintln!("Failed to install service: {}", e); + std::process::exit(1); + } + } + Some("uninstall") => { + if let Err(e) = uninstall_service() { + eprintln!("Failed to uninstall service: {}", e); + std::process::exit(1); + } + } + Some("start") => { + if let Err(e) = start_service() { + eprintln!("Failed to start service: {}", e); + std::process::exit(1); + } + } + Some("stop") => { + if let Err(e) = stop_service() { + eprintln!("Failed to stop service: {}", e); + std::process::exit(1); + } + } + Some("status") => { + if let Err(e) = query_status() { + eprintln!("Failed to query status: {}", e); + std::process::exit(1); + } + } + Some("service") => { + // Called by SCM when service starts + if let Err(e) = run_as_service() { + eprintln!("Service error: {}", e); + std::process::exit(1); + } + } + Some("test") => { + // Test mode: run pipe server directly (for debugging) + println!("Running in test mode (not as service)..."); + if let Err(e) = run_pipe_server() { + eprintln!("Pipe server error: {}", e); + std::process::exit(1); + } + } + _ => { + print_usage(); + } + } +} + +fn print_usage() { + println!("GuruConnect SAS Service"); + println!(); + println!("Usage: guruconnect-sas-service "); + println!(); + println!("Commands:"); + println!(" install Install the service"); + println!(" uninstall Remove the service"); + println!(" start Start the service"); + println!(" stop Stop the service"); + println!(" status Query service status"); + println!(" test Run in test mode (not as service)"); +} + +// Generate the Windows service boilerplate +define_windows_service!(ffi_service_main, service_main); + +/// Entry point called by the Windows Service Control Manager +fn run_as_service() -> Result<()> { + service_dispatcher::start(SERVICE_NAME, ffi_service_main) + .context("Failed to start service dispatcher")?; + Ok(()) +} + +/// Main service function called by the SCM +fn service_main(_arguments: Vec) { + if let Err(e) = run_service() { + tracing::error!("Service error: {}", e); + } +} + +/// The actual service implementation +fn run_service() -> Result<()> { + // Create a channel to receive stop events + let (shutdown_tx, shutdown_rx) = mpsc::channel(); + + // Create the service control handler + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Stop | ServiceControl::Shutdown => { + tracing::info!("Received stop/shutdown command"); + let _ = shutdown_tx.send(()); + ServiceControlHandlerResult::NoError + } + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register the service control handler + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler) + .context("Failed to register service control handler")?; + + // Report that we're starting + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::StartPending, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::from_secs(5), + process_id: None, + }) + .ok(); + + // Report that we're running + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }) + .ok(); + + tracing::info!("GuruConnect SAS Service started"); + + // Run the pipe server in a separate thread + let pipe_handle = std::thread::spawn(|| { + if let Err(e) = run_pipe_server() { + tracing::error!("Pipe server error: {}", e); + } + }); + + // Wait for shutdown signal + let _ = shutdown_rx.recv(); + + tracing::info!("Shutting down..."); + + // Report that we're stopping + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::StopPending, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::from_secs(3), + process_id: None, + }) + .ok(); + + // The pipe thread will exit when the service stops + drop(pipe_handle); + + // Report stopped + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }) + .ok(); + + Ok(()) +} + +/// Run the named pipe server +fn run_pipe_server() -> Result<()> { + tracing::info!("Starting pipe server on {}", PIPE_NAME); + + loop { + // Create the pipe with security that allows all authenticated users + let pipe = unsafe { + // Create a security descriptor that allows everyone + let mut sd = SECURITY_DESCRIPTOR::default(); + InitializeSecurityDescriptor( + PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _), + SECURITY_DESCRIPTOR_REVISION, + )?; + + // Set NULL DACL = allow everyone + SetSecurityDescriptorDacl( + PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _), + true, + None, + false, + )?; + + let mut sa = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: &mut sd as *mut _ as *mut _, + bInheritHandle: false.into(), + }; + + let pipe_name: Vec = PIPE_NAME.encode_utf16().chain(std::iter::once(0)).collect(); + + CreateNamedPipeW( + windows::core::PCWSTR(pipe_name.as_ptr()), + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + 512, + 512, + 0, + Some(&mut sa), + ) + }; + + if pipe == INVALID_HANDLE_VALUE { + tracing::error!("Failed to create named pipe"); + std::thread::sleep(Duration::from_secs(1)); + continue; + } + + tracing::info!("Waiting for client connection..."); + + // Wait for a client to connect + let connected = unsafe { ConnectNamedPipe(pipe, None) }; + if connected.is_err() { + // ERROR_PIPE_CONNECTED means client connected between CreateNamedPipe and ConnectNamedPipe + // That's OK, continue processing + let err = std::io::Error::last_os_error(); + if err.raw_os_error() != Some(535) { + // 535 = ERROR_PIPE_CONNECTED + tracing::warn!("ConnectNamedPipe error: {}", err); + } + } + + tracing::info!("Client connected"); + + // Read command from pipe + let mut buffer = [0u8; 512]; + let mut bytes_read = 0u32; + + let read_result = unsafe { + ReadFile( + pipe, + Some(&mut buffer), + Some(&mut bytes_read), + None, + ) + }; + + if read_result.is_ok() && bytes_read > 0 { + let command = String::from_utf8_lossy(&buffer[..bytes_read as usize]); + let command = command.trim(); + + 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" + } + } + } + "ping" => { + tracing::info!("Ping received"); + "pong\n" + } + _ => { + tracing::warn!("Unknown command: {}", command); + "unknown\n" + } + }; + + // Write response + let mut bytes_written = 0u32; + let _ = unsafe { + WriteFile( + pipe, + Some(response.as_bytes()), + Some(&mut bytes_written), + None, + ) + }; + let _ = unsafe { FlushFileBuffers(pipe) }; + } + + // Disconnect and close the pipe + unsafe { + DisconnectNamedPipe(pipe); + CloseHandle(pipe); + } + } +} + +/// Call SendSAS via sas.dll +fn send_sas() -> Result<()> { + unsafe { + let lib = LoadLibraryW(w!("sas.dll")).context("Failed to load sas.dll")?; + + let proc = GetProcAddress(lib, s!("SendSAS")); + if proc.is_none() { + anyhow::bail!("SendSAS function not found in sas.dll"); + } + + // SendSAS takes a BOOL parameter: FALSE (0) = Ctrl+Alt+Del + type SendSASFn = unsafe extern "system" fn(i32); + let send_sas_fn: SendSASFn = std::mem::transmute(proc.unwrap()); + + tracing::info!("Calling SendSAS(0)..."); + send_sas_fn(0); + + Ok(()) + } +} + +/// Install the service +fn install_service() -> Result<()> { + println!("Installing GuruConnect SAS Service..."); + + // 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)); + + // Create install directory + std::fs::create_dir_all(INSTALL_DIR).context("Failed to create install directory")?; + + // Copy binary + println!("Copying binary to: {:?}", binary_dest); + std::fs::copy(¤t_exe, &binary_dest).context("Failed to copy binary")?; + + // Open service manager + let manager = ServiceManager::local_computer( + None::<&str>, + ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE, + ) + .context("Failed to connect to Service Control Manager. Run as Administrator.")?; + + // Check if service exists and remove it + if let Ok(service) = manager.open_service( + SERVICE_NAME, + ServiceAccess::QUERY_STATUS | ServiceAccess::DELETE | ServiceAccess::STOP, + ) { + println!("Removing existing service..."); + + if let Ok(status) = service.query_status() { + if status.current_state != ServiceState::Stopped { + let _ = service.stop(); + std::thread::sleep(Duration::from_secs(2)); + } + } + + service.delete().context("Failed to delete existing service")?; + drop(service); + std::thread::sleep(Duration::from_secs(2)); + } + + // Create the service + let service_info = ServiceInfo { + name: OsString::from(SERVICE_NAME), + display_name: OsString::from(SERVICE_DISPLAY_NAME), + service_type: ServiceType::OWN_PROCESS, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: binary_dest.clone(), + launch_arguments: vec![OsString::from("service")], + dependencies: vec![], + account_name: None, // LocalSystem + account_password: None, + }; + + let service = manager + .create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START) + .context("Failed to create service")?; + + // Set description + service + .set_description(SERVICE_DESCRIPTION) + .context("Failed to set service description")?; + + // Configure recovery + let _ = std::process::Command::new("sc") + .args([ + "failure", + SERVICE_NAME, + "reset=86400", + "actions=restart/5000/restart/5000/restart/5000", + ]) + .output(); + + println!("\n** GuruConnect SAS Service installed successfully!"); + println!("\nBinary: {:?}", binary_dest); + println!("\nStarting service..."); + + // Start the service + start_service()?; + + Ok(()) +} + +/// Uninstall the service +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 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, + ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE, + ) { + Ok(service) => { + if let Ok(status) = service.query_status() { + if status.current_state != ServiceState::Stopped { + println!("Stopping service..."); + let _ = service.stop(); + std::thread::sleep(Duration::from_secs(3)); + } + } + + println!("Deleting service..."); + service.delete().context("Failed to delete service")?; + } + Err(_) => { + println!("Service was not installed"); + } + } + + // Remove binary + if binary_path.exists() { + std::thread::sleep(Duration::from_secs(1)); + if let Err(e) = std::fs::remove_file(&binary_path) { + println!("Warning: Failed to remove binary: {}", e); + } + } + + println!("\n** GuruConnect SAS Service uninstalled successfully!"); + + Ok(()) +} + +/// 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 service = manager + .open_service(SERVICE_NAME, ServiceAccess::START | ServiceAccess::QUERY_STATUS) + .context("Failed to open service. Is it installed?")?; + + service.start::(&[]).context("Failed to start service")?; + + std::thread::sleep(Duration::from_secs(1)); + + let status = service.query_status()?; + match status.current_state { + ServiceState::Running => println!("** Service started successfully"), + ServiceState::StartPending => println!("** Service is starting..."), + other => println!("Service state: {:?}", other), + } + + Ok(()) +} + +/// 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 service = manager + .open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::QUERY_STATUS) + .context("Failed to open service")?; + + service.stop().context("Failed to stop service")?; + + std::thread::sleep(Duration::from_secs(1)); + + let status = service.query_status()?; + match status.current_state { + ServiceState::Stopped => println!("** Service stopped"), + ServiceState::StopPending => println!("** Service is stopping..."), + other => println!("Service state: {:?}", other), + } + + Ok(()) +} + +/// Query service status +fn query_status() -> Result<()> { + 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) => { + let status = service.query_status()?; + println!("GuruConnect SAS Service"); + println!("======================="); + println!("Name: {}", SERVICE_NAME); + println!("State: {:?}", status.current_state); + println!("Binary: {}\\guruconnect-sas-service.exe", INSTALL_DIR); + println!("Pipe: {}", PIPE_NAME); + } + Err(_) => { + println!("GuruConnect SAS Service"); + println!("======================="); + println!("Status: NOT INSTALLED"); + println!("\nTo install: guruconnect-sas-service install"); + } + } + + Ok(()) +} diff --git a/agent/src/input/keyboard.rs b/agent/src/input/keyboard.rs index 4bb180a..f99a90e 100644 --- a/agent/src/input/keyboard.rs +++ b/agent/src/input/keyboard.rs @@ -129,15 +129,21 @@ impl KeyboardController { /// Send Secure Attention Sequence (Ctrl+Alt+Delete) /// - /// Note: This requires special privileges on Windows. - /// The agent typically needs to run as SYSTEM or use SAS API. + /// This uses a multi-tier approach: + /// 1. Try the GuruConnect SAS Service (runs as SYSTEM, handles via named pipe) + /// 2. Try the sas.dll directly (requires SYSTEM privileges) + /// 3. Fallback to key simulation (won't work on secure desktop) #[cfg(windows)] pub fn send_sas(&mut self) -> Result<()> { - // Try using the SAS library if available - // For now, we'll attempt to send the key combination - // This won't work in all contexts due to Windows security + // Tier 1: Try the SAS service (named pipe IPC to SYSTEM service) + if let Ok(()) = crate::sas_client::request_sas() { + tracing::info!("SAS sent via GuruConnect SAS Service"); + return Ok(()); + } - // Load the sas.dll and call SendSAS if available + 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; @@ -151,13 +157,14 @@ impl KeyboardController { // SendSAS takes a BOOL parameter: FALSE for Ctrl+Alt+Del let send_sas: extern "system" fn(i32) = std::mem::transmute(proc); send_sas(0); // FALSE = Ctrl+Alt+Del + tracing::info!("SAS sent via direct sas.dll call"); return Ok(()); } } } - // Fallback: Try sending the keys (won't work without proper privileges) - tracing::warn!("SAS library not available, Ctrl+Alt+Del may not work"); + // Tier 3: Fallback - try sending the keys (won't work on secure desktop) + tracing::warn!("SAS service and sas.dll not available, Ctrl+Alt+Del may not work"); // VK codes const VK_CONTROL: u16 = 0x11; diff --git a/agent/src/main.rs b/agent/src/main.rs index 3efaf3b..4e9ae8a 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -16,6 +16,7 @@ mod chat; mod config; mod encoder; mod input; +mod sas_client; mod session; mod startup; mod transport; diff --git a/agent/src/sas_client.rs b/agent/src/sas_client.rs new file mode 100644 index 0000000..2251757 --- /dev/null +++ b/agent/src/sas_client.rs @@ -0,0 +1,106 @@ +//! SAS Client - Named pipe client for communicating with GuruConnect SAS Service +//! +//! The SAS Service runs as SYSTEM and handles Ctrl+Alt+Del requests. +//! This client sends commands to the service via named pipe. + +use std::fs::OpenOptions; +use std::io::{Read, Write}; +use std::time::Duration; + +use anyhow::{Context, Result}; +use tracing::{debug, error, info, warn}; + +const PIPE_NAME: &str = r"\\.\pipe\guruconnect-sas"; +const TIMEOUT_MS: u64 = 5000; + +/// Request Ctrl+Alt+Del (Secure Attention Sequence) via the SAS service +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) + { + Ok(p) => p, + Err(e) => { + warn!("Failed to connect to SAS service pipe: {}", e); + return Err(anyhow::anyhow!( + "SAS service not available. Install with: guruconnect-sas-service install" + )); + } + }; + + debug!("Connected to SAS service pipe"); + + // Send the command + pipe.write_all(b"sas\n") + .context("Failed to send command to SAS service")?; + + // Read the response + let mut response = [0u8; 64]; + let n = pipe.read(&mut response) + .context("Failed to read response from SAS service")?; + + let response_str = String::from_utf8_lossy(&response[..n]); + let response_str = response_str.trim(); + + debug!("SAS service response: {}", response_str); + + match response_str { + "ok" => { + info!("SAS request successful"); + Ok(()) + } + "error" => { + error!("SAS service reported an error"); + Err(anyhow::anyhow!("SAS service failed to send Ctrl+Alt+Del")) + } + _ => { + error!("Unexpected response from SAS service: {}", response_str); + Err(anyhow::anyhow!("Unexpected SAS service response: {}", response_str)) + } + } +} + +/// 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) + { + // Send a ping command + if pipe.write_all(b"ping\n").is_ok() { + let mut response = [0u8; 64]; + if let Ok(n) = pipe.read(&mut response) { + let response_str = String::from_utf8_lossy(&response[..n]); + return response_str.trim() == "pong"; + } + } + } + false +} + +/// Get information about SAS service status +pub fn get_service_status() -> String { + if is_service_available() { + "SAS service is running and responding".to_string() + } else { + "SAS service is not available".to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_service_check() { + // This test just checks the function runs without panicking + let available = is_service_available(); + println!("SAS service available: {}", available); + } +} diff --git a/agent/src/startup.rs b/agent/src/startup.rs index 9f81f4f..728596a 100644 --- a/agent/src/startup.rs +++ b/agent/src/startup.rs @@ -177,6 +177,93 @@ pub fn uninstall() -> Result<()> { Ok(()) } +/// Install the SAS service if the binary is available +/// This allows the agent to send Ctrl+Alt+Del even without SYSTEM privileges +#[cfg(windows)] +pub fn install_sas_service() -> Result<()> { + info!("Attempting to install SAS service..."); + + // 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 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"); + if !program_files.exists() { + warn!("SAS service binary not found"); + return Ok(()); + } + } + + // Run the install command + let sas_path = if sas_binary.exists() { + sas_binary + } else { + std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe") + }; + + let output = std::process::Command::new(&sas_path) + .arg("install") + .output(); + + match output { + Ok(result) => { + if result.status.success() { + info!("SAS service installed successfully"); + } else { + let stderr = String::from_utf8_lossy(&result.stderr); + warn!("SAS service install failed: {}", stderr); + } + } + Err(e) => { + warn!("Failed to run SAS service installer: {}", e); + } + } + + Ok(()) +} + +/// Uninstall the SAS service +#[cfg(windows)] +pub fn uninstall_sas_service() -> Result<()> { + info!("Attempting to uninstall SAS service..."); + + // 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")), + ]; + + 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(); + + if let Ok(result) = output { + if result.status.success() { + info!("SAS service uninstalled successfully"); + return Ok(()); + } + } + } + } + } + + warn!("SAS service binary not found for uninstall"); + Ok(()) +} + +/// Check if the SAS service is installed and running +#[cfg(windows)] +pub fn check_sas_service() -> bool { + use crate::sas_client; + sas_client::is_service_available() +} + #[cfg(not(windows))] pub fn add_to_startup() -> Result<()> { warn!("Startup persistence not implemented for this platform"); @@ -193,3 +280,19 @@ pub fn uninstall() -> Result<()> { warn!("Uninstall not implemented for this platform"); Ok(()) } + +#[cfg(not(windows))] +pub fn install_sas_service() -> Result<()> { + warn!("SAS service only available on Windows"); + Ok(()) +} + +#[cfg(not(windows))] +pub fn uninstall_sas_service() -> Result<()> { + Ok(()) +} + +#[cfg(not(windows))] +pub fn check_sas_service() -> bool { + false +}