[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Improve crashing behaviour

Kasper (Sep 14, 2025, 8:01 AM +0200) 89f9defa 143cf351

+56 -26
+1
CHANGELOG.md
··· 2 2 3 3 ## Next 4 4 - Make macOS media key permission request non-intrusive 5 + - Improve crashing behaviour 5 6 6 7 ## 0.19.9 - 2025 Jul 4 7 8 - Fix column size not updating when resizing window
+9 -3
src/electron/ipc.ts
··· 4 4 import is from './is' 5 5 import { check_for_updates } from './update' 6 6 import { init_media_keys } from './shortcuts' 7 + import { trigger_crash } from './main' 7 8 8 9 ipc_main.handle('check_for_updates', (_e) => { 9 10 return check_for_updates() ?? null ··· 15 16 shell.openExternal(url) 16 17 }) 17 18 18 - ipc_main.handle('showMessageBox', async (e, attached, options) => { 19 + ipc_main.handle('showMessageBox', async (e, attached, options, crash) => { 19 20 const window = BrowserWindow.fromWebContents(e.sender) 21 + let result: Awaited<ReturnType<typeof dialog.showMessageBox>> 20 22 if (attached && window) { 21 - return await dialog.showMessageBox(window, options) 23 + result = await dialog.showMessageBox(window, options) 22 24 } else { 23 - return await dialog.showMessageBox(options) 25 + result = await dialog.showMessageBox(options) 24 26 } 27 + if (crash) { 28 + trigger_crash() 29 + } 30 + return result 25 31 }) 26 32 27 33 ipc_main.handle('showOpenDialog', async (e, attached, options) => {
+11 -1
src/electron/main.ts
··· 10 10 import url from 'url' 11 11 import { ipc_main } from './typed_ipc' 12 12 13 + export function trigger_crash() { 14 + app.once('will-quit', () => { 15 + process.exit(1) 16 + }) 17 + app.quit() 18 + setTimeout(() => { 19 + process.exit(1) 20 + }) 21 + } 22 + 13 23 async function err_handler(msg: string, error: Error) { 14 24 app.whenReady().then(() => { 15 25 dialog.showMessageBoxSync({ ··· 18 28 detail: error.stack, 19 29 title: 'Error', 20 30 }) 21 - err_handler(msg, error) 31 + trigger_crash() 22 32 }) 23 33 } 24 34 process.on('uncaughtException', (error) => {
+1
src/electron/typed_ipc.ts
··· 152 152 showMessageBox: ( 153 153 attached: boolean, 154 154 options: Parameters<typeof dialog.showMessageBox>[0], 155 + crash?: boolean, 155 156 ) => ReturnType<typeof dialog.showMessageBox> 156 157 showOpenDialog: ( 157 158 attached: boolean,
+4 -1
src/lib/data.ts
··· 13 13 import { queue } from './queue' 14 14 import { current_playlist_id } from '@/components/TrackList.svelte' 15 15 import { navigate } from './router' 16 - import { call, get_error_message } from './error' 16 + import { call, get_error_message, strict_call } from './error' 17 17 18 18 export const is_dev = window.is_dev 19 19 export const local_data_path = window.local_data_path ··· 235 235 236 236 export function get_artists() { 237 237 return call((addon) => addon.get_artists()) 238 + } 239 + export function get_genres() { 240 + return strict_call((addon) => addon.get_genres()) 238 241 } 239 242 export function move_tracks(playlist_id: TrackListID, indexes: ItemId[], to_index: number) { 240 243 call((data) => data.move_tracks(playlist_id, indexes, to_index))
+30 -21
src/lib/error.ts
··· 20 20 } 21 21 return '' 22 22 } 23 - function error_popup(err: unknown) { 24 - ipc_renderer.invoke('showMessageBox', false, { 25 - type: 'error', 26 - message: get_error_message(err), 27 - detail: get_error_stack(err), 28 - }) 23 + function error_popup(err: unknown, crash = false) { 24 + ipc_renderer.invoke( 25 + 'showMessageBox', 26 + false, 27 + { 28 + type: 'error', 29 + message: get_error_message(err), 30 + detail: get_error_stack(err), 31 + }, 32 + crash, 33 + ) 29 34 } 30 35 31 36 /** @deprecated */ ··· 34 39 const result = cb(window.addon) 35 40 if (result instanceof Promise) { 36 41 return result.catch((err) => { 42 + console.error(err) 37 43 error_popup(err) 38 44 throw err 39 45 }) as P ··· 41 47 return result 42 48 } 43 49 } catch (err) { 44 - console.error('errorPopup:', err) 50 + console.error(err) 45 51 error_popup(err) 46 52 throw err 47 53 } 48 54 } 49 55 50 - export function safe_call<T>( 51 - cb: (addon: typeof window.addon) => T, 52 - ): T extends Promise<unknown> ? Promise<Awaited<T> | Error> : T | Error { 56 + // Crashes on error 57 + export function strict_call<T>(cb: (addon: typeof window.addon) => T): T { 53 58 try { 54 59 const result = cb(window.addon) 55 60 56 - // Handle asynchronous result (Promise) 61 + // Handle async errors 57 62 if (result instanceof Promise) { 58 - return result.catch((err) => { 59 - error_popup(err) 60 - return err as Error 61 - }) as T extends Promise<unknown> ? Promise<Awaited<T> | Error> : never 63 + result.catch((error) => { 64 + error_popup(error, true) 65 + throw error 66 + }) 62 67 } 63 68 64 69 // Handle synchronous result 65 - return result as T extends Promise<unknown> ? never : T | Error 66 - } catch (err) { 70 + return result 71 + } catch (raw_err) { 72 + let error: Error 73 + if (raw_err instanceof Error) { 74 + error = raw_err 75 + } else { 76 + error = new Error('Unexpected error: ' + String(raw_err)) 77 + } 67 78 // Handle synchronous errors 68 - console.error('errorPopup:', err) 69 - error_popup(err) 70 - // Correct return type for synchronous calls 71 - return err as T extends Promise<unknown> ? never : Error 79 + error_popup(error, true) 80 + throw error 72 81 } 73 82 }