mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
less verbone frontend error handling, better process killing for windows, better loading behaviour for available backends
This commit is contained in:
parent
ae8ec47f51
commit
66a710dc95
18 changed files with 376 additions and 320 deletions
64
src/App.tsx
64
src/App.tsx
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
56
src/utils/logger.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue