fix(service): StartPending-Handshake + Fehler sichtbar, kein Konsolen-Fallback

Der Windows-Dienst startete nicht zuverlaessig (blieb in 'Wird gestartet' /
generischer Startfehler). Behoben:
- StartPending -> Running melden (statt direkt Running): sauberer SCM-Handshake.
- service_main protokolliert Start + Fehler in mailclient-fatal.log (Session 0
  hat keine Konsole); Runtime-/Status-Fehler werden als 'gestoppt mit Fehler'
  gemeldet statt ins Leere zu laufen.
- main faellt bei Dispatcher-Fehler NICHT mehr still in den Konsolenmodus (das
  erzeugte einen nicht vom SCM verwalteten Geisterprozess, der einen
  erfolgreichen Start vortaeuschte + die EXE sperrte). Interaktiv via --console.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-06-08 17:21:14 +02:00
parent 3774a378f3
commit ec3760318b
2 changed files with 77 additions and 27 deletions

View File

@ -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;
}
}

View File

@ -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<OsString>) {
// 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(())
}
}