use std::{
ffi::{OsStr, OsString},
path::PathBuf,
process::Command,
sync::{Arc, Mutex},
time::Duration,
};
use miette::{IntoDiagnostic, Result, miette};
use tracing::{error, info, warn};
use windows_service::{
define_windows_service,
service::{
ServiceAccess, ServiceAction, ServiceActionType, ServiceControl, ServiceControlAccept,
ServiceErrorControl, ServiceExitCode, ServiceFailureActions, ServiceFailureResetPeriod,
ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType,
},
service_control_handler::{self, ServiceControlHandlerResult},
service_dispatcher,
service_manager::{ServiceManager, ServiceManagerAccess},
};
use crate::DaemonConfig;
const SERVICE_NAME: &str = "bestool-alertd";
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
static SERVICE_CONFIG: Mutex<Option<DaemonConfig>> = Mutex::new(None);
static TEMP_EXEC_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);
define_windows_service!(ffi_service_main, service_main);
pub fn run_service(config: DaemonConfig) -> Result<()> {
{
let mut guard = SERVICE_CONFIG.lock().unwrap();
*guard = Some(config);
}
service_dispatcher::start(SERVICE_NAME, ffi_service_main).into_diagnostic()?;
Ok(())
}
fn service_main(_arguments: Vec<OsString>) {
if let Err(e) = run_service_main() {
error!("service main error: {e:?}");
}
}
fn run_service_main() -> Result<()> {
let _temp_exec = copy_executable_to_temp()?;
let config = {
let mut guard = SERVICE_CONFIG.lock().unwrap();
guard
.take()
.ok_or_else(|| miette::miette!("service config not set"))?
};
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let shutdown_tx = Arc::new(Mutex::new(Some(shutdown_tx)));
let shutdown_tx_clone = shutdown_tx.clone();
let (reload_tx, reload_rx) = tokio::sync::mpsc::channel(10);
let reload_tx = Arc::new(Mutex::new(reload_tx));
let reload_tx_clone = reload_tx.clone();
let event_handler = move |control_event| -> ServiceControlHandlerResult {
match control_event {
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
ServiceControl::Stop | ServiceControl::Shutdown => {
info!("received service stop/shutdown signal");
let mut tx_guard = shutdown_tx_clone.lock().unwrap();
if let Some(tx) = tx_guard.take() {
let _ = tx.send(());
}
ServiceControlHandlerResult::NoError
}
ServiceControl::TimeChange => {
info!("received system time change event, triggering reload");
let tx_guard = reload_tx_clone.lock().unwrap();
let _ = tx_guard.try_send(());
ServiceControlHandlerResult::NoError
}
_ => ServiceControlHandlerResult::NotImplemented,
}
};
let status_handle =
service_control_handler::register(SERVICE_NAME, event_handler).into_diagnostic()?;
status_handle
.set_service_status(ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::StartPending,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(0),
checkpoint: 1,
wait_hint: Duration::from_secs(10),
process_id: None,
})
.into_diagnostic()?;
let runtime = tokio::runtime::Runtime::new().into_diagnostic()?;
let result = runtime.block_on(async move {
let status_tx = status_handle.clone();
let status_task = tokio::spawn(async move {
let mut checkpoint = 2;
let mut is_running_reported = false;
loop {
if !is_running_reported && checkpoint > 2 {
let _ = status_tx.set_service_status(ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::Running,
controls_accepted: ServiceControlAccept::STOP
| ServiceControlAccept::SHUTDOWN,
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
});
is_running_reported = true;
info!("service reported as Running to Windows SCM");
}
tokio::time::sleep(Duration::from_secs(5)).await;
if is_running_reported {
continue;
}
let _ = status_tx.set_service_status(ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::StartPending,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(0),
checkpoint,
wait_hint: Duration::from_secs(10),
process_id: None,
});
checkpoint += 1;
}
});
let daemon_result =
crate::daemon::run_with_shutdown_and_reload(config, shutdown_rx, Some(reload_rx)).await;
status_task.abort();
daemon_result
});
let final_state = if result.is_ok() {
info!("service stopping normally");
ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::Stopped,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
}
} else {
error!("service stopping with error: {result:?}");
ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::Stopped,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(1),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
}
};
status_handle
.set_service_status(final_state)
.into_diagnostic()?;
cleanup_temp_executable();
result
}
fn copy_executable_to_temp() -> Result<PathBuf> {
let original = std::env::current_exe()
.map_err(|e| miette!("Failed to get current executable path: {}", e))?;
let temp_dir = std::env::temp_dir();
let exe_name = original
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| miette!("Failed to get executable name"))?;
let temp_name = format!("{}.{}.tmp", exe_name, std::process::id());
let temp_path = temp_dir.join(&temp_name);
std::fs::copy(&original, &temp_path)
.map_err(|e| miette!("Failed to copy executable to temp directory: {}", e))?;
{
let mut guard = TEMP_EXEC_PATH.lock().unwrap();
*guard = Some(temp_path.clone());
}
info!("copied executable to temp: {}", temp_path.display());
Ok(temp_path)
}
fn cleanup_temp_executable() {
let guard = TEMP_EXEC_PATH.lock().unwrap();
if let Some(temp_path) = guard.as_ref() {
match std::fs::remove_file(temp_path) {
Ok(_) => info!("cleaned up temp executable: {}", temp_path.display()),
Err(e) => warn!(
"failed to clean up temp executable {}: {}",
temp_path.display(),
e
),
}
}
}
fn run_diagnostics() {
println!("Running diagnostics...\n");
print!("Checking Windows Service Control Manager... ");
match Command::new("sc").args(&["query"]).output() {
Ok(output) if output.status.success() => {
println!("✓ Running");
}
_ => {
println!("✗ May not be accessible");
println!(" Tip: Restart the 'Service Control Manager' service from Services.msc\n");
}
}
print!("Checking if service already exists... ");
match Command::new("sc").args(&["query", SERVICE_NAME]).output() {
Ok(output) if output.status.success() => {
println!("✗ Service already exists");
println!(" Tip: Run 'bestool tamanu alertd uninstall' first\n");
}
_ => {
println!("✓ Service not found (good)");
}
}
print!("Checking executable accessibility... ");
match std::env::current_exe() {
Ok(path) => {
if path.exists() {
println!("✓ Executable found at: {}", path.display());
} else {
println!("✗ Executable path invalid");
println!(" Path: {}\n", path.display());
}
}
Err(e) => {
println!("✗ Cannot determine executable path: {}\n", e);
}
}
println!();
}
fn get_service_log_path() -> Result<std::path::PathBuf> {
use std::path::PathBuf;
let log_dir = std::env::var("ProgramData")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("C:\\ProgramData"));
let log_dir = log_dir.join("BES").join("bestool-alertd");
if !log_dir.exists() {
std::fs::create_dir_all(&log_dir).ok();
}
Ok(log_dir)
}
pub fn install_service() -> Result<()> {
install_service_with_args(&[OsString::from("service")])
}
pub fn install_service_with_args(launch_arguments: &[OsString]) -> Result<()> {
run_diagnostics();
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("Access is denied") || error_msg.contains("ERROR_ACCESS_DENIED") {
miette!("Failed to connect to service manager: {}\n\nThis requires administrator privileges. Please run this command in an Administrator command prompt or PowerShell.", error_msg)
} else {
miette!("Failed to connect to service manager: {}\n\nTroubleshoot:\n - Ensure you have administrator privileges\n - Check that Windows Service Control Manager is running\n - Try running this command in an Administrator command prompt", error_msg)
}
})?;
let service_binary_path = std::env::current_exe()
.map_err(|e| miette!("Failed to get current executable path: {}\n\nTroubleshoot:\n - Ensure the bestool executable is accessible\n - Check that the path is readable and not corrupted", e))?;
let log_path = get_service_log_path()?;
let mut final_arguments = Vec::with_capacity(launch_arguments.len() + 2);
final_arguments.push(OsString::from("--log-file"));
final_arguments.push(OsString::from(log_path));
final_arguments.extend_from_slice(launch_arguments);
let service_info = ServiceInfo {
name: OsString::from("bestool-alertd"),
display_name: OsString::from("BES Alert Daemon"),
service_type: ServiceType::OWN_PROCESS,
start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal,
executable_path: service_binary_path,
launch_arguments: final_arguments,
dependencies: vec![],
account_name: None,
account_password: None,
};
let service = service_manager
.create_service(
&service_info,
ServiceAccess::CHANGE_CONFIG | ServiceAccess::START,
)
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("Already exists") || error_msg.contains("ERROR_SERVICE_EXISTS") {
miette!("Service 'bestool-alertd' already exists.\n\nTroubleshoot:\n - To reinstall, run: bestool tamanu alertd uninstall\n - Then run: bestool tamanu alertd install")
} else if error_msg.contains("Access is denied") || error_msg.contains("ERROR_ACCESS_DENIED") {
miette!("Failed to create service: {}\n\nThis requires administrator privileges. Please run this command in an Administrator command prompt or PowerShell.", error_msg)
} else {
miette!("Failed to create service: {}\n\nTroubleshoot:\n - Restart the 'Service Control Manager' service (Services.msc)\n - Restart Windows if the problem persists\n - Check Windows Event Viewer > Windows Logs > System for related errors\n - Verify the service name 'bestool-alertd' is not reserved or in-use\n - Try running: 'sc query bestool-alertd' to check service state\n - Try running: 'sc delete bestool-alertd' if service is marked for deletion", error_msg)
}
})?;
service
.set_description("Monitors and executes alert definitions from configuration files")
.map_err(|e| miette!("Failed to set service description: {}\n\nThe service was created but configuration failed. Please try uninstalling and reinstalling.", e))?;
apply_failure_actions(&service)?;
service
.start::<&OsStr>(&[])
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("marked for deletion") || error_msg.contains("ERROR_SERVICE_MARKED_FOR_DELETE") {
miette!("Failed to start service: {}\n\nThe service is marked for deletion. Please restart Windows and try again.", error_msg)
} else {
miette!("Failed to start service: {}\n\nTroubleshoot:\n - Check Windows Event Viewer under Windows Logs > System\n - Verify the bestool executable path is correct and accessible\n - Ensure no other service is using the same name\n - Try starting the service manually using Services.msc", error_msg)
}
})?;
print!("Waiting for service to start");
let max_wait = Duration::from_secs(30);
let start = std::time::Instant::now();
let poll_interval = Duration::from_millis(500);
loop {
std::thread::sleep(poll_interval);
print!(".");
std::io::Write::flush(&mut std::io::stdout()).ok();
match service.query_status() {
Ok(status) => {
if status.current_state == ServiceState::Running {
println!(" ✓");
break;
}
}
Err(e) => {
println!();
return Err(miette!(
"Failed to query service status while waiting for startup: {}",
e
));
}
}
if start.elapsed() > max_wait {
println!();
return Err(miette!(
"Service failed to reach Running state within 30 seconds. Check Windows Event Viewer for details."
));
}
}
let log_path = get_service_log_path()?;
println!("\nService installed and started successfully!");
println!("\nTo monitor the service:");
println!(" • Open Services.msc and find 'BES Alert Daemon'");
println!(" • Check status and startup type (should be 'Automatic')");
println!("\nService logs:");
println!(" • Location: {}", log_path.display());
println!(" • Logs are stored in JSON format with timestamps");
println!("\nFor errors:");
println!(" • Check the log files in the directory above");
println!(
" • Or check Windows Event Viewer: Windows Logs > System (search for 'bestool-alertd')"
);
Ok(())
}
pub fn uninstall_service() -> Result<()> {
let manager_access = ServiceManagerAccess::CONNECT;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("Access is denied") || error_msg.contains("ERROR_ACCESS_DENIED") {
miette!("Failed to connect to service manager: {}\n\nThis requires administrator privileges. Please run this command in an Administrator command prompt or PowerShell.", error_msg)
} else {
miette!("Failed to connect to service manager: {}\n\nTroubleshoot:\n - Ensure you have administrator privileges\n - Check that Windows Service Control Manager is running", error_msg)
}
})?;
let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE;
let service = service_manager
.open_service("bestool-alertd", service_access)
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("not found") || error_msg.contains("ERROR_SERVICE_DOES_NOT_EXIST") {
miette!("Service 'bestool-alertd' not found.\n\nThe service doesn't appear to be installed. No action needed.")
} else if error_msg.contains("Access is denied") || error_msg.contains("ERROR_ACCESS_DENIED") {
miette!("Failed to open service: {}\n\nThis requires administrator privileges. Please run this command in an Administrator command prompt or PowerShell.", error_msg)
} else {
miette!("Failed to open service: {}\n\nTroubleshoot:\n - Ensure you have administrator privileges\n - Verify the service 'bestool-alertd' is installed", error_msg)
}
})?;
let service_status = service.query_status().ok();
if let Some(status) = service_status {
if status.current_state != ServiceState::Stopped {
match service.stop() {
Ok(_) => {
std::thread::sleep(Duration::from_millis(500));
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("not running")
|| error_msg.contains("ERROR_SERVICE_NOT_ACTIVE")
{
} else {
eprintln!("Warning: Failed to stop service cleanly: {}", error_msg);
eprintln!(" Attempting to delete service anyway...");
}
}
}
}
}
service
.delete()
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("marked for deletion") || error_msg.contains("ERROR_SERVICE_MARKED_FOR_DELETE") {
miette!("Service is already marked for deletion. It will be removed after the next restart.")
} else {
miette!("Failed to delete service: {}\n\nTroubleshoot:\n - Ensure no processes are using this service\n - The service may need to be restarted first\n - Check Windows Event Viewer for more details\n - You may need to restart Windows and try again", error_msg)
}
})?;
println!("Service stopped and uninstalled successfully");
Ok(())
}
pub fn configure_recovery() -> Result<()> {
let manager_access = ServiceManagerAccess::CONNECT;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("Access is denied") || error_msg.contains("ERROR_ACCESS_DENIED") {
miette!("Failed to connect to service manager: {}\n\nThis requires administrator privileges. Please run this command in an Administrator command prompt or PowerShell.", error_msg)
} else {
miette!("Failed to connect to service manager: {}", error_msg)
}
})?;
let service_access = ServiceAccess::QUERY_CONFIG | ServiceAccess::CHANGE_CONFIG;
let service = service_manager
.open_service(SERVICE_NAME, service_access)
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("not found") || error_msg.contains("ERROR_SERVICE_DOES_NOT_EXIST")
{
miette!(
"Service 'bestool-alertd' not found.\n\nInstall the service first with: bestool-alertd install"
)
} else if error_msg.contains("Access is denied")
|| error_msg.contains("ERROR_ACCESS_DENIED")
{
miette!(
"Failed to open service: {}\n\nThis requires administrator privileges.",
error_msg
)
} else {
miette!("Failed to open service: {}", error_msg)
}
})?;
apply_failure_actions(&service)?;
println!("Failure recovery actions configured successfully");
println!(" 1st failure: restart after 10 seconds");
println!(" 2nd failure: restart after 30 seconds");
println!(" 3rd+ failure: restart after 60 seconds");
println!(" Reset counter after: 24 hours");
Ok(())
}
pub fn is_recovery_configured() -> Result<bool> {
let manager_access = ServiceManagerAccess::CONNECT;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)
.map_err(|e| miette!("Failed to connect to service manager: {}", e))?;
let service_access = ServiceAccess::QUERY_CONFIG;
let service = service_manager
.open_service(SERVICE_NAME, service_access)
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("not found") || error_msg.contains("ERROR_SERVICE_DOES_NOT_EXIST")
{
miette!("Service 'bestool-alertd' not found.")
} else {
miette!("Failed to open service: {}", error_msg)
}
})?;
let actions = service
.get_failure_actions()
.map_err(|e| miette!("Failed to query failure actions: {}", e))?;
let has_restart_action = actions.actions.as_ref().is_some_and(|a| {
a.iter()
.any(|act| act.action_type == ServiceActionType::Restart)
});
let non_crash_enabled = service
.get_failure_actions_on_non_crash_failures()
.unwrap_or(false);
Ok(has_restart_action && non_crash_enabled)
}
fn apply_failure_actions(service: &windows_service::service::Service) -> Result<()> {
let failure_actions = ServiceFailureActions {
reset_period: ServiceFailureResetPeriod::After(Duration::from_secs(86400)),
reboot_msg: None,
command: None,
actions: Some(vec![
ServiceAction {
action_type: ServiceActionType::Restart,
delay: Duration::from_secs(10),
},
ServiceAction {
action_type: ServiceActionType::Restart,
delay: Duration::from_secs(30),
},
ServiceAction {
action_type: ServiceActionType::Restart,
delay: Duration::from_secs(60),
},
]),
};
service
.update_failure_actions(failure_actions)
.map_err(|e| miette!("Failed to configure failure recovery actions: {}", e))?;
service
.set_failure_actions_on_non_crash_failures(true)
.map_err(|e| {
miette!(
"Failed to enable failure actions on non-crash failures: {}",
e
)
})?;
Ok(())
}