feat: implement ProcessManager service for lifecycle management and add UI components for dashboard monitoring

This commit is contained in:
Ümit Tunç
2026-04-03 06:55:32 +03:00
parent cdb36196e5
commit e7be5c1c19
10 changed files with 270 additions and 64 deletions
+29 -26
View File
@@ -1,33 +1,36 @@
<VirtualHost *:{{PORT}}> # Subdirectory Alias for {{HOST}}
ServerName {{HOST}} Alias /{{HOST}} "{{PATH}}"
DocumentRoot "{{PATH}}"
<Directory "{{PATH}}"> <Directory "{{PATH}}">
Options Indexes FollowSymLinks ExecCGI Options Indexes FollowSymLinks ExecCGI
AllowOverride All AllowOverride All
Require all granted Require all granted
# Passing Authorization header to PHP # --- KRİTİK EKLEME BURASI ---
CGIPassAuth On # PHP'ye php.ini dosyasını {{PHP_DIR}} klasöründe aramasını söyler
SetEnv PHPRC "{{PHP_DIR}}"
# ----------------------------
# Directory index handling # Passing Authorization header to PHP
DirectoryIndex index.php index.html CGIPassAuth On
# PHP Handling via mod_actions and CGI # Directory index handling
<FilesMatch "\.php$"> DirectoryIndex index.php index.html
SetHandler application/x-httpd-php-{{HOST}}
</FilesMatch>
# Action maps the handler to the binary via the ScriptAlias URL # PHP Handling via mod_actions and CGI
Action application/x-httpd-php-{{HOST}} "{{PHP_BIN_URL}}/php-cgi.exe" <FilesMatch "\.php$">
</Directory> SetHandler application/x-httpd-php-alias-{{HOST}}
</FilesMatch>
# ScriptAlias definition for the project's PHP CGI binary # Action maps the handler to the binary via the ScriptAlias URL
ScriptAlias {{PHP_BIN_URL}}/ "{{PHP_DIR}}/" Action application/x-httpd-php-alias-{{HOST}} "{{PHP_BIN_URL}}/php-cgi.exe"
</Directory>
<Directory "{{PHP_DIR}}"> # ScriptAlias definition for the project's PHP CGI binary
AllowOverride None ScriptAlias {{PHP_BIN_URL}}/ "{{PHP_DIR}}/"
Options None
Require all granted <Directory "{{PHP_DIR}}">
</Directory> AllowOverride None
</VirtualHost> Options None
Require all granted
</Directory>
+33
View File
@@ -0,0 +1,33 @@
<VirtualHost *:{{PORT}}>
ServerName {{HOST}}
DocumentRoot "{{PATH}}"
<Directory "{{PATH}}">
Options Indexes FollowSymLinks ExecCGI
AllowOverride All
Require all granted
# Passing Authorization header to PHP
CGIPassAuth On
# Directory index handling
DirectoryIndex index.php index.html
# PHP Handling via mod_actions and CGI
<FilesMatch "\.php$">
SetHandler application/x-httpd-php-{{HOST}}
</FilesMatch>
# Action maps the handler to the binary via the ScriptAlias URL
Action application/x-httpd-php-{{HOST}} "{{PHP_BIN_URL}}/php-cgi.exe"
</Directory>
# ScriptAlias definition for the project's PHP CGI binary
ScriptAlias {{PHP_BIN_URL}}/ "{{PHP_DIR}}/"
<Directory "{{PHP_DIR}}">
AllowOverride None
Options None
Require all granted
</Directory>
</VirtualHost>
+4 -1
View File
@@ -11,5 +11,8 @@
"4.4.9": 9009, "4.4.9": 9009,
"3.0.x (latest)": 9010 "3.0.x (latest)": 9010
}, },
"mariadb": {} "mariadb": {
"11.4.2": 3306,
"10.11.8": 3307
}
} }
+115
View File
@@ -1,4 +1,119 @@
[ [
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "zhj5uo002",
"timestamp": 1775188480028
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "lnxl5gj0x",
"timestamp": 1775188480010
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "d7pzpblst",
"timestamp": 1775188479993
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "9v9w3li68",
"timestamp": 1775188479975
},
{
"service": "mariadb:10.11.8",
"action": "start",
"status": "success",
"id": "q2w07qros",
"timestamp": 1775188479689
},
{
"service": "mariadb:11.4.2",
"action": "start",
"status": "success",
"id": "2p8k4gdxd",
"timestamp": 1775188479666
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "pvto9aq7v",
"timestamp": 1775188478980
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "cozuqx8yf",
"timestamp": 1775188478919
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "n6kxec3vz",
"timestamp": 1775187974649
},
{
"service": "apache",
"action": "exit",
"status": "failure",
"details": "Exit code: null",
"id": "nt1xnmbop",
"timestamp": 1775187970713
},
{
"service": "apache",
"action": "stop",
"status": "info",
"id": "cf6yzeo2h",
"timestamp": 1775187970709
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "kcijsqygj",
"timestamp": 1775187963197
},
{
"service": "php:7.4.33",
"action": "exit",
"status": "failure",
"details": "Exit code: null",
"id": "9f03j1ihl",
"timestamp": 1775187959015
},
{
"service": "php:7.4.33",
"action": "stop",
"status": "info",
"id": "meezjpt0j",
"timestamp": 1775187959002
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "4zl8frca6",
"timestamp": 1775187929780
},
{
"service": "apache",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "bjo67q9pj",
"timestamp": 1775187928384
},
{ {
"service": "php:5.6.40", "service": "php:5.6.40",
"action": "start", "action": "start",
+23 -5
View File
@@ -6,6 +6,7 @@ import pidusage from 'pidusage'
import { configService } from './ConfigService' import { configService } from './ConfigService'
import { serviceEventLogger } from './ServiceEventLogger' import { serviceEventLogger } from './ServiceEventLogger'
import { t } from '../utils/i18n' import { t } from '../utils/i18n'
import { portMappingService } from './PortMappingService'
export class ProcessManager { export class ProcessManager {
private processes: Map<string, ChildProcess> = new Map() private processes: Map<string, ChildProcess> = new Map()
@@ -341,12 +342,13 @@ export class ProcessManager {
return !!(child && child.pid) return !!(child && child.pid)
} }
async getStatuses(): Promise<Record<string, { status: string; cpu?: number; memory?: number }>> { async getStatuses(): Promise<Record<string, { status: string; cpu?: number; memory?: number, port?: string | number }>> {
const statuses: Record<string, { status: string; cpu?: number; memory?: number }> = {} const statuses: Record<string, { status: string; cpu?: number; memory?: number, port?: string | number }> = {}
// Add all tracked states first // Add all tracked states first
for (const [name, state] of this.states.entries()) { for (const [name, state] of this.states.entries()) {
statuses[name] = { status: state } const port = this.getServicePort(name)
statuses[name] = { status: state, port }
} }
const entries = Array.from(this.processes.entries()) const entries = Array.from(this.processes.entries())
@@ -358,7 +360,8 @@ export class ProcessManager {
statuses[name] = { statuses[name] = {
status: currentState === 'stopping' ? 'stopping' : 'running', status: currentState === 'stopping' ? 'stopping' : 'running',
cpu: Math.round(stats.cpu * 10) / 10, cpu: Math.round(stats.cpu * 10) / 10,
memory: stats.memory memory: stats.memory,
port: this.getServicePort(name)
} }
if (currentState !== 'stopping' && currentState !== 'starting') { if (currentState !== 'stopping' && currentState !== 'starting') {
this.states.set(name, 'running') this.states.set(name, 'running')
@@ -381,12 +384,27 @@ export class ProcessManager {
// Ensure defaults are present // Ensure defaults are present
const defaultServices = ['nginx', 'apache', 'mariadb'] const defaultServices = ['nginx', 'apache', 'mariadb']
defaultServices.forEach(s => { defaultServices.forEach(s => {
if (!statuses[s]) statuses[s] = { status: this.states.get(s) || 'stopped' } if (!statuses[s]) statuses[s] = { status: this.states.get(s) || 'stopped', port: this.getServicePort(s) }
}) })
return statuses return statuses
} }
private getServicePort(name: string): string | number | undefined {
const settings = configService.getSettings()
if (name === 'nginx') return settings.nginxPort
if (name === 'apache') return settings.apachePort
if (name.startsWith('php:')) {
const version = name.split(':')[1]
return portMappingService.getPhpPort(version)
}
if (name.startsWith('mariadb:')) {
const version = name.split(':')[1]
return portMappingService.getMariaDbPort(version)
}
return undefined
}
private async checkErrorLogs(name: string) { private async checkErrorLogs(name: string) {
if (!this.logDir) return if (!this.logDir) return
const logPaths: Record<string, string> = { const logPaths: Record<string, string> = {
@@ -69,8 +69,10 @@ export default function DashboardDetails({
if (!serviceId) return null; if (!serviceId) return null;
if (serviceId === 'nginx') return 'nginx'; if (serviceId === 'nginx') return 'nginx';
if (serviceId === 'apache') return 'apache'; if (serviceId === 'apache') return 'apache';
if (serviceId.startsWith('php:')) return 'php'; if (serviceId.includes(':') || serviceId.includes('-')) {
if (serviceId.startsWith('mariadb:')) return 'mariadb'; const type = serviceId.includes(':') ? serviceId.split(':')[0] : serviceId.split('-')[0];
return type.toLowerCase();
}
return null; return null;
}; };
@@ -181,6 +183,24 @@ export default function DashboardDetails({
} }
const currentStatus = status[serviceId] || { status: 'stopped' }; const currentStatus = status[serviceId] || { status: 'stopped' };
const getPort = () => {
if (currentStatus.port) return currentStatus.port;
// Fallback for stopped services or missing data
if (serviceId === 'nginx') return settings.nginxPort;
if (serviceId === 'apache') return settings.apachePort;
if (serviceType === 'php') {
const version = serviceId.includes(':') ? serviceId.split(':')[1] : serviceId.split('-')[1];
return version ? '...' : settings.phpPort;
}
if (serviceType === 'mariadb' || serviceType === 'mysql') {
const version = serviceId.includes(':') ? serviceId.split(':')[1] : serviceId.split('-')[1];
if (version && settings.mariaDbPorts && settings.mariaDbPorts[version]) return settings.mariaDbPorts[version];
if (version) return '...';
return settings.mariaDbPort;
}
return 'n/a';
};
const isRunning = currentStatus.status === 'running'; const isRunning = currentStatus.status === 'running';
const serviceProjects = projects.filter(p => { const serviceProjects = projects.filter(p => {
@@ -247,13 +267,13 @@ export default function DashboardDetails({
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}> <Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}>
<Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)}> <Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)}>
<Tab label={t('common.overview', 'Genel Bakış')} /> <Tab label={t('dashboard.overview')} />
<Tab label={t('dashboard.service_settings', 'Servis Ayarları')} /> <Tab label={t('dashboard.service_settings')} />
{(serviceId === 'nginx' || serviceId === 'apache' || serviceType === 'php') && ( {(serviceId === 'nginx' || serviceId === 'apache' || serviceType === 'php') && (
<Tab label={t('dashboard.project_settings', 'Proje Ayarları')} /> <Tab label={t('dashboard.project_settings')} />
)} )}
<Tab label={t('diagnostics.logs', 'Hata Logları')} /> <Tab label={t('diagnostics.logs')} />
<Tab label={t('projects.title', 'Projeler')} /> <Tab label={t('projects.title')} />
</Tabs> </Tabs>
</Box> </Box>
@@ -261,35 +281,26 @@ export default function DashboardDetails({
{activeTab === 0 && ( {activeTab === 0 && (
<Grid container spacing={4}> <Grid container spacing={4}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>SYSTEM STATS</Typography> <Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>{t('dashboard.system_stats')}</Typography>
<Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}> <Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}>
<ResourceMonitor cpu={currentStatus.cpu} memory={currentStatus.memory} /> <ResourceMonitor cpu={currentStatus.cpu} memory={currentStatus.memory} />
</Paper> </Paper>
</Grid> </Grid>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>SERVICE INFO</Typography> <Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>{t('dashboard.service_info')}</Typography>
<Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}> <Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}>
<Stack spacing={2}> <Stack spacing={2}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ opacity: 0.5 }}>Port</Typography> <Typography variant="body2" sx={{ opacity: 0.5 }}>{t('common.port')}</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}> <Typography variant="body2" sx={{ fontWeight: 600 }}>
{currentStatus.port || (() => { {getPort()}
if (serviceId === 'nginx') return settings.nginxPort;
if (serviceId === 'apache') return settings.apachePort;
if (serviceType === 'php') return settings.phpPort;
if (serviceType === 'mariadb') {
const verId = serviceId.split(':')[1];
return (settings.mariaDbPorts && settings.mariaDbPorts[verId]) || settings.mariaDbPort;
}
return 'n/a';
})()}
</Typography> </Typography>
</Box> </Box>
<Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} /> <Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ opacity: 0.5 }}>Status</Typography> <Typography variant="body2" sx={{ opacity: 0.5 }}>{t('common.status')}</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: isRunning ? 'success.main' : 'error.main' }}> <Typography variant="body2" sx={{ fontWeight: 600, color: isRunning ? 'success.main' : 'error.main' }}>
{currentStatus.status.toUpperCase()} {(isRunning ? t('common.running') : t('common.stopped')).toUpperCase()}
</Typography> </Typography>
</Box> </Box>
</Stack> </Stack>
@@ -3,7 +3,6 @@ import {
Typography, Typography,
List, List,
ListItemButton, ListItemButton,
Chip,
CircularProgress CircularProgress
} from '@mui/material' } from '@mui/material'
import { import {
@@ -62,6 +61,11 @@ export default function DashboardSidebar({
<Typography variant="body2" noWrap sx={{ fontWeight: 600 }}> <Typography variant="body2" noWrap sx={{ fontWeight: 600 }}>
{label} {label}
</Typography> </Typography>
{isRunning && status[serviceKey] && (
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', display: 'block', fontSize: '0.65rem', mt: -0.5 }}>
Port: {status[serviceKey].port || '?'} | CPU: {Math.round(status[serviceKey].cpu || 0)}% | RAM: {Math.round((status[serviceKey].memory || 0) / (1024 * 1024))}MB
</Typography>
)}
</Box> </Box>
<Box sx={{ <Box sx={{
width: 10, width: 10,
+7 -1
View File
@@ -56,7 +56,13 @@
"start_all": "Alle starten", "start_all": "Alle starten",
"stop_all": "Alle stoppen", "stop_all": "Alle stoppen",
"all_started": "Alle Dienste wurden gestartet.", "all_started": "Alle Dienste wurden gestartet.",
"all_stopped": "Alle Dienste wurden gestoppt." "all_stopped": "Alle Dienste wurden gestoppt.",
"overview": "Übersicht",
"service_settings": "Service-Einstellungen",
"project_settings": "Projekt-Einstellungen",
"select_service": "Bitte wählen Sie einen Dienst aus",
"system_stats": "SYSTEMSTATISTIKEN",
"service_info": "SERVICEINFO"
}, },
"projects": { "projects": {
"title": "Meine Projekte", "title": "Meine Projekte",
+7 -1
View File
@@ -61,7 +61,13 @@
"start_all": "Start All", "start_all": "Start All",
"stop_all": "Stop All", "stop_all": "Stop All",
"all_started": "All services started.", "all_started": "All services started.",
"all_stopped": "All services stopped." "all_stopped": "All services stopped.",
"overview": "Overview",
"service_settings": "Service Settings",
"project_settings": "Project Settings",
"select_service": "Please select a service",
"system_stats": "SYSTEM STATS",
"service_info": "SERVICE INFO"
}, },
"projects": { "projects": {
"title": "My Projects", "title": "My Projects",
+9 -2
View File
@@ -50,7 +50,8 @@
"reset_confirm_text": "{{service}} verilerini sıfırlamak istiyor musunuz? Bu işlem tüm verileri siler ve temiz kurulum yapar.", "reset_confirm_text": "{{service}} verilerini sıfırlamak istiyor musunuz? Bu işlem tüm verileri siler ve temiz kurulum yapar.",
"reset_btn": "Evet, Sıfırla", "reset_btn": "Evet, Sıfırla",
"operating": "İşlem devam ediyor...", "operating": "İşlem devam ediyor...",
"please_wait": "Lütfen bekleyin..." "please_wait": "Lütfen bekleyin...",
"status": "Durum"
}, },
"dashboard": { "dashboard": {
"title": "Özet", "title": "Özet",
@@ -61,7 +62,13 @@
"start_all": "Tümünü Başlat", "start_all": "Tümünü Başlat",
"stop_all": "Tümünü Durdur", "stop_all": "Tümünü Durdur",
"all_started": "Tüm servisler başlatıldı.", "all_started": "Tüm servisler başlatıldı.",
"all_stopped": "Tüm servisler durduruldu." "all_stopped": "Tüm servisler durduruldu.",
"overview": "Genel Bakış",
"service_settings": "Servis Ayarları",
"project_settings": "Proje Ayarları",
"select_service": "Lütfen bir servis seçin",
"system_stats": "SİSTEM İSTATİSTİKLERİ",
"service_info": "SERVİS BİLGİLERİ"
}, },
"projects": { "projects": {
"title": "Projelerim", "title": "Projelerim",