|
|
@ -75,27 +75,30 @@ try { |
|
|
|
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 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 || ''); |
|
|
|
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'); |
|
|
|
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); |
|
|
|
const userDataPath = app.getPath("userData"); |
|
|
|
logger.info( |
|
|
|
"[Electron] Using fallback user data path after error:", |
|
|
|
userDataPath, |
|
|
|
); |
|
|
|
return userDataPath; |
|
|
|
} |
|
|
|
}; |
|
|
@ -112,7 +115,9 @@ const validateAndNormalizePath = async (filePath: string): Promise<string> => { |
|
|
|
// 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}`); |
|
|
|
throw new Error( |
|
|
|
`Database path must be within app data directory: ${resolvedPath}`, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// Normalize the path
|
|
|
@ -124,7 +129,7 @@ const validateAndNormalizePath = async (filePath: string): Promise<string> => { |
|
|
|
normalized: normalizedPath, |
|
|
|
appDataPath, |
|
|
|
isAbsolute: path.isAbsolute(normalizedPath), |
|
|
|
isWithinAppData: normalizedPath.startsWith(appDataPath) |
|
|
|
isWithinAppData: normalizedPath.startsWith(appDataPath), |
|
|
|
}); |
|
|
|
|
|
|
|
return normalizedPath; |
|
|
@ -143,25 +148,37 @@ const ensureDirectoryExists = async (dirPath: string): Promise<void> => { |
|
|
|
|
|
|
|
// 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); |
|
|
|
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'); |
|
|
|
const testFile = path.join(normalizedPath, ".write-test"); |
|
|
|
try { |
|
|
|
await fs.promises.writeFile(testFile, 'test'); |
|
|
|
await fs.promises.writeFile(testFile, "test"); |
|
|
|
await fs.promises.unlink(testFile); |
|
|
|
logger.info("[Electron] Database directory write test passed:", normalizedPath); |
|
|
|
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); |
|
|
|
logger.error( |
|
|
|
"[Electron] Failed to ensure database directory exists:", |
|
|
|
error, |
|
|
|
); |
|
|
|
throw error; |
|
|
|
} |
|
|
|
}; |
|
|
@ -192,14 +209,22 @@ const initializeDatabasePaths = async (): Promise<void> => { |
|
|
|
await ensureDirectoryExists(dbDir); |
|
|
|
|
|
|
|
// Construct the database path
|
|
|
|
dbPath = await validateAndNormalizePath(path.join(dbDir, 'timesafari.db')); |
|
|
|
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); |
|
|
|
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}`); |
|
|
@ -256,10 +281,13 @@ async function initializeSQLite() { |
|
|
|
readOnly: false, |
|
|
|
encryption: "no-encryption", |
|
|
|
useNative: true, |
|
|
|
mode: "rwc" // Force read-write-create mode
|
|
|
|
mode: "rwc", // Force read-write-create mode
|
|
|
|
}; |
|
|
|
|
|
|
|
logger.info("[Electron] Creating initial connection with options:", connectionOptions); |
|
|
|
logger.info( |
|
|
|
"[Electron] Creating initial connection with options:", |
|
|
|
connectionOptions, |
|
|
|
); |
|
|
|
|
|
|
|
// Log the actual path being used
|
|
|
|
logger.info("[Electron] Using database path:", dbPath); |
|
|
@ -268,19 +296,24 @@ async function initializeSQLite() { |
|
|
|
|
|
|
|
const db = await sqlitePlugin.createConnection(connectionOptions); |
|
|
|
|
|
|
|
if (!db || typeof db !== 'object') { |
|
|
|
throw new Error(`Failed to create database connection - invalid response. Path used: ${dbPath}`); |
|
|
|
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)); |
|
|
|
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); |
|
|
|
logger.error( |
|
|
|
"[Electron] Database connection verification failed:", |
|
|
|
error, |
|
|
|
); |
|
|
|
throw error; |
|
|
|
} |
|
|
|
|
|
|
@ -306,29 +339,32 @@ app.whenReady().then(async () => { |
|
|
|
|
|
|
|
// Initialize database in background
|
|
|
|
initializeSQLite().catch((error) => { |
|
|
|
logger.error("[Electron] Database initialization failed, but continuing:", 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 |
|
|
|
mainWindow.webContents.send("database-status", { |
|
|
|
status: "error", |
|
|
|
error: error.message, |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// Handle window close
|
|
|
|
mainWindow.on('closed', () => { |
|
|
|
mainWindow.on("closed", () => { |
|
|
|
logger.info("[Electron] Main window closed"); |
|
|
|
}); |
|
|
|
|
|
|
|
// Handle window close request
|
|
|
|
mainWindow.on('close', (event) => { |
|
|
|
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.webContents.once("did-finish-load", () => { |
|
|
|
mainWindow.close(); |
|
|
|
}); |
|
|
|
} |
|
|
@ -378,25 +414,32 @@ function createWindow(): BrowserWindow { |
|
|
|
sandbox: false, |
|
|
|
preload: preloadPath, |
|
|
|
webSecurity: true, |
|
|
|
allowRunningInsecureContent: false |
|
|
|
allowRunningInsecureContent: false, |
|
|
|
}, |
|
|
|
}); |
|
|
|
|
|
|
|
// Show window when ready
|
|
|
|
mainWindow.once('ready-to-show', () => { |
|
|
|
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) => { |
|
|
|
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()); |
|
|
|
}); |
|
|
|
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; |
|
|
@ -405,11 +448,15 @@ function createWindow(): BrowserWindow { |
|
|
|
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"); |
|
|
|
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] App is not packaged. Using __dirname for index.html", |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
logger.info("[Electron] Resolved index.html path:", indexPath); |
|
|
@ -417,14 +464,16 @@ function createWindow(): BrowserWindow { |
|
|
|
|
|
|
|
// Load the index.html with retry logic
|
|
|
|
const loadIndexHtml = async (retryCount = 0): Promise<void> => { |
|
|
|
try { |
|
|
|
try { |
|
|
|
if (mainWindow.isDestroyed()) { |
|
|
|
logger.error("[Electron] Window was destroyed before loading index.html"); |
|
|
|
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}`); |
|
|
@ -436,7 +485,7 @@ function createWindow(): BrowserWindow { |
|
|
|
size: stats.size, |
|
|
|
mode: stats.mode, |
|
|
|
uid: stats.uid, |
|
|
|
gid: stats.gid |
|
|
|
gid: stats.gid, |
|
|
|
}); |
|
|
|
|
|
|
|
// Try loadURL first
|
|
|
@ -445,19 +494,25 @@ function createWindow(): BrowserWindow { |
|
|
|
await mainWindow.loadURL(fileUrl); |
|
|
|
logger.info("[Electron] Successfully loaded index.html via loadURL"); |
|
|
|
} catch (loadUrlError) { |
|
|
|
logger.warn("[Electron] loadURL failed, trying loadFile:", 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'; |
|
|
|
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
|
|
|
|
logger.info( |
|
|
|
`[Electron] Retrying index.html load (attempt ${retryCount + 1})`, |
|
|
|
); |
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
|
|
|
|
return loadIndexHtml(retryCount + 1); |
|
|
|
} |
|
|
|
|
|
|
@ -472,7 +527,9 @@ function createWindow(): BrowserWindow { |
|
|
|
</body> |
|
|
|
</html> |
|
|
|
`;
|
|
|
|
await mainWindow.loadURL(`data:text/html,${encodeURIComponent(errorHtml)}`); |
|
|
|
await mainWindow.loadURL( |
|
|
|
`data:text/html,${encodeURIComponent(errorHtml)}`, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
@ -520,7 +577,12 @@ 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); |
|
|
|
logger.error( |
|
|
|
"Error in sqlite-echo:", |
|
|
|
error, |
|
|
|
JSON.stringify(error), |
|
|
|
(error as any)?.stack, |
|
|
|
); |
|
|
|
throw error; |
|
|
|
} |
|
|
|
}); |
|
|
@ -541,24 +603,33 @@ ipcMain.handle("sqlite-create-connection", async (_event, options) => { |
|
|
|
readOnly: false, |
|
|
|
mode: "rwc", // Force read-write-create mode
|
|
|
|
encryption: "no-encryption", |
|
|
|
useNative: true |
|
|
|
useNative: true, |
|
|
|
}; |
|
|
|
|
|
|
|
logger.info("[Electron] Creating database connection with options:", connectionOptions); |
|
|
|
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"); |
|
|
|
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)); |
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100)); |
|
|
|
|
|
|
|
try { |
|
|
|
// Verify connection is not read-only
|
|
|
|
const testResult = await result.query({ statement: "PRAGMA journal_mode;" }); |
|
|
|
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"); |
|
|
|
logger.error( |
|
|
|
"[Electron] Connection opened in read-only mode despite options", |
|
|
|
); |
|
|
|
throw new Error("Database connection opened in read-only mode"); |
|
|
|
} |
|
|
|
} catch (queryError) { |
|
|
@ -578,7 +649,12 @@ 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); |
|
|
|
logger.error( |
|
|
|
"Error in sqlite-execute:", |
|
|
|
error, |
|
|
|
JSON.stringify(error), |
|
|
|
(error as any)?.stack, |
|
|
|
); |
|
|
|
throw error; |
|
|
|
} |
|
|
|
}); |
|
|
@ -587,7 +663,12 @@ 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); |
|
|
|
logger.error( |
|
|
|
"Error in sqlite-query:", |
|
|
|
error, |
|
|
|
JSON.stringify(error), |
|
|
|
(error as any)?.stack, |
|
|
|
); |
|
|
|
throw error; |
|
|
|
} |
|
|
|
}); |
|
|
@ -596,7 +677,12 @@ 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); |
|
|
|
logger.error( |
|
|
|
"Error in sqlite-close-connection:", |
|
|
|
error, |
|
|
|
JSON.stringify(error), |
|
|
|
(error as any)?.stack, |
|
|
|
); |
|
|
|
throw error; |
|
|
|
} |
|
|
|
}); |
|
|
|