diff --git a/packages/tauri-app/src-tauri/src/cli_manager.rs b/packages/tauri-app/src-tauri/src/cli_manager.rs index a1b8013a..3656277c 100644 --- a/packages/tauri-app/src-tauri/src/cli_manager.rs +++ b/packages/tauri-app/src-tauri/src/cli_manager.rs @@ -464,13 +464,33 @@ impl CliProcessManager { let status_clone = status.clone(); let app_clone = app.clone(); thread::spawn(move || { - let code = { - let mut guard = child_holder.lock(); - if let Some(child) = guard.as_mut() { - child.wait().ok() - } else { - None + // Do not hold the child mutex while waiting for process exit. + // Holding the lock across `wait()` deadlocks `stop()`, which needs the + // same lock to send SIGTERM/SIGKILL when the user quits the app. + let code = loop { + let maybe_exited = { + let mut guard = child_holder.lock(); + if guard.is_none() { + return; + } + match guard + .as_mut() + .and_then(|child| child.try_wait().ok().flatten()) + { + Some(status) => { + // Drop the handle after the process exits so other callers + // don't attempt to stop/kill a finished process. + *guard = None; + Some(status) + } + None => None, + } + }; + + if let Some(status) = maybe_exited { + break Some(status); } + thread::sleep(Duration::from_millis(100)); }; let mut locked = status_clone.lock(); diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index 1748a07a..6cc35251 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -4,6 +4,7 @@ mod cli_manager; use cli_manager::{CliProcessManager, CliStatus}; use serde_json::json; +use std::sync::atomic::{AtomicBool, Ordering}; use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder}; use tauri::plugin::{Builder as PluginBuilder, TauriPlugin}; use tauri::webview::Webview; @@ -11,6 +12,8 @@ use tauri::{AppHandle, Emitter, Manager, Runtime, Wry}; use tauri_plugin_opener::OpenerExt; use url::Url; +static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false); + #[derive(Clone)] pub struct AppState { pub manager: CliProcessManager, @@ -167,6 +170,11 @@ fn main() { .expect("error while building tauri application") .run(|app_handle, event| match event { tauri::RunEvent::ExitRequested { api, .. } => { + // `app_handle.exit(0)` triggers another `ExitRequested`. Without a guard, we can + // prevent exit forever and the app never quits (Cmd+Q / Quit menu appears stuck). + if QUIT_REQUESTED.swap(true, Ordering::SeqCst) { + return; + } api.prevent_exit(); let app = app_handle.clone(); std::thread::spawn(move || { @@ -181,6 +189,9 @@ fn main() { .. } => { // Ensure we have time to stop the CLI process before the app exits. + if QUIT_REQUESTED.swap(true, Ordering::SeqCst) { + return; + } api.prevent_close(); let app = app_handle.clone(); std::thread::spawn(move || {