diff --git a/package-lock.json b/package-lock.json index 4160e63..d934401 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,12 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^6.0.0", "@mui/material": "^6.0.0", + "@types/pidusage": "^2.0.5", "@types/prismjs": "^1.26.6", "axios": "^1.14.0", "decompress": "^4.2.1", "i18next": "^23.1.0", + "pidusage": "^4.0.1", "prismjs": "^1.30.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -2469,6 +2471,12 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/pidusage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/pidusage/-/pidusage-2.0.5.tgz", + "integrity": "sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==", + "license": "MIT" + }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", @@ -7665,6 +7673,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidusage": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-4.0.1.tgz", + "integrity": "sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", diff --git a/package.json b/package.json index ce4055c..19ac63c 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,12 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^6.0.0", "@mui/material": "^6.0.0", + "@types/pidusage": "^2.0.5", "@types/prismjs": "^1.26.6", "axios": "^1.14.0", "decompress": "^4.2.1", "i18next": "^23.1.0", + "pidusage": "^4.0.1", "prismjs": "^1.30.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -73,4 +75,4 @@ "icon": "resources/icon.png" } } -} \ No newline at end of file +} diff --git a/src/main/services/ProcessManager.ts b/src/main/services/ProcessManager.ts index 3a911d7..b5e9025 100644 --- a/src/main/services/ProcessManager.ts +++ b/src/main/services/ProcessManager.ts @@ -2,6 +2,7 @@ import { spawn, ChildProcess } from 'child_process' import path from 'path' import { app } from 'electron' import fs from 'fs' +import pidusage from 'pidusage' import { configService } from './ConfigService' import { t } from '../utils/i18n' @@ -281,15 +282,35 @@ export class ProcessManager { return this.processes.has(name) } - getStatuses(): Record { - const statuses: Record = {} - for (const name of Array.from(this.processes.keys())) { - statuses[name] = 'running' + async getStatuses(): Promise> { + const statuses: Record = {} + + const entries = Array.from(this.processes.entries()) + for (const [name, child] of entries) { + if (child.pid) { + try { + const stats = await pidusage(child.pid) + statuses[name] = { + status: 'running', + cpu: Math.round(stats.cpu * 10) / 10, // Round to 1 decimal + memory: stats.memory + } + } catch (e) { + statuses[name] = { status: 'running' } + // Clean up pidusage if error (usually process died) + pidusage.clear() + } + } else { + statuses[name] = { status: 'running' } + } } + // Ensure defaults are present if stopped - if (!statuses['nginx']) statuses['nginx'] = 'stopped' - if (!statuses['apache']) statuses['apache'] = 'stopped' - if (!statuses['mariadb']) statuses['mariadb'] = 'stopped' + const defaultServices = ['nginx', 'apache', 'mariadb'] + defaultServices.forEach(s => { + if (!statuses[s]) statuses[s] = { status: 'stopped' } + }) + return statuses } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 7e632f5..3a49b30 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -87,8 +87,14 @@ declare global { const drawerWidth = 240 +interface ServiceInfo { + status: string; + cpu?: number; + memory?: number; +} + interface ServiceStatus { - [key: string]: string + [key: string]: ServiceInfo; } interface AppSettings { @@ -105,10 +111,10 @@ interface AppSettings { function App(): JSX.Element { const { t, i18n } = useTranslation() const [status, setStatus] = useState({ - nginx: 'stopped', - apache: 'stopped', - php: 'stopped', - mariadb: 'stopped' + nginx: { status: 'stopped' }, + apache: { status: 'stopped' }, + php: { status: 'stopped' }, + mariadb: { status: 'stopped' } }) const [activeTab, setActiveTab] = useState('dashboard') const [settingsTab, setSettingsTab] = useState(0) @@ -333,12 +339,12 @@ function App(): JSX.Element { const handleToggleService = async (service: string) => { if (!window.api) return - const isStarting = (status[service] || 'stopped') === 'stopped' + const isStarting = ((status[service] as any)?.status || status[service] || 'stopped') === 'stopped' if (isStarting) { - setStatus(prev => ({ ...prev, [service]: 'starting' })) + setStatus(prev => ({ ...prev, [service]: { status: 'starting' } })) } - const result = (status[service] || 'stopped') === 'running' + const result = ((status[service] as any)?.status || status[service] || 'stopped') === 'running' ? await window.api.stopService(service) : await window.api.startService(service) @@ -419,23 +425,23 @@ function App(): JSX.Element { if (!window.api) return const criticalServices = ['nginx', 'apache'] for (const s of criticalServices) { - if (status[s] === 'stopped') setStatus(prev => ({ ...prev, [s]: 'starting' })) + if (status[s]?.status === 'stopped') setStatus(prev => ({ ...prev, [s]: { status: 'starting' } })) } const phpToStart = phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`) for (const s of phpToStart) { - if ((status[s] || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: 'starting' })) + if ((status[s]?.status || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: { status: 'starting' } })) } const mariaDbToStart = mariaDbVersions.filter(v => v.status === 'installed').map(v => `mariadb:${v.version}`) for (const s of mariaDbToStart) { - if ((status[s] || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: 'starting' })) + if ((status[s]?.status || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: { status: 'starting' } })) } await Promise.all([ - ...criticalServices.filter(s => status[s] !== 'running').map(s => window.api?.startService(s)), - ...phpToStart.filter(s => (status[s] || 'stopped') !== 'running').map(s => window.api?.startService(s)), - ...mariaDbToStart.filter(s => (status[s] || 'stopped') !== 'running').map(s => window.api?.startService(s)) + ...criticalServices.filter(s => status[s]?.status !== 'running').map(s => window.api?.startService(s)), + ...phpToStart.filter(s => (status[s]?.status || 'stopped') !== 'running').map(s => window.api?.startService(s)), + ...mariaDbToStart.filter(s => (status[s]?.status || 'stopped') !== 'running').map(s => window.api?.startService(s)) ]) fetchStatus() @@ -445,11 +451,11 @@ function App(): JSX.Element { const handleStopAll = async () => { if (!window.api) return const criticalServices = ['nginx', 'apache'] - const phpToStop = Object.keys(status).filter(s => s.startsWith('php:') && (status[s] === 'running' || status[s] === 'starting')) - const mariaDbToStop = Object.keys(status).filter(s => (s.startsWith('mariadb:') || s.startsWith('mysql:')) && (status[s] === 'running' || status[s] === 'starting')) + const phpToStop = Object.keys(status).filter(s => s.startsWith('php:') && (status[s]?.status === 'running' || status[s]?.status === 'starting')) + const mariaDbToStop = Object.keys(status).filter(s => (s.startsWith('mariadb:') || s.startsWith('mysql:')) && (status[s]?.status === 'running' || status[s]?.status === 'starting')) await Promise.all([ - ...criticalServices.filter(s => status[s] === 'running' || status[s] === 'starting').map(s => window.api?.stopService(s)), + ...criticalServices.filter(s => status[s]?.status === 'running' || status[s]?.status === 'starting').map(s => window.api?.stopService(s)), ...phpToStop.map(s => window.api?.stopService(s)), ...mariaDbToStop.map(s => window.api?.stopService(s)), window.api?.stopService('mariadb') // Also stop default @@ -459,15 +465,44 @@ function App(): JSX.Element { setNotification({ open: true, message: t('dashboard.all_stopped'), severity: 'success' }) } - const getStatusChip = (s: string) => { - if (s === 'starting') { + const formatMemory = (bytes?: number) => { + if (!bytes) return '' + const mb = bytes / (1024 * 1024) + if (mb > 1024) return `${(mb / 1024).toFixed(1)} GB` + return `${Math.round(mb)} MB` + } + + const getStatusChip = (s: string | { status: string, cpu?: number, memory?: number }) => { + const state = typeof s === 'string' ? s : s.status + const cpu = typeof s === 'object' ? s.cpu : null + const memory = typeof s === 'object' ? s.memory : null + + if (state === 'starting') { return } /> } - if (s === 'stopping') { + if (state === 'stopping') { return } /> } - const color = s === 'running' ? 'success' : s === 'stopped' ? 'error' : 'warning' - return + const color = state === 'running' ? 'success' : state === 'stopped' ? 'error' : 'warning' + + return ( + + + {state === 'running' && cpu !== null && ( + + + + {cpu}% + + + + {formatMemory(memory as number)} + + + + )} + + ) } const handleAddProject = async () => { @@ -1150,16 +1185,16 @@ function App(): JSX.Element { }} title={t('server.nginx.save_reload')} size="small" - disabled={status.nginx !== 'running'} + disabled={status.nginx?.status !== 'running'} sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }} > handleToggleService('nginx')} - disabled={status.nginx === 'starting'} + disabled={status.nginx?.status === 'starting'} /> @@ -1212,16 +1247,16 @@ function App(): JSX.Element { }} title={t('server.apache.save_reload')} size="small" - disabled={status.apache !== 'running'} + disabled={status.apache?.status !== 'running'} sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }} > handleToggleService('apache')} - disabled={status.apache === 'starting'} + disabled={status.apache?.status === 'starting'} /> @@ -1241,7 +1276,7 @@ function App(): JSX.Element { }}> - {getStatusChip(status[`php:${v.version}`] || 'stopped')} + {getStatusChip(status[`php:${v.version}`] || { status: 'stopped' })} PHP {v.version} Port: {v.port || '?'} @@ -1250,9 +1285,9 @@ function App(): JSX.Element { handleToggleService(`php:${v.version}`)} - disabled={status[`php:${v.version}`] === 'starting'} + disabled={status[`php:${v.version}`]?.status === 'starting'} /> @@ -1272,7 +1307,7 @@ function App(): JSX.Element { }}> - {getStatusChip(status[`mariadb:${v.version}`] || 'stopped')} + {getStatusChip(status[`mariadb:${v.version}`] || { status: 'stopped' })} MariaDB {v.version} Port: {settings.mariaDbPorts?.[v.id] || v.port} @@ -1286,9 +1321,9 @@ function App(): JSX.Element { handleToggleService(`mariadb:${v.version}`)} - disabled={status[`mariadb:${v.version}`] === 'starting'} + disabled={status[`mariadb:${v.version}`]?.status === 'starting'} />