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'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'process.env.VITE_DEV_SERVER_URL': JSON.stringify(
|
||||
process.env.VITE_DEV_SERVER_URL
|
||||
),
|
||||
},
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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": "./",
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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> => {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: [] };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () =>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
|
|
@ -203,6 +203,7 @@ export interface UpdaterAPI {
|
|||
quitAndInstall: () => void;
|
||||
isUpdateDownloaded: () => Promise<boolean>;
|
||||
canAutoUpdate: () => Promise<boolean>;
|
||||
isAURInstallation: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { 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) {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue