#![warn(dead_code)]
use std::ffi::OsString;
use std::sync::OnceLock;
use std::time::Duration;
use windows_service::service::{
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState,
ServiceStatus as ScmStatus, ServiceType,
};
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
use windows_service::{define_windows_service, service_dispatcher};
use super::scm_backend::{describe, raw_code};
use super::{EXIT_ERROR, EXIT_OK, SERVICE_NAME, scm, scm_log};
use crate::api::shutdown as api_shutdown;
use crate::cli::ApiArgs;
use crate::common::config_file::{self, Settings};
static STATUS_HANDLE: OnceLock<service_control_handler::ServiceStatusHandle> = OnceLock::new();
const EXIT_CODE_UNEXPECTED_STOP: u32 = 1;
pub fn run() -> i32 {
match service_dispatcher::start(SERVICE_NAME, ffi_service_main) {
Ok(()) => EXIT_OK,
Err(e) => {
eprintln!(
"error: {}",
scm::console_entry_point_message(raw_code(&e), &describe(&e))
);
EXIT_ERROR
}
}
}
define_windows_service!(ffi_service_main, service_main);
fn service_main(_arguments: Vec<OsString>) {
let log_dir = scm_log::init();
if let Err(e) = run_service(&log_dir) {
tracing::error!("all-smi service failed: {e}");
report(
ServiceState::Stopped,
ServiceControlAccept::empty(),
ServiceExitCode::ServiceSpecific(EXIT_CODE_UNEXPECTED_STOP),
Duration::default(),
);
}
}
fn run_service(log_dir: &Result<std::path::PathBuf, String>) -> Result<(), String> {
let event_handler = move |control: ServiceControl| -> ServiceControlHandlerResult {
match control {
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
ServiceControl::Stop | ServiceControl::Shutdown => {
report(
ServiceState::StopPending,
ServiceControlAccept::empty(),
ServiceExitCode::Win32(0),
Duration::from_secs(scm::TRANSITION_WAIT_HINT_SECS),
);
api_shutdown::request_shutdown();
ServiceControlHandlerResult::NoError
}
_ => ServiceControlHandlerResult::NotImplemented,
}
};
let status_handle =
service_control_handler::register(SERVICE_NAME, event_handler).map_err(|e| {
format!(
"could not register the service control handler: {}",
describe(&e)
)
})?;
let _ = STATUS_HANDLE.set(status_handle);
let accepted = ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN;
report(
ServiceState::StartPending,
accepted,
ServiceExitCode::Win32(0),
Duration::from_secs(scm::TRANSITION_WAIT_HINT_SECS),
);
match log_dir {
Ok(dir) => tracing::info!("all-smi service starting; logging to {}", dir.display()),
Err(e) => eprintln!("warning: {e}"),
}
let settings = load_settings()?;
let args = api_args(&settings);
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(4)
.build()
.map_err(|e| format!("could not build the Tokio runtime: {e}"))?;
runtime.spawn(async move {
api_shutdown::wait_until_serving().await;
if !api_shutdown::shutdown_requested() {
report(
ServiceState::Running,
accepted,
ServiceExitCode::Win32(0),
Duration::default(),
);
}
});
runtime.block_on(async {
crate::api::run_api_mode(&args, &settings).await;
});
let exit_code = if api_shutdown::shutdown_requested() {
tracing::info!("all-smi service stopped cleanly");
ServiceExitCode::Win32(0)
} else {
tracing::error!(
"the API server exited without a stop request; the SCM will apply the configured \
failure actions"
);
ServiceExitCode::ServiceSpecific(EXIT_CODE_UNEXPECTED_STOP)
};
report(
ServiceState::Stopped,
ServiceControlAccept::empty(),
exit_code,
Duration::default(),
);
Ok(())
}
fn report(
current_state: ServiceState,
controls_accepted: ServiceControlAccept,
exit_code: ServiceExitCode,
wait_hint: Duration,
) {
let Some(handle) = STATUS_HANDLE.get() else {
return;
};
let status = ScmStatus {
service_type: ServiceType::OWN_PROCESS,
current_state,
controls_accepted,
exit_code,
checkpoint: 0,
wait_hint,
process_id: None,
};
if let Err(e) = handle.set_service_status(status) {
tracing::error!(
"could not report {current_state:?} to the service control manager: {}",
describe(&e)
);
}
}
fn load_settings() -> Result<Settings, String> {
let outcome = config_file::load(None).map_err(|e| format!("configuration error: {e}"))?;
for w in &outcome.warnings {
tracing::warn!("config: {w}");
}
for k in &outcome.settings.unknown_keys {
tracing::warn!("config: unknown key `{k}` (forward-compatible, preserved)");
}
match crate::common::paths::discover_existing_config() {
Some(path) => tracing::info!("config: loaded {}", path.display()),
None => tracing::info!(
"config: no file found in the search path; using compiled defaults plus environment \
overrides"
),
}
Ok(outcome.settings)
}
fn api_args(settings: &Settings) -> ApiArgs {
ApiArgs {
port: Some(settings.api.port),
interval: Some(settings.api.interval_secs),
processes: Some(settings.api.processes),
}
}
#[cfg(test)]
#[path = "scm_host_tests.rs"]
mod tests;