diff --git a/electron.vite.config.ts b/electron.vite.config.ts index aee44d0..0b587c1 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -11,11 +11,6 @@ export default defineConfig({ '@': resolve(__dirname, './src'), }, }, - define: { - 'process.env.VITE_DEV_SERVER_URL': JSON.stringify( - process.env.VITE_DEV_SERVER_URL - ), - }, }, preload: { plugins: [externalizeDepsPlugin()], diff --git a/eslint.config.ts b/eslint.config.ts index 78a7ab2..650706d 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -152,6 +152,15 @@ const config = [ 'no-console': 'error', + 'no-restricted-globals': [ + 'error', + { + name: 'process', + message: + 'Import specific properties from "process" instead of using the global: import { platform, env } from "process"', + }, + ], + '@typescript-eslint/no-inferrable-types': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/package.json b/package.json index b0e31ae..e02e10c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gerbil", "productName": "Gerbil", - "version": "1.4.0-beta.3", + "version": "1.4.0-beta.4", "description": "Run Large Language Models locally", "main": "out/main/index.js", "homepage": "./", diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 1f143a9..26678bc 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -1,4 +1,12 @@ -import { Group, ActionIcon, Box, Image, Select, AppShell } from '@mantine/core'; +import { + Group, + ActionIcon, + Box, + Image, + Select, + AppShell, + Tooltip, +} from '@mantine/core'; import { Minus, Square, X, Copy, Settings } from 'lucide-react'; import { useState } from 'react'; import { useInterfaceOptions } from '@/hooks/useInterfaceSelection'; @@ -138,20 +146,22 @@ export const TitleBar = ({ - setSettingsModalOpen(true)} - aria-label="Open settings" - tabIndex={-1} - style={{ - borderRadius: 0, - margin: '2px 1px 1px', - outline: 'none', - }} - > - - + + setSettingsModalOpen(true)} + aria-label="Open settings" + tabIndex={-1} + style={{ + borderRadius: 0, + margin: '2px 1px 1px', + outline: 'none', + }} + > + + + { const { @@ -14,14 +15,14 @@ export const UpdateButton = () => { installUpdate, } = useAppUpdateChecker(); + const [showDownload, setShowDownload] = useState(false); + if (!hasUpdate) return null; - const isButton = canAutoUpdate; - const isLink = !canAutoUpdate && releaseUrl; - let color: 'green' | 'blue' | 'orange' = 'orange'; - let label = 'New release available'; + let label = 'New release available - Click to view changelog'; let onClick: (() => void) | undefined; + let icon = ; if (isUpdateDownloaded) { color = 'green'; @@ -30,30 +31,53 @@ export const UpdateButton = () => { } else if (isDownloading) { color = 'blue'; label = 'Downloading update...'; - } else if (canAutoUpdate) { + icon = ; + } else if (showDownload && canAutoUpdate) { color = 'blue'; label = 'Download and install update'; - onClick = downloadUpdate; + onClick = () => { + downloadUpdate(); + setShowDownload(false); + }; + icon = ; + } else { + color = 'orange'; + label = canAutoUpdate + ? 'New release available - Click to view changelog, right-click to download' + : 'New release available - Click to view changelog'; + onClick = () => { + if (releaseUrl) { + window.electronAPI.app.openExternal(releaseUrl); + } + }; } + const handleContextMenu = (e: MouseEvent) => { + e.preventDefault(); + if (canAutoUpdate && !isDownloading && !isUpdateDownloaded) { + setShowDownload(true); + downloadUpdate(); + } + }; + return ( - - - + + + {icon} + + ); }; diff --git a/src/hooks/useAppUpdateChecker.ts b/src/hooks/useAppUpdateChecker.ts index 7ca88eb..7936384 100644 --- a/src/hooks/useAppUpdateChecker.ts +++ b/src/hooks/useAppUpdateChecker.ts @@ -42,6 +42,12 @@ export const useAppUpdateChecker = () => { setIsDownloading(true); await tryExecute(async () => { + const checkResult = await window.electronAPI.updater.checkForUpdates(); + if (!checkResult) { + setIsDownloading(false); + return; + } + const success = await window.electronAPI.updater.downloadUpdate(); if (success) { setIsUpdateDownloaded(true); diff --git a/src/main/cli.ts b/src/main/cli.ts index afb7c8d..fb95665 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import { spawn } from 'child_process'; +import { platform, exit, stdout, stderr, stdin, on } from 'process'; import { terminateProcess } from '@/utils/node/process'; import { pathExists, readJsonFile } from '@/utils/node/fs'; @@ -28,17 +29,17 @@ export async function handleCliMode(args: string[]) { console.error( 'Error: No binary found. Please run the GUI first to download the binary.' ); - process.exit(1); + exit(1); } if (!(await pathExists(currentBinary))) { console.error(`Error: Binary not found at: ${currentBinary}`); console.error('Please run the GUI to download and configure the binary.'); - process.exit(1); + exit(1); } return new Promise((resolve, reject) => { - const isWindows = process.platform === 'win32'; + const isWindows = platform === 'win32'; const child = spawn(currentBinary, args, { stdio: isWindows ? 'pipe' : 'inherit', @@ -50,24 +51,24 @@ export async function handleCliMode(args: string[]) { child.stderr?.setEncoding('utf8'); child.stdout?.on('data', (data) => { - process.stdout.write(data.toString()); + stdout.write(data.toString()); }); child.stderr?.on('data', (data) => { - process.stderr.write(data.toString()); + stderr.write(data.toString()); }); - if (child.stdin && process.stdin.readable) { - process.stdin.pipe(child.stdin); + if (child.stdin && stdin.readable) { + stdin.pipe(child.stdin); } } child.on('exit', (code, signal) => { if (signal) { console.log(`\nProcess terminated with signal: ${signal}`); - process.exit(128 + (signal === 'SIGTERM' ? 15 : 2)); + exit(128 + (signal === 'SIGTERM' ? 15 : 2)); } else if (code !== null) { - process.exit(code); + exit(code); } else { resolve(); } @@ -90,10 +91,10 @@ export async function handleCliMode(args: string[]) { } }; - process.on('SIGINT', handleSignal); - process.on('SIGTERM', handleSignal); - if (process.platform === 'win32') { - process.on('SIGBREAK', handleSignal); + on('SIGINT', handleSignal); + on('SIGTERM', handleSignal); + if (platform === 'win32') { + on('SIGBREAK', handleSignal); } }); } diff --git a/src/main/gui.ts b/src/main/gui.ts index cded5ce..d4563da 100644 --- a/src/main/gui.ts +++ b/src/main/gui.ts @@ -1,4 +1,5 @@ import { app } from 'electron'; +import { platform } from 'process'; import { createMainWindow, @@ -28,7 +29,7 @@ export async function initializeApp() { createMainWindow(); app.on('window-all-closed', () => { - if (process.platform === 'darwin') { + if (platform === 'darwin') { return; } diff --git a/src/main/index.ts b/src/main/index.ts index de886d6..6c242d8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,6 +1,7 @@ +import { argv, exit } from 'process'; import { getAppVersion } from '@/utils/node/fs'; -if (process.argv[1] === '--version') { +if (argv[1] === '--version') { (async () => { try { const version = await getAppVersion(); @@ -10,27 +11,27 @@ if (process.argv[1] === '--version') { // eslint-disable-next-line no-console console.log('unknown'); } - process.exit(0); + exit(0); })(); } else { (async () => { - const isCliMode = process.argv.includes('--cli'); + const isCliMode = argv.includes('--cli'); if (isCliMode) { try { const cliModule = await import('./cli'); - const args = process.argv.slice(process.argv.indexOf('--cli') + 1); + const args = argv.slice(argv.indexOf('--cli') + 1); try { await cliModule.handleCliMode(args); } catch (error) { // eslint-disable-next-line no-console console.error('CLI mode error:', error); - process.exit(1); + exit(1); } } catch (error) { // eslint-disable-next-line no-console console.error('Failed to load CLI module:', error); - process.exit(1); + exit(1); } } else { try { diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 3ddfb86..9a578b8 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,6 +1,7 @@ import { ipcMain, shell, app } from 'electron'; import { join } from 'path'; import * as os from 'os'; +import { platform, versions, arch } from 'process'; import type { MantineColorScheme } from '@mantine/core'; import { launchKoboldCpp, @@ -40,6 +41,7 @@ import { isNpxAvailable, getUvVersion, getSystemNodeVersion, + isAURInstallation, } from '@/main/modules/dependencies'; import { parseKoboldConfig } from '@/utils/node/kobold'; import { getMainWindow } from '@/main/modules/window'; @@ -137,7 +139,7 @@ export function setupIPCHandlers() { getAvailableBackends(includeDisabled) ); - ipcMain.handle('kobold:getPlatform', () => process.platform); + ipcMain.handle('kobold:getPlatform', () => platform); ipcMain.handle('kobold:launchKoboldCpp', (_, args) => launchKoboldCppWithCustomFrontends(args) @@ -173,13 +175,13 @@ export function setupIPCHandlers() { return { appVersion, - electronVersion: process.versions.electron, - nodeVersion: process.versions.node, - chromeVersion: process.versions.chrome, - v8Version: process.versions.v8, + electronVersion: versions.electron, + nodeVersion: versions.node, + chromeVersion: versions.chrome, + v8Version: versions.v8, osVersion: os.release(), - platform: process.platform, - arch: process.arch, + platform, + arch, nodeJsSystemVersion, uvVersion, }; @@ -285,8 +287,17 @@ export function setupIPCHandlers() { ipcMain.handle('app:isUpdateDownloaded', () => isUpdateDownloaded()); - ipcMain.handle( - 'app:canAutoUpdate', - () => process.platform !== 'linux' || Boolean(process.env.APPIMAGE) - ); + ipcMain.handle('app:canAutoUpdate', async () => { + if (!app.isPackaged) return false; + + if (platform === 'linux' && (await isAURInstallation())) { + return false; + } + + return ( + platform === 'win32' || platform === 'darwin' || platform === 'linux' + ); + }); + + ipcMain.handle('app:isAURInstallation', () => isAURInstallation()); } diff --git a/src/main/modules/autoUpdater.ts b/src/main/modules/autoUpdater.ts index 76171b0..a24e4d8 100644 --- a/src/main/modules/autoUpdater.ts +++ b/src/main/modules/autoUpdater.ts @@ -2,6 +2,7 @@ import { autoUpdater } from 'electron-updater'; import { app } from 'electron'; import { logError } from '@/main/modules/logging'; import { safeExecute } from '@/utils/node/logger'; +import { isDevelopment } from '@/utils/node/environment'; export interface UpdateInfo { version: string; @@ -34,17 +35,33 @@ function setupAutoUpdater() { setupAutoUpdater(); -export const checkForUpdates = async () => - (await safeExecute( - () => autoUpdater.checkForUpdates(), - 'Failed to check for updates' - )) !== null; +export const checkForUpdates = async () => { + if (isDevelopment) { + logError('Auto-updater: Cannot check for updates in development mode'); + return false; + } -export const downloadUpdate = async () => - (await safeExecute( - () => autoUpdater.downloadUpdate(), - 'Failed to download update' - )) !== null; + return ( + (await safeExecute( + () => autoUpdater.checkForUpdates(), + 'Failed to check for updates' + )) !== null + ); +}; + +export const downloadUpdate = async () => { + if (isDevelopment) { + logError('Auto-updater: Cannot download updates in development mode'); + return false; + } + + return ( + (await safeExecute( + () => autoUpdater.downloadUpdate(), + 'Failed to download update' + )) !== null + ); +}; export function quitAndInstall() { if (updateDownloaded) { diff --git a/src/main/modules/binary.ts b/src/main/modules/binary.ts index 0da3db1..b1ad1d3 100644 --- a/src/main/modules/binary.ts +++ b/src/main/modules/binary.ts @@ -1,4 +1,5 @@ import { join, dirname } from 'path'; +import { platform } from 'process'; import { pathExists } from '@/utils/node/fs'; import { getCurrentBinaryInfo } from './koboldcpp'; import { detectGPUCapabilities, detectCPU } from './hardware'; @@ -26,7 +27,6 @@ async function detectBackendSupportFromPath(koboldBinaryPath: string) { const binaryDir = dirname(koboldBinaryPath); const internalDir = join(binaryDir, '_internal'); - const platform = process.platform; const libExtension = platform === 'win32' ? '.dll' : '.so'; const hasKoboldCppLib = async (name: string): Promise => { diff --git a/src/main/modules/comfyui.ts b/src/main/modules/comfyui.ts index 0ceeae0..b4c89bc 100644 --- a/src/main/modules/comfyui.ts +++ b/src/main/modules/comfyui.ts @@ -1,6 +1,7 @@ import { spawn } from 'child_process'; import { join } from 'path'; import { access } from 'fs/promises'; +import { platform, on } from 'process'; import type { ChildProcess } from 'child_process'; import yauzl from 'yauzl'; @@ -80,17 +81,17 @@ async function shouldUpdateComfyUI(workspaceDir: string): Promise { let comfyUIProcess: ChildProcess | null = null; function getPythonPath(workspaceDir: string): string { - const isWindows = process.platform === 'win32'; + const isWindows = platform === 'win32'; const pythonExecutable = isWindows ? 'python.exe' : 'python'; const scriptsDir = isWindows ? 'Scripts' : 'bin'; return join(workspaceDir, '.venv', scriptsDir, pythonExecutable); } -process.on('SIGINT', () => { +on('SIGINT', () => { void cleanup(); }); -process.on('SIGTERM', () => { +on('SIGTERM', () => { void cleanup(); }); @@ -99,7 +100,7 @@ async function shouldForceCPUMode(): Promise { const gpus = await getGPUData(); const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD')); const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA')); - const isWindows = process.platform === 'win32'; + const isWindows = platform === 'win32'; return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA); } catch { @@ -122,7 +123,7 @@ async function getPyTorchInstallArgs(pythonPath: string) { const gpus = await getGPUData(); const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD')); const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA')); - const isWindows = process.platform === 'win32'; + const isWindows = platform === 'win32'; if (hasAMD && !isWindows) { sendKoboldOutput( diff --git a/src/main/modules/config.ts b/src/main/modules/config.ts index 4b1a06d..2aeda3e 100644 --- a/src/main/modules/config.ts +++ b/src/main/modules/config.ts @@ -3,6 +3,7 @@ import { safeExecute } from '@/utils/node/logger'; import { getConfigDir } from '@/utils/node/path'; import { homedir } from 'os'; import { join } from 'path'; +import { platform } from 'process'; import { nativeTheme } from 'electron'; import { PRODUCT_NAME } from '@/constants'; import type { FrontendPreference } from '@/types'; @@ -53,7 +54,6 @@ export async function set(key: string, value: ConfigValue) { } function getDefaultInstallDir() { - const platform = process.platform; const home = homedir(); switch (platform) { diff --git a/src/main/modules/dependencies.ts b/src/main/modules/dependencies.ts index 9bd2ae9..d328064 100644 --- a/src/main/modules/dependencies.ts +++ b/src/main/modules/dependencies.ts @@ -1,51 +1,23 @@ -import { spawn } from 'child_process'; import { access, readdir } from 'fs/promises'; import { homedir } from 'os'; import { join } from 'path'; - -interface CommandResult { - success: boolean; - output?: string; -} +import { platform, env as processEnv } from 'process'; +import { app } from 'electron'; +import { execa } from 'execa'; async function executeCommand( command: string, args: string[], env: Record, - timeout = 5000, - useShell = false + timeout = 5000 ) { try { - const testProcess = spawn(command, args, { - stdio: 'pipe', + const { stdout } = await execa(command, args, { env, - shell: useShell, - }); - - return new Promise((resolve) => { - let output = ''; - const timeoutId = setTimeout(() => { - testProcess.kill(); - resolve({ success: false }); - }, timeout); - - testProcess.stdout?.on('data', (data) => { - output += data.toString(); - }); - - testProcess.on('exit', (code) => { - clearTimeout(timeoutId); - resolve({ - success: code === 0, - output: code === 0 ? output.trim() : undefined, - }); - }); - - testProcess.on('error', () => { - clearTimeout(timeoutId); - resolve({ success: false }); - }); + timeout, + reject: false, }); + return { success: true, output: stdout.trim() }; } catch { return { success: false }; } @@ -64,13 +36,7 @@ export async function getUvVersion() { export async function getSystemNodeVersion() { const env = await getNodeEnvironment(); - const result = await executeCommand( - 'node', - ['--version'], - env, - 5000, - process.platform === 'win32' - ); + const result = await executeCommand('node', ['--version'], env); if (result.success && result.output) { return result.output.replace(/^v/, '') || null; @@ -85,7 +51,7 @@ export async function isUvAvailable() { } export async function getUvEnvironment() { - const env = { ...process.env }; + const env = { ...processEnv }; const uvPaths = [ join(homedir(), '.cargo', 'bin'), @@ -103,11 +69,11 @@ export async function getUvEnvironment() { } if (existingPaths.length > 0) { - const pathSeparator = process.platform === 'win32' ? ';' : ':'; + const pathSeparator = platform === 'win32' ? ';' : ':'; env.PATH = `${existingPaths.join(pathSeparator)}${pathSeparator}${env.PATH}`; } - if (process.platform === 'win32') { + if (platform === 'win32') { env.PYTHONIOENCODING = 'utf-8'; env.PYTHONLEGACYWINDOWSSTDIO = '1'; env.PYTHONUTF8 = '1'; @@ -119,20 +85,14 @@ export async function getUvEnvironment() { export async function isNpxAvailable() { const env = await getNodeEnvironment(); - const result = await executeCommand( - 'npx', - ['--version'], - env, - 5000, - process.platform === 'win32' - ); + const result = await executeCommand('npx', ['--version'], env); return result.success; } export async function getNodeEnvironment() { - const env = { ...process.env }; + const env = { ...processEnv }; - if (process.platform === 'win32') { + if (platform === 'win32') { return env; } @@ -144,7 +104,7 @@ export async function getNodeEnvironment() { ]; const systemPaths: string[] = []; - if (process.platform === 'darwin') { + if (platform === 'darwin') { systemPaths.push('/opt/homebrew/bin', '/usr/local/bin'); } @@ -196,11 +156,36 @@ async function tryVersionManagerPath( return false; } +export async function isAURInstallation(): Promise { + if (platform !== 'linux') { + return false; + } + + const appPath = app.getAppPath(); + + const aurPaths = ['/usr/lib/', '/opt/', '/usr/share/']; + const isInSystemPath = aurPaths.some((path) => appPath.startsWith(path)); + + if (!isInSystemPath) { + return false; + } + + try { + const { stdout } = await execa('pacman', ['-Q', 'gerbil'], { + timeout: 1000, + reject: false, + }); + return stdout.trim().length > 0; + } catch { + return false; + } +} + function tryAddPathToEnv( env: Record, path: string ) { - const pathSeparator = process.platform === 'win32' ? ';' : ':'; + const pathSeparator = platform === 'win32' ? ';' : ':'; if (!env.PATH?.includes(path)) { env.PATH = `${path}${pathSeparator}${env.PATH}`; return true; diff --git a/src/main/modules/hardware.ts b/src/main/modules/hardware.ts index 4462841..ffe86a4 100644 --- a/src/main/modules/hardware.ts +++ b/src/main/modules/hardware.ts @@ -1,16 +1,14 @@ /* eslint-disable no-comments/disallowComments */ import si from 'systeminformation'; import { safeExecute } from '@/utils/node/logger'; -import { terminateProcess } from '@/utils/node/process'; import { getGPUData } from '@/utils/node/gpu'; import type { CPUCapabilities, GPUCapabilities, BasicGPUInfo, GPUMemoryInfo, - HardwareDetectionResult, } from '@/types/hardware'; -import { spawn } from 'child_process'; +import { execa } from 'execa'; import { formatDeviceName } from '@/utils/format'; let cpuCapabilitiesCache: CPUCapabilities | null = null; @@ -132,48 +130,33 @@ export async function detectGPUCapabilities() { async function detectCUDA() { try { - const nvidia = spawn( + const { stdout } = await execa( 'nvidia-smi', ['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'], - { timeout: 5000 } + { + timeout: 5000, + reject: false, + } ); - let output = ''; - nvidia.stdout.on('data', (data) => { - output += data.toString(); - }); + if (stdout.trim()) { + const devices = stdout + .trim() + .split('\n') + .map((line) => { + const parts = line.split(','); + const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU'; + return formatDeviceName(rawName); + }) + .filter(Boolean); - return new Promise((resolve) => { - nvidia.on('close', (code) => { - if (code === 0 && output.trim()) { - const devices = output - .trim() - .split('\n') - .map((line) => { - const parts = line.split(','); - const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU'; - return formatDeviceName(rawName); - }) - .filter(Boolean); + return { + supported: devices.length > 0, + devices, + }; + } - resolve({ - supported: devices.length > 0, - devices, - }); - } else { - resolve({ supported: false, devices: [] }); - } - }); - - nvidia.on('error', () => { - resolve({ supported: false, devices: [] }); - }); - - setTimeout(async () => { - await terminateProcess(nvidia); - resolve({ supported: false, devices: [] }); - }, 5000); - }); + return { supported: false, devices: [] }; } catch { return { supported: false, devices: [] }; } @@ -189,6 +172,7 @@ async function findRocminfoCommand() { } } +// eslint-disable-next-line sonarjs/cognitive-complexity export async function detectROCm() { try { const rocminfoCommand = await findRocminfoCommand(); @@ -197,91 +181,74 @@ export async function detectROCm() { } const isWindows = rocminfoCommand.includes('hipInfo'); - const rocminfo = spawn(rocminfoCommand, [], { timeout: 5000 }); - - let output = ''; - rocminfo.stdout.on('data', (data) => { - output += data.toString(); + const { stdout } = await execa(rocminfoCommand, [], { + timeout: 5000, + reject: false, }); - return new Promise((resolve) => { - // eslint-disable-next-line sonarjs/cognitive-complexity - rocminfo.on('close', (code) => { - if (code === 0 && output.trim()) { - const devices: string[] = []; + if (stdout.trim()) { + const devices: string[] = []; - if (isWindows) { - const lines = output.split('\n'); + if (isWindows) { + const lines = stdout.split('\n'); - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine.startsWith('Name:')) { - const name = trimmedLine.split('Name:')[1]?.trim(); - if ( - name && - !name.toLowerCase().includes('cpu') && - !devices.includes(formatDeviceName(name)) - ) { - devices.push(formatDeviceName(name)); + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine.startsWith('Name:')) { + const name = trimmedLine.split('Name:')[1]?.trim(); + if ( + name && + !name.toLowerCase().includes('cpu') && + !devices.includes(formatDeviceName(name)) + ) { + devices.push(formatDeviceName(name)); + } + } + } + } else { + const lines = stdout.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.includes('Marketing Name:')) { + const name = line.split('Marketing Name:')[1]?.trim(); + if (name) { + let deviceType = ''; + + const searchRangeLines = 20; + const searchStartIndex = Math.max(0, i - searchRangeLines); + const searchEndIndex = Math.min( + lines.length, + i + searchRangeLines + ); + + for ( + let searchIndex = searchStartIndex; + searchIndex < searchEndIndex; + searchIndex++ + ) { + if (lines[searchIndex].includes('Device Type:')) { + deviceType = + lines[searchIndex].split('Device Type:')[1]?.trim() || ''; + break; } } - } - } else { - const lines = output.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes('Marketing Name:')) { - const name = line.split('Marketing Name:')[1]?.trim(); - if (name) { - let deviceType = ''; - - const searchRangeLines = 20; - const searchStartIndex = Math.max(0, i - searchRangeLines); - const searchEndIndex = Math.min( - lines.length, - i + searchRangeLines - ); - - for ( - let searchIndex = searchStartIndex; - searchIndex < searchEndIndex; - searchIndex++ - ) { - if (lines[searchIndex].includes('Device Type:')) { - deviceType = - lines[searchIndex].split('Device Type:')[1]?.trim() || - ''; - break; - } - } - - if (deviceType !== 'CPU') { - devices.push(formatDeviceName(name)); - } - } + if (deviceType !== 'CPU') { + devices.push(formatDeviceName(name)); } } } - - resolve({ - supported: devices.length > 0, - devices, - }); - } else { - resolve({ supported: false, devices: [] }); } - }); + } - rocminfo.on('error', () => { - resolve({ supported: false, devices: [] }); - }); + return { + supported: devices.length > 0, + devices, + }; + } - setTimeout(async () => { - await terminateProcess(rocminfo); - resolve({ supported: false, devices: [] }); - }, 5000); - }); + return { supported: false, devices: [] }; } catch { return { supported: false, devices: [] }; } @@ -289,49 +256,34 @@ export async function detectROCm() { async function detectVulkan() { try { - const vulkaninfo = spawn('vulkaninfo', ['--summary'], { timeout: 5000 }); - - let output = ''; - vulkaninfo.stdout.on('data', (data) => { - output += data.toString(); + const { stdout } = await execa('vulkaninfo', ['--summary'], { + timeout: 5000, + reject: false, }); - return new Promise((resolve) => { - vulkaninfo.on('close', (code) => { - if (code === 0 && output.trim()) { - const devices: string[] = []; - const lines = output.split('\n'); + if (stdout.trim()) { + const devices: string[] = []; + const lines = stdout.split('\n'); - for (const line of lines) { - if (line.includes('deviceName') && line.includes('=')) { - const parts = line.split('='); - if (parts.length >= 2) { - const name = parts[1]?.trim(); - if (name) { - devices.push(formatDeviceName(name)); - } - } + for (const line of lines) { + if (line.includes('deviceName') && line.includes('=')) { + const parts = line.split('='); + if (parts.length >= 2) { + const name = parts[1]?.trim(); + if (name) { + devices.push(formatDeviceName(name)); } } - - resolve({ - supported: devices.length > 0, - devices, - }); - } else { - resolve({ supported: false, devices: [] }); } - }); + } - vulkaninfo.on('error', () => { - resolve({ supported: false, devices: [] }); - }); + return { + supported: devices.length > 0, + devices, + }; + } - setTimeout(async () => { - await terminateProcess(vulkaninfo); - resolve({ supported: false, devices: [] }); - }, 5000); - }); + return { supported: false, devices: [] }; } catch { return { supported: false, devices: [] }; } @@ -391,35 +343,20 @@ function findDeviceNameInClInfo(lines: string[], startIndex: number) { async function detectCLBlast() { try { - const clinfo = spawn('clinfo', [], { timeout: 3000 }); - - let output = ''; - clinfo.stdout.on('data', (data) => { - output += data.toString(); + const { stdout } = await execa('clinfo', [], { + timeout: 3000, + reject: false, }); - return new Promise((resolve) => { - clinfo.on('close', (code) => { - if (code === 0 && output.trim()) { - const devices = parseClInfoOutput(output); - resolve({ - supported: devices.length > 0, - devices, - }); - } else { - resolve({ supported: false, devices: [] }); - } - }); + if (stdout.trim()) { + const devices = parseClInfoOutput(stdout); + return { + supported: devices.length > 0, + devices, + }; + } - clinfo.on('error', () => { - resolve({ supported: false, devices: [] }); - }); - - setTimeout(async () => { - await terminateProcess(clinfo); - resolve({ supported: false, devices: [] }); - }, 3000); - }); + return { supported: false, devices: [] }; } catch { return { supported: false, devices: [] }; } diff --git a/src/main/modules/koboldcpp.ts b/src/main/modules/koboldcpp.ts index feb5805..b3dd337 100644 --- a/src/main/modules/koboldcpp.ts +++ b/src/main/modules/koboldcpp.ts @@ -1,6 +1,7 @@ import { spawn, ChildProcess } from 'child_process'; import { createWriteStream } from 'fs'; import { join } from 'path'; +import { platform } from 'process'; import { rm, readdir, @@ -63,7 +64,7 @@ async function removeDirectoryWithRetry( throw error; } - if (isPermissionError && process.platform === 'win32') { + if (isPermissionError && platform === 'win32') { sendKoboldOutput( `Attempt ${attempt}/${maxRetries} failed (file in use), retrying in ${delayMs}ms...` ); @@ -131,7 +132,7 @@ async function downloadFile(asset: GitHubAsset, tempPackedFilePath: string) { await new Promise((resolve, reject) => { writer.on('finish', async () => { - if (process.platform !== 'win32') { + if (platform !== 'win32') { try { await chmod(tempPackedFilePath, 0o755); } catch (error) { @@ -153,9 +154,7 @@ async function setupLauncher( if (!launcherPath || !(await pathExists(launcherPath))) { const expectedLauncherName = - process.platform === 'win32' - ? 'koboldcpp-launcher.exe' - : 'koboldcpp-launcher'; + platform === 'win32' ? 'koboldcpp-launcher.exe' : 'koboldcpp-launcher'; const newLauncherPath = join(unpackedDirPath, expectedLauncherName); if (await pathExists(tempPackedFilePath)) { @@ -297,7 +296,7 @@ async function patchKcppSduiEmbd(unpackedDir: string) { } async function getLauncherPath(unpackedDir: string) { - const extensions = process.platform === 'win32' ? ['.exe', ''] : ['', '.exe']; + const extensions = platform === 'win32' ? ['.exe', ''] : ['', '.exe']; for (const ext of extensions) { const launcherPath = join(unpackedDir, `koboldcpp-launcher${ext}`); diff --git a/src/main/modules/logging.ts b/src/main/modules/logging.ts index 3ceec14..a7b41d4 100644 --- a/src/main/modules/logging.ts +++ b/src/main/modules/logging.ts @@ -17,8 +17,7 @@ export const initializeLogger = () => { format: format.combine( format.timestamp(), format.printf(({ timestamp, level, message, error }) => { - const processInfo = `[${process.type || 'unknown'}:${process.pid}]`; - let logEntry = `${timestamp} ${processInfo} [${level.toUpperCase()}] ${message}`; + let logEntry = `${timestamp} [MAIN] [${level.toUpperCase()}] ${message}`; if (error && error instanceof Error) { logEntry += `\n Error: ${error.message}`; @@ -42,7 +41,6 @@ export const initializeLogger = () => { ], }); - setupGlobalErrorHandlers(); isInitialized = true; }; @@ -72,18 +70,6 @@ export const flushLogs = () => { } }; -const setupGlobalErrorHandlers = () => { - process.on('uncaughtException', (error) => { - logError('Uncaught Exception:', error); - }); - - process.on('unhandledRejection', (reason, _promise) => { - const message = `Unhandled Promise Rejection`; - const error = reason instanceof Error ? reason : new Error(String(reason)); - logError(message, error); - }); -}; - export const getLogFilePath = () => join(app.getPath('userData'), 'logs', 'gerbil.log'); diff --git a/src/main/modules/openwebui.ts b/src/main/modules/openwebui.ts index 9e3754f..923f1a7 100644 --- a/src/main/modules/openwebui.ts +++ b/src/main/modules/openwebui.ts @@ -1,6 +1,7 @@ import { spawn } from 'child_process'; import type { ChildProcess } from 'child_process'; import { join } from 'path'; +import { on } from 'process'; import { logError } from './logging'; import { safeTryExecute, tryExecute } from '@/utils/node/logger'; @@ -16,11 +17,11 @@ let openWebUIProcess: ChildProcess | null = null; const OPENWEBUI_BASE_ARGS = ['--python', '3.11', 'open-webui@latest', 'serve']; -process.on('SIGINT', () => { +on('SIGINT', () => { void cleanup(); }); -process.on('SIGTERM', () => { +on('SIGTERM', () => { void cleanup(); }); diff --git a/src/main/modules/performance.ts b/src/main/modules/performance.ts index 6fba554..4183069 100644 --- a/src/main/modules/performance.ts +++ b/src/main/modules/performance.ts @@ -1,4 +1,4 @@ -import { spawn } from 'child_process'; +import { execa } from 'execa'; import { platform } from 'process'; import { safeExecute } from '@/utils/node/logger'; @@ -12,37 +12,16 @@ const LINUX_PERFORMANCE_APPS = [ ]; async function tryLaunchCommand(command: string, args: string[] = []) { - return new Promise((resolve) => { - const child = spawn(command, args, { + try { + await execa(command, args, { detached: true, stdio: 'ignore', + timeout: 2000, }); - - let hasResolved = false; - - child.on('error', () => { - if (!hasResolved) { - hasResolved = true; - resolve(false); - } - }); - - child.on('spawn', () => { - if (!hasResolved) { - hasResolved = true; - child.unref(); - resolve(true); - } - }); - - setTimeout(() => { - if (!hasResolved) { - hasResolved = true; - child.kill(); - resolve(false); - } - }, 2000); - }); + return true; + } catch { + return false; + } } export const openPerformanceManager = async () => diff --git a/src/main/modules/sillytavern.ts b/src/main/modules/sillytavern.ts index 7c0fa56..ac3e3b8 100644 --- a/src/main/modules/sillytavern.ts +++ b/src/main/modules/sillytavern.ts @@ -2,6 +2,7 @@ import { spawn } from 'child_process'; import { createServer, request, type Server } from 'http'; import { homedir } from 'os'; import { join } from 'path'; +import { platform, on } from 'process'; import type { ChildProcess } from 'child_process'; import { logError } from './logging'; @@ -26,16 +27,15 @@ const SILLYTAVERN_BASE_ARGS = [ '--disableCsrf', ]; -process.on('SIGINT', () => { +on('SIGINT', () => { void cleanup(); }); -process.on('SIGTERM', () => { +on('SIGTERM', () => { void cleanup(); }); function getFallbackDataRoot() { - const platform = process.platform; const home = homedir(); switch (platform) { @@ -76,7 +76,7 @@ async function createNpxProcess(args: string[]) { stdio: ['pipe', 'pipe', 'pipe'], detached: false, env, - shell: process.platform === 'win32', + shell: platform === 'win32', }); } diff --git a/src/preload/index.ts b/src/preload/index.ts index a59a727..ab63c90 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -141,6 +141,7 @@ const updaterAPI: UpdaterAPI = { quitAndInstall: () => ipcRenderer.invoke('app:quitAndInstall'), isUpdateDownloaded: () => ipcRenderer.invoke('app:isUpdateDownloaded'), canAutoUpdate: () => ipcRenderer.invoke('app:canAutoUpdate'), + isAURInstallation: () => ipcRenderer.invoke('app:isAURInstallation'), }; contextBridge.exposeInMainWorld('electronAPI', { diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index e9ef50a..a29846a 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -203,6 +203,7 @@ export interface UpdaterAPI { quitAndInstall: () => void; isUpdateDownloaded: () => Promise; canAutoUpdate: () => Promise; + isAURInstallation: () => Promise; } declare global { diff --git a/src/utils/node/assets.ts b/src/utils/node/assets.ts index 989b79f..5a7e3c5 100644 --- a/src/utils/node/assets.ts +++ b/src/utils/node/assets.ts @@ -1,10 +1,11 @@ import { join } from 'path'; +import { resourcesPath } from 'process'; import { isDevelopment } from '@/utils/node/environment'; export function getAssetPath(assetName: string) { if (isDevelopment) { return join(__dirname, '../../assets', assetName); } else { - return join(process.resourcesPath, '..', 'assets', assetName); + return join(resourcesPath, '..', 'assets', assetName); } } diff --git a/src/utils/node/environment.ts b/src/utils/node/environment.ts index 1944756..b862662 100644 --- a/src/utils/node/environment.ts +++ b/src/utils/node/environment.ts @@ -1 +1,3 @@ -export const isDevelopment = process.env.NODE_ENV === 'development'; +import { env } from 'process'; + +export const isDevelopment = env.NODE_ENV === 'development'; diff --git a/src/utils/node/path.ts b/src/utils/node/path.ts index ca0b0b8..677e0d5 100644 --- a/src/utils/node/path.ts +++ b/src/utils/node/path.ts @@ -1,5 +1,6 @@ import { join } from 'path'; import { homedir } from 'os'; +import { platform } from 'process'; import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants'; export function getConfigDir() { @@ -7,7 +8,6 @@ export function getConfigDir() { } function getConfigDirPath() { - const platform = process.platform; const home = homedir(); switch (platform) { diff --git a/src/utils/node/process.ts b/src/utils/node/process.ts index 2ced428..904197c 100644 --- a/src/utils/node/process.ts +++ b/src/utils/node/process.ts @@ -1,4 +1,5 @@ import { spawn } from 'child_process'; +import { platform } from 'process'; import type { ChildProcess } from 'child_process'; export interface ProcessTerminationOptions { @@ -43,7 +44,7 @@ export async function terminateProcess( } try { - if (process.platform === 'win32') { + if (platform === 'win32') { await killWindowsProcessTree(childProcess.pid, logError); await new Promise((resolve) => {