feat: implement ProcessManager service for lifecycle management and add pidusage dependency

This commit is contained in:
Ümit Tunç
2026-04-02 00:23:05 +03:00
parent 14eb9f6c1e
commit 90bb7bbdbd
4 changed files with 120 additions and 42 deletions
+20
View File
@@ -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",
+2
View File
@@ -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",
+28 -7
View File
@@ -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<string, string> {
const statuses: Record<string, string> = {}
for (const name of Array.from(this.processes.keys())) {
statuses[name] = 'running'
async getStatuses(): Promise<Record<string, { status: string; cpu?: number; memory?: number }>> {
const statuses: Record<string, { status: string; cpu?: number; memory?: number }> = {}
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
}
+69 -34
View File
@@ -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<ServiceStatus>({
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 <Chip label={t('common.starting')} color="warning" size="small" sx={{ fontWeight: 'bold' }} icon={<CircularProgress size={14} color="inherit" />} />
}
if (s === 'stopping') {
if (state === 'stopping') {
return <Chip label={t('common.stopping')} color="warning" size="small" sx={{ fontWeight: 'bold' }} icon={<CircularProgress size={14} color="inherit" />} />
}
const color = s === 'running' ? 'success' : s === 'stopped' ? 'error' : 'warning'
return <Chip label={t(`common.${s}`).toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
const color = state === 'running' ? 'success' : state === 'stopped' ? 'error' : 'warning'
return (
<Stack direction="row" spacing={1} alignItems="center">
<Chip label={t(`common.${state}`).toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
{state === 'running' && cpu !== null && (
<Tooltip title="CPU / RAM Usage">
<Stack direction="row" spacing={0.5} sx={{ opacity: 0.8 }}>
<Typography variant="caption" sx={{ color: 'primary.light', fontWeight: 'bold' }}>
{cpu}%
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 12, my: 'auto', bgcolor: 'rgba(255,255,255,0.2)' }} />
<Typography variant="caption" sx={{ color: 'secondary.light', fontWeight: 'bold' }}>
{formatMemory(memory as number)}
</Typography>
</Stack>
</Tooltip>
)}
</Stack>
)
}
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' } }}
>
<RefreshIcon fontSize="small" />
</IconButton>
</Box>
<Switch
checked={status.nginx === 'running' || status.nginx === 'starting'}
checked={status.nginx?.status === 'running' || status.nginx?.status === 'starting'}
onChange={() => handleToggleService('nginx')}
disabled={status.nginx === 'starting'}
disabled={status.nginx?.status === 'starting'}
/>
</Box>
</Paper>
@@ -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' } }}
>
<RefreshIcon fontSize="small" />
</IconButton>
</Box>
<Switch
checked={status.apache === 'running' || status.apache === 'starting'}
checked={status.apache?.status === 'running' || status.apache?.status === 'starting'}
onChange={() => handleToggleService('apache')}
disabled={status.apache === 'starting'}
disabled={status.apache?.status === 'starting'}
/>
</Box>
</Paper>
@@ -1241,7 +1276,7 @@ function App(): JSX.Element {
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<PhpIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status[`php:${v.version}`] || 'stopped')}
{getStatusChip(status[`php:${v.version}`] || { status: 'stopped' })}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>PHP {v.version}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Port: {v.port || '?'}</Typography>
@@ -1250,9 +1285,9 @@ function App(): JSX.Element {
<TerminalIcon fontSize="small" />
</IconButton>
<Switch
checked={(status[`php:${v.version}`] || 'stopped') === 'running' || status[`php:${v.version}`] === 'starting'}
checked={status[`php:${v.version}`]?.status === 'running' || status[`php:${v.version}`]?.status === 'starting'}
onChange={() => handleToggleService(`php:${v.version}`)}
disabled={status[`php:${v.version}`] === 'starting'}
disabled={status[`php:${v.version}`]?.status === 'starting'}
/>
</Box>
</Paper>
@@ -1272,7 +1307,7 @@ function App(): JSX.Element {
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<DbIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status[`mariadb:${v.version}`] || 'stopped')}
{getStatusChip(status[`mariadb:${v.version}`] || { status: 'stopped' })}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>MariaDB {v.version}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Port: {settings.mariaDbPorts?.[v.id] || v.port}</Typography>
@@ -1286,9 +1321,9 @@ function App(): JSX.Element {
</IconButton>
</Box>
<Switch
checked={(status[`mariadb:${v.version}`] || 'stopped') === 'running' || status[`mariadb:${v.version}`] === 'starting'}
checked={status[`mariadb:${v.version}`]?.status === 'running' || status[`mariadb:${v.version}`]?.status === 'starting'}
onChange={() => handleToggleService(`mariadb:${v.version}`)}
disabled={status[`mariadb:${v.version}`] === 'starting'}
disabled={status[`mariadb:${v.version}`]?.status === 'starting'}
/>
</Box>
</Paper>