Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions electron/SafeLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import fs from 'fs'
import path from 'path'
import { app } from 'electron'

class SafeLogger {
private logDir: string
private mainLogPath: string
private shortcutLogPath: string

constructor() {
// Initialize log directory
this.logDir = path.join(app.getPath('userData'), 'logs')
this.mainLogPath = path.join(this.logDir, 'main-events.log')
this.shortcutLogPath = path.join(this.logDir, 'shortcut-events.log')

// Create logs directory if it doesn't exist
this.ensureLogDirectory()
}

private ensureLogDirectory(): void {
try {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true })
}
} catch (error) {
// Silently fail - we can't log the error since we're creating the logger
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should silently fail this - if this fails there is no way of figuring out what went wrong before or after

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right. If the log directory creation fails, we should at least attempt to use a fallback location or provide some indication of the failure. I'll update this to try a fallback directory and use stderr as a last resort.

}
}

private formatLogMessage(level: string, source: string, ...args: any[]): string {
const timestamp = new Date().toISOString()
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ')
return `[${timestamp}] [${level}] [${source}] ${message}\n`
}

private writeToFile(filePath: string, message: string): void {
try {
fs.appendFileSync(filePath, message, 'utf8')
} catch (error) {
// Silently fail - we can't use console.log here as it would cause the same issue
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This silent failing is fine as everything would be in the log file already.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this silent failing is appropriate here since if we can't write to the log file, using console.log would defeat the purpose and potentially cause the original EIO error we're trying to solve.

}
}

// Main process logging
mainLog(...args: any[]): void {
const message = this.formatLogMessage('INFO', 'MAIN', ...args)
this.writeToFile(this.mainLogPath, message)
}

mainError(...args: any[]): void {
const message = this.formatLogMessage('ERROR', 'MAIN', ...args)
this.writeToFile(this.mainLogPath, message)
}

mainWarn(...args: any[]): void {
const message = this.formatLogMessage('WARN', 'MAIN', ...args)
this.writeToFile(this.mainLogPath, message)
}

// Shortcut process logging
shortcutLog(...args: any[]): void {
const message = this.formatLogMessage('INFO', 'SHORTCUT', ...args)
this.writeToFile(this.shortcutLogPath, message)
}

shortcutError(...args: any[]): void {
const message = this.formatLogMessage('ERROR', 'SHORTCUT', ...args)
this.writeToFile(this.shortcutLogPath, message)
}

shortcutWarn(...args: any[]): void {
const message = this.formatLogMessage('WARN', 'SHORTCUT', ...args)
this.writeToFile(this.shortcutLogPath, message)
}

// Generic logging method
log(source: string, level: 'INFO' | 'ERROR' | 'WARN', ...args: any[]): void {
const logPath = source === 'SHORTCUT' ? this.shortcutLogPath : this.mainLogPath
const message = this.formatLogMessage(level, source, ...args)
this.writeToFile(logPath, message)
}

// Clean up old logs (optional - call this periodically)
cleanupOldLogs(maxAgeDays: number = 7): void {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is this called? I don't see any function calls
If it is user driven - how can a user call it?
If not we can make a path redirect where the user can go themselves and manually delete the file

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function isn't currently called anywhere. I'll either remove it entirely since users can manually manage log files, or if we want to keep it, I can add it as a startup routine that runs once when the app initializes. What would you prefer?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is wise to clear logs at session startup in case one needs to track past errors.
You could do any of the following:

  1. Give a button for log clearance
  2. Add a message in the initial dialog box with a redirect or a path name so users can do it manually
  3. Clear once a certain size limit is hit like 1 GB threshold (Put a message in the dialog box or the README) that the logs will clear after 1GB

Copy link
Author

@shar-mayank shar-mayank Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is wise to clear logs at session startup in case one needs to track past errors.

yeah, you are right!

You could do any of the following:

  1. Give a button for log clearance
  2. Add a message in the initial dialog box with a redirect or a path name so users can do it manually
  3. Clear once a certain size limit is hit like 1 GB threshold (Put a message in the dialog box or the README) that the logs will clear after 1GB

Okay, I'm thinking of doing a combination of 2nd and 3rd by giving the option to delete it manually by redirecting to the logs directory and also implementing the deletion of logs once it hits 1 GB. I'll work on implementing this and will update ASAP!

try {
const maxAge = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000)
const files = fs.readdirSync(this.logDir)

files.forEach(file => {
const filePath = path.join(this.logDir, file)
const stats = fs.statSync(filePath)

if (stats.mtime.getTime() < maxAge) {
fs.unlinkSync(filePath)
}
})
} catch (error) {
// Silently fail
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be logged normally

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, since this would be called during normal operation (not during logger initialization), we can safely log this error using our own logging methods.

}
}
}

// Create singleton instance
export const safeLogger = new SafeLogger()

// Convenience functions for backward compatibility
export const safeLog = (...args: any[]) => safeLogger.mainLog(...args)
export const safeError = (...args: any[]) => safeLogger.mainError(...args)
export const safeWarn = (...args: any[]) => safeLogger.mainWarn(...args)
78 changes: 40 additions & 38 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import { initAutoUpdater } from "./autoUpdater"
import { configHelper } from "./ConfigHelper"
import * as dotenv from "dotenv"
import { safeLogger } from "./SafeLogger"


// Constants
const isDev = process.env.NODE_ENV === "development"
Expand Down Expand Up @@ -242,18 +244,18 @@

// Add more detailed logging for window events
state.mainWindow.webContents.on("did-finish-load", () => {
console.log("Window finished loading")
safeLogger.mainLog("Window finished loading")
})
state.mainWindow.webContents.on(
"did-fail-load",
async (event, errorCode, errorDescription) => {
console.error("Window failed to load:", errorCode, errorDescription)
safeLogger.mainError("Window failed to load:", errorCode, errorDescription)
if (isDev) {
// In development, retry loading after a short delay
console.log("Retrying to load development server...")
safeLogger.mainLog("Retrying to load development server...")
setTimeout(() => {
state.mainWindow?.loadURL("http://localhost:54321").catch((error) => {
console.error("Failed to load dev server on retry:", error)
safeLogger.mainError("Failed to load dev server on retry:", error)
})
}, 1000)
}
Expand All @@ -262,27 +264,27 @@

if (isDev) {
// In development, load from the dev server
console.log("Loading from development server: http://localhost:54321")
safeLogger.mainLog("Loading from development server: http://localhost:54321")
state.mainWindow.loadURL("http://localhost:54321").catch((error) => {
console.error("Failed to load dev server, falling back to local file:", error)
safeLogger.mainError("Failed to load dev server, falling back to local file:", error)
// Fallback to local file if dev server is not available
const indexPath = path.join(__dirname, "../dist/index.html")
console.log("Falling back to:", indexPath)
safeLogger.mainLog("Falling back to:", indexPath)
if (fs.existsSync(indexPath)) {
state.mainWindow.loadFile(indexPath)

Check failure on line 274 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest

'state.mainWindow' is possibly 'null'.

Check failure on line 274 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest

'state.mainWindow' is possibly 'null'.
} else {
console.error("Could not find index.html in dist folder")
safeLogger.mainError("Could not find index.html in dist folder")
}
})
} else {
// In production, load from the built files
const indexPath = path.join(__dirname, "../dist/index.html")
console.log("Loading production build:", indexPath)
safeLogger.mainLog("Loading production build:", indexPath)

if (fs.existsSync(indexPath)) {
state.mainWindow.loadFile(indexPath)
} else {
console.error("Could not find index.html in dist folder")
safeLogger.mainError("Could not find index.html in dist folder")
}
}

Expand All @@ -292,7 +294,7 @@
state.mainWindow.webContents.openDevTools()
}
state.mainWindow.webContents.setWindowOpenHandler(({ url }) => {
console.log("Attempting to open URL:", url)
safeLogger.mainLog("Attempting to open URL:", url)
try {
const parsedURL = new URL(url);
const hostname = parsedURL.hostname;
Expand All @@ -302,7 +304,7 @@
return { action: "deny" }; // Do not open this URL in a new Electron window
}
} catch (error) {
console.error("Invalid URL %d in setWindowOpenHandler: %d" , url , error);
safeLogger.mainError("Invalid URL %d in setWindowOpenHandler: %d" , url , error);
return { action: "deny" }; // Deny access as URL string is malformed or invalid
}
return { action: "allow" };
Expand Down Expand Up @@ -346,21 +348,21 @@
state.currentX = bounds.x
state.currentY = bounds.y
state.isWindowVisible = true

// Set opacity based on user preferences or hide initially
// Ensure the window is visible for the first launch or if opacity > 0.1
const savedOpacity = configHelper.getOpacity();
console.log(`Initial opacity from config: ${savedOpacity}`);
safeLogger.mainLog(`Initial opacity from config: ${savedOpacity}`);

// Always make sure window is shown first
state.mainWindow.showInactive(); // Use showInactive for consistency

if (savedOpacity <= 0.1) {
console.log('Initial opacity too low, setting to 0 and hiding window');
safeLogger.mainLog('Initial opacity too low, setting to 0 and hiding window');
state.mainWindow.setOpacity(0);
state.isWindowVisible = false;
} else {
console.log(`Setting initial opacity to ${savedOpacity}`);
safeLogger.mainLog(`Setting initial opacity to ${savedOpacity}`);
state.mainWindow.setOpacity(savedOpacity);
state.isWindowVisible = true;
}
Expand Down Expand Up @@ -390,40 +392,40 @@
// Window visibility functions
function hideMainWindow(): void {
if (!state.mainWindow?.isDestroyed()) {
const bounds = state.mainWindow.getBounds();

Check failure on line 395 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest

'state.mainWindow' is possibly 'null'.

Check failure on line 395 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest

'state.mainWindow' is possibly 'null'.
state.windowPosition = { x: bounds.x, y: bounds.y };
state.windowSize = { width: bounds.width, height: bounds.height };
state.mainWindow.setIgnoreMouseEvents(true, { forward: true });

Check failure on line 398 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest

'state.mainWindow' is possibly 'null'.

Check failure on line 398 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest

'state.mainWindow' is possibly 'null'.
state.mainWindow.setOpacity(0);

Check failure on line 399 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest

'state.mainWindow' is possibly 'null'.

Check failure on line 399 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest

'state.mainWindow' is possibly 'null'.
state.isWindowVisible = false;
console.log('Window hidden, opacity set to 0');
safeLogger.mainLog('Window hidden, opacity set to 0');
}
}

function showMainWindow(): void {
if (!state.mainWindow?.isDestroyed()) {
if (state.windowPosition && state.windowSize) {
state.mainWindow.setBounds({

Check failure on line 408 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest

'state.mainWindow' is possibly 'null'.

Check failure on line 408 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest

'state.mainWindow' is possibly 'null'.
...state.windowPosition,
...state.windowSize
});
}
state.mainWindow.setIgnoreMouseEvents(false);

Check failure on line 413 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest

'state.mainWindow' is possibly 'null'.

Check failure on line 413 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest

'state.mainWindow' is possibly 'null'.
state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1);

Check failure on line 414 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest

'state.mainWindow' is possibly 'null'.

Check failure on line 414 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest

'state.mainWindow' is possibly 'null'.
state.mainWindow.setVisibleOnAllWorkspaces(true, {

Check failure on line 415 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest

'state.mainWindow' is possibly 'null'.

Check failure on line 415 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest

'state.mainWindow' is possibly 'null'.
visibleOnFullScreen: true
});
state.mainWindow.setContentProtection(true);

Check failure on line 418 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest

'state.mainWindow' is possibly 'null'.

Check failure on line 418 in electron/main.ts

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest

'state.mainWindow' is possibly 'null'.
state.mainWindow.setOpacity(0); // Set opacity to 0 before showing
state.mainWindow.showInactive(); // Use showInactive instead of show+focus
state.mainWindow.setOpacity(1); // Then set opacity to 1 after showing
state.isWindowVisible = true;
console.log('Window shown with showInactive(), opacity set to 1');
safeLogger.mainLog('Window shown with showInactive(), opacity set to 1');
}
}

function toggleMainWindow(): void {
console.log(`Toggling window. Current state: ${state.isWindowVisible ? 'visible' : 'hidden'}`);
safeLogger.mainLog(`Toggling window. Current state: ${state.isWindowVisible ? 'visible' : 'hidden'}`);
if (state.isWindowVisible) {
hideMainWindow();
} else {
Expand Down Expand Up @@ -451,7 +453,7 @@
state.screenHeight + ((state.windowSize?.height || 0) * 2) / 3

// Log the current state and limits
console.log({
safeLogger.mainLog({
newY,
maxUpLimit,
maxDownLimit,
Expand Down Expand Up @@ -490,16 +492,16 @@
// Environment setup
function loadEnvVariables() {
if (isDev) {
console.log("Loading env variables from:", path.join(process.cwd(), ".env"))
safeLogger.mainLog("Loading env variables from:", path.join(process.cwd(), ".env"))
dotenv.config({ path: path.join(process.cwd(), ".env") })
} else {
console.log(
safeLogger.mainLog(
"Loading env variables from:",
path.join(process.resourcesPath, ".env")
)
dotenv.config({ path: path.join(process.resourcesPath, ".env") })
}
console.log("Environment variables loaded for open-source version")
safeLogger.mainLog("Environment variables loaded for open-source version")
}

// Initialize application
Expand All @@ -510,26 +512,26 @@
const sessionPath = path.join(appDataPath, 'session')
const tempPath = path.join(appDataPath, 'temp')
const cachePath = path.join(appDataPath, 'cache')

// Create directories if they don't exist
for (const dir of [appDataPath, sessionPath, tempPath, cachePath]) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
}

app.setPath('userData', appDataPath)
app.setPath('sessionData', sessionPath)
app.setPath('sessionData', sessionPath)
app.setPath('temp', tempPath)
app.setPath('cache', cachePath)

loadEnvVariables()

// Ensure a configuration file exists
if (!configHelper.hasApiKey()) {
console.log("No API key found in configuration. User will need to set up.")
safeLogger.mainLog("No API key found in configuration. User will need to set up.")
}

initializeHelpers()
initializeIpcHandlers({
getMainWindow,
Expand Down Expand Up @@ -564,27 +566,27 @@

// Initialize auto-updater regardless of environment
initAutoUpdater()
console.log(
safeLogger.mainLog(
"Auto-updater initialized in",
isDev ? "development" : "production",
"mode"
)
} catch (error) {
console.error("Failed to initialize application:", error)
safeLogger.mainError("Failed to initialize application:", error)
app.quit()
}
}

// Auth callback handling removed - no longer needed
app.on("open-url", (event, url) => {
console.log("open-url event received:", url)
safeLogger.mainLog("open-url event received:", url)
event.preventDefault()
})

// Handle second instance (removed auth callback handling)
app.on("second-instance", (event, commandLine) => {
console.log("second-instance event received:", commandLine)
safeLogger.mainLog("second-instance event received:", commandLine)

// Focus or create the main window
if (!state.mainWindow) {
createWindow()
Expand Down
Loading
Loading