diff --git a/proxy_agent_extension/src/service_main.rs b/proxy_agent_extension/src/service_main.rs index b0140d7b..a40ba79c 100644 --- a/proxy_agent_extension/src/service_main.rs +++ b/proxy_agent_extension/src/service_main.rs @@ -341,70 +341,85 @@ fn write_state_event( } #[cfg(windows)] -fn report_ebpf_status(status_obj: &mut StatusObj) { - match service::check_service_installed(constants::EBPF_CORE) { - (true, message) => { - logger::write(message.to_string()); - match service::check_service_installed(constants::EBPF_EXT) { - (true, message) => { - logger::write(message.to_string()); - status_obj.substatus = { - let mut substatus = status_obj.substatus.clone(); - substatus.push(SubStatus { - name: constants::EBPF_SUBSTATUS_NAME.to_string(), - status: constants::SUCCESS_STATUS.to_string(), - code: constants::STATUS_CODE_OK, - formattedMessage: FormattedMessage { - lang: constants::LANG_EN_US.to_string(), - message: "Ebpf Drivers successfully queried.".to_string(), - }, - }); - substatus - }; - } - (false, message) => { - logger::write(message.to_string()); - status_obj.substatus = { - let mut substatus = status_obj.substatus.clone(); - substatus.push(SubStatus { - name: constants::EBPF_SUBSTATUS_NAME.to_string(), - status: constants::ERROR_STATUS.to_string(), - code: constants::STATUS_CODE_NOT_OK, - formattedMessage: FormattedMessage { - lang: constants::LANG_EN_US.to_string(), - message: format!( - "Ebpf Driver: {} unsuccessfully queried.", - constants::EBPF_EXT - ), - }, - }); - substatus - }; - } +fn build_ebpf_substatus( + core: &proxy_agent_shared::service::ServiceStatusInfo, + ext: &proxy_agent_shared::service::ServiceStatusInfo, +) -> SubStatus { + use proxy_agent_shared::service::ServiceState; + + let (status, code, message) = match (&core.state, &ext.state) { + (Some(core_state), Some(ext_state)) => { + let both_running = + *core_state == ServiceState::Running && *ext_state == ServiceState::Running; + if both_running { + ( + constants::SUCCESS_STATUS.to_string(), + constants::STATUS_CODE_OK, + format!( + "EbpfCore: {}, NetEbpfExt: {}", + core.summary(), + ext.summary() + ), + ) + } else { + ( + constants::ERROR_STATUS.to_string(), + constants::STATUS_CODE_NOT_OK, + format!( + "EbpfCore: {}, NetEbpfExt: {}", + core.summary(), + ext.summary() + ), + ) } } - (false, message) => { - logger::write(message.to_string()); - status_obj.substatus = { - let mut substatus = status_obj.substatus.clone(); - substatus.push(SubStatus { - name: constants::EBPF_SUBSTATUS_NAME.to_string(), - status: constants::ERROR_STATUS.to_string(), - code: constants::STATUS_CODE_NOT_OK, - formattedMessage: FormattedMessage { - lang: constants::LANG_EN_US.to_string(), - message: format!( - "Ebpf Driver: {} unsuccessfully queried.", - constants::EBPF_CORE - ), - }, - }); - substatus - }; - } + (None, None) => ( + constants::ERROR_STATUS.to_string(), + constants::STATUS_CODE_NOT_OK, + "EbpfCore: unsuccessfully queried, NetEbpfExt: unsuccessfully queried.".to_string(), + ), + (None, _) => ( + constants::ERROR_STATUS.to_string(), + constants::STATUS_CODE_NOT_OK, + format!( + "EbpfCore: unsuccessfully queried, NetEbpfExt: {}", + ext.summary() + ), + ), + (_, None) => ( + constants::ERROR_STATUS.to_string(), + constants::STATUS_CODE_NOT_OK, + format!( + "EbpfCore: {}, NetEbpfExt: unsuccessfully queried.", + core.summary() + ), + ), + }; + + SubStatus { + name: constants::EBPF_SUBSTATUS_NAME.to_string(), + status, + code, + formattedMessage: FormattedMessage { + lang: constants::LANG_EN_US.to_string(), + message, + }, } } +#[cfg(windows)] +fn report_ebpf_status(status_obj: &mut StatusObj) { + let core_status = service::check_service_status(constants::EBPF_CORE); + logger::write(format!("check_service_status: {}", core_status.message())); + + let ext_status = service::check_service_status(constants::EBPF_EXT); + logger::write(format!("check_service_status: {}", ext_status.message())); + + let mut substatus = status_obj.substatus.clone(); + substatus.push(build_ebpf_substatus(&core_status, &ext_status)); + status_obj.substatus = substatus; +} + fn backup_proxy_agent(setup_tool: &String) { match Command::new(setup_tool).arg("backup").output() { Ok(output) => { @@ -1184,6 +1199,155 @@ mod tests { status.substatus[3].name, constants::EBPF_SUBSTATUS_NAME.to_string() ); + + // Verify the eBPF substatus message includes service status info + let ebpf_substatus = &status.substatus[3]; + let ebpf_message = &ebpf_substatus.formattedMessage.message; + if ebpf_message.contains("unsuccessfully queried") { + // At least one service not installed — status should be Error + assert_eq!( + ebpf_substatus.status, + constants::ERROR_STATUS, + "Expected Error status when a service is not installed" + ); + } else { + // Both services found — message should contain status details for each driver + assert!( + ebpf_message.contains("EbpfCore:"), + "Expected message to contain 'EbpfCore:', got: {ebpf_message}" + ); + assert!( + ebpf_message.contains("NetEbpfExt:"), + "Expected message to contain 'NetEbpfExt:', got: {ebpf_message}" + ); + // Status depends on whether both services are running + if ebpf_message.contains("Running") && !ebpf_message.contains("Stopped") { + assert_eq!( + ebpf_substatus.status, + constants::SUCCESS_STATUS, + "Expected Success when both services are running" + ); + assert_eq!(ebpf_substatus.code, constants::STATUS_CODE_OK); + } else { + assert_eq!( + ebpf_substatus.status, + constants::ERROR_STATUS, + "Expected Error when at least one service is not running" + ); + assert_eq!(ebpf_substatus.code, constants::STATUS_CODE_NOT_OK); + } + } + } + + #[test] + #[cfg(windows)] + fn test_build_ebpf_substatus() { + use proxy_agent_shared::service::{ServiceState, ServiceStatusInfo}; + + fn make_info(name: &str, state: Option) -> ServiceStatusInfo { + let start_type = if state.is_some() { + "AutoStart".to_string() + } else { + "NotInstalled".to_string() + }; + ServiceStatusInfo { + service_name: name.to_string(), + state, + start_type, + } + } + + // 1. Both not installed + let sub = super::build_ebpf_substatus( + &make_info(constants::EBPF_CORE, None), + &make_info(constants::EBPF_EXT, None), + ); + assert_eq!(sub.status, constants::ERROR_STATUS, "Both not installed"); + assert_eq!(sub.code, constants::STATUS_CODE_NOT_OK); + let msg = &sub.formattedMessage.message; + assert!( + msg.contains(constants::EBPF_CORE) && msg.contains(constants::EBPF_EXT), + "Expected both driver names in message, got: {msg}" + ); + + // 2. Core not installed, Ext running + let sub = super::build_ebpf_substatus( + &make_info(constants::EBPF_CORE, None), + &make_info(constants::EBPF_EXT, Some(ServiceState::Running)), + ); + assert_eq!(sub.status, constants::ERROR_STATUS, "Core not installed"); + assert_eq!(sub.code, constants::STATUS_CODE_NOT_OK); + let msg = &sub.formattedMessage.message; + assert!( + msg.contains(constants::EBPF_CORE), + "Expected EbpfCore in message, got: {msg}" + ); + assert!( + msg.contains("Running"), + "Expected Ext summary (Running) in message, got: {msg}" + ); + + // 3. Core running, Ext not installed + let sub = super::build_ebpf_substatus( + &make_info(constants::EBPF_CORE, Some(ServiceState::Running)), + &make_info(constants::EBPF_EXT, None), + ); + assert_eq!(sub.status, constants::ERROR_STATUS, "Ext not installed"); + assert_eq!(sub.code, constants::STATUS_CODE_NOT_OK); + let msg = &sub.formattedMessage.message; + assert!( + msg.contains("Running"), + "Expected Core summary (Running) in message, got: {msg}" + ); + assert!( + msg.contains(constants::EBPF_EXT), + "Expected NetEbpfExt in message, got: {msg}" + ); + + // 4. Both running → success + let sub = super::build_ebpf_substatus( + &make_info(constants::EBPF_CORE, Some(ServiceState::Running)), + &make_info(constants::EBPF_EXT, Some(ServiceState::Running)), + ); + assert_eq!(sub.status, constants::SUCCESS_STATUS, "Both running"); + assert_eq!(sub.code, constants::STATUS_CODE_OK); + let msg = &sub.formattedMessage.message; + assert!( + msg.contains("EbpfCore:") && msg.contains("NetEbpfExt:"), + "Expected both driver labels in message, got: {msg}" + ); + + // 5. Core stopped, Ext running → error + let sub = super::build_ebpf_substatus( + &make_info(constants::EBPF_CORE, Some(ServiceState::Stopped)), + &make_info(constants::EBPF_EXT, Some(ServiceState::Running)), + ); + assert_eq!( + sub.status, + constants::ERROR_STATUS, + "Core stopped, Ext running" + ); + assert_eq!(sub.code, constants::STATUS_CODE_NOT_OK); + + // 6. Core running, Ext stopped → error + let sub = super::build_ebpf_substatus( + &make_info(constants::EBPF_CORE, Some(ServiceState::Running)), + &make_info(constants::EBPF_EXT, Some(ServiceState::Stopped)), + ); + assert_eq!( + sub.status, + constants::ERROR_STATUS, + "Core running, Ext stopped" + ); + assert_eq!(sub.code, constants::STATUS_CODE_NOT_OK); + + // 7. Both stopped → error + let sub = super::build_ebpf_substatus( + &make_info(constants::EBPF_CORE, Some(ServiceState::Stopped)), + &make_info(constants::EBPF_EXT, Some(ServiceState::Stopped)), + ); + assert_eq!(sub.status, constants::ERROR_STATUS, "Both stopped"); + assert_eq!(sub.code, constants::STATUS_CODE_NOT_OK); } #[tokio::test] diff --git a/proxy_agent_shared/src/service.rs b/proxy_agent_shared/src/service.rs index 53c2cd6f..2bda4385 100644 --- a/proxy_agent_shared/src/service.rs +++ b/proxy_agent_shared/src/service.rs @@ -145,8 +145,49 @@ pub fn check_service_installed(service_name: &str) -> (bool, String) { } } +/// Checks whether a Windows service is installed and queries its runtime state and start type. +/// Returns a `ServiceStatusInfo` whose `state` is `Some(ServiceState)` when the service exists, +/// or `None` when the service is not installed. +#[cfg(windows)] +pub fn check_service_status(service_name: &str) -> windows_service::ServiceStatusInfo { + let (state, start_type) = match windows_service::query_service_status(service_name) { + Ok(status) => { + let start_type = match windows_service::query_service_config(service_name) { + Ok(config) => format!("{:?}", config.start_type), + Err(e) => { + log::warn!( + "Failed to query config for service '{}': {}", + service_name, + e + ); + "Unknown".to_string() + } + }; + (Some(status.current_state), start_type) + } + Err(e) => { + log::debug!( + "Failed to query status for service '{}': {}. Treating as not installed.", + service_name, + e + ); + (None, "NotInstalled".to_string()) + } + }; + + windows_service::ServiceStatusInfo { + service_name: service_name.to_string(), + state, + start_type, + } +} + #[cfg(windows)] pub use windows_service::set_default_failure_actions; +#[cfg(windows)] +pub use windows_service::ServiceState; +#[cfg(windows)] +pub use windows_service::ServiceStatusInfo; #[cfg(test)] mod tests { @@ -191,4 +232,45 @@ mod tests { _ = super::stop_and_delete_service(service_name).await.unwrap(); } } + + #[tokio::test] + async fn test_check_service_status() { + #[cfg(windows)] + { + let service_name = "test_check_service_status"; + // try delete the service if it exists + _ = super::stop_and_delete_service(service_name).await; + + // Verify non-existent service returns not installed + let status = super::check_service_status(service_name); + assert_eq!(status.state, None, "Expected None for non-existent service"); + assert!(status.message().contains("query failed")); + assert_eq!(status.summary(), "NotInstalled"); + + // Install a test service and verify status is reported + let exe_path = std::env::current_exe().unwrap(); + let result = super::install_service(service_name, service_name, vec![], exe_path); + assert!(result.is_ok()); + + let status = super::check_service_status(service_name); + assert!(status.state.is_some(), "Expected service to be installed"); + assert!(status.message().contains("status:")); + // Service should be stopped (test exe can't actually run as a service) + assert_eq!( + status.state, + Some(super::ServiceState::Stopped), + "Expected Some(ServiceState::Stopped), got: {:?}", + status.state + ); + // Summary should also contain start type info + let summary = status.summary(); + assert!( + summary.contains("AutoStart"), + "Expected summary to contain 'AutoStart', got: {summary}" + ); + + // clean up + _ = super::stop_and_delete_service(service_name).await.unwrap(); + } + } } diff --git a/proxy_agent_shared/src/service/windows_service.rs b/proxy_agent_shared/src/service/windows_service.rs index c2a86de2..b8f552c5 100644 --- a/proxy_agent_shared/src/service/windows_service.rs +++ b/proxy_agent_shared/src/service/windows_service.rs @@ -7,14 +7,43 @@ use std::ffi::OsString; use std::path::PathBuf; use std::str; use std::time::Duration; +pub use windows_service::service::ServiceState; use windows_service::service::{ ServiceAccess, ServiceAction, ServiceActionType, ServiceConfig, ServiceErrorControl, - ServiceFailureResetPeriod, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, - ServiceType, + ServiceFailureResetPeriod, ServiceInfo, ServiceStartType, ServiceStatus, ServiceType, }; use windows_service::service::{ServiceDependency, ServiceFailureActions}; use windows_service::service_manager::{ServiceManager, ServiceManagerAccess}; +/// Holds the runtime status of a Windows service. +#[derive(Debug)] +pub struct ServiceStatusInfo { + pub service_name: String, + pub state: Option, + pub start_type: String, +} + +impl ServiceStatusInfo { + /// Human-readable summary, e.g. "Running, AutoStart" or "NotInstalled". + pub fn summary(&self) -> String { + match self.state { + Some(ref state) => format!("{:?}, {}", state, self.start_type), + None => "NotInstalled".to_string(), + } + } + + /// Log-friendly message including the service name and summary. + pub fn message(&self) -> String { + match self.state { + Some(_) => format!("service: {} status: {}", self.service_name, self.summary()), + None => format!( + "service: {} status query failed, service may not be installed", + self.service_name + ), + } + } +} + pub async fn start_service_with_retry( service_name: &str, retry_count: u32, @@ -167,7 +196,7 @@ pub fn install_or_update_service( } } -fn query_service_status(service_name: &str) -> Result { +pub fn query_service_status(service_name: &str) -> Result { let service_manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT) .map_err(|e| Error::WindowsService(e, std::io::Error::last_os_error()))?; @@ -305,6 +334,7 @@ pub fn set_default_failure_actions(service_name: &str) -> Result<()> { #[cfg(test)] mod tests { use std::{path::PathBuf, process::Command}; + use windows_service::service::ServiceState; #[tokio::test] async fn test_install_service() { @@ -413,4 +443,71 @@ mod tests { //Clean up - delete service super::stop_and_delete_service(service_name).await.unwrap(); } + + #[test] + fn test_service_status_info_summary() { + // Not installed + let info = super::ServiceStatusInfo { + service_name: "TestSvc".to_string(), + state: None, + start_type: "NotInstalled".to_string(), + }; + assert_eq!(info.summary(), "NotInstalled"); + + // Running, AutoStart + let info = super::ServiceStatusInfo { + service_name: "TestSvc".to_string(), + state: Some(ServiceState::Running), + start_type: "AutoStart".to_string(), + }; + assert_eq!(info.summary(), "Running, AutoStart"); + + // Stopped, Disabled + let info = super::ServiceStatusInfo { + service_name: "TestSvc".to_string(), + state: Some(ServiceState::Stopped), + start_type: "Disabled".to_string(), + }; + assert_eq!(info.summary(), "Stopped, Disabled"); + } + + #[test] + fn test_service_status_info_message() { + // Not installed — message must mention the service name and "query failed" + let info = super::ServiceStatusInfo { + service_name: "TestSvc".to_string(), + state: None, + start_type: "NotInstalled".to_string(), + }; + let msg = info.message(); + assert!( + msg.contains("TestSvc"), + "Expected service name in message, got: {msg}" + ); + assert!( + msg.contains("query failed"), + "Expected 'query failed' in message, got: {msg}" + ); + + // Installed and running — message must contain "status:" and the summary + let info = super::ServiceStatusInfo { + service_name: "TestSvc".to_string(), + state: Some(ServiceState::Running), + start_type: "AutoStart".to_string(), + }; + let msg = info.message(); + assert!( + msg.contains("TestSvc"), + "Expected service name in message, got: {msg}" + ); + assert!( + msg.contains("status:"), + "Expected 'status:' in message, got: {msg}" + ); + assert!( + msg.contains(&info.summary()), + "Expected summary '{}' in message, got: {msg}", + info.summary() + ); + } }