11use std:: collections:: HashMap ;
22use std:: sync:: Arc ;
3+ use std:: sync:: atomic:: { AtomicU8 , Ordering } ;
34
45use serde:: Serialize ;
56use sqlx_sqlite_conn_mgr:: Migrator ;
@@ -25,6 +26,23 @@ pub use sqlx_sqlite_toolkit::{
2526/// Default maximum number of concurrently loaded databases.
2627const DEFAULT_MAX_DATABASES : usize = 50 ;
2728
29+ /// Tracks cleanup progress during app exit: 0 = not started, 1 = running, 2 = complete.
30+ static CLEANUP_STATE : AtomicU8 = AtomicU8 :: new ( 0 ) ;
31+
32+ /// Guarantees `CLEANUP_STATE` reaches `2` and `app_handle.exit(0)` fires even if the
33+ /// cleanup task panics. Without this, a panic would leave the state at `1` and subsequent
34+ /// user exit attempts would call `prevent_exit()` indefinitely.
35+ struct ExitGuard < R : Runtime > {
36+ app_handle : tauri:: AppHandle < R > ,
37+ }
38+
39+ impl < R : Runtime > Drop for ExitGuard < R > {
40+ fn drop ( & mut self ) {
41+ CLEANUP_STATE . store ( 2 , Ordering :: SeqCst ) ;
42+ self . app_handle . exit ( 0 ) ;
43+ }
44+ }
45+
2846/// Database instances managed by the plugin.
2947///
3048/// This struct maintains a thread-safe map of database paths to their corresponding
@@ -310,51 +328,61 @@ impl Builder {
310328 return ;
311329 }
312330
331+ // Claim cleanup ownership once. If another handler invocation won
332+ // the race, keep exit prevented while its cleanup finishes.
333+ if CLEANUP_STATE
334+ . compare_exchange ( 0 , 1 , Ordering :: SeqCst , Ordering :: SeqCst )
335+ . is_err ( )
336+ {
337+ if CLEANUP_STATE . load ( Ordering :: SeqCst ) == 1 {
338+ api. prevent_exit ( ) ;
339+ debug ! ( "Exit requested while database cleanup is in progress" ) ;
340+ }
341+ return ;
342+ }
343+
313344 info ! ( "App exit requested - cleaning up transactions and databases" ) ;
314345
315346 // Prevent immediate exit so we can close connections and checkpoint WAL
316347 api. prevent_exit ( ) ;
317348
318349 let app_handle = app. clone ( ) ;
319350
320- let handle = match tokio:: runtime:: Handle :: try_current ( ) {
321- Ok ( h) => h,
322- Err ( _) => {
323- warn ! ( "No tokio runtime available for cleanup" ) ;
324- app_handle. exit ( code. unwrap_or ( 0 ) ) ;
325- return ;
326- }
327- } ;
328-
329351 let instances_clone = app. state :: < DbInstances > ( ) . inner ( ) . clone ( ) ;
330352 let interruptible_txs_clone = app. state :: < ActiveInterruptibleTransactions > ( ) . inner ( ) . clone ( ) ;
331353 let regular_txs_clone = app. state :: < ActiveRegularTransactions > ( ) . inner ( ) . clone ( ) ;
332354 let active_subs_clone = app. state :: < subscriptions:: ActiveSubscriptions > ( ) . inner ( ) . clone ( ) ;
333355
334- // Spawn a blocking thread to abort transactions and close databases
335- // (block_in_place panics on current_thread runtime)
336- let cleanup_result = std:: thread:: spawn ( move || {
337- handle. block_on ( async {
338- // First, abort all subscriptions and transactions
339- debug ! ( "Aborting active subscriptions and transactions" ) ;
340- active_subs_clone. abort_all ( ) . await ;
341- sqlx_sqlite_toolkit:: cleanup_all_transactions ( & interruptible_txs_clone, & regular_txs_clone) . await ;
342-
343- // Close databases (each wrapper's close() disables its own
344- // observer at the crate level, unregistering SQLite hooks)
345- let mut guard = instances_clone. inner . write ( ) . await ;
346- let wrappers: Vec < DatabaseWrapper > =
347- guard. drain ( ) . map ( |( _, v) | v) . collect ( ) ;
348-
349- // Close databases in parallel with timeout
350- let mut set = tokio:: task:: JoinSet :: new ( ) ;
351- for wrapper in wrappers {
352- set. spawn ( async move { wrapper. close ( ) . await } ) ;
353- }
356+ // Run cleanup on the async runtime (without blocking the event loop),
357+ // then trigger a programmatic exit when done. ExitGuard ensures
358+ // CLEANUP_STATE reaches 2 and exit() fires even on panic.
359+ tauri:: async_runtime:: spawn ( async move {
360+ let _guard = ExitGuard { app_handle } ;
354361
362+ // Scope block: drops the RwLock write guard (from instances_clone)
363+ // before _guard fires exit(), whose RunEvent::Exit handler calls
364+ // try_read() on the same lock.
365+ {
355366 let timeout_result = tokio:: time:: timeout (
356367 std:: time:: Duration :: from_secs ( 5 ) ,
357368 async {
369+ // First, abort all subscriptions and transactions
370+ debug ! ( "Aborting active subscriptions and transactions" ) ;
371+ active_subs_clone. abort_all ( ) . await ;
372+ sqlx_sqlite_toolkit:: cleanup_all_transactions ( & interruptible_txs_clone, & regular_txs_clone) . await ;
373+
374+ // Close databases (each wrapper's close() disables its own
375+ // observer at the crate level, unregistering SQLite hooks)
376+ let mut guard = instances_clone. inner . write ( ) . await ;
377+ let wrappers: Vec < DatabaseWrapper > =
378+ guard. drain ( ) . map ( |( _, v) | v) . collect ( ) ;
379+
380+ // Close databases in parallel
381+ let mut set = tokio:: task:: JoinSet :: new ( ) ;
382+ for wrapper in wrappers {
383+ set. spawn ( async move { wrapper. close ( ) . await } ) ;
384+ }
385+
358386 while let Some ( result) = set. join_next ( ) . await {
359387 match result {
360388 Ok ( Err ( e) ) => warn ! ( "Error closing database: {:?}" , e) ,
@@ -371,15 +399,8 @@ impl Builder {
371399 } else {
372400 debug ! ( "Database cleanup complete" ) ;
373401 }
374- } )
375- } )
376- . join ( ) ;
377-
378- if let Err ( e) = cleanup_result {
379- error ! ( "Database cleanup thread panicked: {:?}" , e) ;
380- }
381-
382- app_handle. exit ( code. unwrap_or ( 0 ) ) ;
402+ }
403+ } ) ;
383404 }
384405 RunEvent :: Exit => {
385406 // ExitRequested should have already closed all databases
0 commit comments