Browse Source
The preload script (src/electron/preload.js) was removed as part of a refactor to separate web and electron builds. This script was previously responsible for: - Secure IPC communication between electron main and renderer processes - SQLite database access bridge for the renderer process - Context isolation and API exposure for electron-specific features Current state: - Web app builds successfully without preload script - Electron builds fail due to missing preload script - SQLite initialization works in main process but renderer can't access it - Database operations fail with "Cannot read properties of undefined" This commit is a breaking change for electron builds. The preload script will need to be recreated to restore electron database functionality. Affected files: - Deleted: src/electron/preload.js - Modified: src/main.electron.ts (removed DatabaseManager import) - Modified: src/utils/logger.ts (simplified logging implementation) - Modified: src/types/electron.d.ts (updated ElectronAPI interface) - Modified: src/types/global.d.ts (updated window.electron type definition) Next steps: - Recreate preload script with proper SQLite bridge - Update electron build configuration - Restore database access in renderer processsql-absurd-sql-further
5 changed files with 24 additions and 1089 deletions
@ -1,174 +0,0 @@ |
|||||
const { app, BrowserWindow } = require("electron"); |
|
||||
const path = require("path"); |
|
||||
const fs = require("fs"); |
|
||||
const logger = require("../utils/logger"); |
|
||||
|
|
||||
// Check if running in dev mode
|
|
||||
const isDev = process.argv.includes("--inspect"); |
|
||||
|
|
||||
function createWindow() { |
|
||||
// Add before createWindow function
|
|
||||
const preloadPath = path.join(__dirname, "preload.js"); |
|
||||
logger.log("Checking preload path:", preloadPath); |
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath)); |
|
||||
|
|
||||
// Create the browser window.
|
|
||||
const mainWindow = new BrowserWindow({ |
|
||||
width: 1200, |
|
||||
height: 800, |
|
||||
webPreferences: { |
|
||||
nodeIntegration: false, |
|
||||
contextIsolation: true, |
|
||||
webSecurity: true, |
|
||||
allowRunningInsecureContent: false, |
|
||||
preload: path.join(__dirname, "preload.js"), |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
// Always open DevTools for now
|
|
||||
mainWindow.webContents.openDevTools(); |
|
||||
|
|
||||
// Intercept requests to fix asset paths
|
|
||||
mainWindow.webContents.session.webRequest.onBeforeRequest( |
|
||||
{ |
|
||||
urls: [ |
|
||||
"file://*/*/assets/*", |
|
||||
"file://*/assets/*", |
|
||||
"file:///assets/*", // Catch absolute paths
|
|
||||
"<all_urls>", // Catch all URLs as a fallback
|
|
||||
], |
|
||||
}, |
|
||||
(details, callback) => { |
|
||||
let url = details.url; |
|
||||
|
|
||||
// Handle paths that don't start with file://
|
|
||||
if (!url.startsWith("file://") && url.includes("/assets/")) { |
|
||||
url = `file://${path.join(__dirname, "www", url)}`; |
|
||||
} |
|
||||
|
|
||||
// Handle absolute paths starting with /assets/
|
|
||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) { |
|
||||
const baseDir = url.includes("dist-electron") |
|
||||
? url.substring( |
|
||||
0, |
|
||||
url.indexOf("/dist-electron") + "/dist-electron".length, |
|
||||
) |
|
||||
: `file://${__dirname}`; |
|
||||
const assetPath = url.split("/assets/")[1]; |
|
||||
const newUrl = `${baseDir}/www/assets/${assetPath}`; |
|
||||
callback({ redirectURL: newUrl }); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
callback({}); // No redirect for other URLs
|
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
if (isDev) { |
|
||||
// Debug info
|
|
||||
logger.log("Debug Info:"); |
|
||||
logger.log("Running in dev mode:", isDev); |
|
||||
logger.log("App is packaged:", app.isPackaged); |
|
||||
logger.log("Process resource path:", process.resourcesPath); |
|
||||
logger.log("App path:", app.getAppPath()); |
|
||||
logger.log("__dirname:", __dirname); |
|
||||
logger.log("process.cwd():", process.cwd()); |
|
||||
} |
|
||||
|
|
||||
const indexPath = path.join(__dirname, "www", "index.html"); |
|
||||
|
|
||||
if (isDev) { |
|
||||
logger.log("Loading index from:", indexPath); |
|
||||
logger.log("www path:", path.join(__dirname, "www")); |
|
||||
logger.log("www assets path:", path.join(__dirname, "www", "assets")); |
|
||||
} |
|
||||
|
|
||||
if (!fs.existsSync(indexPath)) { |
|
||||
logger.error(`Index file not found at: ${indexPath}`); |
|
||||
throw new Error("Index file not found"); |
|
||||
} |
|
||||
|
|
||||
// Add CSP headers to allow API connections
|
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived( |
|
||||
(details, callback) => { |
|
||||
callback({ |
|
||||
responseHeaders: { |
|
||||
...details.responseHeaders, |
|
||||
"Content-Security-Policy": [ |
|
||||
"default-src 'self';" + |
|
||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" + |
|
||||
"img-src 'self' data: https: blob:;" + |
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" + |
|
||||
"style-src 'self' 'unsafe-inline';" + |
|
||||
"font-src 'self' data:;", |
|
||||
], |
|
||||
}, |
|
||||
}); |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
// Load the index.html
|
|
||||
mainWindow |
|
||||
.loadFile(indexPath) |
|
||||
.then(() => { |
|
||||
logger.log("Successfully loaded index.html"); |
|
||||
if (isDev) { |
|
||||
mainWindow.webContents.openDevTools(); |
|
||||
logger.log("DevTools opened - running in dev mode"); |
|
||||
} |
|
||||
}) |
|
||||
.catch((err) => { |
|
||||
logger.error("Failed to load index.html:", err); |
|
||||
logger.error("Attempted path:", indexPath); |
|
||||
}); |
|
||||
|
|
||||
// Listen for console messages from the renderer
|
|
||||
mainWindow.webContents.on("console-message", (_event, level, message) => { |
|
||||
logger.log("Renderer Console:", message); |
|
||||
}); |
|
||||
|
|
||||
// Add right after creating the BrowserWindow
|
|
||||
mainWindow.webContents.on( |
|
||||
"did-fail-load", |
|
||||
(event, errorCode, errorDescription) => { |
|
||||
logger.error("Page failed to load:", errorCode, errorDescription); |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
mainWindow.webContents.on("preload-error", (event, preloadPath, error) => { |
|
||||
logger.error("Preload script error:", preloadPath, error); |
|
||||
}); |
|
||||
|
|
||||
mainWindow.webContents.on( |
|
||||
"console-message", |
|
||||
(event, level, message, line, sourceId) => { |
|
||||
logger.log("Renderer Console:", line, sourceId, message); |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
// Enable remote debugging when in dev mode
|
|
||||
if (isDev) { |
|
||||
mainWindow.webContents.openDevTools(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 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); |
|
||||
}); |
|
@ -1,712 +0,0 @@ |
|||||
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<string> => { |
|
||||
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 Main] 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 Main] Using fallback user data path:", userDataPath); |
|
||||
return userDataPath; |
|
||||
} catch (error) { |
|
||||
logger.error("[Electron Main] Error getting app data path:", error); |
|
||||
// Fallback to app.getPath if anything fails
|
|
||||
const userDataPath = app.getPath("userData"); |
|
||||
logger.info( |
|
||||
"[Electron Main] Using fallback user data path after error:", |
|
||||
userDataPath, |
|
||||
); |
|
||||
return userDataPath; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const validateAndNormalizePath = async (filePath: string): Promise<string> => { |
|
||||
// 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 Main] 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<void> => { |
|
||||
try { |
|
||||
// Normalize the path first
|
|
||||
const normalizedPath = path.normalize(dirPath); |
|
||||
|
|
||||
// Check if directory exists
|
|
||||
if (!fs.existsSync(normalizedPath)) { |
|
||||
logger.info("[Electron Main] 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 Main] Database directory permissions verified:", |
|
||||
normalizedPath, |
|
||||
); |
|
||||
} catch (error) { |
|
||||
logger.error("[Electron Main] 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 Main] Database directory write test passed:", |
|
||||
normalizedPath, |
|
||||
); |
|
||||
} catch (error) { |
|
||||
logger.error("[Electron Main] Database directory write test failed:", error); |
|
||||
throw new Error(`Database directory not writable: ${normalizedPath}`); |
|
||||
} |
|
||||
} catch (error) { |
|
||||
logger.error( |
|
||||
"[Electron Main] 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<void> | null = null; |
|
||||
|
|
||||
const initializeDatabasePaths = async (): Promise<void> => { |
|
||||
// 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 Main] 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 Main] 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 Main] Existing database file permissions verified:", |
|
||||
dbPath, |
|
||||
); |
|
||||
} catch (error) { |
|
||||
logger.error("[Electron Main] Database file permission error:", error); |
|
||||
throw new Error(`Database file not accessible: ${dbPath}`); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
dbPathInitialized = true; |
|
||||
} catch (error) { |
|
||||
logger.error("[Electron Main] 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<void> | null = null; |
|
||||
|
|
||||
async function initializeSQLite() { |
|
||||
// Prevent multiple simultaneous initializations
|
|
||||
if (sqliteInitializationPromise) { |
|
||||
return sqliteInitializationPromise; |
|
||||
} |
|
||||
|
|
||||
if (sqliteInitialized) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
sqliteInitializationPromise = (async () => { |
|
||||
try { |
|
||||
logger.info("[Electron Main] 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 Main] 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 Main] Creating initial connection with options:", |
|
||||
connectionOptions, |
|
||||
); |
|
||||
|
|
||||
// Log the actual path being used
|
|
||||
logger.info("[Electron Main] Using database path:", dbPath); |
|
||||
logger.info("[Electron Main] Path exists:", fs.existsSync(dbPath)); |
|
||||
logger.info("[Electron Main] 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 Main] Database connection verified:", result); |
|
||||
} catch (error) { |
|
||||
logger.error( |
|
||||
"[Electron Main] Database connection verification failed:", |
|
||||
error, |
|
||||
); |
|
||||
throw error; |
|
||||
} |
|
||||
|
|
||||
sqliteInitialized = true; |
|
||||
logger.info("[Electron Main] SQLite plugin initialized successfully"); |
|
||||
} catch (error) { |
|
||||
logger.error("[Electron Main] 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(); |
|
||||
|
|
||||
// Wait for window to be ready
|
|
||||
await new Promise<void>((resolve) => { |
|
||||
mainWindow.once('ready-to-show', () => { |
|
||||
logger.info("[Electron Main] Window ready to show"); |
|
||||
mainWindow.show(); |
|
||||
resolve(); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
// Initialize database after window is ready
|
|
||||
try { |
|
||||
await initializeSQLite(); |
|
||||
logger.info("[Electron Main] SQLite plugin initialized successfully"); |
|
||||
|
|
||||
// Now send the ready signal since window is ready
|
|
||||
if (!mainWindow.isDestroyed()) { |
|
||||
mainWindow.webContents.send('sqlite-ready'); |
|
||||
logger.info("[Electron Main] Sent SQLite ready signal to renderer"); |
|
||||
} else { |
|
||||
logger.error("[Electron Main] Window was destroyed before sending SQLite ready signal"); |
|
||||
} |
|
||||
} catch (error) { |
|
||||
logger.error( |
|
||||
"[Electron Main] Database initialization failed:", |
|
||||
error, |
|
||||
); |
|
||||
// Notify renderer about database status
|
|
||||
if (!mainWindow.isDestroyed()) { |
|
||||
mainWindow.webContents.send("database-status", { |
|
||||
status: "error", |
|
||||
error: error instanceof Error ? error.message : 'Unknown error', |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Handle window close
|
|
||||
mainWindow.on("closed", () => { |
|
||||
logger.info("[Electron Main] Main window closed"); |
|
||||
}); |
|
||||
|
|
||||
// Handle window close request
|
|
||||
mainWindow.on("close", (event) => { |
|
||||
logger.info("[Electron Main] Window close requested"); |
|
||||
// Prevent immediate close if we're in the middle of something
|
|
||||
if (mainWindow.webContents.isLoading()) { |
|
||||
event.preventDefault(); |
|
||||
logger.info("[Electron Main] 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 Main] Preload path:", preloadPath); |
|
||||
logger.log("[Electron Main] Preload exists:", fs.existsSync(preloadPath)); |
|
||||
|
|
||||
// Log environment and paths
|
|
||||
logger.log("[Electron Main] process.cwd():", process.cwd()); |
|
||||
logger.log("[Electron Main] __dirname:", __dirname); |
|
||||
logger.log("[Electron Main] app.getAppPath():", app.getAppPath()); |
|
||||
logger.log("[Electron Main] app.isPackaged:", app.isPackaged); |
|
||||
logger.log("[Electron Main] 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 Main] Window ready to show"); |
|
||||
mainWindow.show(); |
|
||||
}); |
|
||||
|
|
||||
// Handle window errors
|
|
||||
mainWindow.webContents.on("render-process-gone", (_event, details) => { |
|
||||
logger.error("[Electron Main] Render process gone:", details); |
|
||||
}); |
|
||||
|
|
||||
mainWindow.webContents.on( |
|
||||
"did-fail-load", |
|
||||
(_event, errorCode, errorDescription) => { |
|
||||
logger.error( |
|
||||
"[Electron Main] Page failed to load:", |
|
||||
errorCode, |
|
||||
errorDescription, |
|
||||
); |
|
||||
logger.error("[Electron Main] 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 Main] App is packaged. Using process.resourcesPath for index.html", |
|
||||
); |
|
||||
} else { |
|
||||
indexPath = path.resolve(__dirname, "www", "index.html"); |
|
||||
fileUrl = `file://${indexPath}`; |
|
||||
logger.info( |
|
||||
"[Electron Main] App is not packaged. Using __dirname for index.html", |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
logger.info("[Electron Main] Resolved index.html path:", indexPath); |
|
||||
logger.info("[Electron Main] Using file URL:", fileUrl); |
|
||||
|
|
||||
// Load the index.html with retry logic
|
|
||||
const loadIndexHtml = async (retryCount = 0): Promise<void> => { |
|
||||
try { |
|
||||
if (mainWindow.isDestroyed()) { |
|
||||
logger.error( |
|
||||
"[Electron Main] Window was destroyed before loading index.html", |
|
||||
); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const exists = fs.existsSync(indexPath); |
|
||||
logger.info(`[Electron Main] 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 Main] index.html stats:", { |
|
||||
size: stats.size, |
|
||||
mode: stats.mode, |
|
||||
uid: stats.uid, |
|
||||
gid: stats.gid, |
|
||||
}); |
|
||||
|
|
||||
// Try loadURL first
|
|
||||
try { |
|
||||
logger.info("[Electron Main] Attempting to load index.html via loadURL"); |
|
||||
await mainWindow.loadURL(fileUrl); |
|
||||
logger.info("[Electron Main] Successfully loaded index.html via loadURL"); |
|
||||
} catch (loadUrlError) { |
|
||||
logger.warn( |
|
||||
"[Electron Main] loadURL failed, trying loadFile:", |
|
||||
loadUrlError, |
|
||||
); |
|
||||
// Fallback to loadFile
|
|
||||
await mainWindow.loadFile(indexPath); |
|
||||
logger.info("[Electron Main] Successfully loaded index.html via loadFile"); |
|
||||
} |
|
||||
} catch (error: unknown) { |
|
||||
const errorMessage = |
|
||||
error instanceof Error ? error.message : "Unknown error occurred"; |
|
||||
logger.error("[Electron Main] Error loading index.html:", errorMessage); |
|
||||
|
|
||||
// Retry logic
|
|
||||
if (retryCount < 3 && !mainWindow.isDestroyed()) { |
|
||||
logger.info( |
|
||||
`[Electron Main] 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 = ` |
|
||||
<html> |
|
||||
<body style="font-family: sans-serif; padding: 20px;"> |
|
||||
<h1>Error Loading Application</h1> |
|
||||
<p>Failed to load the application after multiple attempts.</p> |
|
||||
<pre style="background: #f0f0f0; padding: 10px; border-radius: 4px;">${errorMessage}</pre> |
|
||||
</body> |
|
||||
</html> |
|
||||
`;
|
|
||||
await mainWindow.loadURL( |
|
||||
`data:text/html,${encodeURIComponent(errorHtml)}`, |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
// Start loading the index.html
|
|
||||
loadIndexHtml().catch((error: unknown) => { |
|
||||
logger.error("[Electron Main] 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 Main] 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 Main] Connection opened in read-only mode despite options", |
|
||||
); |
|
||||
throw new Error("Database connection opened in read-only mode"); |
|
||||
} |
|
||||
} catch (queryError) { |
|
||||
logger.error("[Electron Main] Error verifying connection:", queryError); |
|
||||
throw queryError; |
|
||||
} |
|
||||
|
|
||||
logger.info("[Electron Main] Database connection created successfully"); |
|
||||
return result; |
|
||||
} catch (error) { |
|
||||
logger.error("[Electron Main] 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; |
|
||||
}); |
|
@ -1,178 +0,0 @@ |
|||||
const { contextBridge, ipcRenderer } = require("electron"); |
|
||||
|
|
||||
const logger = { |
|
||||
log: (message, ...args) => { |
|
||||
// Always log in development, log with context in production
|
|
||||
if (process.env.NODE_ENV !== "production") { |
|
||||
/* eslint-disable no-console */ |
|
||||
console.log(`[Electron Preload] ${message}`, ...args); |
|
||||
/* eslint-enable no-console */ |
|
||||
} |
|
||||
}, |
|
||||
warn: (message, ...args) => { |
|
||||
// Always log warnings
|
|
||||
/* eslint-disable no-console */ |
|
||||
console.warn(`[Electron Preload] ${message}`, ...args); |
|
||||
/* eslint-enable no-console */ |
|
||||
}, |
|
||||
error: (message, ...args) => { |
|
||||
// Always log errors
|
|
||||
/* eslint-disable no-console */ |
|
||||
console.error(`[Electron Preload] ${message}`, ...args); |
|
||||
/* eslint-enable no-console */ |
|
||||
}, |
|
||||
info: (message, ...args) => { |
|
||||
// Always log info in development, log with context in production
|
|
||||
if (process.env.NODE_ENV !== "production") { |
|
||||
/* eslint-disable no-console */ |
|
||||
console.info(`[Electron Preload] ${message}`, ...args); |
|
||||
/* eslint-enable no-console */ |
|
||||
} |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
// Use a more direct path resolution approach
|
|
||||
const getPath = (pathType) => { |
|
||||
switch (pathType) { |
|
||||
case "userData": |
|
||||
return ( |
|
||||
process.env.APPDATA || |
|
||||
(process.platform === "darwin" |
|
||||
? `${process.env.HOME}/Library/Application Support` |
|
||||
: `${process.env.HOME}/.local/share`) |
|
||||
); |
|
||||
case "home": |
|
||||
return process.env.HOME; |
|
||||
case "appPath": |
|
||||
return process.resourcesPath; |
|
||||
default: |
|
||||
return ""; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
logger.info("Preload script starting..."); |
|
||||
|
|
||||
// Force electron platform in the renderer process
|
|
||||
window.process = { env: { VITE_PLATFORM: "electron" } }; |
|
||||
|
|
||||
try { |
|
||||
contextBridge.exposeInMainWorld("electronAPI", { |
|
||||
// Path utilities
|
|
||||
getPath, |
|
||||
|
|
||||
// IPC functions
|
|
||||
send: (channel, data) => { |
|
||||
const validChannels = ["toMain"]; |
|
||||
if (validChannels.includes(channel)) { |
|
||||
ipcRenderer.send(channel, data); |
|
||||
} |
|
||||
}, |
|
||||
receive: (channel, func) => { |
|
||||
const validChannels = ["fromMain"]; |
|
||||
if (validChannels.includes(channel)) { |
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args)); |
|
||||
} |
|
||||
}, |
|
||||
// Environment info
|
|
||||
env: { |
|
||||
isElectron: true, |
|
||||
isDev: process.env.NODE_ENV === "development", |
|
||||
platform: "electron", // Explicitly set platform
|
|
||||
}, |
|
||||
// Path utilities
|
|
||||
getBasePath: () => { |
|
||||
return process.env.NODE_ENV === "development" ? "/" : "./"; |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
// Create a proxy for the CapacitorSQLite plugin
|
|
||||
const createSQLiteProxy = () => { |
|
||||
const MAX_RETRIES = 3; |
|
||||
const RETRY_DELAY = 1000; // 1 second
|
|
||||
|
|
||||
const withRetry = async (operation, ...args) => { |
|
||||
let lastError; |
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { |
|
||||
try { |
|
||||
return await operation(...args); |
|
||||
} catch (error) { |
|
||||
lastError = error; |
|
||||
if (attempt < MAX_RETRIES) { |
|
||||
logger.warn( |
|
||||
`SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, |
|
||||
error, |
|
||||
); |
|
||||
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
throw new Error( |
|
||||
`SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`, |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
const wrapOperation = (method) => { |
|
||||
return async (...args) => { |
|
||||
try { |
|
||||
return await withRetry( |
|
||||
ipcRenderer.invoke, |
|
||||
"sqlite-" + method, |
|
||||
...args, |
|
||||
); |
|
||||
} catch (error) { |
|
||||
logger.error(`SQLite ${method} failed:`, error); |
|
||||
throw new Error( |
|
||||
`Database operation failed: ${error.message || "Unknown error"}`, |
|
||||
); |
|
||||
} |
|
||||
}; |
|
||||
}; |
|
||||
|
|
||||
// Create a proxy that matches the CapacitorSQLite interface
|
|
||||
return { |
|
||||
echo: wrapOperation("echo"), |
|
||||
createConnection: wrapOperation("create-connection"), |
|
||||
closeConnection: wrapOperation("close-connection"), |
|
||||
execute: wrapOperation("execute"), |
|
||||
query: wrapOperation("query"), |
|
||||
run: wrapOperation("run"), |
|
||||
isAvailable: wrapOperation("is-available"), |
|
||||
getPlatform: () => Promise.resolve("electron"), |
|
||||
// Add other methods as needed
|
|
||||
}; |
|
||||
}; |
|
||||
|
|
||||
// Expose only the CapacitorSQLite proxy
|
|
||||
contextBridge.exposeInMainWorld("electron", { |
|
||||
sqlite: createSQLiteProxy(), |
|
||||
getPath: (pathType) => ipcRenderer.invoke("get-path", pathType), |
|
||||
send: (channel, data) => { |
|
||||
ipcRenderer.send(channel, data); |
|
||||
}, |
|
||||
receive: (channel, func) => { |
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args)); |
|
||||
}, |
|
||||
ipcRenderer: { |
|
||||
on: (channel, func) => { |
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args)); |
|
||||
}, |
|
||||
once: (channel, func) => { |
|
||||
ipcRenderer.once(channel, (event, ...args) => func(...args)); |
|
||||
}, |
|
||||
send: (channel, data) => { |
|
||||
ipcRenderer.send(channel, data); |
|
||||
}, |
|
||||
invoke: (channel, ...args) => { |
|
||||
return ipcRenderer.invoke(channel, ...args); |
|
||||
} |
|
||||
}, |
|
||||
env: { |
|
||||
platform: "electron", |
|
||||
}, |
|
||||
getBasePath: () => ipcRenderer.invoke("get-base-path"), |
|
||||
}); |
|
||||
|
|
||||
logger.info("Preload script completed successfully"); |
|
||||
} catch (error) { |
|
||||
logger.error("Error in preload script:", error); |
|
||||
} |
|
Loading…
Reference in new issue