feat: implement backend service management for Nginx, PHP, and MySQL with configuration templating and IPC handlers

This commit is contained in:
Ümit Tunç
2026-03-28 16:37:16 +03:00
parent 8abffa741f
commit 91b13513fc
6 changed files with 206 additions and 24 deletions
+38 -14
View File
@@ -34,19 +34,41 @@ export function registerIpcHandlers(): void {
if (serviceName === 'nginx') {
const rootPath = path.join(app.getAppPath(), 'www')
const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', {
PORT: settings.nginxPort,
PHP_PORT: settings.phpPort,
ROOT: rootPath
})
const nginxDir = path.join(binDir, 'nginx')
const binPath = findExecutable(nginxDir, 'nginx.exe')
const logDir = path.join(app.getPath('userData'), 'logs')
if (!binPath) return { success: false, message: 'Nginx executable bulunamadı. Lütfen indirin.' }
const success = await processManager.startService('nginx', binPath, ['-c', configPath])
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx dosyaları bulunamadı veya port meşgul.' }
// Nginx prefix = directory containing nginx.exe
const nginxPrefix = path.dirname(binPath)
const confDir = path.join(nginxPrefix, 'conf')
// Ensure temp directories exist (Nginx needs these)
const tempDirs = ['temp', 'temp/client_body_temp', 'temp/proxy_temp', 'temp/fastcgi_temp', 'temp/uwsgi_temp', 'temp/scgi_temp', 'logs']
for (const dir of tempDirs) {
const fullDir = path.join(nginxPrefix, dir)
if (!fs.existsSync(fullDir)) {
fs.mkdirSync(fullDir, { recursive: true })
}
}
const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', {
PORT: settings.nginxPort,
PHP_PORT: settings.phpPort,
ROOT: rootPath,
LOG_DIR: logDir.replace(/\\/g, '/'),
CONF_DIR: confDir.replace(/\\/g, '/')
})
// Test config first with prefix
const testResult = await processManager.runOnce(binPath, ['-t', '-p', nginxPrefix, '-c', configPath], 'nginx')
if (!testResult) {
return { success: false, message: 'Nginx konfigürasyon hatası! Log sekmesini kontrol edin.' }
}
const success = await processManager.startService('nginx', binPath, ['-p', nginxPrefix, '-c', configPath], true)
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx devreye alınamadı (Hata loglarını kontrol edin).' }
}
if (serviceName === 'php') {
@@ -55,10 +77,10 @@ export function registerIpcHandlers(): void {
const extDir = path.join(path.dirname(binPath), 'ext')
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {
EXT_DIR: extDir.replace(/\\/g, '\\\\') // PHP ini needs escaped backslashes or forward slashes
EXT_DIR: extDir
})
const success = await processManager.startService('php', binPath, ['-b', `127.0.0.1:${settings.phpPort}`, '-c', configPath])
const success = await processManager.startService('php', binPath, ['-b', `127.0.0.1:${settings.phpPort}`, '-c', configPath], false)
return { success, message: success ? 'PHP başlatıldı.' : 'PHP devreye alınamadı (Hata loglarını kontrol edin).' }
}
@@ -75,14 +97,16 @@ export function registerIpcHandlers(): void {
await processManager.runOnce(binPath, ['--initialize-insecure', `--datadir=${dataDir.replace(/\\/g, '/')}`])
}
const logFile = path.join(app.getPath('userData'), 'logs', 'mysql_error.log')
const configPath = await configService.generateConfig('my.ini.template', 'my.ini', {
PORT: settings.mysqlPort,
DATADIR: dataDir,
SOCKET: '/tmp/mysql.sock'
DATADIR: dataDir.replace(/\\/g, '/'),
SOCKET: '/tmp/mysql.sock',
LOG_FILE: logFile.replace(/\\/g, '/')
})
const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath])
return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL dosyaları bulunamadı veya port meşgul.' }
const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath], false)
return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL devreye alınamadı (Hata loglarını kontrol edin).' }
}
return { success: false, message: 'Bilinmeyen servis.' }
+41 -6
View File
@@ -63,24 +63,34 @@ export class ProcessManager {
return this.logs.get(name) || []
}
async runOnce(binPath: string, args: string[]): Promise<boolean> {
async runOnce(binPath: string, args: string[], name?: string): Promise<boolean> {
return new Promise((resolve) => {
const child = spawn(binPath, args, { stdio: 'pipe' })
const child = spawn(binPath, args, { stdio: 'pipe', shell: true, windowsHide: true })
child.stdout?.on('data', (data) => console.log(`[INIT] ${data}`))
child.stderr?.on('data', (data) => console.error(`[INIT ERROR] ${data}`))
child.stdout?.on('data', (data) => {
const out = data.toString()
console.log(`[INIT:${name || '?'}] ${out}`)
if (name) this.addLog(name, out)
})
child.stderr?.on('data', (data) => {
const err = data.toString()
console.error(`[INIT ERROR:${name || '?'}] ${err}`)
if (name) this.addLog(name, err)
})
child.on('close', (code) => {
resolve(code === 0)
})
child.on('error', (err) => {
console.error('RunOnce error:', err)
if (name) this.addLog(name, `RunOnce Error: ${err.message}`)
resolve(false)
})
})
}
async startService(name: string, binPath: string, args: string[]): Promise<boolean> {
async startService(name: string, binPath: string, args: string[], shell: boolean = true): Promise<boolean> {
if (this.processes.has(name)) {
return true
}
@@ -95,7 +105,7 @@ export class ProcessManager {
const child = spawn(binPath, args, {
detached: true,
shell: true,
shell: shell,
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'],
cwd: path.dirname(binPath)
@@ -114,6 +124,9 @@ export class ProcessManager {
child.on('exit', (code) => {
this.addLog(name, `Exited with code ${code}`)
if (code !== 0) {
this.checkErrorLogs(name)
}
this.processes.delete(name)
})
@@ -163,6 +176,28 @@ export class ProcessManager {
}
}
private async checkErrorLogs(name: string) {
if (!this.logDir) return
const logPaths: Record<string, string> = {
nginx: path.join(this.logDir, 'nginx_error.log'),
mysql: path.join(this.logDir, 'mysql_error.log')
}
const logPath = logPaths[name]
if (logPath && fs.existsSync(logPath)) {
try {
const content = fs.readFileSync(logPath, 'utf8')
const lines = content.split('\n').slice(-10) // Get last 10 lines
this.addLog(name, '--- ERROR LOG CONTENT ---')
lines.forEach(l => {
if (l.trim()) this.addLog(name, `[CORE ERROR] ${l}`)
})
} catch (e) {
console.error('Failed to read error log:', e)
}
}
}
stopAll(): void {
for (const name of this.processes.keys()) {
this.stopService(name)
+1 -1
View File
@@ -6,7 +6,7 @@ const api = {
startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name),
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
getServiceLogs: (name: string) => electronAPI.ipcRenderer.invoke('services:logs'),
getServiceLogs: (name: string) => electronAPI.ipcRenderer.invoke('services:logs', name),
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config),
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
+121 -1
View File
@@ -55,7 +55,9 @@ import {
Save as SaveIcon,
LanguageOutlined as WebIcon,
Terminal as TerminalIcon,
ContentCopy as CopyIcon
ContentCopy as CopyIcon,
Close as CloseIcon,
ContentCopy as ContentCopyIcon
} from '@mui/icons-material'
import { useTranslation } from 'react-i18next'
@@ -232,6 +234,13 @@ function App(): JSX.Element {
}
}, [logDialogOpen, selectedService])
useEffect(() => {
if (logDialogOpen) {
const end = document.getElementById('log-end')
if (end) end.scrollIntoView({ behavior: 'smooth' })
}
}, [logs, logDialogOpen])
const handleSelectPath = async () => {
if (window.api && window.api.selectDirectory) {
const path = await window.api.selectDirectory()
@@ -735,6 +744,117 @@ function App(): JSX.Element {
{notification.message}
</Alert>
</Snackbar>
{/* Log Viewer Dialog */}
<Dialog
open={logDialogOpen}
onClose={() => setLogDialogOpen(false)}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
bgcolor: '#1e1e1e',
color: '#d4d4d4',
borderRadius: 2,
boxShadow: '0 8px 32px rgba(0,0,0,0.5)'
}
}}
>
<DialogTitle sx={{
borderBottom: '1px solid #333',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
pb: 1
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TerminalIcon color="primary" />
<Typography variant="h6" sx={{ fontWeight: 600, color: '#fff' }}>
{selectedService?.toUpperCase()} - {t('diagnostics.logs', 'Servis Logları')}
</Typography>
</Box>
<IconButton size="small" onClick={() => setLogDialogOpen(false)} sx={{ color: '#666' }}>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 0, bgcolor: '#000' }}>
<Box
sx={{
height: '400px',
overflowY: 'auto',
p: 2,
fontFamily: '"Fira Code", "Consolas", monospace',
fontSize: '0.875rem',
display: 'flex',
flexDirection: 'column',
gap: '2px'
}}
>
{logs.length === 0 ? (
<Typography sx={{ color: '#666', fontStyle: 'italic', textAlign: 'center', mt: 4 }}>
{t('diagnostics.noLogs', 'Henüz log kaydı bulunmuyor.')}
</Typography>
) : (
logs.map((log, i) => {
let color = '#d4d4d4'
if (log.toLowerCase().includes('error') || log.toLowerCase().includes('failed') || log.toLowerCase().includes('emerg') || log.toLowerCase().includes('exit code 1')) {
color = '#f44336'
} else if (log.toLowerCase().includes('warning') || log.toLowerCase().includes('warn')) {
color = '#ff9800'
} else if (log.toLowerCase().includes('starting') || log.toLowerCase().includes('started') || log.toLowerCase().includes('ready') || log.toLowerCase().includes('success')) {
color = '#4caf50'
}
return (
<Box key={i} sx={{ display: 'flex', gap: 1 }}>
<Typography
component="span"
sx={{
color: '#569cd6',
minWidth: '80px',
userSelect: 'none',
fontSize: '0.8rem',
opacity: 0.7
}}
>
{log.match(/\[(\d+:\d+:\d+)\]/)?.[1] || ''}
</Typography>
<Typography
component="span"
sx={{
color,
whiteSpace: 'pre-wrap',
wordBreak: 'break-all'
}}
>
{log.replace(/\[\d+:\d+:\d+\]\s*/, '')}
</Typography>
</Box>
)
})
)}
<div id="log-end" />
</Box>
</DialogContent>
<DialogActions sx={{ borderTop: '1px solid #333', p: 1, gap: 1 }}>
<Button
startIcon={<ContentCopyIcon />}
onClick={() => {
navigator.clipboard.writeText(logs.join('\n'))
}}
sx={{ color: '#888', '&:hover': { color: '#fff' } }}
>
{t('common.copy', 'Kopyala')}
</Button>
<Box sx={{ flexGrow: 1 }} />
<Button
variant="contained"
onClick={() => setLogDialogOpen(false)}
sx={{ borderRadius: '20px' }}
>
{t('common.close', 'Kapat')}
</Button>
</DialogActions>
</Dialog>
<Dialog open={isProjectDialogOpen} onClose={() => setIsProjectDialogOpen(false)} fullWidth maxWidth="sm">
<DialogTitle>Yeni Proje Ekle</DialogTitle>