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 ;
@@ -301,6 +302,9 @@ impl Builder {
301302 Ok ( ( ) )
302303 } )
303304 . on_event ( |app, event| {
305+ // 0 = not started, 1 = running, 2 = complete
306+ static CLEANUP_STATE : AtomicU8 = AtomicU8 :: new ( 0 ) ;
307+
304308 match event {
305309 RunEvent :: ExitRequested { api, code, .. } => {
306310 // Only intercept user-initiated exits (code is None). Programmatic
@@ -310,51 +314,55 @@ impl Builder {
310314 return ;
311315 }
312316
317+ // Claim cleanup ownership once. If another handler invocation won
318+ // the race, keep exit prevented while its cleanup finishes.
319+ if CLEANUP_STATE
320+ . compare_exchange ( 0 , 1 , Ordering :: SeqCst , Ordering :: SeqCst )
321+ . is_err ( )
322+ {
323+ if CLEANUP_STATE . load ( Ordering :: SeqCst ) == 1 {
324+ api. prevent_exit ( ) ;
325+ debug ! ( "Exit requested while database cleanup is in progress" ) ;
326+ }
327+ return ;
328+ }
329+
313330 info ! ( "App exit requested - cleaning up transactions and databases" ) ;
314331
315332 // Prevent immediate exit so we can close connections and checkpoint WAL
316333 api. prevent_exit ( ) ;
317334
318335 let app_handle = app. clone ( ) ;
319336
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-
329337 let instances_clone = app. state :: < DbInstances > ( ) . inner ( ) . clone ( ) ;
330338 let interruptible_txs_clone = app. state :: < ActiveInterruptibleTransactions > ( ) . inner ( ) . clone ( ) ;
331339 let regular_txs_clone = app. state :: < ActiveRegularTransactions > ( ) . inner ( ) . clone ( ) ;
332340 let active_subs_clone = app. state :: < subscriptions:: ActiveSubscriptions > ( ) . inner ( ) . clone ( ) ;
333341
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- }
354-
342+ // Run cleanup on the async runtime (without blocking the event loop),
343+ // then trigger a programmatic exit when done.
344+ tauri:: async_runtime:: spawn ( async move {
345+ {
355346 let timeout_result = tokio:: time:: timeout (
356347 std:: time:: Duration :: from_secs ( 5 ) ,
357348 async {
349+ // First, abort all subscriptions and transactions
350+ debug ! ( "Aborting active subscriptions and transactions" ) ;
351+ active_subs_clone. abort_all ( ) . await ;
352+ sqlx_sqlite_toolkit:: cleanup_all_transactions ( & interruptible_txs_clone, & regular_txs_clone) . await ;
353+
354+ // Close databases (each wrapper's close() disables its own
355+ // observer at the crate level, unregistering SQLite hooks)
356+ let mut guard = instances_clone. inner . write ( ) . await ;
357+ let wrappers: Vec < DatabaseWrapper > =
358+ guard. drain ( ) . map ( |( _, v) | v) . collect ( ) ;
359+
360+ // Close databases in parallel
361+ let mut set = tokio:: task:: JoinSet :: new ( ) ;
362+ for wrapper in wrappers {
363+ set. spawn ( async move { wrapper. close ( ) . await } ) ;
364+ }
365+
358366 while let Some ( result) = set. join_next ( ) . await {
359367 match result {
360368 Ok ( Err ( e) ) => warn ! ( "Error closing database: {:?}" , e) ,
@@ -371,15 +379,15 @@ impl Builder {
371379 } else {
372380 debug ! ( "Database cleanup complete" ) ;
373381 }
374- } )
375- } )
376- . join ( ) ;
377-
378- if let Err ( e) = cleanup_result {
379- error ! ( "Database cleanup thread panicked: {:?}" , e) ;
380- }
382+ }
381383
382- app_handle. exit ( code. unwrap_or ( 0 ) ) ;
384+ // Mark cleanup complete before triggering programmatic exit.
385+ // Follow-up ExitRequested events won't rerun cleanup because:
386+ // - Programmatic shutdown requests bypass the user-exit cleanup path
387+ // - Cleanup can only be started once per process lifetime
388+ CLEANUP_STATE . store ( 2 , Ordering :: SeqCst ) ;
389+ app_handle. exit ( 0 ) ;
390+ } ) ;
383391 }
384392 RunEvent :: Exit => {
385393 // ExitRequested should have already closed all databases
0 commit comments