Browse Source

fix(electron): consolidate SQLite initialization and IPC handling

- Consolidate preload script with secure IPC bridge and channel validation
- Ensure single initialization path in main process
- Add robust error handling and user feedback
- Fix race conditions in window creation and SQLite ready signal

Current state:
- SQLite initializes successfully in main process
- IPC bridge is established and events are transmitted
- Window creation and loading sequence is correct
- Renderer receives ready signal and mounts app
- Database operations still fail in renderer due to connection issues

Known issues:
- SQLite proxy not properly initialized in renderer
- Database connection not established in renderer
- Error logging attempts to use database before ready
- Connection state management needs improvement

This commit represents a stable point where IPC communication
is working but database operations need to be fixed.
sql-absurd-sql-further
Matthew Raymer 5 days ago
parent
commit
91a1c05473
  1. 81
      electron/src/index.ts
  2. 102
      electron/src/preload.ts
  3. 65
      electron/src/rt/sqlite-init.ts
  4. 18
      electron/src/setup.ts
  5. 57
      src/main.electron.ts
  6. 17
      src/router/index.ts

81
electron/src/index.ts

@ -40,68 +40,79 @@ if (electronIsDev) {
// Run Application // Run Application
(async () => { (async () => {
try { try {
// Wait for electron app to be ready. // Wait for electron app to be ready first
await app.whenReady(); await app.whenReady();
console.log('[Electron Main Process] App is ready');
// Security - Set Content-Security-Policy based on whether or not we are in dev mode. // 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()); 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...'); console.log('[Electron Main Process] Starting app initialization...');
await myCapacitorApp.init(); await myCapacitorApp.init();
console.log('[Electron Main Process] App initialization complete'); 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(); const mainWindow = myCapacitorApp.getMainWindow();
if (!mainWindow) { if (!mainWindow) {
throw new Error('Main window not available after app initialization'); 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<void>((resolve) => { await new Promise<void>((resolve) => {
if (mainWindow.isVisible()) { const handleReady = () => {
resolve(); console.log('[Electron Main Process] Window ready to show');
} else { mainWindow.show();
mainWindow.once('show', () => resolve());
}
});
// Now initialize SQLite after window is ready // Wait for window to finish loading
console.log('[Electron Main Process] Starting SQLite initialization...'); mainWindow.webContents.once('did-finish-load', () => {
try { console.log('[Electron Main Process] Window finished loading');
// 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 // Send SQLite ready signal after window is fully loaded
if (!mainWindow.isDestroyed()) { if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('sqlite-ready'); mainWindow.webContents.send('sqlite-ready');
console.log('[Electron Main Process] Sent SQLite ready signal to renderer'); console.log('[Electron Main Process] Sent SQLite ready signal to renderer');
} else { } else {
console.warn('[Electron Main Process] Could not send SQLite ready signal - window was destroyed'); console.warn('[Electron Main Process] Window was destroyed before sending SQLite ready signal');
} }
} catch (error) {
console.error('[Electron Main Process] Failed to initialize SQLite:', error); resolve();
// 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. // Always use the event since isReadyToShow is not reliable
mainWindow.once('ready-to-show', handleReady);
});
// Check for updates if we are in a packaged app
if (!electronIsDev) { if (!electronIsDev) {
console.log('[Electron Main Process] Checking for updates...'); console.log('[Electron Main Process] Checking for updates...');
autoUpdater.checkForUpdatesAndNotify(); 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) { } catch (error) {
console.error('[Electron Main Process] Fatal error during initialization:', error); console.error('[Electron Main Process] Fatal error during initialization:', error);
app.quit(); app.quit();

102
electron/src/preload.ts

@ -28,50 +28,81 @@ interface SQLiteConnectionOptions {
[key: string]: unknown; // Allow other properties [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 createSQLiteProxy = () => {
const MAX_RETRIES = 3; const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1 second const RETRY_DELAY = 1000;
const withRetry = async <T>(operation: (...args: unknown[]) => Promise<T>, ...args: unknown[]): Promise<T> => { const withRetry = async (operation: (...args: unknown[]) => Promise<unknown>, ...args: unknown[]) => {
let lastError: Error | null = null; let lastError;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try { try {
return await operation(...args); return await operation(...args);
} catch (error) { } catch (error) {
lastError = error instanceof Error ? error : new Error(String(error)); lastError = error;
if (attempt < MAX_RETRIES) { 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)); 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) => { const wrapOperation = (method: string) => {
return async (...args: unknown[]): Promise<unknown> => { return async (...args: unknown[]) => {
try { try {
// For createConnection, ensure readOnly is false const handlerName = `sqlite-${method}`;
if (method === 'create-connection') { return await withRetry(ipcRenderer.invoke, handlerName, ...args);
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);
} catch (error) { } catch (error) {
logger.error(`[CapacitorSQLite] SQLite ${method} failed:`, error); logger.error(`[Preload] SQLite ${method} failed:`, error);
throw new Error(`[CapacitorSQLite] Database operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error;
} }
}; };
}; };
// Create a proxy that matches the CapacitorSQLite interface
return { return {
echo: wrapOperation('echo'), echo: wrapOperation('echo'),
createConnection: wrapOperation('create-connection'), createConnection: wrapOperation('create-connection'),
@ -80,24 +111,25 @@ const createSQLiteProxy = () => {
query: wrapOperation('query'), query: wrapOperation('query'),
run: wrapOperation('run'), run: wrapOperation('run'),
isAvailable: wrapOperation('is-available'), isAvailable: wrapOperation('is-available'),
getPlatform: () => Promise.resolve('electron'), getPlatform: () => Promise.resolve('electron')
// Add other methods as needed
}; };
}; };
// Expose the Electron IPC API try {
// Expose the secure IPC bridge and SQLite proxy
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
ipcRenderer: { ipcRenderer: createSecureIPCBridge(),
on: (channel: string, func: (...args: unknown[]) => void) => ipcRenderer.on(channel, (event, ...args) => func(...args)), sqlite: createSQLiteProxy(),
once: (channel: string, func: (...args: unknown[]) => void) => ipcRenderer.once(channel, (event, ...args) => func(...args)), env: {
send: (channel: string, data: unknown) => ipcRenderer.send(channel, data), platform: 'electron',
invoke: (channel: string, ...args: unknown[]) => ipcRenderer.invoke(channel, ...args), isDev: process.env.NODE_ENV === 'development'
}, }
// Add other APIs as needed
}); });
// Expose CapacitorSQLite proxy as before logger.info('[Preload] IPC bridge and SQLite proxy initialized successfully');
contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy()); } catch (error) {
logger.error('[Preload] Failed to initialize IPC bridge:', error);
}
// Log startup // Log startup
logger.log('[CapacitorSQLite] Preload script starting...'); logger.log('[CapacitorSQLite] Preload script starting...');

65
electron/src/rt/sqlite-init.ts

@ -531,6 +531,7 @@ export function setupSQLiteHandlers(): void {
'sqlite-create-connection', 'sqlite-create-connection',
'sqlite-execute', 'sqlite-execute',
'sqlite-query', 'sqlite-query',
'sqlite-run',
'sqlite-close-connection', 'sqlite-close-connection',
'sqlite-get-error' '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'); logger.info('SQLite IPC handlers registered successfully');
} }

18
electron/src/setup.ts

@ -105,8 +105,15 @@ export class ElectronCapacitorApp {
defaultWidth: 1000, defaultWidth: 1000,
defaultHeight: 800, 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({ this.MainWindow = new BrowserWindow({
icon, icon,
show: false, show: false,
@ -115,11 +122,12 @@ export class ElectronCapacitorApp {
width: this.mainWindowState.width, width: this.mainWindowState.width,
height: this.mainWindowState.height, height: this.mainWindowState.height,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
// Use preload to inject the electron varriant overrides for capacitor plugins. sandbox: false,
// preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"),
preload: preloadPath, preload: preloadPath,
webSecurity: true,
allowRunningInsecureContent: false,
}, },
}); });
this.mainWindowState.manage(this.MainWindow); this.mainWindowState.manage(this.MainWindow);

57
src/main.electron.ts

@ -17,17 +17,18 @@ const app = initializeApp();
// Create a promise that resolves when SQLite is ready // Create a promise that resolves when SQLite is ready
const sqliteReady = new Promise<void>((resolve, reject) => { const sqliteReady = new Promise<void>((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 // Set a timeout to prevent hanging
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
logger.error("[Main Electron] SQLite initialization timeout");
reject(new Error("SQLite initialization timeout")); reject(new Error("SQLite initialization timeout"));
}, 30000); // 30 second timeout }, 30000); // 30 second timeout
// 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', () => { window.electron.ipcRenderer.once('sqlite-ready', () => {
clearTimeout(timeout); clearTimeout(timeout);
logger.info("[Main Electron] Received SQLite ready signal"); logger.info("[Main Electron] Received SQLite ready signal");
@ -39,9 +40,33 @@ const sqliteReady = new Promise<void>((resolve, reject) => {
clearTimeout(timeout); clearTimeout(timeout);
const status = args[0] as { status: string; error?: string }; const status = args[0] as { status: string; error?: string };
if (status.status === 'error') { if (status.status === 'error') {
logger.error("[Main Electron] Database error:", status.error);
reject(new Error(status.error || 'Database initialization failed')); 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 // Wait for SQLite to be ready before mounting
@ -49,17 +74,31 @@ sqliteReady
.then(() => { .then(() => {
logger.info("[Main Electron] SQLite ready, mounting app..."); logger.info("[Main Electron] SQLite ready, mounting app...");
app.mount("#app"); app.mount("#app");
logger.info("[Main Electron] App mounted successfully");
}) })
.catch((error) => { .catch((error) => {
logger.error("[Main Electron] Failed to initialize SQLite:", error instanceof Error ? error.message : 'Unknown 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"); const errorDiv = document.createElement("div");
errorDiv.style.cssText = 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 = ` errorDiv.innerHTML = `
<h2>Failed to Initialize Application</h2> <h2>Failed to Initialize Application</h2>
<p>There was an error initializing the database. Please try restarting.</p> <p>There was an error initializing the database. This could be due to:</p>
<ul style="text-align: left; margin: 10px 0;">
<li>Database file is locked by another process</li>
<li>Insufficient permissions to access the database</li>
<li>Database file is corrupted</li>
</ul>
<p>Error details: ${error instanceof Error ? error.message : 'Unknown error'}</p> <p>Error details: ${error instanceof Error ? error.message : 'Unknown error'}</p>
<div style="margin-top: 15px;">
<button onclick="window.location.reload()" style="margin: 0 5px; padding: 8px 16px; background: #c62828; color: white; border: none; border-radius: 4px; cursor: pointer;">
Retry
</button>
<button onclick="window.electron.ipcRenderer.send('sqlite-status', { action: 'reset' })" style="margin: 0 5px; padding: 8px 16px; background: #f57c00; color: white; border: none; border-radius: 4px; cursor: pointer;">
Reset Database
</button>
</div>
`; `;
document.body.appendChild(errorDiv); document.body.appendChild(errorDiv);
}); });

17
src/router/index.ts

@ -277,18 +277,31 @@ const initialPath = isElectron
? window.location.pathname.split("/dist-electron/www/")[1] || "/" ? window.location.pathname.split("/dist-electron/www/")[1] || "/"
: window.location.pathname; : window.location.pathname;
logger.info("[Router] Initializing router", { isElectron, initialPath });
const history = isElectron const history = isElectron
? createMemoryHistory() // Memory history for Electron ? createMemoryHistory() // Memory history for Electron
: createWebHistory("/"); // Add base path for web apps : createWebHistory("/"); // Add base path for web apps
/** @type {*} */
const router = createRouter({ const router = createRouter({
history, history,
routes, 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 // 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 = ( const errorHandler = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

Loading…
Cancel
Save