mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
auto update improvements, dont allow process globals (import instead), refactor to use execa more
This commit is contained in:
parent
611a5b4615
commit
3eb97f5683
27 changed files with 353 additions and 384 deletions
|
|
@ -11,11 +11,6 @@ export default defineConfig({
|
||||||
'@': resolve(__dirname, './src'),
|
'@': resolve(__dirname, './src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
define: {
|
|
||||||
'process.env.VITE_DEV_SERVER_URL': JSON.stringify(
|
|
||||||
process.env.VITE_DEV_SERVER_URL
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
plugins: [externalizeDepsPlugin()],
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,15 @@ const config = [
|
||||||
|
|
||||||
'no-console': 'error',
|
'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/no-inferrable-types': 'warn',
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
"version": "1.4.0-beta.3",
|
"version": "1.4.0-beta.4",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
|
|
|
||||||
|
|
@ -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 { Minus, Square, X, Copy, Settings } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useInterfaceOptions } from '@/hooks/useInterfaceSelection';
|
import { useInterfaceOptions } from '@/hooks/useInterfaceSelection';
|
||||||
|
|
@ -138,6 +146,7 @@ export const TitleBar = ({
|
||||||
<Group gap="0" style={{ WebkitAppRegion: 'no-drag' }}>
|
<Group gap="0" style={{ WebkitAppRegion: 'no-drag' }}>
|
||||||
<UpdateButton />
|
<UpdateButton />
|
||||||
|
|
||||||
|
<Tooltip label="Settings" position="bottom">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size={TITLEBAR_HEIGHT}
|
size={TITLEBAR_HEIGHT}
|
||||||
|
|
@ -152,6 +161,7 @@ export const TitleBar = ({
|
||||||
>
|
>
|
||||||
<Settings size="1.25rem" />
|
<Settings size="1.25rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { ActionIcon } from '@mantine/core';
|
import { ActionIcon, Tooltip } from '@mantine/core';
|
||||||
import { CircleFadingArrowUp } from 'lucide-react';
|
import { CircleFadingArrowUp, Download } from 'lucide-react';
|
||||||
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
||||||
import { TITLEBAR_HEIGHT } from '@/constants';
|
import { TITLEBAR_HEIGHT } from '@/constants';
|
||||||
|
import { useState, type MouseEvent } from 'react';
|
||||||
|
|
||||||
export const UpdateButton = () => {
|
export const UpdateButton = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -14,14 +15,14 @@ export const UpdateButton = () => {
|
||||||
installUpdate,
|
installUpdate,
|
||||||
} = useAppUpdateChecker();
|
} = useAppUpdateChecker();
|
||||||
|
|
||||||
|
const [showDownload, setShowDownload] = useState(false);
|
||||||
|
|
||||||
if (!hasUpdate) return null;
|
if (!hasUpdate) return null;
|
||||||
|
|
||||||
const isButton = canAutoUpdate;
|
|
||||||
const isLink = !canAutoUpdate && releaseUrl;
|
|
||||||
|
|
||||||
let color: 'green' | 'blue' | 'orange' = 'orange';
|
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 onClick: (() => void) | undefined;
|
||||||
|
let icon = <CircleFadingArrowUp size="1.25rem" />;
|
||||||
|
|
||||||
if (isUpdateDownloaded) {
|
if (isUpdateDownloaded) {
|
||||||
color = 'green';
|
color = 'green';
|
||||||
|
|
@ -30,30 +31,53 @@ export const UpdateButton = () => {
|
||||||
} else if (isDownloading) {
|
} else if (isDownloading) {
|
||||||
color = 'blue';
|
color = 'blue';
|
||||||
label = 'Downloading update...';
|
label = 'Downloading update...';
|
||||||
} else if (canAutoUpdate) {
|
icon = <Download size="1.25rem" />;
|
||||||
|
} else if (showDownload && canAutoUpdate) {
|
||||||
color = 'blue';
|
color = 'blue';
|
||||||
label = 'Download and install update';
|
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 (
|
return (
|
||||||
|
<Tooltip label={label} position="bottom">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
component={(isButton ? 'button' : 'a') as 'button' | 'a'}
|
component="button"
|
||||||
href={isLink ? releaseUrl : undefined}
|
|
||||||
target={isLink ? '_blank' : undefined}
|
|
||||||
rel={isLink ? 'noopener noreferrer' : undefined}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color={color}
|
color={color}
|
||||||
size={TITLEBAR_HEIGHT}
|
size={TITLEBAR_HEIGHT}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircleFadingArrowUp size="1.25rem" />
|
{icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,12 @@ export const useAppUpdateChecker = () => {
|
||||||
setIsDownloading(true);
|
setIsDownloading(true);
|
||||||
|
|
||||||
await tryExecute(async () => {
|
await tryExecute(async () => {
|
||||||
|
const checkResult = await window.electronAPI.updater.checkForUpdates();
|
||||||
|
if (!checkResult) {
|
||||||
|
setIsDownloading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const success = await window.electronAPI.updater.downloadUpdate();
|
const success = await window.electronAPI.updater.downloadUpdate();
|
||||||
if (success) {
|
if (success) {
|
||||||
setIsUpdateDownloaded(true);
|
setIsUpdateDownloaded(true);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
import { platform, exit, stdout, stderr, stdin, on } from 'process';
|
||||||
|
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
import { pathExists, readJsonFile } from '@/utils/node/fs';
|
import { pathExists, readJsonFile } from '@/utils/node/fs';
|
||||||
|
|
@ -28,17 +29,17 @@ export async function handleCliMode(args: string[]) {
|
||||||
console.error(
|
console.error(
|
||||||
'Error: No binary found. Please run the GUI first to download the binary.'
|
'Error: No binary found. Please run the GUI first to download the binary.'
|
||||||
);
|
);
|
||||||
process.exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await pathExists(currentBinary))) {
|
if (!(await pathExists(currentBinary))) {
|
||||||
console.error(`Error: Binary not found at: ${currentBinary}`);
|
console.error(`Error: Binary not found at: ${currentBinary}`);
|
||||||
console.error('Please run the GUI to download and configure the binary.');
|
console.error('Please run the GUI to download and configure the binary.');
|
||||||
process.exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = platform === 'win32';
|
||||||
|
|
||||||
const child = spawn(currentBinary, args, {
|
const child = spawn(currentBinary, args, {
|
||||||
stdio: isWindows ? 'pipe' : 'inherit',
|
stdio: isWindows ? 'pipe' : 'inherit',
|
||||||
|
|
@ -50,24 +51,24 @@ export async function handleCliMode(args: string[]) {
|
||||||
child.stderr?.setEncoding('utf8');
|
child.stderr?.setEncoding('utf8');
|
||||||
|
|
||||||
child.stdout?.on('data', (data) => {
|
child.stdout?.on('data', (data) => {
|
||||||
process.stdout.write(data.toString());
|
stdout.write(data.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr?.on('data', (data) => {
|
child.stderr?.on('data', (data) => {
|
||||||
process.stderr.write(data.toString());
|
stderr.write(data.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (child.stdin && process.stdin.readable) {
|
if (child.stdin && stdin.readable) {
|
||||||
process.stdin.pipe(child.stdin);
|
stdin.pipe(child.stdin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
child.on('exit', (code, signal) => {
|
child.on('exit', (code, signal) => {
|
||||||
if (signal) {
|
if (signal) {
|
||||||
console.log(`\nProcess terminated with signal: ${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) {
|
} else if (code !== null) {
|
||||||
process.exit(code);
|
exit(code);
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
|
@ -90,10 +91,10 @@ export async function handleCliMode(args: string[]) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on('SIGINT', handleSignal);
|
on('SIGINT', handleSignal);
|
||||||
process.on('SIGTERM', handleSignal);
|
on('SIGTERM', handleSignal);
|
||||||
if (process.platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
process.on('SIGBREAK', handleSignal);
|
on('SIGBREAK', handleSignal);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
import { platform } from 'process';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createMainWindow,
|
createMainWindow,
|
||||||
|
|
@ -28,7 +29,7 @@ export async function initializeApp() {
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { argv, exit } from 'process';
|
||||||
import { getAppVersion } from '@/utils/node/fs';
|
import { getAppVersion } from '@/utils/node/fs';
|
||||||
|
|
||||||
if (process.argv[1] === '--version') {
|
if (argv[1] === '--version') {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const version = await getAppVersion();
|
const version = await getAppVersion();
|
||||||
|
|
@ -10,27 +11,27 @@ if (process.argv[1] === '--version') {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('unknown');
|
console.log('unknown');
|
||||||
}
|
}
|
||||||
process.exit(0);
|
exit(0);
|
||||||
})();
|
})();
|
||||||
} else {
|
} else {
|
||||||
(async () => {
|
(async () => {
|
||||||
const isCliMode = process.argv.includes('--cli');
|
const isCliMode = argv.includes('--cli');
|
||||||
|
|
||||||
if (isCliMode) {
|
if (isCliMode) {
|
||||||
try {
|
try {
|
||||||
const cliModule = await import('./cli');
|
const cliModule = await import('./cli');
|
||||||
const args = process.argv.slice(process.argv.indexOf('--cli') + 1);
|
const args = argv.slice(argv.indexOf('--cli') + 1);
|
||||||
try {
|
try {
|
||||||
await cliModule.handleCliMode(args);
|
await cliModule.handleCliMode(args);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('CLI mode error:', error);
|
console.error('CLI mode error:', error);
|
||||||
process.exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('Failed to load CLI module:', error);
|
console.error('Failed to load CLI module:', error);
|
||||||
process.exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ipcMain, shell, app } from 'electron';
|
import { ipcMain, shell, app } from 'electron';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import { platform, versions, arch } from 'process';
|
||||||
import type { MantineColorScheme } from '@mantine/core';
|
import type { MantineColorScheme } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
launchKoboldCpp,
|
launchKoboldCpp,
|
||||||
|
|
@ -40,6 +41,7 @@ import {
|
||||||
isNpxAvailable,
|
isNpxAvailable,
|
||||||
getUvVersion,
|
getUvVersion,
|
||||||
getSystemNodeVersion,
|
getSystemNodeVersion,
|
||||||
|
isAURInstallation,
|
||||||
} from '@/main/modules/dependencies';
|
} from '@/main/modules/dependencies';
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
import { getMainWindow } from '@/main/modules/window';
|
import { getMainWindow } from '@/main/modules/window';
|
||||||
|
|
@ -137,7 +139,7 @@ export function setupIPCHandlers() {
|
||||||
getAvailableBackends(includeDisabled)
|
getAvailableBackends(includeDisabled)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getPlatform', () => process.platform);
|
ipcMain.handle('kobold:getPlatform', () => platform);
|
||||||
|
|
||||||
ipcMain.handle('kobold:launchKoboldCpp', (_, args) =>
|
ipcMain.handle('kobold:launchKoboldCpp', (_, args) =>
|
||||||
launchKoboldCppWithCustomFrontends(args)
|
launchKoboldCppWithCustomFrontends(args)
|
||||||
|
|
@ -173,13 +175,13 @@ export function setupIPCHandlers() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appVersion,
|
appVersion,
|
||||||
electronVersion: process.versions.electron,
|
electronVersion: versions.electron,
|
||||||
nodeVersion: process.versions.node,
|
nodeVersion: versions.node,
|
||||||
chromeVersion: process.versions.chrome,
|
chromeVersion: versions.chrome,
|
||||||
v8Version: process.versions.v8,
|
v8Version: versions.v8,
|
||||||
osVersion: os.release(),
|
osVersion: os.release(),
|
||||||
platform: process.platform,
|
platform,
|
||||||
arch: process.arch,
|
arch,
|
||||||
nodeJsSystemVersion,
|
nodeJsSystemVersion,
|
||||||
uvVersion,
|
uvVersion,
|
||||||
};
|
};
|
||||||
|
|
@ -285,8 +287,17 @@ export function setupIPCHandlers() {
|
||||||
|
|
||||||
ipcMain.handle('app:isUpdateDownloaded', () => isUpdateDownloaded());
|
ipcMain.handle('app:isUpdateDownloaded', () => isUpdateDownloaded());
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle('app:canAutoUpdate', async () => {
|
||||||
'app:canAutoUpdate',
|
if (!app.isPackaged) return false;
|
||||||
() => process.platform !== 'linux' || Boolean(process.env.APPIMAGE)
|
|
||||||
);
|
if (platform === 'linux' && (await isAURInstallation())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
platform === 'win32' || platform === 'darwin' || platform === 'linux'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('app:isAURInstallation', () => isAURInstallation());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { autoUpdater } from 'electron-updater';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { logError } from '@/main/modules/logging';
|
import { logError } from '@/main/modules/logging';
|
||||||
import { safeExecute } from '@/utils/node/logger';
|
import { safeExecute } from '@/utils/node/logger';
|
||||||
|
import { isDevelopment } from '@/utils/node/environment';
|
||||||
|
|
||||||
export interface UpdateInfo {
|
export interface UpdateInfo {
|
||||||
version: string;
|
version: string;
|
||||||
|
|
@ -34,17 +35,33 @@ function setupAutoUpdater() {
|
||||||
|
|
||||||
setupAutoUpdater();
|
setupAutoUpdater();
|
||||||
|
|
||||||
export const checkForUpdates = async () =>
|
export const checkForUpdates = async () => {
|
||||||
|
if (isDevelopment) {
|
||||||
|
logError('Auto-updater: Cannot check for updates in development mode');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
(await safeExecute(
|
(await safeExecute(
|
||||||
() => autoUpdater.checkForUpdates(),
|
() => autoUpdater.checkForUpdates(),
|
||||||
'Failed to check for updates'
|
'Failed to check for updates'
|
||||||
)) !== null;
|
)) !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const downloadUpdate = async () =>
|
export const downloadUpdate = async () => {
|
||||||
|
if (isDevelopment) {
|
||||||
|
logError('Auto-updater: Cannot download updates in development mode');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
(await safeExecute(
|
(await safeExecute(
|
||||||
() => autoUpdater.downloadUpdate(),
|
() => autoUpdater.downloadUpdate(),
|
||||||
'Failed to download update'
|
'Failed to download update'
|
||||||
)) !== null;
|
)) !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function quitAndInstall() {
|
export function quitAndInstall() {
|
||||||
if (updateDownloaded) {
|
if (updateDownloaded) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
|
import { platform } from 'process';
|
||||||
import { pathExists } from '@/utils/node/fs';
|
import { pathExists } from '@/utils/node/fs';
|
||||||
import { getCurrentBinaryInfo } from './koboldcpp';
|
import { getCurrentBinaryInfo } from './koboldcpp';
|
||||||
import { detectGPUCapabilities, detectCPU } from './hardware';
|
import { detectGPUCapabilities, detectCPU } from './hardware';
|
||||||
|
|
@ -26,7 +27,6 @@ async function detectBackendSupportFromPath(koboldBinaryPath: string) {
|
||||||
const binaryDir = dirname(koboldBinaryPath);
|
const binaryDir = dirname(koboldBinaryPath);
|
||||||
const internalDir = join(binaryDir, '_internal');
|
const internalDir = join(binaryDir, '_internal');
|
||||||
|
|
||||||
const platform = process.platform;
|
|
||||||
const libExtension = platform === 'win32' ? '.dll' : '.so';
|
const libExtension = platform === 'win32' ? '.dll' : '.so';
|
||||||
|
|
||||||
const hasKoboldCppLib = async (name: string): Promise<boolean> => {
|
const hasKoboldCppLib = async (name: string): Promise<boolean> => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { access } from 'fs/promises';
|
import { access } from 'fs/promises';
|
||||||
|
import { platform, on } from 'process';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
import yauzl from 'yauzl';
|
import yauzl from 'yauzl';
|
||||||
|
|
||||||
|
|
@ -80,17 +81,17 @@ async function shouldUpdateComfyUI(workspaceDir: string): Promise<boolean> {
|
||||||
let comfyUIProcess: ChildProcess | null = null;
|
let comfyUIProcess: ChildProcess | null = null;
|
||||||
|
|
||||||
function getPythonPath(workspaceDir: string): string {
|
function getPythonPath(workspaceDir: string): string {
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = platform === 'win32';
|
||||||
const pythonExecutable = isWindows ? 'python.exe' : 'python';
|
const pythonExecutable = isWindows ? 'python.exe' : 'python';
|
||||||
const scriptsDir = isWindows ? 'Scripts' : 'bin';
|
const scriptsDir = isWindows ? 'Scripts' : 'bin';
|
||||||
return join(workspaceDir, '.venv', scriptsDir, pythonExecutable);
|
return join(workspaceDir, '.venv', scriptsDir, pythonExecutable);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
on('SIGINT', () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
on('SIGTERM', () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -99,7 +100,7 @@ async function shouldForceCPUMode(): Promise<boolean> {
|
||||||
const gpus = await getGPUData();
|
const gpus = await getGPUData();
|
||||||
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
||||||
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = platform === 'win32';
|
||||||
|
|
||||||
return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA);
|
return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -122,7 +123,7 @@ async function getPyTorchInstallArgs(pythonPath: string) {
|
||||||
const gpus = await getGPUData();
|
const gpus = await getGPUData();
|
||||||
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD'));
|
||||||
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = platform === 'win32';
|
||||||
|
|
||||||
if (hasAMD && !isWindows) {
|
if (hasAMD && !isWindows) {
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { safeExecute } from '@/utils/node/logger';
|
||||||
import { getConfigDir } from '@/utils/node/path';
|
import { getConfigDir } from '@/utils/node/path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { platform } from 'process';
|
||||||
import { nativeTheme } from 'electron';
|
import { nativeTheme } from 'electron';
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import type { FrontendPreference } from '@/types';
|
import type { FrontendPreference } from '@/types';
|
||||||
|
|
@ -53,7 +54,6 @@ export async function set(key: string, value: ConfigValue) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultInstallDir() {
|
function getDefaultInstallDir() {
|
||||||
const platform = process.platform;
|
|
||||||
const home = homedir();
|
const home = homedir();
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,23 @@
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import { access, readdir } from 'fs/promises';
|
import { access, readdir } from 'fs/promises';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { platform, env as processEnv } from 'process';
|
||||||
interface CommandResult {
|
import { app } from 'electron';
|
||||||
success: boolean;
|
import { execa } from 'execa';
|
||||||
output?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeCommand(
|
async function executeCommand(
|
||||||
command: string,
|
command: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
env: Record<string, string | undefined>,
|
env: Record<string, string | undefined>,
|
||||||
timeout = 5000,
|
timeout = 5000
|
||||||
useShell = false
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const testProcess = spawn(command, args, {
|
const { stdout } = await execa(command, args, {
|
||||||
stdio: 'pipe',
|
|
||||||
env,
|
env,
|
||||||
shell: useShell,
|
timeout,
|
||||||
});
|
reject: false,
|
||||||
|
|
||||||
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 });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
return { success: true, output: stdout.trim() };
|
||||||
} catch {
|
} catch {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
@ -64,13 +36,7 @@ export async function getUvVersion() {
|
||||||
|
|
||||||
export async function getSystemNodeVersion() {
|
export async function getSystemNodeVersion() {
|
||||||
const env = await getNodeEnvironment();
|
const env = await getNodeEnvironment();
|
||||||
const result = await executeCommand(
|
const result = await executeCommand('node', ['--version'], env);
|
||||||
'node',
|
|
||||||
['--version'],
|
|
||||||
env,
|
|
||||||
5000,
|
|
||||||
process.platform === 'win32'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success && result.output) {
|
if (result.success && result.output) {
|
||||||
return result.output.replace(/^v/, '') || null;
|
return result.output.replace(/^v/, '') || null;
|
||||||
|
|
@ -85,7 +51,7 @@ export async function isUvAvailable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUvEnvironment() {
|
export async function getUvEnvironment() {
|
||||||
const env = { ...process.env };
|
const env = { ...processEnv };
|
||||||
|
|
||||||
const uvPaths = [
|
const uvPaths = [
|
||||||
join(homedir(), '.cargo', 'bin'),
|
join(homedir(), '.cargo', 'bin'),
|
||||||
|
|
@ -103,11 +69,11 @@ export async function getUvEnvironment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingPaths.length > 0) {
|
if (existingPaths.length > 0) {
|
||||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
const pathSeparator = platform === 'win32' ? ';' : ':';
|
||||||
env.PATH = `${existingPaths.join(pathSeparator)}${pathSeparator}${env.PATH}`;
|
env.PATH = `${existingPaths.join(pathSeparator)}${pathSeparator}${env.PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
env.PYTHONIOENCODING = 'utf-8';
|
env.PYTHONIOENCODING = 'utf-8';
|
||||||
env.PYTHONLEGACYWINDOWSSTDIO = '1';
|
env.PYTHONLEGACYWINDOWSSTDIO = '1';
|
||||||
env.PYTHONUTF8 = '1';
|
env.PYTHONUTF8 = '1';
|
||||||
|
|
@ -119,20 +85,14 @@ export async function getUvEnvironment() {
|
||||||
|
|
||||||
export async function isNpxAvailable() {
|
export async function isNpxAvailable() {
|
||||||
const env = await getNodeEnvironment();
|
const env = await getNodeEnvironment();
|
||||||
const result = await executeCommand(
|
const result = await executeCommand('npx', ['--version'], env);
|
||||||
'npx',
|
|
||||||
['--version'],
|
|
||||||
env,
|
|
||||||
5000,
|
|
||||||
process.platform === 'win32'
|
|
||||||
);
|
|
||||||
return result.success;
|
return result.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodeEnvironment() {
|
export async function getNodeEnvironment() {
|
||||||
const env = { ...process.env };
|
const env = { ...processEnv };
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +104,7 @@ export async function getNodeEnvironment() {
|
||||||
];
|
];
|
||||||
|
|
||||||
const systemPaths: string[] = [];
|
const systemPaths: string[] = [];
|
||||||
if (process.platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
systemPaths.push('/opt/homebrew/bin', '/usr/local/bin');
|
systemPaths.push('/opt/homebrew/bin', '/usr/local/bin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,11 +156,36 @@ async function tryVersionManagerPath(
|
||||||
return false;
|
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(
|
function tryAddPathToEnv(
|
||||||
env: Record<string, string | undefined>,
|
env: Record<string, string | undefined>,
|
||||||
path: string
|
path: string
|
||||||
) {
|
) {
|
||||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
const pathSeparator = platform === 'win32' ? ';' : ':';
|
||||||
if (!env.PATH?.includes(path)) {
|
if (!env.PATH?.includes(path)) {
|
||||||
env.PATH = `${path}${pathSeparator}${env.PATH}`;
|
env.PATH = `${path}${pathSeparator}${env.PATH}`;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
/* eslint-disable no-comments/disallowComments */
|
/* eslint-disable no-comments/disallowComments */
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
import { safeExecute } from '@/utils/node/logger';
|
import { safeExecute } from '@/utils/node/logger';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
|
||||||
import { getGPUData } from '@/utils/node/gpu';
|
import { getGPUData } from '@/utils/node/gpu';
|
||||||
import type {
|
import type {
|
||||||
CPUCapabilities,
|
CPUCapabilities,
|
||||||
GPUCapabilities,
|
GPUCapabilities,
|
||||||
BasicGPUInfo,
|
BasicGPUInfo,
|
||||||
GPUMemoryInfo,
|
GPUMemoryInfo,
|
||||||
HardwareDetectionResult,
|
|
||||||
} from '@/types/hardware';
|
} from '@/types/hardware';
|
||||||
import { spawn } from 'child_process';
|
import { execa } from 'execa';
|
||||||
import { formatDeviceName } from '@/utils/format';
|
import { formatDeviceName } from '@/utils/format';
|
||||||
|
|
||||||
let cpuCapabilitiesCache: CPUCapabilities | null = null;
|
let cpuCapabilitiesCache: CPUCapabilities | null = null;
|
||||||
|
|
@ -132,21 +130,17 @@ export async function detectGPUCapabilities() {
|
||||||
|
|
||||||
async function detectCUDA() {
|
async function detectCUDA() {
|
||||||
try {
|
try {
|
||||||
const nvidia = spawn(
|
const { stdout } = await execa(
|
||||||
'nvidia-smi',
|
'nvidia-smi',
|
||||||
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
|
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
|
||||||
{ timeout: 5000 }
|
{
|
||||||
|
timeout: 5000,
|
||||||
|
reject: false,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let output = '';
|
if (stdout.trim()) {
|
||||||
nvidia.stdout.on('data', (data) => {
|
const devices = stdout
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise<HardwareDetectionResult>((resolve) => {
|
|
||||||
nvidia.on('close', (code) => {
|
|
||||||
if (code === 0 && output.trim()) {
|
|
||||||
const devices = output
|
|
||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
|
|
@ -156,24 +150,13 @@ async function detectCUDA() {
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
resolve({
|
return {
|
||||||
supported: devices.length > 0,
|
supported: devices.length > 0,
|
||||||
devices,
|
devices,
|
||||||
});
|
};
|
||||||
} else {
|
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
nvidia.on('error', () => {
|
return { supported: false, devices: [] };
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await terminateProcess(nvidia);
|
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
return { supported: false, devices: [] };
|
return { supported: false, devices: [] };
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +172,7 @@ async function findRocminfoCommand() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export async function detectROCm() {
|
export async function detectROCm() {
|
||||||
try {
|
try {
|
||||||
const rocminfoCommand = await findRocminfoCommand();
|
const rocminfoCommand = await findRocminfoCommand();
|
||||||
|
|
@ -197,21 +181,16 @@ export async function detectROCm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWindows = rocminfoCommand.includes('hipInfo');
|
const isWindows = rocminfoCommand.includes('hipInfo');
|
||||||
const rocminfo = spawn(rocminfoCommand, [], { timeout: 5000 });
|
const { stdout } = await execa(rocminfoCommand, [], {
|
||||||
|
timeout: 5000,
|
||||||
let output = '';
|
reject: false,
|
||||||
rocminfo.stdout.on('data', (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise<HardwareDetectionResult>((resolve) => {
|
if (stdout.trim()) {
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
rocminfo.on('close', (code) => {
|
|
||||||
if (code === 0 && output.trim()) {
|
|
||||||
const devices: string[] = [];
|
const devices: string[] = [];
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
const lines = output.split('\n');
|
const lines = stdout.split('\n');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmedLine = line.trim();
|
const trimmedLine = line.trim();
|
||||||
|
|
@ -227,7 +206,7 @@ export async function detectROCm() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const lines = output.split('\n');
|
const lines = stdout.split('\n');
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
|
|
||||||
|
|
@ -250,8 +229,7 @@ export async function detectROCm() {
|
||||||
) {
|
) {
|
||||||
if (lines[searchIndex].includes('Device Type:')) {
|
if (lines[searchIndex].includes('Device Type:')) {
|
||||||
deviceType =
|
deviceType =
|
||||||
lines[searchIndex].split('Device Type:')[1]?.trim() ||
|
lines[searchIndex].split('Device Type:')[1]?.trim() || '';
|
||||||
'';
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -264,24 +242,13 @@ export async function detectROCm() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve({
|
return {
|
||||||
supported: devices.length > 0,
|
supported: devices.length > 0,
|
||||||
devices,
|
devices,
|
||||||
});
|
};
|
||||||
} else {
|
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
rocminfo.on('error', () => {
|
return { supported: false, devices: [] };
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await terminateProcess(rocminfo);
|
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
return { supported: false, devices: [] };
|
return { supported: false, devices: [] };
|
||||||
}
|
}
|
||||||
|
|
@ -289,18 +256,14 @@ export async function detectROCm() {
|
||||||
|
|
||||||
async function detectVulkan() {
|
async function detectVulkan() {
|
||||||
try {
|
try {
|
||||||
const vulkaninfo = spawn('vulkaninfo', ['--summary'], { timeout: 5000 });
|
const { stdout } = await execa('vulkaninfo', ['--summary'], {
|
||||||
|
timeout: 5000,
|
||||||
let output = '';
|
reject: false,
|
||||||
vulkaninfo.stdout.on('data', (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise<HardwareDetectionResult>((resolve) => {
|
if (stdout.trim()) {
|
||||||
vulkaninfo.on('close', (code) => {
|
|
||||||
if (code === 0 && output.trim()) {
|
|
||||||
const devices: string[] = [];
|
const devices: string[] = [];
|
||||||
const lines = output.split('\n');
|
const lines = stdout.split('\n');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.includes('deviceName') && line.includes('=')) {
|
if (line.includes('deviceName') && line.includes('=')) {
|
||||||
|
|
@ -314,24 +277,13 @@ async function detectVulkan() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve({
|
return {
|
||||||
supported: devices.length > 0,
|
supported: devices.length > 0,
|
||||||
devices,
|
devices,
|
||||||
});
|
};
|
||||||
} else {
|
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
vulkaninfo.on('error', () => {
|
return { supported: false, devices: [] };
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await terminateProcess(vulkaninfo);
|
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
return { supported: false, devices: [] };
|
return { supported: false, devices: [] };
|
||||||
}
|
}
|
||||||
|
|
@ -391,35 +343,20 @@ function findDeviceNameInClInfo(lines: string[], startIndex: number) {
|
||||||
|
|
||||||
async function detectCLBlast() {
|
async function detectCLBlast() {
|
||||||
try {
|
try {
|
||||||
const clinfo = spawn('clinfo', [], { timeout: 3000 });
|
const { stdout } = await execa('clinfo', [], {
|
||||||
|
timeout: 3000,
|
||||||
let output = '';
|
reject: false,
|
||||||
clinfo.stdout.on('data', (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise<HardwareDetectionResult>((resolve) => {
|
if (stdout.trim()) {
|
||||||
clinfo.on('close', (code) => {
|
const devices = parseClInfoOutput(stdout);
|
||||||
if (code === 0 && output.trim()) {
|
return {
|
||||||
const devices = parseClInfoOutput(output);
|
|
||||||
resolve({
|
|
||||||
supported: devices.length > 0,
|
supported: devices.length > 0,
|
||||||
devices,
|
devices,
|
||||||
});
|
};
|
||||||
} else {
|
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
clinfo.on('error', () => {
|
return { supported: false, devices: [] };
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await terminateProcess(clinfo);
|
|
||||||
resolve({ supported: false, devices: [] });
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
return { supported: false, devices: [] };
|
return { supported: false, devices: [] };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { spawn, ChildProcess } from 'child_process';
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { platform } from 'process';
|
||||||
import {
|
import {
|
||||||
rm,
|
rm,
|
||||||
readdir,
|
readdir,
|
||||||
|
|
@ -63,7 +64,7 @@ async function removeDirectoryWithRetry(
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPermissionError && process.platform === 'win32') {
|
if (isPermissionError && platform === 'win32') {
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
`Attempt ${attempt}/${maxRetries} failed (file in use), retrying in ${delayMs}ms...`
|
`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) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
writer.on('finish', async () => {
|
writer.on('finish', async () => {
|
||||||
if (process.platform !== 'win32') {
|
if (platform !== 'win32') {
|
||||||
try {
|
try {
|
||||||
await chmod(tempPackedFilePath, 0o755);
|
await chmod(tempPackedFilePath, 0o755);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -153,9 +154,7 @@ async function setupLauncher(
|
||||||
|
|
||||||
if (!launcherPath || !(await pathExists(launcherPath))) {
|
if (!launcherPath || !(await pathExists(launcherPath))) {
|
||||||
const expectedLauncherName =
|
const expectedLauncherName =
|
||||||
process.platform === 'win32'
|
platform === 'win32' ? 'koboldcpp-launcher.exe' : 'koboldcpp-launcher';
|
||||||
? 'koboldcpp-launcher.exe'
|
|
||||||
: 'koboldcpp-launcher';
|
|
||||||
const newLauncherPath = join(unpackedDirPath, expectedLauncherName);
|
const newLauncherPath = join(unpackedDirPath, expectedLauncherName);
|
||||||
|
|
||||||
if (await pathExists(tempPackedFilePath)) {
|
if (await pathExists(tempPackedFilePath)) {
|
||||||
|
|
@ -297,7 +296,7 @@ async function patchKcppSduiEmbd(unpackedDir: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLauncherPath(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) {
|
for (const ext of extensions) {
|
||||||
const launcherPath = join(unpackedDir, `koboldcpp-launcher${ext}`);
|
const launcherPath = join(unpackedDir, `koboldcpp-launcher${ext}`);
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@ export const initializeLogger = () => {
|
||||||
format: format.combine(
|
format: format.combine(
|
||||||
format.timestamp(),
|
format.timestamp(),
|
||||||
format.printf(({ timestamp, level, message, error }) => {
|
format.printf(({ timestamp, level, message, error }) => {
|
||||||
const processInfo = `[${process.type || 'unknown'}:${process.pid}]`;
|
let logEntry = `${timestamp} [MAIN] [${level.toUpperCase()}] ${message}`;
|
||||||
let logEntry = `${timestamp} ${processInfo} [${level.toUpperCase()}] ${message}`;
|
|
||||||
|
|
||||||
if (error && error instanceof Error) {
|
if (error && error instanceof Error) {
|
||||||
logEntry += `\n Error: ${error.message}`;
|
logEntry += `\n Error: ${error.message}`;
|
||||||
|
|
@ -42,7 +41,6 @@ export const initializeLogger = () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
setupGlobalErrorHandlers();
|
|
||||||
isInitialized = true;
|
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 = () =>
|
export const getLogFilePath = () =>
|
||||||
join(app.getPath('userData'), 'logs', 'gerbil.log');
|
join(app.getPath('userData'), 'logs', 'gerbil.log');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { on } from 'process';
|
||||||
|
|
||||||
import { logError } from './logging';
|
import { logError } from './logging';
|
||||||
import { safeTryExecute, tryExecute } from '@/utils/node/logger';
|
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'];
|
const OPENWEBUI_BASE_ARGS = ['--python', '3.11', 'open-webui@latest', 'serve'];
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
on('SIGINT', () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
on('SIGTERM', () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { spawn } from 'child_process';
|
import { execa } from 'execa';
|
||||||
import { platform } from 'process';
|
import { platform } from 'process';
|
||||||
import { safeExecute } from '@/utils/node/logger';
|
import { safeExecute } from '@/utils/node/logger';
|
||||||
|
|
||||||
|
|
@ -12,37 +12,16 @@ const LINUX_PERFORMANCE_APPS = [
|
||||||
];
|
];
|
||||||
|
|
||||||
async function tryLaunchCommand(command: string, args: string[] = []) {
|
async function tryLaunchCommand(command: string, args: string[] = []) {
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
const child = spawn(command, args, {
|
await execa(command, args, {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
|
timeout: 2000,
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
let hasResolved = false;
|
} catch {
|
||||||
|
return 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openPerformanceManager = async () =>
|
export const openPerformanceManager = async () =>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
||||||
import { createServer, request, type Server } from 'http';
|
import { createServer, request, type Server } from 'http';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { platform, on } from 'process';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
import { logError } from './logging';
|
import { logError } from './logging';
|
||||||
|
|
@ -26,16 +27,15 @@ const SILLYTAVERN_BASE_ARGS = [
|
||||||
'--disableCsrf',
|
'--disableCsrf',
|
||||||
];
|
];
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
on('SIGINT', () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
on('SIGTERM', () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
function getFallbackDataRoot() {
|
function getFallbackDataRoot() {
|
||||||
const platform = process.platform;
|
|
||||||
const home = homedir();
|
const home = homedir();
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
|
|
@ -76,7 +76,7 @@ async function createNpxProcess(args: string[]) {
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
detached: false,
|
detached: false,
|
||||||
env,
|
env,
|
||||||
shell: process.platform === 'win32',
|
shell: platform === 'win32',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ const updaterAPI: UpdaterAPI = {
|
||||||
quitAndInstall: () => ipcRenderer.invoke('app:quitAndInstall'),
|
quitAndInstall: () => ipcRenderer.invoke('app:quitAndInstall'),
|
||||||
isUpdateDownloaded: () => ipcRenderer.invoke('app:isUpdateDownloaded'),
|
isUpdateDownloaded: () => ipcRenderer.invoke('app:isUpdateDownloaded'),
|
||||||
canAutoUpdate: () => ipcRenderer.invoke('app:canAutoUpdate'),
|
canAutoUpdate: () => ipcRenderer.invoke('app:canAutoUpdate'),
|
||||||
|
isAURInstallation: () => ipcRenderer.invoke('app:isAURInstallation'),
|
||||||
};
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
|
|
||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
|
|
@ -203,6 +203,7 @@ export interface UpdaterAPI {
|
||||||
quitAndInstall: () => void;
|
quitAndInstall: () => void;
|
||||||
isUpdateDownloaded: () => Promise<boolean>;
|
isUpdateDownloaded: () => Promise<boolean>;
|
||||||
canAutoUpdate: () => Promise<boolean>;
|
canAutoUpdate: () => Promise<boolean>;
|
||||||
|
isAURInstallation: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { resourcesPath } from 'process';
|
||||||
import { isDevelopment } from '@/utils/node/environment';
|
import { isDevelopment } from '@/utils/node/environment';
|
||||||
|
|
||||||
export function getAssetPath(assetName: string) {
|
export function getAssetPath(assetName: string) {
|
||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
return join(__dirname, '../../assets', assetName);
|
return join(__dirname, '../../assets', assetName);
|
||||||
} else {
|
} else {
|
||||||
return join(process.resourcesPath, '..', 'assets', assetName);
|
return join(resourcesPath, '..', 'assets', assetName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
export const isDevelopment = process.env.NODE_ENV === 'development';
|
import { env } from 'process';
|
||||||
|
|
||||||
|
export const isDevelopment = env.NODE_ENV === 'development';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
import { platform } from 'process';
|
||||||
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
|
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
|
||||||
|
|
||||||
export function getConfigDir() {
|
export function getConfigDir() {
|
||||||
|
|
@ -7,7 +8,6 @@ export function getConfigDir() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfigDirPath() {
|
function getConfigDirPath() {
|
||||||
const platform = process.platform;
|
|
||||||
const home = homedir();
|
const home = homedir();
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
import { platform } from 'process';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
export interface ProcessTerminationOptions {
|
export interface ProcessTerminationOptions {
|
||||||
|
|
@ -43,7 +44,7 @@ export async function terminateProcess(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
await killWindowsProcessTree(childProcess.pid, logError);
|
await killWindowsProcessTree(childProcess.pid, logError);
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue