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:
23
src/main.rs
23
src/main.rs
@ -50,18 +50,27 @@ fn exe_dir() -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Unter Windows: standardmäßig versuchen, als Dienst zu starten. Wird die
|
// Unter Windows: ohne `--console` als Windows-Dienst laufen (SCM-Dispatcher).
|
||||||
// EXE interaktiv gestartet (Doppelklick / Konsole), scheitert der
|
// Für den interaktiven Betrieb (Doppelklick / Terminal) `--console` nutzen.
|
||||||
// SCM-Dispatcher (nicht vom SCM gestartet) → Konsolenmodus. `--console`
|
|
||||||
// erzwingt den Konsolenmodus direkt.
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let console = std::env::args().any(|a| a == "--console" || a == "-c");
|
let console = std::env::args().any(|a| a == "--console" || a == "-c");
|
||||||
if !console {
|
if !console {
|
||||||
match service::run() {
|
// KEIN automatischer Konsolen-Fallback: scheiterte hier der
|
||||||
Ok(()) => return, // lief als Dienst, sauber beendet
|
// Dispatcher, würde ein Fallback einen NICHT vom SCM verwalteten
|
||||||
Err(_e) => { /* interaktiv gestartet → Konsole */ }
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,13 @@
|
|||||||
//!
|
//!
|
||||||
//! Ablauf: `run()` übergibt den Prozess an den SCM. Der SCM ruft `service_main`
|
//! Ablauf: `run()` übergibt den Prozess an den SCM. Der SCM ruft `service_main`
|
||||||
//! in einem eigenen Thread; dort registrieren wir einen Control-Handler
|
//! in einem eigenen Thread; dort registrieren wir einen Control-Handler
|
||||||
//! (für Stop/Shutdown), melden „Running", starten eine eigene tokio-Runtime und
|
//! (Stop/Shutdown), melden zuerst `StartPending`, dann `Running`, starten eine
|
||||||
//! lassen `crate::run_app` laufen, bis der Handler ein Stop-Signal schickt.
|
//! 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::ffi::OsString;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
@ -17,24 +22,32 @@ use windows_service::service_control_handler::{self, ServiceControlHandlerResult
|
|||||||
use windows_service::{define_windows_service, service_dispatcher};
|
use windows_service::{define_windows_service, service_dispatcher};
|
||||||
|
|
||||||
/// Interner Dienst-Name (Schlüssel). **Muss** mit dem `-Name` im
|
/// Interner Dienst-Name (Schlüssel). **Muss** mit dem `-Name` im
|
||||||
/// Install-Skript (`install-service.ps1`) übereinstimmen. Der Anzeigename
|
/// Install-Skript (`install-service.ps1`) übereinstimmen.
|
||||||
/// „Holzleitner App Mails" wird separat beim Registrieren gesetzt.
|
|
||||||
const SERVICE_NAME: &str = "HolzleitnerAppMails";
|
const SERVICE_NAME: &str = "HolzleitnerAppMails";
|
||||||
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
|
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
|
||||||
|
|
||||||
define_windows_service!(ffi_service_main, service_main);
|
define_windows_service!(ffi_service_main, service_main);
|
||||||
|
|
||||||
/// Übergibt den Prozess an den SCM. Gibt `Err` zurück, wenn der Prozess NICHT
|
/// Ü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
|
/// vom SCM gestartet wurde (interaktiver Start → Fehler 1063) oder der
|
||||||
/// den Konsolenmodus zurück.
|
/// Dispatcher anderweitig scheitert. Der Aufrufer (`main`) protokolliert das.
|
||||||
pub fn run() -> windows_service::Result<()> {
|
pub fn run() -> windows_service::Result<()> {
|
||||||
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
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>) {
|
fn service_main(_args: Vec<OsString>) {
|
||||||
// Fehler hier landen (falls vor dem Logger) im fallback_log von run_app bzw.
|
// Erstes Lebenszeichen: bestätigt, dass der SCM unsere ServiceMain erreicht
|
||||||
// werden vom SCM als nicht-laufend erkannt.
|
// hat (grenzt „Dispatcher kam nie an" von „Start-Logik scheitert" ab).
|
||||||
let _ = run_service();
|
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<()> {
|
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)?;
|
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 {
|
status_handle.set_service_status(ServiceStatus {
|
||||||
service_type: SERVICE_TYPE,
|
service_type: SERVICE_TYPE,
|
||||||
current_state: ServiceState::Running,
|
current_state: ServiceState::Running,
|
||||||
@ -65,11 +107,6 @@ fn run_service() -> windows_service::Result<()> {
|
|||||||
process_id: None,
|
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 {
|
rt.block_on(async move {
|
||||||
// mpsc::recv ist blockierend → in spawn_blocking auslagern und als
|
// mpsc::recv ist blockierend → in spawn_blocking auslagern und als
|
||||||
// Future an run_app übergeben.
|
// Future an run_app übergeben.
|
||||||
@ -83,15 +120,19 @@ fn run_service() -> windows_service::Result<()> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// „Stopped" melden.
|
// „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,
|
service_type: SERVICE_TYPE,
|
||||||
current_state: ServiceState::Stopped,
|
current_state: ServiceState::Stopped,
|
||||||
controls_accepted: ServiceControlAccept::empty(),
|
controls_accepted: ServiceControlAccept::empty(),
|
||||||
exit_code: ServiceExitCode::Win32(0),
|
exit_code,
|
||||||
checkpoint: 0,
|
checkpoint: 0,
|
||||||
wait_hint: Duration::default(),
|
wait_hint: Duration::default(),
|
||||||
process_id: None,
|
process_id: None,
|
||||||
})?;
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user