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 { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
import { UI } from '@/constants';
|
||||
import { Logger } from '@/utils/logger';
|
||||
import type { DownloadItem } from '@/types/electron';
|
||||
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ export const App = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const checkInstallation = async () => {
|
||||
try {
|
||||
await Logger.safeExecute(async () => {
|
||||
const [versions, currentBinaryPath, hasSeenWelcome, preference] =
|
||||
await Promise.all([
|
||||
window.electronAPI.kobold.getInstalledVersions(),
|
||||
|
|
@ -81,20 +82,14 @@ export const App = () => {
|
|||
setCurrentScreenWithTransition('download');
|
||||
}
|
||||
|
||||
setHasInitialized(true);
|
||||
|
||||
if (versions.length > 0) {
|
||||
setTimeout(() => {
|
||||
checkForUpdates();
|
||||
}, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Error checking installation:',
|
||||
error as Error
|
||||
);
|
||||
setHasInitialized(true);
|
||||
}
|
||||
}, 'Error checking installation:');
|
||||
|
||||
setHasInitialized(true);
|
||||
};
|
||||
|
||||
checkInstallation();
|
||||
|
|
@ -102,7 +97,7 @@ export const App = () => {
|
|||
}, []);
|
||||
|
||||
const handleBinaryUpdate = async (download: DownloadItem) => {
|
||||
try {
|
||||
await Logger.safeExecute(async () => {
|
||||
const success = await sharedHandleDownload({
|
||||
type: 'asset',
|
||||
item: download,
|
||||
|
|
@ -113,16 +108,11 @@ export const App = () => {
|
|||
if (success) {
|
||||
dismissUpdate();
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to update binary:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}, 'Failed to update binary:');
|
||||
};
|
||||
|
||||
const handleDownloadComplete = async () => {
|
||||
try {
|
||||
await Logger.safeExecute(async () => {
|
||||
const [versions, currentBinaryPath] = await Promise.all([
|
||||
window.electronAPI.kobold.getInstalledVersions(),
|
||||
window.electronAPI.config.get('currentKoboldBinary') as Promise<string>,
|
||||
|
|
@ -143,12 +133,7 @@ export const App = () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Error refreshing versions after download:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}, 'Error refreshing versions after download:');
|
||||
|
||||
setTimeout(() => {
|
||||
setCurrentScreenWithTransition('launch');
|
||||
|
|
@ -176,16 +161,8 @@ export const App = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const performEject = async () => {
|
||||
try {
|
||||
await window.electronAPI.kobold.stopKoboldCpp();
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Error stopping KoboldCpp:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
|
||||
const performEject = () => {
|
||||
window.electronAPI.kobold.stopKoboldCpp();
|
||||
handleBackToLaunch();
|
||||
};
|
||||
|
||||
|
|
@ -302,17 +279,14 @@ export const App = () => {
|
|||
opened={settingsOpened}
|
||||
onClose={async () => {
|
||||
setSettingsOpened(false);
|
||||
try {
|
||||
const preference = (await window.electronAPI.config.get(
|
||||
'frontendPreference'
|
||||
)) as FrontendPreference;
|
||||
setFrontendPreference(preference || 'koboldcpp');
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to load frontend preference:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
const preference = await Logger.safeExecute(
|
||||
() =>
|
||||
window.electronAPI.config.get(
|
||||
'frontendPreference'
|
||||
) as Promise<FrontendPreference>,
|
||||
'Failed to load frontend preference:'
|
||||
);
|
||||
setFrontendPreference(preference || 'koboldcpp');
|
||||
}}
|
||||
currentScreen={currentScreen || undefined}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { useState } from 'react';
|
|||
import type { InstalledVersion, DownloadItem } from '@/types/electron';
|
||||
import { getDisplayNameFromPath } from '@/utils/version';
|
||||
import { GITHUB_API } from '@/constants';
|
||||
import { Logger } from '@/utils/logger';
|
||||
|
||||
interface UpdateAvailableModalProps {
|
||||
opened: boolean;
|
||||
|
|
@ -37,15 +38,12 @@ export const UpdateAvailableModal = ({
|
|||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
setIsUpdating(true);
|
||||
await Logger.safeExecute(async () => {
|
||||
await onUpdate(availableUpdate);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError('Failed to update:', error as Error);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, 'Failed to update:');
|
||||
setIsUpdating(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { getPlatformDisplayName } from '@/utils/platform';
|
|||
import { formatDownloadSize } from '@/utils/download';
|
||||
import { getAssetDescription, sortDownloadsByType } from '@/utils/assets';
|
||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
import { Logger } from '@/utils/logger';
|
||||
import type { DownloadItem } from '@/types/electron';
|
||||
|
||||
interface DownloadScreenProps {
|
||||
|
|
@ -33,7 +34,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
async (download: DownloadItem) => {
|
||||
setDownloadingAsset(download.name);
|
||||
|
||||
try {
|
||||
await Logger.safeExecute(async () => {
|
||||
const success = await sharedHandleDownload({
|
||||
type: 'asset',
|
||||
item: download,
|
||||
|
|
@ -48,14 +49,9 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
setDownloadingAsset(null);
|
||||
}, 200);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
`Failed to download ${download.name}:`,
|
||||
error as Error
|
||||
);
|
||||
} finally {
|
||||
setDownloadingAsset(null);
|
||||
}
|
||||
}, `Failed to download ${download.name}:`);
|
||||
|
||||
setDownloadingAsset(null);
|
||||
},
|
||||
[sharedHandleDownload, onDownloadComplete]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
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 { useLaunchConfigStore } from '@/stores/launchConfigStore';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
|
|
@ -69,14 +69,14 @@ export const TerminalTab = ({
|
|||
const serverPort = port || 5001;
|
||||
|
||||
if (frontendPreference === 'sillytavern') {
|
||||
if (newData.includes('SillyTavern is listening on')) {
|
||||
if (newData.includes(SERVER_READY_SIGNALS.SILLYTAVERN)) {
|
||||
setTimeout(
|
||||
() => onServerReady(`http://${serverHost}:${serverPort}`),
|
||||
1500
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (newData.includes('Please connect to custom endpoint at')) {
|
||||
if (newData.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
|
||||
setTimeout(
|
||||
() => onServerReady(`http://${serverHost}:${serverPort}`),
|
||||
1500
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useState, useEffect } from 'react';
|
|||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { Logger } from '@/utils/logger';
|
||||
|
||||
export const AdvancedTab = () => {
|
||||
const {
|
||||
|
|
@ -34,25 +35,21 @@ export const AdvancedTab = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const detectBackendSupport = async () => {
|
||||
try {
|
||||
const support = await window.electronAPI.kobold.detectBackendSupport();
|
||||
if (support) {
|
||||
setBackendSupport({
|
||||
noavx2: support.noavx2,
|
||||
failsafe: support.failsafe,
|
||||
});
|
||||
} else {
|
||||
setBackendSupport({ noavx2: false, failsafe: false });
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to detect backend support:',
|
||||
error as Error
|
||||
);
|
||||
const support = await Logger.safeExecute(
|
||||
() => window.electronAPI.kobold.detectBackendSupport(),
|
||||
'Failed to detect backend support:'
|
||||
);
|
||||
|
||||
if (support) {
|
||||
setBackendSupport({
|
||||
noavx2: support.noavx2,
|
||||
failsafe: support.failsafe,
|
||||
});
|
||||
} else {
|
||||
setBackendSupport({ noavx2: false, failsafe: false });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
void detectBackendSupport();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { InfoTooltip } from '@/components/InfoTooltip';
|
|||
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
|
||||
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { Logger } from '@/utils/logger';
|
||||
import type { BackendOption } from '@/types';
|
||||
|
||||
export const BackendSelector = () => {
|
||||
|
|
@ -19,22 +20,19 @@ export const BackendSelector = () => {
|
|||
const [availableBackends, setAvailableBackends] = useState<BackendOption[]>(
|
||||
[]
|
||||
);
|
||||
const [isLoadingBackends, setIsLoadingBackends] = useState(false);
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadBackends = async () => {
|
||||
try {
|
||||
const backends =
|
||||
await window.electronAPI.kobold.getAvailableBackends(true);
|
||||
setAvailableBackends(backends);
|
||||
hasInitialized.current = true;
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to detect available backends:',
|
||||
error as Error
|
||||
);
|
||||
setAvailableBackends([]);
|
||||
}
|
||||
setIsLoadingBackends(true);
|
||||
const backends = await Logger.safeExecute(
|
||||
() => window.electronAPI.kobold.getAvailableBackends(true),
|
||||
'Failed to detect available backends:'
|
||||
);
|
||||
setAvailableBackends(backends || []);
|
||||
setIsLoadingBackends(false);
|
||||
hasInitialized.current = true;
|
||||
};
|
||||
|
||||
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." />
|
||||
</Group>
|
||||
<Select
|
||||
placeholder="Select backend"
|
||||
placeholder={
|
||||
isLoadingBackends ? 'Loading backends...' : 'Select backend'
|
||||
}
|
||||
value={backend}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
|
|
@ -72,7 +72,7 @@ export const BackendSelector = () => {
|
|||
label: b.label,
|
||||
disabled: b.disabled,
|
||||
}))}
|
||||
disabled={availableBackends.length === 0}
|
||||
disabled={isLoadingBackends || availableBackends.length === 0}
|
||||
renderOption={({ option }) => {
|
||||
const backendData = availableBackends.find(
|
||||
(b) => b.value === option.value
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationT
|
|||
import { WarningDisplay } from '@/components/WarningDisplay';
|
||||
import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager';
|
||||
import { DEFAULT_MODEL_URL } from '@/constants';
|
||||
import { Logger } from '@/utils/logger';
|
||||
import type { ConfigFile } from '@/types';
|
||||
|
||||
interface LaunchScreenProps {
|
||||
|
|
@ -83,23 +84,19 @@ export const LaunchScreen = ({
|
|||
});
|
||||
|
||||
const setHappyDefaults = useCallback(async () => {
|
||||
try {
|
||||
const backends = await window.electronAPI.kobold.getAvailableBackends();
|
||||
const backends = await Logger.safeExecute(
|
||||
() => window.electronAPI.kobold.getAvailableBackends(),
|
||||
'Failed to set defaults:'
|
||||
);
|
||||
|
||||
if (!backend && backends.length > 0) {
|
||||
handleBackendChange(backends[0].value);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to set defaults:',
|
||||
error as Error
|
||||
);
|
||||
if (!backend && backends && backends.length > 0) {
|
||||
handleBackendChange(backends[0].value);
|
||||
}
|
||||
}, [backend, handleBackendChange]);
|
||||
|
||||
const setInitialDefaults = useCallback(
|
||||
async (currentModelPath: string, currentSdModel: string) => {
|
||||
try {
|
||||
await Logger.tryExecute(async () => {
|
||||
if (
|
||||
!defaultsSetRef.current &&
|
||||
!currentModelPath.trim() &&
|
||||
|
|
@ -108,12 +105,7 @@ export const LaunchScreen = ({
|
|||
handleModelPathChange(DEFAULT_MODEL_URL);
|
||||
defaultsSetRef.current = true;
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to set initial defaults:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}, 'Failed to set initial defaults:');
|
||||
},
|
||||
[handleModelPathChange]
|
||||
);
|
||||
|
|
@ -194,49 +186,43 @@ export const LaunchScreen = ({
|
|||
});
|
||||
|
||||
const handleCreateNewConfig = async (configName: string) => {
|
||||
try {
|
||||
await Logger.safeExecute(async () => {
|
||||
const fullConfigName = `${configName}.json`;
|
||||
const success = await window.electronAPI.kobold.saveConfigFile(
|
||||
const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
|
||||
fullConfigName,
|
||||
buildConfigData()
|
||||
);
|
||||
|
||||
if (success) {
|
||||
if (saveSuccess) {
|
||||
await loadConfigFiles();
|
||||
|
||||
setSelectedFile(fullConfigName);
|
||||
await window.electronAPI.kobold.setSelectedConfig(fullConfigName);
|
||||
} else {
|
||||
window.electronAPI.logs.logError(
|
||||
Logger.error(
|
||||
'Failed to create new configuration',
|
||||
new Error('Save operation failed')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to create new configuration:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}, 'Failed to create new configuration:');
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!selectedFile) {
|
||||
window.electronAPI.logs.logError(
|
||||
Logger.error(
|
||||
'No configuration file selected for saving',
|
||||
new Error('Selected file is null')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await window.electronAPI.kobold.saveConfigFile(
|
||||
const success = await Logger.safeExecute(async () => {
|
||||
const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
|
||||
selectedFile,
|
||||
buildConfigData()
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
window.electronAPI.logs.logError(
|
||||
if (!saveSuccess) {
|
||||
Logger.error(
|
||||
'Failed to save configuration',
|
||||
new Error('Save operation failed')
|
||||
);
|
||||
|
|
@ -244,13 +230,9 @@ export const LaunchScreen = ({
|
|||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to save configuration:',
|
||||
error as Error
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}, 'Failed to save configuration:');
|
||||
|
||||
return success ?? false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Folder, FolderOpen, Monitor } from 'lucide-react';
|
|||
import styles from '@/styles/layout.module.css';
|
||||
import type { FrontendPreference } from '@/types';
|
||||
import { FRONTENDS } from '@/constants';
|
||||
import { Logger } from '@/utils/logger';
|
||||
|
||||
export const GeneralTab = () => {
|
||||
const [installDir, setInstallDir] = useState<string>('');
|
||||
|
|
@ -29,31 +30,21 @@ export const GeneralTab = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const checkNpxAvailability = async () => {
|
||||
try {
|
||||
const available = await window.electronAPI.sillytavern.isNpxAvailable();
|
||||
setIsNpxAvailable(available);
|
||||
const available = await Logger.safeExecute(
|
||||
() => window.electronAPI.sillytavern.isNpxAvailable(),
|
||||
'Failed to check npx availability:'
|
||||
);
|
||||
|
||||
if (!available && FrontendPreference === 'sillytavern') {
|
||||
await window.electronAPI.config.set(
|
||||
'frontendPreference',
|
||||
'koboldcpp'
|
||||
);
|
||||
setFrontendPreference('koboldcpp');
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to check npx availability:',
|
||||
error as Error
|
||||
const isAvailable = available ?? false;
|
||||
setIsNpxAvailable(isAvailable);
|
||||
|
||||
if (!isAvailable && FrontendPreference === 'sillytavern') {
|
||||
await Logger.tryExecute(
|
||||
() =>
|
||||
window.electronAPI.config.set('frontendPreference', 'koboldcpp'),
|
||||
'Failed to reset frontend preference:'
|
||||
);
|
||||
setIsNpxAvailable(false);
|
||||
|
||||
if (FrontendPreference === 'sillytavern') {
|
||||
await window.electronAPI.config.set(
|
||||
'frontendPreference',
|
||||
'koboldcpp'
|
||||
);
|
||||
setFrontendPreference('koboldcpp');
|
||||
}
|
||||
setFrontendPreference('koboldcpp');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -63,58 +54,46 @@ export const GeneralTab = () => {
|
|||
}, [FrontendPreference]);
|
||||
|
||||
const loadCurrentInstallDir = async () => {
|
||||
try {
|
||||
const currentDir = await window.electronAPI.kobold.getCurrentInstallDir();
|
||||
const currentDir = await Logger.safeExecute(
|
||||
() => window.electronAPI.kobold.getCurrentInstallDir(),
|
||||
'Failed to load install directory:'
|
||||
);
|
||||
if (currentDir) {
|
||||
setInstallDir(currentDir);
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to load install directory:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const loadFrontendPreference = async () => {
|
||||
try {
|
||||
const frontendPreference = (await window.electronAPI.config.get(
|
||||
'frontendPreference'
|
||||
)) as FrontendPreference;
|
||||
setFrontendPreference(frontendPreference || 'koboldcpp');
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to load frontend preference:',
|
||||
error as Error
|
||||
const frontendPreference = await Logger.safeExecute(
|
||||
() => window.electronAPI.config.get('frontendPreference'),
|
||||
'Failed to load frontend preference:'
|
||||
);
|
||||
if (frontendPreference) {
|
||||
setFrontendPreference(
|
||||
(frontendPreference as FrontendPreference) || 'koboldcpp'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectInstallDir = async () => {
|
||||
try {
|
||||
const selectedDir =
|
||||
await window.electronAPI.kobold.selectInstallDirectory();
|
||||
|
||||
if (selectedDir) {
|
||||
setInstallDir(selectedDir);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to select install directory:',
|
||||
error as Error
|
||||
);
|
||||
const selectedDir = await Logger.safeExecute(
|
||||
() => window.electronAPI.kobold.selectInstallDirectory(),
|
||||
'Failed to select install directory:'
|
||||
);
|
||||
if (selectedDir) {
|
||||
setInstallDir(selectedDir);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFrontendPreferenceChange = async (value: string | null) => {
|
||||
if (!value || (value !== 'koboldcpp' && value !== 'sillytavern')) return;
|
||||
|
||||
try {
|
||||
await window.electronAPI.config.set('frontendPreference', value);
|
||||
const success = await Logger.tryExecute(
|
||||
() => window.electronAPI.config.set('frontendPreference', value),
|
||||
'Failed to save frontend preference:'
|
||||
);
|
||||
if (success) {
|
||||
setFrontendPreference(value);
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to save frontend preference:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
stripAssetExtensions,
|
||||
compareVersions,
|
||||
} from '@/utils/version';
|
||||
import { Logger } from '@/utils/logger';
|
||||
import { formatDownloadSize } from '@/utils/download';
|
||||
|
||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
|
|
@ -62,7 +63,7 @@ export const VersionsTab = () => {
|
|||
const loadInstalledVersions = useCallback(async () => {
|
||||
setLoadingInstalled(true);
|
||||
|
||||
try {
|
||||
await Logger.safeExecute(async () => {
|
||||
const [versions, currentBinaryPath] = await Promise.all([
|
||||
window.electronAPI.kobold.getInstalledVersions(),
|
||||
window.electronAPI.config.get('currentKoboldBinary') as Promise<string>,
|
||||
|
|
@ -95,25 +96,18 @@ export const VersionsTab = () => {
|
|||
await window.electronAPI.config.set('currentKoboldBinary', '');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to load installed versions:',
|
||||
error as Error
|
||||
);
|
||||
} finally {
|
||||
setLoadingInstalled(false);
|
||||
}
|
||||
}, 'Failed to load installed versions:');
|
||||
|
||||
setLoadingInstalled(false);
|
||||
}, []);
|
||||
|
||||
const loadLatestRelease = useCallback(async () => {
|
||||
try {
|
||||
const release = await getLatestReleaseWithDownloadStatus();
|
||||
const release = await Logger.safeExecute(
|
||||
() => getLatestReleaseWithDownloadStatus(),
|
||||
'Failed to load latest release:'
|
||||
);
|
||||
if (release) {
|
||||
setLatestRelease(release);
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to load latest release:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}, [getLatestReleaseWithDownloadStatus]);
|
||||
|
||||
|
|
@ -205,7 +199,7 @@ export const VersionsTab = () => {
|
|||
}, [downloading]);
|
||||
|
||||
const handleDownload = async (version: VersionInfo) => {
|
||||
try {
|
||||
await Logger.safeExecute(async () => {
|
||||
const download = availableDownloads.find((d) => d.name === version.name);
|
||||
if (!download) {
|
||||
throw new Error('Download not found');
|
||||
|
|
@ -221,13 +215,11 @@ export const VersionsTab = () => {
|
|||
if (success) {
|
||||
await loadInstalledVersions();
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError('Failed to download:', error as Error);
|
||||
}
|
||||
}, 'Failed to download:');
|
||||
};
|
||||
|
||||
const handleUpdate = async (version: VersionInfo) => {
|
||||
try {
|
||||
await Logger.safeExecute(async () => {
|
||||
const download = availableDownloads.find((d) => d.name === version.name);
|
||||
if (!download) {
|
||||
throw new Error('Download not found');
|
||||
|
|
@ -243,23 +235,21 @@ export const VersionsTab = () => {
|
|||
if (success) {
|
||||
await loadInstalledVersions();
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError('Failed to update:', error as Error);
|
||||
}
|
||||
}, 'Failed to update:');
|
||||
};
|
||||
|
||||
const makeCurrent = async (version: VersionInfo) => {
|
||||
if (!version.installedPath) return;
|
||||
|
||||
try {
|
||||
await Logger.safeExecute(async () => {
|
||||
const success = await window.electronAPI.kobold.setCurrentVersion(
|
||||
version.installedPath
|
||||
version.installedPath!
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await window.electronAPI.config.set(
|
||||
'currentKoboldBinary',
|
||||
version.installedPath
|
||||
version.installedPath!
|
||||
);
|
||||
|
||||
const newCurrentVersion = installedVersions.find(
|
||||
|
|
@ -269,12 +259,7 @@ export const VersionsTab = () => {
|
|||
setCurrentVersion(newCurrentVersion);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to set current version:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}, 'Failed to set current version:');
|
||||
};
|
||||
|
||||
const isLoading = loadingInstalled || loadingPlatform || loadingRemote;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ export const 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 const GITHUB_API = {
|
||||
|
|
|
|||
|
|
@ -32,14 +32,16 @@ export class IPCHandlers {
|
|||
|
||||
private async launchKoboldCppWithCustomFrontends(args: string[] = []) {
|
||||
try {
|
||||
const result = await this.koboldManager.launchKoboldCpp(args);
|
||||
|
||||
const frontendPreference =
|
||||
await this.configManager.get('frontendPreference');
|
||||
|
||||
this.koboldManager.launchKoboldCpp(args);
|
||||
|
||||
if (frontendPreference === 'sillytavern') {
|
||||
this.sillyTavernManager.startFrontend(args);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logManager.logError('Error in enhanced launch:', error as Error);
|
||||
return {
|
||||
|
|
@ -50,7 +52,7 @@ export class IPCHandlers {
|
|||
}
|
||||
|
||||
setupHandlers() {
|
||||
ipcMain.handle('kobold:downloadRelease', async (_event, asset) => {
|
||||
ipcMain.handle('kobold:downloadRelease', async (_, asset) => {
|
||||
try {
|
||||
const mainWindow = this.koboldManager
|
||||
.getWindowManager()
|
||||
|
|
@ -79,21 +81,19 @@ export class IPCHandlers {
|
|||
this.koboldManager.getConfigFiles()
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
'kobold:saveConfigFile',
|
||||
async (_event, configName, configData) =>
|
||||
this.koboldManager.saveConfigFile(configName, configData)
|
||||
ipcMain.handle('kobold:saveConfigFile', async (_, configName, configData) =>
|
||||
this.koboldManager.saveConfigFile(configName, configData)
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:getSelectedConfig', () =>
|
||||
this.configManager.getSelectedConfig()
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:setSelectedConfig', (_event, configName) =>
|
||||
ipcMain.handle('kobold:setSelectedConfig', (_, configName) =>
|
||||
this.configManager.setSelectedConfig(configName)
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:setCurrentVersion', (_event, version) =>
|
||||
ipcMain.handle('kobold: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)
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:stopKoboldCpp', async () => {
|
||||
try {
|
||||
await this.koboldManager.stopKoboldCpp();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
ipcMain.handle('kobold:stopKoboldCpp', () => {
|
||||
this.koboldManager.stopKoboldCpp();
|
||||
this.sillyTavernManager.stopFrontend();
|
||||
});
|
||||
|
||||
ipcMain.handle('kobold:parseConfigFile', (_event, filePath) =>
|
||||
ipcMain.handle('kobold:parseConfigFile', (_, filePath) =>
|
||||
this.koboldManager.parseConfigFile(filePath)
|
||||
);
|
||||
|
||||
|
|
@ -173,15 +169,15 @@ export class IPCHandlers {
|
|||
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)
|
||||
);
|
||||
|
||||
ipcMain.handle('app:getVersion', () => app.getVersion());
|
||||
|
||||
ipcMain.handle('app:openExternal', async (_event, url) => {
|
||||
ipcMain.handle('app:openExternal', async (_, url) => {
|
||||
try {
|
||||
await shell.openExternal(url);
|
||||
return { success: true };
|
||||
|
|
@ -190,12 +186,9 @@ export class IPCHandlers {
|
|||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
'logs:logError',
|
||||
(_event, message: string, error?: Error) => {
|
||||
this.logManager.logError(message, error);
|
||||
}
|
||||
);
|
||||
ipcMain.handle('logs:logError', (_, message: string, error?: Error) => {
|
||||
this.logManager.logError(message, error);
|
||||
});
|
||||
|
||||
ipcMain.handle('sillytavern:isNpxAvailable', () =>
|
||||
this.sillyTavernManager.isNpxAvailable()
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { pipeline } from 'stream/promises';
|
|||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import { LogManager } from '@/main/managers/LogManager';
|
||||
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 type {
|
||||
GitHubAsset,
|
||||
|
|
@ -700,14 +700,47 @@ export class KoboldCppManager {
|
|||
this.windowManager.sendKoboldOutput(commandLine);
|
||||
}, 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) => {
|
||||
const output = data.toString();
|
||||
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) => {
|
||||
const output = data.toString();
|
||||
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) => {
|
||||
|
|
@ -720,6 +753,14 @@ export class KoboldCppManager {
|
|||
: `\n[INFO] Process exited with code ${code}`;
|
||||
this.windowManager.sendKoboldOutput(displayMessage);
|
||||
this.koboldProcess = null;
|
||||
|
||||
if (!isReady) {
|
||||
_readyReject?.(
|
||||
new Error(
|
||||
`Process exited before ready signal (code: ${code}, signal: ${signal})`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
|
|
@ -732,9 +773,13 @@ export class KoboldCppManager {
|
|||
`\n[ERROR] Process error: ${error.message}\n`
|
||||
);
|
||||
this.koboldProcess = null;
|
||||
|
||||
if (!isReady) {
|
||||
_readyReject?.(error);
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, pid: child.pid };
|
||||
return await readyPromise;
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message;
|
||||
this.logManager.logError(
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ export class LogManager {
|
|||
this.logError('Uncaught Exception:', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
const message = `Unhandled Promise Rejection at: ${promise}`;
|
||||
process.on('unhandledRejection', (reason, _promise) => {
|
||||
const message = `Unhandled Promise Rejection`;
|
||||
const error =
|
||||
reason instanceof Error ? reason : new Error(String(reason));
|
||||
this.logError(message, error);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,14 @@ export class SillyTavernManager {
|
|||
private proxyServer: Server | null = null;
|
||||
private logManager: LogManager;
|
||||
private windowManager: WindowManager;
|
||||
private static readonly SILLYTAVERN_BASE_ARGS = [
|
||||
'sillytavern',
|
||||
'--global',
|
||||
'--listen',
|
||||
'--browserLaunchEnabled',
|
||||
'false',
|
||||
'--disableCsrf',
|
||||
];
|
||||
|
||||
constructor(logManager: LogManager, windowManager: WindowManager) {
|
||||
this.logManager = logManager;
|
||||
|
|
@ -109,14 +117,6 @@ export class SillyTavernManager {
|
|||
return join(this.getSillyTavernDataRoot(), 'default-user', 'settings.json');
|
||||
}
|
||||
|
||||
private static readonly SILLYTAVERN_BASE_ARGS = [
|
||||
'sillytavern',
|
||||
'--listen',
|
||||
'--browserLaunchEnabled',
|
||||
'false',
|
||||
'--disableCsrf',
|
||||
];
|
||||
|
||||
async isNpxAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const testProcess = spawn('npx', ['--version'], { stdio: 'pipe' });
|
||||
|
|
@ -146,10 +146,6 @@ export class SillyTavernManager {
|
|||
return spawn('npx', args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
env: {
|
||||
...process.env,
|
||||
DATA_ROOT: this.getSillyTavernDataRoot(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +153,9 @@ export class SillyTavernManager {
|
|||
const settingsPath = this.getSillyTavernSettingsPath();
|
||||
|
||||
if (existsSync(settingsPath)) {
|
||||
this.windowManager.sendKoboldOutput('SillyTavern settings found');
|
||||
this.windowManager.sendKoboldOutput(
|
||||
`SillyTavern settings found at ${settingsPath}`
|
||||
);
|
||||
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 port = 5001;
|
||||
let hasSdModel = false;
|
||||
|
||||
for (let i = 0; i < args.length - 1; i++) {
|
||||
if (args[i] === '--hostname' || args[i] === '--host') {
|
||||
|
|
@ -262,23 +265,23 @@ export class SillyTavernManager {
|
|||
if (!isNaN(parsedPort)) {
|
||||
port = parsedPort;
|
||||
}
|
||||
} else if (args[i] === '--sdmodel') {
|
||||
hasSdModel = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { host, port };
|
||||
const isImageMode = hasSdModel;
|
||||
|
||||
return { host, port, isImageMode };
|
||||
}
|
||||
|
||||
private async setupSillyTavernConfig(
|
||||
koboldHost: string,
|
||||
koboldPort: number
|
||||
koboldPort: number,
|
||||
isImageMode: boolean
|
||||
): Promise<void> {
|
||||
try {
|
||||
const configPath = this.getSillyTavernSettingsPath();
|
||||
|
||||
this.windowManager.sendKoboldOutput(
|
||||
`Configuring SillyTavern settings at: ${configPath}`
|
||||
);
|
||||
|
||||
let settings: Record<string, unknown> = {};
|
||||
|
||||
if (existsSync(configPath)) {
|
||||
|
|
@ -299,6 +302,18 @@ export class SillyTavernManager {
|
|||
|
||||
if (!settings.power_user) settings.power_user = {};
|
||||
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)
|
||||
settings.textgenerationwebui_settings = {};
|
||||
|
|
@ -313,7 +328,10 @@ export class SillyTavernManager {
|
|||
|
||||
settings.main_api = 'textgenerationwebui';
|
||||
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');
|
||||
|
||||
|
|
@ -407,8 +425,11 @@ export class SillyTavernManager {
|
|||
port: SILLYTAVERN.PORT,
|
||||
proxyPort: SILLYTAVERN.PROXY_PORT,
|
||||
};
|
||||
const { host: koboldHost, port: koboldPort } =
|
||||
this.parseKoboldConfig(args);
|
||||
const {
|
||||
host: koboldHost,
|
||||
port: koboldPort,
|
||||
isImageMode,
|
||||
} = this.parseKoboldConfig(args);
|
||||
|
||||
await this.stopFrontend();
|
||||
|
||||
|
|
@ -417,7 +438,7 @@ export class SillyTavernManager {
|
|||
);
|
||||
|
||||
await this.ensureSillyTavernSettings();
|
||||
await this.setupSillyTavernConfig(koboldHost, koboldPort);
|
||||
await this.setupSillyTavernConfig(koboldHost, koboldPort, isImageMode);
|
||||
|
||||
this.windowManager.sendKoboldOutput(
|
||||
`Starting ${config.name} frontend on port ${config.port}...`
|
||||
|
|
@ -479,19 +500,6 @@ export class SillyTavernManager {
|
|||
}
|
||||
|
||||
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>[] = [];
|
||||
|
||||
if (this.sillyTavernProcess) {
|
||||
|
|
@ -501,7 +509,6 @@ export class SillyTavernManager {
|
|||
this.logManager.logError(message, error),
|
||||
}).then(() => {
|
||||
this.sillyTavernProcess = null;
|
||||
this.windowManager.sendKoboldOutput('SillyTavern process terminated');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -511,7 +518,6 @@ export class SillyTavernManager {
|
|||
new Promise((resolve) => {
|
||||
this.proxyServer?.close(() => {
|
||||
this.proxyServer = null;
|
||||
this.windowManager.sendKoboldOutput('Proxy server closed');
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
|
|
@ -519,7 +525,6 @@ export class SillyTavernManager {
|
|||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
this.windowManager.sendKoboldOutput('SillyTavern frontend stopped');
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export class WindowManager {
|
|||
private setupContextMenu() {
|
||||
if (!this.mainWindow) return;
|
||||
|
||||
this.mainWindow.webContents.on('context-menu', (_event, params) => {
|
||||
this.mainWindow.webContents.on('context-menu', (_, params) => {
|
||||
const hasLinkURL = !!params.linkURL;
|
||||
const isDev = this.isDevelopment();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
HardwareInfo,
|
||||
GPUMemoryInfo,
|
||||
} from '@/types/hardware';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
export class HardwareService {
|
||||
private cpuCapabilitiesCache: CPUCapabilities | null = null;
|
||||
|
|
@ -147,7 +148,6 @@ export class HardwareService {
|
|||
devices: string[];
|
||||
}> {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const nvidia = spawn(
|
||||
'nvidia-smi',
|
||||
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
|
||||
|
|
@ -200,7 +200,6 @@ export class HardwareService {
|
|||
devices: string[];
|
||||
}> {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const rocminfo = spawn('rocminfo', [], { timeout: 5000 });
|
||||
|
||||
let output = '';
|
||||
|
|
@ -278,7 +277,6 @@ export class HardwareService {
|
|||
devices: string[];
|
||||
}> {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const vulkaninfo = spawn('vulkaninfo', ['--summary'], { timeout: 5000 });
|
||||
|
||||
let output = '';
|
||||
|
|
@ -390,7 +388,6 @@ export class HardwareService {
|
|||
devices: string[];
|
||||
}> {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const clinfo = spawn('clinfo', [], { timeout: 3000 });
|
||||
|
||||
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';
|
||||
|
||||
export interface ProcessTerminationOptions {
|
||||
|
|
@ -5,6 +6,32 @@ export interface ProcessTerminationOptions {
|
|||
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(
|
||||
childProcess: ChildProcess,
|
||||
options: ProcessTerminationOptions = {}
|
||||
|
|
@ -16,26 +43,43 @@ export async function terminateProcess(
|
|||
}
|
||||
|
||||
try {
|
||||
const signal = process.platform === 'win32' ? undefined : 'SIGTERM';
|
||||
childProcess.kill(signal);
|
||||
if (process.platform === 'win32') {
|
||||
await killWindowsProcessTree(childProcess.pid, logError);
|
||||
|
||||
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);
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
Math.min(timeoutMs, 2000)
|
||||
);
|
||||
|
||||
childProcess.once('exit', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
childProcess.once('exit', () => {
|
||||
clearTimeout(timeout);
|
||||
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) {
|
||||
logError?.('Error terminating process:', error as Error);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue