You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

692 lines
20 KiB

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] 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<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] 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] 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<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] 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<void> | 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<void> => {
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 = `
<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] 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;
});