All checks were successful
SPEC-002 Phase 1 Task 6, code-reviewed APPROVED (2 rounds), locally verified (cargo fmt + clippy -D warnings exit 0 + cargo test --workspace 70 pass + build). - Viewer WH_KEYBOARD_LL hook diverts system combos (Win/Win+R, Alt+Tab, Alt+Esc, Ctrl+Esc) to the remote as a full KeyEvent (vk + scan + is_extended + modifiers) and suppresses local handling - GATED on the viewer window having focus AND a "send system keys" toggle (default on; Pause/Break host-key), so it never bricks the technician's local keyboard when unfocused. - Agent injection via SendInput KEYEVENTF_SCANCODE + correct KEYEVENTF_EXTENDEDKEY (right Ctrl/Alt, arrows, nav, Win, NumLock, numpad Divide) - layout-independent, extended-key-correct. - Ctrl+Alt+Del completes through the SAS helper (SYSTEM SendSAS); installer sets the SoftwareSASGeneration policy; 3-tier fail-loud (no false success). SAS named pipe DACL tightened from NULL/Everyone to Authenticated Users. - Modifier hygiene: viewer emits key-ups for held Ctrl/Alt/Shift/Win on focus loss / close so modifiers never stick on the remote. - proto: KeyEvent.is_extended = 7 (additive; older agents derive the flag). Closes Win+R / Ctrl+C-V / Ctrl+Alt+Del / arrows-vs-numpad fidelity. Live on-device testing is plan Task 8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
743 lines
24 KiB
Rust
743 lines
24 KiB
Rust
//! 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::sync::mpsc;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::{Context, Result};
|
|
use windows::core::{s, w};
|
|
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
|
|
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";
|
|
|
|
// Windows named pipe constants
|
|
const PIPE_ACCESS_DUPLEX: u32 = 0x00000003;
|
|
const PIPE_TYPE_MESSAGE: u32 = 0x00000004;
|
|
const PIPE_READMODE_MESSAGE: u32 = 0x00000002;
|
|
const PIPE_WAIT: u32 = 0x00000000;
|
|
const PIPE_UNLIMITED_INSTANCES: u32 = 255;
|
|
const INVALID_HANDLE_VALUE: isize = -1;
|
|
/// SDDL revision passed to `ConvertStringSecurityDescriptorToSecurityDescriptorW`
|
|
/// (`SDDL_REVISION_1`).
|
|
const SDDL_REVISION_1: u32 = 1;
|
|
|
|
/// Restrictive DACL for the SAS named pipe, in SDDL form.
|
|
///
|
|
/// `D:` introduces the DACL; `(A;;GA;;;AU)` is an ACE granting GENERIC_ALL (`GA`) to
|
|
/// Authenticated Users (`AU`). Anonymous / null-session callers are NOT authenticated and
|
|
/// are therefore denied — closing the original NULL-DACL hole where any local process
|
|
/// (Everyone) could connect and make this SYSTEM service raise the secure-attention
|
|
/// screen. The agent runs in the interactive logon session and IS an authenticated user,
|
|
/// so it can still connect and request a SAS.
|
|
const PIPE_SDDL: &str = "D:(A;;GA;;;AU)";
|
|
|
|
// FFI declarations for named pipe operations
|
|
#[link(name = "kernel32")]
|
|
extern "system" {
|
|
fn CreateNamedPipeW(
|
|
lpName: *const u16,
|
|
dwOpenMode: u32,
|
|
dwPipeMode: u32,
|
|
nMaxInstances: u32,
|
|
nOutBufferSize: u32,
|
|
nInBufferSize: u32,
|
|
nDefaultTimeOut: u32,
|
|
lpSecurityAttributes: *mut SECURITY_ATTRIBUTES,
|
|
) -> isize;
|
|
|
|
fn ConnectNamedPipe(hNamedPipe: isize, lpOverlapped: *mut std::ffi::c_void) -> i32;
|
|
fn DisconnectNamedPipe(hNamedPipe: isize) -> i32;
|
|
fn CloseHandle(hObject: isize) -> i32;
|
|
fn ReadFile(
|
|
hFile: isize,
|
|
lpBuffer: *mut u8,
|
|
nNumberOfBytesToRead: u32,
|
|
lpNumberOfBytesRead: *mut u32,
|
|
lpOverlapped: *mut std::ffi::c_void,
|
|
) -> i32;
|
|
fn WriteFile(
|
|
hFile: isize,
|
|
lpBuffer: *const u8,
|
|
nNumberOfBytesToWrite: u32,
|
|
lpNumberOfBytesWritten: *mut u32,
|
|
lpOverlapped: *mut std::ffi::c_void,
|
|
) -> i32;
|
|
fn FlushFileBuffers(hFile: isize) -> i32;
|
|
fn LocalFree(hMem: *mut std::ffi::c_void) -> *mut std::ffi::c_void;
|
|
}
|
|
|
|
#[link(name = "advapi32")]
|
|
extern "system" {
|
|
/// Build a self-relative security descriptor from an SDDL string. The descriptor is
|
|
/// allocated with `LocalAlloc` and must be released with `LocalFree`.
|
|
fn ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
StringSecurityDescriptor: *const u16,
|
|
StringSDRevision: u32,
|
|
SecurityDescriptor: *mut *mut std::ffi::c_void,
|
|
SecurityDescriptorSize: *mut u32,
|
|
) -> i32;
|
|
}
|
|
|
|
// Field names mirror the Win32 SECURITY_ATTRIBUTES ABI struct.
|
|
#[allow(non_snake_case)]
|
|
#[repr(C)]
|
|
struct SECURITY_ATTRIBUTES {
|
|
nLength: u32,
|
|
lpSecurityDescriptor: *mut u8,
|
|
bInheritHandle: i32,
|
|
}
|
|
|
|
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 <command>");
|
|
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<OsString>) {
|
|
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 {
|
|
// Build a restrictive security descriptor from SDDL: grant access only to
|
|
// Authenticated Users (excludes anonymous / null-session callers). See PIPE_SDDL.
|
|
let sddl: Vec<u16> = PIPE_SDDL.encode_utf16().chain(std::iter::once(0)).collect();
|
|
let mut sd_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
|
|
let converted = unsafe {
|
|
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
sddl.as_ptr(),
|
|
SDDL_REVISION_1,
|
|
&mut sd_ptr,
|
|
std::ptr::null_mut(),
|
|
)
|
|
};
|
|
if converted == 0 || sd_ptr.is_null() {
|
|
let err = std::io::Error::last_os_error();
|
|
tracing::error!(
|
|
"Failed to build pipe security descriptor from SDDL: {}",
|
|
err
|
|
);
|
|
std::thread::sleep(Duration::from_secs(1));
|
|
continue;
|
|
}
|
|
|
|
let mut sa = SECURITY_ATTRIBUTES {
|
|
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
|
lpSecurityDescriptor: sd_ptr as *mut u8,
|
|
bInheritHandle: 0,
|
|
};
|
|
|
|
// Create the pipe name as wide string
|
|
let pipe_name: Vec<u16> = PIPE_NAME.encode_utf16().chain(std::iter::once(0)).collect();
|
|
|
|
// Create the named pipe
|
|
let pipe = unsafe {
|
|
CreateNamedPipeW(
|
|
pipe_name.as_ptr(),
|
|
PIPE_ACCESS_DUPLEX,
|
|
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
|
PIPE_UNLIMITED_INSTANCES,
|
|
512,
|
|
512,
|
|
0,
|
|
&mut sa,
|
|
)
|
|
};
|
|
|
|
// CreateNamedPipeW copies the descriptor into the kernel object, so the SDDL-built
|
|
// copy can be freed now regardless of success.
|
|
unsafe {
|
|
LocalFree(sd_ptr);
|
|
}
|
|
|
|
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, std::ptr::null_mut()) };
|
|
if connected == 0 {
|
|
let err = std::io::Error::last_os_error();
|
|
// ERROR_PIPE_CONNECTED (535) means client connected between Create and Connect
|
|
if err.raw_os_error() != Some(535) {
|
|
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,
|
|
buffer.as_mut_ptr(),
|
|
buffer.len() as u32,
|
|
&mut bytes_read,
|
|
std::ptr::null_mut(),
|
|
)
|
|
};
|
|
|
|
if read_result != 0 && 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;
|
|
unsafe {
|
|
WriteFile(
|
|
pipe,
|
|
response.as_ptr(),
|
|
response.len() as u32,
|
|
&mut bytes_written,
|
|
std::ptr::null_mut(),
|
|
);
|
|
FlushFileBuffers(pipe);
|
|
}
|
|
}
|
|
|
|
// Disconnect and close the pipe
|
|
unsafe {
|
|
DisconnectNamedPipe(pipe);
|
|
CloseHandle(pipe);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enable the `SoftwareSASGeneration` Winlogon policy so `SendSAS` is permitted.
|
|
///
|
|
/// Without this policy, `sas.dll!SendSAS` is a silent no-op even when called from
|
|
/// SYSTEM. The value lives at
|
|
/// `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\SoftwareSASGeneration`
|
|
/// and is a DWORD bitmask:
|
|
/// 0 = none, 1 = services, 2 = ease-of-access apps, 3 = both.
|
|
///
|
|
/// We set `1` (services) because the GuruConnect SAS helper runs as a SYSTEM service.
|
|
/// This is invoked from the SAS service installer; the broader agent installer should
|
|
/// ensure this runs (see `// TODO(installer)` below).
|
|
fn set_software_sas_policy() -> Result<()> {
|
|
use windows::core::PCWSTR;
|
|
use windows::Win32::System::Registry::{
|
|
RegCloseKey, RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_LOCAL_MACHINE, KEY_SET_VALUE,
|
|
REG_DWORD, REG_OPTION_NON_VOLATILE,
|
|
};
|
|
|
|
let subkey: Vec<u16> = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
|
|
.encode_utf16()
|
|
.chain(std::iter::once(0))
|
|
.collect();
|
|
let value_name: Vec<u16> = "SoftwareSASGeneration"
|
|
.encode_utf16()
|
|
.chain(std::iter::once(0))
|
|
.collect();
|
|
// DWORD 1 = allow services to generate a software SAS.
|
|
let data: u32 = 1;
|
|
|
|
unsafe {
|
|
let mut hkey = HKEY::default();
|
|
let status = RegCreateKeyExW(
|
|
HKEY_LOCAL_MACHINE,
|
|
PCWSTR(subkey.as_ptr()),
|
|
0,
|
|
PCWSTR::null(),
|
|
REG_OPTION_NON_VOLATILE,
|
|
KEY_SET_VALUE,
|
|
None,
|
|
&mut hkey,
|
|
None,
|
|
);
|
|
if status.is_err() {
|
|
anyhow::bail!("RegCreateKeyExW(Policies\\System) failed: {:?}", status);
|
|
}
|
|
|
|
let set = RegSetValueExW(
|
|
hkey,
|
|
PCWSTR(value_name.as_ptr()),
|
|
0,
|
|
REG_DWORD,
|
|
Some(&data.to_ne_bytes()),
|
|
);
|
|
let _ = RegCloseKey(hkey);
|
|
|
|
if set.is_err() {
|
|
anyhow::bail!("RegSetValueExW(SoftwareSASGeneration) failed: {:?}", set);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// 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();
|
|
|
|
// Enable the SoftwareSASGeneration policy so SendSAS actually works from the
|
|
// SYSTEM service. TODO(installer): the top-level managed agent installer should
|
|
// also ensure this policy is set (and that this SAS service is installed) as part
|
|
// of unattended deployment, rather than relying on a manual SAS-service install.
|
|
match set_software_sas_policy() {
|
|
Ok(()) => println!("Enabled SoftwareSASGeneration policy (services)"),
|
|
Err(e) => println!(
|
|
"Warning: failed to set SoftwareSASGeneration policy: {}. \
|
|
Ctrl+Alt+Del may not reach the secure desktop until this is set.",
|
|
e
|
|
),
|
|
}
|
|
|
|
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::<String>(&[])
|
|
.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(())
|
|
}
|