diff --git a/ferrum-addon/addon.d.ts b/ferrum-addon/addon.d.ts index 8be3a38..766677a 100644 --- a/ferrum-addon/addon.d.ts +++ b/ferrum-addon/addon.d.ts @@ -14,6 +14,8 @@ export declare function add_skip(trackId: string): void export declare function add_tracks_to_playlist(playlistId: string, trackIds: Array): void +export declare function close_cache_db(): Promise + export interface CountObject { count: number fromDate: MsSinceUnixEpoch diff --git a/package-lock.json b/package-lock.json index 5aee507..72db7a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@sveltejs/vite-plugin-svelte": "^5.1.0", "@tailwindcss/vite": "^4.1.8", "@tsconfig/svelte": "^5.0.4", - "electron": "^28.3.3", + "electron": "^29.4.6", "electron-builder": "^26.0.12", "eslint": "^9.28.0", "eslint-config-prettier": "^10.1.5", @@ -4469,13 +4469,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.110", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.110.tgz", - "integrity": "sha512-WW2o4gTmREtSnqKty9nhqF/vA0GKd0V/rbC0OyjSk9Bz6bzlsXKT+i7WDdS/a0z74rfT2PO4dArVCSnapNLA5Q==", + "version": "20.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.15.tgz", + "integrity": "sha512-W3bqcbLsRdFDVcmAM5l6oLlcl67vjevn8j1FPZ4nx+K5jNoWCh+FC/btxFoBPnvQlrHHDwfjp1kjIEDfwJ0Mog==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, "node_modules/@types/plist": { @@ -6256,15 +6256,15 @@ } }, "node_modules/electron": { - "version": "28.3.3", - "resolved": "https://registry.npmjs.org/electron/-/electron-28.3.3.tgz", - "integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==", + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.6.tgz", + "integrity": "sha512-fz8ndj8cmmf441t4Yh2FDP3Rn0JhLkVGvtUf2YVMbJ5SdJPlc0JWll9jYkhh60jDKVVCr/tBAmfxqRnXMWJpzg==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -11164,9 +11164,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 3546678..d942bd8 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@sveltejs/vite-plugin-svelte": "^5.1.0", "@tailwindcss/vite": "^4.1.8", "@tsconfig/svelte": "^5.0.4", - "electron": "^28.3.3", + "electron": "^29.4.6", "electron-builder": "^26.0.12", "eslint": "^9.28.0", "eslint-config-prettier": "^10.1.5", diff --git a/src-native/tracks/cover.rs b/src-native/tracks/cover.rs index 1aa7819..95669d0 100644 --- a/src-native/tracks/cover.rs +++ b/src-native/tracks/cover.rs @@ -5,13 +5,12 @@ use fast_image_resize::{IntoImageView, Resizer}; use image::codecs::jpeg::JpegEncoder; use image::codecs::png::PngEncoder; use image::{ImageEncoder, ImageFormat, ImageReader}; -use lazy_static::lazy_static; use napi::bindgen_prelude::Buffer; use redb::{Database, TableDefinition}; use std::fs; use std::io::{BufWriter, Cursor}; use std::path::PathBuf; -use std::sync::{Arc, RwLock}; +use std::sync::{LazyLock, RwLock}; use std::time::{Instant, UNIX_EPOCH}; // (modified_timestamp_ms, image_bytes) @@ -19,35 +18,57 @@ type CacheEntry = (i64, Vec); const IMG_CACHE_TABLE: TableDefinition<&str, CacheEntry> = TableDefinition::new("img_cache"); -lazy_static! { - static ref CACHE_DB: Arc>> = Arc::new(RwLock::new(None)); -} +static CACHE_DB: LazyLock>> = LazyLock::new(|| RwLock::new(None)); fn init_cache_db(path: String) -> Result<()> { let now = Instant::now(); - let cache_db_mutex = CACHE_DB.read().unwrap(); - if cache_db_mutex.is_none() { - drop(cache_db_mutex); - let mut cache_db_mutex = CACHE_DB.write().unwrap(); - if cache_db_mutex.is_none() { - let db = Database::create(&path).context("Could not load image cache: {}")?; - let init_txn = db - .begin_write() - .context("Could not begin write transaction: {}")?; - { - // Create table - init_txn - .open_table(IMG_CACHE_TABLE) - .context("Could not open table: {}")?; - } - init_txn.commit().context("Could not commit cache: {}")?; - *cache_db_mutex = Some(db); - println!("Initialized Cache.redb: {}ms", now.elapsed().as_millis()); + + // Check quickly with a read-lock first + { + let cache_db_global = &*CACHE_DB; + let cache_db_lock = cache_db_global.read().unwrap(); + if cache_db_lock.is_some() { + return Ok(()); + } + } + + let cache_db_global = &*CACHE_DB; + let mut cache_db_lock = cache_db_global.write().unwrap(); + if cache_db_lock.is_none() { + let db = Database::create(&path).context("Could not load image cache")?; + let init_txn = db + .begin_write() + .context("Could not begin write transaction")?; + { + // Create table + init_txn + .open_table(IMG_CACHE_TABLE) + .context("Could not open table")?; } + init_txn.commit().context("Could not commit cache")?; + *cache_db_lock = Some(db); + println!("Initialized Cache.redb: {}ms", now.elapsed().as_millis()); } Ok(()) } +#[napi(js_name = "close_cache_db")] +#[allow(dead_code)] +pub async fn close_cache_db() -> napi::Result<()> { + let cache_db_global = &*CACHE_DB; + let mut cache_db_lock = cache_db_global.write().unwrap(); + if let Some(cache_db) = cache_db_lock.take() { + let now = Instant::now(); + drop(cache_db); + println!("Closed Cache.redb: {}ms", now.elapsed().as_millis()); + } else { + // This can happen if the app crashes during startup + println!("Cache.redb was not open"); + } + + Ok(()) +} + /// Returns `None` if the file does not have an image fn get_modified_timestamp_ms(path: &str) -> Result> { let file_metadata = match fs::metadata(path) { @@ -129,8 +150,9 @@ pub async fn read_small_cover_async( init_cache_db(cache_db_path)?; - let cache_db_mutex = CACHE_DB.read().unwrap(); - let cache_db = cache_db_mutex.as_ref().unwrap(); + let cache_db_global = &*CACHE_DB; + let cache_db_lock = cache_db_global.read().unwrap(); + let cache_db = cache_db_lock.as_ref().unwrap(); let date_modified_ms: Option = get_modified_timestamp_ms(&path)?.map(|n| n.try_into().unwrap()); diff --git a/src/electron/ipc.ts b/src/electron/ipc.ts index 3091fb9..c10c5a3 100644 --- a/src/electron/ipc.ts +++ b/src/electron/ipc.ts @@ -25,7 +25,7 @@ ipc_main.handle('showMessageBox', async (e, attached, options, crash) => { result = await dialog.showMessageBox(options) } if (crash) { - trigger_crash() + trigger_crash(null) } return result }) diff --git a/src/electron/main.ts b/src/electron/main.ts index 481e33e..797861c 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -10,32 +10,43 @@ import path from 'path' import url from 'url' import { ipc_main } from './typed_ipc' -export function trigger_crash() { - app.once('will-quit', () => { - process.exit(1) - }) - app.quit() - setTimeout(() => { - process.exit(1) - }) +async function close_cache_db() { + await addon.close_cache_db() } -async function err_handler(msg: string, error: Error | string) { - app.whenReady().then(() => { - dialog.showMessageBoxSync({ - type: 'error', - message: msg, - detail: error instanceof Error ? error.stack : error, - title: 'Error', +let triggering_crash = false +export function trigger_crash(popup: { msg: string; error: Error | string } | null) { + if (triggering_crash) { + return + } + triggering_crash = true + quitting = true + const close_promise = close_cache_db() + app.whenReady().then(async () => { + if (popup) { + dialog.showMessageBoxSync({ + type: 'error', + message: popup.msg, + detail: popup.error instanceof Error ? popup.error.stack : popup.error, + title: 'Error', + }) + } + browser_windows.main_window?.close() + await close_promise + app.once('will-quit', () => { + process.exit(1) + }) + app.quit() + setTimeout(() => { + process.exit(1) }) - trigger_crash() }) } process.on('uncaughtException', (error) => { - err_handler('Unhandled Error', error) + trigger_crash({ msg: 'Unhandled Error', error }) }) process.on('unhandledRejection', (error: Error) => { - err_handler('Unhandled Promise Rejection', error) + trigger_crash({ msg: 'Unhandled Promise Rejection', error }) }) const app_data = app.getPath('appData') @@ -55,8 +66,11 @@ if (is.dev) { let quitting = false let app_loaded = false -app.on('window-all-closed', () => { - app.quit() +app.on('window-all-closed', async () => { + if (!triggering_crash) { + await close_cache_db() + app.quit() + } }) protocol.registerSchemesAsPrivileged([ @@ -180,16 +194,16 @@ app.whenReady().then(async () => { main_window.webContents.on('render-process-gone', (_e, details) => { if (details.reason === 'crashed') { // we have a napi-rs panic handler message popup already - trigger_crash() + trigger_crash(null) } else if ( details.reason !== 'clean-exit' && details.reason !== 'abnormal-exit' && details.reason !== 'killed' ) { - err_handler( - `Crashed with code ${details.exitCode} (${details.reason})`, - 'Error message was likely logged to console.', - ) + trigger_crash({ + msg: `Crashed with code ${details.exitCode} (${details.reason})`, + error: 'Error message was likely logged to console.', + }) } }) ipc_main.handle('app_loaded', () => { diff --git a/src/lib/player.ts b/src/lib/player.ts index 6f61349..ad75a69 100644 --- a/src/lib/player.ts +++ b/src/lib/player.ts @@ -236,7 +236,7 @@ export function stop() { } } -quit.setHandler('player', () => { +quit.set_handler('player', () => { stop() }) diff --git a/src/lib/quit.ts b/src/lib/quit.ts index 7c264ce..d37b5f5 100644 --- a/src/lib/quit.ts +++ b/src/lib/quit.ts @@ -18,7 +18,7 @@ function set_handler(name: string, callback: Callback) { handlers[name] = callback } -export default { notReady: not_ready, ready, setHandler: set_handler } +export default { not_ready, ready, set_handler } function try_to_quit() { if (unfinished_tasks === 0) {