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) + } + }) +}