diff --git a/src/electron/main.ts b/src/electron/main.ts index c125d0ea..dd2c5823 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -72,204 +72,229 @@ try { } // Database path resolution utilities -const getAppDataPath = (): string => { - const userDataPath = app.getPath('userData'); - logger.info("[Electron] User data path:", userDataPath); - return userDataPath; -}; - -const validateAndNormalizePath = (filePath: string): string => { +const getAppDataPath = async (): Promise => { try { - // Resolve any relative paths and normalize - const normalizedPath = path.resolve(filePath); - logger.info("[Electron] Normalized database path:", normalizedPath); - - // Verify the path is absolute - if (!path.isAbsolute(normalizedPath)) { - throw new Error(`Database path must be absolute: ${normalizedPath}`); - } + // Read config file directly + const configPath = path.join(__dirname, '..', 'capacitor.config.json'); + const configContent = await fs.promises.readFile(configPath, 'utf-8'); + const config = JSON.parse(configContent); + const linuxPath = config?.plugins?.CapacitorSQLite?.electronLinuxLocation; - // Verify the path is within the user data directory - const userDataPath = getAppDataPath(); - if (!normalizedPath.startsWith(userDataPath)) { - throw new Error(`Database path must be within user data directory: ${normalizedPath}`); + if (linuxPath) { + // Expand ~ to home directory + const expandedPath = linuxPath.replace(/^~/, process.env.HOME || ''); + logger.info("[Electron] Using configured database path:", expandedPath); + return expandedPath; } - return normalizedPath; + // Fallback to app.getPath if config path is not available + const userDataPath = app.getPath('userData'); + logger.info("[Electron] Using fallback user data path:", userDataPath); + return userDataPath; } catch (error) { - logger.error("[Electron] Path validation failed:", error); - throw error; + logger.error("[Electron] Error getting app data path:", error); + // Fallback to app.getPath if anything fails + const userDataPath = app.getPath('userData'); + logger.info("[Electron] Using fallback user data path after error:", userDataPath); + return userDataPath; } }; -const ensureDirectoryExists = (dirPath: string): void => { +const validateAndNormalizePath = async (filePath: string): Promise => { + // Resolve any relative paths + const resolvedPath = path.resolve(filePath); + + // Ensure it's an absolute path + if (!path.isAbsolute(resolvedPath)) { + throw new Error(`Database path must be absolute: ${resolvedPath}`); + } + + // Ensure it's within the app data directory + const appDataPath = await getAppDataPath(); + if (!resolvedPath.startsWith(appDataPath)) { + throw new Error(`Database path must be within app data directory: ${resolvedPath}`); + } + + // Normalize the path + const normalizedPath = path.normalize(resolvedPath); + + logger.info("[Electron] Validated database path:", { + original: filePath, + resolved: resolvedPath, + normalized: normalizedPath, + appDataPath, + isAbsolute: path.isAbsolute(normalizedPath), + isWithinAppData: normalizedPath.startsWith(appDataPath) + }); + + return normalizedPath; +}; + +const ensureDirectoryExists = async (dirPath: string): Promise => { try { - const normalizedDir = validateAndNormalizePath(dirPath); - if (!fs.existsSync(normalizedDir)) { - fs.mkdirSync(normalizedDir, { recursive: true, mode: 0o755 }); - logger.info("[Electron] Created directory:", normalizedDir); + // Normalize the path first + const normalizedPath = path.normalize(dirPath); + + // Check if directory exists + if (!fs.existsSync(normalizedPath)) { + logger.info("[Electron] Creating database directory:", normalizedPath); + await fs.promises.mkdir(normalizedPath, { recursive: true }); } + // Verify directory permissions - const stats = fs.statSync(normalizedDir); - if (!stats.isDirectory()) { - throw new Error(`Path exists but is not a directory: ${normalizedDir}`); + try { + await fs.promises.access(normalizedPath, fs.constants.R_OK | fs.constants.W_OK); + logger.info("[Electron] Database directory permissions verified:", normalizedPath); + } catch (error) { + logger.error("[Electron] Database directory permission error:", error); + throw new Error(`Database directory not accessible: ${normalizedPath}`); } - // Check if directory is writable + + // Test write permissions + const testFile = path.join(normalizedPath, '.write-test'); try { - const testFile = path.join(normalizedDir, '.write-test'); - fs.writeFileSync(testFile, 'test'); - fs.unlinkSync(testFile); - } catch (err) { - throw new Error(`Directory not writable: ${normalizedDir}`); + await fs.promises.writeFile(testFile, 'test'); + await fs.promises.unlink(testFile); + logger.info("[Electron] Database directory write test passed:", normalizedPath); + } catch (error) { + logger.error("[Electron] Database directory write test failed:", error); + throw new Error(`Database directory not writable: ${normalizedPath}`); } } catch (error) { - logger.error("[Electron] Directory setup failed:", error); + logger.error("[Electron] Failed to ensure database directory exists:", error); throw error; } }; -// Database path logic -let dbPath: string; -let dbDir: string; - // Initialize database paths -const initializeDatabasePaths = (): void => { - try { - const basePath = getAppDataPath(); - dbDir = path.join(basePath, 'timesafari'); - dbPath = path.join(dbDir, 'timesafari.db'); - - // Validate and normalize paths - dbDir = validateAndNormalizePath(dbDir); - dbPath = validateAndNormalizePath(dbPath); - - // Ensure directory exists and is writable - ensureDirectoryExists(dbDir); - - logger.info("[Electron] Database directory:", dbDir); - logger.info("[Electron] Database file path:", dbPath); - - // Verify database file permissions if it exists - if (fs.existsSync(dbPath)) { - try { - // Ensure the database file is writable - fs.accessSync(dbPath, fs.constants.R_OK | fs.constants.W_OK); - // Try to open the file in write mode to verify permissions - const fd = fs.openSync(dbPath, 'r+'); - fs.closeSync(fd); - logger.info("[Electron] Database file exists and is writable"); - } catch (err) { - logger.error("[Electron] Database file exists but is not writable:", err); - // Try to fix permissions +let dbPath: string | undefined; +let dbDir: string | undefined; +let dbPathInitialized = false; +let dbPathInitializationPromise: Promise | null = null; + +const initializeDatabasePaths = async (): Promise => { + // Prevent multiple simultaneous initializations + if (dbPathInitializationPromise) { + return dbPathInitializationPromise; + } + + if (dbPathInitialized) { + return; + } + + dbPathInitializationPromise = (async () => { + try { + // Get the base directory from config + dbDir = await getAppDataPath(); + logger.info("[Electron] Database directory:", dbDir); + + // Ensure the directory exists and is writable + await ensureDirectoryExists(dbDir); + + // Construct the database path + dbPath = await validateAndNormalizePath(path.join(dbDir, 'timesafari.db')); + logger.info("[Electron] Database path initialized:", dbPath); + + // Verify the database file if it exists + if (fs.existsSync(dbPath)) { try { - fs.chmodSync(dbPath, 0o644); - logger.info("[Electron] Fixed database file permissions"); - } catch (chmodErr) { - logger.error("[Electron] Failed to fix database permissions:", chmodErr); - throw new Error(`Database file not writable: ${dbPath}`); + await fs.promises.access(dbPath, fs.constants.R_OK | fs.constants.W_OK); + logger.info("[Electron] Existing database file permissions verified:", dbPath); + } catch (error) { + logger.error("[Electron] Database file permission error:", error); + throw new Error(`Database file not accessible: ${dbPath}`); } } + + dbPathInitialized = true; + } catch (error) { + logger.error("[Electron] Failed to initialize database paths:", error); + throw error; + } finally { + dbPathInitializationPromise = null; } - } catch (error) { - logger.error("[Electron] Failed to initialize database paths:", error); - throw error; - } + })(); + + return dbPathInitializationPromise; }; -// Initialize paths when app is ready -app.whenReady().then(() => { - initializeDatabasePaths(); -}); - // Initialize SQLite plugin let sqlitePlugin: any = null; +let sqliteInitialized = false; +let sqliteInitializationPromise: Promise | null = null; async function initializeSQLite() { - try { - logger.info("[Electron] Initializing SQLite plugin..."); - sqlitePlugin = new CapacitorSQLite(); - - // Test the plugin - const echoResult = await sqlitePlugin.echo({ value: "test" }); - logger.info("[Electron] SQLite plugin echo test:", echoResult); - - // Initialize database connection using absolute dbPath - const connectionOptions = { - database: dbPath, // This is now guaranteed to be a valid absolute path - version: 1, - readOnly: false, - encryption: "no-encryption", - useNative: true, - mode: "rwc" // Force read-write-create mode - }; - - logger.info("[Electron] Creating initial connection with options:", connectionOptions); - - // Log the actual path being used - logger.info("[Electron] Using database path:", dbPath); - logger.info("[Electron] Path exists:", fs.existsSync(dbPath)); - logger.info("[Electron] Path is absolute:", path.isAbsolute(dbPath)); - - const db = await sqlitePlugin.createConnection(connectionOptions); - - if (!db || typeof db !== 'object') { - throw new Error(`Failed to create database connection - invalid response. Path used: ${dbPath}`); - } - - // Wait a moment for the connection to be fully established - await new Promise(resolve => setTimeout(resolve, 100)); - + // Prevent multiple simultaneous initializations + if (sqliteInitializationPromise) { + return sqliteInitializationPromise; + } + + if (sqliteInitialized) { + return; + } + + sqliteInitializationPromise = (async () => { try { - // Verify connection is not read-only - const result = await db.query({ statement: "PRAGMA journal_mode;" }); - logger.info("[Electron] Database journal mode:", result); + logger.info("[Electron] Initializing SQLite plugin..."); + sqlitePlugin = new CapacitorSQLite(); - if (result?.values?.[0]?.journal_mode === "off") { - logger.warn("[Electron] Database opened in read-only mode, attempting to fix..."); - // Try to close and reopen with explicit permissions - await db.closeConnection(); - const newDb = await sqlitePlugin.createConnection({ - ...connectionOptions, - mode: "rwc", - readOnly: false - }); - - if (!newDb || typeof newDb !== 'object') { - throw new Error(`Failed to create new database connection - invalid response. Path used: ${dbPath}`); - } - - logger.info("[Electron] Reopened database connection"); - return newDb; - } - } catch (queryError) { - logger.error("[Electron] Error verifying database connection:", queryError); - // If we can't query, try to close and reopen - try { - await db.closeConnection(); - } catch (closeError) { - logger.warn("[Electron] Error closing failed connection:", closeError); + // Initialize database paths first + await initializeDatabasePaths(); + + if (!dbPath) { + throw new Error("Database path not initialized"); } - // Try one more time with basic options - const retryDb = await sqlitePlugin.createConnection({ + // Test the plugin + const echoResult = await sqlitePlugin.echo({ value: "test" }); + logger.info("[Electron] SQLite plugin echo test:", echoResult); + + // Initialize database connection using validated dbPath + const connectionOptions = { database: dbPath, version: 1, - readOnly: false - }); + readOnly: false, + encryption: "no-encryption", + useNative: true, + mode: "rwc" // Force read-write-create mode + }; - if (!retryDb || typeof retryDb !== 'object') { - throw new Error(`Failed to create database connection after retry. Path used: ${dbPath}`); + logger.info("[Electron] Creating initial connection with options:", connectionOptions); + + // Log the actual path being used + logger.info("[Electron] Using database path:", dbPath); + logger.info("[Electron] Path exists:", fs.existsSync(dbPath)); + logger.info("[Electron] Path is absolute:", path.isAbsolute(dbPath)); + + const db = await sqlitePlugin.createConnection(connectionOptions); + + if (!db || typeof db !== 'object') { + throw new Error(`Failed to create database connection - invalid response. Path used: ${dbPath}`); + } + + // Wait a moment for the connection to be fully established + await new Promise(resolve => setTimeout(resolve, 100)); + + // Verify the connection is working + try { + const result = await db.query("PRAGMA journal_mode;"); + logger.info("[Electron] Database connection verified:", result); + } catch (error) { + logger.error("[Electron] Database connection verification failed:", error); + throw error; } - return retryDb; + sqliteInitialized = true; + logger.info("[Electron] SQLite plugin initialized successfully"); + } catch (error) { + logger.error("[Electron] Failed to initialize SQLite plugin:", error); + throw error; + } finally { + sqliteInitializationPromise = null; } - - logger.info("[Electron] SQLite plugin initialized successfully"); - return db; - } catch (error) { - logger.error("[Electron] Failed to initialize SQLite plugin:", error); - throw error; - } + })(); + + return sqliteInitializationPromise; } // Initialize app when ready @@ -311,7 +336,7 @@ app.whenReady().then(async () => { }); // Check if running in dev mode -const isDev = process.argv.includes("--inspect"); +// const isDev = process.argv.includes("--inspect"); function createWindow(): BrowserWindow { // Resolve preload path based on environment @@ -364,11 +389,11 @@ function createWindow(): BrowserWindow { }); // Handle window errors - mainWindow.webContents.on('render-process-gone', (event, details) => { + mainWindow.webContents.on('render-process-gone', (_event, details) => { logger.error("[Electron] Render process gone:", details); }); - mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => { + mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription) => { logger.error("[Electron] Page failed to load:", errorCode, errorDescription); logger.error("[Electron] Failed URL:", mainWindow.webContents.getURL()); }); @@ -391,15 +416,15 @@ function createWindow(): BrowserWindow { logger.info("[Electron] Using file URL:", fileUrl); // Load the index.html with retry logic - const loadIndexHtml = async (retryCount = 0) => { - try { + const loadIndexHtml = async (retryCount = 0): Promise => { + try { if (mainWindow.isDestroyed()) { logger.error("[Electron] Window was destroyed before loading index.html"); return; } - const exists = fs.existsSync(indexPath); - logger.info(`[Electron] fs.existsSync for index.html: ${exists}`); + const exists = fs.existsSync(indexPath); + logger.info(`[Electron] fs.existsSync for index.html: ${exists}`); if (!exists) { throw new Error(`index.html not found at path: ${indexPath}`); @@ -453,7 +478,7 @@ function createWindow(): BrowserWindow { }; // Start loading the index.html - loadIndexHtml().catch(error => { + loadIndexHtml().catch((error: unknown) => { logger.error("[Electron] Fatal error loading index.html:", error); }); @@ -502,6 +527,13 @@ ipcMain.handle("sqlite-echo", async (_event, value) => { ipcMain.handle("sqlite-create-connection", async (_event, options) => { try { + // Ensure database is initialized + await initializeSQLite(); + + if (!dbPath) { + throw new Error("Database path not initialized"); + } + // Override any provided database path with our resolved path const connectionOptions = { ...options, @@ -531,19 +563,7 @@ ipcMain.handle("sqlite-create-connection", async (_event, options) => { } } catch (queryError) { logger.error("[Electron] Error verifying connection:", queryError); - // If verification fails, try a simpler connection - await result.closeConnection().catch(() => {}); - const retryResult = await sqlitePlugin.createConnection({ - database: dbPath, - version: 1, - readOnly: false - }); - - if (!retryResult || typeof retryResult !== 'object') { - throw new Error("Failed to create database connection after retry"); - } - - return retryResult; + throw queryError; } logger.info("[Electron] Database connection created successfully");