diff --git a/electron/src/index.ts b/electron/src/index.ts index 87c50e3e..d653e443 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -40,68 +40,79 @@ if (electronIsDev) { // Run Application (async () => { try { - // Wait for electron app to be ready. + // Wait for electron app to be ready first await app.whenReady(); - - // Security - Set Content-Security-Policy based on whether or not we are in dev mode. + console.log('[Electron Main Process] App is ready'); + + // Initialize SQLite plugin and handlers BEFORE creating any windows + console.log('[Electron Main Process] Initializing SQLite...'); + setupSQLiteHandlers(); + await initializeSQLite(); + console.log('[Electron Main Process] SQLite initialization complete'); + + // Security - Set Content-Security-Policy setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme()); - // Initialize our app, build windows, and load content first + // Initialize our app and create window console.log('[Electron Main Process] Starting app initialization...'); await myCapacitorApp.init(); console.log('[Electron Main Process] App initialization complete'); - // Get the main window and wait for it to be ready + // Get the main window const mainWindow = myCapacitorApp.getMainWindow(); if (!mainWindow) { throw new Error('Main window not available after app initialization'); } - // Wait for window to be ready + // Wait for window to be ready and loaded await new Promise((resolve) => { - if (mainWindow.isVisible()) { - resolve(); - } else { - mainWindow.once('show', () => resolve()); - } + const handleReady = () => { + console.log('[Electron Main Process] Window ready to show'); + mainWindow.show(); + + // Wait for window to finish loading + mainWindow.webContents.once('did-finish-load', () => { + console.log('[Electron Main Process] Window finished loading'); + + // Send SQLite ready signal after window is fully loaded + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send('sqlite-ready'); + console.log('[Electron Main Process] Sent SQLite ready signal to renderer'); + } else { + console.warn('[Electron Main Process] Window was destroyed before sending SQLite ready signal'); + } + + resolve(); + }); + }; + + // Always use the event since isReadyToShow is not reliable + mainWindow.once('ready-to-show', handleReady); }); - // Now initialize SQLite after window is ready - console.log('[Electron Main Process] Starting SQLite initialization...'); - try { - // Register handlers first to prevent "no handler" errors - setupSQLiteHandlers(); - console.log('[Electron Main Process] SQLite handlers registered'); - - // Then initialize the plugin - await initializeSQLite(); - console.log('[Electron Main Process] SQLite plugin initialized successfully'); - - // Send SQLite ready signal since window is ready - if (!mainWindow.isDestroyed()) { - mainWindow.webContents.send('sqlite-ready'); - console.log('[Electron Main Process] Sent SQLite ready signal to renderer'); - } else { - console.warn('[Electron Main Process] Could not send SQLite ready signal - window was destroyed'); - } - } catch (error) { - console.error('[Electron Main Process] Failed to initialize SQLite:', error); - // Notify renderer about database status - if (!mainWindow.isDestroyed()) { - mainWindow.webContents.send('database-status', { - status: 'error', - error: error instanceof Error ? error.message : 'Unknown error' - }); - } - // Don't proceed with app initialization if SQLite fails - throw new Error(`SQLite initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - - // Check for updates if we are in a packaged app. + // Check for updates if we are in a packaged app if (!electronIsDev) { console.log('[Electron Main Process] Checking for updates...'); autoUpdater.checkForUpdatesAndNotify(); } + + // Handle window close + mainWindow.on('closed', () => { + console.log('[Electron Main Process] Main window closed'); + }); + + // Handle window close request + mainWindow.on('close', (event) => { + console.log('[Electron Main Process] Window close requested'); + if (mainWindow.webContents.isLoading()) { + event.preventDefault(); + console.log('[Electron Main Process] Deferring window close due to loading state'); + mainWindow.webContents.once('did-finish-load', () => { + mainWindow.close(); + }); + } + }); + } catch (error) { console.error('[Electron Main Process] Fatal error during initialization:', error); app.quit(); diff --git a/electron/src/preload.ts b/electron/src/preload.ts index 290801ac..f63d7abf 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -28,50 +28,81 @@ interface SQLiteConnectionOptions { [key: string]: unknown; // Allow other properties } -// Create a proxy for the CapacitorSQLite plugin +// Define valid channels for security +const VALID_CHANNELS = { + send: ['toMain', 'sqlite-status'], + receive: ['fromMain', 'sqlite-ready', 'database-status'], + invoke: ['sqlite-is-available', 'sqlite-echo', 'sqlite-create-connection', 'sqlite-execute', 'sqlite-query', 'sqlite-run', 'sqlite-close-connection', 'get-path', 'get-base-path'] +}; + +// Create a secure IPC bridge +const createSecureIPCBridge = () => { + return { + send: (channel: string, data: unknown) => { + if (VALID_CHANNELS.send.includes(channel)) { + ipcRenderer.send(channel, data); + } else { + logger.warn(`[Preload] Attempted to send on invalid channel: ${channel}`); + } + }, + receive: (channel: string, func: (...args: unknown[]) => void) => { + if (VALID_CHANNELS.receive.includes(channel)) { + ipcRenderer.on(channel, (_event, ...args) => func(...args)); + } else { + logger.warn(`[Preload] Attempted to receive on invalid channel: ${channel}`); + } + }, + once: (channel: string, func: (...args: unknown[]) => void) => { + if (VALID_CHANNELS.receive.includes(channel)) { + ipcRenderer.once(channel, (_event, ...args) => func(...args)); + } else { + logger.warn(`[Preload] Attempted to receive once on invalid channel: ${channel}`); + } + }, + invoke: async (channel: string, ...args: unknown[]) => { + if (VALID_CHANNELS.invoke.includes(channel)) { + return await ipcRenderer.invoke(channel, ...args); + } else { + logger.warn(`[Preload] Attempted to invoke on invalid channel: ${channel}`); + throw new Error(`Invalid channel: ${channel}`); + } + } + }; +}; + +// Create SQLite proxy with retry logic const createSQLiteProxy = () => { const MAX_RETRIES = 3; - const RETRY_DELAY = 1000; // 1 second + const RETRY_DELAY = 1000; - const withRetry = async (operation: (...args: unknown[]) => Promise, ...args: unknown[]): Promise => { - let lastError: Error | null = null; + const withRetry = async (operation: (...args: unknown[]) => Promise, ...args: unknown[]) => { + let lastError; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { return await operation(...args); } catch (error) { - lastError = error instanceof Error ? error : new Error(String(error)); + lastError = error; if (attempt < MAX_RETRIES) { - logger.warn(`[CapacitorSQLite] SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error); + logger.warn(`[Preload] SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); } } } - throw new Error(`[CapacitorSQLite] SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`); + throw new Error(`SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`); }; const wrapOperation = (method: string) => { - return async (...args: unknown[]): Promise => { + return async (...args: unknown[]) => { try { - // For createConnection, ensure readOnly is false - if (method === 'create-connection') { - const options = args[0] as SQLiteConnectionOptions; - if (options && typeof options === 'object') { - // Set readOnly to false and ensure mode is rwc - options.readOnly = false; - options.mode = 'rwc'; - // Remove any lowercase readonly property if it exists - delete options.readonly; - } - } - return await withRetry(ipcRenderer.invoke, 'sqlite-' + method, ...args); + const handlerName = `sqlite-${method}`; + return await withRetry(ipcRenderer.invoke, handlerName, ...args); } catch (error) { - logger.error(`[CapacitorSQLite] SQLite ${method} failed:`, error); - throw new Error(`[CapacitorSQLite] Database operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + logger.error(`[Preload] SQLite ${method} failed:`, error); + throw error; } }; }; - // Create a proxy that matches the CapacitorSQLite interface return { echo: wrapOperation('echo'), createConnection: wrapOperation('create-connection'), @@ -80,24 +111,25 @@ const createSQLiteProxy = () => { query: wrapOperation('query'), run: wrapOperation('run'), isAvailable: wrapOperation('is-available'), - getPlatform: () => Promise.resolve('electron'), - // Add other methods as needed + getPlatform: () => Promise.resolve('electron') }; }; -// Expose the Electron IPC API -contextBridge.exposeInMainWorld('electron', { - ipcRenderer: { - on: (channel: string, func: (...args: unknown[]) => void) => ipcRenderer.on(channel, (event, ...args) => func(...args)), - once: (channel: string, func: (...args: unknown[]) => void) => ipcRenderer.once(channel, (event, ...args) => func(...args)), - send: (channel: string, data: unknown) => ipcRenderer.send(channel, data), - invoke: (channel: string, ...args: unknown[]) => ipcRenderer.invoke(channel, ...args), - }, - // Add other APIs as needed -}); +try { + // Expose the secure IPC bridge and SQLite proxy + contextBridge.exposeInMainWorld('electron', { + ipcRenderer: createSecureIPCBridge(), + sqlite: createSQLiteProxy(), + env: { + platform: 'electron', + isDev: process.env.NODE_ENV === 'development' + } + }); -// Expose CapacitorSQLite proxy as before -contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy()); + logger.info('[Preload] IPC bridge and SQLite proxy initialized successfully'); +} catch (error) { + logger.error('[Preload] Failed to initialize IPC bridge:', error); +} // Log startup logger.log('[CapacitorSQLite] Preload script starting...'); diff --git a/electron/src/rt/sqlite-init.ts b/electron/src/rt/sqlite-init.ts index 464969e6..87b39653 100644 --- a/electron/src/rt/sqlite-init.ts +++ b/electron/src/rt/sqlite-init.ts @@ -531,6 +531,7 @@ export function setupSQLiteHandlers(): void { 'sqlite-create-connection', 'sqlite-execute', 'sqlite-query', + 'sqlite-run', 'sqlite-close-connection', 'sqlite-get-error' ]; @@ -578,7 +579,69 @@ export function setupSQLiteHandlers(): void { } }); - // ... other handlers ... + // Add the sqlite-run handler + ipcMain.handle('sqlite-run', async (_event, options) => { + try { + if (!await verifyPluginState()) { + throw new SQLiteError('Plugin not available', 'sqlite-run'); + } + + const { statement, values } = options; + if (!statement) { + throw new SQLiteError('No statement provided', 'sqlite-run'); + } + + // Add database name to options + const runOptions = { + database: 'timesafari', // Use the same database name as in initialization + statement, + values: values || [] + }; + + logger.debug('Running SQL statement:', runOptions); + const result = await pluginState.instance.run(runOptions); + + if (!result) { + throw new SQLiteError('Run operation returned no result', 'sqlite-run'); + } + + return result; + } catch (error) { + throw handleError(error, 'sqlite-run'); + } + }); + + // Add the sqlite-query handler + ipcMain.handle('sqlite-query', async (_event, options) => { + try { + if (!await verifyPluginState()) { + throw new SQLiteError('Plugin not available', 'sqlite-query'); + } + + const { statement, values } = options; + if (!statement) { + throw new SQLiteError('No statement provided', 'sqlite-query'); + } + + // Add database name to options + const queryOptions = { + database: 'timesafari', // Use the same database name as in initialization + statement, + values: values || [] + }; + + logger.debug('Executing SQL query:', queryOptions); + const result = await pluginState.instance.query(queryOptions); + + if (!result) { + throw new SQLiteError('Query operation returned no result', 'sqlite-query'); + } + + return result; + } catch (error) { + throw handleError(error, 'sqlite-query'); + } + }); logger.info('SQLite IPC handlers registered successfully'); } \ No newline at end of file diff --git a/electron/src/setup.ts b/electron/src/setup.ts index 70152b6b..02559500 100644 --- a/electron/src/setup.ts +++ b/electron/src/setup.ts @@ -105,8 +105,15 @@ export class ElectronCapacitorApp { defaultWidth: 1000, defaultHeight: 800, }); - // Setup preload script path and construct our main window. - const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js'); + + // Setup preload script path based on environment + const preloadPath = app.isPackaged + ? join(process.resourcesPath, 'preload.js') + : join(__dirname, 'preload.js'); + + console.log('[Electron Main Process] Preload path:', preloadPath); + console.log('[Electron Main Process] Preload exists:', require('fs').existsSync(preloadPath)); + this.MainWindow = new BrowserWindow({ icon, show: false, @@ -115,11 +122,12 @@ export class ElectronCapacitorApp { width: this.mainWindowState.width, height: this.mainWindowState.height, webPreferences: { - nodeIntegration: true, + nodeIntegration: false, contextIsolation: true, - // Use preload to inject the electron varriant overrides for capacitor plugins. - // preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"), + sandbox: false, preload: preloadPath, + webSecurity: true, + allowRunningInsecureContent: false, }, }); this.mainWindowState.manage(this.MainWindow); diff --git a/src/main.electron.ts b/src/main.electron.ts index 56bf9b39..4647350a 100644 --- a/src/main.electron.ts +++ b/src/main.electron.ts @@ -17,31 +17,56 @@ const app = initializeApp(); // Create a promise that resolves when SQLite is ready const sqliteReady = new Promise((resolve, reject) => { - if (!window.electron?.ipcRenderer) { - logger.error("[Main Electron] IPC renderer not available"); - reject(new Error("IPC renderer not available")); - return; - } - // Set a timeout to prevent hanging const timeout = setTimeout(() => { + logger.error("[Main Electron] SQLite initialization timeout"); reject(new Error("SQLite initialization timeout")); }, 30000); // 30 second timeout - window.electron.ipcRenderer.once('sqlite-ready', () => { - clearTimeout(timeout); - logger.info("[Main Electron] Received SQLite ready signal"); - resolve(); - }); + // Wait for electron bridge to be available + const checkElectronBridge = () => { + if (window.electron?.ipcRenderer) { + logger.info("[Main Electron] IPC renderer bridge available"); + + // Listen for SQLite ready signal + window.electron.ipcRenderer.once('sqlite-ready', () => { + clearTimeout(timeout); + logger.info("[Main Electron] Received SQLite ready signal"); + resolve(); + }); + + // Also listen for database errors + window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => { + clearTimeout(timeout); + const status = args[0] as { status: string; error?: string }; + if (status.status === 'error') { + logger.error("[Main Electron] Database error:", status.error); + reject(new Error(status.error || 'Database initialization failed')); + } + }); - // Also listen for database errors - window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => { - clearTimeout(timeout); - const status = args[0] as { status: string; error?: string }; - if (status.status === 'error') { - reject(new Error(status.error || 'Database initialization failed')); + // Check if SQLite is already available + window.electron.ipcRenderer.invoke('sqlite-is-available') + .then((result: unknown) => { + const isAvailable = Boolean(result); + if (isAvailable) { + logger.info("[Main Electron] SQLite is already available"); + // Don't resolve here - wait for the ready signal + // This prevents race conditions where the ready signal arrives after this check + } + }) + .catch((error: Error) => { + logger.error("[Main Electron] Failed to check SQLite availability:", error); + // Don't reject here - wait for either ready signal or timeout + }); + } else { + // Check again in 100ms if bridge isn't ready + setTimeout(checkElectronBridge, 100); } - }); + }; + + // Start checking for bridge + checkElectronBridge(); }); // Wait for SQLite to be ready before mounting @@ -49,17 +74,31 @@ sqliteReady .then(() => { logger.info("[Main Electron] SQLite ready, mounting app..."); app.mount("#app"); + logger.info("[Main Electron] App mounted successfully"); }) .catch((error) => { logger.error("[Main Electron] Failed to initialize SQLite:", error instanceof Error ? error.message : 'Unknown error'); - // Show error to user + // Show error to user with retry option const errorDiv = document.createElement("div"); errorDiv.style.cssText = - "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%;"; + "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%; z-index: 9999;"; errorDiv.innerHTML = `

Failed to Initialize Application

-

There was an error initializing the database. Please try restarting.

+

There was an error initializing the database. This could be due to:

+
    +
  • Database file is locked by another process
  • +
  • Insufficient permissions to access the database
  • +
  • Database file is corrupted
  • +

Error details: ${error instanceof Error ? error.message : 'Unknown error'}

+
+ + +
`; document.body.appendChild(errorDiv); }); diff --git a/src/router/index.ts b/src/router/index.ts index 0b9aa52b..45a656e9 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -277,18 +277,31 @@ const initialPath = isElectron ? window.location.pathname.split("/dist-electron/www/")[1] || "/" : window.location.pathname; +logger.info("[Router] Initializing router", { isElectron, initialPath }); + const history = isElectron ? createMemoryHistory() // Memory history for Electron : createWebHistory("/"); // Add base path for web apps -/** @type {*} */ const router = createRouter({ history, routes, }); +// Set initial route +router.beforeEach((to, from, next) => { + logger.info("[Router] Navigation", { to: to.path, from: from.path }); + next(); +}); + // Replace initial URL to start at `/` if necessary -router.replace(initialPath || "/"); +if (initialPath === "/" || !initialPath) { + logger.info("[Router] Setting initial route to /"); + router.replace("/"); +} else { + logger.info("[Router] Setting initial route to", initialPath); + router.replace(initialPath); +} const errorHandler = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any