fix(db): resolve SQLite channel and initialization issues
- Add sqlite-status to valid IPC channels - Fix SQLite ready signal handling - Improve database status tracking - Add proper error handling for status updates - Keep database connection open during initialization Technical Details: - Added sqlite-status to VALID_CHANNELS.invoke list - Implemented sqlite-status handler with proper verification - Added database open state verification - Improved error handling and logging - Fixed premature database closing Testing Notes: - Verify SQLite ready signal is received correctly - Confirm database stays open after initialization - Check status updates are processed properly - Verify error handling for invalid states Security: - Validates all IPC channels - Verifies database state before operations - Maintains proper connection lifecycle - Implements proper error boundaries Author: Matthew Raymer
This commit is contained in:
@@ -38,7 +38,7 @@ interface SQLiteConnectionOptions {
|
||||
|
||||
// Define valid channels for security
|
||||
const VALID_CHANNELS = {
|
||||
send: ['toMain', 'sqlite-status'] as const,
|
||||
send: ['toMain'] as const,
|
||||
receive: ['fromMain', 'sqlite-ready', 'database-status'] as const,
|
||||
invoke: [
|
||||
'sqlite-is-available',
|
||||
@@ -51,6 +51,7 @@ const VALID_CHANNELS = {
|
||||
'sqlite-open',
|
||||
'sqlite-close',
|
||||
'sqlite-is-db-open',
|
||||
'sqlite-status',
|
||||
'get-path',
|
||||
'get-base-path'
|
||||
] as const
|
||||
|
||||
@@ -868,5 +868,35 @@ export function setupSQLiteHandlers(): void {
|
||||
}
|
||||
});
|
||||
|
||||
// Handler for SQLite status updates
|
||||
registerHandler('sqlite-status', async (_event, status: { status: string; database: string; timestamp: number }) => {
|
||||
logger.debug('SQLite status update:', status);
|
||||
try {
|
||||
startDatabaseOperation();
|
||||
if (!pluginState.instance) {
|
||||
throw new SQLiteError('Plugin not initialized', 'sqlite-status');
|
||||
}
|
||||
|
||||
// Verify database is still open
|
||||
const isOpen = await pluginState.instance.isDBOpen({ database: status.database });
|
||||
if (!isOpen) {
|
||||
throw new SQLiteError('Database not open', 'sqlite-status');
|
||||
}
|
||||
|
||||
logger.info('SQLite status update processed:', {
|
||||
status: status.status,
|
||||
database: status.database,
|
||||
timestamp: new Date(status.timestamp).toISOString()
|
||||
});
|
||||
|
||||
return { success: true, isOpen };
|
||||
} catch (error) {
|
||||
logger.error('SQLite status update failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
endDatabaseOperation();
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('SQLite IPC handlers setup complete');
|
||||
}
|
||||
@@ -54,15 +54,15 @@ const sqliteReady = new Promise<void>((resolve, reject) => {
|
||||
|
||||
// Set timeout for this attempt
|
||||
initializationTimeout = setTimeout(() => {
|
||||
if (retryCount < SQLITE_CONFIG.INITIALIZATION.RETRY_ATTEMPTS) {
|
||||
if (retryCount < 3) { // Use same retry count as ElectronPlatformService
|
||||
retryCount++;
|
||||
logger.warn(`[Main Electron] SQLite initialization attempt ${retryCount} timed out, retrying...`);
|
||||
setTimeout(attemptInitialization, SQLITE_CONFIG.INITIALIZATION.RETRY_DELAY_MS);
|
||||
setTimeout(attemptInitialization, 1000); // Use same delay as ElectronPlatformService
|
||||
} else {
|
||||
logger.error("[Main Electron] SQLite initialization failed after all retries");
|
||||
reject(new Error("SQLite initialization timeout after all retries"));
|
||||
}
|
||||
}, SQLITE_CONFIG.INITIALIZATION.TIMEOUT_MS);
|
||||
}, 10000); // Use same timeout as ElectronPlatformService
|
||||
|
||||
// Wait for electron bridge to be available
|
||||
const checkElectronBridge = () => {
|
||||
@@ -143,27 +143,24 @@ const sqliteReady = new Promise<void>((resolve, reject) => {
|
||||
logger.debug("[Main Electron] [IPC:sqlite-query] Executing test query");
|
||||
const testQuery = await ipcRenderer.invoke('sqlite-query', {
|
||||
database: 'timesafari',
|
||||
statement: 'SELECT * FROM secret;'
|
||||
statement: 'SELECT 1 as test;' // Safe test query
|
||||
}) as SQLiteQueryResult;
|
||||
logger.info("[Main Electron] [IPC:sqlite-query] Test query successful:", {
|
||||
hasResults: Boolean(testQuery?.values),
|
||||
resultCount: testQuery?.values?.length,
|
||||
results: testQuery?.values
|
||||
resultCount: testQuery?.values?.length
|
||||
});
|
||||
|
||||
// Close the database
|
||||
logger.debug("[Main Electron] [IPC:sqlite-close] Closing database");
|
||||
await ipcRenderer.invoke('sqlite-close', {
|
||||
database: 'timesafari'
|
||||
// Signal that SQLite is ready - database stays open
|
||||
logger.debug("[Main Electron] [IPC:sqlite-status] Sending SQLite ready status");
|
||||
await ipcRenderer.invoke('sqlite-status', {
|
||||
status: 'ready',
|
||||
database: 'timesafari',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
logger.info("[Main Electron] [IPC:sqlite-close] Database closed successfully");
|
||||
|
||||
// Close the connection
|
||||
logger.debug("[Main Electron] [IPC:sqlite-close-connection] Closing database connection");
|
||||
await ipcRenderer.invoke('sqlite-close-connection', {
|
||||
database: 'timesafari'
|
||||
});
|
||||
logger.info("[Main Electron] [IPC:sqlite-close-connection] Database connection closed successfully");
|
||||
logger.info("[Main Electron] SQLite ready status sent, database connection maintained");
|
||||
|
||||
// Remove the close operations - database stays open for component use
|
||||
// Database will be closed during app shutdown
|
||||
} catch (error) {
|
||||
logger.error("[Main Electron] [IPC:*] SQLite test operation failed:", {
|
||||
error,
|
||||
|
||||
@@ -44,6 +44,26 @@ export interface SQLiteQueryResult {
|
||||
changes?: { changes: number; lastId?: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared SQLite initialization state
|
||||
* Used to coordinate initialization between main and service
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
export interface SQLiteInitState {
|
||||
isReady: boolean;
|
||||
isInitializing: boolean;
|
||||
error?: Error;
|
||||
lastReadyCheck?: number;
|
||||
}
|
||||
|
||||
// Singleton instance for shared state
|
||||
const sqliteInitState: SQLiteInitState = {
|
||||
isReady: false,
|
||||
isInitializing: false,
|
||||
lastReadyCheck: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Platform service implementation for Electron (desktop) platform.
|
||||
* Provides native desktop functionality through Electron and Capacitor plugins for:
|
||||
@@ -51,6 +71,8 @@ export interface SQLiteQueryResult {
|
||||
* - Camera integration (TODO)
|
||||
* - SQLite database operations
|
||||
* - System-level features (TODO)
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
export class ElectronPlatformService implements PlatformService {
|
||||
private sqlite: any;
|
||||
@@ -58,53 +80,152 @@ export class ElectronPlatformService implements PlatformService {
|
||||
private isInitialized = false;
|
||||
private dbFatalError = false;
|
||||
private sqliteReadyPromise: Promise<void> | null = null;
|
||||
private initializationTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
// SQLite initialization configuration
|
||||
private static readonly SQLITE_CONFIG = {
|
||||
INITIALIZATION: {
|
||||
TIMEOUT_MS: 10000, // 10 seconds for initial setup
|
||||
RETRY_ATTEMPTS: 3,
|
||||
RETRY_DELAY_MS: 1000,
|
||||
READY_CHECK_INTERVAL_MS: 100 // How often to check if SQLite is already ready
|
||||
}
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.sqliteReadyPromise = new Promise<void>(async (resolve, reject) => {
|
||||
try {
|
||||
// Verify Electron API exposure first
|
||||
await verifyElectronAPI();
|
||||
logger.info('[ElectronPlatformService] Electron API verification successful');
|
||||
this.sqliteReadyPromise = new Promise<void>((resolve, reject) => {
|
||||
let retryCount = 0;
|
||||
|
||||
const cleanup = () => {
|
||||
if (this.initializationTimeout) {
|
||||
clearTimeout(this.initializationTimeout);
|
||||
this.initializationTimeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (!window.electron?.ipcRenderer) {
|
||||
logger.warn('[ElectronPlatformService] IPC renderer not available');
|
||||
reject(new Error('IPC renderer not available'));
|
||||
const checkExistingReadiness = async (): Promise<boolean> => {
|
||||
try {
|
||||
if (!window.electron?.ipcRenderer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if SQLite is already available
|
||||
const isAvailable = await window.electron.ipcRenderer.invoke('sqlite-is-available');
|
||||
if (!isAvailable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if database is already open
|
||||
const isOpen = await window.electron.ipcRenderer.invoke('sqlite-is-db-open', {
|
||||
database: this.dbName
|
||||
});
|
||||
|
||||
if (isOpen) {
|
||||
logger.info('[ElectronPlatformService] SQLite is already ready and database is open');
|
||||
sqliteInitState.isReady = true;
|
||||
sqliteInitState.isInitializing = false;
|
||||
sqliteInitState.lastReadyCheck = Date.now();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.warn('[ElectronPlatformService] Error checking existing readiness:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const attemptInitialization = async () => {
|
||||
cleanup(); // Clear any existing timeout
|
||||
|
||||
// Check if SQLite is already ready
|
||||
if (await checkExistingReadiness()) {
|
||||
this.isInitialized = true;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('SQLite initialization timeout'));
|
||||
}, 30000);
|
||||
|
||||
window.electron.ipcRenderer.once('sqlite-ready', async () => {
|
||||
clearTimeout(timeout);
|
||||
logger.info('[ElectronPlatformService] Received SQLite ready signal');
|
||||
// If someone else is initializing, wait for them
|
||||
if (sqliteInitState.isInitializing) {
|
||||
logger.info('[ElectronPlatformService] Another initialization in progress, waiting...');
|
||||
setTimeout(attemptInitialization, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.READY_CHECK_INTERVAL_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
sqliteInitState.isInitializing = true;
|
||||
|
||||
try {
|
||||
// Test SQLite operations after receiving ready signal
|
||||
await testSQLiteOperations();
|
||||
logger.info('[ElectronPlatformService] SQLite operations test successful');
|
||||
|
||||
this.isInitialized = true;
|
||||
resolve();
|
||||
} catch (error) {
|
||||
logger.error('[ElectronPlatformService] SQLite operations test failed:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
// Verify Electron API exposure first
|
||||
await verifyElectronAPI();
|
||||
logger.info('[ElectronPlatformService] Electron API verification successful');
|
||||
|
||||
window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => {
|
||||
clearTimeout(timeout);
|
||||
const status = args[0] as { status: string; error?: string };
|
||||
if (status.status === 'error') {
|
||||
this.dbFatalError = true;
|
||||
reject(new Error(status.error || 'Database initialization failed'));
|
||||
if (!window.electron?.ipcRenderer) {
|
||||
logger.warn('[ElectronPlatformService] IPC renderer not available');
|
||||
reject(new Error('IPC renderer not available'));
|
||||
return;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[ElectronPlatformService] Initialization failed:', error);
|
||||
reject(error);
|
||||
}
|
||||
|
||||
// Set timeout for this attempt
|
||||
this.initializationTimeout = setTimeout(() => {
|
||||
if (retryCount < ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.RETRY_ATTEMPTS) {
|
||||
retryCount++;
|
||||
logger.warn(`[ElectronPlatformService] SQLite initialization attempt ${retryCount} timed out, retrying...`);
|
||||
setTimeout(attemptInitialization, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.RETRY_DELAY_MS);
|
||||
} else {
|
||||
cleanup();
|
||||
sqliteInitState.isInitializing = false;
|
||||
sqliteInitState.error = new Error('SQLite initialization timeout after all retries');
|
||||
logger.error('[ElectronPlatformService] SQLite initialization failed after all retries');
|
||||
reject(sqliteInitState.error);
|
||||
}
|
||||
}, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.TIMEOUT_MS);
|
||||
|
||||
// Set up ready signal handler
|
||||
window.electron.ipcRenderer.once('sqlite-ready', async () => {
|
||||
cleanup();
|
||||
logger.info('[ElectronPlatformService] Received SQLite ready signal');
|
||||
|
||||
try {
|
||||
// Test SQLite operations after receiving ready signal
|
||||
await testSQLiteOperations();
|
||||
logger.info('[ElectronPlatformService] SQLite operations test successful');
|
||||
|
||||
this.isInitialized = true;
|
||||
sqliteInitState.isReady = true;
|
||||
sqliteInitState.isInitializing = false;
|
||||
sqliteInitState.lastReadyCheck = Date.now();
|
||||
resolve();
|
||||
} catch (error) {
|
||||
sqliteInitState.error = error as Error;
|
||||
sqliteInitState.isInitializing = false;
|
||||
logger.error('[ElectronPlatformService] SQLite operations test failed:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up error handler
|
||||
window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => {
|
||||
cleanup();
|
||||
const status = args[0] as { status: string; error?: string };
|
||||
if (status.status === 'error') {
|
||||
this.dbFatalError = true;
|
||||
sqliteInitState.error = new Error(status.error || 'Database initialization failed');
|
||||
sqliteInitState.isInitializing = false;
|
||||
reject(sqliteInitState.error);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
sqliteInitState.error = error as Error;
|
||||
sqliteInitState.isInitializing = false;
|
||||
logger.error('[ElectronPlatformService] Initialization failed:', error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Start first initialization attempt
|
||||
attemptInitialization();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user