Compare commits

..

4 commits

Author SHA1 Message Date
8e39cba942
chore: restore github badge, remove stars badge 2026-05-05 17:58:22 -07:00
ffb1297b03
new release 2026-05-05 16:52:58 -07:00
Egor
0aa967be9e fix: reduce Windows hardware detection hangs
- Replace siCpu() with synchronous os.cpus() — no PowerShell needed
- Replace siMem() with os.totalmem() in detectSystemMemory
- Add 3s timeout to siMemLayout() and siGraphics() so they don't
  block indefinitely behind monitoring's PowerShell queue
- Slow memory polling to 3s on Windows (was 1s) to free up the
  shared PowerShell session for hardware detection queries
- Split SystemTab loading: render version info immediately,
  hardware info fills in as background tasks complete
2026-05-05 16:38:54 -07:00
Egor
350c607d7c WIP: fixes from windows testing 2026-05-05 15:19:14 -07:00
12 changed files with 203 additions and 122 deletions

View file

@ -8,7 +8,6 @@
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)](https://github.com/lone-cloud/gerbil/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)](https://github.com/lone-cloud/gerbil/releases)
[![GitHub stars](https://img.shields.io/github/stars/lone-cloud/gerbil)](https://github.com/lone-cloud/gerbil/stargazers)
[![AUR version](https://img.shields.io/aur/version/gerbil)](https://aur.archlinux.org/packages/gerbil) [![AUR version](https://img.shields.io/aur/version/gerbil)](https://aur.archlinux.org/packages/gerbil)
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;"> <div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;">

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,11 +1,13 @@
{ {
"name": "gerbil", "name": "gerbil",
"version": "1.24.1", "version": "1.24.2",
"description": "Run Large Language Models locally", "description": "Run Large Language Models locally",
"keywords": [ "keywords": [
"ai", "ai",
"desktop",
"electron", "electron",
"koboldcpp", "gguf",
"image-generation",
"language-model", "language-model",
"llm", "llm",
"local-ai", "local-ai",
@ -58,7 +60,7 @@
"electron": "^41.5.0", "electron": "^41.5.0",
"electron-builder": "^26.8.1", "electron-builder": "^26.8.1",
"electron-vite": "^5.0.0", "electron-vite": "^5.0.0",
"jiti": "^2.6.1", "jiti": "^2.7.0",
"lucide-react": "^1.14.0", "lucide-react": "^1.14.0",
"oxfmt": "^0.48.0", "oxfmt": "^0.48.0",
"oxlint": "^1.63.0", "oxlint": "^1.63.0",

38
pnpm-lock.yaml generated
View file

@ -77,7 +77,7 @@ importers:
version: 4.25.9(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.41.1)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) version: 4.25.9(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.41.1)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)) version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0))
cross-env: cross-env:
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0 version: 10.1.0
@ -89,10 +89,10 @@ importers:
version: 26.8.1(electron-builder-squirrel-windows@26.8.1) version: 26.8.1(electron-builder-squirrel-windows@26.8.1)
electron-vite: electron-vite:
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.0(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)) version: 5.0.0(vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0))
jiti: jiti:
specifier: ^2.6.1 specifier: ^2.7.0
version: 2.6.1 version: 2.7.0
lucide-react: lucide-react:
specifier: ^1.14.0 specifier: ^1.14.0
version: 1.14.0(react@19.2.5) version: 1.14.0(react@19.2.5)
@ -137,7 +137,7 @@ importers:
version: 6.0.3 version: 6.0.3
vite: vite:
specifier: ^8.0.10 specifier: ^8.0.10
version: 8.0.10(@types/node@25.6.0)(jiti@2.6.1) version: 8.0.10(@types/node@25.6.0)(jiti@2.7.0)
zustand: zustand:
specifier: ^5.0.13 specifier: ^5.0.13
version: 5.0.13(@types/react@19.2.14)(react@19.2.5) version: 5.0.13(@types/react@19.2.14)(react@19.2.5)
@ -1449,8 +1449,8 @@ packages:
electron-publish@26.8.1: electron-publish@26.8.1:
resolution: {integrity: sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==} resolution: {integrity: sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==}
electron-to-chromium@1.5.349: electron-to-chromium@1.5.350:
resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==} resolution: {integrity: sha512-/KWD4qK8nMqIoJh35Rpc37fiVyOe80mcUQKpfje0Dp9uot2ROuipsh+EriCdfInxjleD5v1S4OlIn41I0LXP0g==}
electron-updater@6.8.3: electron-updater@6.8.3:
resolution: {integrity: sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==} resolution: {integrity: sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==}
@ -1848,8 +1848,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
jiti@2.6.1: jiti@2.7.0:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true hasBin: true
js-tokens@4.0.0: js-tokens@4.0.0:
@ -3770,10 +3770,10 @@ snapshots:
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1))': '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7 '@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.10(@types/node@25.6.0)(jiti@2.6.1) vite: 8.0.10(@types/node@25.6.0)(jiti@2.7.0)
'@xmldom/xmldom@0.8.13': {} '@xmldom/xmldom@0.8.13': {}
@ -3834,7 +3834,7 @@ snapshots:
fs-extra: 10.1.0 fs-extra: 10.1.0
hosted-git-info: 4.1.0 hosted-git-info: 4.1.0
isbinaryfile: 5.0.7 isbinaryfile: 5.0.7
jiti: 2.6.1 jiti: 2.7.0
js-yaml: 4.1.1 js-yaml: 4.1.1
json5: 2.2.3 json5: 2.2.3
lazy-val: 1.0.5 lazy-val: 1.0.5
@ -3896,7 +3896,7 @@ snapshots:
dependencies: dependencies:
baseline-browser-mapping: 2.10.27 baseline-browser-mapping: 2.10.27
caniuse-lite: 1.0.30001791 caniuse-lite: 1.0.30001791
electron-to-chromium: 1.5.349 electron-to-chromium: 1.5.350
node-releases: 2.0.38 node-releases: 2.0.38
update-browserslist-db: 1.2.3(browserslist@4.28.2) update-browserslist-db: 1.2.3(browserslist@4.28.2)
@ -4220,7 +4220,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
electron-to-chromium@1.5.349: {} electron-to-chromium@1.5.350: {}
electron-updater@6.8.3: electron-updater@6.8.3:
dependencies: dependencies:
@ -4235,7 +4235,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
electron-vite@5.0.0(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)): electron-vite@5.0.0(vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0)):
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
@ -4243,7 +4243,7 @@ snapshots:
esbuild: 0.25.12 esbuild: 0.25.12
magic-string: 0.30.21 magic-string: 0.30.21
picocolors: 1.1.1 picocolors: 1.1.1
vite: 8.0.10(@types/node@25.6.0)(jiti@2.6.1) vite: 8.0.10(@types/node@25.6.0)(jiti@2.7.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -4721,7 +4721,7 @@ snapshots:
filelist: 1.0.6 filelist: 1.0.6
picocolors: 1.1.1 picocolors: 1.1.1
jiti@2.6.1: {} jiti@2.7.0: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@ -5921,7 +5921,7 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.0.3 vfile-message: 4.0.3
vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1): vite@8.0.10(@types/node@25.6.0)(jiti@2.7.0):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
picomatch: 4.0.4 picomatch: 4.0.4
@ -5931,7 +5931,7 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 25.6.0 '@types/node': 25.6.0
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1 jiti: 2.7.0
w3c-keyname@2.2.8: {} w3c-keyname@2.2.8: {}

View file

@ -24,7 +24,13 @@ export const SystemTab = () => {
setVersionInfo(info); setVersionInfo(info);
setKoboldVersion(currentBackend?.version ?? null); setKoboldVersion(currentBackend?.version ?? null);
} catch (error) {
window.electronAPI.logs.logError('Failed to load system info', error as Error);
} finally {
setLoading(false);
}
try {
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([ const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([
window.electronAPI.kobold.detectCPU(), window.electronAPI.kobold.detectCPU(),
window.electronAPI.kobold.detectGPU(), window.electronAPI.kobold.detectGPU(),
@ -41,9 +47,7 @@ export const SystemTab = () => {
systemMemory, systemMemory,
}); });
} catch (error) { } catch (error) {
window.electronAPI.logs.logError('Failed to load system info', error as Error); window.electronAPI.logs.logError('Failed to load hardware info', error as Error);
} finally {
setLoading(false);
} }
}; };

View file

@ -1,7 +1,8 @@
import { cpus as osCpus, totalmem } from 'node:os';
import { platform } from 'node:process'; import { platform } from 'node:process';
import { execa } from 'execa'; import { execa } from 'execa';
import { cpu as siCpu, mem as siMem, memLayout as siMemLayout } from 'systeminformation'; import { memLayout as siMemLayout } from 'systeminformation';
import type { import type {
BasicGPUInfo, BasicGPUInfo,
@ -33,36 +34,26 @@ let basicGPUInfoCache: BasicGPUInfo | null = null;
let gpuCapabilitiesCache: GPUCapabilities | null = null; let gpuCapabilitiesCache: GPUCapabilities | null = null;
let gpuMemoryInfoCache: GPUMemoryInfo[] | null = null; let gpuMemoryInfoCache: GPUMemoryInfo[] | null = null;
export async function detectCPU() { export function detectCPU() {
if (cpuCapabilitiesCache) { if (cpuCapabilitiesCache) {
return cpuCapabilitiesCache; return cpuCapabilitiesCache;
} }
const result = await safeExecute(async () => { const cpuList = osCpus();
const cpu = await siCpu(); const brand = cpuList[0]?.model;
const cores = cpuList.length;
const speed = (cpuList[0]?.speed ?? 0) / 1000;
const devices: { name: string; detailedName: string }[] = []; const devices: { name: string; detailedName: string }[] = [];
if (cpu.brand) { if (brand) {
const name = formatDeviceName(cpu.brand); const name = formatDeviceName(brand);
devices.push({ devices.push({
detailedName: `${name} (${cpu.cores} cores) @ ${cpu.speed} GHz`, detailedName: `${name} (${cores} cores) @ ${speed} GHz`,
name, name,
}); });
} }
const capabilities = { cpuCapabilitiesCache = { devices };
devices,
};
cpuCapabilitiesCache = capabilities;
return capabilities;
}, 'CPU detection failed');
cpuCapabilitiesCache = result ?? {
devices: [],
};
return cpuCapabilitiesCache; return cpuCapabilitiesCache;
} }
@ -370,10 +361,13 @@ export async function detectGPUMemory() {
} }
export const detectSystemMemory = async () => { export const detectSystemMemory = async () => {
try { const totalGB = (totalmem() / 1024 ** 3).toFixed(2);
const [memInfo, memLayout] = await Promise.all([siMem(), siMemLayout()]);
const totalGB = (memInfo.total / 1024 ** 3).toFixed(2); try {
const memLayout = await Promise.race([
siMemLayout(),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
]);
let speed: number | undefined; let speed: number | undefined;
let type: string | undefined; let type: string | undefined;
@ -394,15 +388,8 @@ export const detectSystemMemory = async () => {
} }
} }
return { return { speed, totalGB, type };
speed,
totalGB,
type,
};
} catch { } catch {
const mem = await siMem(); return { totalGB };
return {
totalGB: (mem.total / 1024 ** 3).toFixed(2),
};
} }
}; };

View file

@ -199,6 +199,7 @@ export async function launchKoboldCpp(
const resolvedArgs = await resolveModelPaths(args); const resolvedArgs = await resolveModelPaths(args);
const finalArgs = resolvedArgs.filter((arg) => arg !== '--remotetunnel'); const finalArgs = resolvedArgs.filter((arg) => arg !== '--remotetunnel');
await stopProxy();
await startProxy(koboldHost, koboldPort); await startProxy(koboldHost, koboldPort);
const rocmEnv = const rocmEnv =
@ -251,7 +252,7 @@ export async function launchKoboldCpp(
readyResolve?.({ pid: child.pid, success: true }); readyResolve?.({ pid: child.pid, success: true });
if (remotetunnel) { if (remotetunnel && isKoboldFrontend) {
void startTunnel(frontendPreference, true); void startTunnel(frontendPreference, true);
} }
}; };
@ -350,7 +351,7 @@ export const launchKoboldCppWithCustomFrontends = async (
const frontendPreference = getConfig('frontendPreference'); const frontendPreference = getConfig('frontendPreference');
const imageGenerationFrontendPreference = getConfig('imageGenerationFrontendPreference'); const imageGenerationFrontendPreference = getConfig('imageGenerationFrontendPreference');
const { isTextMode } = parseKoboldConfig(args); const { isTextMode, remotetunnel } = parseKoboldConfig(args);
const result = await launchKoboldCpp( const result = await launchKoboldCpp(
args, args,
@ -367,17 +368,29 @@ export const launchKoboldCppWithCustomFrontends = async (
} }
if (frontendPreference === 'sillytavern') { if (frontendPreference === 'sillytavern') {
startSillyTavernFrontend(args).catch((error) => { startSillyTavernFrontend(args)
.then(() => {
if (remotetunnel) {
void startTunnel(frontendPreference, true);
}
})
.catch((error) => {
logError('Failed to start SillyTavern frontend:', error); logError('Failed to start SillyTavern frontend:', error);
sendKoboldOutput( sendKoboldOutput(
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`, `Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
); );
}); });
} else if (frontendPreference === 'openwebui') { } else if (frontendPreference === 'openwebui') {
startOpenWebUIFrontend(args).catch((error) => { startOpenWebUIFrontend(args)
logError('Failed to start OpenWebUI frontend:', error); .then(() => {
if (remotetunnel) {
void startTunnel(frontendPreference, true);
}
})
.catch((error) => {
logError('Failed to start Open WebUI frontend:', error);
sendKoboldOutput( sendKoboldOutput(
`Failed to start OpenWebUI: ${error instanceof Error ? error.message : String(error)}`, `Failed to start Open WebUI: ${error instanceof Error ? error.message : String(error)}`,
); );
}); });
} }

View file

@ -60,6 +60,7 @@ export const patchKliteEmbd = (unpackedDir: string) =>
const possiblePaths = [ const possiblePaths = [
join(unpackedDir, '_internal', 'embd_res', 'klite.embd'), join(unpackedDir, '_internal', 'embd_res', 'klite.embd'),
join(unpackedDir, 'embd_res', 'klite.embd'), join(unpackedDir, 'embd_res', 'klite.embd'),
join(unpackedDir, 'klite.embd'),
]; ];
let kliteEmbdPath: string | null = null; let kliteEmbdPath: string | null = null;
@ -96,6 +97,7 @@ export const patchKcppSduiEmbd = (unpackedDir: string) =>
tryExecute(async () => { tryExecute(async () => {
const possiblePaths = [ const possiblePaths = [
join(unpackedDir, '_internal', 'embd_res', 'kcpp_sdui.embd'), join(unpackedDir, '_internal', 'embd_res', 'kcpp_sdui.embd'),
join(unpackedDir, 'embd_res', 'kcpp_sdui.embd'),
join(unpackedDir, 'kcpp_sdui.embd'), join(unpackedDir, 'kcpp_sdui.embd'),
]; ];
@ -113,6 +115,7 @@ export const patchLcppGzEmbd = (unpackedDir: string) =>
tryExecute(async () => { tryExecute(async () => {
const possiblePaths = [ const possiblePaths = [
join(unpackedDir, '_internal', 'embd_res', 'lcpp.gz.embd'), join(unpackedDir, '_internal', 'embd_res', 'lcpp.gz.embd'),
join(unpackedDir, 'embd_res', 'lcpp.gz.embd'),
join(unpackedDir, 'lcpp.gz.embd'), join(unpackedDir, 'lcpp.gz.embd'),
]; ];

View file

@ -61,6 +61,7 @@ let memoryInterval: ReturnType<typeof setInterval> | null = null;
let gpuInterval: ReturnType<typeof setInterval> | null = null; let gpuInterval: ReturnType<typeof setInterval> | null = null;
let isRunning = false; let isRunning = false;
const updateFrequency = 1000; const updateFrequency = 1000;
const memoryUpdateFrequency = platform === 'win32' ? 3000 : updateFrequency;
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
let latestCpuMetrics: CpuMetrics | null = null; let latestCpuMetrics: CpuMetrics | null = null;
@ -87,7 +88,7 @@ export function startMonitoring(window: BrowserWindow) {
void collectAndSendMemoryMetrics(); void collectAndSendMemoryMetrics();
memoryInterval = setInterval(() => { memoryInterval = setInterval(() => {
void collectAndSendMemoryMetrics(); void collectAndSendMemoryMetrics();
}, updateFrequency); }, memoryUpdateFrequency);
if (platform === 'linux') { if (platform === 'linux') {
void collectAndSendGpuMetrics(); void collectAndSendGpuMetrics();

View file

@ -1,5 +1,6 @@
import type { ChildProcess } from 'node:child_process'; import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { readFile, unlink, writeFile } from 'node:fs/promises';
import { createServer, request } from 'node:http'; import { createServer, request } from 'node:http';
import type { Server } from 'node:http'; import type { Server } from 'node:http';
import { join } from 'node:path'; import { join } from 'node:path';
@ -10,7 +11,7 @@ import { PROXY } from '@/constants/proxy';
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs'; import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
import { parseKoboldConfig } from '@/utils/node/kobold'; import { parseKoboldConfig } from '@/utils/node/kobold';
import { logError, tryExecute } from '@/utils/node/logging'; import { logError, tryExecute } from '@/utils/node/logging';
import { terminateProcess } from '@/utils/node/process'; import { killProcessByPid, terminateProcess } from '@/utils/node/process';
import { getInstallDir } from './config'; import { getInstallDir } from './config';
import { getNodeEnvironment } from './dependencies'; import { getNodeEnvironment } from './dependencies';
@ -39,6 +40,27 @@ const getSillyTavernServerPath = () =>
const getSillyTavernSettingsPath = () => const getSillyTavernSettingsPath = () =>
join(getSillyTavernDataDir(), 'default-user', 'settings.json'); join(getSillyTavernDataDir(), 'default-user', 'settings.json');
const getSillyTavernPidPath = () => join(getSillyTavernInstallDir(), 'sillytavern.pid');
async function saveSillyTavernPid(pid: number) {
await writeFile(getSillyTavernPidPath(), String(pid), 'utf8').catch(() => {});
}
async function clearSillyTavernPid() {
await unlink(getSillyTavernPidPath()).catch(() => {});
}
async function killOrphanedSillyTavern() {
const pidPath = getSillyTavernPidPath();
if (!(await pathExists(pidPath))) return;
const raw = await readFile(pidPath, 'utf8').catch(() => null);
const pid = raw ? parseInt(raw.trim(), 10) : NaN;
if (!isNaN(pid)) {
await killProcessByPid(pid);
}
await clearSillyTavernPid();
}
async function ensureSillyTavernInstalled() { async function ensureSillyTavernInstalled() {
const serverPath = getSillyTavernServerPath(); const serverPath = getSillyTavernServerPath();
const installDir = getSillyTavernInstallDir(); const installDir = getSillyTavernInstallDir();
@ -73,7 +95,10 @@ async function ensureSillyTavernInstalled() {
sendKoboldOutput('Installing SillyTavern via npm...'); sendKoboldOutput('Installing SillyTavern via npm...');
} }
return new Promise<void>((resolve, reject) => { await killOrphanedSillyTavern();
const runNpmInstall = () =>
new Promise<void>((resolve, reject) => {
const npmProcess = spawn( const npmProcess = spawn(
'npm', 'npm',
[ [
@ -105,17 +130,31 @@ async function ensureSillyTavernInstalled() {
sendKoboldOutput('SillyTavern is ready'); sendKoboldOutput('SillyTavern is ready');
resolve(); resolve();
} else { } else {
if (errorOutput) { reject(Object.assign(new Error(`npm install failed with code ${code}`), { errorOutput }));
sendKoboldOutput(`npm install error: ${errorOutput.trim()}`);
}
reject(new Error(`npm install failed with code ${code}`));
} }
}); });
npmProcess.on('error', (error) => { npmProcess.on('error', reject);
reject(error);
});
}); });
try {
await runNpmInstall();
} catch (err) {
const isBusy =
err instanceof Error && (err.message.includes('4294963214') || err.message.includes('-4082'));
if (isBusy && platform === 'win32') {
sendKoboldOutput('File busy, retrying npm install...');
await new Promise<void>((resolve) => setTimeout(resolve, 2000));
await runNpmInstall();
} else {
const output = (err as { errorOutput?: string }).errorOutput;
if (output) {
sendKoboldOutput(`npm install error: ${output.trim()}`);
}
throw err;
}
}
} }
async function createNpxProcess(args: string[]) { async function createNpxProcess(args: string[]) {
@ -289,8 +328,15 @@ async function waitForSillyTavernToStart() {
const createProxyServer = (targetPort: number, proxyPort: number) => { const createProxyServer = (targetPort: number, proxyPort: number) => {
proxyServer = createServer((req, res) => { proxyServer = createServer((req, res) => {
const headers = { ...req.headers };
delete headers['x-forwarded-for'];
delete headers['x-real-ip'];
delete headers['cf-connecting-ip'];
delete headers['x-forwarded-host'];
delete headers.forwarded;
const options = { const options = {
headers: req.headers, headers,
hostname: 'localhost', hostname: 'localhost',
method: req.method, method: req.method,
path: req.url, path: req.url,
@ -298,9 +344,9 @@ const createProxyServer = (targetPort: number, proxyPort: number) => {
}; };
const proxyReq = request(options, (proxyRes) => { const proxyReq = request(options, (proxyRes) => {
const headers = { ...proxyRes.headers }; const responseHeaders = { ...proxyRes.headers };
delete headers['x-frame-options']; delete responseHeaders['x-frame-options'];
res.writeHead(proxyRes.statusCode ?? 200, headers); res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);
proxyRes.pipe(res); proxyRes.pipe(res);
}); });
@ -341,6 +387,8 @@ export async function startFrontend(args: string[]) {
sendKoboldOutput(`Starting ${config.name} frontend on port ${config.port}...`); sendKoboldOutput(`Starting ${config.name} frontend on port ${config.port}...`);
createProxyServer(config.port, config.proxyPort);
const sillyTavernDataDir = getSillyTavernDataDir(); const sillyTavernDataDir = getSillyTavernDataDir();
const sillyTavernArgs = [ const sillyTavernArgs = [
@ -353,6 +401,10 @@ export async function startFrontend(args: string[]) {
sillyTavernProcess = await createNpxProcess(sillyTavernArgs); sillyTavernProcess = await createNpxProcess(sillyTavernArgs);
if (sillyTavernProcess.pid) {
void saveSillyTavernPid(sillyTavernProcess.pid);
}
if (sillyTavernProcess.stdout) { if (sillyTavernProcess.stdout) {
sillyTavernProcess.stdout.on('data', (data: Buffer) => { sillyTavernProcess.stdout.on('data', (data: Buffer) => {
sendKoboldOutput(data.toString(), true); sendKoboldOutput(data.toString(), true);
@ -371,6 +423,7 @@ export async function startFrontend(args: string[]) {
: `SillyTavern exited with code ${code}`; : `SillyTavern exited with code ${code}`;
sendKoboldOutput(message); sendKoboldOutput(message);
sillyTavernProcess = null; sillyTavernProcess = null;
void clearSillyTavernPid();
}); });
sillyTavernProcess.on('error', (error) => { sillyTavernProcess.on('error', (error) => {
@ -381,12 +434,12 @@ export async function startFrontend(args: string[]) {
}); });
await waitForSillyTavernToStart(); await waitForSillyTavernToStart();
createProxyServer(config.port, config.proxyPort);
} catch (error) { } catch (error) {
logError( logError(
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`, `Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
error as Error, error as Error,
); );
await stopFrontend();
throw error; throw error;
} }
} }
@ -410,4 +463,10 @@ export async function stopFrontend() {
} }
await Promise.all(promises); await Promise.all(promises);
if (platform === 'win32') {
await new Promise<void>((resolve) => {
setTimeout(resolve, 1000);
});
}
} }

View file

@ -151,7 +151,10 @@ async function getLinuxGPUData() {
async function getWindowsGPUData() { async function getWindowsGPUData() {
try { try {
const graphics = await siGraphics(); const graphics = await Promise.race([
siGraphics(),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
]);
const discreteControllers = graphics.controllers.filter( const discreteControllers = graphics.controllers.filter(
(controller) => controller.vram && controller.vram >= 1024, (controller) => controller.vram && controller.vram >= 1024,

View file

@ -11,6 +11,16 @@ async function killWindowsProcessTree(pid: number) {
} catch {} } catch {}
} }
export async function killProcessByPid(pid: number) {
if (platform === 'win32') {
await killWindowsProcessTree(pid);
} else {
try {
process.kill(pid, 'SIGKILL');
} catch {}
}
}
export async function terminateProcess(childProcess: ChildProcess | null) { export async function terminateProcess(childProcess: ChildProcess | null) {
if (!childProcess?.pid) { if (!childProcess?.pid) {
return; return;