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", "@emotion/styled": "^11.11.0",
"@mui/icons-material": "^6.0.0", "@mui/icons-material": "^6.0.0",
"@mui/material": "^6.0.0", "@mui/material": "^6.0.0",
"@types/pidusage": "^2.0.5",
"@types/prismjs": "^1.26.6", "@types/prismjs": "^1.26.6",
"axios": "^1.14.0", "axios": "^1.14.0",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"i18next": "^23.1.0", "i18next": "^23.1.0",
"pidusage": "^4.0.1",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -2469,6 +2471,12 @@
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT" "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": { "node_modules/@types/plist": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
@@ -7665,6 +7673,18 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/pify": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+2
View File
@@ -27,10 +27,12 @@
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/icons-material": "^6.0.0", "@mui/icons-material": "^6.0.0",
"@mui/material": "^6.0.0", "@mui/material": "^6.0.0",
"@types/pidusage": "^2.0.5",
"@types/prismjs": "^1.26.6", "@types/prismjs": "^1.26.6",
"axios": "^1.14.0", "axios": "^1.14.0",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"i18next": "^23.1.0", "i18next": "^23.1.0",
"pidusage": "^4.0.1",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^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 path from 'path'
import { app } from 'electron' import { app } from 'electron'
import fs from 'fs' import fs from 'fs'
import pidusage from 'pidusage'
import { configService } from './ConfigService' import { configService } from './ConfigService'
import { t } from '../utils/i18n' import { t } from '../utils/i18n'
@@ -281,15 +282,35 @@ export class ProcessManager {
return this.processes.has(name) return this.processes.has(name)
} }
getStatuses(): Record<string, string> { async getStatuses(): Promise<Record<string, { status: string; cpu?: number; memory?: number }>> {
const statuses: Record<string, string> = {} const statuses: Record<string, { status: string; cpu?: number; memory?: number }> = {}
for (const name of Array.from(this.processes.keys())) {
statuses[name] = 'running' 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 // Ensure defaults are present if stopped
if (!statuses['nginx']) statuses['nginx'] = 'stopped' const defaultServices = ['nginx', 'apache', 'mariadb']
if (!statuses['apache']) statuses['apache'] = 'stopped' defaultServices.forEach(s => {
if (!statuses['mariadb']) statuses['mariadb'] = 'stopped' if (!statuses[s]) statuses[s] = { status: 'stopped' }
})
return statuses return statuses
} }
+69 -34
View File
@@ -87,8 +87,14 @@ declare global {
const drawerWidth = 240 const drawerWidth = 240
interface ServiceInfo {
status: string;
cpu?: number;
memory?: number;
}
interface ServiceStatus { interface ServiceStatus {
[key: string]: string [key: string]: ServiceInfo;
} }
interface AppSettings { interface AppSettings {
@@ -105,10 +111,10 @@ interface AppSettings {
function App(): JSX.Element { function App(): JSX.Element {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const [status, setStatus] = useState<ServiceStatus>({ const [status, setStatus] = useState<ServiceStatus>({
nginx: 'stopped', nginx: { status: 'stopped' },
apache: 'stopped', apache: { status: 'stopped' },
php: 'stopped', php: { status: 'stopped' },
mariadb: 'stopped' mariadb: { status: 'stopped' }
}) })
const [activeTab, setActiveTab] = useState('dashboard') const [activeTab, setActiveTab] = useState('dashboard')
const [settingsTab, setSettingsTab] = useState(0) const [settingsTab, setSettingsTab] = useState(0)
@@ -333,12 +339,12 @@ function App(): JSX.Element {
const handleToggleService = async (service: string) => { const handleToggleService = async (service: string) => {
if (!window.api) return if (!window.api) return
const isStarting = (status[service] || 'stopped') === 'stopped' const isStarting = ((status[service] as any)?.status || status[service] || 'stopped') === 'stopped'
if (isStarting) { 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.stopService(service)
: await window.api.startService(service) : await window.api.startService(service)
@@ -419,23 +425,23 @@ function App(): JSX.Element {
if (!window.api) return if (!window.api) return
const criticalServices = ['nginx', 'apache'] const criticalServices = ['nginx', 'apache']
for (const s of criticalServices) { 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}`) const phpToStart = phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`)
for (const s of phpToStart) { 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}`) const mariaDbToStart = mariaDbVersions.filter(v => v.status === 'installed').map(v => `mariadb:${v.version}`)
for (const s of mariaDbToStart) { 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([ await Promise.all([
...criticalServices.filter(s => status[s] !== '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] || 'stopped') !== '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] || 'stopped') !== 'running').map(s => window.api?.startService(s)) ...mariaDbToStart.filter(s => (status[s]?.status || 'stopped') !== 'running').map(s => window.api?.startService(s))
]) ])
fetchStatus() fetchStatus()
@@ -445,11 +451,11 @@ function App(): JSX.Element {
const handleStopAll = async () => { const handleStopAll = async () => {
if (!window.api) return if (!window.api) return
const criticalServices = ['nginx', 'apache'] const criticalServices = ['nginx', 'apache']
const phpToStop = Object.keys(status).filter(s => s.startsWith('php:') && (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] === 'running' || status[s] === '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([ 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)), ...phpToStop.map(s => window.api?.stopService(s)),
...mariaDbToStop.map(s => window.api?.stopService(s)), ...mariaDbToStop.map(s => window.api?.stopService(s)),
window.api?.stopService('mariadb') // Also stop default 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' }) setNotification({ open: true, message: t('dashboard.all_stopped'), severity: 'success' })
} }
const getStatusChip = (s: string) => { const formatMemory = (bytes?: number) => {
if (s === 'starting') { 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" />} /> 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" />} /> 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' const color = state === 'running' ? 'success' : state === 'stopped' ? 'error' : 'warning'
return <Chip label={t(`common.${s}`).toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
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 () => { const handleAddProject = async () => {
@@ -1150,16 +1185,16 @@ function App(): JSX.Element {
}} }}
title={t('server.nginx.save_reload')} title={t('server.nginx.save_reload')}
size="small" size="small"
disabled={status.nginx !== 'running'} disabled={status.nginx?.status !== 'running'}
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }} sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
> >
<RefreshIcon fontSize="small" /> <RefreshIcon fontSize="small" />
</IconButton> </IconButton>
</Box> </Box>
<Switch <Switch
checked={status.nginx === 'running' || status.nginx === 'starting'} checked={status.nginx?.status === 'running' || status.nginx?.status === 'starting'}
onChange={() => handleToggleService('nginx')} onChange={() => handleToggleService('nginx')}
disabled={status.nginx === 'starting'} disabled={status.nginx?.status === 'starting'}
/> />
</Box> </Box>
</Paper> </Paper>
@@ -1212,16 +1247,16 @@ function App(): JSX.Element {
}} }}
title={t('server.apache.save_reload')} title={t('server.apache.save_reload')}
size="small" size="small"
disabled={status.apache !== 'running'} disabled={status.apache?.status !== 'running'}
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }} sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
> >
<RefreshIcon fontSize="small" /> <RefreshIcon fontSize="small" />
</IconButton> </IconButton>
</Box> </Box>
<Switch <Switch
checked={status.apache === 'running' || status.apache === 'starting'} checked={status.apache?.status === 'running' || status.apache?.status === 'starting'}
onChange={() => handleToggleService('apache')} onChange={() => handleToggleService('apache')}
disabled={status.apache === 'starting'} disabled={status.apache?.status === 'starting'}
/> />
</Box> </Box>
</Paper> </Paper>
@@ -1241,7 +1276,7 @@ function App(): JSX.Element {
}}> }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<PhpIcon color="primary" sx={{ fontSize: 40 }} /> <PhpIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status[`php:${v.version}`] || 'stopped')} {getStatusChip(status[`php:${v.version}`] || { status: 'stopped' })}
</Box> </Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>PHP {v.version}</Typography> <Typography variant="h6" sx={{ fontWeight: 'bold' }}>PHP {v.version}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Port: {v.port || '?'}</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" /> <TerminalIcon fontSize="small" />
</IconButton> </IconButton>
<Switch <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}`)} onChange={() => handleToggleService(`php:${v.version}`)}
disabled={status[`php:${v.version}`] === 'starting'} disabled={status[`php:${v.version}`]?.status === 'starting'}
/> />
</Box> </Box>
</Paper> </Paper>
@@ -1272,7 +1307,7 @@ function App(): JSX.Element {
}}> }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<DbIcon color="primary" sx={{ fontSize: 40 }} /> <DbIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status[`mariadb:${v.version}`] || 'stopped')} {getStatusChip(status[`mariadb:${v.version}`] || { status: 'stopped' })}
</Box> </Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>MariaDB {v.version}</Typography> <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> <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> </IconButton>
</Box> </Box>
<Switch <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}`)} onChange={() => handleToggleService(`mariadb:${v.version}`)}
disabled={status[`mariadb:${v.version}`] === 'starting'} disabled={status[`mariadb:${v.version}`]?.status === 'starting'}
/> />
</Box> </Box>
</Paper> </Paper>