feat: implement IPC service management handlers for Nginx, PHP, and MySQL and add multi-language support.

This commit is contained in:
Ümit Tunç
2026-03-28 21:47:45 +03:00
parent a411138732
commit 2a66537f86
7 changed files with 178 additions and 88 deletions
+23 -12
View File
@@ -115,32 +115,43 @@ export function registerIpcHandlers(): void {
return { success, message: success ? `${phpVersion || ''} PHP başlatıldı.` : `${phpVersion || ''} PHP devreye alınamadı.` } return { success, message: success ? `${phpVersion || ''} PHP başlatıldı.` : `${phpVersion || ''} PHP devreye alınamadı.` }
} }
if (serviceName === 'mysql') { if (serviceName.startsWith('mysql')) {
const dataDir = path.join(app.getPath('userData'), 'mysql_data') const versionMatch = serviceName.match(/^mysql:(.+)$/)
const binPath = findExecutable(binDir, 'mysqld.exe') const mysqlVersion = versionMatch ? versionMatch[1] : null
const mysqlId = mysqlVersion ? `mysql-${mysqlVersion}` : null
const dataDir = mysqlId
? path.join(app.getPath('userData'), `mysql_data_${mysqlId}`)
: path.join(app.getPath('userData'), 'mysql_data')
const mysqlDir = mysqlId ? path.join(binDir, mysqlId) : binDir
const binPath = findExecutable(mysqlDir, 'mysqld.exe')
if (!binPath) return { success: false, message: 'MySQL executable bulunamadı. Lütfen indirin.' } if (!binPath) return { success: false, message: 'MySQL executable bulunamadı. Lütfen indirin.' }
// Force kill any existing instances before starting // Force kill this specific version if running or generic mysql
await processManager.forceKillAll('mysql') // Actually processManager handles by serviceName
await processManager.forceKillAll(serviceName)
// Initialize if data directory is empty // Initialize if data directory is empty
if (!fs.existsSync(dataDir) || fs.readdirSync(dataDir).length === 0) { if (!fs.existsSync(dataDir) || fs.readdirSync(dataDir).length === 0) {
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }) if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
console.log('Initializing MySQL data directory...') console.log(`Initializing MySQL data directory for ${serviceName}...`)
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 mysqlPort = mysqlId ? (settings.mysqlPorts?.[mysqlId] || '3306') : settings.mysqlPort
const configPath = await configService.generateConfig('my.ini.template', 'my.ini', { const logFile = path.join(app.getPath('userData'), 'logs', `${serviceName}_error.log`)
PORT: settings.mysqlPort,
const configPath = await configService.generateConfig('my.ini.template', `my_${mysqlId || 'default'}.ini`, {
PORT: mysqlPort,
DATADIR: dataDir.replace(/\\/g, '/'), DATADIR: dataDir.replace(/\\/g, '/'),
SOCKET: '/tmp/mysql.sock', SOCKET: `/tmp/${serviceName}.sock`,
LOG_FILE: logFile.replace(/\\/g, '/') LOG_FILE: logFile.replace(/\\/g, '/')
}) })
const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath], false) const success = await processManager.startService(serviceName, binPath, ['--defaults-file=' + configPath], false)
return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL devreye alınamadı (Hata loglarını kontrol edin).' } return { success, message: success ? `${mysqlVersion || ''} MySQL başlatıldı.` : `${mysqlVersion || ''} MySQL devreye alınamadı.` }
} }
return { success: false, message: 'Bilinmeyen servis.' } return { success: false, message: 'Bilinmeyen servis.' }
+1
View File
@@ -10,6 +10,7 @@ export class ConfigService {
nginxPort: '80', nginxPort: '80',
phpPort: '9001', phpPort: '9001',
mysqlPort: '3306', mysqlPort: '3306',
mysqlPorts: {} as Record<string, string>,
language: 'tr' language: 'tr'
} }
+6
View File
@@ -1,6 +1,7 @@
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { app } from 'electron' import { app } from 'electron'
import { configService } from './ConfigService'
export interface MySqlVersion { export interface MySqlVersion {
id: string id: string
@@ -9,6 +10,7 @@ export interface MySqlVersion {
url: string url: string
status: 'available' | 'downloading' | 'installed' status: 'available' | 'downloading' | 'installed'
progress: number progress: number
port: string
description?: string description?: string
} }
@@ -55,6 +57,9 @@ export class MySqlManagerService {
// Working CDN Pattern: https://cdn.mysql.com/archives/mysql-MAJOR.MINOR/mysql-VERSION-winx64.zip // Working CDN Pattern: https://cdn.mysql.com/archives/mysql-MAJOR.MINOR/mysql-VERSION-winx64.zip
const url = `https://cdn.mysql.com/archives/mysql-${majorMinor}/mysql-${version}-winx64.zip` const url = `https://cdn.mysql.com/archives/mysql-${majorMinor}/mysql-${version}-winx64.zip`
const settings = configService.getSettings()
const customPort = settings.mysqlPorts?.[`mysql-${version}`] || '3306'
return { return {
id: `mysql-${version}`, id: `mysql-${version}`,
version, version,
@@ -62,6 +67,7 @@ export class MySqlManagerService {
url, url,
status: 'available', status: 'available',
progress: 0, progress: 0,
port: customPort,
description: v.status description: v.status
} }
}) })
+127 -70
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, Fragment } from 'react' import React, { useState, useEffect } from 'react'
import logo from './assets/logo.svg' import logo from './assets/logo.svg'
import { import {
Box, Box,
@@ -79,15 +79,14 @@ declare global {
const drawerWidth = 240 const drawerWidth = 240
interface ServiceStatus { interface ServiceStatus {
nginx: string [key: string]: string
php: string
mysql: string
} }
interface AppSettings { interface AppSettings {
nginxPort: string nginxPort: string
phpPort: string phpPort: string
mysqlPort: string mysqlPort: string
mysqlPorts?: Record<string, string>
language: string language: string
} }
@@ -330,20 +329,25 @@ function App(): JSX.Element {
const handleStartAll = async () => { const handleStartAll = async () => {
if (!window.api) return if (!window.api) return
const criticalServices = ['nginx', 'mysql'] const criticalServices = ['nginx']
for (const s of criticalServices) { for (const s of criticalServices) {
if (status[s] === 'stopped') setStatus(prev => ({ ...prev, [s]: 'starting' })) if (status[s] === 'stopped') setStatus(prev => ({ ...prev, [s]: 'starting' }))
} }
// Also starting each installed PHP version
const phpToStart = phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`) const phpToStart = phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`)
for (const s of phpToStart) { for (const s of phpToStart) {
if ((status[s] || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: 'starting' })) if ((status[s] || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: 'starting' }))
} }
const mysqlToStart = mySqlVersions.filter(v => v.status === 'installed').map(v => `mysql:${v.version}`)
for (const s of mysqlToStart) {
if ((status[s] || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: 'starting' }))
}
await Promise.all([ await Promise.all([
...criticalServices.filter(s => status[s] !== 'running').map(s => window.api?.startService(s)), ...criticalServices.filter(s => status[s] !== 'running').map(s => window.api?.startService(s)),
...phpToStart.filter(s => (status[s] || 'stopped') !== 'running').map(s => window.api?.startService(s)) ...phpToStart.filter(s => (status[s] || 'stopped') !== 'running').map(s => window.api?.startService(s)),
...mysqlToStart.filter(s => (status[s] || 'stopped') !== 'running').map(s => window.api?.startService(s))
]) ])
fetchStatus() fetchStatus()
@@ -352,12 +356,15 @@ function App(): JSX.Element {
const handleStopAll = async () => { const handleStopAll = async () => {
if (!window.api) return if (!window.api) return
const criticalServices = ['nginx', 'mysql'] const criticalServices = ['nginx']
const phpToStop = Object.keys(status).filter(s => s.startsWith('php:') && (status[s] === 'running' || status[s] === 'starting')) const phpToStop = Object.keys(status).filter(s => s.startsWith('php:') && (status[s] === 'running' || status[s] === 'starting'))
const mysqlToStop = Object.keys(status).filter(s => s.startsWith('mysql:') && (status[s] === 'running' || status[s] === 'starting'))
await Promise.all([ await Promise.all([
...criticalServices.filter(s => status[s] === 'running' || status[s] === 'starting').map(s => window.api?.stopService(s)), ...criticalServices.filter(s => status[s] === 'running' || status[s] === 'starting').map(s => window.api?.stopService(s)),
...phpToStop.map(s => window.api?.stopService(s)) ...phpToStop.map(s => window.api?.stopService(s)),
...mysqlToStop.map(s => window.api?.stopService(s)),
window.api?.stopService('mysql') // Also stop legacy if any
]) ])
fetchStatus() fetchStatus()
@@ -610,20 +617,35 @@ function App(): JSX.Element {
secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'} secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'}
sx={{ flexGrow: 1 }} sx={{ flexGrow: 1 }}
/> />
<Box sx={{ minWidth: 120, textAlign: 'right' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{v.status === 'available' && (
<Button variant="outlined" size="small" startIcon={<DownloadIcon />} onClick={() => handleDownloadMySql(v.id)}>
İNDİR
</Button>
)}
{v.status === 'downloading' && (
<Box sx={{ width: '100%', mt: 1 }}>
<LinearProgress variant="determinate" value={v.progress} sx={{ height: 8, borderRadius: 5 }} />
</Box>
)}
{v.status === 'installed' && ( {v.status === 'installed' && (
<Chip label="HAZIR" color="success" variant="outlined" size="small" /> <TextField
label="Port"
value={settings.mysqlPorts?.[v.id] || '3306'}
onChange={(e) => {
const newPorts = { ...(settings.mysqlPorts || {}), [v.id]: e.target.value }
setSettings({ ...settings, mysqlPorts: newPorts })
}}
variant="outlined"
size="small"
sx={{ width: 80 }}
/>
)} )}
<Box sx={{ minWidth: 120, textAlign: 'right' }}>
{v.status === 'available' && (
<Button variant="outlined" size="small" startIcon={<DownloadIcon />} onClick={() => handleDownloadMySql(v.id)}>
İNDİR
</Button>
)}
{v.status === 'downloading' && (
<Box sx={{ width: '100%', mt: 1 }}>
<LinearProgress variant="determinate" value={v.progress} sx={{ height: 8, borderRadius: 5 }} />
</Box>
)}
{v.status === 'installed' && (
<Chip label="HAZIR" color="success" variant="outlined" size="small" />
)}
</Box>
</Box> </Box>
</ListItem> </ListItem>
))} ))}
@@ -715,64 +737,99 @@ function App(): JSX.Element {
<Typography variant="h5" gutterBottom sx={{ mb: 3, fontWeight: 'medium' }}> <Typography variant="h5" gutterBottom sx={{ mb: 3, fontWeight: 'medium' }}>
Hızlı Bakış Hızlı Bakış
</Typography> </Typography>
<Paper elevation={4} sx={{ mb: 4, borderRadius: 3, overflow: 'hidden', bgcolor: 'rgba(255, 255, 255, 0.03)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255, 255, 255, 0.05)' }}>
<List dense> <Grid container spacing={3} sx={{ mb: 4 }}>
<ListItem sx={{ py: 1.5 }}> {/* Nginx Box */}
<ListItemIcon><WebIcon color="primary" /></ListItemIcon> <Grid item xs={12} sm={6} md={4}>
<ListItemText primary="Nginx Web Sunucusu" secondary={`Port: ${settings.nginxPort}`} /> <Paper elevation={4} sx={{
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> p: 3, borderRadius: 3,
<IconButton size="small" onClick={() => handleOpenLogs('nginx')} title="Logları Görüntüle"> bgcolor: 'rgba(255, 255, 255, 0.03)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.05)',
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.02)' }
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<WebIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status.nginx)}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>Nginx</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Port: {settings.nginxPort}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<IconButton onClick={() => handleOpenLogs('nginx')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
<TerminalIcon fontSize="small" /> <TerminalIcon fontSize="small" />
</IconButton> </IconButton>
{getStatusChip(status.nginx)}
<Switch <Switch
checked={status.nginx === 'running' || status.nginx === 'starting'} checked={status.nginx === 'running' || status.nginx === 'starting'}
onChange={() => handleToggleService('nginx')} onChange={() => handleToggleService('nginx')}
disabled={status.nginx === 'starting'} disabled={status.nginx === 'starting'}
/> />
</Box> </Box>
</ListItem> </Paper>
{installedPhpVersions.map(v => ( </Grid>
<Fragment key={v.id}>
<Divider variant="inset" /> {/* PHP Boxes */}
<ListItem sx={{ py: 1.5 }}> {installedPhpVersions.map(v => (
<ListItemIcon><PhpIcon color="primary" /></ListItemIcon> <Grid item xs={12} sm={6} md={4} key={v.id}>
<ListItemText <Paper elevation={4} sx={{
primary={`PHP ${v.version} FastCGI`} p: 3, borderRadius: 3,
secondary={`Port: ${v.port || '?'}`} bgcolor: 'rgba(255, 255, 255, 0.03)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.05)',
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.02)' }
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<PhpIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status[`php:${v.version}`] || 'stopped')}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>PHP {v.version}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Port: {v.port || '?'}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<IconButton onClick={() => handleOpenLogs(`php:${v.version}`)} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
<TerminalIcon fontSize="small" />
</IconButton>
<Switch
checked={(status[`php:${v.version}`] || 'stopped') === 'running' || status[`php:${v.version}`] === 'starting'}
onChange={() => handleToggleService(`php:${v.version}`)}
disabled={status[`php:${v.version}`] === 'starting'}
/> />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> </Box>
<IconButton size="small" onClick={() => handleOpenLogs(`php:${v.version}`)} title="Logları Görüntüle"> </Paper>
<TerminalIcon fontSize="small" /> </Grid>
</IconButton> ))}
{getStatusChip(status[`php:${v.version}`] || 'stopped')}
<Switch {/* MySQL Boxes */}
checked={(status[`php:${v.version}`] || 'stopped') === 'running' || status[`php:${v.version}`] === 'starting'} {mySqlVersions.filter(v => v.status === 'installed').map(v => (
onChange={() => handleToggleService(`php:${v.version}`)} <Grid item xs={12} sm={6} md={4} key={v.id}>
disabled={status[`php:${v.version}`] === 'starting'} <Paper elevation={4} sx={{
/> p: 3, borderRadius: 3,
</Box> bgcolor: 'rgba(255, 255, 255, 0.03)',
</ListItem> backdropFilter: 'blur(10px)',
</Fragment> border: '1px solid rgba(255, 255, 255, 0.05)',
))} transition: 'transform 0.2s',
<Divider variant="inset" /> '&:hover': { transform: 'scale(1.02)' }
<ListItem sx={{ py: 1.5 }}> }}>
<ListItemIcon><DbIcon color="primary" /></ListItemIcon> <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<ListItemText primary="MySQL / MariaDB" secondary={`Port: ${settings.mysqlPort}`} /> <DbIcon color="primary" sx={{ fontSize: 40 }} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> {getStatusChip(status[`mysql:${v.version}`] || 'stopped')}
<IconButton size="small" onClick={() => handleOpenLogs('mysql')} title="Logları Görüntüle"> </Box>
<TerminalIcon fontSize="small" /> <Typography variant="h6" sx={{ fontWeight: 'bold' }}>MySQL {v.version}</Typography>
</IconButton> <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Port: {settings.mysqlPorts?.[v.id] || '3306'}</Typography>
{getStatusChip(status.mysql)} <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Switch <IconButton onClick={() => handleOpenLogs(`mysql:${v.version}`)} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
checked={status.mysql === 'running' || status.mysql === 'starting'} <TerminalIcon fontSize="small" />
onChange={() => handleToggleService('mysql')} </IconButton>
disabled={status.mysql === 'starting'} <Switch
/> checked={(status[`mysql:${v.version}`] || 'stopped') === 'running' || status[`mysql:${v.version}`] === 'starting'}
</Box> onChange={() => handleToggleService(`mysql:${v.version}`)}
</ListItem> disabled={status[`mysql:${v.version}`] === 'starting'}
</List> />
</Paper> </Box>
</Paper>
</Grid>
))}
</Grid>
<Box sx={{ display: 'flex', gap: 2 }}> <Box sx={{ display: 'flex', gap: 2 }}>
<Button variant="contained" fullWidth startIcon={<StartIcon />} size="large" onClick={handleStartAll} sx={{ borderRadius: 2, padding: '12px', background: 'linear-gradient(45deg, #153E5E 30%, #4A7CA3 90%)', boxShadow: '0 3px 5px 2px rgba(21, 62, 94, .3)' }}> <Button variant="contained" fullWidth startIcon={<StartIcon />} size="large" onClick={handleStartAll} sx={{ borderRadius: 2, padding: '12px', background: 'linear-gradient(45deg, #153E5E 30%, #4A7CA3 90%)', boxShadow: '0 3px 5px 2px rgba(21, 62, 94, .3)' }}>
+7 -2
View File
@@ -6,12 +6,17 @@
"settings": "Einstellungen", "settings": "Einstellungen",
"tools": "Werkzeuge", "tools": "Werkzeuge",
"close": "Schließen", "close": "Schließen",
"quit": "Beenden" "quit": "Beenden",
"save": "Speichern",
"port": "Port"
}, },
"dashboard": { "dashboard": {
"title": "Trunçgil Multi-PHP Server", "title": "Trunçgil Multi-PHP Server",
"status": "Serverstatus", "status": "Serverstatus",
"services": "Dienste", "services": "Dienste",
"active_version": "Aktive PHP-Version" "active_version": "Aktive PHP-Version",
"quick_look": "Schnellansicht",
"start_all": "Alle starten",
"stop_all": "Alle stoppen"
} }
} }
+7 -2
View File
@@ -6,12 +6,17 @@
"settings": "Settings", "settings": "Settings",
"tools": "Tools", "tools": "Tools",
"close": "Close", "close": "Close",
"quit": "Quit" "quit": "Quit",
"save": "Save",
"port": "Port"
}, },
"dashboard": { "dashboard": {
"title": "Trunçgil Multi-PHP Server", "title": "Trunçgil Multi-PHP Server",
"status": "Server Status", "status": "Server Status",
"services": "Services", "services": "Services",
"active_version": "Active PHP Version" "active_version": "Active PHP Version",
"quick_look": "Quick Look",
"start_all": "Start All",
"stop_all": "Stop All"
} }
} }
+7 -2
View File
@@ -6,12 +6,17 @@
"settings": "Ayarlar", "settings": "Ayarlar",
"tools": "Araçlar", "tools": "Araçlar",
"close": "Kapat", "close": "Kapat",
"quit": "Çıkış" "quit": "Çıkış",
"save": "Kaydet",
"port": "Port"
}, },
"dashboard": { "dashboard": {
"title": "Trunçgil Multi-PHP Server", "title": "Trunçgil Multi-PHP Server",
"status": "Sunucu Durumu", "status": "Sunucu Durumu",
"services": "Servisler", "services": "Servisler",
"active_version": "Aktif PHP Sürümü" "active_version": "Aktif PHP Sürümü",
"quick_look": "Hızlı Bakış",
"start_all": "Tümünü Başlat",
"stop_all": "Tümünü Durdur"
} }
} }