From 4f0cbf0d81849b6e8c372f7144681f0a5ed285f6 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 28 Jul 2026 18:52:00 -0700 Subject: [PATCH] Desktop: ask before quitting on top of a running install (#7550) * Desktop: ask before quitting on top of a running install This is the trigger neither #7492 nor #7490 addresses -- both start from a venv that is already broken. Confirmed: neither PR touches cleanup_child_processes. Quitting runs cleanup_child_processes -> install::stop_install, which SIGTERMs the installer's process group. In the reported session that landed at "5/10 studio deps", so the venv kept the CLI's dependencies and lost the server stack, and the next launch died on `import structlog`. Three minutes of installing, destroyed with no warning and no way back. So ask. Only from the tray Quit item -- a deliberate action with a UI present. The RunEvent::Exit path (OS shutdown, SIGTERM) is left alone: it must never block on a dialog nobody can answer. The call already runs off the menu callback thread, which is also what blocking_show requires. Closing the window was already safe (it hides to tray); this closes the remaining way to lose an install by accident. * Tighten comments in desktop quit-during-install guard * Condense comments in quit-during-install guard --------- Co-authored-by: danielhanchen --- studio/src-tauri/src/install.rs | 8 ++++++++ studio/src-tauri/src/main.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/studio/src-tauri/src/install.rs b/studio/src-tauri/src/install.rs index d7226bf901..39d67dc427 100644 --- a/studio/src-tauri/src/install.rs +++ b/studio/src-tauri/src/install.rs @@ -783,6 +783,14 @@ pub fn record_install_intentional_stop(state: &InstallState, diagnostics: &Diagn } } +/// True while an installer runs; quitting now would leave a broken venv. +pub fn is_install_running(state: &InstallState) -> bool { + state + .lock() + .map(|install| install.child.is_some()) + .unwrap_or(false) +} + /// Stop a running install process gracefully. /// Unix: SIGTERM to process group -> wait up to 5s -> SIGKILL /// Windows: hidden taskkill /T /F to terminate the installer tree diff --git a/studio/src-tauri/src/main.rs b/studio/src-tauri/src/main.rs index a867700035..0d39217ecd 100644 --- a/studio/src-tauri/src/main.rs +++ b/studio/src-tauri/src/main.rs @@ -85,6 +85,33 @@ fn setup_custom_titlebar(app: &tauri::App) -> Result<(), Box bool { + use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; + + let Some(install_state) = app.try_state::() else { + return true; + }; + if !install::is_install_running(&install_state) { + return true; + } + app.dialog() + .message( + "Unsloth Studio is still installing. Quitting now stops it part-way and \ + leaves the installation incomplete, so it will need to be repaired before \ + it can start.", + ) + .kind(MessageDialogKind::Warning) + .title("Installation in progress") + .buttons(MessageDialogButtons::OkCancelCustom( + "Quit anyway".to_string(), + "Keep installing".to_string(), + )) + .blocking_show() +} + fn cleanup_child_processes(app: &tauri::AppHandle) { let diagnostics_state = app .try_state::() @@ -138,6 +165,9 @@ fn setup_tray(app: &tauri::App) -> Result<(), Box> { // leaving the backend orphaned. let app_handle = app.clone(); std::thread::spawn(move || { + if !confirm_quit_during_install(&app_handle) { + return; + } cleanup_child_processes(&app_handle); app_handle.exit(0); });