mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
changing metrics because windows sucks
This commit is contained in:
parent
f3dbabd98a
commit
280ae26d77
11 changed files with 438 additions and 311 deletions
|
|
@ -144,6 +144,7 @@ const config = [
|
||||||
'arrow-body-style': ['error', 'as-needed'],
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
|
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
|
||||||
'object-shorthand': ['error', 'always'],
|
'object-shorthand': ['error', 'always'],
|
||||||
|
'prefer-const': 'error',
|
||||||
|
|
||||||
'no-console': 'error',
|
'no-console': 'error',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
"@typescript-eslint/parser": "^8.43.0",
|
"@typescript-eslint/parser": "^8.43.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"electron": "^38.0.0",
|
"electron": "^38.1.0",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^4.0.0",
|
"electron-vite": "^4.0.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.35.0",
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@ import { Settings } from 'lucide-react';
|
||||||
import { SettingsModal } from '@/components/settings/SettingsModal';
|
import { SettingsModal } from '@/components/settings/SettingsModal';
|
||||||
import { safeExecute } from '@/utils/logger';
|
import { safeExecute } from '@/utils/logger';
|
||||||
import type { FrontendPreference, Screen } from '@/types';
|
import type { FrontendPreference, Screen } from '@/types';
|
||||||
import type { SystemMetrics } from '@/main/modules/monitoring';
|
import type {
|
||||||
|
CpuMetrics,
|
||||||
|
MemoryMetrics,
|
||||||
|
GpuMetrics,
|
||||||
|
} from '@/main/modules/monitoring';
|
||||||
|
|
||||||
interface StatusBarProps {
|
interface StatusBarProps {
|
||||||
maxDataPoints?: number;
|
maxDataPoints?: number;
|
||||||
|
|
@ -28,9 +32,11 @@ export const StatusBar = ({
|
||||||
frontendPreference: _frontendPreference,
|
frontendPreference: _frontendPreference,
|
||||||
onFrontendPreferenceChange,
|
onFrontendPreferenceChange,
|
||||||
}: StatusBarProps) => {
|
}: StatusBarProps) => {
|
||||||
const [currentMetrics, setCurrentMetrics] = useState<SystemMetrics | null>(
|
const [cpuMetrics, setCpuMetrics] = useState<CpuMetrics | null>(null);
|
||||||
|
const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null);
|
||||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const colorScheme = useComputedColorScheme('light', {
|
const colorScheme = useComputedColorScheme('light', {
|
||||||
|
|
@ -40,29 +46,42 @@ export const StatusBar = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
const handleMetrics = async (newMetrics: SystemMetrics) => {
|
const handleCpuMetrics = async (metrics: CpuMetrics) => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
setCpuMetrics(metrics);
|
||||||
setCurrentMetrics(newMetrics);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.electronAPI.monitoring.onMetrics(handleMetrics);
|
const handleMemoryMetrics = async (metrics: MemoryMetrics) => {
|
||||||
|
if (!isMounted) return;
|
||||||
|
setMemoryMetrics(metrics);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGpuMetrics = async (metrics: GpuMetrics) => {
|
||||||
|
if (!isMounted) return;
|
||||||
|
setGpuMetrics(metrics);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.electronAPI.monitoring.onCpuMetrics(handleCpuMetrics);
|
||||||
|
window.electronAPI.monitoring.onMemoryMetrics(handleMemoryMetrics);
|
||||||
|
window.electronAPI.monitoring.onGpuMetrics(handleGpuMetrics);
|
||||||
void window.electronAPI.monitoring.start();
|
void window.electronAPI.monitoring.start();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
window.electronAPI.monitoring.removeMetricsListener();
|
window.electronAPI.monitoring.removeCpuMetricsListener();
|
||||||
|
window.electronAPI.monitoring.removeMemoryMetricsListener();
|
||||||
|
window.electronAPI.monitoring.removeGpuMetricsListener();
|
||||||
window.electronAPI.monitoring.stop();
|
window.electronAPI.monitoring.stop();
|
||||||
};
|
};
|
||||||
}, [maxDataPoints]);
|
}, [maxDataPoints]);
|
||||||
|
|
||||||
if (!currentMetrics) {
|
if (!cpuMetrics || !memoryMetrics) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
pr="xs"
|
px="xs"
|
||||||
style={{
|
style={{
|
||||||
borderTop: `1px solid ${colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[3]}`,
|
borderTop: `1px solid ${colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[3]}`,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
|
|
@ -73,37 +92,50 @@ export const StatusBar = ({
|
||||||
>
|
>
|
||||||
<Flex align="center" justify="space-between">
|
<Flex align="center" justify="space-between">
|
||||||
<Group gap="xs" wrap="nowrap">
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Tooltip
|
<Tooltip label={`${cpuMetrics.usage.toFixed(1)}%`} position="top">
|
||||||
label={`${currentMetrics.cpu.usage.toFixed(1)}%`}
|
<Badge
|
||||||
position="top"
|
size="sm"
|
||||||
>
|
variant="light"
|
||||||
<Badge size="sm" variant="light">
|
style={{ minWidth: '5rem', textAlign: 'center' }}
|
||||||
CPU: {currentMetrics.cpu.usage.toFixed(1)}%
|
>
|
||||||
|
CPU: {cpuMetrics.usage.toFixed(1)}%
|
||||||
</Badge>
|
</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={`${(currentMetrics.memory.used / 1024 ** 3).toFixed(1)} GB / ${(currentMetrics.memory.total / 1024 ** 3).toFixed(1)} GB (${currentMetrics.memory.usage.toFixed(1)}%)`}
|
label={`${(memoryMetrics.used / 1024 ** 3).toFixed(1)} GB / ${(memoryMetrics.total / 1024 ** 3).toFixed(1)} GB (${memoryMetrics.usage.toFixed(1)}%)`}
|
||||||
position="top"
|
position="top"
|
||||||
>
|
>
|
||||||
<Badge size="sm" variant="light">
|
<Badge
|
||||||
RAM: {currentMetrics.memory.usage.toFixed(1)}%
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
style={{ minWidth: '5rem', textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
RAM: {memoryMetrics.usage.toFixed(1)}%
|
||||||
</Badge>
|
</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{currentMetrics.gpu?.map((gpu, index) => (
|
{gpuMetrics?.gpus.map((gpu, index) => (
|
||||||
<Group key={`gpu-${index}`} gap={4} wrap="nowrap">
|
<Group key={`gpu-${index}`} gap={4} wrap="nowrap">
|
||||||
<Tooltip label={`${gpu.usage.toFixed(1)}%`} position="top">
|
<Tooltip label={`${gpu.usage.toFixed(1)}%`} position="top">
|
||||||
<Badge size="sm" variant="light">
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
style={{ minWidth: '5rem', textAlign: 'center' }}
|
||||||
|
>
|
||||||
GPU: {gpu.usage.toFixed(1)}%
|
GPU: {gpu.usage.toFixed(1)}%
|
||||||
</Badge>
|
</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={`${(gpu.memoryUsed / 1024 ** 2).toFixed(1)} MB / ${(gpu.memoryTotal / 1024 ** 2).toFixed(1)} MB (${gpu.memoryUsage.toFixed(1)}%)`}
|
label={`${(gpu.memoryUsed / 1024).toFixed(1)} GB / ${(gpu.memoryTotal / 1024).toFixed(1)} GB (${gpu.memoryUsage.toFixed(1)}%)`}
|
||||||
position="top"
|
position="top"
|
||||||
>
|
>
|
||||||
<Badge size="sm" variant="light">
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
style={{ minWidth: '5rem', textAlign: 'center' }}
|
||||||
|
>
|
||||||
VRAM: {gpu.memoryUsage.toFixed(1)}%
|
VRAM: {gpu.memoryUsage.toFixed(1)}%
|
||||||
</Badge>
|
</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,7 @@ import {
|
||||||
detectBackendSupport,
|
detectBackendSupport,
|
||||||
getAvailableBackends,
|
getAvailableBackends,
|
||||||
} from '@/main/modules/binary';
|
} from '@/main/modules/binary';
|
||||||
import {
|
import { startMonitoring, stopMonitoring } from '@/main/modules/monitoring';
|
||||||
setMainWindow,
|
|
||||||
startMonitoring,
|
|
||||||
stopMonitoring,
|
|
||||||
} from '@/main/modules/monitoring';
|
|
||||||
import type { FrontendPreference } from '@/types';
|
import type { FrontendPreference } from '@/types';
|
||||||
|
|
||||||
async function launchKoboldCppWithCustomFrontends(args: string[] = []) {
|
async function launchKoboldCppWithCustomFrontends(args: string[] = []) {
|
||||||
|
|
@ -238,8 +234,7 @@ export function setupIPCHandlers() {
|
||||||
ipcMain.handle('monitoring:start', () => {
|
ipcMain.handle('monitoring:start', () => {
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = getMainWindow();
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
setMainWindow(mainWindow);
|
startMonitoring(mainWindow);
|
||||||
startMonitoring();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
import { logError } from '@/main/modules/logging';
|
import { logError } from '@/main/modules/logging';
|
||||||
import { terminateProcess } from '@/utils/process';
|
import { terminateProcess } from '@/utils/process';
|
||||||
|
import { getGPUData } from '@/utils/gpu';
|
||||||
import type {
|
import type {
|
||||||
CPUCapabilities,
|
CPUCapabilities,
|
||||||
GPUCapabilities,
|
GPUCapabilities,
|
||||||
|
|
@ -58,35 +59,32 @@ export async function detectGPU() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const graphics = await si.graphics();
|
const gpuData = await getGPUData();
|
||||||
|
|
||||||
let hasAMD = false;
|
let hasAMD = false;
|
||||||
let hasNVIDIA = false;
|
let hasNVIDIA = false;
|
||||||
const gpuInfo: string[] = [];
|
const gpuInfo: string[] = [];
|
||||||
|
|
||||||
for (const controller of graphics.controllers) {
|
for (const gpu of gpuData) {
|
||||||
if (controller.model) {
|
if (gpu.deviceName) {
|
||||||
gpuInfo.push(formatDeviceName(controller.model));
|
gpuInfo.push(formatDeviceName(gpu.deviceName));
|
||||||
}
|
}
|
||||||
|
|
||||||
const vendor = controller.vendor?.toLowerCase() || '';
|
const deviceName = gpu.deviceName?.toLowerCase() || '';
|
||||||
const model = controller.model?.toLowerCase() || '';
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
vendor.includes('amd') ||
|
deviceName.includes('amd') ||
|
||||||
vendor.includes('ati') ||
|
deviceName.includes('ati') ||
|
||||||
model.includes('radeon') ||
|
deviceName.includes('radeon')
|
||||||
model.includes('amd')
|
|
||||||
) {
|
) {
|
||||||
hasAMD = true;
|
hasAMD = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
vendor.includes('nvidia') ||
|
deviceName.includes('nvidia') ||
|
||||||
model.includes('nvidia') ||
|
deviceName.includes('geforce') ||
|
||||||
model.includes('geforce') ||
|
deviceName.includes('gtx') ||
|
||||||
model.includes('gtx') ||
|
deviceName.includes('rtx')
|
||||||
model.includes('rtx')
|
|
||||||
) {
|
) {
|
||||||
hasNVIDIA = true;
|
hasNVIDIA = true;
|
||||||
}
|
}
|
||||||
|
|
@ -434,18 +432,18 @@ export async function detectGPUMemory() {
|
||||||
const memoryInfo: GPUMemoryInfo[] = [];
|
const memoryInfo: GPUMemoryInfo[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const graphics = await si.graphics();
|
const gpuData = await getGPUData();
|
||||||
|
|
||||||
for (const controller of graphics.controllers) {
|
for (const gpu of gpuData) {
|
||||||
if (controller.model) {
|
if (gpu.deviceName) {
|
||||||
let vram = controller.vram;
|
let vram: number | null = gpu.memoryTotal;
|
||||||
|
|
||||||
if (!vram || vram === 1) {
|
if (!vram || vram <= 1) {
|
||||||
vram = null;
|
vram = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
memoryInfo.push({
|
memoryInfo.push({
|
||||||
deviceName: formatDeviceName(controller.model),
|
deviceName: formatDeviceName(gpu.deviceName),
|
||||||
totalMemoryMB: vram,
|
totalMemoryMB: vram,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,29 @@
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow } from 'electron';
|
||||||
import { logError } from '@/main/modules/logging';
|
import { logError } from '@/main/modules/logging';
|
||||||
import { readFile, readdir } from 'fs/promises';
|
import { getGPUData } from '@/utils/gpu';
|
||||||
import { join } from 'path';
|
|
||||||
import { spawn } from 'child_process';
|
export interface CpuMetrics {
|
||||||
import { platform } from 'process';
|
usage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryMetrics {
|
||||||
|
used: number;
|
||||||
|
total: number;
|
||||||
|
usage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GpuMetrics {
|
||||||
|
gpus: {
|
||||||
|
name: string;
|
||||||
|
usage: number;
|
||||||
|
memoryUsed: number;
|
||||||
|
memoryTotal: number;
|
||||||
|
memoryUsage: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SystemMetrics {
|
export interface SystemMetrics {
|
||||||
timestamp: number;
|
|
||||||
cpu: {
|
cpu: {
|
||||||
usage: number;
|
usage: number;
|
||||||
};
|
};
|
||||||
|
|
@ -25,275 +41,105 @@ export interface SystemMetrics {
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let interval: ReturnType<typeof setInterval> | null = null;
|
let cpuInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let memoryInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let gpuInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let updateFrequency = 1000;
|
const cpuUpdateFrequency = 1000;
|
||||||
|
const memoryUpdateFrequency = 1000;
|
||||||
|
const gpuUpdateFrequency = 2000;
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
export function setMainWindow(window: BrowserWindow) {
|
export function startMonitoring(window: BrowserWindow) {
|
||||||
mainWindow = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startMonitoring() {
|
|
||||||
if (isRunning) return;
|
if (isRunning) return;
|
||||||
|
|
||||||
|
mainWindow = window;
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
collectAndSendMetrics();
|
|
||||||
interval = setInterval(() => {
|
collectAndSendCpuMetrics();
|
||||||
collectAndSendMetrics();
|
cpuInterval = setInterval(() => {
|
||||||
}, updateFrequency);
|
collectAndSendCpuMetrics();
|
||||||
|
}, cpuUpdateFrequency);
|
||||||
|
|
||||||
|
collectAndSendMemoryMetrics();
|
||||||
|
memoryInterval = setInterval(() => {
|
||||||
|
collectAndSendMemoryMetrics();
|
||||||
|
}, memoryUpdateFrequency);
|
||||||
|
|
||||||
|
collectAndSendGpuMetrics();
|
||||||
|
gpuInterval = setInterval(() => {
|
||||||
|
collectAndSendGpuMetrics();
|
||||||
|
}, gpuUpdateFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopMonitoring() {
|
export function stopMonitoring() {
|
||||||
if (interval) {
|
if (cpuInterval) {
|
||||||
clearInterval(interval);
|
clearInterval(cpuInterval);
|
||||||
interval = null;
|
cpuInterval = null;
|
||||||
|
}
|
||||||
|
if (memoryInterval) {
|
||||||
|
clearInterval(memoryInterval);
|
||||||
|
memoryInterval = null;
|
||||||
|
}
|
||||||
|
if (gpuInterval) {
|
||||||
|
clearInterval(gpuInterval);
|
||||||
|
gpuInterval = null;
|
||||||
}
|
}
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectAndSendMetrics() {
|
async function collectAndSendCpuMetrics() {
|
||||||
try {
|
try {
|
||||||
const metrics = await collectMetrics();
|
const cpuData = await si.currentLoad();
|
||||||
|
const metrics: CpuMetrics = {
|
||||||
|
usage: cpuData.currentLoad,
|
||||||
|
};
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('system-metrics', metrics);
|
mainWindow.webContents.send('cpu-metrics', metrics);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Failed to collect system metrics:', error as Error);
|
logError('Failed to collect CPU metrics:', error as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWindowsGPUData() {
|
async function collectAndSendMemoryMetrics() {
|
||||||
return new Promise<
|
|
||||||
{
|
|
||||||
deviceName: string;
|
|
||||||
usage: number;
|
|
||||||
memoryUsed: number;
|
|
||||||
memoryTotal: number;
|
|
||||||
}[]
|
|
||||||
>((resolve) => {
|
|
||||||
const gpus: {
|
|
||||||
deviceName: string;
|
|
||||||
usage: number;
|
|
||||||
memoryUsed: number;
|
|
||||||
memoryTotal: number;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
const powershellScript = `
|
|
||||||
# Get GPU information using WMI and Performance Counters
|
|
||||||
$gpuInfo = Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -notlike "*Microsoft*" -and $_.Name -notlike "*Remote*"}
|
|
||||||
|
|
||||||
foreach ($gpu in $gpuInfo) {
|
|
||||||
$name = $gpu.Name
|
|
||||||
$vramTotal = if ($gpu.AdapterRAM) { $gpu.AdapterRAM } else { 0 }
|
|
||||||
|
|
||||||
# Try to get GPU usage from performance counters
|
|
||||||
$usage = 0
|
|
||||||
try {
|
|
||||||
$counter = Get-Counter "\\GPU Engine(*engtype_3D)\\Utilization Percentage" -ErrorAction SilentlyContinue
|
|
||||||
if ($counter) {
|
|
||||||
$usage = ($counter.CounterSamples | Measure-Object -Property CookedValue -Maximum).Maximum
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
# Try alternative performance counter for GPU usage
|
|
||||||
if ($usage -eq 0) {
|
|
||||||
try {
|
|
||||||
$counter = Get-Counter "\\GPU Process Memory(*)\\Dedicated Usage" -ErrorAction SilentlyContinue
|
|
||||||
if ($counter) {
|
|
||||||
$memUsed = ($counter.CounterSamples | Measure-Object -Property CookedValue -Sum).Sum
|
|
||||||
$usage = if ($vramTotal -gt 0) { [math]::Round(($memUsed / $vramTotal) * 100, 2) } else { 0 }
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Output as JSON
|
|
||||||
@{
|
|
||||||
Name = $name
|
|
||||||
Usage = $usage
|
|
||||||
MemoryUsed = 0
|
|
||||||
MemoryTotal = $vramTotal
|
|
||||||
} | ConvertTo-Json -Compress
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const powershell = spawn(
|
|
||||||
'powershell.exe',
|
|
||||||
[
|
|
||||||
'-NoProfile',
|
|
||||||
'-ExecutionPolicy',
|
|
||||||
'Bypass',
|
|
||||||
'-Command',
|
|
||||||
powershellScript,
|
|
||||||
],
|
|
||||||
{ timeout: 5000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = '';
|
|
||||||
powershell.stdout.on('data', (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
powershell.on('close', (code) => {
|
|
||||||
if (code === 0 && output.trim()) {
|
|
||||||
try {
|
|
||||||
const lines = output
|
|
||||||
.trim()
|
|
||||||
.split('\n')
|
|
||||||
.filter((line) => line.trim());
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
try {
|
|
||||||
const gpuData = JSON.parse(line);
|
|
||||||
if (gpuData.Name) {
|
|
||||||
gpus.push({
|
|
||||||
deviceName: 'GPU',
|
|
||||||
usage: parseFloat(gpuData.Usage) || 0,
|
|
||||||
memoryUsed: parseInt(gpuData.MemoryUsed) || 0,
|
|
||||||
memoryTotal: parseInt(gpuData.MemoryTotal) || 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logError('Failed to parse Windows GPU data:', error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(gpus);
|
|
||||||
});
|
|
||||||
|
|
||||||
powershell.on('error', () => {
|
|
||||||
resolve(gpus);
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
powershell.kill();
|
|
||||||
resolve(gpus);
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLinuxGPUData() {
|
|
||||||
try {
|
try {
|
||||||
return getLinuxDrmMetrics();
|
const memData = await si.mem();
|
||||||
} catch (error) {
|
const metrics: MemoryMetrics = {
|
||||||
logError('Failed to get Linux GPU data:', error as Error);
|
used: memData.active || memData.used,
|
||||||
return [];
|
total: memData.total,
|
||||||
}
|
usage: ((memData.active || memData.used) / memData.total) * 100,
|
||||||
}
|
};
|
||||||
|
|
||||||
async function getLinuxDrmMetrics() {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
try {
|
mainWindow.webContents.send('memory-metrics', metrics);
|
||||||
const drmPath = '/sys/class/drm';
|
|
||||||
const entries = await readdir(drmPath);
|
|
||||||
const cardEntries = entries.filter(
|
|
||||||
(entry) => entry.startsWith('card') && !entry.includes('-')
|
|
||||||
);
|
|
||||||
|
|
||||||
const gpus = [];
|
|
||||||
for (const card of cardEntries) {
|
|
||||||
const devicePath = join(drmPath, card, 'device');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const usageData = await readFile(
|
|
||||||
`${devicePath}/gpu_busy_percent`,
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
const memUsedData = await readFile(
|
|
||||||
`${devicePath}/mem_info_vram_used`,
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
const memTotalData = await readFile(
|
|
||||||
`${devicePath}/mem_info_vram_total`,
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
const usage = parseInt(usageData.trim(), 10) || 0;
|
|
||||||
const memoryUsed = parseInt(memUsedData.trim(), 10) || 0;
|
|
||||||
const memoryTotal = parseInt(memTotalData.trim(), 10) || 0;
|
|
||||||
|
|
||||||
gpus.push({
|
|
||||||
deviceName: 'GPU',
|
|
||||||
usage,
|
|
||||||
memoryUsed,
|
|
||||||
memoryTotal,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
return gpus;
|
logError('Failed to collect memory metrics:', error as Error);
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getGPUData() {
|
async function collectAndSendGpuMetrics() {
|
||||||
if (platform === 'win32') {
|
try {
|
||||||
return getWindowsGPUData();
|
const gpuData = await getGPUData();
|
||||||
} else if (platform === 'linux') {
|
const metrics: GpuMetrics = {
|
||||||
return getLinuxGPUData();
|
gpus: gpuData.map((gpuInfo) => ({
|
||||||
} else {
|
name: gpuInfo.deviceName,
|
||||||
return [];
|
usage: gpuInfo.usage,
|
||||||
|
memoryUsed: gpuInfo.memoryUsed,
|
||||||
|
memoryTotal: gpuInfo.memoryTotal,
|
||||||
|
memoryUsage:
|
||||||
|
gpuInfo.memoryTotal > 0
|
||||||
|
? (gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100
|
||||||
|
: 0,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('gpu-metrics', metrics);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logError('Failed to collect GPU metrics:', error as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectMetrics(): Promise<SystemMetrics> {
|
|
||||||
const timestamp = Date.now();
|
|
||||||
|
|
||||||
const [cpuData, memData, gpuData] = await Promise.allSettled([
|
|
||||||
si.currentLoad(),
|
|
||||||
si.mem(),
|
|
||||||
getGPUData(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const cpu = {
|
|
||||||
usage: cpuData.status === 'fulfilled' ? cpuData.value.currentLoad : 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const memory = {
|
|
||||||
used:
|
|
||||||
memData.status === 'fulfilled'
|
|
||||||
? memData.value.active || memData.value.used
|
|
||||||
: 0,
|
|
||||||
total: memData.status === 'fulfilled' ? memData.value.total : 0,
|
|
||||||
usage:
|
|
||||||
memData.status === 'fulfilled'
|
|
||||||
? ((memData.value.active || memData.value.used) / memData.value.total) *
|
|
||||||
100
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let gpu: SystemMetrics['gpu'];
|
|
||||||
if (gpuData.status === 'fulfilled' && gpuData.value.length > 0) {
|
|
||||||
gpu = gpuData.value.map((gpuInfo: {
|
|
||||||
deviceName: string;
|
|
||||||
usage: number;
|
|
||||||
memoryUsed: number;
|
|
||||||
memoryTotal: number;
|
|
||||||
}) => ({
|
|
||||||
name: gpuInfo.deviceName,
|
|
||||||
usage: gpuInfo.usage,
|
|
||||||
memoryUsed: gpuInfo.memoryUsed,
|
|
||||||
memoryTotal: gpuInfo.memoryTotal,
|
|
||||||
memoryUsage:
|
|
||||||
gpuInfo.memoryTotal > 0
|
|
||||||
? (gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100
|
|
||||||
: 0,
|
|
||||||
}));
|
|
||||||
} else if (gpuData.status === 'rejected') {
|
|
||||||
logError('GPU detection failed', gpuData.reason as Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
timestamp,
|
|
||||||
cpu,
|
|
||||||
memory,
|
|
||||||
gpu,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -111,11 +111,23 @@ const openwebUIAPI: OpenWebUIAPI = {
|
||||||
const monitoringAPI: MonitoringAPI = {
|
const monitoringAPI: MonitoringAPI = {
|
||||||
start: () => ipcRenderer.invoke('monitoring:start'),
|
start: () => ipcRenderer.invoke('monitoring:start'),
|
||||||
stop: () => ipcRenderer.invoke('monitoring:stop'),
|
stop: () => ipcRenderer.invoke('monitoring:stop'),
|
||||||
onMetrics: (callback) => {
|
onCpuMetrics: (callback) => {
|
||||||
ipcRenderer.on('system-metrics', (_, metrics) => callback(metrics));
|
ipcRenderer.on('cpu-metrics', (_, metrics) => callback(metrics));
|
||||||
},
|
},
|
||||||
removeMetricsListener: () => {
|
onMemoryMetrics: (callback) => {
|
||||||
ipcRenderer.removeAllListeners('system-metrics');
|
ipcRenderer.on('memory-metrics', (_, metrics) => callback(metrics));
|
||||||
|
},
|
||||||
|
onGpuMetrics: (callback) => {
|
||||||
|
ipcRenderer.on('gpu-metrics', (_, metrics) => callback(metrics));
|
||||||
|
},
|
||||||
|
removeCpuMetricsListener: () => {
|
||||||
|
ipcRenderer.removeAllListeners('cpu-metrics');
|
||||||
|
},
|
||||||
|
removeMemoryMetricsListener: () => {
|
||||||
|
ipcRenderer.removeAllListeners('memory-metrics');
|
||||||
|
},
|
||||||
|
removeGpuMetricsListener: () => {
|
||||||
|
ipcRenderer.removeAllListeners('gpu-metrics');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
14
src/types/electron.d.ts
vendored
14
src/types/electron.d.ts
vendored
|
|
@ -6,7 +6,11 @@ import type {
|
||||||
} from '@/types/hardware';
|
} from '@/types/hardware';
|
||||||
import type { BackendOption, BackendSupport } from '@/types';
|
import type { BackendOption, BackendSupport } from '@/types';
|
||||||
import type { MantineColorScheme } from '@mantine/core';
|
import type { MantineColorScheme } from '@mantine/core';
|
||||||
import type { SystemMetrics } from '@/main/modules/monitoring';
|
import type {
|
||||||
|
CpuMetrics,
|
||||||
|
MemoryMetrics,
|
||||||
|
GpuMetrics,
|
||||||
|
} from '@/main/modules/monitoring';
|
||||||
|
|
||||||
export interface GitHubAsset {
|
export interface GitHubAsset {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -176,8 +180,12 @@ export interface OpenWebUIAPI {
|
||||||
export interface MonitoringAPI {
|
export interface MonitoringAPI {
|
||||||
start: () => Promise<void>;
|
start: () => Promise<void>;
|
||||||
stop: () => Promise<void>;
|
stop: () => Promise<void>;
|
||||||
onMetrics: (callback: (metrics: SystemMetrics) => void) => void;
|
onCpuMetrics: (callback: (metrics: CpuMetrics) => void) => void;
|
||||||
removeMetricsListener: () => void;
|
onMemoryMetrics: (callback: (metrics: MemoryMetrics) => void) => void;
|
||||||
|
onGpuMetrics: (callback: (metrics: GpuMetrics) => void) => void;
|
||||||
|
removeCpuMetricsListener: () => void;
|
||||||
|
removeMemoryMetricsListener: () => void;
|
||||||
|
removeGpuMetricsListener: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
||||||
235
src/utils/gpu.ts
Normal file
235
src/utils/gpu.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
import { readFile, readdir } from 'fs/promises';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { platform } from 'process';
|
||||||
|
|
||||||
|
export async function getGPUData() {
|
||||||
|
if (platform === 'win32') {
|
||||||
|
return getWindowsGPUData();
|
||||||
|
} else if (platform === 'linux') {
|
||||||
|
return getLinuxGPUData();
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWindowsGPUData() {
|
||||||
|
return new Promise<
|
||||||
|
{
|
||||||
|
deviceName: string;
|
||||||
|
usage: number;
|
||||||
|
memoryUsed: number;
|
||||||
|
memoryTotal: number;
|
||||||
|
}[]
|
||||||
|
>((resolve) => {
|
||||||
|
const script = `
|
||||||
|
# Get GPU basic info and total VRAM from registry
|
||||||
|
$gpus = Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -notlike "*Microsoft*"}
|
||||||
|
$correctVRAM = @{}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$regPath = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}"
|
||||||
|
$adapterKeys = Get-ChildItem $regPath -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '000[0-9]' }
|
||||||
|
|
||||||
|
foreach ($key in $adapterKeys) {
|
||||||
|
$keyPath = $key.PSPath
|
||||||
|
$driverDesc = Get-ItemProperty -Path $keyPath -Name "DriverDesc" -ErrorAction SilentlyContinue
|
||||||
|
$vramSize = Get-ItemProperty -Path $keyPath -Name "HardwareInformation.qwMemorySize" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($driverDesc -and $vramSize -and $driverDesc.DriverDesc -notlike "*Microsoft*") {
|
||||||
|
$correctVRAM[$driverDesc.DriverDesc] = $vramSize.'HardwareInformation.qwMemorySize'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
# Get VRAM usage from GPU Adapter Memory counters (what Task Manager uses)
|
||||||
|
$vramUsageByLuid = @{}
|
||||||
|
try {
|
||||||
|
$adapterMemory = Get-Counter "\\GPU Adapter Memory(*)\\Dedicated Usage" -ErrorAction SilentlyContinue
|
||||||
|
if ($adapterMemory) {
|
||||||
|
foreach ($sample in $adapterMemory.CounterSamples) {
|
||||||
|
$instanceName = $sample.InstanceName
|
||||||
|
$usageBytes = $sample.CookedValue
|
||||||
|
$vramUsageByLuid[$instanceName] = $usageBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
# Get GPU utilization from performance counters
|
||||||
|
$gpuUtilization = @{}
|
||||||
|
try {
|
||||||
|
$engineCounters = Get-Counter "\\GPU Engine(*)\\Utilization Percentage" -ErrorAction SilentlyContinue
|
||||||
|
if ($engineCounters) {
|
||||||
|
$utilizationByLuid = @{}
|
||||||
|
foreach ($sample in $engineCounters.CounterSamples) {
|
||||||
|
$instanceName = $sample.InstanceName
|
||||||
|
$utilization = $sample.CookedValue
|
||||||
|
|
||||||
|
if ($instanceName -match "luid_(0x[0-9a-fA-F]+_0x[0-9a-fA-F]+)") {
|
||||||
|
$luid = $matches[1]
|
||||||
|
if (-not $utilizationByLuid[$luid]) {
|
||||||
|
$utilizationByLuid[$luid] = 0
|
||||||
|
}
|
||||||
|
$utilizationByLuid[$luid] = [Math]::Max($utilizationByLuid[$luid], $utilization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find the main GPU LUID (the one with actual VRAM usage)
|
||||||
|
$mainLuid = ""
|
||||||
|
$maxVramUsage = 0
|
||||||
|
foreach ($luid in $vramUsageByLuid.Keys) {
|
||||||
|
if ($vramUsageByLuid[$luid] -gt $maxVramUsage) {
|
||||||
|
$maxVramUsage = $vramUsageByLuid[$luid]
|
||||||
|
$mainLuid = $luid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract LUID from main adapter
|
||||||
|
if ($mainLuid -match "luid_(0x[0-9a-fA-F]+_0x[0-9a-fA-F]+)") {
|
||||||
|
$mainLuidKey = $matches[1]
|
||||||
|
foreach ($gpu in $gpus) {
|
||||||
|
if ($utilizationByLuid[$mainLuidKey]) {
|
||||||
|
$gpuUtilization[$gpu.Name] = $utilizationByLuid[$mainLuidKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
foreach ($gpu in $gpus) {
|
||||||
|
$totalVRAM = $correctVRAM[$gpu.Name]
|
||||||
|
if (-not $totalVRAM -or $totalVRAM -le 0) {
|
||||||
|
$totalVRAM = $gpu.AdapterRAM
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get VRAM usage for the main GPU (highest usage LUID)
|
||||||
|
$usedVRAM = 0
|
||||||
|
$maxUsage = 0
|
||||||
|
foreach ($luid in $vramUsageByLuid.Keys) {
|
||||||
|
$usage = $vramUsageByLuid[$luid]
|
||||||
|
if ($usage -gt $maxUsage) {
|
||||||
|
$maxUsage = $usage
|
||||||
|
$usedVRAM = $usage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$utilization = 0
|
||||||
|
if ($gpuUtilization[$gpu.Name]) {
|
||||||
|
$utilization = $gpuUtilization[$gpu.Name]
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "$($gpu.Name)|$totalVRAM|$usedVRAM|$utilization"
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const powershell = spawn(
|
||||||
|
'powershell.exe',
|
||||||
|
[
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-ExecutionPolicy',
|
||||||
|
'Bypass',
|
||||||
|
'-Command',
|
||||||
|
script,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
windowsHide: true,
|
||||||
|
shell: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
|
||||||
|
powershell.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
powershell.on('close', (code) => {
|
||||||
|
const gpus: {
|
||||||
|
deviceName: string;
|
||||||
|
usage: number;
|
||||||
|
memoryUsed: number;
|
||||||
|
memoryTotal: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
if (code === 0 && output.trim()) {
|
||||||
|
const lines = output.trim().split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.split('|');
|
||||||
|
if (parts.length === 4 && parts[0]) {
|
||||||
|
const name = parts[0].trim();
|
||||||
|
const totalRAM = parseInt(parts[1]) || 0;
|
||||||
|
const usedRAM = parseInt(parts[2]) || 0;
|
||||||
|
const utilization = parseFloat(parts[3]) || 0;
|
||||||
|
|
||||||
|
gpus.push({
|
||||||
|
deviceName: name,
|
||||||
|
usage: Math.round(utilization * 100) / 100,
|
||||||
|
memoryUsed: usedRAM > 0 ? Math.round(usedRAM / (1024 * 1024)) : 0,
|
||||||
|
memoryTotal:
|
||||||
|
totalRAM > 0 ? Math.round(totalRAM / (1024 * 1024)) : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(gpus);
|
||||||
|
});
|
||||||
|
|
||||||
|
powershell.on('error', () => resolve([]));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
powershell.kill('SIGTERM');
|
||||||
|
resolve([]);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLinuxGPUData() {
|
||||||
|
try {
|
||||||
|
const drmPath = '/sys/class/drm';
|
||||||
|
const entries = await readdir(drmPath);
|
||||||
|
const cardEntries = entries.filter(
|
||||||
|
(entry) => entry.startsWith('card') && !entry.includes('-')
|
||||||
|
);
|
||||||
|
|
||||||
|
const gpus = [];
|
||||||
|
for (const card of cardEntries) {
|
||||||
|
const devicePath = join(drmPath, card, 'device');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usageData = await readFile(
|
||||||
|
`${devicePath}/gpu_busy_percent`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
const memUsedData = await readFile(
|
||||||
|
`${devicePath}/mem_info_vram_used`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
const memTotalData = await readFile(
|
||||||
|
`${devicePath}/mem_info_vram_total`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const usage = parseInt(usageData.trim(), 10) || 0;
|
||||||
|
const memoryUsed = parseInt(memUsedData.trim(), 10) || 0;
|
||||||
|
const memoryTotal = parseInt(memTotalData.trim(), 10) || 0;
|
||||||
|
|
||||||
|
gpus.push({
|
||||||
|
deviceName: 'GPU',
|
||||||
|
usage,
|
||||||
|
memoryUsed,
|
||||||
|
memoryTotal,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gpus;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
10
yarn.lock
10
yarn.lock
|
|
@ -2698,16 +2698,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"electron@npm:^38.0.0":
|
"electron@npm:^38.1.0":
|
||||||
version: 38.0.0
|
version: 38.1.0
|
||||||
resolution: "electron@npm:38.0.0"
|
resolution: "electron@npm:38.1.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@electron/get": "npm:^2.0.0"
|
"@electron/get": "npm:^2.0.0"
|
||||||
"@types/node": "npm:^22.7.7"
|
"@types/node": "npm:^22.7.7"
|
||||||
extract-zip: "npm:^2.0.1"
|
extract-zip: "npm:^2.0.1"
|
||||||
bin:
|
bin:
|
||||||
electron: cli.js
|
electron: cli.js
|
||||||
checksum: 10c0/df3e4eaead2b0de4612a1b593f46667dfd4aff4755727371b5630aada40094cddf82b3837ce0ba3babed8c503275da59187f11da9f4ab26cd20e1362dc103ea6
|
checksum: 10c0/186082b15862b0e9617828ca395a79311ebe2a713bb2eb15a67cf6bea208e8cda892395a143e72915e189088ac71e91b29c1a46a0c4d343022cbc33004d4d302
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -3637,7 +3637,7 @@ __metadata:
|
||||||
"@vitejs/plugin-react": "npm:^5.0.2"
|
"@vitejs/plugin-react": "npm:^5.0.2"
|
||||||
axios: "npm:^1.11.0"
|
axios: "npm:^1.11.0"
|
||||||
cross-env: "npm:^10.0.0"
|
cross-env: "npm:^10.0.0"
|
||||||
electron: "npm:^38.0.0"
|
electron: "npm:^38.1.0"
|
||||||
electron-builder: "npm:^26.0.12"
|
electron-builder: "npm:^26.0.12"
|
||||||
electron-vite: "npm:^4.0.0"
|
electron-vite: "npm:^4.0.0"
|
||||||
eslint: "npm:^9.35.0"
|
eslint: "npm:^9.35.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue