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.
This commit is contained in:
@@ -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<void>((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();
|
||||
|
||||
@@ -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 <T>(operation: (...args: unknown[]) => Promise<T>, ...args: unknown[]): Promise<T> => {
|
||||
let lastError: Error | null = null;
|
||||
const withRetry = async (operation: (...args: unknown[]) => Promise<unknown>, ...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<unknown> => {
|
||||
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...');
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user