//! Windows Service management for OSTP Client //! //! Installs, configures, and manages the OSTP client as a Windows Service. //! Uses sc.exe for broader compatibility. use anyhow::{Context, Result}; use std::path::PathBuf; use std::process::Command; /// Service name const SERVICE_NAME: &str = "OstpClient"; /// Service display name const SERVICE_DISPLAY_NAME: &str = "Ospab Network OSTP Client"; /// Service description const SERVICE_DESCRIPTION: &str = "Provides secure, stealth VPN connectivity through the OSTP protocol."; /// Install OSTP client as a Windows Service using sc.exe pub async fn install_service(install_path: &PathBuf) -> Result<()> { #[cfg(windows)] { let executable_path = install_path.join("ostp-client.exe"); if !executable_path.exists() { anyhow::bail!("ostp-client.exe not found at {:?}", executable_path); } let config_path = install_path.join("ostp-client.json"); let bin_path = format!( "\"{}\" --service --config \"{}\"", executable_path.display(), config_path.display() ); // Create service let output = Command::new("sc.exe") .args([ "create", SERVICE_NAME, &format!("binPath= {}", bin_path), &format!("DisplayName= {}", SERVICE_DISPLAY_NAME), "start= auto", "depend= Tcpip/Dnscache", ]) .output() .context("Failed to run sc.exe create")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); if !stderr.contains("1073") { // Service already exists anyhow::bail!("Failed to create service: {}", stderr); } } // Set description let _ = Command::new("sc.exe") .args(["description", SERVICE_NAME, SERVICE_DESCRIPTION]) .output(); // Configure recovery options (restart on failure) let _ = Command::new("sc.exe") .args([ "failure", SERVICE_NAME, "reset= 86400", "actions= restart/60000/restart/120000/restart/300000", ]) .output(); tracing::info!("Service {} installed successfully", SERVICE_NAME); } #[cfg(not(windows))] { let _ = install_path; anyhow::bail!("Windows Service installation is only supported on Windows"); } Ok(()) } /// Start the OSTP service pub async fn start_service() -> Result<()> { #[cfg(windows)] { let output = Command::new("sc.exe") .args(["start", SERVICE_NAME]) .output() .context("Failed to run sc.exe start")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); if !stderr.contains("1056") { // Service already running anyhow::bail!("Failed to start service: {}", stderr); } } tracing::info!("Service {} started", SERVICE_NAME); } Ok(()) } /// Stop the OSTP service pub async fn stop_service() -> Result<()> { #[cfg(windows)] { let output = Command::new("sc.exe") .args(["stop", SERVICE_NAME]) .output() .context("Failed to run sc.exe stop")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); if !stderr.contains("1062") { // Service not running anyhow::bail!("Failed to stop service: {}", stderr); } } // Wait for service to stop for _ in 0..30 { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; if !is_running() { break; } } tracing::info!("Service {} stopped", SERVICE_NAME); } Ok(()) } /// Uninstall the OSTP service pub async fn uninstall_service() -> Result<()> { // Stop the service first stop_service().await.ok(); #[cfg(windows)] { let output = Command::new("sc.exe") .args(["delete", SERVICE_NAME]) .output() .context("Failed to run sc.exe delete")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); if !stderr.contains("1060") { // Service doesn't exist anyhow::bail!("Failed to delete service: {}", stderr); } } tracing::info!("Service {} uninstalled", SERVICE_NAME); } Ok(()) } /// Enable service to start at system startup pub async fn enable_autostart() -> Result<()> { #[cfg(windows)] { Command::new("sc.exe") .args(["config", SERVICE_NAME, "start= auto"]) .output() .context("Failed to enable autostart")?; tracing::info!("Autostart enabled for {}", SERVICE_NAME); } Ok(()) } /// Disable service autostart #[allow(dead_code)] pub async fn disable_autostart() -> Result<()> { #[cfg(windows)] { Command::new("sc.exe") .args(["config", SERVICE_NAME, "start= demand"]) .output() .context("Failed to disable autostart")?; tracing::info!("Autostart disabled for {}", SERVICE_NAME); } Ok(()) } /// Check if service is installed #[allow(dead_code)] pub fn is_installed() -> bool { #[cfg(windows)] { Command::new("sc.exe") .args(["query", SERVICE_NAME]) .output() .map(|o| o.status.success()) .unwrap_or(false) } #[cfg(not(windows))] false } /// Check if service is running pub fn is_running() -> bool { #[cfg(windows)] { Command::new("sc.exe") .args(["query", SERVICE_NAME]) .output() .map(|o| { let stdout = String::from_utf8_lossy(&o.stdout); stdout.contains("RUNNING") }) .unwrap_or(false) } #[cfg(not(windows))] false } /// Get service status #[allow(dead_code)] pub fn get_status() -> Option { #[cfg(windows)] { Command::new("sc.exe") .args(["query", SERVICE_NAME]) .output() .ok() .and_then(|o| { let stdout = String::from_utf8_lossy(&o.stdout); if stdout.contains("RUNNING") { Some("Running".to_string()) } else if stdout.contains("STOPPED") { Some("Stopped".to_string()) } else if stdout.contains("PENDING") { Some("Pending".to_string()) } else { Some("Unknown".to_string()) } }) } #[cfg(not(windows))] None }