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}}"
|
socket="{{SOCKET}}"
|
||||||
character-set-server=utf8mb4
|
character-set-server=utf8mb4
|
||||||
collation-server=utf8mb4_unicode_ci
|
collation-server=utf8mb4_unicode_ci
|
||||||
|
log-error="{{LOG_FILE}}"
|
||||||
|
|
||||||
[client]
|
[client]
|
||||||
port={{PORT}}
|
port={{PORT}}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
error_log "{{LOG_DIR}}/nginx_error.log";
|
||||||
|
pid "{{LOG_DIR}}/nginx.pid";
|
||||||
worker_processes 1;
|
worker_processes 1;
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
}
|
}
|
||||||
http {
|
http {
|
||||||
include mime.types;
|
include "{{CONF_DIR}}/mime.types";
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
sendfile on;
|
sendfile on;
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
@@ -22,7 +24,7 @@ http {
|
|||||||
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
|
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
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') {
|
if (serviceName === 'nginx') {
|
||||||
const rootPath = path.join(app.getAppPath(), 'www')
|
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 nginxDir = path.join(binDir, 'nginx')
|
||||||
const binPath = findExecutable(nginxDir, 'nginx.exe')
|
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.' }
|
if (!binPath) return { success: false, message: 'Nginx executable bulunamadı. Lütfen indirin.' }
|
||||||
|
|
||||||
const success = await processManager.startService('nginx', binPath, ['-c', configPath])
|
// Nginx prefix = directory containing nginx.exe
|
||||||
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx dosyaları bulunamadı veya port meşgul.' }
|
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') {
|
if (serviceName === 'php') {
|
||||||
@@ -55,10 +77,10 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
const extDir = path.join(path.dirname(binPath), 'ext')
|
const extDir = path.join(path.dirname(binPath), 'ext')
|
||||||
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {
|
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).' }
|
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, '/')}`])
|
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', {
|
const configPath = await configService.generateConfig('my.ini.template', 'my.ini', {
|
||||||
PORT: settings.mysqlPort,
|
PORT: settings.mysqlPort,
|
||||||
DATADIR: dataDir,
|
DATADIR: dataDir.replace(/\\/g, '/'),
|
||||||
SOCKET: '/tmp/mysql.sock'
|
SOCKET: '/tmp/mysql.sock',
|
||||||
|
LOG_FILE: logFile.replace(/\\/g, '/')
|
||||||
})
|
})
|
||||||
|
|
||||||
const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath])
|
const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath], false)
|
||||||
return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL dosyaları bulunamadı veya port meşgul.' }
|
return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL devreye alınamadı (Hata loglarını kontrol edin).' }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, message: 'Bilinmeyen servis.' }
|
return { success: false, message: 'Bilinmeyen servis.' }
|
||||||
|
|||||||
@@ -63,24 +63,34 @@ export class ProcessManager {
|
|||||||
return this.logs.get(name) || []
|
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) => {
|
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.stdout?.on('data', (data) => {
|
||||||
child.stderr?.on('data', (data) => console.error(`[INIT ERROR] ${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) => {
|
child.on('close', (code) => {
|
||||||
resolve(code === 0)
|
resolve(code === 0)
|
||||||
})
|
})
|
||||||
child.on('error', (err) => {
|
child.on('error', (err) => {
|
||||||
console.error('RunOnce error:', err)
|
console.error('RunOnce error:', err)
|
||||||
|
if (name) this.addLog(name, `RunOnce Error: ${err.message}`)
|
||||||
resolve(false)
|
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)) {
|
if (this.processes.has(name)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -95,7 +105,7 @@ export class ProcessManager {
|
|||||||
|
|
||||||
const child = spawn(binPath, args, {
|
const child = spawn(binPath, args, {
|
||||||
detached: true,
|
detached: true,
|
||||||
shell: true,
|
shell: shell,
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
cwd: path.dirname(binPath)
|
cwd: path.dirname(binPath)
|
||||||
@@ -114,6 +124,9 @@ export class ProcessManager {
|
|||||||
|
|
||||||
child.on('exit', (code) => {
|
child.on('exit', (code) => {
|
||||||
this.addLog(name, `Exited with code ${code}`)
|
this.addLog(name, `Exited with code ${code}`)
|
||||||
|
if (code !== 0) {
|
||||||
|
this.checkErrorLogs(name)
|
||||||
|
}
|
||||||
this.processes.delete(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 {
|
stopAll(): void {
|
||||||
for (const name of this.processes.keys()) {
|
for (const name of this.processes.keys()) {
|
||||||
this.stopService(name)
|
this.stopService(name)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const api = {
|
|||||||
startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name),
|
startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name),
|
||||||
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
|
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
|
||||||
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
|
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),
|
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
|
||||||
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config),
|
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config),
|
||||||
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
|
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
|
||||||
|
|||||||
+121
-1
@@ -55,7 +55,9 @@ import {
|
|||||||
Save as SaveIcon,
|
Save as SaveIcon,
|
||||||
LanguageOutlined as WebIcon,
|
LanguageOutlined as WebIcon,
|
||||||
Terminal as TerminalIcon,
|
Terminal as TerminalIcon,
|
||||||
ContentCopy as CopyIcon
|
ContentCopy as CopyIcon,
|
||||||
|
Close as CloseIcon,
|
||||||
|
ContentCopy as ContentCopyIcon
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@@ -232,6 +234,13 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [logDialogOpen, selectedService])
|
}, [logDialogOpen, selectedService])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logDialogOpen) {
|
||||||
|
const end = document.getElementById('log-end')
|
||||||
|
if (end) end.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}, [logs, logDialogOpen])
|
||||||
|
|
||||||
const handleSelectPath = async () => {
|
const handleSelectPath = async () => {
|
||||||
if (window.api && window.api.selectDirectory) {
|
if (window.api && window.api.selectDirectory) {
|
||||||
const path = await window.api.selectDirectory()
|
const path = await window.api.selectDirectory()
|
||||||
@@ -735,6 +744,117 @@ function App(): JSX.Element {
|
|||||||
{notification.message}
|
{notification.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</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">
|
<Dialog open={isProjectDialogOpen} onClose={() => setIsProjectDialogOpen(false)} fullWidth maxWidth="sm">
|
||||||
<DialogTitle>Yeni Proje Ekle</DialogTitle>
|
<DialogTitle>Yeni Proje Ekle</DialogTitle>
|
||||||
|
|||||||
Reference in New Issue
Block a user