import { app, BrowserWindow, ipcMain } from "electron"; import { Capacitor } from "@capacitor/core"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; // Get __dirname equivalent in ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Set global variables that the plugin needs global.__dirname = __dirname; global.__filename = __filename; // Now import the plugin after setting up globals import { CapacitorSQLite } from "@capacitor-community/sqlite/electron/dist/plugin.js"; // Simple logger implementation const logger = { // eslint-disable-next-line no-console log: (...args: unknown[]) => console.log("[Main]", ...args), // eslint-disable-next-line no-console error: (...args: unknown[]) => console.error("[Main]", ...args), // eslint-disable-next-line no-console info: (...args: unknown[]) => console.info("[Main]", ...args), // eslint-disable-next-line no-console warn: (...args: unknown[]) => console.warn("[Main]", ...args), // eslint-disable-next-line no-console debug: (...args: unknown[]) => console.debug("[Main]", ...args), }; logger.info("Starting main process initialization..."); // Initialize Capacitor for Electron in main process try { logger.info("About to initialize Capacitor..."); logger.info("Capacitor before init:", { hasPlatform: "platform" in Capacitor, hasIsNativePlatform: "isNativePlatform" in Capacitor, platformType: typeof Capacitor.platform, isNativePlatformType: typeof Capacitor.isNativePlatform, }); // Try direct assignment first try { (Capacitor as unknown as { platform: string }).platform = "electron"; (Capacitor as unknown as { isNativePlatform: boolean }).isNativePlatform = true; logger.info("Direct assignment successful"); } catch (e) { logger.warn("Direct assignment failed, trying defineProperty:", e); Object.defineProperty(Capacitor, "isNativePlatform", { get: () => true, configurable: true, }); Object.defineProperty(Capacitor, "platform", { get: () => "electron", configurable: true, }); } logger.info("Capacitor after init:", { platform: Capacitor.platform, isNativePlatform: Capacitor.isNativePlatform, platformType: typeof Capacitor.platform, isNativePlatformType: typeof Capacitor.isNativePlatform, }); } catch (error) { logger.error("Failed to initialize Capacitor:", error); throw error; } // Database path resolution utilities const getAppDataPath = async (): Promise => { try { // 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; if (linuxPath) { // Expand ~ to home directory const expandedPath = linuxPath.replace(/^~/, process.env.HOME || ""); logger.info("[Electron] Using configured database path:", expandedPath); return expandedPath; } // 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] 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 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 { // 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 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}`); } // Test write permissions const testFile = path.join(normalizedPath, ".write-test"); try { 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] Failed to ensure database directory exists:", error, ); throw error; } }; // Initialize database paths 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 { 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; } })(); return dbPathInitializationPromise; }; // Initialize SQLite plugin let sqlitePlugin: any = null; let sqliteInitialized = false; let sqliteInitializationPromise: Promise | null = null; async function initializeSQLite() { // Prevent multiple simultaneous initializations if (sqliteInitializationPromise) { return sqliteInitializationPromise; } if (sqliteInitialized) { return; } sqliteInitializationPromise = (async () => { try { logger.info("[Electron] Initializing SQLite plugin..."); sqlitePlugin = new CapacitorSQLite(); // Initialize database paths first await initializeDatabasePaths(); if (!dbPath) { throw new Error("Database path not initialized"); } // 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, 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)); // 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; } 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; } })(); return sqliteInitializationPromise; } // Initialize app when ready app.whenReady().then(async () => { logger.info("App is ready, starting initialization..."); // Create window first const mainWindow = createWindow(); // Initialize database in background initializeSQLite().catch((error) => { logger.error( "[Electron] Database initialization failed, but continuing:", error, ); // Notify renderer about database status if (!mainWindow.isDestroyed()) { mainWindow.webContents.send("database-status", { status: "error", error: error.message, }); } }); // Handle window close mainWindow.on("closed", () => { logger.info("[Electron] Main window closed"); }); // Handle window close request mainWindow.on("close", (event) => { logger.info("[Electron] Window close requested"); // Prevent immediate close if we're in the middle of something if (mainWindow.webContents.isLoading()) { event.preventDefault(); logger.info("[Electron] Deferring window close due to loading state"); mainWindow.webContents.once("did-finish-load", () => { mainWindow.close(); }); } }); }); // Check if running in dev mode // const isDev = process.argv.includes("--inspect"); function createWindow(): BrowserWindow { // Resolve preload path based on environment const preloadPath = app.isPackaged ? path.join(process.resourcesPath, "preload.js") : path.join(__dirname, "preload.js"); logger.log("[Electron] Preload path:", preloadPath); logger.log("[Electron] Preload exists:", fs.existsSync(preloadPath)); // Log environment and paths logger.log("[Electron] process.cwd():", process.cwd()); logger.log("[Electron] __dirname:", __dirname); logger.log("[Electron] app.getAppPath():", app.getAppPath()); logger.log("[Electron] app.isPackaged:", app.isPackaged); logger.log("[Electron] process.resourcesPath:", process.resourcesPath); // List files in __dirname and __dirname/www try { logger.log("Files in __dirname:", fs.readdirSync(__dirname)); const wwwDir = path.join(__dirname, "www"); if (fs.existsSync(wwwDir)) { logger.log("Files in www:", fs.readdirSync(wwwDir)); } else { logger.log("www directory does not exist in __dirname"); } } catch (e) { logger.error("Error reading directories:", e); } // Create the browser window with proper error handling const mainWindow = new BrowserWindow({ width: 1200, height: 800, show: false, // Don't show until ready webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: false, preload: preloadPath, webSecurity: true, allowRunningInsecureContent: false, }, }); // Show window when ready mainWindow.once("ready-to-show", () => { logger.info("[Electron] Window ready to show"); mainWindow.show(); }); // Handle window errors mainWindow.webContents.on("render-process-gone", (_event, details) => { logger.error("[Electron] Render process gone:", details); }); 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()); }, ); // Load the index.html let indexPath: string; let fileUrl: string; if (app.isPackaged) { indexPath = path.join(process.resourcesPath, "www", "index.html"); fileUrl = `file://${indexPath}`; logger.info( "[Electron] App is packaged. Using process.resourcesPath for index.html", ); } else { indexPath = path.resolve(__dirname, "www", "index.html"); fileUrl = `file://${indexPath}`; logger.info( "[Electron] App is not packaged. Using __dirname for index.html", ); } logger.info("[Electron] Resolved index.html path:", indexPath); logger.info("[Electron] Using file URL:", fileUrl); // Load the index.html with retry logic 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}`); if (!exists) { throw new Error(`index.html not found at path: ${indexPath}`); } // Try to read the file to verify it's accessible const stats = fs.statSync(indexPath); logger.info("[Electron] index.html stats:", { size: stats.size, mode: stats.mode, uid: stats.uid, gid: stats.gid, }); // Try loadURL first try { logger.info("[Electron] Attempting to load index.html via loadURL"); await mainWindow.loadURL(fileUrl); logger.info("[Electron] Successfully loaded index.html via loadURL"); } catch (loadUrlError) { logger.warn( "[Electron] loadURL failed, trying loadFile:", loadUrlError, ); // Fallback to loadFile await mainWindow.loadFile(indexPath); logger.info("[Electron] Successfully loaded index.html via loadFile"); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; logger.error("[Electron] Error loading index.html:", errorMessage); // Retry logic if (retryCount < 3 && !mainWindow.isDestroyed()) { logger.info( `[Electron] Retrying index.html load (attempt ${retryCount + 1})`, ); await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second return loadIndexHtml(retryCount + 1); } // If we've exhausted retries, show error in window if (!mainWindow.isDestroyed()) { const errorHtml = `

Error Loading Application

Failed to load the application after multiple attempts.

${errorMessage}
`; await mainWindow.loadURL( `data:text/html,${encodeURIComponent(errorHtml)}`, ); } } }; // Start loading the index.html loadIndexHtml().catch((error: unknown) => { logger.error("[Electron] Fatal error loading index.html:", error); }); // Only open DevTools if not in production if (!app.isPackaged) { mainWindow.webContents.openDevTools({ mode: "detach" }); } return mainWindow; } // Handle app ready app.whenReady().then(createWindow); // Handle all windows closed app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } }); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); // Handle any errors process.on("uncaughtException", (error) => { logger.error("Uncaught Exception:", error); }); // Set up IPC handlers for SQLite operations ipcMain.handle("check-sqlite-availability", () => { return sqlitePlugin !== null; }); ipcMain.handle("sqlite-echo", async (_event, value) => { try { return await sqlitePlugin.echo({ value }); } catch (error) { logger.error( "Error in sqlite-echo:", error, JSON.stringify(error), (error as any)?.stack, ); throw error; } }); 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, database: dbPath, readOnly: false, mode: "rwc", // Force read-write-create mode encryption: "no-encryption", useNative: true, }; logger.info( "[Electron] Creating database connection with options:", connectionOptions, ); const result = await sqlitePlugin.createConnection(connectionOptions); if (!result || typeof result !== "object") { throw new Error( "Failed to create database connection - invalid response", ); } // Wait a moment for the connection to be fully established await new Promise((resolve) => setTimeout(resolve, 100)); try { // Verify connection is not read-only const testResult = await result.query({ statement: "PRAGMA journal_mode;", }); if (testResult?.values?.[0]?.journal_mode === "off") { logger.error( "[Electron] Connection opened in read-only mode despite options", ); throw new Error("Database connection opened in read-only mode"); } } catch (queryError) { logger.error("[Electron] Error verifying connection:", queryError); throw queryError; } logger.info("[Electron] Database connection created successfully"); return result; } catch (error) { logger.error("[Electron] Error in sqlite-create-connection:", error); throw error; } }); ipcMain.handle("sqlite-execute", async (_event, options) => { try { return await sqlitePlugin.execute(options); } catch (error) { logger.error( "Error in sqlite-execute:", error, JSON.stringify(error), (error as any)?.stack, ); throw error; } }); ipcMain.handle("sqlite-query", async (_event, options) => { try { return await sqlitePlugin.query(options); } catch (error) { logger.error( "Error in sqlite-query:", error, JSON.stringify(error), (error as any)?.stack, ); throw error; } }); ipcMain.handle("sqlite-close-connection", async (_event, options) => { try { return await sqlitePlugin.closeConnection(options); } catch (error) { logger.error( "Error in sqlite-close-connection:", error, JSON.stringify(error), (error as any)?.stack, ); throw error; } }); ipcMain.handle("sqlite-is-available", async () => { return sqlitePlugin !== null; });