Fix #352 install enhanced (#404)

This commit is contained in:
Wendong-Fan 2025-09-30 23:56:29 +08:00 committed by GitHub
commit 5588eca97e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 943 additions and 509 deletions

View file

@ -4,7 +4,7 @@ import path from 'node:path'
import os, { homedir } from 'node:os'
import log from 'electron-log'
import { update, registerUpdateIpcHandlers } from './update'
import { checkToolInstalled, installDependencies, killProcessOnPort, startBackend } from './init'
import { checkToolInstalled, killProcessOnPort, startBackend } from './init'
import { WebViewManager } from './webview'
import { FileReader } from './fileReader'
import { ChildProcessWithoutNullStreams } from 'node:child_process'
@ -18,9 +18,9 @@ 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'
const userData = app.getPath('userData');
const versionFile = path.join(userData, 'version.txt');
// ==================== constants ====================
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -50,69 +50,6 @@ findAvailablePort(browser_port).then(port => {
app.commandLine.appendSwitch('remote-debugging-port', port + '');
});
// Read last run version and install dependencies on update
async function checkAndInstallDepsOnUpdate(): Promise<boolean> {
const currentVersion = app.getVersion();
return new Promise(async (resolve, reject) => {
try {
log.info(' start check version', { currentVersion });
// Check if version file exists
const versionExists = fs.existsSync(versionFile);
let savedVersion = '';
if (versionExists) {
savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
log.info(' read saved version', { savedVersion });
} else {
log.info(' version file not exist, will create new file');
}
// If version file does not exist or version does not match, reinstall dependencies
if (!versionExists || savedVersion !== currentVersion) {
log.info(' version changed, prepare to reinstall uv dependencies...', {
currentVersion,
savedVersion: versionExists ? savedVersion : 'none',
reason: !versionExists ? 'version file not exist' : 'version not match'
});
// Notify frontend to update
if (win && !win.isDestroyed()) {
win.webContents.send('update-notification', {
type: 'version-update',
currentVersion,
previousVersion: versionExists ? savedVersion : 'none',
reason: !versionExists ? 'version file not exist' : 'version not match'
});
}
// Update version file
fs.writeFileSync(versionFile, currentVersion);
log.info(' version file updated', { currentVersion });
// Install dependencies
const result = await installDependencies();
if (!result) {
log.error(' install dependencies failed');
resolve(false);
return
}
resolve(true);
log.info(' install dependencies complete');
return
} else {
log.info(' version not changed, skip install dependencies', { currentVersion });
resolve(true);
return
}
} catch (error) {
log.error(' check version and install dependencies error:', error);
resolve(false);
return
}
})
}
// ==================== app config ====================
process.env.APP_ROOT = MAIN_DIST;
process.env.VITE_PUBLIC = VITE_PUBLIC;
@ -259,51 +196,6 @@ const checkManagerInstance = (manager: any, name: string) => {
return manager;
};
export const handleDependencyInstallation = async () => {
try {
log.info(' start install dependencies...');
const isSuccess = await installDependencies();
if (!isSuccess) {
log.error(' install dependencies failed');
return { success: false, error: 'install dependencies failed' };
}
log.info(' install dependencies success, check tool installed status...');
const isToolInstalled = await checkToolInstalled();
log.info('isToolInstalled && !python_process', isToolInstalled && !python_process);
if (isToolInstalled && !python_process) {
log.info(' tool installed, start backend service...');
python_process = await startBackend((port) => {
backendPort = port;
log.info(' backend service start success', { port });
});
// Notify frontend to install success
if (win && !win.isDestroyed()) {
win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
}
python_process?.on('exit', (code, signal) => {
log.info(' python process exit', { code, signal });
});
} else if (!isToolInstalled) {
log.warn(' tool not installed, skip backend start');
} else {
log.info(' backend process already exist, skip start');
}
log.info(' install dependencies complete');
return { success: true };
} catch (error: any) {
log.error(' install dependencies error:', error);
if (win && !win.isDestroyed()) {
win.webContents.send('install-dependencies-complete', { success: false, code: 2 });
}
return { success: false, error: error.message };
}
};
function registerIpcHandlers() {
// ==================== basic info handler ====================
ipcMain.handle('get-browser-port', () => {
@ -934,13 +826,35 @@ function registerIpcHandlers() {
});
// ==================== dependency install handler ====================
ipcMain.handle('install-dependencies', handleDependencyInstallation);
ipcMain.handle('frontend-ready', handleDependencyInstallation);
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 };
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 };
}
@ -1006,12 +920,13 @@ async function createWindow() {
update(win);
// ==================== check tool installed ====================
let res = await checkAndInstallDepsOnUpdate();
if (!res) {
log.info('checkAndInstallDepsOnUpdate,install dependencies failed');
win.webContents.send('install-dependencies-complete', { success: false, code: 2 });
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);
await checkAndStartBackend();
}
@ -1069,27 +984,30 @@ const setupExternalLinkHandling = () => {
// ==================== 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...');
const isToolInstalled = await checkToolInstalled();
if (isToolInstalled) {
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 });
}
// 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');
}
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)
}
};

View file

@ -6,82 +6,30 @@ import path from 'path'
import * as net from "net";
import { ipcMain, BrowserWindow, app } from 'electron'
import { promisify } from 'util'
import { detectInstallationLogs, PromiseReturnType } from "./install-deps";
const execAsync = promisify(exec);
// helper function to get main window
function getMainWindow(): BrowserWindow | null {
export function getMainWindow(): BrowserWindow | null {
const windows = BrowserWindow.getAllWindows();
return windows.length > 0 ? windows[0] : null;
}
export async function checkToolInstalled() {
return new Promise(async (resolve, reject) => {
return new Promise<PromiseReturnType>(async (resolve, reject) => {
if (!(await isBinaryExists('uv'))) {
resolve(false)
resolve({success: false, message: "uv doesn't exist"})
return
}
if (!(await isBinaryExists('bun'))) {
resolve(false)
resolve({success: false, message: "Bun doesn't exist"})
return
}
resolve(true)
})
}
/**
* Check if command line tools are installed, install if not
*/
export async function installCommandTool() {
return new Promise(async (resolve, reject) => {
const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise<boolean> => {
if (await isBinaryExists(toolName)) {
return true;
}
console.log(`start install ${toolName}`);
const isSuccess = await runInstallScript(scriptName);
const mainWindow = getMainWindow();
if (mainWindow && !mainWindow.isDestroyed() && !isSuccess) {
mainWindow.webContents.send('install-dependencies-complete', {
success: false,
code: 2,
error: `${toolName} installation failed`,
});
return false
}
const installed = await isBinaryExists(toolName);
if (mainWindow && !mainWindow.isDestroyed()) {
if (installed) {
mainWindow.webContents.send('install-dependencies-log', {
type: 'stdout',
data: `${toolName} installed successfully`,
});
} else {
mainWindow.webContents.send('install-dependencies-complete', {
success: false,
code: 2,
error: `${toolName} installation failed`,
});
}
}
return installed;
};
if (!(await ensureInstalled('uv', 'install-uv.js'))) {
return reject("uv install failed");
}
if (!(await ensureInstalled('bun', 'install-bun.js'))) {
return reject("bun install failed");
}
return resolve(true);
resolve({success: true, message: "Tools exist already"})
})
}
@ -167,124 +115,6 @@ export async function installCommandTool() {
// })
// })
// }
export async function installDependencies() {
return new Promise<boolean>(async (resolve, reject) => {
console.log('start install dependencies')
const mainWindow = getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('install-dependencies-start');
}
const isInstalCommandTool = await installCommandTool()
if (!isInstalCommandTool) {
resolve(false)
return
}
const uv_path = await getBinaryPath('uv')
const backendPath = getBackendPath()
if (!fs.existsSync(backendPath)) {
fs.mkdirSync(backendPath, { recursive: true })
}
const installingLockPath = path.join(backendPath, 'uv_installing.lock')
fs.writeFileSync(installingLockPath, '')
const installedLockPath = path.join(backendPath, 'uv_installed.lock')
// const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple']
const proxyArgs = ['--default-index', 'https://mirrors.aliyun.com/pypi/simple/']
const runInstall = (extraArgs: string[]) => {
return new Promise<boolean>((resolveInner, rejectInner) => {
try {
const node_process = spawn(uv_path, [
'sync',
'--no-dev',
'--cache-dir', getCachePath('uv_cache'),
...extraArgs], {
cwd: backendPath,
env: {
...process.env,
UV_TOOL_DIR: getCachePath('uv_tool'),
UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
}
})
console.log('start install dependencies', extraArgs)
node_process.stdout.on('data', (data) => {
log.info(`Script output: ${data}`)
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('install-dependencies-log', { type: 'stdout', data: data.toString() });
}
})
node_process.stderr.on('data', (data) => {
log.error(`Script error: ${data}`)
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('install-dependencies-log', { type: 'stderr', data: data.toString() });
}
})
node_process.on('close', (code) => {
console.log('install dependencies end', code === 0)
resolveInner(code === 0)
})
} catch (err) {
log.error('run install failed', err)
// Clean up uv_installing.lock file if installation fails
if (fs.existsSync(installingLockPath)) {
fs.unlinkSync(installingLockPath);
}
rejectInner(err)
}
})
}
// try default install
const installSuccess = await runInstall([])
if (installSuccess) {
fs.unlinkSync(installingLockPath)
fs.writeFileSync(installedLockPath, '')
log.info('Script completed successfully')
console.log('end install dependencies')
spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
resolve(true)
return
}
// try mirror install
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
let mirrorInstallSuccess = false
if (timezone === 'Asia/Shanghai') {
mirrorInstallSuccess = await runInstall(proxyArgs)
} else {
mirrorInstallSuccess = await runInstall([])
}
fs.existsSync(installingLockPath) && fs.unlinkSync(installingLockPath)
if (mirrorInstallSuccess) {
fs.writeFileSync(installedLockPath, '')
log.info('Mirror script completed successfully')
console.log('end install dependencies (mirror)')
spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
resolve(true)
} else {
log.error('Both default and mirror install failed')
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('install-dependencies-complete', { success: false, error: 'Both default and mirror install failed' });
}
resolve(false)
}
})
}
export async function startBackend(setPort?: (port: number) => void): Promise<any> {
console.log('start fastapi')
@ -330,6 +160,9 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
const displayFilteredLogs = (data: String) => {
if (!data) return;
const msg = data.toString().trimEnd();
//Detect if uv sync is run
detectInstallationLogs(msg);
if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) {
log.error(`BACKEND: ${msg}`);
} else if (msg.toLowerCase().includes("warn")) {
@ -343,6 +176,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
}
return new Promise((resolve, reject) => {
//Implicitly runs uv sync
const node_process = spawn(
uv_path,
["run", "uvicorn", "main:api", "--port", port.toString(), "--loop", "asyncio"],

View file

@ -0,0 +1,424 @@
import { app, BrowserWindow } from 'electron'
import path from 'node:path'
import log from 'electron-log'
import { getMainWindow } from './init'
import fs from 'node:fs'
import { getBackendPath, getBinaryPath, getCachePath, isBinaryExists, runInstallScript } from './utils/process'
import { spawn } from 'child_process'
import { safeMainWindowSend } from './utils/safeWebContentsSend'
const userData = app.getPath('userData');
const versionFile = path.join(userData, 'version.txt');
export type PromiseReturnType = {
message: string;
success: boolean;
}
interface checkInstallProps {
win:BrowserWindow|null;
forceInstall?:boolean
}
// Read last run version and install dependencies on update
export const checkAndInstallDepsOnUpdate = async ({win, forceInstall=false}:checkInstallProps):
Promise<PromiseReturnType> => {
const currentVersion = app.getVersion();
let savedVersion = '';
const checkInstallOperations = {
getSavedVersion: ():boolean => {
// Check if version file exists
const versionExists = fs.existsSync(versionFile);
if (versionExists) {
log.info('[DEPS INSTALL] start check version', { currentVersion });
savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
log.info('[DEPS INSTALL] read saved version', { savedVersion });
} else {
log.info('[DEPS INSTALL] version file not exist, will create new file');
}
return versionExists;
},
handleUpdateNotification: (versionExists:boolean) => {
if (win && !win.isDestroyed()) {
win.webContents.send('update-notification', {
type: 'version-update',
currentVersion,
previousVersion: versionExists ? savedVersion : 'none',
reason: !versionExists ? 'version file not exist' : 'version not match'
});
} else {
log.warn('[DEPS INSTALL] Cannot send update notification - window not available');
}
},
createVersionFile: () => {
fs.writeFileSync(versionFile, currentVersion);
log.info('[DEPS INSTALL] version file updated', { currentVersion });
}
}
return new Promise(async (resolve, reject) => {
try {
const versionExists:boolean = checkInstallOperations.getSavedVersion();
// If version file does not exist or version does not match, reinstall dependencies
if (forceInstall || !versionExists || savedVersion !== currentVersion) {
log.info('[DEPS INSTALL] version changed, prepare to reinstall uv dependencies...', {
currentVersion,
savedVersion: versionExists ? savedVersion : 'none',
reason: !versionExists ? 'version file not exist' : 'version not match'
});
// Notify frontend to update
checkInstallOperations.handleUpdateNotification(versionExists);
// Update version file
checkInstallOperations.createVersionFile();
// Install dependencies
const result = await installDependencies();
if (!result.success) {
log.error(' install dependencies failed');
resolve({ message: `Install dependencies failed, msg ${result.message}`, success: false });
return
}
resolve({ message: "Dependencies installed successfully after update", success: true });
log.info('[DEPS INSTALL] install dependencies complete');
return
} else {
log.info('[DEPS INSTALL] version not changed, skip install dependencies', { currentVersion });
resolve({ message: "Version not changed, skipped installation", success: true });
return
}
} catch (error) {
log.error(' check version and install dependencies error:', error);
resolve({ message: `Error checking version: ${error}`, success: false });
return
}
})
}
/**
* Check if command line tools are installed, install if not
*/
export async function installCommandTool(): Promise<PromiseReturnType> {
try {
const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise<PromiseReturnType> => {
if (await isBinaryExists(toolName)) {
return { message: `${toolName} already installed`, success: true };
}
console.log(`start install ${toolName}`);
await runInstallScript(scriptName);
const installed = await isBinaryExists(toolName);
if (installed) {
safeMainWindowSend('install-dependencies-log', {
type: 'stdout',
data: `${toolName} installed successfully`,
});
} else {
safeMainWindowSend('install-dependencies-complete', {
success: false,
code: 2,
error: `${toolName} installation failed (script exit code 2)`,
});
}
return {
message: installed ? `${toolName} installed successfully` : `${toolName} installation failed`,
success: installed
};
};
const uvResult = await ensureInstalled('uv', 'install-uv.js');
if (!uvResult.success) {
return { message: uvResult.message, success: false };
}
const bunResult = await ensureInstalled('bun', 'install-bun.js');
if (!bunResult.success) {
return { message: bunResult.message, success: false };
}
return { message: "Command tools installed successfully", success: true };
} catch (error) {
return { message: `Command tool installation failed: ${error}`, success: false };
}
}
let uv_path:string;
const mainWindow = getMainWindow();
const backendPath = getBackendPath();
const installingLockPath = path.join(backendPath, 'uv_installing.lock')
const installedLockPath = path.join(backendPath, 'uv_installed.lock')
// const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple']
const proxyArgs = ['--default-index', 'https://mirrors.aliyun.com/pypi/simple/']
/**
* Get current installation status by checking lock files
* @returns Object with installation status information
*/
export async function getInstallationStatus(): Promise<{
isInstalling: boolean;
hasLockFile: boolean;
installedExists: boolean;
}> {
try {
const installingExists = fs.existsSync(installingLockPath);
const installedExists = fs.existsSync(installedLockPath);
// If installing lock exists, installation is in progress
// If installed lock exists, installation completed previously
return {
isInstalling: installingExists,
hasLockFile: installingExists || installedExists,
installedExists: installedExists
};
} catch (error) {
console.error('[getInstallationStatus] Error checking installation status:', error);
return {
isInstalling: false,
hasLockFile: false,
installedExists: false
};
}
}
class InstallLogs {
private node_process;
constructor(extraArgs:string[]) {
console.log('start install dependencies', extraArgs)
this.node_process = spawn(uv_path, [
'sync',
'--no-dev',
'--cache-dir', getCachePath('uv_cache'),
...extraArgs], {
cwd: backendPath,
env: {
...process.env,
UV_TOOL_DIR: getCachePath('uv_tool'),
UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
}
})
}
/**Display filtered logs based on severity */
displayFilteredLogs(data:String) {
if (!data) return;
const msg = data.toString().trimEnd();
//Detect if uv sync is run
detectInstallationLogs(msg);
if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) {
log.error(`BACKEND: [DEPS INSTALL] ${msg}`);
safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() });
} else {
log.info(`BACKEND: [DEPS INSTALL] ${msg}`);
safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
}
}
/**Handle stdout data */
onStdout() {
this.node_process.stdout.on('data', (data:any) => {
this.displayFilteredLogs(data);
})
}
/**Handle stderr data */
onStderr() {
this.node_process.stderr.on('data', (data:any) => {
this.displayFilteredLogs(data);
})
}
/**Handle process close event */
onClose(resolveInner:(code: number | null) => void) {
this.node_process.on('close', resolveInner);
}
/**
* Set installing Lock Path
* Creates uv_installing.lock file to indicate installation in progress
* Creates backend directory if not exists
*/
static setLockPath() {
if (!fs.existsSync(backendPath)) {
fs.mkdirSync(backendPath, { recursive: true })
}
fs.writeFileSync(installingLockPath, '')
}
/**Clean installing Lock Path */
static cleanLockPath() {
if (fs.existsSync(installingLockPath)) {
fs.unlinkSync(installingLockPath);
}
}
}
const runInstall = (extraArgs: string[]) => {
const installLogs = new InstallLogs(extraArgs);
return new Promise<PromiseReturnType>((resolveInner, rejectInner) => {
try {
installLogs.onStdout();
installLogs.onStderr();
installLogs.onClose((code) => {
console.log('install dependencies end', code === 0)
InstallLogs.cleanLockPath()
resolveInner({
message: code === 0 ? "Installation completed successfully" : `Installation failed with code ${code}`,
success: code === 0
})
})
} catch (err) {
log.error('run install failed', err)
// Clean up uv_installing.lock file if installation fails
InstallLogs.cleanLockPath();
rejectInner({ message: `Installation failed: ${err}`, success: false })
}
})
}
export async function installDependencies(): Promise<PromiseReturnType> {
uv_path = await getBinaryPath('uv');
const handleInstallOperations = {
spawnBabel: (message:"mirror"|"main"="main") => {
fs.writeFileSync(installedLockPath, '')
log.info('[DEPS INSTALL] Script completed successfully')
console.log(`Install Dependencies completed ${message}`)
spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
},
notifyInstallDependenciesPage: ():boolean => {
const success = safeMainWindowSend('install-dependencies-start');
if (!success) {
log.warn('[DEPS INSTALL] Main window not available, continuing installation without UI updates');
}
return success;
}
}
return new Promise<PromiseReturnType>(async (resolve, reject) => {
console.log('start install dependencies')
const mainWindowAvailable = handleInstallOperations.notifyInstallDependenciesPage();
if (!mainWindowAvailable) {
log.info('[DEPS INSTALL] Proceeding with installation without UI notifications');
}
const isInstalCommandTool = await installCommandTool()
if (!isInstalCommandTool.success) {
resolve({ message: "Command tool installation failed", success: false })
return
}
// Set Installing Lock Files
InstallLogs.setLockPath();
// try default install
const installSuccess = await runInstall([])
if (installSuccess.success) {
handleInstallOperations.spawnBabel()
resolve({ message: "Dependencies installed successfully", success: true })
return
}
// try mirror install
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
let mirrorInstallSuccess: PromiseReturnType = { message: "", success: false }
mirrorInstallSuccess = (timezone === 'Asia/Shanghai')? await runInstall(proxyArgs) :await runInstall([])
if (mirrorInstallSuccess.success) {
handleInstallOperations.spawnBabel("mirror")
resolve({ message: "Dependencies installed successfully with mirror", success: true })
} else {
log.error('Both default and mirror install failed')
safeMainWindowSend('install-dependencies-complete', {
success: false,
error: 'Both default and mirror install failed'
});
resolve({ message: "Both default and mirror install failed", success: false })
}
})
}
let dependencyInstallationDetected = false;
let installationNotificationSent = false;
export function detectInstallationLogs(msg:string) {
// Check for UV dependency installation patterns
const installPatterns = [
"Resolved", // UV resolving dependencies
"Downloaded", // UV downloading packages
"Installing", // UV installing packages
"Built", // UV building packages
"Prepared", // UV preparing virtual environment
"Syncing", // UV sync process
"Creating virtualenv", // Virtual environment creation
"Updating", // UV updating packages
"× No solution found when resolving dependencies", // Dependency resolution issues
"Audited" // UV auditing dependencies
];
// Detect if UV is installing dependencies
if (!dependencyInstallationDetected && installPatterns.some(pattern =>
msg.includes(pattern) && !msg.includes("Uvicorn running on")
)) {
dependencyInstallationDetected = true;
log.info('[BACKEND STARTUP] UV dependency installation detected during uvicorn startup');
// Create installing lock file to maintain consistency with install-deps.ts
InstallLogs.setLockPath();
log.info('[BACKEND STARTUP] Created uv_installing.lock file');
// Notify frontend that installation has started (only once)
if (!installationNotificationSent) {
installationNotificationSent = true;
const notificationSent = safeMainWindowSend('install-dependencies-start');
if (notificationSent) {
log.info('[BACKEND STARTUP] Notified frontend of dependency installation start');
} else {
log.warn('[BACKEND STARTUP] Failed to notify frontend of dependency installation start');
}
}
}
// Send installation logs to frontend if installation was detected
if (dependencyInstallationDetected && !msg.includes("Uvicorn running on")) {
safeMainWindowSend('install-dependencies-log', {
type: msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback") ? 'stderr' : 'stdout',
data: msg
});
}
// Check if installation is complete (uvicorn starts successfully)
if (dependencyInstallationDetected && msg.includes("Uvicorn running on")) {
log.info('[BACKEND STARTUP] UV dependency installation completed, uvicorn started successfully');
// Clean up installing lock and create installed lock
InstallLogs.cleanLockPath();
fs.writeFileSync(installedLockPath, '');
log.info('[BACKEND STARTUP] Created uv_installed.lock file');
safeMainWindowSend('install-dependencies-complete', {
success: true,
message: 'Dependencies installed successfully during backend startup'
});
}
// Handle installation failures
if (dependencyInstallationDetected && (
msg.toLowerCase().includes("failed to resolve dependencies") ||
msg.toLowerCase().includes("installation failed") ||
msg.includes("× No solution found when resolving dependencies")
)) {
log.error('[BACKEND STARTUP] UV dependency installation failed');
// Clean up installing lock file
InstallLogs.cleanLockPath();
log.info('[BACKEND STARTUP] Cleaned up uv_installing.lock file after failure');
safeMainWindowSend('install-dependencies-complete', {
success: false,
error: 'Dependency installation failed during backend startup'
});
}
}

View file

@ -0,0 +1,20 @@
import log from 'electron-log'
import { getMainWindow } from "../init";
/**
* Safely send message to main window if it exists and is not destroyed
* @param channel - The IPC channel to send message to
* @param data - The data to send
*/
function safeMainWindowSend(channel: string, data?: any) {
const mainWindow = getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(channel, data);
return true;
} else {
log.warn(`[WEBCONTENTS SEND] Cannot send message to main window: ${channel}`, data);
return false;
}
}
export {safeMainWindowSend}

View file

@ -62,9 +62,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
deleteFolder: (email: string) => ipcRenderer.invoke('delete-folder', email),
getMcpConfigPath: (email: string) => ipcRenderer.invoke('get-mcp-config-path', email),
// install dependencies related API
installDependencies: () => ipcRenderer.invoke('install-dependencies'),
frontendReady: () => ipcRenderer.invoke('frontend-ready'),
checkAndInstallDepsOnUpdate: () => ipcRenderer.invoke('install-dependencies'),
checkInstallBrowser: () => ipcRenderer.invoke('check-install-browser'),
getInstallationStatus: () => ipcRenderer.invoke('get-installation-status'),
onInstallDependenciesStart: (callback: () => void) => {
ipcRenderer.on('install-dependencies-start', callback);
},

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React from "react";
import { useAuthStore } from "@/store/authStore";
import { ProgressInstall } from "@/components/ui/progress-install";
import { FileDown, RefreshCcw } from "lucide-react";
@ -14,158 +14,21 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
import { useTranslation } from "react-i18next";
import { useInstallationUI } from "@/store/installationStore";
import { TooltipSimple } from "../ui/tooltip";
interface InstallLog {
type: "stdout" | "stderr";
data: string;
timestamp: Date;
}
export const InstallDependencies: React.FC<{
isInstalling: boolean;
setIsInstalling: (isInstalling: boolean) => void;
}> = ({ isInstalling, setIsInstalling }) => {
const { initState, setInitState } = useAuthStore();
const {t} = useTranslation()
const [logs, setLogs] = useState<InstallLog[]>([]);
const [status, setStatus] = useState<
"idle" | "installing" | "success" | "error"
>("idle");
const [showInstallScreen, setShowInstallScreen] = useState(true);
const [progress, setProgress] = useState(20);
useEffect(() => {
// listen to install start event
window.electronAPI.onInstallDependenciesStart(() => {
setIsInstalling(true);
setStatus("installing");
setShowInstallScreen(true);
setLogs([]);
console.log("start installing dependencies...");
setProgress(20);
});
// listen to install log
window.electronAPI.onInstallDependenciesLog(
(data: { type: string; data: string }) => {
console.log("data", data);
const newLog: InstallLog = {
type: data?.type as "stdout" | "stderr",
data: data?.data,
timestamp: new Date(),
};
setProgress((prev) => {
const progress = prev + 5;
if (progress >= 90) {
return 90;
}
return progress;
});
console.log(`install log [${data?.type}]:`, data?.data);
setLogs((prev) => [...prev, newLog]);
}
);
// listen to install complete event
window.electronAPI.onInstallDependenciesComplete(
(data: { success: boolean; code?: number; error?: string }) => {
setIsInstalling(false);
if (data?.success) {
setStatus("success");
console.log("dependencies installed successfully!");
setProgress(100);
setInitState("done");
} else {
setStatus("error");
console.error("dependencies installation failed:", data?.code);
console.error("dependencies installation failed:", data?.error);
}
}
);
// after component mounted, notify main process frontend is ready
const notifyFrontendReady = async () => {
try {
// check if there is frontend-ready API
if (window.electronAPI.frontendReady) {
await window.electronAPI.frontendReady();
}
} catch (error) {
console.log(
"frontend ready notification failed, maybe manual install mode:",
error
);
}
};
// delay notification, ensure component is fully initialized
setTimeout(notifyFrontendReady, 500);
// clean up listeners
return () => {
window.electronAPI.removeAllListeners("install-dependencies-start");
window.electronAPI.removeAllListeners("install-dependencies-log");
window.electronAPI.removeAllListeners("install-dependencies-complete");
};
}, []);
const handleInstall = async () => {
try {
setStatus("installing");
setIsInstalling(true);
setLogs([]);
setShowInstallScreen(true);
const result = await window.electronAPI.installDependencies();
console.log("result", result);
if (!result.success) {
setStatus("error");
setIsInstalling(false);
return;
}
setStatus("success");
setProgress(100);
setIsInstalling(false);
setInitState("done");
} catch (error) {
console.error("install start failed:", error);
setStatus("error");
setIsInstalling(false);
}
};
const exportLog = async () => {
try {
const response = await window.electronAPI.exportLog();
if (!response.success) {
alert("Export cancelled:" + response.error);
return;
}
if (response.savedPath) {
window.location.href =
"https://github.com/eigent-ai/eigent/issues/new/choose";
alert("log saved:" + response.savedPath);
}
} catch (e: any) {
alert("export error:" + e.message);
}
};
// if not show install interface, return null
if (initState === "done" && !isInstalling) {
return (
<Dialog open={status === "error"}>
<DialogContent className="bg-white-100%">
<DialogHeader>
<DialogTitle>{t("layout.installation-failed")}</DialogTitle>
</DialogHeader>
<DialogFooter>
<Button onClick={handleInstall}>{t("layout.retry")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export const InstallDependencies: React.FC = () => {
const { initState } = useAuthStore();
const {t} = useTranslation();
const {
progress,
latestLog,
error,
isInstalling,
retryInstallation,
exportLog,
} = useInstallationUI();
return (
<div className="fixed !z-[100] inset-0 !bg-bg-page bg-opacity-80 h-full w-full flex items-center justify-center backdrop-blur-sm">
@ -180,16 +43,18 @@ export const InstallDependencies: React.FC<{
<div className="flex items-center gap-2 justify-between">
<div className="text-text-label text-xs font-normal leading-tight ">
{isInstalling ? "System Installing ..." : ""}
<span className="pl-2">{logs.at(-1)?.data}</span>
<span className="pl-2">{latestLog?.data}</span>
</div>
<Button
size="icon"
variant="outline"
className="mt-1"
onClick={handleInstall}
>
<RefreshCcw className="w-4 h-4" />
</Button>
<TooltipSimple content={`Cannot retry because state is ${error}`} hidden={true}>
<Button
size="icon"
variant="outline"
className="mt-1"
onClick={retryInstallation}
>
<RefreshCcw className="w-4 h-4" />
</Button>
</TooltipSimple>
</div>
</div>
</div>
@ -214,7 +79,7 @@ export const InstallDependencies: React.FC<{
<FileDown className="w-4 h-4" />
{t("layout.report-bug")}
</Button>
<Button size="sm" onClick={handleInstall}>
<Button size="sm" onClick={retryInstallation}>
{t("layout.retry")}
</Button>
</DialogFooter>

View file

@ -0,0 +1,50 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { error } from "electron-log";
import { t } from "i18next";
import React from "react";
interface InstallationErrorDialogProps {
error: string;
installationState: string;
latestLog: any;
retryInstallation: () => void;
}
const InstallationErrorDialog = ({
error,
installationState,
latestLog,
retryInstallation,
}:InstallationErrorDialogProps) => {
return (
<Dialog open={installationState == "error"}>
<DialogContent className="bg-white-100%">
<DialogHeader>
<DialogTitle>{t("layout.installation-failed")}</DialogTitle>
</DialogHeader>
<div className="text-text-label text-xs font-normal leading-tight mb-4">
{
<div className="mb-1">
<span className="text-text-label/60">
Error: {error} <br />
Log: {latestLog?.data}
</span>
</div>
}
</div>
<DialogFooter>
<Button onClick={retryInstallation}>{t("layout.retry")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default InstallationErrorDialog;

View file

@ -8,13 +8,25 @@ import { AnimationJson } from "@/components/AnimationJson";
import animationData from "@/assets/animation/onboarding_success.json";
import CloseNoticeDialog from "../Dialog/CloseNotice";
import { useChatStore } from "@/store/chatStore";
import { useInstallationUI } from "@/store/installationStore";
import { useInstallationSetup } from "@/hooks/useInstallationSetup";
import InstallationErrorDialog from "../InstallStep/InstallationErrorDialog/InstallationErrorDialog";
const Layout = () => {
const { initState, setInitState, isFirstLaunch, setIsFirstLaunch } =
useAuthStore();
const [isInstalling, setIsInstalling] = useState(false);
const { initState, isFirstLaunch, setIsFirstLaunch } = useAuthStore();
const [noticeOpen, setNoticeOpen] = useState(false);
const chatStore = useChatStore();
const {
installationState,
latestLog,
error,
isInstalling,
shouldShowInstallScreen,
retryInstallation,
} = useInstallationUI();
// Setup installation IPC listeners and state synchronization
useInstallationSetup();
useEffect(() => {
const handleBeforeClose = () => {
const currentStatus = chatStore.tasks[chatStore.activeTaskId as string]?.status;
@ -32,43 +44,42 @@ const Layout = () => {
};
}, [chatStore.tasks, chatStore.activeTaskId]);
useEffect(() => {
const checkToolInstalled = async () => {
// in render process
const result = await window.ipcRenderer.invoke("check-tool-installed");
if (result.success) {
if (initState === "done" && !result.isInstalled) {
setInitState("carousel");
}
console.log("tool is installed:");
} else {
console.error("check failed:", result.error);
}
};
checkToolInstalled();
}, []);
// Determine what to show based on states
const shouldShowOnboarding = initState === "done" && isFirstLaunch && !isInstalling;
const shouldShowMainContent = !shouldShowInstallScreen;
return (
<div className="h-full flex flex-col">
<TopBar />
<div className="flex-1 h-full p-2">
{initState === "done" && isFirstLaunch && !isInstalling && (
{/* Onboarding animation */}
{shouldShowOnboarding && (
<AnimationJson
onComplete={() => {
setIsFirstLaunch(false);
}}
onComplete={() => setIsFirstLaunch(false)}
animationData={animationData}
/>
)}
{(initState !== "done" || isInstalling) && (
<InstallDependencies
isInstalling={isInstalling}
setIsInstalling={setIsInstalling}
/>
{/* Installation screen */}
{shouldShowInstallScreen && <InstallDependencies />}
{/* Main app content */}
{shouldShowMainContent && (
<>
<Outlet />
<HistorySidebar />
</>
)}
<Outlet />
<HistorySidebar />
{(error != "" && error !=undefined) &&
<InstallationErrorDialog
error={error}
installationState={installationState}
latestLog={latestLog}
retryInstallation={retryInstallation}/>
}
<CloseNoticeDialog
onOpenChange={setNoticeOpen}
open={noticeOpen}

View file

@ -0,0 +1,89 @@
import { useEffect } from 'react';
import { useInstallationStore } from '@/store/installationStore';
import { useAuthStore } from '@/store/authStore';
/**
* Hook that sets up Electron IPC listeners and handles installation state synchronization
* This should be called once in your App component or Layout component
*/
export const useInstallationSetup = () => {
const { initState, setInitState } = useAuthStore();
// Extract only the functions we need to avoid dependency issues
const startInstallation = useInstallationStore(state => state.startInstallation);
const addLog = useInstallationStore(state => state.addLog);
const setSuccess = useInstallationStore(state => state.setSuccess);
const setError = useInstallationStore(state => state.setError);
// Check tool installation status on mount
useEffect(() => {
const checkToolInstalled = async () => {
try {
console.log('[useInstallationSetup] Checking tool installation status...');
const result = await window.ipcRenderer.invoke("check-tool-installed");
if (result.success && initState === "done" && !result.isInstalled) {
console.log('[useInstallationSetup] Tool not installed, setting initState to carousel');
setInitState("carousel");
}
} catch (error) {
console.error("[useInstallationSetup] Tool installation check failed:", error);
}
};
const checkBackendStatus = async() => {
// Also check if installation is currently in progress
const installationStatus = await window.electronAPI.getInstallationStatus();
console.log('[useInstallationSetup] Installation status check:', installationStatus);
if (installationStatus.success && installationStatus.isInstalling) {
console.log('[useInstallationSetup] Installation in progress, starting frontend state');
startInstallation();
}
}
checkToolInstalled();
checkBackendStatus();
}, [initState, setInitState, startInstallation]);
// Setup Electron IPC listeners (only once)
useEffect(() => {
// Electron IPC event handlers
const handleInstallStart = () => {
startInstallation();
};
const handleInstallLog = (data: { type: string; data: string }) => {
addLog({
type: data.type as 'stdout' | 'stderr',
data: data.data,
timestamp: new Date(),
});
};
const handleInstallComplete = (data: { success: boolean; code?: number; error?: string }) => {
console.log('[useInstallationSetup] Install complete event received:', data);
if (data.success) {
setSuccess();
setInitState('done');
} else {
setError(data.error || 'Installation failed');
}
};
// Register Electron IPC listeners
window.electronAPI.onInstallDependenciesStart(handleInstallStart);
window.electronAPI.onInstallDependenciesLog(handleInstallLog);
window.electronAPI.onInstallDependenciesComplete(handleInstallComplete);
console.log('[useInstallationSetup] Installation listeners registered');
// Cleanup listeners on unmount
return () => {
window.electronAPI.removeAllListeners('install-dependencies-start');
window.electronAPI.removeAllListeners('install-dependencies-log');
window.electronAPI.removeAllListeners('install-dependencies-complete');
};
}, [startInstallation, addLog, setSuccess, setError, setInitState]);
};

View file

@ -0,0 +1,216 @@
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
// Define all possible installation states
export type InstallationState =
| 'idle'
| 'checking-permissions'
| 'showing-carousel'
| 'installing'
| 'error'
| 'completed';
// Installation log entry
export interface InstallationLog {
type: 'stdout' | 'stderr';
data: string;
timestamp: Date;
}
// Installation store state
interface InstallationStoreState {
// Core state
state: InstallationState;
progress: number;
logs: InstallationLog[];
error?: string;
isVisible: boolean;
// Actions
startInstallation: () => void;
addLog: (log: InstallationLog) => void;
setSuccess: () => void;
setError: (error: string) => void;
retryInstallation: () => void;
completeSetup: () => void;
updateProgress: (progress: number) => void;
setVisible: (visible: boolean) => void;
reset: () => void;
// Async actions
performInstallation: () => Promise<void>;
exportLog: () => Promise<void>;
}
// Initial state
const initialState = {
state: 'idle' as InstallationState,
progress: 20,
logs: [] as InstallationLog[],
error: undefined,
isVisible: false,
};
// Create the installation store
export const useInstallationStore = create<InstallationStoreState>()(
subscribeWithSelector(
(set, get) => ({
// Initial state
...initialState,
// Basic actions
startInstallation: () =>
set({
state: 'installing',
progress: 20,
logs: [],
error: undefined,
isVisible: true,
}),
addLog: (log: InstallationLog) =>
set((state) => {
const newProgress = Math.min(state.progress + 5, 90);
return {
logs: [...state.logs, log],
progress: newProgress,
};
}),
setSuccess: () =>
set({
state: 'completed',
progress: 100,
}),
setError: (error: string) =>
set((state) => ({
state: 'error',
error,
logs: [
...state.logs,
{
type: 'stderr',
data: error,
timestamp: new Date(),
},
],
})),
retryInstallation: () => {
set({
...initialState,
isVisible: true,
state: 'installing',
});
get().performInstallation();
},
completeSetup: () =>
set({
state: 'completed',
isVisible: false,
}),
updateProgress: (progress: number) =>
set({ progress }),
setVisible: (visible: boolean) =>
set({ isVisible: visible }),
reset: () =>
set(initialState),
// Async actions
performInstallation: async () => {
const { startInstallation, setSuccess, setError } = get();
try {
startInstallation();
const result = await window.electronAPI.checkAndInstallDepsOnUpdate();
if (result.success) {
setSuccess();
// Update auth store
const { useAuthStore } = await import('./authStore');
useAuthStore.getState().setInitState('done');
} else {
setError('Installation failed');
}
} catch (error) {
setError(error instanceof Error ? error.message : 'Unknown error');
}
},
exportLog: async () => {
try {
const response = await window.electronAPI.exportLog();
if (!response.success) {
alert('Export cancelled: ' + response.error);
return;
}
if (response.savedPath) {
window.location.href = 'https://github.com/eigent-ai/eigent/issues/new/choose';
alert('Log saved: ' + response.savedPath);
}
} catch (e: any) {
alert('Export error: ' + e.message);
}
},
})
)
);
// Computed selectors
export const useLatestLog = () => useInstallationStore(state =>
state.logs[state.logs.length - 1]
);
export const useInstallationActions = () => useInstallationStore(state => ({
startInstallation: state.startInstallation,
retryInstallation: state.retryInstallation,
completeSetup: state.completeSetup,
performInstallation: state.performInstallation,
exportLog: state.exportLog,
}));
// Combined hook for components that need multiple pieces of state
export const useInstallationStatus = () => {
const state = useInstallationStore(state => state.state);
const isVisible = useInstallationStore(state => state.isVisible);
return {
isInstalling: state === 'installing',
installationState: state,
shouldShowInstallScreen: isVisible && state !== 'completed',
isInstallationComplete: state === 'completed',
canRetry: state === 'error',
};
};
// Hook for the main installation UI component
export const useInstallationUI = () => {
const state = useInstallationStore(state => state.state);
const progress = useInstallationStore(state => state.progress);
const logs = useInstallationStore(state => state.logs);
const error = useInstallationStore(state => state.error);
const isVisible = useInstallationStore(state => state.isVisible);
const performInstallation = useInstallationStore(state => state.performInstallation);
const retryInstallation = useInstallationStore(state => state.retryInstallation);
const exportLog = useInstallationStore(state => state.exportLog);
return {
installationState: state,
progress,
latestLog: logs[logs.length - 1],
error,
isInstalling: state === 'installing',
shouldShowInstallScreen: isVisible && state !== 'completed',
canRetry: state === 'error',
performInstallation,
retryInstallation,
exportLog,
};
};

View file

@ -45,9 +45,16 @@ interface ElectronAPI {
envRemove: (email: string, key: string) => Promise<any>;
getEnvPath: (email: string) => Promise<string>;
executeCommand: (command: string,email:string) => Promise<{ success: boolean; stdout?: string; stderr?: string; error?: string }>;
installDependencies: () => Promise<{ success: boolean; error?: string }>;
frontendReady: () => Promise<{ success: boolean; error?: string }>;
checkAndInstallDepsOnUpdate: () => Promise<{ success: boolean; error?: string }>;
checkInstallBrowser: () => Promise<{ data:any[] }>;
getInstallationStatus: () => Promise<{
success: boolean;
isInstalling?: boolean;
hasLockFile?: boolean;
installedExists?: boolean;
timestamp?: number;
error?: string
}>;
onInstallDependenciesStart: (callback: () => void) => void;
onInstallDependenciesLog: (callback: (data: { type: string; data: string }) => void) => void;
onInstallDependenciesComplete: (callback: (data: { success: boolean; code?: number; error?: string }) => void) => void;