less verbone frontend error handling, better process killing for windows, better loading behaviour for available backends

This commit is contained in:
lone-cloud 2025-08-29 16:32:22 -07:00
parent ae8ec47f51
commit 66a710dc95
18 changed files with 376 additions and 320 deletions

View file

@ -12,6 +12,7 @@ import { AppHeader } from '@/components/AppHeader';
import { useUpdateChecker } from '@/hooks/useUpdateChecker'; import { useUpdateChecker } from '@/hooks/useUpdateChecker';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { UI } from '@/constants'; import { UI } from '@/constants';
import { Logger } from '@/utils/logger';
import type { DownloadItem } from '@/types/electron'; import type { DownloadItem } from '@/types/electron';
import type { InterfaceTab, FrontendPreference, Screen } from '@/types'; import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
@ -45,7 +46,7 @@ export const App = () => {
useEffect(() => { useEffect(() => {
const checkInstallation = async () => { const checkInstallation = async () => {
try { await Logger.safeExecute(async () => {
const [versions, currentBinaryPath, hasSeenWelcome, preference] = const [versions, currentBinaryPath, hasSeenWelcome, preference] =
await Promise.all([ await Promise.all([
window.electronAPI.kobold.getInstalledVersions(), window.electronAPI.kobold.getInstalledVersions(),
@ -81,20 +82,14 @@ export const App = () => {
setCurrentScreenWithTransition('download'); setCurrentScreenWithTransition('download');
} }
setHasInitialized(true);
if (versions.length > 0) { if (versions.length > 0) {
setTimeout(() => { setTimeout(() => {
checkForUpdates(); checkForUpdates();
}, 2000); }, 2000);
} }
} catch (error) { }, 'Error checking installation:');
window.electronAPI.logs.logError(
'Error checking installation:', setHasInitialized(true);
error as Error
);
setHasInitialized(true);
}
}; };
checkInstallation(); checkInstallation();
@ -102,7 +97,7 @@ export const App = () => {
}, []); }, []);
const handleBinaryUpdate = async (download: DownloadItem) => { const handleBinaryUpdate = async (download: DownloadItem) => {
try { await Logger.safeExecute(async () => {
const success = await sharedHandleDownload({ const success = await sharedHandleDownload({
type: 'asset', type: 'asset',
item: download, item: download,
@ -113,16 +108,11 @@ export const App = () => {
if (success) { if (success) {
dismissUpdate(); dismissUpdate();
} }
} catch (error) { }, 'Failed to update binary:');
window.electronAPI.logs.logError(
'Failed to update binary:',
error as Error
);
}
}; };
const handleDownloadComplete = async () => { const handleDownloadComplete = async () => {
try { await Logger.safeExecute(async () => {
const [versions, currentBinaryPath] = await Promise.all([ const [versions, currentBinaryPath] = await Promise.all([
window.electronAPI.kobold.getInstalledVersions(), window.electronAPI.kobold.getInstalledVersions(),
window.electronAPI.config.get('currentKoboldBinary') as Promise<string>, window.electronAPI.config.get('currentKoboldBinary') as Promise<string>,
@ -143,12 +133,7 @@ export const App = () => {
} }
} }
} }
} catch (error) { }, 'Error refreshing versions after download:');
window.electronAPI.logs.logError(
'Error refreshing versions after download:',
error as Error
);
}
setTimeout(() => { setTimeout(() => {
setCurrentScreenWithTransition('launch'); setCurrentScreenWithTransition('launch');
@ -176,16 +161,8 @@ export const App = () => {
} }
}; };
const performEject = async () => { const performEject = () => {
try { window.electronAPI.kobold.stopKoboldCpp();
await window.electronAPI.kobold.stopKoboldCpp();
} catch (error) {
window.electronAPI.logs.logError(
'Error stopping KoboldCpp:',
error as Error
);
}
handleBackToLaunch(); handleBackToLaunch();
}; };
@ -302,17 +279,14 @@ export const App = () => {
opened={settingsOpened} opened={settingsOpened}
onClose={async () => { onClose={async () => {
setSettingsOpened(false); setSettingsOpened(false);
try { const preference = await Logger.safeExecute(
const preference = (await window.electronAPI.config.get( () =>
'frontendPreference' window.electronAPI.config.get(
)) as FrontendPreference; 'frontendPreference'
setFrontendPreference(preference || 'koboldcpp'); ) as Promise<FrontendPreference>,
} catch (error) { 'Failed to load frontend preference:'
window.electronAPI.logs.logError( );
'Failed to load frontend preference:', setFrontendPreference(preference || 'koboldcpp');
error as Error
);
}
}} }}
currentScreen={currentScreen || undefined} currentScreen={currentScreen || undefined}
/> />

View file

@ -14,6 +14,7 @@ import { useState } from 'react';
import type { InstalledVersion, DownloadItem } from '@/types/electron'; import type { InstalledVersion, DownloadItem } from '@/types/electron';
import { getDisplayNameFromPath } from '@/utils/version'; import { getDisplayNameFromPath } from '@/utils/version';
import { GITHUB_API } from '@/constants'; import { GITHUB_API } from '@/constants';
import { Logger } from '@/utils/logger';
interface UpdateAvailableModalProps { interface UpdateAvailableModalProps {
opened: boolean; opened: boolean;
@ -37,15 +38,12 @@ export const UpdateAvailableModal = ({
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const handleUpdate = async () => { const handleUpdate = async () => {
try { setIsUpdating(true);
setIsUpdating(true); await Logger.safeExecute(async () => {
await onUpdate(availableUpdate); await onUpdate(availableUpdate);
onClose(); onClose();
} catch (error) { }, 'Failed to update:');
window.electronAPI.logs.logError('Failed to update:', error as Error); setIsUpdating(false);
} finally {
setIsUpdating(false);
}
}; };
return ( return (

View file

@ -5,6 +5,7 @@ import { getPlatformDisplayName } from '@/utils/platform';
import { formatDownloadSize } from '@/utils/download'; import { formatDownloadSize } from '@/utils/download';
import { getAssetDescription, sortDownloadsByType } from '@/utils/assets'; import { getAssetDescription, sortDownloadsByType } from '@/utils/assets';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { Logger } from '@/utils/logger';
import type { DownloadItem } from '@/types/electron'; import type { DownloadItem } from '@/types/electron';
interface DownloadScreenProps { interface DownloadScreenProps {
@ -33,7 +34,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
async (download: DownloadItem) => { async (download: DownloadItem) => {
setDownloadingAsset(download.name); setDownloadingAsset(download.name);
try { await Logger.safeExecute(async () => {
const success = await sharedHandleDownload({ const success = await sharedHandleDownload({
type: 'asset', type: 'asset',
item: download, item: download,
@ -48,14 +49,9 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
setDownloadingAsset(null); setDownloadingAsset(null);
}, 200); }, 200);
} }
} catch (error) { }, `Failed to download ${download.name}:`);
window.electronAPI.logs.logError(
`Failed to download ${download.name}:`, setDownloadingAsset(null);
error as Error
);
} finally {
setDownloadingAsset(null);
}
}, },
[sharedHandleDownload, onDownloadComplete] [sharedHandleDownload, onDownloadComplete]
); );

View file

@ -8,7 +8,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import styles from '@/styles/layout.module.css'; import styles from '@/styles/layout.module.css';
import { UI } from '@/constants'; import { UI, SERVER_READY_SIGNALS } from '@/constants';
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal'; import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
import { useLaunchConfigStore } from '@/stores/launchConfigStore'; import { useLaunchConfigStore } from '@/stores/launchConfigStore';
import type { FrontendPreference } from '@/types'; import type { FrontendPreference } from '@/types';
@ -69,14 +69,14 @@ export const TerminalTab = ({
const serverPort = port || 5001; const serverPort = port || 5001;
if (frontendPreference === 'sillytavern') { if (frontendPreference === 'sillytavern') {
if (newData.includes('SillyTavern is listening on')) { if (newData.includes(SERVER_READY_SIGNALS.SILLYTAVERN)) {
setTimeout( setTimeout(
() => onServerReady(`http://${serverHost}:${serverPort}`), () => onServerReady(`http://${serverHost}:${serverPort}`),
1500 1500
); );
} }
} else { } else {
if (newData.includes('Please connect to custom endpoint at')) { if (newData.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
setTimeout( setTimeout(
() => onServerReady(`http://${serverHost}:${serverPort}`), () => onServerReady(`http://${serverHost}:${serverPort}`),
1500 1500

View file

@ -3,6 +3,7 @@ import { useState, useEffect } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { Logger } from '@/utils/logger';
export const AdvancedTab = () => { export const AdvancedTab = () => {
const { const {
@ -34,25 +35,21 @@ export const AdvancedTab = () => {
useEffect(() => { useEffect(() => {
const detectBackendSupport = async () => { const detectBackendSupport = async () => {
try { const support = await Logger.safeExecute(
const support = await window.electronAPI.kobold.detectBackendSupport(); () => window.electronAPI.kobold.detectBackendSupport(),
if (support) { 'Failed to detect backend support:'
setBackendSupport({ );
noavx2: support.noavx2,
failsafe: support.failsafe, if (support) {
}); setBackendSupport({
} else { noavx2: support.noavx2,
setBackendSupport({ noavx2: false, failsafe: false }); failsafe: support.failsafe,
} });
} catch (error) { } else {
window.electronAPI.logs.logError(
'Failed to detect backend support:',
error as Error
);
setBackendSupport({ noavx2: false, failsafe: false }); setBackendSupport({ noavx2: false, failsafe: false });
} finally {
setIsLoading(false);
} }
setIsLoading(false);
}; };
void detectBackendSupport(); void detectBackendSupport();

View file

@ -4,6 +4,7 @@ import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem'; import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector'; import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { Logger } from '@/utils/logger';
import type { BackendOption } from '@/types'; import type { BackendOption } from '@/types';
export const BackendSelector = () => { export const BackendSelector = () => {
@ -19,22 +20,19 @@ export const BackendSelector = () => {
const [availableBackends, setAvailableBackends] = useState<BackendOption[]>( const [availableBackends, setAvailableBackends] = useState<BackendOption[]>(
[] []
); );
const [isLoadingBackends, setIsLoadingBackends] = useState(false);
const hasInitialized = useRef(false); const hasInitialized = useRef(false);
useEffect(() => { useEffect(() => {
const loadBackends = async () => { const loadBackends = async () => {
try { setIsLoadingBackends(true);
const backends = const backends = await Logger.safeExecute(
await window.electronAPI.kobold.getAvailableBackends(true); () => window.electronAPI.kobold.getAvailableBackends(true),
setAvailableBackends(backends); 'Failed to detect available backends:'
hasInitialized.current = true; );
} catch (error) { setAvailableBackends(backends || []);
window.electronAPI.logs.logError( setIsLoadingBackends(false);
'Failed to detect available backends:', hasInitialized.current = true;
error as Error
);
setAvailableBackends([]);
}
}; };
if (!hasInitialized.current) { if (!hasInitialized.current) {
@ -60,7 +58,9 @@ export const BackendSelector = () => {
<InfoTooltip label="Select a backend to use. CUDA runs on NVIDIA GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast work on all GPUs." /> <InfoTooltip label="Select a backend to use. CUDA runs on NVIDIA GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast work on all GPUs." />
</Group> </Group>
<Select <Select
placeholder="Select backend" placeholder={
isLoadingBackends ? 'Loading backends...' : 'Select backend'
}
value={backend} value={backend}
onChange={(value) => { onChange={(value) => {
if (value) { if (value) {
@ -72,7 +72,7 @@ export const BackendSelector = () => {
label: b.label, label: b.label,
disabled: b.disabled, disabled: b.disabled,
}))} }))}
disabled={availableBackends.length === 0} disabled={isLoadingBackends || availableBackends.length === 0}
renderOption={({ option }) => { renderOption={({ option }) => {
const backendData = availableBackends.find( const backendData = availableBackends.find(
(b) => b.value === option.value (b) => b.value === option.value

View file

@ -10,6 +10,7 @@ import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationT
import { WarningDisplay } from '@/components/WarningDisplay'; import { WarningDisplay } from '@/components/WarningDisplay';
import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager'; import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager';
import { DEFAULT_MODEL_URL } from '@/constants'; import { DEFAULT_MODEL_URL } from '@/constants';
import { Logger } from '@/utils/logger';
import type { ConfigFile } from '@/types'; import type { ConfigFile } from '@/types';
interface LaunchScreenProps { interface LaunchScreenProps {
@ -83,23 +84,19 @@ export const LaunchScreen = ({
}); });
const setHappyDefaults = useCallback(async () => { const setHappyDefaults = useCallback(async () => {
try { const backends = await Logger.safeExecute(
const backends = await window.electronAPI.kobold.getAvailableBackends(); () => window.electronAPI.kobold.getAvailableBackends(),
'Failed to set defaults:'
);
if (!backend && backends.length > 0) { if (!backend && backends && backends.length > 0) {
handleBackendChange(backends[0].value); handleBackendChange(backends[0].value);
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to set defaults:',
error as Error
);
} }
}, [backend, handleBackendChange]); }, [backend, handleBackendChange]);
const setInitialDefaults = useCallback( const setInitialDefaults = useCallback(
async (currentModelPath: string, currentSdModel: string) => { async (currentModelPath: string, currentSdModel: string) => {
try { await Logger.tryExecute(async () => {
if ( if (
!defaultsSetRef.current && !defaultsSetRef.current &&
!currentModelPath.trim() && !currentModelPath.trim() &&
@ -108,12 +105,7 @@ export const LaunchScreen = ({
handleModelPathChange(DEFAULT_MODEL_URL); handleModelPathChange(DEFAULT_MODEL_URL);
defaultsSetRef.current = true; defaultsSetRef.current = true;
} }
} catch (error) { }, 'Failed to set initial defaults:');
window.electronAPI.logs.logError(
'Failed to set initial defaults:',
error as Error
);
}
}, },
[handleModelPathChange] [handleModelPathChange]
); );
@ -194,49 +186,43 @@ export const LaunchScreen = ({
}); });
const handleCreateNewConfig = async (configName: string) => { const handleCreateNewConfig = async (configName: string) => {
try { await Logger.safeExecute(async () => {
const fullConfigName = `${configName}.json`; const fullConfigName = `${configName}.json`;
const success = await window.electronAPI.kobold.saveConfigFile( const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
fullConfigName, fullConfigName,
buildConfigData() buildConfigData()
); );
if (success) { if (saveSuccess) {
await loadConfigFiles(); await loadConfigFiles();
setSelectedFile(fullConfigName); setSelectedFile(fullConfigName);
await window.electronAPI.kobold.setSelectedConfig(fullConfigName); await window.electronAPI.kobold.setSelectedConfig(fullConfigName);
} else { } else {
window.electronAPI.logs.logError( Logger.error(
'Failed to create new configuration', 'Failed to create new configuration',
new Error('Save operation failed') new Error('Save operation failed')
); );
} }
} catch (error) { }, 'Failed to create new configuration:');
window.electronAPI.logs.logError(
'Failed to create new configuration:',
error as Error
);
}
}; };
const handleSaveConfig = async () => { const handleSaveConfig = async () => {
if (!selectedFile) { if (!selectedFile) {
window.electronAPI.logs.logError( Logger.error(
'No configuration file selected for saving', 'No configuration file selected for saving',
new Error('Selected file is null') new Error('Selected file is null')
); );
return false; return false;
} }
try { const success = await Logger.safeExecute(async () => {
const success = await window.electronAPI.kobold.saveConfigFile( const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
selectedFile, selectedFile,
buildConfigData() buildConfigData()
); );
if (!success) { if (!saveSuccess) {
window.electronAPI.logs.logError( Logger.error(
'Failed to save configuration', 'Failed to save configuration',
new Error('Save operation failed') new Error('Save operation failed')
); );
@ -244,13 +230,9 @@ export const LaunchScreen = ({
} }
return true; return true;
} catch (error) { }, 'Failed to save configuration:');
window.electronAPI.logs.logError(
'Failed to save configuration:', return success ?? false;
error as Error
);
return false;
}
}; };
useEffect(() => { useEffect(() => {

View file

@ -13,6 +13,7 @@ import { Folder, FolderOpen, Monitor } from 'lucide-react';
import styles from '@/styles/layout.module.css'; import styles from '@/styles/layout.module.css';
import type { FrontendPreference } from '@/types'; import type { FrontendPreference } from '@/types';
import { FRONTENDS } from '@/constants'; import { FRONTENDS } from '@/constants';
import { Logger } from '@/utils/logger';
export const GeneralTab = () => { export const GeneralTab = () => {
const [installDir, setInstallDir] = useState<string>(''); const [installDir, setInstallDir] = useState<string>('');
@ -29,31 +30,21 @@ export const GeneralTab = () => {
useEffect(() => { useEffect(() => {
const checkNpxAvailability = async () => { const checkNpxAvailability = async () => {
try { const available = await Logger.safeExecute(
const available = await window.electronAPI.sillytavern.isNpxAvailable(); () => window.electronAPI.sillytavern.isNpxAvailable(),
setIsNpxAvailable(available); 'Failed to check npx availability:'
);
if (!available && FrontendPreference === 'sillytavern') { const isAvailable = available ?? false;
await window.electronAPI.config.set( setIsNpxAvailable(isAvailable);
'frontendPreference',
'koboldcpp' if (!isAvailable && FrontendPreference === 'sillytavern') {
); await Logger.tryExecute(
setFrontendPreference('koboldcpp'); () =>
} window.electronAPI.config.set('frontendPreference', 'koboldcpp'),
} catch (error) { 'Failed to reset frontend preference:'
window.electronAPI.logs.logError(
'Failed to check npx availability:',
error as Error
); );
setIsNpxAvailable(false); setFrontendPreference('koboldcpp');
if (FrontendPreference === 'sillytavern') {
await window.electronAPI.config.set(
'frontendPreference',
'koboldcpp'
);
setFrontendPreference('koboldcpp');
}
} }
}; };
@ -63,58 +54,46 @@ export const GeneralTab = () => {
}, [FrontendPreference]); }, [FrontendPreference]);
const loadCurrentInstallDir = async () => { const loadCurrentInstallDir = async () => {
try { const currentDir = await Logger.safeExecute(
const currentDir = await window.electronAPI.kobold.getCurrentInstallDir(); () => window.electronAPI.kobold.getCurrentInstallDir(),
'Failed to load install directory:'
);
if (currentDir) {
setInstallDir(currentDir); setInstallDir(currentDir);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to load install directory:',
error as Error
);
} }
}; };
const loadFrontendPreference = async () => { const loadFrontendPreference = async () => {
try { const frontendPreference = await Logger.safeExecute(
const frontendPreference = (await window.electronAPI.config.get( () => window.electronAPI.config.get('frontendPreference'),
'frontendPreference' 'Failed to load frontend preference:'
)) as FrontendPreference; );
setFrontendPreference(frontendPreference || 'koboldcpp'); if (frontendPreference) {
} catch (error) { setFrontendPreference(
window.electronAPI.logs.logError( (frontendPreference as FrontendPreference) || 'koboldcpp'
'Failed to load frontend preference:',
error as Error
); );
} }
}; };
const handleSelectInstallDir = async () => { const handleSelectInstallDir = async () => {
try { const selectedDir = await Logger.safeExecute(
const selectedDir = () => window.electronAPI.kobold.selectInstallDirectory(),
await window.electronAPI.kobold.selectInstallDirectory(); 'Failed to select install directory:'
);
if (selectedDir) { if (selectedDir) {
setInstallDir(selectedDir); setInstallDir(selectedDir);
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to select install directory:',
error as Error
);
} }
}; };
const handleFrontendPreferenceChange = async (value: string | null) => { const handleFrontendPreferenceChange = async (value: string | null) => {
if (!value || (value !== 'koboldcpp' && value !== 'sillytavern')) return; if (!value || (value !== 'koboldcpp' && value !== 'sillytavern')) return;
try { const success = await Logger.tryExecute(
await window.electronAPI.config.set('frontendPreference', value); () => window.electronAPI.config.set('frontendPreference', value),
'Failed to save frontend preference:'
);
if (success) {
setFrontendPreference(value); setFrontendPreference(value);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to save frontend preference:',
error as Error
);
} }
}; };

View file

@ -18,6 +18,7 @@ import {
stripAssetExtensions, stripAssetExtensions,
compareVersions, compareVersions,
} from '@/utils/version'; } from '@/utils/version';
import { Logger } from '@/utils/logger';
import { formatDownloadSize } from '@/utils/download'; import { formatDownloadSize } from '@/utils/download';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
@ -62,7 +63,7 @@ export const VersionsTab = () => {
const loadInstalledVersions = useCallback(async () => { const loadInstalledVersions = useCallback(async () => {
setLoadingInstalled(true); setLoadingInstalled(true);
try { await Logger.safeExecute(async () => {
const [versions, currentBinaryPath] = await Promise.all([ const [versions, currentBinaryPath] = await Promise.all([
window.electronAPI.kobold.getInstalledVersions(), window.electronAPI.kobold.getInstalledVersions(),
window.electronAPI.config.get('currentKoboldBinary') as Promise<string>, window.electronAPI.config.get('currentKoboldBinary') as Promise<string>,
@ -95,25 +96,18 @@ export const VersionsTab = () => {
await window.electronAPI.config.set('currentKoboldBinary', ''); await window.electronAPI.config.set('currentKoboldBinary', '');
} }
} }
} catch (error) { }, 'Failed to load installed versions:');
window.electronAPI.logs.logError(
'Failed to load installed versions:', setLoadingInstalled(false);
error as Error
);
} finally {
setLoadingInstalled(false);
}
}, []); }, []);
const loadLatestRelease = useCallback(async () => { const loadLatestRelease = useCallback(async () => {
try { const release = await Logger.safeExecute(
const release = await getLatestReleaseWithDownloadStatus(); () => getLatestReleaseWithDownloadStatus(),
'Failed to load latest release:'
);
if (release) {
setLatestRelease(release); setLatestRelease(release);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to load latest release:',
error as Error
);
} }
}, [getLatestReleaseWithDownloadStatus]); }, [getLatestReleaseWithDownloadStatus]);
@ -205,7 +199,7 @@ export const VersionsTab = () => {
}, [downloading]); }, [downloading]);
const handleDownload = async (version: VersionInfo) => { const handleDownload = async (version: VersionInfo) => {
try { await Logger.safeExecute(async () => {
const download = availableDownloads.find((d) => d.name === version.name); const download = availableDownloads.find((d) => d.name === version.name);
if (!download) { if (!download) {
throw new Error('Download not found'); throw new Error('Download not found');
@ -221,13 +215,11 @@ export const VersionsTab = () => {
if (success) { if (success) {
await loadInstalledVersions(); await loadInstalledVersions();
} }
} catch (error) { }, 'Failed to download:');
window.electronAPI.logs.logError('Failed to download:', error as Error);
}
}; };
const handleUpdate = async (version: VersionInfo) => { const handleUpdate = async (version: VersionInfo) => {
try { await Logger.safeExecute(async () => {
const download = availableDownloads.find((d) => d.name === version.name); const download = availableDownloads.find((d) => d.name === version.name);
if (!download) { if (!download) {
throw new Error('Download not found'); throw new Error('Download not found');
@ -243,23 +235,21 @@ export const VersionsTab = () => {
if (success) { if (success) {
await loadInstalledVersions(); await loadInstalledVersions();
} }
} catch (error) { }, 'Failed to update:');
window.electronAPI.logs.logError('Failed to update:', error as Error);
}
}; };
const makeCurrent = async (version: VersionInfo) => { const makeCurrent = async (version: VersionInfo) => {
if (!version.installedPath) return; if (!version.installedPath) return;
try { await Logger.safeExecute(async () => {
const success = await window.electronAPI.kobold.setCurrentVersion( const success = await window.electronAPI.kobold.setCurrentVersion(
version.installedPath version.installedPath!
); );
if (success) { if (success) {
await window.electronAPI.config.set( await window.electronAPI.config.set(
'currentKoboldBinary', 'currentKoboldBinary',
version.installedPath version.installedPath!
); );
const newCurrentVersion = installedVersions.find( const newCurrentVersion = installedVersions.find(
@ -269,12 +259,7 @@ export const VersionsTab = () => {
setCurrentVersion(newCurrentVersion); setCurrentVersion(newCurrentVersion);
} }
} }
} catch (error) { }, 'Failed to set current version:');
window.electronAPI.logs.logError(
'Failed to set current version:',
error as Error
);
}
}; };
const isLoading = loadingInstalled || loadingPlatform || loadingRemote; const isLoading = loadingInstalled || loadingPlatform || loadingRemote;

View file

@ -9,6 +9,11 @@ export const UI = {
export const { HEADER_HEIGHT } = UI; export const { HEADER_HEIGHT } = UI;
export const SERVER_READY_SIGNALS = {
KOBOLDCPP: 'Please connect to custom endpoint at',
SILLYTAVERN: 'SillyTavern is listening on',
} as const;
export * from './defaults'; export * from './defaults';
export const GITHUB_API = { export const GITHUB_API = {

View file

@ -32,14 +32,16 @@ export class IPCHandlers {
private async launchKoboldCppWithCustomFrontends(args: string[] = []) { private async launchKoboldCppWithCustomFrontends(args: string[] = []) {
try { try {
const result = await this.koboldManager.launchKoboldCpp(args);
const frontendPreference = const frontendPreference =
await this.configManager.get('frontendPreference'); await this.configManager.get('frontendPreference');
this.koboldManager.launchKoboldCpp(args);
if (frontendPreference === 'sillytavern') { if (frontendPreference === 'sillytavern') {
this.sillyTavernManager.startFrontend(args); this.sillyTavernManager.startFrontend(args);
} }
return result;
} catch (error) { } catch (error) {
this.logManager.logError('Error in enhanced launch:', error as Error); this.logManager.logError('Error in enhanced launch:', error as Error);
return { return {
@ -50,7 +52,7 @@ export class IPCHandlers {
} }
setupHandlers() { setupHandlers() {
ipcMain.handle('kobold:downloadRelease', async (_event, asset) => { ipcMain.handle('kobold:downloadRelease', async (_, asset) => {
try { try {
const mainWindow = this.koboldManager const mainWindow = this.koboldManager
.getWindowManager() .getWindowManager()
@ -79,21 +81,19 @@ export class IPCHandlers {
this.koboldManager.getConfigFiles() this.koboldManager.getConfigFiles()
); );
ipcMain.handle( ipcMain.handle('kobold:saveConfigFile', async (_, configName, configData) =>
'kobold:saveConfigFile', this.koboldManager.saveConfigFile(configName, configData)
async (_event, configName, configData) =>
this.koboldManager.saveConfigFile(configName, configData)
); );
ipcMain.handle('kobold:getSelectedConfig', () => ipcMain.handle('kobold:getSelectedConfig', () =>
this.configManager.getSelectedConfig() this.configManager.getSelectedConfig()
); );
ipcMain.handle('kobold:setSelectedConfig', (_event, configName) => ipcMain.handle('kobold:setSelectedConfig', (_, configName) =>
this.configManager.setSelectedConfig(configName) this.configManager.setSelectedConfig(configName)
); );
ipcMain.handle('kobold:setCurrentVersion', (_event, version) => ipcMain.handle('kobold:setCurrentVersion', (_, version) =>
this.koboldManager.setCurrentVersion(version) this.koboldManager.setCurrentVersion(version)
); );
@ -152,20 +152,16 @@ export class IPCHandlers {
} }
}); });
ipcMain.handle('kobold:launchKoboldCpp', (_event, args) => ipcMain.handle('kobold:launchKoboldCpp', (_, args) =>
this.launchKoboldCppWithCustomFrontends(args) this.launchKoboldCppWithCustomFrontends(args)
); );
ipcMain.handle('kobold:stopKoboldCpp', async () => { ipcMain.handle('kobold:stopKoboldCpp', () => {
try { this.koboldManager.stopKoboldCpp();
await this.koboldManager.stopKoboldCpp(); this.sillyTavernManager.stopFrontend();
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}); });
ipcMain.handle('kobold:parseConfigFile', (_event, filePath) => ipcMain.handle('kobold:parseConfigFile', (_, filePath) =>
this.koboldManager.parseConfigFile(filePath) this.koboldManager.parseConfigFile(filePath)
); );
@ -173,15 +169,15 @@ export class IPCHandlers {
this.koboldManager.selectModelFile() this.koboldManager.selectModelFile()
); );
ipcMain.handle('config:get', (_event, key) => this.configManager.get(key)); ipcMain.handle('config:get', (_, key) => this.configManager.get(key));
ipcMain.handle('config:set', (_event, key, value) => ipcMain.handle('config:set', (_, key, value) =>
this.configManager.set(key, value) this.configManager.set(key, value)
); );
ipcMain.handle('app:getVersion', () => app.getVersion()); ipcMain.handle('app:getVersion', () => app.getVersion());
ipcMain.handle('app:openExternal', async (_event, url) => { ipcMain.handle('app:openExternal', async (_, url) => {
try { try {
await shell.openExternal(url); await shell.openExternal(url);
return { success: true }; return { success: true };
@ -190,12 +186,9 @@ export class IPCHandlers {
} }
}); });
ipcMain.handle( ipcMain.handle('logs:logError', (_, message: string, error?: Error) => {
'logs:logError', this.logManager.logError(message, error);
(_event, message: string, error?: Error) => { });
this.logManager.logError(message, error);
}
);
ipcMain.handle('sillytavern:isNpxAvailable', () => ipcMain.handle('sillytavern:isNpxAvailable', () =>
this.sillyTavernManager.isNpxAvailable() this.sillyTavernManager.isNpxAvailable()

View file

@ -21,7 +21,7 @@ import { pipeline } from 'stream/promises';
import { ConfigManager } from '@/main/managers/ConfigManager'; import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager'; import { LogManager } from '@/main/managers/LogManager';
import { WindowManager } from '@/main/managers/WindowManager'; import { WindowManager } from '@/main/managers/WindowManager';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME, SERVER_READY_SIGNALS } from '@/constants';
import { stripAssetExtensions } from '@/utils/version'; import { stripAssetExtensions } from '@/utils/version';
import type { import type {
GitHubAsset, GitHubAsset,
@ -700,14 +700,47 @@ export class KoboldCppManager {
this.windowManager.sendKoboldOutput(commandLine); this.windowManager.sendKoboldOutput(commandLine);
}, 200); }, 200);
let readyResolve:
| ((value: { success: boolean; pid?: number; error?: string }) => void)
| null = null;
let _readyReject: ((error: Error) => void) | null = null;
let isReady = false;
const readyPromise = new Promise<{
success: boolean;
pid?: number;
error?: string;
}>((resolve, reject) => {
readyResolve = resolve;
_readyReject = reject;
setTimeout(() => {
if (!isReady) {
reject(
new Error('Timeout waiting for KoboldCpp ready signal (30s)')
);
}
}, 30000);
});
child.stdout?.on('data', (data) => { child.stdout?.on('data', (data) => {
const output = data.toString(); const output = data.toString();
this.windowManager.sendKoboldOutput(output, true); this.windowManager.sendKoboldOutput(output, true);
if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
isReady = true;
readyResolve?.({ success: true, pid: child.pid });
}
}); });
child.stderr?.on('data', (data) => { child.stderr?.on('data', (data) => {
const output = data.toString(); const output = data.toString();
this.windowManager.sendKoboldOutput(output, true); this.windowManager.sendKoboldOutput(output, true);
if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
isReady = true;
readyResolve?.({ success: true, pid: child.pid });
}
}); });
child.on('exit', (code, signal) => { child.on('exit', (code, signal) => {
@ -720,6 +753,14 @@ export class KoboldCppManager {
: `\n[INFO] Process exited with code ${code}`; : `\n[INFO] Process exited with code ${code}`;
this.windowManager.sendKoboldOutput(displayMessage); this.windowManager.sendKoboldOutput(displayMessage);
this.koboldProcess = null; this.koboldProcess = null;
if (!isReady) {
_readyReject?.(
new Error(
`Process exited before ready signal (code: ${code}, signal: ${signal})`
)
);
}
}); });
child.on('error', (error) => { child.on('error', (error) => {
@ -732,9 +773,13 @@ export class KoboldCppManager {
`\n[ERROR] Process error: ${error.message}\n` `\n[ERROR] Process error: ${error.message}\n`
); );
this.koboldProcess = null; this.koboldProcess = null;
if (!isReady) {
_readyReject?.(error);
}
}); });
return { success: true, pid: child.pid }; return await readyPromise;
} catch (error) { } catch (error) {
const errorMessage = (error as Error).message; const errorMessage = (error as Error).message;
this.logManager.logError( this.logManager.logError(

View file

@ -70,8 +70,8 @@ export class LogManager {
this.logError('Uncaught Exception:', error); this.logError('Uncaught Exception:', error);
}); });
process.on('unhandledRejection', (reason, promise) => { process.on('unhandledRejection', (reason, _promise) => {
const message = `Unhandled Promise Rejection at: ${promise}`; const message = `Unhandled Promise Rejection`;
const error = const error =
reason instanceof Error ? reason : new Error(String(reason)); reason instanceof Error ? reason : new Error(String(reason));
this.logError(message, error); this.logError(message, error);

View file

@ -21,6 +21,14 @@ export class SillyTavernManager {
private proxyServer: Server | null = null; private proxyServer: Server | null = null;
private logManager: LogManager; private logManager: LogManager;
private windowManager: WindowManager; private windowManager: WindowManager;
private static readonly SILLYTAVERN_BASE_ARGS = [
'sillytavern',
'--global',
'--listen',
'--browserLaunchEnabled',
'false',
'--disableCsrf',
];
constructor(logManager: LogManager, windowManager: WindowManager) { constructor(logManager: LogManager, windowManager: WindowManager) {
this.logManager = logManager; this.logManager = logManager;
@ -109,14 +117,6 @@ export class SillyTavernManager {
return join(this.getSillyTavernDataRoot(), 'default-user', 'settings.json'); return join(this.getSillyTavernDataRoot(), 'default-user', 'settings.json');
} }
private static readonly SILLYTAVERN_BASE_ARGS = [
'sillytavern',
'--listen',
'--browserLaunchEnabled',
'false',
'--disableCsrf',
];
async isNpxAvailable(): Promise<boolean> { async isNpxAvailable(): Promise<boolean> {
try { try {
const testProcess = spawn('npx', ['--version'], { stdio: 'pipe' }); const testProcess = spawn('npx', ['--version'], { stdio: 'pipe' });
@ -146,10 +146,6 @@ export class SillyTavernManager {
return spawn('npx', args, { return spawn('npx', args, {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
detached: false, detached: false,
env: {
...process.env,
DATA_ROOT: this.getSillyTavernDataRoot(),
},
}); });
} }
@ -157,7 +153,9 @@ export class SillyTavernManager {
const settingsPath = this.getSillyTavernSettingsPath(); const settingsPath = this.getSillyTavernSettingsPath();
if (existsSync(settingsPath)) { if (existsSync(settingsPath)) {
this.windowManager.sendKoboldOutput('SillyTavern settings found'); this.windowManager.sendKoboldOutput(
`SillyTavern settings found at ${settingsPath}`
);
return; return;
} }
@ -250,9 +248,14 @@ export class SillyTavernManager {
}); });
} }
private parseKoboldConfig(args: string[]): { host: string; port: number } { private parseKoboldConfig(args: string[]): {
host: string;
port: number;
isImageMode: boolean;
} {
let host = 'localhost'; let host = 'localhost';
let port = 5001; let port = 5001;
let hasSdModel = false;
for (let i = 0; i < args.length - 1; i++) { for (let i = 0; i < args.length - 1; i++) {
if (args[i] === '--hostname' || args[i] === '--host') { if (args[i] === '--hostname' || args[i] === '--host') {
@ -262,23 +265,23 @@ export class SillyTavernManager {
if (!isNaN(parsedPort)) { if (!isNaN(parsedPort)) {
port = parsedPort; port = parsedPort;
} }
} else if (args[i] === '--sdmodel') {
hasSdModel = true;
} }
} }
return { host, port }; const isImageMode = hasSdModel;
return { host, port, isImageMode };
} }
private async setupSillyTavernConfig( private async setupSillyTavernConfig(
koboldHost: string, koboldHost: string,
koboldPort: number koboldPort: number,
isImageMode: boolean
): Promise<void> { ): Promise<void> {
try { try {
const configPath = this.getSillyTavernSettingsPath(); const configPath = this.getSillyTavernSettingsPath();
this.windowManager.sendKoboldOutput(
`Configuring SillyTavern settings at: ${configPath}`
);
let settings: Record<string, unknown> = {}; let settings: Record<string, unknown> = {};
if (existsSync(configPath)) { if (existsSync(configPath)) {
@ -299,6 +302,18 @@ export class SillyTavernManager {
if (!settings.power_user) settings.power_user = {}; if (!settings.power_user) settings.power_user = {};
const powerUser = settings.power_user as Record<string, unknown>; const powerUser = settings.power_user as Record<string, unknown>;
powerUser.auto_connect = true;
if (isImageMode) {
this.windowManager.sendKoboldOutput(
`Image generation mode detected. Please configure SillyTavern manually:\n` +
`1. Open SillyTavern and navigate to Settings (top-right gear icon)\n` +
`2. Go to 'Extensions' tab and enable 'Image Generation'\n` +
`3. In Image Generation settings, set Source to 'Stable Diffusion WebUI (AUTOMATIC1111)'\n` +
`4. Set API URL to: ${koboldUrl}\n` +
`5. Click 'Connect' to test the connection`
);
}
if (!settings.textgenerationwebui_settings) if (!settings.textgenerationwebui_settings)
settings.textgenerationwebui_settings = {}; settings.textgenerationwebui_settings = {};
@ -313,7 +328,10 @@ export class SillyTavernManager {
settings.main_api = 'textgenerationwebui'; settings.main_api = 'textgenerationwebui';
textgenSettings.type = 'koboldcpp'; textgenSettings.type = 'koboldcpp';
powerUser.auto_connect = true;
this.windowManager.sendKoboldOutput(
`Configured SillyTavern for text generation with KoboldCpp at ${koboldUrl}`
);
writeFileSync(configPath, JSON.stringify(settings, null, 2), 'utf-8'); writeFileSync(configPath, JSON.stringify(settings, null, 2), 'utf-8');
@ -407,8 +425,11 @@ export class SillyTavernManager {
port: SILLYTAVERN.PORT, port: SILLYTAVERN.PORT,
proxyPort: SILLYTAVERN.PROXY_PORT, proxyPort: SILLYTAVERN.PROXY_PORT,
}; };
const { host: koboldHost, port: koboldPort } = const {
this.parseKoboldConfig(args); host: koboldHost,
port: koboldPort,
isImageMode,
} = this.parseKoboldConfig(args);
await this.stopFrontend(); await this.stopFrontend();
@ -417,7 +438,7 @@ export class SillyTavernManager {
); );
await this.ensureSillyTavernSettings(); await this.ensureSillyTavernSettings();
await this.setupSillyTavernConfig(koboldHost, koboldPort); await this.setupSillyTavernConfig(koboldHost, koboldPort, isImageMode);
this.windowManager.sendKoboldOutput( this.windowManager.sendKoboldOutput(
`Starting ${config.name} frontend on port ${config.port}...` `Starting ${config.name} frontend on port ${config.port}...`
@ -479,19 +500,6 @@ export class SillyTavernManager {
} }
async stopFrontend(): Promise<void> { async stopFrontend(): Promise<void> {
this.windowManager.sendKoboldOutput('Stopping SillyTavern frontend...');
try {
this.windowManager.sendKoboldOutput(
`Killing processes on port ${SILLYTAVERN.PORT}...`
);
} catch (error) {
this.logManager.logError(
'Error killing processes on port:',
error as Error
);
}
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
if (this.sillyTavernProcess) { if (this.sillyTavernProcess) {
@ -501,7 +509,6 @@ export class SillyTavernManager {
this.logManager.logError(message, error), this.logManager.logError(message, error),
}).then(() => { }).then(() => {
this.sillyTavernProcess = null; this.sillyTavernProcess = null;
this.windowManager.sendKoboldOutput('SillyTavern process terminated');
}) })
); );
} }
@ -511,7 +518,6 @@ export class SillyTavernManager {
new Promise((resolve) => { new Promise((resolve) => {
this.proxyServer?.close(() => { this.proxyServer?.close(() => {
this.proxyServer = null; this.proxyServer = null;
this.windowManager.sendKoboldOutput('Proxy server closed');
resolve(); resolve();
}); });
}) })
@ -519,7 +525,6 @@ export class SillyTavernManager {
} }
await Promise.all(promises); await Promise.all(promises);
this.windowManager.sendKoboldOutput('SillyTavern frontend stopped');
} }
async cleanup(): Promise<void> { async cleanup(): Promise<void> {

View file

@ -149,7 +149,7 @@ export class WindowManager {
private setupContextMenu() { private setupContextMenu() {
if (!this.mainWindow) return; if (!this.mainWindow) return;
this.mainWindow.webContents.on('context-menu', (_event, params) => { this.mainWindow.webContents.on('context-menu', (_, params) => {
const hasLinkURL = !!params.linkURL; const hasLinkURL = !!params.linkURL;
const isDev = this.isDevelopment(); const isDev = this.isDevelopment();

View file

@ -10,6 +10,7 @@ import type {
HardwareInfo, HardwareInfo,
GPUMemoryInfo, GPUMemoryInfo,
} from '@/types/hardware'; } from '@/types/hardware';
import { spawn } from 'child_process';
export class HardwareService { export class HardwareService {
private cpuCapabilitiesCache: CPUCapabilities | null = null; private cpuCapabilitiesCache: CPUCapabilities | null = null;
@ -147,7 +148,6 @@ export class HardwareService {
devices: string[]; devices: string[];
}> { }> {
try { try {
const { spawn } = await import('child_process');
const nvidia = spawn( const nvidia = spawn(
'nvidia-smi', 'nvidia-smi',
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'], ['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
@ -200,7 +200,6 @@ export class HardwareService {
devices: string[]; devices: string[];
}> { }> {
try { try {
const { spawn } = await import('child_process');
const rocminfo = spawn('rocminfo', [], { timeout: 5000 }); const rocminfo = spawn('rocminfo', [], { timeout: 5000 });
let output = ''; let output = '';
@ -278,7 +277,6 @@ export class HardwareService {
devices: string[]; devices: string[];
}> { }> {
try { try {
const { spawn } = await import('child_process');
const vulkaninfo = spawn('vulkaninfo', ['--summary'], { timeout: 5000 }); const vulkaninfo = spawn('vulkaninfo', ['--summary'], { timeout: 5000 });
let output = ''; let output = '';
@ -390,7 +388,6 @@ export class HardwareService {
devices: string[]; devices: string[];
}> { }> {
try { try {
const { spawn } = await import('child_process');
const clinfo = spawn('clinfo', [], { timeout: 3000 }); const clinfo = spawn('clinfo', [], { timeout: 3000 });
let output = ''; let output = '';

56
src/utils/logger.ts Normal file
View file

@ -0,0 +1,56 @@
export class Logger {
private static logError(message: string, error: Error): void {
if (window.electronAPI?.logs?.logError) {
window.electronAPI.logs.logError(message, error);
}
}
static async safeExecute<T>(
operation: () => Promise<T>,
errorMessage: string
): Promise<T | null> {
try {
return await operation();
} catch (error) {
this.logError(errorMessage, error as Error);
return null;
}
}
static safeExecuteSync<T>(
operation: () => T,
errorMessage: string
): T | null {
try {
return operation();
} catch (error) {
this.logError(errorMessage, error as Error);
return null;
}
}
static async tryExecute(
operation: () => Promise<void>,
errorMessage: string
): Promise<boolean> {
try {
await operation();
return true;
} catch (error) {
this.logError(errorMessage, error as Error);
return false;
}
}
static withErrorHandling<TArgs extends unknown[], TReturn>(
fn: (...args: TArgs) => Promise<TReturn>,
errorMessage: string
): (...args: TArgs) => Promise<TReturn | null> {
return async (...args: TArgs) =>
this.safeExecute(() => fn(...args), errorMessage);
}
static error(message: string, error: Error): void {
this.logError(message, error);
}
}

View file

@ -1,3 +1,4 @@
import { spawn } from 'child_process';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
export interface ProcessTerminationOptions { export interface ProcessTerminationOptions {
@ -5,6 +6,32 @@ export interface ProcessTerminationOptions {
logError?: (message: string, error: Error) => void; logError?: (message: string, error: Error) => void;
} }
async function killWindowsProcessTree(
pid: number,
logError?: (message: string, error: Error) => void
): Promise<void> {
return new Promise((resolve) => {
const taskkill = spawn('taskkill', ['/pid', pid.toString(), '/t', '/f'], {
stdio: 'pipe',
});
taskkill.on('exit', (code) => {
if (code !== 0 && code !== 128) {
logError?.(
`taskkill exited with code ${code} for PID ${pid}`,
new Error(`taskkill failed with exit code ${code}`)
);
}
resolve();
});
taskkill.on('error', (error) => {
logError?.(`Failed to execute taskkill for PID ${pid}:`, error);
resolve();
});
});
}
export async function terminateProcess( export async function terminateProcess(
childProcess: ChildProcess, childProcess: ChildProcess,
options: ProcessTerminationOptions = {} options: ProcessTerminationOptions = {}
@ -16,26 +43,43 @@ export async function terminateProcess(
} }
try { try {
const signal = process.platform === 'win32' ? undefined : 'SIGTERM'; if (process.platform === 'win32') {
childProcess.kill(signal); await killWindowsProcessTree(childProcess.pid, logError);
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
const timeout = setTimeout(() => { const timeout = setTimeout(
if (childProcess && !childProcess.killed) { () => {
try { resolve();
childProcess.kill('SIGKILL'); },
} catch (error) { Math.min(timeoutMs, 2000)
logError?.('Error force-killing process:', error as Error); );
}
}
resolve();
}, timeoutMs);
childProcess.once('exit', () => { childProcess.once('exit', () => {
clearTimeout(timeout); clearTimeout(timeout);
resolve(); resolve();
});
}); });
}); } else {
childProcess.kill('SIGTERM');
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
if (childProcess && !childProcess.killed) {
try {
childProcess.kill('SIGKILL');
} catch (error) {
logError?.('Error force-killing process:', error as Error);
}
}
resolve();
}, timeoutMs);
childProcess.once('exit', () => {
clearTimeout(timeout);
resolve();
});
});
}
} catch (error) { } catch (error) {
logError?.('Error terminating process:', error as Error); logError?.('Error terminating process:', error as Error);
} }