From 91b13513fcd4018621d85ae7998fdfa00a18cbf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Sat, 28 Mar 2026 16:37:16 +0300 Subject: [PATCH] feat: implement backend service management for Nginx, PHP, and MySQL with configuration templating and IPC handlers --- config/my.ini.template | 1 + config/nginx.conf.template | 6 +- src/main/ipc/index.ts | 52 ++++++++---- src/main/services/ProcessManager.ts | 47 +++++++++-- src/preload/index.ts | 2 +- src/renderer/src/App.tsx | 122 +++++++++++++++++++++++++++- 6 files changed, 206 insertions(+), 24 deletions(-) diff --git a/config/my.ini.template b/config/my.ini.template index 9a38d8b..cb18370 100644 --- a/config/my.ini.template +++ b/config/my.ini.template @@ -4,6 +4,7 @@ datadir="{{DATADIR}}" socket="{{SOCKET}}" character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci +log-error="{{LOG_FILE}}" [client] port={{PORT}} diff --git a/config/nginx.conf.template b/config/nginx.conf.template index b4e0856..16f9e16 100644 --- a/config/nginx.conf.template +++ b/config/nginx.conf.template @@ -1,9 +1,11 @@ +error_log "{{LOG_DIR}}/nginx_error.log"; +pid "{{LOG_DIR}}/nginx.pid"; worker_processes 1; events { worker_connections 1024; } http { - include mime.types; + include "{{CONF_DIR}}/mime.types"; default_type application/octet-stream; sendfile on; keepalive_timeout 65; @@ -22,7 +24,7 @@ http { fastcgi_pass 127.0.0.1:{{PHP_PORT}}; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - include fastcgi_params; + include "{{CONF_DIR}}/fastcgi_params"; } } } diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 478e00e..0f8abae 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -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.' } diff --git a/src/main/services/ProcessManager.ts b/src/main/services/ProcessManager.ts index aac164b..aad7368 100644 --- a/src/main/services/ProcessManager.ts +++ b/src/main/services/ProcessManager.ts @@ -63,24 +63,34 @@ export class ProcessManager { return this.logs.get(name) || [] } - async runOnce(binPath: string, args: string[]): Promise { + async runOnce(binPath: string, args: string[], name?: string): Promise { 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 { + async startService(name: string, binPath: string, args: string[], shell: boolean = true): Promise { 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 = { + 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) diff --git a/src/preload/index.ts b/src/preload/index.ts index 909afda..bb8aead 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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'), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 236e59b..be34a44 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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} + {/* Log Viewer Dialog */} + setLogDialogOpen(false)} + maxWidth="md" + fullWidth + PaperProps={{ + sx: { + bgcolor: '#1e1e1e', + color: '#d4d4d4', + borderRadius: 2, + boxShadow: '0 8px 32px rgba(0,0,0,0.5)' + } + }} + > + + + + + {selectedService?.toUpperCase()} - {t('diagnostics.logs', 'Servis Logları')} + + + setLogDialogOpen(false)} sx={{ color: '#666' }}> + + + + + + {logs.length === 0 ? ( + + {t('diagnostics.noLogs', 'Henüz log kaydı bulunmuyor.')} + + ) : ( + 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 ( + + + {log.match(/\[(\d+:\d+:\d+)\]/)?.[1] || ''} + + + {log.replace(/\[\d+:\d+:\d+\]\s*/, '')} + + + ) + }) + )} +
+ + + + + + + +
setIsProjectDialogOpen(false)} fullWidth maxWidth="sm"> Yeni Proje Ekle