diff --git a/Cargo.lock b/Cargo.lock index 197a5ad5c..f49b7815d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1874,6 +1874,8 @@ dependencies = [ "toml", "webrtc-dtls", "webrtc-util", + "windows", + "windows-service", ] [[package]] @@ -3861,6 +3863,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-service" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +dependencies = [ + "bitflags 2.9.1", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "windows-strings" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 5425e1627..8856713bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,24 @@ sha2 = "0.10.8" [target.'cfg(unix)'.dependencies] libc = "0.2.148" +[target.'cfg(windows)'.dependencies] +windows-service = "0.7.0" +windows = { version = "0.61.2", features = [ + "Win32_Security", + "Win32_System_Services", + "Win32_System_Registry", + "Win32_Foundation", + "Win32_UI_WindowsAndMessaging", + "Win32_System_EventLog", + "Win32_Storage_FileSystem", + "Win32_System_IO", + "Win32_System_Threading", + "Win32_System_RemoteDesktop", + "Win32_System_Environment", + "Win32_System_Diagnostics_ToolHelp", + "Win32_System_Console", +] } + [features] default = [ "gtk", diff --git a/input-emulation/Cargo.toml b/input-emulation/Cargo.toml index e2c98b138..305d838c1 100644 --- a/input-emulation/Cargo.toml +++ b/input-emulation/Cargo.toml @@ -59,6 +59,10 @@ windows = { version = "0.61.2", features = [ "Win32_Graphics_Gdi", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", + "Win32_System_StationsAndDesktops", + "Win32_System_RemoteDesktop", + "Win32_System_Shutdown", + "Win32_Security", ] } [features] diff --git a/input-emulation/src/windows.rs b/input-emulation/src/windows.rs index 6610ec257..5a713d5cb 100644 --- a/input-emulation/src/windows.rs +++ b/input-emulation/src/windows.rs @@ -8,6 +8,11 @@ use async_trait::async_trait; use std::ops::BitOrAssign; use std::time::Duration; use tokio::task::AbortHandle; +use windows::Win32::System::StationsAndDesktops::{ + CloseDesktop, DESKTOP_ACCESS_FLAGS, DESKTOP_CONTROL_FLAGS, GetThreadDesktop, OpenInputDesktop, + SetThreadDesktop, +}; +use windows::Win32::System::Threading::GetCurrentThreadId; use windows::Win32::UI::Input::KeyboardAndMouse::{ INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, @@ -21,16 +26,35 @@ use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2}; use super::{Emulation, EmulationHandle}; +// Desktop access rights for input injection +// GENERIC_WRITE (0x40000000) + DESKTOP_CREATEWINDOW (0x0002) + DESKTOP_HOOKCONTROL (0x0008) +// DF_ALLOWOTHERACCOUNTHOOK (0x0001) allows accessing desktops owned by other accounts +const GENERIC_WRITE: u32 = 0x40000000; +const DESKTOP_CREATEWINDOW: u32 = 0x0002; +const DESKTOP_HOOKCONTROL: u32 = 0x0008; +const DF_ALLOWOTHERACCOUNTHOOK: u32 = 0x0001; +const DESKTOP_ACCESS_FOR_INPUT: u32 = DESKTOP_CREATEWINDOW | DESKTOP_HOOKCONTROL | GENERIC_WRITE; + const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32); +// Linux keycodes for modifier tracking +const KEY_LEFT_META: u32 = 125; +const KEY_RIGHT_META: u32 = 126; +// Linux keycode for L +const KEY_L: u32 = 38; + pub(crate) struct WindowsEmulation { repeat_task: Option, + meta_pressed: bool, } impl WindowsEmulation { pub(crate) fn new() -> Result { - Ok(Self { repeat_task: None }) + Ok(Self { + repeat_task: None, + meta_pressed: false, + }) } } @@ -60,6 +84,20 @@ impl Emulation for WindowsEmulation { key, state, } => { + // Track Meta/Super key state + if key == KEY_LEFT_META || key == KEY_RIGHT_META { + self.meta_pressed = state == 1; + } + + // Intercept Win+L: LockWorkStation() cannot be triggered + // via SendInput because Windows blocks it as a Secure + // Attention Sequence. Instead we lock the session directly. + if key == KEY_L && state == 1 && self.meta_pressed { + log::info!("Win+L detected, locking workstation"); + lock_workstation(); + return Ok(()); + } + match state { // pressed 0 => self.kill_repeat_task(), @@ -103,14 +141,51 @@ impl WindowsEmulation { } } +/// Send input with desktop switching to handle UAC prompts and other secure desktops. +/// When running in a user session (spawned by the Windows service), this allows +/// input injection on the Secure Desktop (UAC prompts) by temporarily switching +/// to the current input desktop. fn send_input_safe(input: INPUT) { unsafe { - loop { - /* retval = number of successfully submitted events */ - if SendInput(&[input], std::mem::size_of::() as i32) > 0 { - break; + // Try to open the current input desktop (may be Secure Desktop during UAC) + // This only works when running in the user's session, not from Session 0 + let input_desktop = match OpenInputDesktop( + DESKTOP_CONTROL_FLAGS(DF_ALLOWOTHERACCOUNTHOOK), + true, // fInherit + DESKTOP_ACCESS_FLAGS(DESKTOP_ACCESS_FOR_INPUT), + ) { + Ok(desktop) => desktop, + Err(e) => { + // Desktop switching not available - fall back to direct SendInput + // This works for normal desktop but won't reach UAC/login screen + log::debug!("OpenInputDesktop failed: {} - using direct SendInput", e); + SendInput(&[input], std::mem::size_of::() as i32); + return; } + }; + + // Save current desktop, switch to input desktop, send input, restore + let old_desktop = GetThreadDesktop(GetCurrentThreadId()); + + if SetThreadDesktop(input_desktop).is_err() { + log::warn!("SetThreadDesktop failed, using direct SendInput"); + let _ = CloseDesktop(input_desktop); + SendInput(&[input], std::mem::size_of::() as i32); + return; + } + + let count = SendInput(&[input], std::mem::size_of::() as i32); + if count == 0 { + log::warn!("SendInput failed after desktop switch"); } + + // Restore original desktop + if let Ok(desktop) = old_desktop { + if !desktop.is_invalid() { + let _ = SetThreadDesktop(desktop); + } + } + let _ = CloseDesktop(input_desktop); } } @@ -235,3 +310,36 @@ fn linux_keycode_to_windows_scancode(linux_keycode: u32) -> Option { log::trace!("windows code: {windows_scancode:?}"); Some(windows_scancode as u16) } + +/// Lock the workstation. +/// +/// `Win+L` is a Secure Attention Sequence that Windows blocks from being +/// injected via `SendInput`. When running inside a user session (Session != 0) +/// we can call `LockWorkStation()` which is the documented public API. +/// +/// When running in Session 0 (the service session) there is no interactive +/// desktop to lock, so we disconnect the console session via +/// `WTSDisconnectSession` which achieves the same visible effect (returns to +/// the lock / login screen). +fn lock_workstation() { + // Try the simple path first — works when we are in the user's session. + unsafe { + use windows::Win32::System::Shutdown::LockWorkStation; + if LockWorkStation().is_ok() { + log::info!("LockWorkStation succeeded"); + return; + } + log::warn!("LockWorkStation failed, trying WTSDisconnectSession"); + + // Fallback for Session 0: disconnect the active console session. + use windows::Win32::System::RemoteDesktop::{ + WTS_CURRENT_SERVER_HANDLE, WTSDisconnectSession, WTSGetActiveConsoleSessionId, + }; + let session_id = WTSGetActiveConsoleSessionId(); + if WTSDisconnectSession(Some(WTS_CURRENT_SERVER_HANDLE), session_id, true).is_err() { + log::error!("WTSDisconnectSession also failed"); + } else { + log::info!("WTSDisconnectSession succeeded (session {})", session_id); + } + } +} diff --git a/run.ps1 b/run.ps1 new file mode 100644 index 000000000..696d52dc4 --- /dev/null +++ b/run.ps1 @@ -0,0 +1,137 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Build and run lan-mouse (as service or directly) +.DESCRIPTION + Optionally builds lan-mouse in debug mode, stops any running service, copies binaries to target/svc/, + then either starts the Windows service or runs the executable directly. +.PARAMETER Build + Build lan-mouse before deployment +.PARAMETER Service + Start the lan-mouse Windows service after deployment +.PARAMETER Direct + Run ./target/svc/lan-mouse.exe directly (not as a service) +.PARAMETER Install + If specified with -Service, registers the service via 'lan-mouse install' before starting +.PARAMETER Clean + Truncate all log files in C:\ProgramData\lan-mouse\ before starting +.EXAMPLE + .\run.ps1 -Build -Service + .\run.ps1 -Build -Service -Install + .\run.ps1 -Build -Direct + .\run.ps1 -Direct + .\run.ps1 -Clean -Service +#> + +param( + [switch]$Build, + [switch]$Service, + [switch]$Direct, + [switch]$Install, + [switch]$Clean +) + +if (-not $Service -and -not $Direct) { + Write-Host "Error: You must specify either -Service or -Direct" -ForegroundColor Red + Write-Host " -Service Start the lan-mouse Windows service" + Write-Host " -Direct Run the executable directly" + exit 1 +} + +if ($Service -and $Direct) { + Write-Host "Error: Cannot specify both -Service and -Direct" -ForegroundColor Red + exit 1 +} + +$ErrorActionPreference = "Stop" + +# Change to repository root +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Push-Location $ScriptDir + +try { + if ($Build) { + Write-Host "Building lan-mouse (debug, no default features)..." -ForegroundColor Cyan + cargo build --no-default-features + if ($LASTEXITCODE -ne 0) { + throw "Build failed with exit code $LASTEXITCODE" + } + } + + Write-Host "`nStopping lan-mouse service..." -ForegroundColor Cyan + $svcInfo = Get-Service -Name "lan-mouse" -ErrorAction SilentlyContinue + if ($svcInfo -and $svcInfo.Status -eq "Running") { + try { + Stop-Service -Name "lan-mouse" -Force -ErrorAction Stop + Write-Host "Service stopped" -ForegroundColor Green + } catch { + Write-Host "Stop-Service failed: $($_.Exception.Message)" -ForegroundColor Yellow + Write-Host "Attempting to kill lan-mouse process..." -ForegroundColor Yellow + $proc = Get-Process -Name "lan-mouse" -ErrorAction SilentlyContinue + if ($proc) { + $proc | Stop-Process -Force + Write-Host "Process killed" -ForegroundColor Green + } else { + Write-Host "No lan-mouse process found" -ForegroundColor Yellow + } + } + } elseif ($svcInfo) { + Write-Host "Service exists but not running (status: $($svcInfo.Status))" -ForegroundColor Yellow + # Still check for orphan process + $proc = Get-Process -Name "lan-mouse" -ErrorAction SilentlyContinue + if ($proc) { + Write-Host "Found orphan lan-mouse process, killing..." -ForegroundColor Yellow + $proc | Stop-Process -Force + Write-Host "Process killed" -ForegroundColor Green + } + } else { + Write-Host "Service not registered (will install if -Install is used)" -ForegroundColor Yellow + } + + Write-Host "`nCopying binaries to target/svc/..." -ForegroundColor Cyan + $SvcDir = Join-Path $ScriptDir "target\svc" + if (-not (Test-Path $SvcDir)) { + New-Item -ItemType Directory -Path $SvcDir | Out-Null + } + + Copy-Item -Path "target\debug\*" -Destination $SvcDir -Recurse -Force + Write-Host "Binaries copied" -ForegroundColor Green + + if ($Clean) { + Write-Host "`nTruncating log files in C:\ProgramData\lan-mouse\..." -ForegroundColor Cyan + $LogDir = "C:\ProgramData\lan-mouse" + Get-ChildItem -Path $LogDir -Filter "*.log" -ErrorAction SilentlyContinue | ForEach-Object { + Clear-Content -Path $_.FullName -ErrorAction SilentlyContinue + Write-Host " Truncated: $($_.Name)" -ForegroundColor Gray + } + } + + if ($Service) { + if ($Install) { + Write-Host "`nInstalling service..." -ForegroundColor Cyan + $ServiceExe = Join-Path $SvcDir "lan-mouse.exe" + & $ServiceExe install + if ($LASTEXITCODE -ne 0) { + throw "Service installation failed with exit code $LASTEXITCODE" + } + Write-Host "Service installed" -ForegroundColor Green + } + + Write-Host "`nStarting lan-mouse service..." -ForegroundColor Cyan + Start-Service -Name "lan-mouse" + Write-Host "Service started" -ForegroundColor Green + + Write-Host "`nDeployment complete!" -ForegroundColor Green + Write-Host "`nTailing service log (Ctrl+C to stop)..." -ForegroundColor Cyan + Get-Content -Wait -Tail 20 "C:\ProgramData\lan-mouse\winsvc.log" + } elseif ($Direct) { + Write-Host "`nRunning lan-mouse directly..." -ForegroundColor Cyan + $ServiceExe = Join-Path $SvcDir "lan-mouse.exe" + & $ServiceExe + } +} catch { + Write-Host "`nError: $_" -ForegroundColor Red + exit 1 +} finally { + Pop-Location +} diff --git a/src/config.rs b/src/config.rs index c0584cc31..a724cc65f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,9 +37,20 @@ fn default_path() -> Result { #[cfg(not(unix))] let default_path = { - let app_data = - env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?)); - format!("{app_data}\\lan-mouse\\") + #[cfg(windows)] + if crate::is_windows_service() { + "C:\\ProgramData\\lan-mouse\\".to_string() + } else { + let app_data = + env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?)); + format!("{app_data}\\lan-mouse\\") + } + #[cfg(not(windows))] + { + let app_data = + env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?)); + format!("{app_data}\\lan-mouse\\") + } }; Ok(PathBuf::from(default_path)) } @@ -111,6 +122,19 @@ pub enum Command { Cli(CliArgs), /// run in daemon mode Daemon, + /// Install as system service (Windows: SCM service, Linux: systemd, macOS: launchd) + #[cfg(windows)] + Install, + /// Uninstall system service + #[cfg(windows)] + Uninstall, + /// Query service status + #[cfg(windows)] + Status, + /// Run as Windows service (internal - spawns session daemons) + #[cfg(windows)] + #[command(hide = true)] + WinSvc, } #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)] @@ -290,6 +314,23 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] = impl Config { pub fn new() -> Result { let args = Args::parse(); + Self::from_args(args) + } + + pub fn new_with_args(args_iter: I) -> Result + where + I: IntoIterator, + T: Into + Clone, + { + let args = Args::parse_from(args_iter); + Self::from_args(args) + } + + fn from_args(args: Args) -> Result { + #[cfg(windows)] + if matches!(args.command, Some(Command::WinSvc)) { + crate::set_is_windows_service(true); + } // --config overrules default location let config_path = args diff --git a/src/emulation.rs b/src/emulation.rs index 853267ca8..13d1e69ff 100644 --- a/src/emulation.rs +++ b/src/emulation.rs @@ -148,7 +148,9 @@ impl ListenTask { self.emulation_proxy.remove(addr); self.listener.reply(addr, ProtoEvent::Ack(0)).await; } - ProtoEvent::Input(event) => self.emulation_proxy.consume(event, addr), + ProtoEvent::Input(input_event) => { + self.emulation_proxy.consume(input_event, addr); + } ProtoEvent::Ping => self.listener.reply(addr, ProtoEvent::Pong(self.emulation_proxy.emulation_active.get())).await, _ => {} } @@ -256,6 +258,8 @@ impl EmulationProxy { self.request_tx .send(ProxyRequest::Input(event, addr)) .expect("channel closed"); + } else { + log::warn!("emulation inactive, dropping event: {:?}", event); } } diff --git a/src/lib.rs b/src/lib.rs index ae9c66e5c..bdb82f03d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,3 +9,21 @@ mod emulation; pub mod emulation_test; mod listen; pub mod service; +#[cfg(windows)] +pub mod windows; +#[cfg(windows)] +pub mod windows_service; + +#[cfg(windows)] +static IS_WINDOWS_SERVICE: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +#[cfg(windows)] +pub fn set_is_windows_service(is_service: bool) { + IS_WINDOWS_SERVICE.store(is_service, std::sync::atomic::Ordering::SeqCst); +} + +#[cfg(windows)] +pub fn is_windows_service() -> bool { + IS_WINDOWS_SERVICE.load(std::sync::atomic::Ordering::SeqCst) +} diff --git a/src/main.rs b/src/main.rs index 0ca54c93d..0cae964ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,10 +11,12 @@ use lan_mouse_cli::CliError; #[cfg(feature = "gtk")] use lan_mouse_gtk::GtkError; use lan_mouse_ipc::{IpcError, IpcListenerCreationError}; +#[cfg(feature = "gtk")] +use std::process::Child; use std::{ future::Future, io, - process::{self, Child}, + process::{self}, }; use thiserror::Error; use tokio::task::LocalSet; @@ -41,62 +43,135 @@ enum LanMouseError { } fn main() { - // init logging - let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info"); - env_logger::init_from_env(env); + let config = match config::Config::new() { + Ok(c) => c, + Err(e) => { + eprintln!("Error loading config: {e}"); + process::exit(1); + } + }; + + let command = config.command(); + init_logging(&command); - if let Err(e) = run() { + if let Err(e) = run(config, command) { log::error!("{e}"); process::exit(1); } } -fn run() -> Result<(), LanMouseError> { - let config = config::Config::new()?; - match config.command() { +fn init_logging(_command: &Option) { + let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info"); + + // On Windows, log to a file only for the background daemon (no console) + // or the Windows service. All other cases (GTK app, CLI, terminal daemon) + // log to stderr via the default init below. + #[cfg(windows)] + { + use windows::Win32::System::Console::GetConsoleWindow; + let has_console = unsafe { !GetConsoleWindow().is_invalid() }; + + let log_file_name = match _command { + Some(Command::Daemon) if !has_console => Some("daemon.log"), + Some(Command::WinSvc) => Some("winsvc.log"), + _ => None, + }; + + if let Some(name) = log_file_name { + let log_dir = std::path::Path::new("C:\\ProgramData\\lan-mouse"); + let _ = std::fs::create_dir_all(log_dir); + let log_path = log_dir.join(name); + + if let Ok(file) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + { + env_logger::Builder::from_env(env) + .format_timestamp_secs() + .target(env_logger::Target::Pipe(Box::new(file))) + .init(); + return; + } + } + } + + env_logger::init_from_env(env); +} + +fn run(config: Config, command: Option) -> Result<(), LanMouseError> { + match command { Some(command) => match command { Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?, Command::TestCapture(args) => run_async(capture_test::run(config, args))?, Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?, - Command::Daemon => { - // if daemon is specified we run the service - match run_async(run_service(config)) { - Err(LanMouseError::Service(ServiceError::IpcListen( - IpcListenerCreationError::AlreadyRunning, - ))) => log::info!("service already running!"), - r => r?, - } + Command::Daemon => match run_async(run_daemon(config)) { + Err(LanMouseError::Service(ServiceError::IpcListen( + IpcListenerCreationError::AlreadyRunning, + ))) => log::info!("service already running!"), + r => r?, + }, + #[cfg(windows)] + Command::Install => { + lan_mouse::windows::install().map_err(io::Error::other)?; + } + #[cfg(windows)] + Command::Uninstall => { + lan_mouse::windows::uninstall().map_err(io::Error::other)?; + } + #[cfg(windows)] + Command::Status => { + lan_mouse::windows_service::service_status().map_err(io::Error::other)?; + } + #[cfg(windows)] + Command::WinSvc => { + // This starts the Windows service dispatcher + lan_mouse::windows_service::run_dispatch().map_err(|e| { + io::Error::other(format!("Failed to start service dispatcher: {e}")) + })?; } }, None => { - // otherwise start the service as a child process and - // run a frontend - #[cfg(feature = "gtk")] - { - let mut service = start_service()?; - let res = lan_mouse_gtk::run(); - #[cfg(unix)] - { - // on unix we give the service a chance to terminate gracefully - let pid = service.id() as libc::pid_t; - unsafe { - libc::kill(pid, libc::SIGINT); - } - service.wait()?; - } - service.kill()?; - res?; - } - #[cfg(not(feature = "gtk"))] - { - // run daemon if gtk is diabled - match run_async(run_service(config)) { - Err(LanMouseError::Service(ServiceError::IpcListen( - IpcListenerCreationError::AlreadyRunning, - ))) => log::info!("service already running!"), - r => r?, - } + // Default behavior: GUI + Daemon + run_gui_and_daemon(config)?; + } + } + + Ok(()) +} + +fn run_gui_and_daemon(_config: Config) -> Result<(), LanMouseError> { + #[cfg(feature = "gtk")] + { + let mut daemon = start_daemon_process()?; + let res = lan_mouse_gtk::run(); + + #[cfg(unix)] + { + // give the daemon a chance to terminate gracefully + let pid = daemon.id() as libc::pid_t; + unsafe { + libc::kill(pid, libc::SIGINT); } + daemon.wait()?; + } + + #[cfg(not(unix))] + { + let _ = daemon.kill(); + let _ = daemon.wait(); + } + + res?; + } + + #[cfg(not(feature = "gtk"))] + { + match run_async(run_daemon(_config)) { + Err(LanMouseError::Service(ServiceError::IpcListen( + IpcListenerCreationError::AlreadyRunning, + ))) => log::info!("daemon already running!"), + r => r?, } } @@ -118,7 +193,8 @@ where Ok(runtime.block_on(LocalSet::new().run_until(f))?) } -fn start_service() -> Result { +#[cfg(feature = "gtk")] +fn start_daemon_process() -> Result { let child = process::Command::new(std::env::current_exe()?) .args(std::env::args().skip(1)) .arg("daemon") @@ -126,13 +202,13 @@ fn start_service() -> Result { Ok(child) } -async fn run_service(config: Config) -> Result<(), ServiceError> { +async fn run_daemon(config: Config) -> Result<(), ServiceError> { let release_bind = config.release_bind(); let config_path = config.config_path().to_owned(); let mut service = Service::new(config).await?; log::info!("using config: {config_path:?}"); log::info!("Press {release_bind:?} to release the mouse"); service.run().await?; - log::info!("service exited!"); + log::info!("daemon exited!"); Ok(()) } diff --git a/src/service.rs b/src/service.rs index f708193e6..72f96a2e7 100644 --- a/src/service.rs +++ b/src/service.rs @@ -142,6 +142,13 @@ impl Service { } pub async fn run(&mut self) -> Result<(), ServiceError> { + self.run_with_shutdown(None).await + } + + pub async fn run_with_shutdown( + &mut self, + mut shutdown: Option>, + ) -> Result<(), ServiceError> { let active = self.client_manager.active_clients(); for handle in active.iter() { // small hack: `activate_client()` checks, if the client @@ -161,7 +168,11 @@ impl Service { event = self.emulation.event() => self.handle_emulation_event(event), event = self.capture.event() => self.handle_capture_event(event), event = self.resolver.event() => self.handle_resolver_event(event), - r = signal::ctrl_c() => break r.expect("failed to wait for CTRL+C"), + r = signal::ctrl_c(), if shutdown.is_none() => break r.expect("failed to wait for CTRL+C"), + _ = async { shutdown.as_mut().unwrap().recv().await }, if shutdown.is_some() => { + log::info!("Shutdown signal received"); + break; + }, } } diff --git a/src/windows.rs b/src/windows.rs new file mode 100644 index 000000000..38537524b --- /dev/null +++ b/src/windows.rs @@ -0,0 +1,236 @@ +//! Windows-specific platform support: install/uninstall commands. +//! +//! This module handles registering lan-mouse as a Windows service +//! and related setup tasks like config/cert migration and firewall rules. + +use lan_mouse_ipc::DEFAULT_PORT; +use log::info; +use std::os::windows::ffi::OsStrExt; +use std::process::Command; +use windows::Win32::System::Registry::{ + HKEY, HKEY_LOCAL_MACHINE, KEY_WRITE, REG_DWORD, REG_OPTION_NON_VOLATILE, REG_SZ, RegCloseKey, + RegCreateKeyExW, RegSetValueExW, +}; +use windows::Win32::System::Services::{ + ChangeServiceConfig2W, ControlService, CreateServiceW, DeleteService, OpenSCManagerW, + OpenServiceW, SC_MANAGER_ALL_ACCESS, SERVICE_ALL_ACCESS, SERVICE_AUTO_START, + SERVICE_CONFIG_DESCRIPTION, SERVICE_CONTROL_STOP, SERVICE_DESCRIPTIONW, SERVICE_ERROR_NORMAL, + SERVICE_WIN32_OWN_PROCESS, StartServiceW, +}; +use windows::core::{HSTRING, PWSTR}; + +/// Install lan-mouse as a Windows service. +pub fn install() -> Result<(), String> { + unsafe { + let scm = OpenSCManagerW(None, None, SC_MANAGER_ALL_ACCESS) + .map_err(|e| format!("Failed to open SCM: {}", e))?; + + let exe_path = std::env::current_exe() + .map_err(|e| format!("Failed to get current exe path: {}", e))?; + + // Add "win-svc" argument to the service command line + let exe_path_str = exe_path.to_str().ok_or("Invalid exe path")?; + let cmd_line = format!("\"{}\" win-svc", exe_path_str); + let cmd_line_h = HSTRING::from(cmd_line); + + let service_name = HSTRING::from("lan-mouse"); + let display_name = HSTRING::from("Lan Mouse"); + + let service = CreateServiceW( + scm, + &service_name, + &display_name, + SERVICE_ALL_ACCESS, + SERVICE_WIN32_OWN_PROCESS, + SERVICE_AUTO_START, + SERVICE_ERROR_NORMAL, + &cmd_line_h, + None, + None, + None, + None, + None, + ) + .map_err(|e| format!("Failed to create service: {}", e))?; + + info!("Service installed successfully"); + + // Copy config to ProgramData + let program_data = std::path::Path::new("C:\\ProgramData\\lan-mouse"); + let _ = std::fs::create_dir_all(program_data); + let dst_config = program_data.join("config.toml"); + + if !dst_config.exists() { + if let Ok(app_data) = std::env::var("LOCALAPPDATA") { + let src_config = std::path::Path::new(&app_data) + .join("lan-mouse") + .join("config.toml"); + if src_config.exists() { + let _ = std::fs::copy(src_config, dst_config); + } + } + } + + // Copy certificate (lan-mouse.pem) from user's LOCALAPPDATA if present + let dst_cert = program_data.join("lan-mouse.pem"); + if !dst_cert.exists() { + if let Ok(app_data) = std::env::var("LOCALAPPDATA") { + let src_cert = std::path::Path::new(&app_data) + .join("lan-mouse") + .join("lan-mouse.pem"); + if src_cert.exists() { + match std::fs::copy(&src_cert, &dst_cert) { + Ok(_) => info!("Copied user certificate to ProgramData: {:?}", dst_cert), + Err(e) => log::warn!("Failed to copy certificate to ProgramData: {}", e), + } + } + } + } + + // Create Windows Firewall rule to allow incoming connections on DEFAULT_PORT + // Use netsh advfirewall to add a rule for domain and private profiles (not Public) + let port = DEFAULT_PORT.to_string(); + let rule_name = format!("Lan Mouse ({})", port); + let netsh_args: Vec = vec![ + "advfirewall".to_string(), + "firewall".to_string(), + "add".to_string(), + "rule".to_string(), + format!("name={}", rule_name), + "dir=in".to_string(), + "action=allow".to_string(), + "protocol=TCP".to_string(), + format!("localport={}", port), + "profile=domain,private".to_string(), + "enable=yes".to_string(), + ]; + + // Run netsh; don't fail install if firewall command fails, just log + match Command::new("netsh").args(&netsh_args).output() { + Ok(output) => { + if output.status.success() { + info!("Firewall rule added: {}", rule_name); + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + log::warn!( + "Failed to add firewall rule (netsh returned non-zero): {}", + stderr + ); + } + } + Err(e) => { + log::warn!("Failed to execute netsh to add firewall rule: {}", e); + } + } + + // Register event source + let sub_key = + HSTRING::from("SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\lan-mouse"); + let mut h_key = HKEY::default(); + if RegCreateKeyExW( + HKEY_LOCAL_MACHINE, + &sub_key, + Some(0), + None, + REG_OPTION_NON_VOLATILE, + KEY_WRITE, + None, + &mut h_key, + None, + ) + .is_ok() + { + let path_wide: Vec = exe_path + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let _ = RegSetValueExW( + h_key, + &HSTRING::from("EventMessageFile"), + Some(0), + REG_SZ, + Some(std::slice::from_raw_parts( + path_wide.as_ptr() as *const u8, + path_wide.len() * 2, + )), + ); + let types_supported = 7u32; + let _ = RegSetValueExW( + h_key, + &HSTRING::from("TypesSupported"), + Some(0), + REG_DWORD, + Some(std::slice::from_raw_parts( + &types_supported as *const u32 as *const u8, + 4, + )), + ); + let _ = RegCloseKey(h_key); + } + + // Try to set service description using ChangeServiceConfig2W (preferred) + let description = "Lan Mouse - share mouse and keyboard across local networks"; + let mut desc_wide: Vec = description + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + let mut svc_desc = SERVICE_DESCRIPTIONW { + lpDescription: PWSTR(desc_wide.as_mut_ptr()), + }; + + let desc_ptr = &mut svc_desc as *mut _ as *const std::ffi::c_void; + // Some Windows versions or environments may not support ChangeServiceConfig2W + // Treat failure to set the description as non-fatal: log and continue. + match ChangeServiceConfig2W(service, SERVICE_CONFIG_DESCRIPTION, Some(desc_ptr)) { + Ok(_) => info!("Service description set via ChangeServiceConfig2W"), + Err(e) => log::warn!("ChangeServiceConfig2W failed (continuing): {}", e), + } + + if let Err(e) = StartServiceW(service, None) { + log::warn!("Failed to start service after installation: {}", e); + } else { + info!("Service started"); + } + + Ok(()) + } +} + +/// Uninstall the lan-mouse Windows service. +/// +/// Stops the service if running, removes service registration, and cleans up +/// registry entries. +pub fn uninstall() -> Result<(), String> { + unsafe { + let scm = OpenSCManagerW(None, None, SC_MANAGER_ALL_ACCESS) + .map_err(|e| format!("Failed to open SCM: {}", e))?; + + let service_name = HSTRING::from("lan-mouse"); + let service = match OpenServiceW(scm, &service_name, SERVICE_ALL_ACCESS) { + Ok(s) => s, + Err(e) => { + // Check if the service doesn't exist (error code 1060) + let hresult = e.code(); + if hresult.0 == -2147024908i32 { + // 1060 in HRESULT format (ERROR_SERVICE_DOES_NOT_EXIST) + return Ok(()); + } + return Err(format!("Failed to open service: {}", e)); + } + }; + + let mut status = windows::Win32::System::Services::SERVICE_STATUS::default(); + let _ = ControlService(service, SERVICE_CONTROL_STOP, &mut status); + + DeleteService(service).map_err(|e| format!("Failed to delete service: {}", e))?; + + // Cleanup event source registry + let sub_key = + HSTRING::from("SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\lan-mouse"); + let _ = windows::Win32::System::Registry::RegDeleteKeyW(HKEY_LOCAL_MACHINE, &sub_key); + + info!("Service uninstalled successfully"); + Ok(()) + } +} diff --git a/src/windows_service.rs b/src/windows_service.rs new file mode 100644 index 000000000..2ffb55c5a --- /dev/null +++ b/src/windows_service.rs @@ -0,0 +1,570 @@ +use std::ffi::OsString; +use std::ptr; +use std::time::Duration; +use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0}; +use windows::Win32::Security::{ + DuplicateTokenEx, SecurityImpersonation, SetTokenInformation, TOKEN_ALL_ACCESS, + TOKEN_ASSIGN_PRIMARY, TokenPrimary, TokenUIAccess, +}; +use windows::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS, +}; +use windows::Win32::System::Environment::{CreateEnvironmentBlock, DestroyEnvironmentBlock}; +use windows::Win32::System::RemoteDesktop::{ProcessIdToSessionId, WTSGetActiveConsoleSessionId}; +use windows::Win32::System::Services::{ + ControlService, OpenSCManagerW, OpenServiceW, SC_MANAGER_ALL_ACCESS, SERVICE_ALL_ACCESS, +}; +use windows::Win32::System::Threading::{ + CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, CreateProcessAsUserW, OpenProcess, + OpenProcessToken, PROCESS_ALL_ACCESS, PROCESS_INFORMATION, STARTUPINFOW, TerminateProcess, + WaitForSingleObject, +}; +use windows::core::{HSTRING, PWSTR}; +use windows_service::{ + define_windows_service, + service::{ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceStatus, ServiceType}, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, +}; + +define_windows_service!(ffi_service_main, lan_mouse_service_main); + +/// Starts the Windows service dispatcher. +/// This should only be called when the process is started by the Service Control Manager. +pub fn run_dispatch() -> Result<(), windows_service::Error> { + service_dispatcher::start("lan-mouse", ffi_service_main) +} + +fn lan_mouse_service_main(_arguments: Vec) { + log::info!("lan-mouse Windows service starting"); + + if let Err(e) = run_win_service() { + log::error!("Windows service error: {:?}", e); + } +} + +fn run_win_service() -> Result<(), windows_service::Error> { + /* ================================================================================== + * WINDOWS SERVICE - Session Manager for lan-mouse + * ================================================================================== + * + * This service runs in Session 0 as SYSTEM and manages session daemon processes: + * + * 1. Monitor active console session via WTSGetActiveConsoleSessionId() + * 2. Spawn `lan-mouse daemon` in user session using CreateProcessAsUser() + * 3. Acquire appropriate token (WTSQueryUserToken or winlogon token) + * 4. Monitor daemon health, respawn on crash or session change + * 5. Handle SendSAS for Ctrl+Alt+Del (future: when IPC is implemented) + * + * Session daemons perform actual input capture/emulation since they run in the + * user's session where SendInput works correctly. + * ================================================================================== + */ + + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Stop | ServiceControl::Shutdown => { + log::info!("Received stop/shutdown signal"); + tx.send(()).unwrap_or_default(); + ServiceControlHandlerResult::NoError + } + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + let status_handle = service_control_handler::register("lan-mouse", event_handler)?; + + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: windows_service::service::ServiceState::StartPending, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::from_secs(5), + process_id: None, + })?; + + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() + { + Ok(r) => r, + Err(e) => { + log::error!("Failed to create tokio runtime: {:?}", e); + return Err(windows_service::Error::Winapi(std::io::Error::other( + "Failed to create tokio runtime", + ))); + } + }; + + let local = tokio::task::LocalSet::new(); + rt.block_on(local.run_until(async { + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: windows_service::service::ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }) + .unwrap(); + + log::info!("Windows service running - monitoring sessions and managing daemons"); + + if let Err(e) = session_manager_loop(rx).await { + log::error!("Windows service main loop error: {:?}", e); + } + + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: windows_service::service::ServiceState::StopPending, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::from_secs(5), + process_id: None, + }) + .unwrap(); + })); + + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: windows_service::service::ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + log::info!("Windows service stopped"); + Ok(()) +} + +async fn session_manager_loop( + mut shutdown_rx: tokio::sync::mpsc::UnboundedReceiver<()>, +) -> Result<(), std::io::Error> { + log::info!("Windows service main loop started - monitoring console sessions"); + + let mut current_session_id: Option = None; + let mut session_daemon: Option = None; + let mut crash_count = 0u32; + let mut last_crash_time = std::time::Instant::now(); + + loop { + tokio::select! { + _ = shutdown_rx.recv() => { + log::info!("Windows service received shutdown signal"); + + // Terminate session daemon if running + if let Some(daemon) = session_daemon.take() { + log::info!("Terminating session daemon (PID={})", daemon.process_id); + daemon.terminate(); + } + + break; + } + _ = tokio::time::sleep(Duration::from_millis(500)) => { + // Check active console session + let active_session = get_active_console_session(); + + // 0xFFFFFFFF means no active session (e.g., no user logged in) + if active_session == 0xFFFFFFFF { + if current_session_id.is_some() { + log::info!("No active console session (user logged out or switching sessions)"); + + // Terminate session daemon + if let Some(daemon) = session_daemon.take() { + log::info!("Terminating session daemon (PID={})", daemon.process_id); + daemon.terminate(); + } + + current_session_id = None; + } + continue; + } + + // Check if daemon crashed (if we think we have one but it's not running) + if let Some(ref daemon) = session_daemon { + if !daemon.is_running() { + let now = std::time::Instant::now(); + let time_since_last_crash = now.duration_since(last_crash_time); + + // Reset crash count if it's been more than 60 seconds since last crash + if time_since_last_crash.as_secs() > 60 { + crash_count = 0; + } + + crash_count += 1; + last_crash_time = now; + + log::error!("Session daemon crashed (PID={}, crash #{}) - will respawn after backoff", + daemon.process_id, crash_count); + + // Exponential backoff: 1s, 2s, 4s, 8s, max 30s + let backoff_secs = std::cmp::min(1u64 << (crash_count - 1), 30); + log::info!("Waiting {}s before respawn attempt...", backoff_secs); + + // Remain responsive to shutdown during backoff wait + let shutdown = tokio::select! { + _ = shutdown_rx.recv() => true, + _ = tokio::time::sleep(Duration::from_secs(backoff_secs)) => false, + }; + if shutdown { + log::info!("Shutdown received during crash backoff"); + if let Some(d) = session_daemon.take() { + d.terminate(); + } + break; + } + + session_daemon = None; + // Will respawn below + } + } + + // Check if session changed + let session_changed = current_session_id != Some(active_session); + let need_spawn = session_changed || (session_daemon.is_none() && current_session_id.is_some()); + + if session_changed { + log::info!("Console session changed: {:?} -> {}", current_session_id, active_session); + + // Terminate old session daemon + if let Some(daemon) = session_daemon.take() { + log::info!("Terminating session daemon in old session (PID={})", daemon.process_id); + daemon.terminate(); + } + + current_session_id = Some(active_session); + } + + // Spawn daemon in active session if needed + if need_spawn && session_daemon.is_none() { + match get_session_token(active_session) { + Ok(token) => { + match spawn_session_daemon(active_session, token) { + Ok(daemon) => { + log::info!("Successfully spawned session daemon in session {} (PID={})", + active_session, daemon.process_id); + session_daemon = Some(daemon); + } + Err(e) => { + log::error!("Failed to spawn session daemon: {}", e); + } + } + + // Clean up token + unsafe { let _ = CloseHandle(token); } + } + Err(e) => { + log::warn!("Failed to get session token for session {}: {} (will retry)", active_session, e); + } + } + } + } + } + } + + log::info!("Windows service main loop shutting down"); + Ok(()) +} + +fn get_active_console_session() -> u32 { + unsafe { WTSGetActiveConsoleSessionId() } +} + +/// Holds process information for a spawned session daemon +struct SessionDaemonHandle { + process_handle: HANDLE, + thread_handle: HANDLE, + process_id: u32, +} + +impl SessionDaemonHandle { + fn is_running(&self) -> bool { + unsafe { + // WAIT_TIMEOUT = 0x00000102, means process still running + WaitForSingleObject(self.process_handle, 0) != WAIT_OBJECT_0 + } + } + + fn terminate(&self) { + unsafe { + let _ = TerminateProcess(self.process_handle, 1); + // Wait up to 5 seconds for process to exit + let _ = WaitForSingleObject(self.process_handle, 5000); + } + } +} + +impl Drop for SessionDaemonHandle { + fn drop(&mut self) { + unsafe { + let _ = CloseHandle(self.process_handle); + let _ = CloseHandle(self.thread_handle); + } + } +} + +/// Find a process by name in a specific session +fn find_process_in_session(process_name: &str, session_id: u32) -> Result { + unsafe { + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).map_err(|e| { + std::io::Error::other(format!("CreateToolhelp32Snapshot failed: {}", e)) + })?; + + let mut entry = PROCESSENTRY32W { + dwSize: std::mem::size_of::() as u32, + ..Default::default() + }; + + if Process32FirstW(snapshot, &mut entry).is_err() { + let _ = CloseHandle(snapshot); + return Err(std::io::Error::other("Process32FirstW failed")); + } + + let target_name_lower = process_name.to_lowercase(); + + loop { + // Convert process name from wide string + let name_len = entry + .szExeFile + .iter() + .position(|&c| c == 0) + .unwrap_or(entry.szExeFile.len()); + let process_name_str = String::from_utf16_lossy(&entry.szExeFile[..name_len]); + + if process_name_str.to_lowercase() == target_name_lower { + // Check if process is in the target session + let mut proc_session_id = 0u32; + if ProcessIdToSessionId(entry.th32ProcessID, &mut proc_session_id).is_ok() + && proc_session_id == session_id + { + let pid = entry.th32ProcessID; + let _ = CloseHandle(snapshot); + return Ok(pid); + } + } + + if Process32NextW(snapshot, &mut entry).is_err() { + break; + } + } + + let _ = CloseHandle(snapshot); + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!( + "Process '{}' not found in session {}", + process_name, session_id + ), + )) + } +} + +/// Get a session token for spawning the daemon in the target session. +/// +/// Uses the winlogon.exe token because it runs with SYSTEM privileges and has +/// access to the Secure Desktop (UAC prompts). A winlogon-derived token lets +/// the spawned daemon call OpenInputDesktop successfully. +/// +/// WTSQueryUserToken only yields a limited user token which cannot access the +/// Secure Desktop even with TokenUIAccess set. +fn get_session_token(session_id: u32) -> Result { + unsafe { + // Find winlogon.exe in the target session + let winlogon_pid = find_process_in_session("winlogon.exe", session_id)?; + + // Open the winlogon process + let process_handle = OpenProcess(PROCESS_ALL_ACCESS, false, winlogon_pid).map_err(|e| { + std::io::Error::other(format!( + "OpenProcess failed for winlogon PID {}: {}", + winlogon_pid, e + )) + })?; + + // Get the winlogon process token + let mut source_token = HANDLE::default(); + let result = OpenProcessToken( + process_handle, + TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, + &mut source_token, + ); + let _ = CloseHandle(process_handle); + + result.map_err(|e| { + std::io::Error::other(format!("OpenProcessToken failed for winlogon: {}", e)) + })?; + + // Duplicate the token as a primary token + let mut new_token = HANDLE::default(); + DuplicateTokenEx( + source_token, + TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, + None, + SecurityImpersonation, + TokenPrimary, + &mut new_token, + ) + .map_err(|e| { + let _ = CloseHandle(source_token); + std::io::Error::other(format!("DuplicateTokenEx failed: {}", e)) + })?; + + let _ = CloseHandle(source_token); + + // Enable UIAccess on the token for secure desktop access + let ui_access: u32 = 1; + SetTokenInformation( + new_token, + TokenUIAccess, + &ui_access as *const u32 as *const _, + std::mem::size_of::() as u32, + ) + .map_err(|e| { + log::warn!( + "SetTokenInformation(TokenUIAccess) failed: {} (may need code signing)", + e + ); + // Don't fail here - token is still usable, just without UIAccess + }) + .ok(); + + log::info!( + "Acquired winlogon token for session {} (PID={})", + session_id, + winlogon_pid + ); + Ok(new_token) + } +} + +/// Spawn session daemon in the specified session with the given token +fn spawn_session_daemon( + session_id: u32, + token: HANDLE, +) -> Result { + unsafe { + let exe_path = std::env::current_exe()?; + + // Build command line with explicit config path pointing to ProgramData + // This ensures the session daemon uses the machine-wide config regardless of + // which user token (or winlogon token) is used to spawn it + let config_path = r"C:\ProgramData\lan-mouse\config.toml"; + let command = format!( + r#""{}" --config "{}" daemon"#, + exe_path.display(), + config_path + ); + + log::info!( + "Spawning session daemon in session {} with command: {}", + session_id, + command + ); + + let mut command_wide: Vec = command.encode_utf16().chain(std::iter::once(0)).collect(); + let mut desktop: Vec = "winsta0\\Default" + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + + let startup_info = STARTUPINFOW { + cb: std::mem::size_of::() as u32, + lpDesktop: PWSTR(desktop.as_mut_ptr()), + ..Default::default() + }; + + // Create environment block for the user session + let mut env_block = ptr::null_mut(); + CreateEnvironmentBlock(&mut env_block, Some(token), false) + .map_err(|e| std::io::Error::other(format!("CreateEnvironmentBlock failed: {}", e)))?; + + let mut proc_info = PROCESS_INFORMATION::default(); + + let result = CreateProcessAsUserW( + Some(token), + None, + Some(PWSTR(command_wide.as_mut_ptr())), + None, // Process security + None, // Thread security + true, // Inherit handles + CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW, + Some(env_block as *const _), + None, // Current directory + &startup_info, + &mut proc_info, + ); + + DestroyEnvironmentBlock(env_block) + .map_err(|e| log::warn!("DestroyEnvironmentBlock failed: {}", e)) + .ok(); + + result.map_err(|e| std::io::Error::other(format!("CreateProcessAsUserW failed: {}", e)))?; + + log::info!( + "Session daemon spawned successfully: PID={}", + proc_info.dwProcessId + ); + + Ok(SessionDaemonHandle { + process_handle: proc_info.hProcess, + thread_handle: proc_info.hThread, + process_id: proc_info.dwProcessId, + }) + } +} + +pub fn service_status() -> Result<(), String> { + unsafe { + // This will fail on non-administrative/elevated calls + let scm = OpenSCManagerW(None, None, SC_MANAGER_ALL_ACCESS) + .map_err(|e| format!("Failed to open SCM: {}", e))?; + + let service_name = HSTRING::from("lan-mouse"); + let service = match OpenServiceW(scm, &service_name, SERVICE_ALL_ACCESS) { + Ok(s) => s, + Err(e) => { + // Check if the service doesn't exist (error code 1060) + let hresult = e.code(); + if hresult.0 == -2147024908i32 { + // 1060 in HRESULT format (ERROR_SERVICE_DOES_NOT_EXIST) + println!("Service not installed"); + return Ok(()); + } + return Err(format!("Failed to open service: {}", e)); + } + }; + + // Query service status + let mut status = windows::Win32::System::Services::SERVICE_STATUS::default(); + ControlService( + service, + windows::Win32::System::Services::SERVICE_CONTROL_INTERROGATE, + &mut status, + ) + .ok(); + + let status_str = match status.dwCurrentState.0 { + 1 => "Stopped", + 2 => "Start Pending", + 3 => "Stop Pending", + 4 => "Running", + 5 => "Continue Pending", + 6 => "Pause Pending", + 7 => "Paused", + _ => "Unknown", + }; + + println!("Service Status: {}", status_str); + Ok(()) + } +}