feat: implement backend service management for Nginx, PHP, and MySQL with configuration templating and IPC handlers
This commit is contained in:
@@ -4,6 +4,7 @@ datadir="{{DATADIR}}"
|
||||
socket="{{SOCKET}}"
|
||||
character-set-server=utf8mb4
|
||||
collation-server=utf8mb4_unicode_ci
|
||||
log-error="{{LOG_FILE}}"
|
||||
|
||||
[client]
|
||||
port={{PORT}}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-14
@@ -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.' }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user