mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-19 07:59:39 +00:00
1410 lines
45 KiB
TypeScript
1410 lines
45 KiB
TypeScript
import { app, BrowserWindow, shell, ipcMain, Menu, dialog, nativeTheme, protocol, session } from 'electron'
|
|
import { fileURLToPath } from 'node:url'
|
|
import path from 'node:path'
|
|
import os, { homedir } from 'node:os'
|
|
import log from 'electron-log'
|
|
import { update, registerUpdateIpcHandlers } from './update'
|
|
import { checkToolInstalled, killProcessOnPort, startBackend } from './init'
|
|
import { WebViewManager } from './webview'
|
|
import { FileReader } from './fileReader'
|
|
import { ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
import fs, { existsSync, readFileSync } from 'node:fs'
|
|
import fsp from 'fs/promises'
|
|
import { addMcp, removeMcp, updateMcp, readMcpConfig } from './utils/mcpConfig'
|
|
import { getEnvPath, updateEnvBlock, removeEnvKey, getEmailFolderPath } from './utils/envUtil'
|
|
import { copyBrowserData } from './copy'
|
|
import { findAvailablePort } from './init'
|
|
import kill from 'tree-kill';
|
|
import { zipFolder } from './utils/log'
|
|
import axios from 'axios';
|
|
import FormData from 'form-data';
|
|
import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps'
|
|
import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process'
|
|
|
|
const userData = app.getPath('userData');
|
|
|
|
// ==================== constants ====================
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const MAIN_DIST = path.join(__dirname, '../..');
|
|
const RENDERER_DIST = path.join(MAIN_DIST, 'dist');
|
|
const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
|
|
const VITE_PUBLIC = VITE_DEV_SERVER_URL
|
|
? path.join(MAIN_DIST, 'public')
|
|
: RENDERER_DIST;
|
|
|
|
// ==================== global variables ====================
|
|
let win: BrowserWindow | null = null;
|
|
let webViewManager: WebViewManager | null = null;
|
|
let fileReader: FileReader | null = null;
|
|
let python_process: ChildProcessWithoutNullStreams | null = null;
|
|
let backendPort: number = 5001;
|
|
let browser_port = 9222;
|
|
|
|
// ==================== path config ====================
|
|
const preload = path.join(__dirname, '../preload/index.mjs');
|
|
const indexHtml = path.join(RENDERER_DIST, 'index.html');
|
|
const logPath = log.transports.file.getFile().path;
|
|
|
|
// Set remote debugging port
|
|
findAvailablePort(browser_port).then(port => {
|
|
browser_port = port;
|
|
app.commandLine.appendSwitch('remote-debugging-port', port + '');
|
|
});
|
|
|
|
// Memory optimization settings
|
|
app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096');
|
|
app.commandLine.appendSwitch('force-gpu-mem-available-mb', '512');
|
|
app.commandLine.appendSwitch('max_old_space_size', '4096');
|
|
app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
|
|
app.commandLine.appendSwitch('renderer-process-limit', '8');
|
|
|
|
// ==================== app config ====================
|
|
process.env.APP_ROOT = MAIN_DIST;
|
|
process.env.VITE_PUBLIC = VITE_PUBLIC;
|
|
|
|
// Disable system theme
|
|
nativeTheme.themeSource = 'light';
|
|
|
|
// Set log level
|
|
log.transports.console.level = 'info';
|
|
log.transports.file.level = 'info';
|
|
log.transports.console.format = '[{level}]{text}';
|
|
log.transports.file.format = '[{level}]{text}';
|
|
|
|
// Disable GPU Acceleration for Windows 7
|
|
if (os.release().startsWith('6.1')) app.disableHardwareAcceleration()
|
|
|
|
// Set application name for Windows 10+ notifications
|
|
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
|
|
|
|
if (!app.requestSingleInstanceLock()) {
|
|
app.quit()
|
|
process.exit(0)
|
|
}
|
|
|
|
// ==================== protocol config ====================
|
|
const setupProtocolHandlers = () => {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
const isDefault = app.isDefaultProtocolClient('eigent', process.execPath, [path.resolve(process.argv[1])]);
|
|
if (!isDefault) {
|
|
app.setAsDefaultProtocolClient('eigent', process.execPath, [path.resolve(process.argv[1])]);
|
|
}
|
|
} else {
|
|
app.setAsDefaultProtocolClient('eigent');
|
|
}
|
|
};
|
|
|
|
// ==================== protocol url handle ====================
|
|
function handleProtocolUrl(url: string) {
|
|
log.info('enter handleProtocolUrl', url);
|
|
const urlObj = new URL(url);
|
|
const code = urlObj.searchParams.get('code');
|
|
const share_token = urlObj.searchParams.get('share_token');
|
|
|
|
log.info('urlObj', urlObj);
|
|
log.info('code', code);
|
|
log.info('share_token', share_token);
|
|
|
|
if (win && !win.isDestroyed()) {
|
|
log.info('urlObj.pathname', urlObj.pathname);
|
|
|
|
if (urlObj.pathname === '/oauth') {
|
|
log.info('oauth');
|
|
const provider = urlObj.searchParams.get('provider');
|
|
const code = urlObj.searchParams.get('code');
|
|
log.info("protocol oauth", provider, code);
|
|
win.webContents.send('oauth-authorized', { provider, code });
|
|
return;
|
|
}
|
|
|
|
if (code) {
|
|
log.error('protocol code:', code);
|
|
win.webContents.send('auth-code-received', code);
|
|
}
|
|
|
|
if (share_token) {
|
|
win.webContents.send('auth-share-token-received', share_token);
|
|
}
|
|
} else {
|
|
log.error('window not available');
|
|
}
|
|
}
|
|
|
|
// ==================== single instance lock ====================
|
|
const setupSingleInstanceLock = () => {
|
|
const gotLock = app.requestSingleInstanceLock();
|
|
if (!gotLock) {
|
|
log.info("no-lock");
|
|
app.quit();
|
|
} else {
|
|
app.on('second-instance', (event, argv) => {
|
|
log.info("second-instance", argv);
|
|
const url = argv.find(arg => arg.startsWith('eigent://'));
|
|
if (url) handleProtocolUrl(url);
|
|
if (win) win.show();
|
|
});
|
|
|
|
app.on('open-url', (event, url) => {
|
|
log.info("open-url");
|
|
event.preventDefault();
|
|
handleProtocolUrl(url);
|
|
});
|
|
}
|
|
};
|
|
|
|
// ==================== initialize config ====================
|
|
const initializeApp = () => {
|
|
setupProtocolHandlers();
|
|
setupSingleInstanceLock();
|
|
};
|
|
|
|
/**
|
|
* Registers all IPC handlers once when the app starts
|
|
* This prevents "Attempted to register a second handler" errors
|
|
* when windows are reopened
|
|
*/
|
|
// Get backup log path
|
|
const getBackupLogPath = () => {
|
|
const userDataPath = app.getPath('userData')
|
|
return path.join(userDataPath, 'logs', 'main.log')
|
|
}
|
|
// Constants define
|
|
const BROWSER_PATHS = {
|
|
win32: {
|
|
chrome: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
edge: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
firefox: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
|
|
qq: 'C:\\Program Files\\Tencent\\QQBrowser\\QQBrowser.exe',
|
|
'360': path.join(homedir(), 'AppData\\Local\\360Chrome\\Chrome\\Application\\360chrome.exe'),
|
|
arc: path.join(homedir(), 'AppData\\Local\\Arc\\User Data\\Arc.exe'),
|
|
dia: path.join(homedir(), 'AppData\\Local\\Dia\\Application\\dia.exe'),
|
|
fellou: path.join(homedir(), 'AppData\\Local\\Fellou\\Application\\fellou.exe'),
|
|
},
|
|
darwin: {
|
|
chrome: '/Applications/Google Chrome.app',
|
|
edge: '/Applications/Microsoft Edge.app',
|
|
firefox: '/Applications/Firefox.app',
|
|
safari: '/Applications/Safari.app',
|
|
arc: '/Applications/Arc.app',
|
|
dia: '/Applications/Dia.app',
|
|
fellou: '/Applications/Fellou.app',
|
|
},
|
|
} as const;
|
|
|
|
// Tool function
|
|
const getSystemLanguage = async () => {
|
|
const locale = app.getLocale();
|
|
return locale === 'zh-CN' ? 'zh-cn' : 'en';
|
|
};
|
|
|
|
const checkManagerInstance = (manager: any, name: string) => {
|
|
if (!manager) {
|
|
throw new Error(`${name} not initialized`);
|
|
}
|
|
return manager;
|
|
};
|
|
|
|
function registerIpcHandlers() {
|
|
// ==================== basic info handler ====================
|
|
ipcMain.handle('get-browser-port', () => {
|
|
log.info('Starting new task')
|
|
return browser_port
|
|
});
|
|
ipcMain.handle('get-app-version', () => app.getVersion());
|
|
ipcMain.handle('get-backend-port', () => backendPort);
|
|
ipcMain.handle('restart-backend', async () => {
|
|
try {
|
|
if (backendPort) {
|
|
log.info('Restarting backend service...');
|
|
await cleanupPythonProcess();
|
|
await checkAndStartBackend();
|
|
log.info('Backend restart completed successfully');
|
|
return { success: true };
|
|
} else {
|
|
log.warn('No backend port found, starting fresh backend');
|
|
await checkAndStartBackend();
|
|
return { success: true };
|
|
}
|
|
} catch (error) {
|
|
log.error('Failed to restart backend:', error);
|
|
return { success: false, error: String(error) };
|
|
}
|
|
});
|
|
ipcMain.handle('get-system-language', getSystemLanguage);
|
|
ipcMain.handle('is-fullscreen', () => win?.isFullScreen() || false);
|
|
ipcMain.handle('get-home-dir', () => {
|
|
const platform = process.platform;
|
|
return platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
|
|
});
|
|
|
|
|
|
// ==================== command execution handler ====================
|
|
ipcMain.handle('get-email-folder-path', async (event, email: string) => {
|
|
return getEmailFolderPath(email);
|
|
});
|
|
ipcMain.handle('execute-command', async (event, command: string, email: string) => {
|
|
log.info("execute-command", command);
|
|
const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email);
|
|
|
|
try {
|
|
const { spawn } = await import('child_process');
|
|
|
|
// Add --host parameter
|
|
const commandWithHost = `${command} --debug --host dev.eigent.ai/api/oauth/notion/callback?code=1`;
|
|
// const commandWithHost = `${command}`;
|
|
|
|
log.info(' start execute command:', commandWithHost);
|
|
|
|
// Parse command and arguments
|
|
const [cmd, ...args] = commandWithHost.split(' ');
|
|
log.info('start execute command:', commandWithHost.split(' '));
|
|
console.log(cmd, args)
|
|
return new Promise((resolve) => {
|
|
const child = spawn(cmd, args, {
|
|
cwd: process.cwd(),
|
|
env: { ...process.env, MCP_REMOTE_CONFIG_DIR },
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
// Realtime listen standard output
|
|
child.stdout.on('data', (data) => {
|
|
const output = data.toString();
|
|
stdout += output;
|
|
log.info('Real-time output:', output.trim());
|
|
});
|
|
|
|
// Realtime listen error output
|
|
child.stderr.on('data', (data) => {
|
|
const output = data.toString();
|
|
stderr += output;
|
|
if (output.includes('OAuth callback server running at')) {
|
|
const url = output.split('OAuth callback server running at')[1].trim();
|
|
log.info('detect OAuth callback URL:', url);
|
|
|
|
// Notify frontend to callback URL
|
|
if (win && !win.isDestroyed()) {
|
|
const match = url.match(/^https?:\/\/[^:\n]+:\d+/);
|
|
const cleanedUrl = match ? match[0] : null;
|
|
log.info('cleanedUrl', cleanedUrl);
|
|
win.webContents.send('oauth-callback-url', {
|
|
url: cleanedUrl,
|
|
provider: 'notion' // TODO: can be set dynamically according to actual situation
|
|
});
|
|
|
|
}
|
|
}
|
|
if (output.includes('Press Ctrl+C to exit')) {
|
|
child.kill();
|
|
}
|
|
log.info(' real-time error output:', output.trim());
|
|
});
|
|
|
|
// Listen process exit
|
|
child.on('close', (code) => {
|
|
log.info(` command execute complete, exit code: ${code}`);
|
|
resolve({ success: code === null, stdout, stderr });
|
|
});
|
|
|
|
// Listen process error
|
|
child.on('error', (error) => {
|
|
log.error(' command execute error:', error);
|
|
resolve({ success: false, error: error.message });
|
|
});
|
|
});
|
|
} catch (error: any) {
|
|
log.error(' command execute failed:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
});
|
|
|
|
// ==================== log export handler ====================
|
|
ipcMain.handle('export-log', async () => {
|
|
try {
|
|
let targetLogPath = logPath;
|
|
if (!fs.existsSync(targetLogPath)) {
|
|
const backupPath = getBackupLogPath();
|
|
if (fs.existsSync(backupPath)) {
|
|
targetLogPath = backupPath;
|
|
} else {
|
|
return { success: false, error: 'no log file' };
|
|
}
|
|
}
|
|
|
|
await fsp.access(targetLogPath, fs.constants.R_OK);
|
|
const stats = await fsp.stat(targetLogPath);
|
|
if (stats.size === 0) {
|
|
return { success: true, data: 'log file is empty' };
|
|
}
|
|
|
|
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'] }]
|
|
});
|
|
|
|
if (canceled || !filePath) {
|
|
return { success: false, error: '' };
|
|
}
|
|
|
|
await fsp.writeFile(filePath, logContent, 'utf-8');
|
|
return { success: true, savedPath: filePath };
|
|
} catch (error: any) {
|
|
return { success: false, error: error.message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('upload-log', async (event, email: string, taskId: string, baseUrl: string, token: string) => {
|
|
let zipPath: string | null = null;
|
|
|
|
try {
|
|
// Validate required parameters
|
|
if (!email || !taskId || !baseUrl || !token) {
|
|
return { success: false, error: 'Missing required parameters' };
|
|
}
|
|
|
|
// Sanitize taskId to prevent path traversal attacks
|
|
const sanitizedTaskId = taskId.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
if (!sanitizedTaskId) {
|
|
return { success: false, error: 'Invalid task ID' };
|
|
}
|
|
|
|
const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email);
|
|
const logFolderName = `task_${sanitizedTaskId}`;
|
|
const logFolderPath = path.join(MCP_REMOTE_CONFIG_DIR, logFolderName);
|
|
|
|
// Check if log folder exists
|
|
if (!fs.existsSync(logFolderPath)) {
|
|
return { success: false, error: 'Log folder not found' };
|
|
}
|
|
|
|
zipPath = path.join(MCP_REMOTE_CONFIG_DIR, `${logFolderName}.zip`);
|
|
await zipFolder(logFolderPath, zipPath);
|
|
|
|
// Create form data with file stream
|
|
const formData = new FormData();
|
|
const fileStream = fs.createReadStream(zipPath);
|
|
formData.append('file', fileStream);
|
|
formData.append('task_id', sanitizedTaskId);
|
|
|
|
// Upload with timeout
|
|
const response = await axios.post(baseUrl + '/api/chat/logs', formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
timeout: 60000, // 60 second timeout
|
|
maxContentLength: Infinity,
|
|
maxBodyLength: Infinity
|
|
});
|
|
|
|
fileStream.destroy();
|
|
|
|
if (response.status === 200) {
|
|
return { success: true, data: response.data };
|
|
} else {
|
|
return { success: false, error: response.data };
|
|
}
|
|
} catch (error: any) {
|
|
log.error('Failed to upload log:', error);
|
|
return { success: false, error: error.message || 'Upload failed' };
|
|
} finally {
|
|
// Clean up zip file
|
|
if (zipPath && fs.existsSync(zipPath)) {
|
|
try {
|
|
fs.unlinkSync(zipPath);
|
|
} catch (cleanupError) {
|
|
log.error('Failed to clean up zip file:', cleanupError);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// ==================== MCP manage handler ====================
|
|
ipcMain.handle('mcp-install', async (event, name, mcp) => {
|
|
// Convert args from JSON string to array if needed
|
|
if (mcp.args && typeof mcp.args === 'string') {
|
|
try {
|
|
mcp.args = JSON.parse(mcp.args);
|
|
} catch (e) {
|
|
// If parsing fails, split by comma as fallback
|
|
mcp.args = mcp.args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== '');
|
|
}
|
|
}
|
|
addMcp(name, mcp);
|
|
return { success: true };
|
|
});
|
|
|
|
ipcMain.handle('mcp-remove', async (event, name) => {
|
|
removeMcp(name);
|
|
return { success: true };
|
|
});
|
|
|
|
ipcMain.handle('mcp-update', async (event, name, mcp) => {
|
|
// Convert args from JSON string to array if needed
|
|
if (mcp.args && typeof mcp.args === 'string') {
|
|
try {
|
|
mcp.args = JSON.parse(mcp.args);
|
|
} catch (e) {
|
|
// If parsing fails, split by comma as fallback
|
|
mcp.args = mcp.args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== '');
|
|
}
|
|
}
|
|
updateMcp(name, mcp);
|
|
return { success: true };
|
|
});
|
|
|
|
ipcMain.handle('mcp-list', async () => {
|
|
return readMcpConfig();
|
|
});
|
|
|
|
// ==================== browser related handler ====================
|
|
// TODO: next version implement
|
|
ipcMain.handle('check-install-browser', async () => {
|
|
try {
|
|
const platform = process.platform;
|
|
const results: Record<string, boolean> = {};
|
|
const paths = BROWSER_PATHS[platform as keyof typeof BROWSER_PATHS];
|
|
|
|
if (!paths) {
|
|
log.warn(`not support current platform: ${platform}`);
|
|
return {};
|
|
}
|
|
|
|
for (const [browser, execPath] of Object.entries(paths)) {
|
|
results[browser] = existsSync(execPath);
|
|
}
|
|
|
|
return results;
|
|
} catch (error: any) {
|
|
log.error('Failed to check browser installation:', error);
|
|
return {};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('start-browser-import', async (event, args) => {
|
|
const isWin = process.platform === 'win32';
|
|
const localAppData = process.env.LOCALAPPDATA || '';
|
|
const appData = process.env.APPDATA || '';
|
|
const home = os.homedir();
|
|
|
|
const candidates: Record<string, string> = {
|
|
chrome: isWin
|
|
? `${localAppData}\\Google\\Chrome\\User Data\\Default`
|
|
: `${home}/Library/Application Support/Google/Chrome/Default`,
|
|
edge: isWin
|
|
? `${localAppData}\\Microsoft\\Edge\\User Data\\Default`
|
|
: `${home}/Library/Application Support/Microsoft Edge/Default`,
|
|
firefox: isWin
|
|
? `${appData}\\Mozilla\\Firefox\\Profiles`
|
|
: `${home}/Library/Application Support/Firefox/Profiles`,
|
|
qq: `${localAppData}\\Tencent\\QQBrowser\\User Data\\Default`,
|
|
'360': `${localAppData}\\360Chrome\\Chrome\\User Data\\Default`,
|
|
arc: isWin
|
|
? `${localAppData}\\Arc\\User Data\\Default`
|
|
: `${home}/Library/Application Support/Arc/Default`,
|
|
dia: `${localAppData}\\Dia\\User Data\\Default`,
|
|
fellou: `${localAppData}\\Fellou\\User Data\\Default`,
|
|
safari: `${home}/Library/Safari`,
|
|
};
|
|
|
|
// Filter unchecked browser
|
|
Object.keys(candidates).forEach((key) => {
|
|
const browser = args.find((item: any) => item.browserId === key);
|
|
if (!browser || !browser.checked) {
|
|
delete candidates[key];
|
|
}
|
|
});
|
|
|
|
const result: Record<string, string | null> = {};
|
|
for (const [name, p] of Object.entries(candidates)) {
|
|
result[name] = fs.existsSync(p) ? p : null;
|
|
}
|
|
|
|
const electronUserDataPath = app.getPath('userData');
|
|
|
|
for (const [browserName, browserPath] of Object.entries(result)) {
|
|
if (!browserPath) continue;
|
|
await copyBrowserData(browserName, browserPath, electronUserDataPath);
|
|
}
|
|
|
|
return { success: true };
|
|
});
|
|
|
|
// ==================== window control handler ====================
|
|
ipcMain.on('window-close', (_, data) => {
|
|
if(data.isForceQuit) {
|
|
return app?.quit()
|
|
}
|
|
return win?.close()
|
|
});
|
|
ipcMain.on('window-minimize', () => win?.minimize());
|
|
ipcMain.on('window-toggle-maximize', () => {
|
|
if (win?.isMaximized()) {
|
|
win?.unmaximize();
|
|
} else {
|
|
win?.maximize();
|
|
}
|
|
});
|
|
|
|
// ==================== file operation handler ====================
|
|
ipcMain.handle('select-file', async (event, options = {}) => {
|
|
const result = await dialog.showOpenDialog(win!, {
|
|
properties: ['openFile', 'multiSelections'],
|
|
...options
|
|
});
|
|
|
|
if (!result.canceled && result.filePaths.length > 0) {
|
|
const files = result.filePaths.map(filePath => ({
|
|
filePath,
|
|
fileName: filePath.split(/[/\\]/).pop() || ''
|
|
}));
|
|
|
|
return {
|
|
success: true,
|
|
files,
|
|
fileCount: files.length
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
canceled: result.canceled
|
|
};
|
|
});
|
|
|
|
ipcMain.handle("reveal-in-folder", async (event, filePath: string) => {
|
|
try {
|
|
const stats = await fs.promises.stat(filePath.replace(/\/$/, '')).catch(() => null);
|
|
if (stats && stats.isDirectory()) {
|
|
shell.openPath(filePath);
|
|
} else {
|
|
shell.showItemInFolder(filePath);
|
|
}
|
|
} catch (e) {
|
|
log.error("reveal in folder failed", e);
|
|
}
|
|
});
|
|
|
|
// ==================== read file handler ====================
|
|
ipcMain.handle('read-file', async (event, filePath: string) => {
|
|
try {
|
|
log.info('Reading file:', filePath);
|
|
|
|
// Check if file exists
|
|
if (!fs.existsSync(filePath)) {
|
|
log.error('File does not exist:', filePath);
|
|
return { success: false, error: 'File does not exist' };
|
|
}
|
|
|
|
// Check if it's a directory
|
|
const stats = await fsp.stat(filePath);
|
|
if (stats.isDirectory()) {
|
|
log.error('Path is a directory, not a file:', filePath);
|
|
return { success: false, error: 'EISDIR: illegal operation on a directory, read' };
|
|
}
|
|
|
|
// Read file content
|
|
const fileContent = await fsp.readFile(filePath);
|
|
log.info('File read successfully:', filePath);
|
|
|
|
return {
|
|
success: true,
|
|
data: fileContent,
|
|
size: fileContent.length
|
|
};
|
|
} catch (error: any) {
|
|
log.error('Failed to read file:', filePath, error);
|
|
return {
|
|
success: false,
|
|
error: error.message || 'Failed to read file'
|
|
};
|
|
}
|
|
});
|
|
|
|
// ==================== delete folder handler ====================
|
|
ipcMain.handle('delete-folder', async (event, email: string) => {
|
|
const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email);
|
|
try {
|
|
log.info('Deleting folder:', MCP_REMOTE_CONFIG_DIR);
|
|
|
|
// Check if folder exists
|
|
if (!fs.existsSync(MCP_REMOTE_CONFIG_DIR)) {
|
|
log.error('Folder does not exist:', MCP_REMOTE_CONFIG_DIR);
|
|
return { success: false, error: 'Folder does not exist' };
|
|
}
|
|
|
|
// Check if it's actually a directory
|
|
const stats = await fsp.stat(MCP_REMOTE_CONFIG_DIR);
|
|
if (!stats.isDirectory()) {
|
|
log.error('Path is not a directory:', MCP_REMOTE_CONFIG_DIR);
|
|
return { success: false, error: 'Path is not a directory' };
|
|
}
|
|
|
|
// Delete folder recursively
|
|
await fsp.rm(MCP_REMOTE_CONFIG_DIR, { recursive: true, force: true });
|
|
log.info('Folder deleted successfully:', MCP_REMOTE_CONFIG_DIR);
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Folder deleted successfully'
|
|
};
|
|
} catch (error: any) {
|
|
log.error('Failed to delete folder:', MCP_REMOTE_CONFIG_DIR, error);
|
|
return {
|
|
success: false,
|
|
error: error.message || 'Failed to delete folder'
|
|
};
|
|
}
|
|
});
|
|
|
|
// ==================== get MCP config path handler ====================
|
|
ipcMain.handle('get-mcp-config-path', async (event, email: string) => {
|
|
try {
|
|
const { MCP_REMOTE_CONFIG_DIR, tempEmail } = getEmailFolderPath(email);
|
|
log.info('Getting MCP config path for email:', email);
|
|
log.info('MCP config path:', MCP_REMOTE_CONFIG_DIR);
|
|
return {
|
|
success: MCP_REMOTE_CONFIG_DIR,
|
|
path: MCP_REMOTE_CONFIG_DIR,
|
|
tempEmail: tempEmail,
|
|
};
|
|
} catch (error: any) {
|
|
log.error('Failed to get MCP config path:', error);
|
|
return {
|
|
success: false,
|
|
error: error.message || 'Failed to get MCP config path'
|
|
};
|
|
}
|
|
});
|
|
|
|
// ==================== env handler ====================
|
|
|
|
ipcMain.handle('get-env-path', async (_event, email) => {
|
|
return getEnvPath(email);
|
|
});
|
|
|
|
ipcMain.handle('get-env-has-key', async (_event, email, key) => {
|
|
const ENV_PATH = getEnvPath(email);
|
|
let content = '';
|
|
try {
|
|
content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : '';
|
|
} catch (error) {
|
|
log.error("env-remove error:", error);
|
|
}
|
|
let lines = content.split(/\r?\n/);
|
|
return { success: lines.some(line => line.startsWith(key + '=')) };
|
|
});
|
|
|
|
ipcMain.handle('env-write', async (_event, email, { key, value }) => {
|
|
const ENV_PATH = getEnvPath(email);
|
|
let content = '';
|
|
try {
|
|
content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : '';
|
|
} catch (error) {
|
|
log.error("env-write error:", error);
|
|
}
|
|
let lines = content.split(/\r?\n/);
|
|
lines = updateEnvBlock(lines, { [key]: value });
|
|
fs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8');
|
|
return { success: true };
|
|
});
|
|
|
|
ipcMain.handle('env-remove', async (_event, email, key) => {
|
|
log.info("env-remove", key);
|
|
const ENV_PATH = getEnvPath(email);
|
|
let content = '';
|
|
try {
|
|
content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : '';
|
|
} catch (error) {
|
|
log.error("env-remove error:", error);
|
|
}
|
|
let lines = content.split(/\r?\n/);
|
|
lines = removeEnvKey(lines, key);
|
|
fs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8');
|
|
log.info("env-remove success", ENV_PATH);
|
|
return { success: true };
|
|
});
|
|
|
|
// ==================== new window handler ====================
|
|
ipcMain.handle('open-win', (_, arg) => {
|
|
const childWindow = new BrowserWindow({
|
|
webPreferences: {
|
|
preload,
|
|
nodeIntegration: true,
|
|
contextIsolation: false,
|
|
},
|
|
});
|
|
|
|
if (VITE_DEV_SERVER_URL) {
|
|
childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`);
|
|
} else {
|
|
childWindow.loadFile(indexHtml, { hash: arg });
|
|
}
|
|
});
|
|
|
|
// ==================== FileReader handler ====================
|
|
ipcMain.handle('open-file', async (_, type: string, filePath: string, isShowSourceCode: boolean) => {
|
|
const manager = checkManagerInstance(fileReader, 'FileReader');
|
|
return manager.openFile(type, filePath, isShowSourceCode);
|
|
});
|
|
|
|
ipcMain.handle('download-file', async (_, url: string) => {
|
|
try {
|
|
const https = await import('https');
|
|
const http = await import('http');
|
|
|
|
// extract file name from URL
|
|
const urlObj = new URL(url);
|
|
const fileName = urlObj.pathname.split('/').pop() || 'download';
|
|
|
|
// get download directory
|
|
const downloadPath = path.join(app.getPath('downloads'), fileName);
|
|
|
|
// create write stream
|
|
const fileStream = fs.createWriteStream(downloadPath);
|
|
|
|
// choose module according to protocol
|
|
const client = url.startsWith('https:') ? https : http;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = client.get(url, (response) => {
|
|
if (response.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${response.statusCode}`));
|
|
return;
|
|
}
|
|
|
|
response.pipe(fileStream);
|
|
|
|
fileStream.on('finish', () => {
|
|
fileStream.close();
|
|
shell.showItemInFolder(downloadPath);
|
|
resolve({ success: true, path: downloadPath });
|
|
});
|
|
|
|
fileStream.on('error', (err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
|
|
request.on('error', (err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
} catch (error: any) {
|
|
log.error('Download file error:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('get-file-list', async (_, email: string, taskId: string) => {
|
|
const manager = checkManagerInstance(fileReader, 'FileReader');
|
|
return manager.getFileList(email, taskId);
|
|
});
|
|
|
|
ipcMain.handle('delete-task-files', async (_, email: string, taskId: string) => {
|
|
const manager = checkManagerInstance(fileReader, 'FileReader');
|
|
return manager.deleteTaskFiles(email, taskId);
|
|
});
|
|
|
|
ipcMain.handle('get-log-folder', async (_, email: string) => {
|
|
const manager = checkManagerInstance(fileReader, 'FileReader');
|
|
return manager.getLogFolder(email);
|
|
});
|
|
|
|
// ==================== WebView handler ====================
|
|
const webviewHandlers = [
|
|
{ name: 'capture-webview', method: 'captureWebview' },
|
|
{ name: 'create-webview', method: 'createWebview' },
|
|
{ name: 'hide-webview', method: 'hideWebview' },
|
|
{ name: 'show-webview', method: 'showWebview' },
|
|
{ name: 'change-view-size', method: 'changeViewSize' },
|
|
{ name: 'hide-all-webview', method: 'hideAllWebview' },
|
|
{ name: 'get-active-webview', method: 'getActiveWebview' },
|
|
{ name: 'set-size', method: 'setSize' },
|
|
{ name: 'get-show-webview', method: 'getShowWebview' },
|
|
{ name: 'webview-destroy', method: 'destroyWebview' },
|
|
];
|
|
|
|
webviewHandlers.forEach(({ name, method }) => {
|
|
ipcMain.handle(name, async (_, ...args) => {
|
|
const manager = checkManagerInstance(webViewManager, 'WebViewManager');
|
|
return manager[method as keyof typeof manager](...args);
|
|
});
|
|
});
|
|
|
|
// ==================== dependency install handler ====================
|
|
ipcMain.handle('install-dependencies', async () => {
|
|
try {
|
|
if(win === null) throw new Error("Window is null");
|
|
//Force installation even if versionFile exists
|
|
const isInstalled = await checkAndInstallDepsOnUpdate({win, forceInstall: true});
|
|
return { success: true, isInstalled };
|
|
} catch (error) {
|
|
return { success: false, error: (error as Error).message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('check-tool-installed', async () => {
|
|
try {
|
|
const isInstalled = await checkToolInstalled();
|
|
return { success: true, isInstalled: isInstalled.success };
|
|
} catch (error) {
|
|
return { success: false, error: (error as Error).message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('get-installation-status', async () => {
|
|
try {
|
|
const { isInstalling, hasLockFile } = await getInstallationStatus();
|
|
return {
|
|
success: true,
|
|
isInstalling,
|
|
hasLockFile,
|
|
timestamp: Date.now()
|
|
};
|
|
} catch (error) {
|
|
return { success: false, error: (error as Error).message };
|
|
}
|
|
});
|
|
|
|
// ==================== register update related handler ====================
|
|
registerUpdateIpcHandlers();
|
|
}
|
|
|
|
// ==================== ensure eigent directories ====================
|
|
const ensureEigentDirectories = () => {
|
|
const eigentBase = path.join(os.homedir(), '.eigent');
|
|
const requiredDirs = [
|
|
eigentBase,
|
|
path.join(eigentBase, 'bin'),
|
|
path.join(eigentBase, 'cache'),
|
|
path.join(eigentBase, 'venvs'),
|
|
path.join(eigentBase, 'runtime'),
|
|
];
|
|
|
|
for (const dir of requiredDirs) {
|
|
if (!fs.existsSync(dir)) {
|
|
log.info(`Creating directory: ${dir}`);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
log.info('.eigent directory structure ensured');
|
|
};
|
|
|
|
// ==================== window create ====================
|
|
async function createWindow() {
|
|
const isMac = process.platform === 'darwin';
|
|
|
|
// Ensure .eigent directories exist before anything else
|
|
ensureEigentDirectories();
|
|
|
|
win = new BrowserWindow({
|
|
title: 'Eigent',
|
|
width: 1200,
|
|
height: 800,
|
|
minWidth: 1050,
|
|
minHeight: 650,
|
|
frame: false,
|
|
transparent: true,
|
|
vibrancy: 'sidebar',
|
|
visualEffectState: 'active',
|
|
backgroundColor: '#00000000',
|
|
titleBarStyle: isMac ? 'hidden' : undefined,
|
|
trafficLightPosition: isMac ? { x: 10, y: 10 } : undefined,
|
|
icon: path.join(VITE_PUBLIC, 'favicon.ico'),
|
|
roundedCorners: true,
|
|
webPreferences: {
|
|
webSecurity: false,
|
|
preload,
|
|
nodeIntegration: true,
|
|
contextIsolation: true,
|
|
webviewTag: true,
|
|
spellcheck: false,
|
|
},
|
|
});
|
|
|
|
// ==================== initialize manager ====================
|
|
fileReader = new FileReader(win);
|
|
webViewManager = new WebViewManager(win);
|
|
|
|
// create initial webviews (reduced from 8 to 3)
|
|
for (let i = 1; i <= 3; i++) {
|
|
webViewManager.createWebview(i === 1 ? undefined : i.toString());
|
|
}
|
|
|
|
// ==================== set event listeners ====================
|
|
setupWindowEventListeners();
|
|
setupDevToolsShortcuts();
|
|
setupExternalLinkHandling();
|
|
handleBeforeClose();
|
|
|
|
// ==================== auto update ====================
|
|
update(win);
|
|
|
|
// ==================== CHECK IF INSTALLATION IS NEEDED BEFORE LOADING CONTENT ====================
|
|
log.info('Pre-checking if dependencies need to be installed...');
|
|
|
|
// Check version and tools status synchronously
|
|
const currentVersion = app.getVersion();
|
|
const versionFile = path.join(app.getPath('userData'), 'version.txt');
|
|
const versionExists = fs.existsSync(versionFile);
|
|
let savedVersion = '';
|
|
if (versionExists) {
|
|
savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
|
|
}
|
|
|
|
const uvExists = await isBinaryExists('uv');
|
|
const bunExists = await isBinaryExists('bun');
|
|
|
|
// Check if installation was previously completed
|
|
const backendPath = getBackendPath();
|
|
const installedLockPath = path.join(backendPath, 'uv_installed.lock');
|
|
const installationCompleted = fs.existsSync(installedLockPath);
|
|
|
|
// Check if venv path exists for current version
|
|
const venvPath = getVenvPath(currentVersion);
|
|
const venvExists = fs.existsSync(venvPath);
|
|
|
|
const needsInstallation = !versionExists || savedVersion !== currentVersion || !uvExists || !bunExists || !installationCompleted || !venvExists;
|
|
|
|
log.info('Installation check result:', {
|
|
needsInstallation,
|
|
versionExists,
|
|
versionMatch: savedVersion === currentVersion,
|
|
uvExists,
|
|
bunExists,
|
|
installationCompleted,
|
|
venvExists,
|
|
venvPath
|
|
});
|
|
|
|
// Handle localStorage based on installation state
|
|
if (needsInstallation) {
|
|
log.info('Installation needed - clearing auth storage to force carousel state');
|
|
|
|
// Clear the persisted auth storage file to force fresh initialization with carousel
|
|
const localStoragePath = path.join(app.getPath('userData'), 'Local Storage');
|
|
const leveldbPath = path.join(localStoragePath, 'leveldb');
|
|
|
|
try {
|
|
// Delete the localStorage database to force fresh init
|
|
if (fs.existsSync(leveldbPath)) {
|
|
log.info('Removing localStorage database to force fresh state...');
|
|
fs.rmSync(leveldbPath, { recursive: true, force: true });
|
|
log.info('Successfully cleared localStorage');
|
|
}
|
|
} catch (error) {
|
|
log.error('Error clearing localStorage:', error);
|
|
}
|
|
|
|
// Set up the injection for when page loads
|
|
win.webContents.once('dom-ready', () => {
|
|
if (!win || win.isDestroyed()) {
|
|
log.warn('Window destroyed before DOM ready - skipping localStorage injection');
|
|
return;
|
|
}
|
|
log.info('DOM ready - creating auth-storage with carousel state');
|
|
win.webContents.executeJavaScript(`
|
|
(function() {
|
|
try {
|
|
// Create fresh auth storage with carousel state
|
|
const newAuthStorage = {
|
|
state: {
|
|
token: null,
|
|
username: null,
|
|
email: null,
|
|
user_id: null,
|
|
appearance: 'light',
|
|
language: 'system',
|
|
isFirstLaunch: true,
|
|
modelType: 'cloud',
|
|
cloud_model_type: 'gpt-4.1',
|
|
initState: 'carousel',
|
|
share_token: null,
|
|
workerListData: {}
|
|
},
|
|
version: 0
|
|
};
|
|
localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
|
|
console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
|
|
} catch (e) {
|
|
console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
|
|
}
|
|
})();
|
|
`).catch(err => {
|
|
log.error('Failed to inject script:', err);
|
|
});
|
|
});
|
|
} else {
|
|
// Installation is complete - ensure initState is set to 'done'
|
|
log.info('Installation already complete - ensuring initState is done');
|
|
|
|
win.webContents.once('dom-ready', () => {
|
|
if (!win || win.isDestroyed()) {
|
|
log.warn('Window destroyed before DOM ready - skipping localStorage update');
|
|
return;
|
|
}
|
|
log.info('DOM ready - checking and updating auth-storage to done state');
|
|
win.webContents.executeJavaScript(`
|
|
(function() {
|
|
try {
|
|
const authStorage = localStorage.getItem('auth-storage');
|
|
if (authStorage) {
|
|
const parsed = JSON.parse(authStorage);
|
|
if (parsed.state && parsed.state.initState !== 'done') {
|
|
console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done');
|
|
// Only update the initState field, preserve all other data
|
|
const updatedStorage = {
|
|
...parsed,
|
|
state: {
|
|
...parsed.state,
|
|
initState: 'done'
|
|
}
|
|
};
|
|
localStorage.setItem('auth-storage', JSON.stringify(updatedStorage));
|
|
console.log('[ELECTRON] initState updated to done, reloading page...');
|
|
return true; // Signal that we need to reload
|
|
}
|
|
}
|
|
return false; // No reload needed
|
|
} catch (e) {
|
|
console.error('[ELECTRON] Failed to update initState:', e);
|
|
// Don't modify localStorage if there's an error to prevent data corruption
|
|
return false;
|
|
}
|
|
})();
|
|
`).then(needsReload => {
|
|
if (needsReload) {
|
|
log.info('Reloading window after localStorage update');
|
|
win!.reload();
|
|
}
|
|
}).catch(err => {
|
|
log.error('Failed to inject script:', err);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Load content
|
|
if (VITE_DEV_SERVER_URL) {
|
|
win.loadURL(VITE_DEV_SERVER_URL);
|
|
win.webContents.openDevTools();
|
|
} else {
|
|
win.loadFile(indexHtml);
|
|
}
|
|
|
|
// Wait for window to be ready
|
|
await new Promise<void>(resolve => {
|
|
win!.webContents.once('did-finish-load', () => {
|
|
log.info('Window content loaded, starting dependency check immediately...');
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Now check and install dependencies
|
|
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 });
|
|
return;
|
|
}
|
|
log.info("[DEPS INSTALL] Dependency Success: ", res.message);
|
|
|
|
// Start backend after dependencies are ready
|
|
await checkAndStartBackend();
|
|
}
|
|
|
|
// ==================== window event listeners ====================
|
|
const setupWindowEventListeners = () => {
|
|
if (!win) return;
|
|
|
|
// close default menu
|
|
Menu.setApplicationMenu(null);
|
|
};
|
|
|
|
// ==================== devtools shortcuts ====================
|
|
const setupDevToolsShortcuts = () => {
|
|
if (!win) return;
|
|
|
|
const toggleDevTools = () => win?.webContents.toggleDevTools();
|
|
|
|
win.webContents.on('before-input-event', (event, input) => {
|
|
// F12 key
|
|
if (input.key === 'F12' && input.type === 'keyDown') {
|
|
toggleDevTools();
|
|
}
|
|
|
|
// Ctrl+Shift+I (Windows/Linux) or Cmd+Shift+I (Mac)
|
|
if (input.control && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') {
|
|
toggleDevTools();
|
|
}
|
|
|
|
// Mac Cmd+Shift+I
|
|
if (input.meta && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') {
|
|
toggleDevTools();
|
|
}
|
|
});
|
|
};
|
|
|
|
// ==================== external link handle ====================
|
|
const setupExternalLinkHandling = () => {
|
|
if (!win) return;
|
|
|
|
// handle new window open
|
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
if (url.startsWith('https:') || url.startsWith('http:')) {
|
|
shell.openExternal(url);
|
|
}
|
|
return { action: 'deny' };
|
|
});
|
|
|
|
// handle navigation
|
|
win.webContents.on('will-navigate', (event, url) => {
|
|
event.preventDefault();
|
|
shell.openExternal(url);
|
|
});
|
|
};
|
|
|
|
// ==================== check and start backend ====================
|
|
const checkAndStartBackend = async () => {
|
|
log.info('Checking and starting backend service...');
|
|
try {
|
|
const isToolInstalled = await checkToolInstalled();
|
|
if (isToolInstalled.success) {
|
|
log.info('Tool installed, starting backend service...');
|
|
|
|
// Notify frontend installation success
|
|
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 });
|
|
});
|
|
|
|
python_process?.on('exit', (code, signal) => {
|
|
|
|
log.info('Python process exited', { code, signal });
|
|
});
|
|
} else {
|
|
log.warn('Tool not installed, cannot start backend service');
|
|
}
|
|
} catch (error) {
|
|
log.debug("Cannot Start Backend due to ", error)
|
|
}
|
|
};
|
|
|
|
// ==================== process cleanup ====================
|
|
const cleanupPythonProcess = async () => {
|
|
try {
|
|
// First attempt: Try to kill using PID
|
|
if (python_process?.pid) {
|
|
const pid = python_process.pid;
|
|
log.info('Cleaning up Python process', { pid });
|
|
|
|
// Remove all listeners to prevent memory leaks
|
|
python_process.removeAllListeners();
|
|
|
|
await new Promise<void>((resolve) => {
|
|
kill(pid, 'SIGTERM', (err) => {
|
|
if (err) {
|
|
log.error('Failed to clean up process tree with SIGTERM:', err);
|
|
// Try SIGKILL as fallback
|
|
kill(pid, 'SIGKILL', (killErr) => {
|
|
if (killErr) {
|
|
log.error('Failed to force kill process tree:', killErr);
|
|
}
|
|
resolve();
|
|
});
|
|
} else {
|
|
log.info('Successfully cleaned up Python process tree');
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Second attempt: Use port-based cleanup as fallback
|
|
const portFile = path.join(userData, 'port.txt');
|
|
if (fs.existsSync(portFile)) {
|
|
try {
|
|
const port = parseInt(fs.readFileSync(portFile, 'utf-8').trim(), 10);
|
|
if (!isNaN(port) && port > 0 && port < 65536) {
|
|
log.info(`Attempting to kill process on port: ${port}`);
|
|
await killProcessOnPort(port);
|
|
}
|
|
fs.unlinkSync(portFile);
|
|
} catch (error) {
|
|
log.error('Error handling port file:', error);
|
|
}
|
|
}
|
|
|
|
// Clean up any temporary files in userData
|
|
try {
|
|
const tempFiles = ['backend.lock', 'uv_installing.lock'];
|
|
for (const file of tempFiles) {
|
|
const filePath = path.join(userData, file);
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
log.error('Error cleaning up temp files:', error);
|
|
}
|
|
|
|
python_process = null;
|
|
} catch (error) {
|
|
log.error('Error occurred while cleaning up process:', error);
|
|
}
|
|
};
|
|
|
|
// 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");
|
|
}
|
|
})
|
|
}
|
|
|
|
// ==================== app event handle ====================
|
|
app.whenReady().then(() => {
|
|
|
|
// ==================== download handle ====================
|
|
session.defaultSession.on('will-download', (event, item, webContents) => {
|
|
item.once('done', (event, state) => {
|
|
shell.showItemInFolder(item.getURL().replace('localfile://', ''));
|
|
});
|
|
});
|
|
|
|
// ==================== protocol handle ====================
|
|
protocol.handle('localfile', async (request) => {
|
|
const url = decodeURIComponent(request.url.replace('localfile://', ''));
|
|
const filePath = path.normalize(url);
|
|
|
|
try {
|
|
const data = await fsp.readFile(filePath);
|
|
|
|
// set correct Content-Type according to file extension
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
let contentType = 'application/octet-stream';
|
|
|
|
switch (ext) {
|
|
case '.pdf':
|
|
contentType = 'application/pdf';
|
|
break;
|
|
case '.html':
|
|
case '.htm':
|
|
contentType = 'text/html';
|
|
break;
|
|
}
|
|
|
|
return new Response(new Uint8Array(data), {
|
|
headers: {
|
|
'Content-Type': contentType,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
return new Response('Not Found', { status: 404 });
|
|
}
|
|
});
|
|
|
|
// ==================== initialize app ====================
|
|
initializeApp();
|
|
registerIpcHandlers();
|
|
createWindow();
|
|
});
|
|
|
|
// ==================== 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();
|
|
}
|
|
});
|
|
|
|
// ==================== app activate event ====================
|
|
app.on('activate', () => {
|
|
const allWindows = BrowserWindow.getAllWindows();
|
|
log.info('activate', allWindows.length);
|
|
|
|
if (allWindows.length) {
|
|
allWindows[0].focus();
|
|
} else {
|
|
cleanupPythonProcess();
|
|
createWindow();
|
|
}
|
|
});
|
|
|
|
// ==================== app exit event ====================
|
|
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);
|
|
} finally {
|
|
// Force quit after cleanup
|
|
app.exit(0);
|
|
}
|
|
});
|
|
|