From c4f4de6c2eaed69afd8d2792e1f7fe5dc52e22d8 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Thu, 18 Sep 2025 19:54:47 +0300 Subject: [PATCH 01/17] refactor: isolate install logic --- electron/main/index.ts | 69 +--------- electron/main/init.ts | 165 +----------------------- electron/main/install-deps.ts | 234 ++++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 230 deletions(-) create mode 100644 electron/main/install-deps.ts diff --git a/electron/main/index.ts b/electron/main/index.ts index 9d87c76ac..571834c8c 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -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, installDependencies } 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 { - 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; @@ -1006,7 +943,7 @@ async function createWindow() { update(win); // ==================== check tool installed ==================== - let res = await checkAndInstallDepsOnUpdate(); + let res = await checkAndInstallDepsOnUpdate(win); if (!res) { log.info('checkAndInstallDepsOnUpdate,install dependencies failed'); win.webContents.send('install-dependencies-complete', { success: false, code: 2 }); diff --git a/electron/main/init.ts b/electron/main/init.ts index 1902fcad2..734d41fe0 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -10,7 +10,7 @@ import { promisify } from 'util' 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; } @@ -31,51 +31,6 @@ export async function checkToolInstalled() { } -/** - * 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 => { - if (await isBinaryExists(toolName)) { - return true; - } - - console.log(`start install ${toolName}`); - await runInstallScript(scriptName); - const installed = await isBinaryExists(toolName); - - const mainWindow = getMainWindow(); - 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 (script exit code 2)`, - }); - } - } - - 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); - }) - -} - // export async function installDependencies() { // return new Promise(async (resolve, reject) => { // console.log('start install dependencies') @@ -157,124 +112,6 @@ export async function installCommandTool() { // }) // }) // } -export async function installDependencies() { - return new Promise(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((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 { console.log('start fastapi') diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts new file mode 100644 index 000000000..5797cc366 --- /dev/null +++ b/electron/main/install-deps.ts @@ -0,0 +1,234 @@ +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' + +const userData = app.getPath('userData'); +const versionFile = path.join(userData, 'version.txt'); + +// Read last run version and install dependencies on update +export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { + 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 + } + }) +} + +/** + * 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 => { + if (await isBinaryExists(toolName)) { + return true; + } + + console.log(`start install ${toolName}`); + await runInstallScript(scriptName); + const installed = await isBinaryExists(toolName); + + const mainWindow = getMainWindow(); + 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 (script exit code 2)`, + }); + } + } + + 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); + }) +} + +export async function installDependencies() { + const { spawn } = await import('child_process'); + return new Promise(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((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) + } + }) +} From bd875d1aeb0363fd5016141eeec1e1ac512d5e9c Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Thu, 18 Sep 2025 21:14:30 +0300 Subject: [PATCH 02/17] refactor: use class to organize install --- electron/main/install-deps.ts | 240 +++++++++++++++++++--------------- 1 file changed, 135 insertions(+), 105 deletions(-) diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 5797cc366..3875da7e4 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -4,6 +4,7 @@ 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' const userData = app.getPath('userData'); const versionFile = path.join(userData, 'version.txt'); @@ -76,7 +77,7 @@ export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { - const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise => { + const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise => { if (await isBinaryExists(toolName)) { return true; } @@ -115,120 +116,149 @@ export async function installCommandTool() { }) } -export async function installDependencies() { - const { spawn } = await import('child_process'); - return new Promise(async (resolve, reject) => { - console.log('start install dependencies') +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/'] - const mainWindow = getMainWindow(); - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('install-dependencies-start'); +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'), + } + }) + } + + /**Handle stdout data */ + onStdout() { + this.node_process.stdout.on('data', (data:any) => { + log.info(`Script output: ${data}`) + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('install-dependencies-log', { type: 'stdout', data: data.toString() }); + } + }) } - const isInstalCommandTool = await installCommandTool() - if (!isInstalCommandTool) { - resolve(false) - return + /**Handle stderr data */ + onStderr() { + this.node_process.stderr.on('data', (data:any) => { + log.error(`Script error: ${data}`) + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('install-dependencies-log', { type: 'stderr', data: data.toString() }); + } + }) } - const uv_path = await getBinaryPath('uv') - const backendPath = getBackendPath() - - if (!fs.existsSync(backendPath)) { - fs.mkdirSync(backendPath, { recursive: true }) + /**Handle process close event */ + onClose(resolveInner:(code: number | null) => void) { + this.node_process.on('close', resolveInner); } - 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((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) - } - - }) + /** + * 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, '') } - // 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 + /**Clean installing Lock Path */ + static cleanLockPath() { + if (fs.existsSync(installingLockPath)) { + fs.unlinkSync(installingLockPath); + } } +} - // 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) - } +const runInstall = (extraArgs: string[]) => { + const installLogs = new InstallLogs(extraArgs); + return new Promise((resolveInner, rejectInner) => { + try { + installLogs.onStdout(); + installLogs.onStderr(); + installLogs.onClose((code) => { + console.log('install dependencies end', code === 0) + InstallLogs.cleanLockPath() + resolveInner(code === 0) + }) + } catch (err) { + log.error('run install failed', err) + // Clean up uv_installing.lock file if installation fails + InstallLogs.cleanLockPath(); + rejectInner(err) + } + }) +} + +export async function installDependencies() { + uv_path = await getBinaryPath('uv'); + const handleCompletion = { + spawnBabel: (type:"mirror"|"main"="main") => { + fs.writeFileSync(installedLockPath, '') + log.info('Script completed successfully') + console.log(`Install Dependencies completed ${type}`) + spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath }) + } + } + + return new Promise(async (resolve, reject) => { + console.log('start install dependencies') + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('install-dependencies-start'); + } else { + resolve(false) + return + } + + const isInstalCommandTool = await installCommandTool() + if (!isInstalCommandTool) { + resolve(false) + return + } + + // Set Installing Lock Files + InstallLogs.setLockPath(); + + // try default install + const installSuccess = await runInstall([]) + if (installSuccess) { + handleCompletion.spawnBabel() + resolve(true) + return + } + + // try mirror install + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone + let mirrorInstallSuccess = false + mirrorInstallSuccess = (timezone === 'Asia/Shanghai')? await runInstall(proxyArgs) :await runInstall([]) + + if (mirrorInstallSuccess) { + handleCompletion.spawnBabel("mirror") + 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) + } }) } From 9b1f0efe17cb01486b64f9a7b36dc3a330e7bbc9 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Thu, 18 Sep 2025 21:28:07 +0300 Subject: [PATCH 03/17] feat: update promise return type to add logs --- electron/main/index.ts | 12 ++++--- electron/main/init.ts | 9 ++--- electron/main/install-deps.ts | 66 +++++++++++++++++++++-------------- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 571834c8c..1e048640c 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -18,7 +18,8 @@ import kill from 'tree-kill'; import { zipFolder } from './utils/log' import axios from 'axios'; import FormData from 'form-data'; -import { checkAndInstallDepsOnUpdate, installDependencies } from './install-deps' +import { checkAndInstallDepsOnUpdate, installDependencies, PromiseReturnType } from './install-deps' +import e from 'express' const userData = app.getPath('userData'); @@ -943,12 +944,13 @@ async function createWindow() { update(win); // ==================== check tool installed ==================== - let res = await checkAndInstallDepsOnUpdate(win); - if (!res) { - log.info('checkAndInstallDepsOnUpdate,install dependencies failed'); + let res:PromiseReturnType = await checkAndInstallDepsOnUpdate(win); + if (!res.success) { + log.info(res.message); win.webContents.send('install-dependencies-complete', { success: false, code: 2 }); return; - } + } + log.info("[DEPS INSTALL] Dependency Success: ", res.message); await checkAndStartBackend(); } diff --git a/electron/main/init.ts b/electron/main/init.ts index 734d41fe0..57d596f3e 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -6,6 +6,7 @@ import path from 'path' import * as net from "net"; import { ipcMain, BrowserWindow, app } from 'electron' import { promisify } from 'util' +import { PromiseReturnType } from "./install-deps"; const execAsync = promisify(exec); @@ -17,16 +18,16 @@ export function getMainWindow(): BrowserWindow | null { export async function checkToolInstalled() { - return new Promise(async (resolve, reject) => { + return new Promise(async (resolve, reject) => { if (!(await isBinaryExists('uv'))) { - resolve(false) + resolve({success: false, message: "uv doesn't exist"}) } if (!(await isBinaryExists('bun'))) { - resolve(false) + resolve({success: false, message: "Bun doesn't exist"}) } - resolve(true) + resolve({success: true, message: "Tools exist already"}) }) } diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 3875da7e4..172d3a0b4 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -9,8 +9,13 @@ import { spawn } from 'child_process' const userData = app.getPath('userData'); const versionFile = path.join(userData, 'version.txt'); +export type PromiseReturnType = { + message: string; + success: boolean; +} + // Read last run version and install dependencies on update -export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { +export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { const currentVersion = app.getVersion(); return new Promise(async (resolve, reject) => { try { @@ -51,22 +56,22 @@ export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { return new Promise(async (resolve, reject) => { const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise => { if (await isBinaryExists(toolName)) { - return true; + return { message: `${toolName} already installed`, success: true }; } console.log(`start install ${toolName}`); @@ -102,17 +107,23 @@ export async function installCommandTool() { } } - return installed; + return { + message: installed ? `${toolName} installed successfully` : `${toolName} installation failed`, + success: installed + }; }; - if (!(await ensureInstalled('uv', 'install-uv.js'))) { - return reject("uv install failed"); + const uvResult = await ensureInstalled('uv', 'install-uv.js'); + if (!uvResult.success) { + return reject({ message: uvResult.message, success: false }); } - if (!(await ensureInstalled('bun', 'install-bun.js'))) { - return reject("bun install failed"); + + const bunResult = await ensureInstalled('bun', 'install-bun.js'); + if (!bunResult.success) { + return reject({ message: bunResult.message, success: false }); } - return resolve(true); + return resolve({ message: "Command tools installed successfully", success: true }); }) } @@ -197,18 +208,21 @@ const runInstall = (extraArgs: string[]) => { installLogs.onClose((code) => { console.log('install dependencies end', code === 0) InstallLogs.cleanLockPath() - resolveInner(code === 0) + 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(err) + rejectInner({ message: `Installation failed: ${err}`, success: false }) } }) } -export async function installDependencies() { +export async function installDependencies(): Promise { uv_path = await getBinaryPath('uv'); const handleCompletion = { spawnBabel: (type:"mirror"|"main"="main") => { @@ -224,13 +238,13 @@ export async function installDependencies() { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('install-dependencies-start'); } else { - resolve(false) + resolve({ message: "Main window not available", success: false }) return } const isInstalCommandTool = await installCommandTool() - if (!isInstalCommandTool) { - resolve(false) + if (!isInstalCommandTool.success) { + resolve({ message: "Command tool installation failed", success: false }) return } @@ -239,26 +253,26 @@ export async function installDependencies() { // try default install const installSuccess = await runInstall([]) - if (installSuccess) { + if (installSuccess.success) { handleCompletion.spawnBabel() - resolve(true) + resolve({ message: "Dependencies installed successfully", success: true }) return } // try mirror install const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone - let mirrorInstallSuccess = false + let mirrorInstallSuccess: PromiseReturnType = { message: "", success: false } mirrorInstallSuccess = (timezone === 'Asia/Shanghai')? await runInstall(proxyArgs) :await runInstall([]) - if (mirrorInstallSuccess) { + if (mirrorInstallSuccess.success) { handleCompletion.spawnBabel("mirror") - resolve(true) + resolve({ message: "Dependencies installed successfully with mirror", success: 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) + resolve({ message: "Both default and mirror install failed", success: false }) } }) } From 72a138c8c74449c2cfa45e0de3f69de56697a6c8 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Thu, 18 Sep 2025 21:28:18 +0300 Subject: [PATCH 04/17] enhane: update logs --- electron/main/index.ts | 28 ++++++++++++++-------------- electron/main/install-deps.ts | 18 +++++++++--------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 1e048640c..73e080470 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -201,20 +201,20 @@ export const handleDependencyInstallation = async () => { try { log.info(' start install dependencies...'); - const isSuccess = await installDependencies(); - if (!isSuccess) { - log.error(' install dependencies failed'); + const isSuccess:PromiseReturnType = await installDependencies(); + if (!isSuccess.success) { + log.error('[DEPS INSTALL] install dependencies failed '+isSuccess.message); return { success: false, error: 'install dependencies failed' }; } - log.info(' install dependencies success, check tool installed status...'); + log.info('[DEPS INSTALL] 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...'); + log.info('[DEPS INSTALL] isToolInstalled && !python_process', isToolInstalled.success && !python_process); + if (isToolInstalled.success && !python_process) { + log.info('[DEPS INSTALL] tool installed, start backend service...'); python_process = await startBackend((port) => { backendPort = port; - log.info(' backend service start success', { port }); + log.info('[DEPS INSTALL] backend service start success', { port }); }); // Notify frontend to install success @@ -223,18 +223,18 @@ export const handleDependencyInstallation = async () => { } python_process?.on('exit', (code, signal) => { - log.info(' python process exit', { code, signal }); + log.info('[DEPS INSTALL] python process exit', { code, signal }); }); - } else if (!isToolInstalled) { - log.warn(' tool not installed, skip backend start'); + } else if (!isToolInstalled.success) { + log.warn('[DEPS INSTALL] tool not installed, skip backend start'+isToolInstalled.message); } else { - log.info(' backend process already exist, skip start'); + log.info('[DEPS INSTALL] backend process already exist, skip start'); } - log.info(' install dependencies complete'); + log.info('[DEPS INSTALL] install dependencies complete'); return { success: true }; } catch (error: any) { - log.error(' install dependencies error:', error); + log.error('[DEPS INSTALL] install dependencies error:', error); if (win && !win.isDestroyed()) { win.webContents.send('install-dependencies-complete', { success: false, code: 2 }); } diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 172d3a0b4..f07faf08e 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -19,7 +19,7 @@ export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { try { - log.info(' start check version', { currentVersion }); + log.info('[DEPS INSTALL] start check version', { currentVersion }); // Check if version file exists const versionExists = fs.existsSync(versionFile); @@ -27,14 +27,14 @@ export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { - log.info(`Script output: ${data}`) + log.info(`[DEPS INSTALL] Script output: ${data}`) if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('install-dependencies-log', { type: 'stdout', data: data.toString() }); } @@ -227,7 +227,7 @@ export async function installDependencies(): Promise { const handleCompletion = { spawnBabel: (type:"mirror"|"main"="main") => { fs.writeFileSync(installedLockPath, '') - log.info('Script completed successfully') + log.info('[DEPS INSTALL] Script completed successfully') console.log(`Install Dependencies completed ${type}`) spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath }) } From bc0d179e46f133a2ad103477adde41f8949458a2 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Fri, 19 Sep 2025 00:22:26 +0300 Subject: [PATCH 05/17] enhance: checkAndInstallDepsOnUpdate --- electron/main/install-deps.ts | 74 ++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index f07faf08e..7e54715fb 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -17,20 +17,39 @@ export type PromiseReturnType = { // Read last run version and install dependencies on update export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { const currentVersion = app.getVersion(); - return new Promise(async (resolve, reject) => { - try { - log.info('[DEPS INSTALL] start check version', { currentVersion }); - + let savedVersion = ''; + const checkInstallOperations = { + getSavedVersion: ():boolean => { // Check if version file exists const versionExists = fs.existsSync(versionFile); - let savedVersion = ''; - 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' + }); + } + }, + 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 (!versionExists || savedVersion !== currentVersion) { @@ -41,24 +60,16 @@ export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { export async function installDependencies(): Promise { uv_path = await getBinaryPath('uv'); - const handleCompletion = { - spawnBabel: (type:"mirror"|"main"="main") => { + const handleInstallOperations = { + spawnBabel: (message:"mirror"|"main"="main") => { fs.writeFileSync(installedLockPath, '') log.info('[DEPS INSTALL] Script completed successfully') - console.log(`Install Dependencies completed ${type}`) + console.log(`Install Dependencies completed ${message}`) spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath }) + }, + notifyInstallDependenciesPage: ():boolean => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('install-dependencies-start'); + return true; + } else { + log.warn('[DEPS INSTALL] Main window not available, continuing installation without UI updates'); + return false; + } } } return new Promise(async (resolve, reject) => { console.log('start install dependencies') - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('install-dependencies-start'); - } else { - resolve({ message: "Main window not available", success: false }) - return + const mainWindowAvailable = handleInstallOperations.notifyInstallDependenciesPage(); + + if (!mainWindowAvailable) { + log.info('[DEPS INSTALL] Proceeding with installation without UI notifications'); } const isInstalCommandTool = await installCommandTool() @@ -254,7 +274,7 @@ export async function installDependencies(): Promise { // try default install const installSuccess = await runInstall([]) if (installSuccess.success) { - handleCompletion.spawnBabel() + handleInstallOperations.spawnBabel() resolve({ message: "Dependencies installed successfully", success: true }) return } @@ -265,7 +285,7 @@ export async function installDependencies(): Promise { mirrorInstallSuccess = (timezone === 'Asia/Shanghai')? await runInstall(proxyArgs) :await runInstall([]) if (mirrorInstallSuccess.success) { - handleCompletion.spawnBabel("mirror") + handleInstallOperations.spawnBabel("mirror") resolve({ message: "Dependencies installed successfully with mirror", success: true }) } else { log.error('Both default and mirror install failed') From 174f4291cf94b6d7b08e0d6f0a7503ee4cedc215 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Fri, 19 Sep 2025 12:44:02 +0300 Subject: [PATCH 06/17] fix: redirect false error logs --- electron/main/index.ts | 2 +- electron/main/install-deps.ts | 67 +++++++++++-------- .../InstallStep/InstallDependencies.tsx | 17 ++++- 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 73e080470..218823d2f 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -946,7 +946,7 @@ async function createWindow() { // ==================== check tool installed ==================== let res:PromiseReturnType = await checkAndInstallDepsOnUpdate(win); if (!res.success) { - log.info(res.message); + log.info("[DEPS INSTALL] Dependency Error: ", res.message); win.webContents.send('install-dependencies-complete', { success: false, code: 2 }); return; } diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 7e54715fb..d573a490f 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -39,6 +39,8 @@ export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { @@ -93,35 +95,35 @@ export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { return new Promise(async (resolve, reject) => { - const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise => { - if (await isBinaryExists(toolName)) { - return { message: `${toolName} already installed`, success: true }; - } + const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise => { + if (await isBinaryExists(toolName)) { + return { message: `${toolName} already installed`, success: true }; + } - console.log(`start install ${toolName}`); - await runInstallScript(scriptName); - const installed = await isBinaryExists(toolName); + console.log(`start install ${toolName}`); + await runInstallScript(scriptName); + const installed = await isBinaryExists(toolName); const mainWindow = getMainWindow(); if (mainWindow && !mainWindow.isDestroyed()) { - if (installed) { + if (installed) { mainWindow.webContents.send('install-dependencies-log', { - type: 'stdout', - data: `${toolName} installed successfully`, - }); - } else { + type: 'stdout', + data: `${toolName} installed successfully`, + }); + } else { mainWindow.webContents.send('install-dependencies-complete', { - success: false, - code: 2, - error: `${toolName} installation failed (script exit code 2)`, + success: false, + code: 2, + error: `${toolName} installation failed (script exit code 2)`, }); } - } + } - return { - message: installed ? `${toolName} installed successfully` : `${toolName} installation failed`, - success: installed - }; + return { + message: installed ? `${toolName} installed successfully` : `${toolName} installation failed`, + success: installed + }; }; const uvResult = await ensureInstalled('uv', 'install-uv.js'); @@ -164,25 +166,32 @@ class InstallLogs { } }) } + + /**Display filtered logs based on severity */ + displayFilteredLogs(data:String) { + if (!data) return; + const msg = data.toString().trimEnd(); + 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}`); // treat uvicorn info logs as normal + // Use immediate send for stdout logs (non-critical) + safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() }); + } + } /**Handle stdout data */ onStdout() { this.node_process.stdout.on('data', (data:any) => { - log.info(`[DEPS INSTALL] Script output: ${data}`) - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('install-dependencies-log', { type: 'stdout', data: data.toString() }); - console.log("data from installer", data.toString()) - } + this.displayFilteredLogs(data); }) } /**Handle stderr data */ onStderr() { this.node_process.stderr.on('data', (data:any) => { - log.error(`Script error: ${data}`) - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('install-dependencies-log', { type: 'stderr', data: data.toString() }); - } + this.displayFilteredLogs(data); }) } diff --git a/src/components/InstallStep/InstallDependencies.tsx b/src/components/InstallStep/InstallDependencies.tsx index 24b8212cf..bd27a0dfd 100644 --- a/src/components/InstallStep/InstallDependencies.tsx +++ b/src/components/InstallStep/InstallDependencies.tsx @@ -76,8 +76,14 @@ export const InstallDependencies: React.FC<{ setInitState("done"); } else { setStatus("error"); - console.error("dependencies installation failed:", data?.code); - console.error("dependencies installation failed:", data?.error); + console.error("dependencies installation failed:", data); + + const newLog: InstallLog = { + type: "stderr", + data: data?.error || "Unknown error", + timestamp: new Date(), + }; + setLogs((prev) => [...prev, newLog]); } } ); @@ -159,6 +165,13 @@ export const InstallDependencies: React.FC<{ {t("layout.installation-failed")} +
+ {
+ + [{logs.at(-1)?.timestamp.toLocaleTimeString()}] {logs.at(-1)?.data} + +
} +
From b7bdc3db57115062bf0bb4fb3fee0453eed8f05b Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Fri, 19 Sep 2025 13:38:52 +0300 Subject: [PATCH 07/17] enhance: add safe window send --- electron/main/install-deps.ts | 40 ++++++++++------------ electron/main/utils/safeWebContentsSend.ts | 20 +++++++++++ 2 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 electron/main/utils/safeWebContentsSend.ts diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index d573a490f..52fb7a75b 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -5,6 +5,7 @@ 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'); @@ -104,20 +105,17 @@ export async function installCommandTool(): Promise { await runInstallScript(scriptName); const installed = await isBinaryExists(toolName); - const mainWindow = getMainWindow(); - if (mainWindow && !mainWindow.isDestroyed()) { if (installed) { - mainWindow.webContents.send('install-dependencies-log', { - type: 'stdout', - data: `${toolName} installed successfully`, - }); + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: `${toolName} installed successfully`, + }); } else { - mainWindow.webContents.send('install-dependencies-complete', { - success: false, - code: 2, - error: `${toolName} installation failed (script exit code 2)`, - }); - } + safeMainWindowSend('install-dependencies-complete', { + success: false, + code: 2, + error: `${toolName} installation failed (script exit code 2)`, + }); } return { @@ -175,8 +173,7 @@ class InstallLogs { log.error(`BACKEND: [DEPS INSTALL] ${msg}`); safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() }); } else { - log.info(`BACKEND: [DEPS INSTALL] ${msg}`); // treat uvicorn info logs as normal - // Use immediate send for stdout logs (non-critical) + log.info(`BACKEND: [DEPS INSTALL] ${msg}`); safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() }); } } @@ -253,13 +250,11 @@ export async function installDependencies(): Promise { spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath }) }, notifyInstallDependenciesPage: ():boolean => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('install-dependencies-start'); - return true; - } else { + const success = safeMainWindowSend('install-dependencies-start'); + if (!success) { log.warn('[DEPS INSTALL] Main window not available, continuing installation without UI updates'); - return false; } + return success; } } @@ -298,9 +293,10 @@ export async function installDependencies(): Promise { resolve({ message: "Dependencies installed successfully with mirror", success: 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' }); - } + safeMainWindowSend('install-dependencies-complete', { + success: false, + error: 'Both default and mirror install failed' + }); resolve({ message: "Both default and mirror install failed", success: false }) } }) diff --git a/electron/main/utils/safeWebContentsSend.ts b/electron/main/utils/safeWebContentsSend.ts new file mode 100644 index 000000000..0e72da65d --- /dev/null +++ b/electron/main/utils/safeWebContentsSend.ts @@ -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} \ No newline at end of file From 01acb458cc15e2a71d3827bd5ea961fd99b4e831 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Tue, 23 Sep 2025 21:31:22 +0300 Subject: [PATCH 08/17] fix: handle backend error --- electron/main/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 218823d2f..0c8560d50 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -947,7 +947,7 @@ async function createWindow() { 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 }); + win.webContents.send('install-dependencies-complete', { success: false, code: 2, error: res.message }); return; } log.info("[DEPS INSTALL] Dependency Success: ", res.message); @@ -1008,7 +1008,7 @@ const setupExternalLinkHandling = () => { // ==================== check and start backend ==================== const checkAndStartBackend = async () => { log.info('Checking and starting backend service...'); - + try { const isToolInstalled = await checkToolInstalled(); if (isToolInstalled) { log.info('Tool installed, starting backend service...'); @@ -1029,6 +1029,9 @@ const checkAndStartBackend = async () => { }); } else { log.warn('Tool not installed, cannot start backend service'); + } + } catch (error) { + log.debug("Cannot Start Backend due to ", error) } }; From 65bf0f546a5a4e7e7a642bf3a87f0acdcb7cd956 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Tue, 23 Sep 2025 21:34:46 +0300 Subject: [PATCH 09/17] enhance: remove duplicate install checks --- electron/main/index.ts | 73 ++++++++++++++------------------------- electron/preload/index.ts | 3 +- src/types/electron.d.ts | 3 +- 3 files changed, 27 insertions(+), 52 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 0c8560d50..fa2b45d5d 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -18,7 +18,7 @@ import kill from 'tree-kill'; import { zipFolder } from './utils/log' import axios from 'axios'; import FormData from 'form-data'; -import { checkAndInstallDepsOnUpdate, installDependencies, PromiseReturnType } from './install-deps' +import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps' import e from 'express' const userData = app.getPath('userData'); @@ -197,51 +197,6 @@ const checkManagerInstance = (manager: any, name: string) => { return manager; }; -export const handleDependencyInstallation = async () => { - try { - log.info(' start install dependencies...'); - - const isSuccess:PromiseReturnType = await installDependencies(); - if (!isSuccess.success) { - log.error('[DEPS INSTALL] install dependencies failed '+isSuccess.message); - return { success: false, error: 'install dependencies failed' }; - } - - log.info('[DEPS INSTALL] install dependencies success, check tool installed status...'); - const isToolInstalled = await checkToolInstalled(); - log.info('[DEPS INSTALL] isToolInstalled && !python_process', isToolInstalled.success && !python_process); - if (isToolInstalled.success && !python_process) { - log.info('[DEPS INSTALL] tool installed, start backend service...'); - python_process = await startBackend((port) => { - backendPort = port; - log.info('[DEPS INSTALL] 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('[DEPS INSTALL] python process exit', { code, signal }); - }); - } else if (!isToolInstalled.success) { - log.warn('[DEPS INSTALL] tool not installed, skip backend start'+isToolInstalled.message); - } else { - log.info('[DEPS INSTALL] backend process already exist, skip start'); - } - - log.info('[DEPS INSTALL] install dependencies complete'); - return { success: true }; - } catch (error: any) { - log.error('[DEPS INSTALL] 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', () => { @@ -872,8 +827,16 @@ 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 { @@ -884,6 +847,20 @@ function registerIpcHandlers() { } }); + 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(); } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 0c89d17d2..2c8c0b65f 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -62,8 +62,7 @@ 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'), onInstallDependenciesStart: (callback: () => void) => { ipcRenderer.on('install-dependencies-start', callback); diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index e1319bac5..b46ab6c05 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -45,8 +45,7 @@ interface ElectronAPI { envRemove: (email: string, key: string) => Promise; getEnvPath: (email: string) => Promise; 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[] }>; onInstallDependenciesStart: (callback: () => void) => void; onInstallDependenciesLog: (callback: (data: { type: string; data: string }) => void) => void; From d8c502df214124f01ee1e2d99b7f0506ad3facf2 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Tue, 23 Sep 2025 21:36:32 +0300 Subject: [PATCH 10/17] enhance: isolate intallation logic from component --- .../InstallStep/InstallDependencies.tsx | 202 +++--------------- src/hooks/useInstallationSetup.ts | 90 ++++++++ 2 files changed, 117 insertions(+), 175 deletions(-) create mode 100644 src/hooks/useInstallationSetup.ts diff --git a/src/components/InstallStep/InstallDependencies.tsx b/src/components/InstallStep/InstallDependencies.tsx index bd27a0dfd..5b913ba46 100644 --- a/src/components/InstallStep/InstallDependencies.tsx +++ b/src/components/InstallStep/InstallDependencies.tsx @@ -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,171 +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([]); - 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); - - const newLog: InstallLog = { - type: "stderr", - data: data?.error || "Unknown error", - timestamp: new Date(), - }; - setLogs((prev) => [...prev, newLog]); - } - } - ); - - // 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 ( - - - - {t("layout.installation-failed")} - -
- {
- - [{logs.at(-1)?.timestamp.toLocaleTimeString()}] {logs.at(-1)?.data} - -
} -
- - - -
-
- ); - } +export const InstallDependencies: React.FC = () => { + const { initState } = useAuthStore(); + const {t} = useTranslation(); + + const { + progress, + latestLog, + error, + isInstalling, + retryInstallation, + exportLog, + } = useInstallationUI(); return (
@@ -193,16 +43,18 @@ export const InstallDependencies: React.FC<{
{isInstalling ? "System Installing ..." : ""} - {logs.at(-1)?.data} + {latestLog?.data}
- +
@@ -227,7 +79,7 @@ export const InstallDependencies: React.FC<{ {t("layout.report-bug")} - diff --git a/src/hooks/useInstallationSetup.ts b/src/hooks/useInstallationSetup.ts new file mode 100644 index 000000000..f93e21366 --- /dev/null +++ b/src/hooks/useInstallationSetup.ts @@ -0,0 +1,90 @@ +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) { + console.log('[useInstallationSetup] Setting success state and initState to done'); + 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]); +}; \ No newline at end of file From 4de6eb13369f8b2659cf861d7168a2ed5b68818f Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Tue, 23 Sep 2025 21:37:29 +0300 Subject: [PATCH 11/17] enhance: isolate install logic from layout component --- .../InstallationErrorDialog.tsx | 50 ++++++++++++++ src/components/Layout/index.tsx | 69 +++++++++++-------- 2 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx diff --git a/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx b/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx new file mode 100644 index 000000000..bdfd2c1a0 --- /dev/null +++ b/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx @@ -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 ( + + + + {t("layout.installation-failed")} + +
+ { +
+ + Error: {error}
+ Log: {latestLog?.data} +
+
+ } +
+ + + +
+
+ ); +}; + +export default InstallationErrorDialog; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 685531bfe..2b4d645e1 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -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 (
- {initState === "done" && isFirstLaunch && !isInstalling && ( + {/* Onboarding animation */} + {shouldShowOnboarding && ( { - setIsFirstLaunch(false); - }} + onComplete={() => setIsFirstLaunch(false)} animationData={animationData} /> )} - {(initState !== "done" || isInstalling) && ( - + + {/* Installation screen */} + {shouldShowInstallScreen && } + + {/* Main app content */} + {shouldShowMainContent && ( + <> + + + )} - - + + {(error != "" && error !=undefined) && + + } + Date: Tue, 23 Sep 2025 21:40:02 +0300 Subject: [PATCH 12/17] feat: detectInstallationLogs when uvsync & uvcorn for post init downloads --- electron/main/index.ts | 42 ++++++++-------- electron/main/init.ts | 6 ++- electron/main/install-deps.ts | 93 ++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 24 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index fa2b45d5d..e070e1abd 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -921,7 +921,7 @@ async function createWindow() { update(win); // ==================== check tool installed ==================== - let res:PromiseReturnType = await checkAndInstallDepsOnUpdate(win); + let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win }); if (!res.success) { log.info("[DEPS INSTALL] Dependency Error: ", res.message); win.webContents.send('install-dependencies-complete', { success: false, code: 2, error: res.message }); @@ -986,26 +986,26 @@ const setupExternalLinkHandling = () => { const checkAndStartBackend = async () => { log.info('Checking and starting backend service...'); try { - 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 }); - } - - 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'); + 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 }); + } + + 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) diff --git a/electron/main/init.ts b/electron/main/init.ts index d1db6b8a7..f993b60d2 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -6,7 +6,7 @@ import path from 'path' import * as net from "net"; import { ipcMain, BrowserWindow, app } from 'electron' import { promisify } from 'util' -import { PromiseReturnType } from "./install-deps"; +import { detectInstallationLogs, PromiseReturnType } from "./install-deps"; const execAsync = promisify(exec); @@ -158,6 +158,9 @@ export async function startBackend(setPort?: (port: number) => void): Promise { 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")) { @@ -171,6 +174,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise { + //Implicitly runs uv sync const node_process = spawn( uv_path, ["run", "uvicorn", "main:api", "--port", port.toString(), "--loop", "asyncio"], diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 52fb7a75b..d4205c18e 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -15,8 +15,13 @@ export type PromiseReturnType = { success: boolean; } +interface checkInstallProps { + win:BrowserWindow|null; + forceInstall?:boolean +} // Read last run version and install dependencies on update -export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { +export const checkAndInstallDepsOnUpdate = async ({win, forceInstall=false}:checkInstallProps): +Promise => { const currentVersion = app.getVersion(); let savedVersion = ''; const checkInstallOperations = { @@ -55,7 +60,7 @@ export async function checkAndInstallDepsOnUpdate(win:BrowserWindow): Promise { } }) } + +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' + }); + } +} \ No newline at end of file From 2b567189ba975ba60f907205f61fca87a5942cf9 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Tue, 23 Sep 2025 21:40:19 +0300 Subject: [PATCH 13/17] feat: installationStore --- src/store/installationStore.ts | 216 +++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 src/store/installationStore.ts diff --git a/src/store/installationStore.ts b/src/store/installationStore.ts new file mode 100644 index 000000000..374d88965 --- /dev/null +++ b/src/store/installationStore.ts @@ -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; + exportLog: () => Promise; +} + +// 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()( + 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, + }; +}; \ No newline at end of file From 761d11dae28055bbb404608c6402bdca70c8b43a Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Tue, 23 Sep 2025 21:40:46 +0300 Subject: [PATCH 14/17] feat: getInstallationStatus event to catch up after rending registers --- electron/main/install-deps.ts | 30 ++++++++++++++++++++++++++++++ electron/preload/index.ts | 1 + src/types/electron.d.ts | 8 ++++++++ 3 files changed, 39 insertions(+) diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index d4205c18e..9c251b629 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -151,6 +151,36 @@ 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; diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 2c8c0b65f..d1c5e9e85 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -64,6 +64,7 @@ contextBridge.exposeInMainWorld('electronAPI', { // install dependencies related API 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); }, diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index b46ab6c05..470529c9e 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -47,6 +47,14 @@ interface ElectronAPI { executeCommand: (command: string,email:string) => Promise<{ success: boolean; stdout?: string; stderr?: string; 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; From b09e00d0dc2bd450d05cc6e927e01c51fe7ff574 Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Tue, 23 Sep 2025 21:46:29 +0300 Subject: [PATCH 15/17] chore: remove frontend log --- src/hooks/useInstallationSetup.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hooks/useInstallationSetup.ts b/src/hooks/useInstallationSetup.ts index f93e21366..c17cba94e 100644 --- a/src/hooks/useInstallationSetup.ts +++ b/src/hooks/useInstallationSetup.ts @@ -65,7 +65,6 @@ export const useInstallationSetup = () => { console.log('[useInstallationSetup] Install complete event received:', data); if (data.success) { - console.log('[useInstallationSetup] Setting success state and initState to done'); setSuccess(); setInitState('done'); } else { From 453800032e8e0b2b6e8bf40f1fd66d5a84aac8c6 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Mon, 29 Sep 2025 01:09:49 +0800 Subject: [PATCH 16/17] Fix #352 install enhanced PR404 --- backend/app/component/command.py | 2 +- electron/main/index.ts | 13 ++++++------- electron/main/install-deps.ts | 24 +++++++++++++----------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/backend/app/component/command.py b/backend/app/component/command.py index 1b320dc88..0f8e549a6 100644 --- a/backend/app/component/command.py +++ b/backend/app/component/command.py @@ -6,4 +6,4 @@ def bun(): def uv(): - return os.path.expanduser("~/.eigent/bin/uv") + return os.path.expanduser("~/.local/bin/uv") diff --git a/electron/main/index.ts b/electron/main/index.ts index e070e1abd..f9bfe0f04 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -19,7 +19,6 @@ import { zipFolder } from './utils/log' import axios from 'axios'; import FormData from 'form-data'; import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps' -import e from 'express' const userData = app.getPath('userData'); @@ -841,7 +840,7 @@ function registerIpcHandlers() { 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 }; } @@ -987,21 +986,21 @@ const checkAndStartBackend = async () => { log.info('Checking and starting backend service...'); try { const isToolInstalled = await checkToolInstalled(); - if (isToolInstalled) { + 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 { diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 9c251b629..1fbb4dde2 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -100,7 +100,7 @@ Promise => { * Check if command line tools are installed, install if not */ export async function installCommandTool(): Promise { - return new Promise(async (resolve, reject) => { + try { const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise => { if (await isBinaryExists(toolName)) { return { message: `${toolName} already installed`, success: true }; @@ -123,24 +123,26 @@ export async function installCommandTool(): Promise { }); } - return { + return { message: installed ? `${toolName} installed successfully` : `${toolName} installation failed`, - success: installed + success: installed }; }; const uvResult = await ensureInstalled('uv', 'install-uv.js'); if (!uvResult.success) { - return reject({ message: uvResult.message, success: false }); - } - - const bunResult = await ensureInstalled('bun', 'install-bun.js'); - if (!bunResult.success) { - return reject({ message: bunResult.message, success: false }); + return { message: uvResult.message, success: false }; } - return resolve({ message: "Command tools installed successfully", success: true }); - }) + 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; From b5e887a7161d0efca79365ee057e7ffed885d430 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Tue, 30 Sep 2025 23:55:49 +0800 Subject: [PATCH 17/17] update --- backend/app/component/command.py | 2 +- electron/main/init.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/app/component/command.py b/backend/app/component/command.py index 0f8e549a6..1b320dc88 100644 --- a/backend/app/component/command.py +++ b/backend/app/component/command.py @@ -6,4 +6,4 @@ def bun(): def uv(): - return os.path.expanduser("~/.local/bin/uv") + return os.path.expanduser("~/.eigent/bin/uv") diff --git a/electron/main/init.ts b/electron/main/init.ts index f993b60d2..709244035 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -21,10 +21,12 @@ export async function checkToolInstalled() { return new Promise(async (resolve, reject) => { if (!(await isBinaryExists('uv'))) { resolve({success: false, message: "uv doesn't exist"}) + return } if (!(await isBinaryExists('bun'))) { resolve({success: false, message: "Bun doesn't exist"}) + return } resolve({success: true, message: "Tools exist already"})