Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
185 changes: 127 additions & 58 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { copyBrowserData } from './copy'
import { findAvailablePort } from './init'
import kill from 'tree-kill';
import { zipFolder } from './utils/log'
import archiver from 'archiver';
import axios from 'axios';
import FormData from 'form-data';
import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps'
Expand Down Expand Up @@ -168,6 +169,11 @@ const getBackupLogPath = () => {
const userDataPath = app.getPath('userData')
return path.join(userDataPath, 'logs', 'main.log')
}
// Get app log path
const getAppLogPath = () => {
const eigentBase = path.join(os.homedir(), '.eigent');
return path.join(eigentBase, 'runtime', 'log')
}
// Constants define
const BROWSER_PATHS = {
win32: {
Expand Down Expand Up @@ -323,16 +329,27 @@ function registerIpcHandlers() {
// ==================== log export handler ====================
ipcMain.handle('export-log', async () => {
try {
let targetLogPath = logPath;
// Always use main.log as the target log file
let targetLogPath = getBackupLogPath(); // This returns main.log
let appLogPath = getAppLogPath();

// Check if main.log exists, if not try the electron-log path
if (!fs.existsSync(targetLogPath)) {
const backupPath = getBackupLogPath();
if (fs.existsSync(backupPath)) {
targetLogPath = backupPath;
} else {
return { success: false, error: 'no log file' };
}
targetLogPath = logPath; // fallback to electron-log path
}

// Check if app log path exists
if (!fs.existsSync(appLogPath)) {
log.warn('App log path does not exist:', appLogPath);
appLogPath = ''; // Set to empty if not exists
}

// Debug log paths
log.info('Target log path:', targetLogPath);
log.info('App log path:', appLogPath);
log.info('Target log exists:', fs.existsSync(targetLogPath));
log.info('App log exists:', fs.existsSync(appLogPath));

await fsp.access(targetLogPath, fs.constants.R_OK);
const stats = await fsp.stat(targetLogPath);
if (stats.size === 0) {
Expand All @@ -342,25 +359,78 @@ function registerIpcHandlers() {
const logContent = await fsp.readFile(targetLogPath, 'utf-8');

// Get app version and system version
const appVersion = app.getVersion();
const platform = process.platform;
const arch = process.arch;
const systemVersion = `${platform}-${arch}`;
const defaultFileName = `eigent-${appVersion}-${systemVersion}-${Date.now()}.log`;

// Show save dialog
const { canceled, filePath } = await dialog.showSaveDialog({
title: 'save log file',
defaultPath: defaultFileName,
filters: [{ name: 'log file', extensions: ['log', 'txt'] }]
});
const appVersion = app.getVersion();
const platform = process.platform;
const arch = process.arch;
const systemVersion = `${platform}-${arch}`;
const defaultFileName = `eigent-logs-${appVersion}-${systemVersion}-${Date.now()}.zip`;

// Show save dialog
const { canceled, filePath } = await dialog.showSaveDialog({
title: 'Save Log Archive',
defaultPath: defaultFileName,
filters: [{ name: 'ZIP Archive', extensions: ['zip'] }]
});

if (canceled || !filePath) {
return { success: false, error: '' };
}
if (canceled || !filePath) {
return { success: false, error: '' };
}

// Create zip archive
const output = fs.createWriteStream(filePath);
const archive = archiver('zip', {
zlib: { level: 9 } // Maximum compression
});

// Handle archive events
archive.on('error', (err) => {
throw err;
});

archive.on('warning', (err) => {
if (err.code === 'ENOENT') {
log.warn('Archive warning:', err);
} else {
throw err;
}
});

// Pipe archive data to the file
archive.pipe(output);

// Add the target log file to the root of the zip
if (fs.existsSync(targetLogPath)) {
const logFileName = path.basename(targetLogPath);
archive.file(targetLogPath, { name: logFileName });
}

// Add the app log folder contents to the second level
if (appLogPath && fs.existsSync(appLogPath)) {
const appLogStats = await fsp.stat(appLogPath);
if (appLogStats.isDirectory()) {
// Add entire directory to archive with its folder name
const folderName = path.basename(appLogPath);
archive.directory(appLogPath, folderName);
} else if (appLogStats.isFile()) {
// Add single file to archive
const appLogFileName = path.basename(appLogPath);
archive.file(appLogPath, { name: appLogFileName });
}
}

// Finalize the archive
await archive.finalize();

// Wait for the output stream to finish
await new Promise((resolve, reject) => {
output.on('close', () => {
log.info(`Archive created successfully. Total bytes: ${archive.pointer()}`);
resolve(void 0);
});
output.on('error', reject);
});

await fsp.writeFile(filePath, logContent, 'utf-8');
return { success: true, savedPath: filePath };
return { success: true, savedPath: filePath };
} catch (error: any) {
return { success: false, error: error.message };
}
Expand Down Expand Up @@ -545,7 +615,7 @@ function registerIpcHandlers() {

// ==================== window control handler ====================
ipcMain.on('window-close', (_, data) => {
if(data.isForceQuit) {
if (data.isForceQuit) {
return app?.quit()
}
return win?.close()
Expand Down Expand Up @@ -848,9 +918,9 @@ function registerIpcHandlers() {
// ==================== dependency install handler ====================
ipcMain.handle('install-dependencies', async () => {
try {
if(win === null) throw new Error("Window is null");
if (win === null) throw new Error("Window is null");
//Force installation even if versionFile exists
const isInstalled = await checkAndInstallDepsOnUpdate({win, forceInstall: true});
const isInstalled = await checkAndInstallDepsOnUpdate({ win, forceInstall: true });
return { success: true, isInstalled };
} catch (error) {
return { success: false, error: (error as Error).message };
Expand All @@ -869,9 +939,9 @@ function registerIpcHandlers() {
ipcMain.handle('get-installation-status', async () => {
try {
const { isInstalling, hasLockFile } = await getInstallationStatus();
return {
success: true,
isInstalling,
return {
success: true,
isInstalling,
hasLockFile,
timestamp: Date.now()
};
Expand Down Expand Up @@ -1115,7 +1185,7 @@ async function createWindow() {
});

// Now check and install dependencies
let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win });
let res: PromiseReturnType = await checkAndInstallDepsOnUpdate({ win });
if (!res.success) {
log.info("[DEPS INSTALL] Dependency Error: ", res.message);
win.webContents.send('install-dependencies-complete', { success: false, code: 2, error: res.message });
Expand Down Expand Up @@ -1186,15 +1256,14 @@ const checkAndStartBackend = async () => {
if (isToolInstalled.success) {
log.info('Tool installed, starting backend service...');

// Notify frontend installation success
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the fix! a question though:
Looking at startBackend() in init.ts:209, it resolves as soon as it sees "Uvicorn running on":

if (!started && data.toString().includes("Uvicorn running on")) {
    started = true;
    resolve(node_process);  // Resolves immediately
}

There could still be a gap between Uvicorn starting and FastAPI routes being fully loaded. If users submit tasks immediately after installation completes, won't we still hit the race condition from issue #529?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we add a health check poll before notifying the frontend?

if (win && !win.isDestroyed()) {
win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
}

python_process = await startBackend((port) => {
backendPort = port;
log.info('Backend service started successfully', { port });
});
// Notify frontend installation success
if (win && !win.isDestroyed()) {
win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
}

python_process?.on('exit', (code, signal) => {

Expand Down Expand Up @@ -1274,18 +1343,18 @@ const cleanupPythonProcess = async () => {

// before close
const handleBeforeClose = () => {
let isQuitting = false;
app.on('before-quit', () => {
isQuitting = true;
});
win?.on("close", (event) => {
if (!isQuitting) {
event.preventDefault();
win?.webContents.send("before-close");
}
})
let isQuitting = false;

app.on('before-quit', () => {
isQuitting = true;
});

win?.on("close", (event) => {
if (!isQuitting) {
event.preventDefault();
win?.webContents.send("before-close");
}
})
}

// ==================== app event handle ====================
Expand Down Expand Up @@ -1339,15 +1408,15 @@ app.whenReady().then(() => {
// ==================== window close event ====================
app.on('window-all-closed', () => {
log.info('window-all-closed');

// Clean up WebView manager
if (webViewManager) {
webViewManager.destroy();
webViewManager = null;
}

win = null;

if (process.platform !== 'darwin') {
app.quit();
}
Expand All @@ -1370,35 +1439,35 @@ app.on('activate', () => {
app.on('before-quit', async (event) => {
log.info('before-quit');
log.info('quit python_process.pid: ' + python_process?.pid);

// Prevent default quit to ensure cleanup completes
event.preventDefault();

try {
// Clean up resources
if (webViewManager) {
webViewManager.destroy();
webViewManager = null;
}

if (win && !win.isDestroyed()) {
win.destroy();
win = null;
}

// Wait for Python process cleanup
await cleanupPythonProcess();

// Clean up file reader if exists
if (fileReader) {
fileReader = null;
}

// Clear any remaining timeouts/intervals
if (global.gc) {
global.gc();
}

log.info('All cleanup completed, exiting...');
} catch (error) {
log.error('Error during cleanup:', error);
Expand Down
Loading