auto update improvements, dont allow process globals (import instead), refactor to use execa more

This commit is contained in:
Egor 2025-09-18 17:20:59 -07:00
parent 611a5b4615
commit 3eb97f5683
27 changed files with 353 additions and 384 deletions

View file

@ -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()],

View file

@ -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',

View file

@ -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": "./",

View file

@ -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 = ({
<Group gap="0" style={{ WebkitAppRegion: 'no-drag' }}>
<UpdateButton />
<ActionIcon
variant="subtle"
size={TITLEBAR_HEIGHT}
onClick={() => setSettingsModalOpen(true)}
aria-label="Open settings"
tabIndex={-1}
style={{
borderRadius: 0,
margin: '2px 1px 1px',
outline: 'none',
}}
>
<Settings size="1.25rem" />
</ActionIcon>
<Tooltip label="Settings" position="bottom">
<ActionIcon
variant="subtle"
size={TITLEBAR_HEIGHT}
onClick={() => setSettingsModalOpen(true)}
aria-label="Open settings"
tabIndex={-1}
style={{
borderRadius: 0,
margin: '2px 1px 1px',
outline: 'none',
}}
>
<Settings size="1.25rem" />
</ActionIcon>
</Tooltip>
<Box
style={{

View file

@ -1,7 +1,8 @@
import { ActionIcon } from '@mantine/core';
import { CircleFadingArrowUp } from 'lucide-react';
import { ActionIcon, Tooltip } from '@mantine/core';
import { CircleFadingArrowUp, Download } from 'lucide-react';
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
import { TITLEBAR_HEIGHT } from '@/constants';
import { useState, type MouseEvent } from 'react';
export const UpdateButton = () => {
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 = <CircleFadingArrowUp size="1.25rem" />;
if (isUpdateDownloaded) {
color = 'green';
@ -30,30 +31,53 @@ export const UpdateButton = () => {
} else if (isDownloading) {
color = 'blue';
label = 'Downloading update...';
} else if (canAutoUpdate) {
icon = <Download size="1.25rem" />;
} else if (showDownload && canAutoUpdate) {
color = 'blue';
label = 'Download and install update';
onClick = downloadUpdate;
onClick = () => {
downloadUpdate();
setShowDownload(false);
};
icon = <Download size="1.25rem" />;
} 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<HTMLButtonElement>) => {
e.preventDefault();
if (canAutoUpdate && !isDownloading && !isUpdateDownloaded) {
setShowDownload(true);
downloadUpdate();
}
};
return (
<ActionIcon
component={(isButton ? 'button' : 'a') as 'button' | 'a'}
href={isLink ? releaseUrl : undefined}
target={isLink ? '_blank' : undefined}
rel={isLink ? 'noopener noreferrer' : undefined}
variant="subtle"
color={color}
size={TITLEBAR_HEIGHT}
aria-label={label}
tabIndex={-1}
onClick={onClick}
style={{
borderRadius: 0,
margin: 0,
}}
>
<CircleFadingArrowUp size="1.25rem" />
</ActionIcon>
<Tooltip label={label} position="bottom">
<ActionIcon
component="button"
variant="subtle"
color={color}
size={TITLEBAR_HEIGHT}
aria-label={label}
tabIndex={-1}
onClick={onClick}
onContextMenu={handleContextMenu}
style={{
borderRadius: 0,
margin: 0,
}}
>
{icon}
</ActionIcon>
</Tooltip>
);
};

View file

@ -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);

View file

@ -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<void>((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);
}
});
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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());
}

View file

@ -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) {

View file

@ -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<boolean> => {

View file

@ -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<boolean> {
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<boolean> {
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(

View file

@ -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) {

View file

@ -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<string, string | undefined>,
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<CommandResult>((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<boolean> {
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<string, string | undefined>,
path: string
) {
const pathSeparator = process.platform === 'win32' ? ';' : ':';
const pathSeparator = platform === 'win32' ? ';' : ':';
if (!env.PATH?.includes(path)) {
env.PATH = `${path}${pathSeparator}${env.PATH}`;
return true;

View file

@ -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<HardwareDetectionResult>((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<HardwareDetectionResult>((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<HardwareDetectionResult>((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<HardwareDetectionResult>((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: [] };
}

View file

@ -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<void>((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}`);

View file

@ -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');

View file

@ -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();
});

View file

@ -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 () =>

View file

@ -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',
});
}

View file

@ -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', {

View file

@ -203,6 +203,7 @@ export interface UpdaterAPI {
quitAndInstall: () => void;
isUpdateDownloaded: () => Promise<boolean>;
canAutoUpdate: () => Promise<boolean>;
isAURInstallation: () => Promise<boolean>;
}
declare global {

View file

@ -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);
}
}

View file

@ -1 +1,3 @@
export const isDevelopment = process.env.NODE_ENV === 'development';
import { env } from 'process';
export const isDevelopment = env.NODE_ENV === 'development';

View file

@ -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) {

View file

@ -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<void>((resolve) => {