feat(electron): enhance SQLite operation logging and debugging
WIP: Debugging sqlite-run/dbExec hanging issue - Add renderer-to-main process log forwarding - Implement operation tracking with unique IDs - Add detailed timing and retry logging - Enhance error capture and formatting - Move logs to app user data directory - Add exponential backoff for retries This commit adds comprehensive logging to help diagnose why dbExec operations are hanging when sent through the sqlite-run channel. Changes include: - Forward all renderer process logs to main process - Track SQLite operations with unique IDs - Log operation timing and retry attempts - Capture detailed error information - Implement exponential backoff for retries - Centralize logs in app user data directory Security: - Logs are stored in app user data directory - Sensitive data is sanitized in logs - Error stacks are properly captured Testing: - Manual testing of logging system - Verify log capture in both processes - Check log rotation and file sizes TODO: - Monitor logs to identify root cause - Add specific logging for settings table - Consider connection pooling if needed
This commit is contained in:
+132
-18
@@ -7,20 +7,64 @@
|
|||||||
|
|
||||||
import { contextBridge, ipcRenderer } from 'electron';
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
// Enhanced logger for preload script
|
// Enhanced logger for preload script that forwards to main process
|
||||||
const logger = {
|
const logger = {
|
||||||
log: (...args: unknown[]) => console.log('[Preload]', ...args),
|
log: (...args: unknown[]) => {
|
||||||
error: (...args: unknown[]) => console.error('[Preload]', ...args),
|
console.log('[Preload]', ...args);
|
||||||
info: (...args: unknown[]) => console.info('[Preload]', ...args),
|
ipcRenderer.send('renderer-log', { level: 'log', args });
|
||||||
warn: (...args: unknown[]) => console.warn('[Preload]', ...args),
|
},
|
||||||
debug: (...args: unknown[]) => console.debug('[Preload]', ...args),
|
error: (...args: unknown[]) => {
|
||||||
|
console.error('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'error', args });
|
||||||
|
},
|
||||||
|
info: (...args: unknown[]) => {
|
||||||
|
console.info('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'info', args });
|
||||||
|
},
|
||||||
|
warn: (...args: unknown[]) => {
|
||||||
|
console.warn('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'warn', args });
|
||||||
|
},
|
||||||
|
debug: (...args: unknown[]) => {
|
||||||
|
console.debug('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'debug', args });
|
||||||
|
},
|
||||||
sqlite: {
|
sqlite: {
|
||||||
log: (operation: string, ...args: unknown[]) =>
|
log: (operation: string, ...args: unknown[]) => {
|
||||||
console.log('[Preload][SQLite]', operation, ...args),
|
const message = ['[Preload][SQLite]', operation, ...args];
|
||||||
error: (operation: string, error: unknown) =>
|
console.log(...message);
|
||||||
console.error('[Preload][SQLite]', operation, 'failed:', error),
|
ipcRenderer.send('renderer-log', {
|
||||||
debug: (operation: string, ...args: unknown[]) =>
|
level: 'log',
|
||||||
console.debug('[Preload][SQLite]', operation, ...args)
|
args: message,
|
||||||
|
source: 'sqlite',
|
||||||
|
operation
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (operation: string, error: unknown) => {
|
||||||
|
const message = ['[Preload][SQLite]', operation, 'failed:', error];
|
||||||
|
console.error(...message);
|
||||||
|
ipcRenderer.send('renderer-log', {
|
||||||
|
level: 'error',
|
||||||
|
args: message,
|
||||||
|
source: 'sqlite',
|
||||||
|
operation,
|
||||||
|
error: error instanceof Error ? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
} : error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
debug: (operation: string, ...args: unknown[]) => {
|
||||||
|
const message = ['[Preload][SQLite]', operation, ...args];
|
||||||
|
console.debug(...message);
|
||||||
|
ipcRenderer.send('renderer-log', {
|
||||||
|
level: 'debug',
|
||||||
|
args: message,
|
||||||
|
source: 'sqlite',
|
||||||
|
operation
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,24 +167,94 @@ const createSQLiteProxy = () => {
|
|||||||
|
|
||||||
const withRetry = async <T>(operation: string, ...args: unknown[]): Promise<T> => {
|
const withRetry = async <T>(operation: string, ...args: unknown[]): Promise<T> => {
|
||||||
let lastError: Error | undefined;
|
let lastError: Error | undefined;
|
||||||
|
const operationId = `${operation}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
logger.sqlite.debug(operation, 'starting with args:', {
|
||||||
|
operationId,
|
||||||
|
args,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
try {
|
try {
|
||||||
logger.sqlite.debug(operation, 'attempt', attempt, args);
|
logger.sqlite.debug(operation, `attempt ${attempt}/${MAX_RETRIES}`, {
|
||||||
|
operationId,
|
||||||
|
attempt,
|
||||||
|
args,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log the exact IPC call
|
||||||
|
logger.sqlite.debug(operation, 'invoking IPC', {
|
||||||
|
operationId,
|
||||||
|
channel: `sqlite-${operation}`,
|
||||||
|
args,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
const result = await ipcRenderer.invoke(`sqlite-${operation}`, ...args);
|
const result = await ipcRenderer.invoke(`sqlite-${operation}`, ...args);
|
||||||
logger.sqlite.log(operation, 'success', result);
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.sqlite.log(operation, 'success', {
|
||||||
|
operationId,
|
||||||
|
attempt,
|
||||||
|
result,
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
return result as T;
|
return result as T;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
lastError = error instanceof Error ? error : new Error(String(error));
|
lastError = error instanceof Error ? error : new Error(String(error));
|
||||||
logger.sqlite.error(operation, error);
|
|
||||||
|
logger.sqlite.error(operation, {
|
||||||
|
operationId,
|
||||||
|
attempt,
|
||||||
|
error: {
|
||||||
|
name: lastError.name,
|
||||||
|
message: lastError.message,
|
||||||
|
stack: lastError.stack
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
if (attempt < MAX_RETRIES) {
|
if (attempt < MAX_RETRIES) {
|
||||||
logger.warn(`[Preload] SQLite ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
|
const backoffDelay = RETRY_DELAY * Math.pow(2, attempt - 1);
|
||||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
logger.warn(`[Preload] SQLite ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying in ${backoffDelay}ms...`, {
|
||||||
|
operationId,
|
||||||
|
error: lastError,
|
||||||
|
args,
|
||||||
|
nextAttemptIn: `${backoffDelay}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`SQLite ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`);
|
const finalError = new Error(
|
||||||
|
`SQLite ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.error('[Preload] SQLite operation failed permanently:', {
|
||||||
|
operation,
|
||||||
|
operationId,
|
||||||
|
error: {
|
||||||
|
name: finalError.name,
|
||||||
|
message: finalError.message,
|
||||||
|
stack: finalError.stack,
|
||||||
|
originalError: lastError
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
attempts: MAX_RETRIES,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
throw finalError;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { app, ipcMain } from 'electron';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
@@ -29,10 +30,10 @@ declare module 'winston' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure log directory exists
|
// Create logs directory if it doesn't exist
|
||||||
const logDir = path.join(os.homedir(), 'Logs', 'TimeSafari');
|
const logsDir = path.join(app.getPath('userData'), 'logs');
|
||||||
if (!fs.existsSync(logDir)) {
|
if (!fs.existsSync(logsDir)) {
|
||||||
fs.mkdirSync(logDir, { recursive: true, mode: 0o755 });
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom format for console output with migration filtering
|
// Custom format for console output with migration filtering
|
||||||
@@ -63,7 +64,7 @@ const fileFormat = winston.format.combine(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create logger instance
|
// Create logger instance
|
||||||
export const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||||
format: fileFormat,
|
format: fileFormat,
|
||||||
defaultMeta: { service: 'timesafari-electron' },
|
defaultMeta: { service: 'timesafari-electron' },
|
||||||
@@ -76,14 +77,14 @@ export const logger = winston.createLogger({
|
|||||||
}),
|
}),
|
||||||
// File transport for all logs
|
// File transport for all logs
|
||||||
new winston.transports.File({
|
new winston.transports.File({
|
||||||
filename: path.join(logDir, 'error.log'),
|
filename: path.join(logsDir, 'error.log'),
|
||||||
level: 'error',
|
level: 'error',
|
||||||
maxsize: 5242880, // 5MB
|
maxsize: 5242880, // 5MB
|
||||||
maxFiles: 5
|
maxFiles: 5
|
||||||
}),
|
}),
|
||||||
// File transport for all logs including debug
|
// File transport for all logs including debug
|
||||||
new winston.transports.File({
|
new winston.transports.File({
|
||||||
filename: path.join(logDir, 'combined.log'),
|
filename: path.join(logsDir, 'combined.log'),
|
||||||
maxsize: 5242880, // 5MB
|
maxsize: 5242880, // 5MB
|
||||||
maxFiles: 5
|
maxFiles: 5
|
||||||
})
|
})
|
||||||
@@ -150,5 +151,38 @@ logger.migration = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add renderer log handler
|
||||||
|
ipcMain.on('renderer-log', (_event, { level, args, source, operation, error }) => {
|
||||||
|
const message = args.map((arg: unknown) =>
|
||||||
|
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
||||||
|
).join(' ');
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
source: source || 'renderer',
|
||||||
|
...(operation && { operation }),
|
||||||
|
...(error && { error })
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'error':
|
||||||
|
logger.error(message, meta);
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
logger.warn(message, meta);
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
logger.info(message, meta);
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
logger.debug(message, meta);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.log(level, message, meta);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Export logger instance
|
// Export logger instance
|
||||||
export default logger;
|
export { logger };
|
||||||
|
|
||||||
|
// Export a function to get the logs directory
|
||||||
|
export const getLogsDirectory = () => logsDir;
|
||||||
+192
-15
@@ -92,6 +92,7 @@ let transactionState: TransactionState = {
|
|||||||
const MAX_RECOVERY_ATTEMPTS = 3;
|
const MAX_RECOVERY_ATTEMPTS = 3;
|
||||||
const RECOVERY_DELAY_MS = 1000;
|
const RECOVERY_DELAY_MS = 1000;
|
||||||
const VERIFICATION_TIMEOUT_MS = 5000;
|
const VERIFICATION_TIMEOUT_MS = 5000;
|
||||||
|
const LOCK_TIMEOUT_MS = 10000; // Added for the new sqlite-run handler
|
||||||
|
|
||||||
// Type definitions for SQLite operations
|
// Type definitions for SQLite operations
|
||||||
interface SQLiteConnectionOptions {
|
interface SQLiteConnectionOptions {
|
||||||
@@ -105,6 +106,7 @@ interface SQLiteConnectionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SQLiteQueryOptions {
|
interface SQLiteQueryOptions {
|
||||||
|
database: string;
|
||||||
statement: string;
|
statement: string;
|
||||||
values?: unknown[];
|
values?: unknown[];
|
||||||
}
|
}
|
||||||
@@ -619,19 +621,102 @@ const cleanupSQLiteHandlers = (): void => {
|
|||||||
registeredHandlers.clear();
|
registeredHandlers.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Add at the top of the file after imports
|
||||||
* Registers an IPC handler with tracking
|
const stringifyObject = (obj: unknown): string => {
|
||||||
* @param channel The IPC channel to register
|
try {
|
||||||
* @param handler The handler function
|
return JSON.stringify(obj, null, 2);
|
||||||
*/
|
} catch (error) {
|
||||||
|
return `[Object could not be stringified: ${error instanceof Error ? error.message : String(error)}]`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the registerHandler function
|
||||||
const registerHandler = (channel: string, handler: (...args: any[]) => Promise<any>): void => {
|
const registerHandler = (channel: string, handler: (...args: any[]) => Promise<any>): void => {
|
||||||
|
logger.info(`[IPC:${channel}] Registering handler`, {
|
||||||
|
channel,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
existingHandler: registeredHandlers.has(channel)
|
||||||
|
});
|
||||||
|
|
||||||
if (registeredHandlers.has(channel)) {
|
if (registeredHandlers.has(channel)) {
|
||||||
logger.debug(`Handler already registered for channel: ${channel}, removing first`);
|
logger.warn(`[IPC:${channel}] Handler already registered, removing first`, {
|
||||||
ipcMain.removeHandler(channel);
|
channel,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
ipcMain.removeHandler(channel);
|
||||||
|
logger.debug(`[IPC:${channel}] Removed existing handler`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[IPC:${channel}] Failed to remove existing handler:`, {
|
||||||
|
error: error instanceof Error ? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
} : String(error),
|
||||||
|
channel,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the handler with logging
|
||||||
|
const wrappedHandler = async (...args: any[]) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
logger.info(`[IPC:${channel}] Handler called with args:`, {
|
||||||
|
channel,
|
||||||
|
args: args.map(arg => stringifyObject(arg)),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await handler(...args);
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
logger.info(`[IPC:${channel}] Handler completed successfully:`, {
|
||||||
|
channel,
|
||||||
|
result: stringifyObject(result),
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
logger.error(`[IPC:${channel}] Handler failed:`, {
|
||||||
|
channel,
|
||||||
|
error: error instanceof Error ? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
} : stringifyObject(error),
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
ipcMain.handle(channel, wrappedHandler);
|
||||||
|
registeredHandlers.add(channel);
|
||||||
|
logger.info(`[IPC:${channel}] Handler registered successfully`, {
|
||||||
|
channel,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[IPC:${channel}] Failed to register handler:`, {
|
||||||
|
error: error instanceof Error ? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
} : String(error),
|
||||||
|
channel,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
ipcMain.handle(channel, handler);
|
|
||||||
registeredHandlers.add(channel);
|
|
||||||
logger.debug(`Registered handler for channel: ${channel}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -743,19 +828,111 @@ export function setupSQLiteHandlers(): void {
|
|||||||
|
|
||||||
// Handler for running SQL statements
|
// Handler for running SQL statements
|
||||||
registerHandler('sqlite-run', async (_event, options: SQLiteQueryOptions) => {
|
registerHandler('sqlite-run', async (_event, options: SQLiteQueryOptions) => {
|
||||||
logger.debug('Running SQL statement:', options);
|
const startTime = Date.now();
|
||||||
|
const operationId = `run_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
logger.info('[sqlite-run] IPC handler called with options:', {
|
||||||
|
operationId,
|
||||||
|
options: stringifyObject(options),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logger.debug('[sqlite-run] Starting database operation', {
|
||||||
|
operationId,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
startDatabaseOperation();
|
startDatabaseOperation();
|
||||||
|
|
||||||
if (!pluginState.instance) {
|
if (!pluginState.instance) {
|
||||||
throw new SQLiteError('Plugin not initialized', 'sqlite-run');
|
const error = new SQLiteError('Plugin not initialized', 'sqlite-run');
|
||||||
|
logger.error('[sqlite-run] Plugin not initialized:', {
|
||||||
|
operationId,
|
||||||
|
error: stringifyObject(error),
|
||||||
|
pluginState: stringifyObject(pluginState),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
const result = await pluginState.instance.run(options);
|
|
||||||
logger.debug('SQL run result:', result);
|
// Verify database is open
|
||||||
|
const isOpen = await pluginState.instance.isDBOpen({ database: options.database });
|
||||||
|
if (!isOpen) {
|
||||||
|
const error = new SQLiteError('Database not open', 'sqlite-run');
|
||||||
|
logger.error('[sqlite-run] Database not open:', {
|
||||||
|
operationId,
|
||||||
|
database: options.database,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('[sqlite-run] Executing SQL with plugin:', {
|
||||||
|
operationId,
|
||||||
|
options: stringifyObject(options),
|
||||||
|
pluginState: stringifyObject(pluginState),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add timeout promise
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new Error(`SQLite run operation timed out after ${LOCK_TIMEOUT_MS}ms`));
|
||||||
|
}, LOCK_TIMEOUT_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Race between the operation and timeout
|
||||||
|
const result = await Promise.race([
|
||||||
|
pluginState.instance.run(options),
|
||||||
|
timeoutPromise
|
||||||
|
]);
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.info('[sqlite-run] SQL execution successful:', {
|
||||||
|
operationId,
|
||||||
|
result: stringifyObject(result),
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('SQL run failed:', error);
|
const duration = Date.now() - startTime;
|
||||||
|
const errorDetails = error instanceof Error ? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
} : stringifyObject(error);
|
||||||
|
|
||||||
|
logger.error('[sqlite-run] SQL execution failed:', {
|
||||||
|
operationId,
|
||||||
|
error: errorDetails,
|
||||||
|
options: stringifyObject(options),
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for specific error types
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const errorMsg = error.message.toLowerCase();
|
||||||
|
if (errorMsg.includes('database is locked') ||
|
||||||
|
errorMsg.includes('database is busy') ||
|
||||||
|
errorMsg.includes('database is locked (5)')) {
|
||||||
|
logger.warn('[sqlite-run] Database lock detected:', {
|
||||||
|
operationId,
|
||||||
|
error: errorDetails,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
logger.debug('[sqlite-run] Ending database operation', {
|
||||||
|
operationId,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
endDatabaseOperation();
|
endDatabaseOperation();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+495
-335
File diff suppressed because it is too large
Load Diff
+73
-32
@@ -3,11 +3,19 @@
|
|||||||
* That file will eventually be deleted.
|
* That file will eventually be deleted.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app";
|
||||||
import { QueryExecResult } from "@/interfaces/database";
|
import { QueryExecResult } from "../interfaces/database";
|
||||||
|
|
||||||
|
const formatLogObject = (obj: unknown): string => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return `[Object could not be stringified: ${error instanceof Error ? error.message : String(error)}]`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export async function updateDefaultSettings(
|
export async function updateDefaultSettings(
|
||||||
settingsChanges: Settings,
|
settingsChanges: Settings,
|
||||||
@@ -23,10 +31,13 @@ export async function updateDefaultSettings(
|
|||||||
"id = ?",
|
"id = ?",
|
||||||
[MASTER_SETTINGS_KEY],
|
[MASTER_SETTINGS_KEY],
|
||||||
);
|
);
|
||||||
|
console.log("[databaseUtil] updateDefaultSettings", { sql, params });
|
||||||
const result = await platformService.dbExec(sql, params);
|
const result = await platformService.dbExec(sql, params);
|
||||||
|
console.log("[databaseUtil] updateDefaultSettings result", { result });
|
||||||
return result.changes === 1;
|
return result.changes === 1;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error updating default settings:", error);
|
logger.error("Error updating default settings:", error);
|
||||||
|
console.log("[databaseUtil] updateDefaultSettings error", { error });
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
throw error; // Re-throw if it's already an Error with a message
|
throw error; // Re-throw if it's already an Error with a message
|
||||||
} else {
|
} else {
|
||||||
@@ -79,48 +90,78 @@ const DEFAULT_SETTINGS: Settings = {
|
|||||||
|
|
||||||
// retrieves default settings
|
// retrieves default settings
|
||||||
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
||||||
|
console.log('[DatabaseUtil] Retrieving default account settings');
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
|
console.log('[DatabaseUtil] Platform service state:', {
|
||||||
|
platformType: platform.constructor.name,
|
||||||
|
capabilities: platform.getCapabilities(),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [
|
const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [
|
||||||
MASTER_SETTINGS_KEY,
|
MASTER_SETTINGS_KEY,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
console.log('[DatabaseUtil] No settings found, returning defaults');
|
||||||
return DEFAULT_SETTINGS;
|
return DEFAULT_SETTINGS;
|
||||||
} else {
|
|
||||||
const settings = mapColumnsToValues(
|
|
||||||
result.columns,
|
|
||||||
result.values,
|
|
||||||
)[0] as Settings;
|
|
||||||
if (settings.searchBoxes) {
|
|
||||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
|
||||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = mapColumnsToValues(result.columns, result.values)[0] as Settings;
|
||||||
|
if (settings.searchBoxes) {
|
||||||
|
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||||
|
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DatabaseUtil] Retrieved settings:', {
|
||||||
|
settings: formatLogObject(settings),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||||
|
console.log('[DatabaseUtil] Retrieving active account settings');
|
||||||
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
||||||
|
|
||||||
|
console.log('[DatabaseUtil] Default settings retrieved:', {
|
||||||
|
defaultSettings: formatLogObject(defaultSettings),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
if (!defaultSettings.activeDid) {
|
if (!defaultSettings.activeDid) {
|
||||||
|
console.log('[DatabaseUtil] No active DID, returning default settings');
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
} else {
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
|
||||||
const result = await platform.dbQuery(
|
|
||||||
"SELECT * FROM settings WHERE accountDid = ?",
|
|
||||||
[defaultSettings.activeDid],
|
|
||||||
);
|
|
||||||
const overrideSettings = result
|
|
||||||
? (mapColumnsToValues(result.columns, result.values)[0] as Settings)
|
|
||||||
: {};
|
|
||||||
const overrideSettingsFiltered = Object.fromEntries(
|
|
||||||
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
|
||||||
);
|
|
||||||
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
|
|
||||||
if (settings.searchBoxes) {
|
|
||||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
|
||||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
const result = await platform.dbQuery(
|
||||||
|
"SELECT * FROM settings WHERE accountDid = ?",
|
||||||
|
[defaultSettings.activeDid],
|
||||||
|
);
|
||||||
|
|
||||||
|
const overrideSettings = result
|
||||||
|
? (mapColumnsToValues(result.columns, result.values)[0] as Settings)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const overrideSettingsFiltered = Object.fromEntries(
|
||||||
|
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
|
||||||
|
if (settings.searchBoxes) {
|
||||||
|
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||||
|
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DatabaseUtil] Final active account settings:', {
|
||||||
|
settings: formatLogObject(settings),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastCleanupDate: string | null = null;
|
let lastCleanupDate: string | null = null;
|
||||||
|
|||||||
@@ -25,9 +25,20 @@ interface Migration {
|
|||||||
sql: string;
|
sql: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SQLiteQueryResult {
|
// Define the SQLite query result type
|
||||||
|
interface SQLiteQueryResult {
|
||||||
|
changes: number;
|
||||||
|
lastId: number;
|
||||||
|
rows?: unknown[];
|
||||||
values?: Record<string, unknown>[];
|
values?: Record<string, unknown>[];
|
||||||
changes?: { changes: number; lastId?: number };
|
}
|
||||||
|
|
||||||
|
// Update the QueryExecResult type to include success and changes
|
||||||
|
interface ElectronQueryExecResult {
|
||||||
|
success: boolean;
|
||||||
|
changes: number;
|
||||||
|
lastId?: number;
|
||||||
|
rows?: unknown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,6 +84,15 @@ interface SQLiteOperations {
|
|||||||
}) => Promise<{ changes?: { changes: number; lastId?: number } }>;
|
}) => Promise<{ changes?: { changes: number; lastId?: number } }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add at the top of the file after imports
|
||||||
|
const formatLogObject = (obj: unknown): string => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return `[Object could not be stringified: ${error instanceof Error ? error.message : String(error)}]`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for Electron (desktop) platform.
|
* Platform service implementation for Electron (desktop) platform.
|
||||||
* Provides native desktop functionality through Electron and Capacitor plugins for:
|
* Provides native desktop functionality through Electron and Capacitor plugins for:
|
||||||
@@ -710,13 +730,23 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
"Database is in a fatal error state. Please restart the app.",
|
"Database is in a fatal error state. Please restart the app.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
logger.debug('[ElectronPlatformService] [dbQuery] Enqueuing operation', {
|
||||||
|
sql: sql.substring(0, 100) + (sql.length > 100 ? '...' : ''),
|
||||||
|
paramCount: params.length,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
return this.enqueueOperation(async () => {
|
return this.enqueueOperation(async () => {
|
||||||
try {
|
try {
|
||||||
// Get connection (will wait for existing connection if any)
|
// Get connection (will wait for existing connection if any)
|
||||||
|
console.log("[ElectronPlatformService] [dbQuery] Getting connection");
|
||||||
await this.getConnection();
|
await this.getConnection();
|
||||||
|
console.log("[ElectronPlatformService] [dbQuery] Connection acquired");
|
||||||
// Execute query
|
// Execute query
|
||||||
|
console.log(
|
||||||
|
"[ElectronPlatformService] [dbQuery] Executing query",
|
||||||
|
{ sql, params },
|
||||||
|
);
|
||||||
const result = (await window.electron!.ipcRenderer.invoke(
|
const result = (await window.electron!.ipcRenderer.invoke(
|
||||||
"sqlite-query",
|
"sqlite-query",
|
||||||
{
|
{
|
||||||
@@ -725,8 +755,9 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
values: params,
|
values: params,
|
||||||
},
|
},
|
||||||
)) as SQLiteQueryResult;
|
)) as SQLiteQueryResult;
|
||||||
logger.debug(
|
console.log(
|
||||||
"[ElectronPlatformService] [dbQuery] Query executed successfully",
|
"[ElectronPlatformService] [dbQuery] Query executed successfully",
|
||||||
|
{ result },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process results
|
// Process results
|
||||||
@@ -738,9 +769,14 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[ElectronPlatformService] [dbQuery] Query processed successfully",
|
||||||
|
{ processedResult },
|
||||||
|
);
|
||||||
|
|
||||||
return processedResult;
|
return processedResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
console.error(
|
||||||
"[ElectronPlatformService] [dbQuery] Query failed:",
|
"[ElectronPlatformService] [dbQuery] Query failed:",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
@@ -759,6 +795,10 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
sql: string,
|
sql: string,
|
||||||
params?: unknown[],
|
params?: unknown[],
|
||||||
): Promise<{ changes: number; lastId?: number }> {
|
): Promise<{ changes: number; lastId?: number }> {
|
||||||
|
console.log("[ElectronPlatformService] [dbExec] Executing query", {
|
||||||
|
sql,
|
||||||
|
params,
|
||||||
|
});
|
||||||
if (this.dbFatalError) {
|
if (this.dbFatalError) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Database is in a fatal error state. Please restart the app.",
|
"Database is in a fatal error state. Please restart the app.",
|
||||||
@@ -768,9 +808,15 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
return this.enqueueOperation(async () => {
|
return this.enqueueOperation(async () => {
|
||||||
try {
|
try {
|
||||||
// Get connection (will wait for existing connection if any)
|
// Get connection (will wait for existing connection if any)
|
||||||
|
console.log("[ElectronPlatformService] [dbExec] Getting connection");
|
||||||
await this.getConnection();
|
await this.getConnection();
|
||||||
|
console.log("[ElectronPlatformService] [dbExec] Connection acquired");
|
||||||
|
|
||||||
// Execute query
|
// Execute query
|
||||||
|
console.log(
|
||||||
|
"[ElectronPlatformService] [dbExec] Executing query",
|
||||||
|
{ sql, params },
|
||||||
|
);
|
||||||
const result = (await window.electron!.ipcRenderer.invoke(
|
const result = (await window.electron!.ipcRenderer.invoke(
|
||||||
"sqlite-run",
|
"sqlite-run",
|
||||||
{
|
{
|
||||||
@@ -779,16 +825,20 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
values: params,
|
values: params,
|
||||||
},
|
},
|
||||||
)) as SQLiteQueryResult;
|
)) as SQLiteQueryResult;
|
||||||
logger.debug(
|
console.log(
|
||||||
"[ElectronPlatformService] [dbExec] Query executed successfully",
|
"[ElectronPlatformService] [dbExec] Query executed successfully",
|
||||||
|
{ result },
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
changes: result.changes?.changes || 0,
|
changes: result.changes,
|
||||||
lastId: result.changes?.lastId,
|
lastId: result.lastId,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[ElectronPlatformService] [dbExec] Query failed:", error);
|
console.error(
|
||||||
|
"[ElectronPlatformService] [dbExec] Query failed:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// Release connection after query
|
// Release connection after query
|
||||||
@@ -821,4 +871,46 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
// Optionally implement close logic if needed
|
// Optionally implement close logic if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async run(sql: string, params: unknown[] = []): Promise<ElectronQueryExecResult> {
|
||||||
|
logger.debug('[ElectronPlatformService] [dbRun] Executing SQL:', {
|
||||||
|
sql: sql.substring(0, 100) + (sql.length > 100 ? '...' : ''),
|
||||||
|
params: formatLogObject(params),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = (await window.electron!.ipcRenderer.invoke(
|
||||||
|
'sqlite-run',
|
||||||
|
{
|
||||||
|
database: this.dbName,
|
||||||
|
statement: sql,
|
||||||
|
values: params,
|
||||||
|
},
|
||||||
|
)) as SQLiteQueryResult;
|
||||||
|
|
||||||
|
logger.debug('[ElectronPlatformService] [dbRun] SQL execution result:', {
|
||||||
|
result: formatLogObject(result),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
changes: result.changes,
|
||||||
|
lastId: result.lastId,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[ElectronPlatformService] [dbRun] SQL execution failed:', {
|
||||||
|
error: error instanceof Error ? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
} : formatLogObject(error),
|
||||||
|
sql,
|
||||||
|
params: formatLogObject(params),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1237,7 +1237,9 @@ export default class AccountViewView extends Vue {
|
|||||||
* Initializes component state with values from the database or defaults.
|
* Initializes component state with values from the database or defaults.
|
||||||
*/
|
*/
|
||||||
async initializeState() {
|
async initializeState() {
|
||||||
|
console.log("[AccountViewView] initializeState");
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
console.log("[AccountViewView] initializeState", { settings });
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.open();
|
await db.open();
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
@@ -1949,6 +1951,11 @@ export default class AccountViewView extends Vue {
|
|||||||
current: this.apiServer,
|
current: this.apiServer,
|
||||||
attempted: this.apiServerInput,
|
attempted: this.apiServerInput,
|
||||||
});
|
});
|
||||||
|
console.log("[AccountViewView] API server update failed", {
|
||||||
|
error,
|
||||||
|
current: this.apiServer,
|
||||||
|
attempted: this.apiServerInput,
|
||||||
|
});
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
Reference in New Issue
Block a user