Skip to content

Commit 892eaa1

Browse files
fix: infinite loop on user-initiated exit
1 parent 00baf2c commit 892eaa1

1 file changed

Lines changed: 46 additions & 38 deletions

File tree

src/lib.rs

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::HashMap;
2+
use std::sync::atomic::{AtomicU8, Ordering};
23
use std::sync::Arc;
34

45
use serde::Serialize;
@@ -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

Comments
 (0)