diff --git a/src/main.rs b/src/main.rs index 724169a..bdd939f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,18 +50,27 @@ fn exe_dir() -> PathBuf { } fn main() { - // Unter Windows: standardmäßig versuchen, als Dienst zu starten. Wird die - // EXE interaktiv gestartet (Doppelklick / Konsole), scheitert der - // SCM-Dispatcher (nicht vom SCM gestartet) → Konsolenmodus. `--console` - // erzwingt den Konsolenmodus direkt. + // Unter Windows: ohne `--console` als Windows-Dienst laufen (SCM-Dispatcher). + // Für den interaktiven Betrieb (Doppelklick / Terminal) `--console` nutzen. #[cfg(windows)] { let console = std::env::args().any(|a| a == "--console" || a == "-c"); if !console { - match service::run() { - Ok(()) => return, // lief als Dienst, sauber beendet - Err(_e) => { /* interaktiv gestartet → Konsole */ } + // KEIN automatischer Konsolen-Fallback: scheiterte hier der + // Dispatcher, würde ein Fallback einen NICHT vom SCM verwalteten + // Geisterprozess starten, der einen erfolgreichen Dienst-Start nur + // vortäuscht. Stattdessen den Fehler sichtbar machen (Session 0 hat + // keine Konsole → Fatal-Log) und beenden. + if let Err(e) = service::run() { + fallback_log( + &exe_dir(), + &format!( + "Dienst-Dispatcher fehlgeschlagen: {e}. \ + Falls interaktiv gestartet: mit '--console' starten." + ), + ); } + return; } } diff --git a/src/service.rs b/src/service.rs index 08b054f..57587f3 100644 --- a/src/service.rs +++ b/src/service.rs @@ -3,8 +3,13 @@ //! //! Ablauf: `run()` übergibt den Prozess an den SCM. Der SCM ruft `service_main` //! in einem eigenen Thread; dort registrieren wir einen Control-Handler -//! (für Stop/Shutdown), melden „Running", starten eine eigene tokio-Runtime und -//! lassen `crate::run_app` laufen, bis der Handler ein Stop-Signal schickt. +//! (Stop/Shutdown), melden zuerst `StartPending`, dann `Running`, starten eine +//! eigene tokio-Runtime und lassen `crate::run_app` laufen, bis der Handler ein +//! Stop-Signal schickt. +//! +//! Diagnose: Im Dienstmodus gibt es KEINE Konsole. Damit ein Fehlstart nicht +//! unsichtbar bleibt, schreiben wir Schlüsselereignisse + Fehler in +//! `mailclient-fatal.log` (neben der EXE) — die einzige Spur in Session 0. use std::ffi::OsString; use std::sync::mpsc; @@ -17,24 +22,32 @@ use windows_service::service_control_handler::{self, ServiceControlHandlerResult use windows_service::{define_windows_service, service_dispatcher}; /// Interner Dienst-Name (Schlüssel). **Muss** mit dem `-Name` im -/// Install-Skript (`install-service.ps1`) übereinstimmen. Der Anzeigename -/// „Holzleitner App Mails" wird separat beim Registrieren gesetzt. +/// Install-Skript (`install-service.ps1`) übereinstimmen. const SERVICE_NAME: &str = "HolzleitnerAppMails"; 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. +/// vom SCM gestartet wurde (interaktiver Start → Fehler 1063) oder der +/// Dispatcher anderweitig scheitert. Der Aufrufer (`main`) protokolliert das. pub fn run() -> windows_service::Result<()> { service_dispatcher::start(SERVICE_NAME, ffi_service_main) } +/// Schreibt eine Diagnosezeile in `mailclient-fatal.log` neben der EXE. Im +/// Dienstmodus die einzige Spur (kein stdout, Logger steht evtl. noch nicht). +fn log_fatal(msg: &str) { + crate::fallback_log(&crate::exe_dir(), msg); +} + fn service_main(_args: Vec) { - // Fehler hier landen (falls vor dem Logger) im fallback_log von run_app bzw. - // werden vom SCM als nicht-laufend erkannt. - let _ = run_service(); + // Erstes Lebenszeichen: bestätigt, dass der SCM unsere ServiceMain erreicht + // hat (grenzt „Dispatcher kam nie an" von „Start-Logik scheitert" ab). + log_fatal("service_main: vom SCM gestartet"); + if let Err(e) = run_service() { + log_fatal(&format!("service_main: run_service fehlgeschlagen: {e}")); + } } fn run_service() -> windows_service::Result<()> { @@ -54,7 +67,36 @@ fn run_service() -> windows_service::Result<()> { let status_handle = service_control_handler::register(SERVICE_NAME, handler)?; - // „Running" melden (akzeptiert Stop + System-Shutdown). + // 1) StartPending melden (mit wait_hint), BEVOR potenziell langsamer Init + // (Runtime-Aufbau) läuft. Der SCM weiß dadurch „Start läuft" und bricht + // nicht vorzeitig ab — der saubere Start-Handshake. + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::StartPending, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::from_secs(15), + process_id: None, + })?; + + // 2) Eigene tokio-Runtime im Dienst-Thread (kein #[tokio::main]). + let rt = match tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(e) => { + log_fatal(&format!("service: tokio-Runtime-Aufbau fehlgeschlagen: {e}")); + // Sauber als „gestoppt mit Fehler" melden, damit der SCM den Start + // als fehlgeschlagen erkennt (statt Timeout). + status_handle + .set_service_status(stopped_status(ServiceExitCode::ServiceSpecific(1)))?; + return Ok(()); + } + }; + + // 3) Running melden (akzeptiert Stop + System-Shutdown). status_handle.set_service_status(ServiceStatus { service_type: SERVICE_TYPE, current_state: ServiceState::Running, @@ -65,11 +107,6 @@ fn run_service() -> windows_service::Result<()> { 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"); rt.block_on(async move { // mpsc::recv ist blockierend → in spawn_blocking auslagern und als // Future an run_app übergeben. @@ -83,15 +120,19 @@ fn run_service() -> windows_service::Result<()> { }); // „Stopped" melden. - status_handle.set_service_status(ServiceStatus { + status_handle.set_service_status(stopped_status(ServiceExitCode::Win32(0)))?; + Ok(()) +} + +/// Baut einen `Stopped`-Status mit dem gegebenen Exit-Code. +fn stopped_status(exit_code: ServiceExitCode) -> ServiceStatus { + ServiceStatus { service_type: SERVICE_TYPE, current_state: ServiceState::Stopped, controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::Win32(0), + exit_code, checkpoint: 0, wait_hint: Duration::default(), process_id: None, - })?; - - Ok(()) + } }