feat: implement ProcessManager service for lifecycle management and add pidusage dependency
This commit is contained in:
Generated
+20
@@ -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",
|
||||
|
||||
+3
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user