fix: reliably keep server alive after GUI close on Windows (#402)

The HTTP /watchdog/disable request races with process exit on Windows,
causing the watchdog to kill the server before the request arrives.

Added a .keep-running sentinel file as a reliable fallback:
- Tauri writes the file to data_dir before sending the HTTP request
- The watchdog checks for it during the grace period after detecting
  parent death
- The file is removed after being read to avoid stale state

This approach works regardless of HTTP timing because file writes
complete synchronously before the Tauri process exits.

Fixes #372

Co-authored-by: Matt Van Horn <[email protected]>
This commit is contained in:
Matt Van Horn
2026-04-16 01:56:30 -07:00
committed by GitHub
co-authored by Matt Van Horn
parent 3e7727d1d2
commit 479bc7fc5e
2 changed files with 33 additions and 0 deletions
+17
View File
@@ -105,6 +105,11 @@ def _start_parent_watchdog(parent_pid, data_dir=None):
This is the clean shutdown mechanism: instead of the Tauri app trying to
forcefully kill the server (which spawns console windows on Windows),
the server monitors its parent and shuts itself down gracefully.
The Tauri app writes a .keep-running sentinel file to data_dir before
exiting when "remain running after close" is enabled. This is a reliable
fallback for the HTTP /watchdog/disable request, which can race with
process exit on Windows.
"""
import os
import signal
@@ -178,6 +183,18 @@ def _start_parent_watchdog(parent_pid, data_dir=None):
if _watchdog_disabled:
watchdog_logger.info("Watchdog was disabled during grace period, keeping server alive")
return
# Check for sentinel file written by Tauri before exit.
# This catches the case where the HTTP disable request
# didn't arrive before the parent process died (common
# on Windows where process teardown is fast).
sentinel = os.path.join(data_dir, ".keep-running") if data_dir else None
if sentinel and os.path.exists(sentinel):
watchdog_logger.info("Found .keep-running sentinel file, keeping server alive")
try:
os.remove(sentinel)
except OSError:
pass
return
watchdog_logger.info("Watchdog still enabled after grace period, shutting down server...")
if sys.platform == "win32":
# sys.exit triggers SystemExit, allowing uvicorn to run
+16
View File
@@ -860,6 +860,22 @@ pub fn run() {
// Tell the server to disable its watchdog so it survives
// after this process exits.
println!("Keep server running: disabling watchdog...");
// Write a sentinel file as a reliable fallback. On Windows
// the HTTP request below can race with process exit, leaving
// the watchdog unaware it should stay alive. The sentinel
// file is checked during the watchdog grace period.
let data_dir = app
.path()
.app_data_dir()
.unwrap_or_default();
let sentinel = data_dir.join(".keep-running");
if let Err(e) = std::fs::write(&sentinel, b"1") {
eprintln!("Failed to write keep-running sentinel: {}", e);
} else {
println!("Wrote keep-running sentinel to {:?}", sentinel);
}
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(2))
.build()