From 479bc7fc5e0e416d1b228bc01f3f99908e87917d Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Thu, 16 Apr 2026 04:56:30 -0400 Subject: [PATCH] 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 <455140+mvanhorn@users.noreply.github.com> --- backend/server.py | 17 +++++++++++++++++ tauri/src-tauri/src/main.rs | 16 ++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/backend/server.py b/backend/server.py index bc6a81b2..2262d4c8 100644 --- a/backend/server.py +++ b/backend/server.py @@ -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 diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index c65dda0c..ca0cdf07 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -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()