//! Windows-Dienst-Integration (Service Control Manager). Nur unter Windows //! kompiliert (`#[cfg(windows)] mod service` in `main.rs`). //! //! Ablauf: `run()` übergibt den Prozess an den SCM. Der SCM ruft `service_main` //! in einem eigenen Thread; dort registrieren wir einen Control-Handler //! (Stop/Shutdown), melden „Running", starten eine eigene tokio-Runtime und //! lassen `crate::run_app` laufen, bis der Handler ein Stop-Signal schickt. //! //! Das Arbeitsverzeichnis wird auf das EXE-Verzeichnis gesetzt, weil ein Dienst //! sonst in `C:\Windows\System32` startet — damit landen `config.toml`, //! `data/` und `logs/` neben der EXE (so wie das Install-Skript es erwartet). use std::ffi::OsString; use std::path::Path; use std::sync::mpsc; use std::time::Duration; use windows_service::service::{ ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, }; use windows_service::service_control_handler::{self, ServiceControlHandlerResult}; use windows_service::{define_windows_service, service_dispatcher}; /// Interner Dienst-Name (Schlüssel). **Muss** mit dem `-ServiceName` im /// Install-Skript (`install-service.ps1`) übereinstimmen. Der Anzeigename /// „Holzleitner Backend" wird separat beim Registrieren gesetzt. const SERVICE_NAME: &str = "HolzleitnerBackend"; const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; define_windows_service!(ffi_service_main, service_main); /// Übergibt den Prozess an den SCM. Gibt `Err` zurück, wenn der Prozess NICHT /// vom SCM gestartet wurde (interaktiver Start) — der Aufrufer fällt dann in /// den Konsolenmodus zurück. pub fn run() -> windows_service::Result<()> { service_dispatcher::start(SERVICE_NAME, ffi_service_main) } fn service_main(_args: Vec) { let _ = run_service(); } fn run_service() -> windows_service::Result<()> { // Arbeitsverzeichnis aufs EXE-Verzeichnis setzen (Dienst startet sonst in // System32) — damit config.toml/data/logs neben der EXE aufgelöst werden. if let Some(dir) = std::env::current_exe() .ok() .and_then(|p| p.parent().map(Path::to_path_buf)) { let _ = std::env::set_current_dir(&dir); } // Kanal: Control-Handler -> Loop. Stop/Shutdown sendet ein () hinein. let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(); let handler = move |control| -> ServiceControlHandlerResult { match control { ServiceControl::Stop | ServiceControl::Shutdown => { let _ = shutdown_tx.send(()); ServiceControlHandlerResult::NoError } ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, _ => ServiceControlHandlerResult::NotImplemented, } }; let status_handle = service_control_handler::register(SERVICE_NAME, handler)?; // „Running" melden (akzeptiert Stop + System-Shutdown). status_handle.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, })?; // Eigene tokio-Runtime im Dienst-Thread (kein #[tokio::main]). let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .expect("tokio runtime"); let result = rt.block_on(async move { // mpsc::recv ist blockierend → in spawn_blocking auslagern und als // Future an run_app übergeben. let shutdown = async move { let _ = tokio::task::spawn_blocking(move || { let _ = shutdown_rx.recv(); }) .await; }; crate::run_app(shutdown, true).await }); // Boot-/Laufzeitfehler: vor der Logger-Initialisierung gibt es im Dienst // keine Spur — daher in eine Fallback-Datei neben die EXE schreiben. let exit_code = match result { Ok(()) => ServiceExitCode::Win32(0), Err(e) => { fallback_log(&format!("run_app fehlgeschlagen: {e:#}")); ServiceExitCode::Win32(1) } }; // „Stopped" melden. status_handle.set_service_status(ServiceStatus { service_type: SERVICE_TYPE, current_state: ServiceState::Stopped, controls_accepted: ServiceControlAccept::empty(), exit_code, checkpoint: 0, wait_hint: Duration::default(), process_id: None, })?; Ok(()) } /// Notfall-Logging, wenn der reguläre Logger (noch) nicht steht — schreibt nach /// `\holzleitner-backend-fatal.log`. Im Dienst-Modus die einzige Spur, /// falls schon das Laden von `config.toml` scheitert. fn fallback_log(msg: &str) { let file = crate::exe_dir().join("holzleitner-backend-fatal.log"); if let Ok(mut f) = std::fs::OpenOptions::new() .create(true) .append(true) .open(&file) { use std::io::Write; let _ = writeln!( f, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg ); } }