// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { spawn } from 'child_process'; import { app } from 'electron'; import log from 'electron-log'; import fs from 'fs'; import os from 'os'; import path from 'path'; export function getResourcePath() { return path.join(app.getAppPath(), 'resources'); } export function getBackendPath() { if (app.isPackaged) { // after packaging, backend is in extraResources return path.join(process.resourcesPath, 'backend'); } else { // development environment return path.join(app.getAppPath(), 'backend'); } } export function runInstallScript(scriptPath: string): Promise { return new Promise((resolve, reject) => { const installScriptPath = path.join( getResourcePath(), 'scripts', scriptPath ); log.info(`Running script at: ${installScriptPath}`); const nodeProcess = spawn(process.execPath, [installScriptPath], { env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, }); let stderrOutput = ''; nodeProcess.stdout.on('data', (data) => { log.info(`Script output: ${data}`); }); nodeProcess.stderr.on('data', (data) => { const errorMsg = data.toString(); stderrOutput += errorMsg; log.error(`Script error: ${errorMsg}`); }); nodeProcess.on('close', (code) => { if (code === 0) { log.info('Script completed successfully'); resolve(true); } else { log.error(`Script exited with code ${code}`); const errorMessage = stderrOutput.trim() || `Script exited with code ${code}`; reject(new Error(errorMessage)); } }); }); } export async function getBinaryName(name: string): Promise { if (process.platform === 'win32') { return `${name}.exe`; } return name; } /** * Get path to prebuilt binary (if available in packaged app) */ export function getPrebuiltBinaryPath(name?: string): string | null { if (!app.isPackaged) { return null; } const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin'); if (!fs.existsSync(prebuiltBinDir)) { return null; } if (!name) { return prebuiltBinDir; } const binaryName = process.platform === 'win32' ? `${name}.exe` : name; const binaryPath = path.join(prebuiltBinDir, binaryName); return fs.existsSync(binaryPath) ? binaryPath : null; } export async function getBinaryPath(name?: string): Promise { // First check for prebuilt binary in packaged app if (app.isPackaged) { const prebuiltPath = getPrebuiltBinaryPath(name); if (prebuiltPath) { log.info(`Using prebuilt binary: ${prebuiltPath}`); return prebuiltPath; } } const binariesDir = path.join(os.homedir(), '.eigent', 'bin'); // Ensure .eigent/bin directory exists if (!fs.existsSync(binariesDir)) { fs.mkdirSync(binariesDir, { recursive: true }); } if (!name) { return binariesDir; } const binaryName = await getBinaryName(name); return path.join(binariesDir, binaryName); } export function getCachePath(folder: string): string { // For packaged app, try to use prebuilt cache first if (app.isPackaged) { const prebuiltCachePath = path.join( process.resourcesPath, 'prebuilt', 'cache', folder ); if (fs.existsSync(prebuiltCachePath)) { log.info(`Using prebuilt cache: ${prebuiltCachePath}`); return prebuiltCachePath; } } const cacheDir = path.join(os.homedir(), '.eigent', 'cache', folder); // Ensure cache directory exists if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); } return cacheDir; } /** * Get path to prebuilt venv (if available in packaged app) */ export function getPrebuiltVenvPath(): string | null { if (!app.isPackaged) { return null; } const prebuiltVenvPath = path.join(process.resourcesPath, 'prebuilt', 'venv'); if (fs.existsSync(prebuiltVenvPath)) { const pyvenvCfg = path.join(prebuiltVenvPath, 'pyvenv.cfg'); if (fs.existsSync(pyvenvCfg)) { // Verify Python executable exists (Windows: Scripts/python.exe, Unix: bin/python) const isWindows = process.platform === 'win32'; const pythonExePath = isWindows ? path.join(prebuiltVenvPath, 'Scripts', 'python.exe') : path.join(prebuiltVenvPath, 'bin', 'python'); if (fs.existsSync(pythonExePath)) { log.info(`Using prebuilt venv: ${prebuiltVenvPath}`); return prebuiltVenvPath; } else { log.warn( `Prebuilt venv found but Python executable missing at: ${pythonExePath}. ` + `Falling back to user venv.` ); } } } return null; } /** * Find Python executable in prebuilt Python directory for terminal venv */ function findPythonForTerminalVenv(): string | null { const prebuiltPythonDir = getPrebuiltPythonDir(); if (!prebuiltPythonDir) { return null; } // Look for Python executable in the prebuilt directory // UV stores Python in subdirectories like: cpython-3.11.x+.../install/bin/python const possiblePaths: string[] = []; // First, try common direct paths possiblePaths.push( path.join(prebuiltPythonDir, 'install', 'bin', 'python'), path.join(prebuiltPythonDir, 'install', 'python.exe'), path.join(prebuiltPythonDir, 'bin', 'python'), path.join(prebuiltPythonDir, 'python.exe') ); // Then, search in subdirectories (UV stores Python in versioned directories) try { if (fs.existsSync(prebuiltPythonDir)) { const entries = fs.readdirSync(prebuiltPythonDir, { withFileTypes: true, }); for (const entry of entries) { if (entry.isDirectory() && entry.name.startsWith('cpython-')) { const subDir = path.join(prebuiltPythonDir, entry.name); possiblePaths.push( path.join(subDir, 'install', 'bin', 'python'), path.join(subDir, 'install', 'python.exe'), path.join(subDir, 'bin', 'python'), path.join(subDir, 'python.exe') ); } } } } catch (error) { log.warn('[PROCESS] Error searching for prebuilt Python:', error); } for (const pythonPath of possiblePaths) { if (fs.existsSync(pythonPath)) { return pythonPath; } } return null; } /** * Get path to prebuilt terminal venv (if available in packaged app) */ export function getPrebuiltTerminalVenvPath(): string | null { if (!app.isPackaged) { return null; } const prebuiltTerminalVenvPath = path.join( process.resourcesPath, 'prebuilt', 'terminal_venv' ); if (fs.existsSync(prebuiltTerminalVenvPath)) { const pyvenvCfg = path.join(prebuiltTerminalVenvPath, 'pyvenv.cfg'); const installedMarker = path.join( prebuiltTerminalVenvPath, '.packages_installed' ); if (fs.existsSync(pyvenvCfg) && fs.existsSync(installedMarker)) { const isWindows = process.platform === 'win32'; const pythonExePath = isWindows ? path.join(prebuiltTerminalVenvPath, 'Scripts', 'python.exe') : path.join(prebuiltTerminalVenvPath, 'bin', 'python'); if (fs.existsSync(pythonExePath)) { log.info(`Using prebuilt terminal venv: ${prebuiltTerminalVenvPath}`); return prebuiltTerminalVenvPath; } else { // Try to fix the missing Python executable by creating a symlink to prebuilt Python log.warn( `Prebuilt terminal venv found but Python executable missing at: ${pythonExePath}. ` + `Attempting to fix...` ); const prebuiltPython = findPythonForTerminalVenv(); if (prebuiltPython && fs.existsSync(prebuiltPython)) { try { const binDir = isWindows ? path.join(prebuiltTerminalVenvPath, 'Scripts') : path.join(prebuiltTerminalVenvPath, 'bin'); // Ensure bin directory exists if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); } // Create symlink to prebuilt Python if (fs.existsSync(pythonExePath)) { // Remove existing broken symlink or file fs.unlinkSync(pythonExePath); } // Calculate relative path for symlink const relativePath = path.relative(binDir, prebuiltPython); fs.symlinkSync(relativePath, pythonExePath); log.info( `Fixed terminal venv Python symlink: ${pythonExePath} -> ${prebuiltPython}` ); return prebuiltTerminalVenvPath; } catch (error) { log.warn(`Failed to fix terminal venv Python symlink: ${error}`); } } log.warn(`Falling back to user terminal venv.`); } } } return null; } export function getVenvPath(version: string): string { // First check for prebuilt venv in packaged app if (app.isPackaged) { const prebuiltVenv = getPrebuiltVenvPath(); if (prebuiltVenv) { return prebuiltVenv; } } const venvDir = path.join( os.homedir(), '.eigent', 'venvs', `backend-${version}` ); // Ensure venvs directory exists (parent of the actual venv) const venvsBaseDir = path.dirname(venvDir); if (!fs.existsSync(venvsBaseDir)) { fs.mkdirSync(venvsBaseDir, { recursive: true }); } return venvDir; } export function getVenvsBaseDir(): string { return path.join(os.homedir(), '.eigent', 'venvs'); } /** * Packages to install in the terminal base venv. * These are commonly used packages for terminal tasks (data processing, visualization, etc.) * Keep this list minimal - users can install additional packages as needed. */ export const TERMINAL_BASE_PACKAGES = [ 'pandas', 'numpy', 'matplotlib', 'requests', 'openpyxl', 'beautifulsoup4', 'pillow', ]; /** * Get path to the terminal base venv. * This is a lightweight venv with common packages for terminal tasks, * separate from the backend venv. */ export function getTerminalVenvPath(version: string): string { // First check for prebuilt terminal venv in packaged app if (app.isPackaged) { const prebuiltTerminalVenv = getPrebuiltTerminalVenvPath(); if (prebuiltTerminalVenv) { return prebuiltTerminalVenv; } } const venvDir = path.join( os.homedir(), '.eigent', 'venvs', `terminal_base-${version}` ); // Ensure venvs directory exists const venvsBaseDir = path.dirname(venvDir); if (!fs.existsSync(venvsBaseDir)) { fs.mkdirSync(venvsBaseDir, { recursive: true }); } return venvDir; } export async function cleanupOldVenvs(currentVersion: string): Promise { const venvsBaseDir = getVenvsBaseDir(); // Check if venvs directory exists if (!fs.existsSync(venvsBaseDir)) { return; } // Patterns to match: backend-{version} and terminal_base-{version} const venvPatterns = [ { prefix: 'backend-', regex: /^backend-(.+)$/ }, { prefix: 'terminal_base-', regex: /^terminal_base-(.+)$/ }, ]; try { const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; for (const pattern of venvPatterns) { if (entry.name.startsWith(pattern.prefix)) { const versionMatch = entry.name.match(pattern.regex); if (versionMatch && versionMatch[1] !== currentVersion) { const oldVenvPath = path.join(venvsBaseDir, entry.name); console.log(`Cleaning up old venv: ${oldVenvPath}`); try { // Remove old venv directory recursively fs.rmSync(oldVenvPath, { recursive: true, force: true }); console.log(`Successfully removed old venv: ${entry.name}`); } catch (err) { console.error(`Failed to remove old venv ${entry.name}:`, err); } } break; // Found matching pattern, no need to check others } } } } catch (err) { console.error('Error during venv cleanup:', err); } } export async function isBinaryExists(name: string): Promise { const cmd = await getBinaryPath(name); return fs.existsSync(cmd); } /** * Get path to prebuilt Python installation (if available in packaged app) */ export function getPrebuiltPythonDir(): string | null { if (!app.isPackaged) { return null; } const prebuiltPythonDir = path.join( process.resourcesPath, 'prebuilt', 'uv_python' ); if (fs.existsSync(prebuiltPythonDir)) { log.info(`Using prebuilt Python: ${prebuiltPythonDir}`); return prebuiltPythonDir; } return null; } /** * Get unified UV environment variables for consistent Python environment management. * This ensures both installation and runtime use the same paths. * @param version - The app version for venv path * @returns Environment variables for UV commands */ export function getUvEnv(version: string): Record { // Use prebuilt Python if available (packaged app) const prebuiltPython = getPrebuiltPythonDir(); const pythonInstallDir = prebuiltPython || getCachePath('uv_python'); return { UV_PYTHON_INSTALL_DIR: pythonInstallDir, UV_TOOL_DIR: getCachePath('uv_tool'), UV_PROJECT_ENVIRONMENT: getVenvPath(version), UV_HTTP_TIMEOUT: '300', }; } export async function killProcessByName(name: string): Promise { const platform = process.platform; try { if (platform === 'win32') { await new Promise((resolve, reject) => { // /F = force, /IM = image name const cmd = spawn('taskkill', ['/F', '/IM', `${name}.exe`]); cmd.on('close', (code) => { // code 0 = success, code 128 = process not found (which is fine) if (code === 0 || code === 128) resolve(); else reject(new Error(`taskkill exited with code ${code}`)); }); cmd.on('error', reject); }); } else { await new Promise((resolve, reject) => { const cmd = spawn('pkill', ['-9', name]); cmd.on('close', (code) => { // code 0 = success, code 1 = no process found (which is fine) if (code === 0 || code === 1) resolve(); else reject(new Error(`pkill exited with code ${code}`)); }); cmd.on('error', reject); }); } } catch (err) { // Ignore errors, just best effort log.warn(`Failed to kill process ${name}:`, err); } }