feat: implement service management system for Nginx, PHP, and MySQL with IPC handlers and process orchestration

This commit is contained in:
Ümit Tunç
2026-03-28 22:04:21 +03:00
parent d9e1e758a6
commit 139b8df7b2
15 changed files with 90 additions and 39 deletions
+1
View File
@@ -0,0 +1 @@
60544
View File
+15
View File
@@ -0,0 +1,15 @@
{
"php": {
"8.5.4": 9001,
"8.4.19": 9002,
"8.3.30": 9003,
"8.2.30": 9004,
"8.1.34": 9005,
"8.0.30": 9006,
"7.4.33": 9007,
"5.6.40": 9008,
"4.4.9": 9009,
"3.0.x (latest)": 9010
},
"mysql": {}
}
+7
View File
@@ -0,0 +1,7 @@
{
"nginxPort": "80",
"phpPort": "9001",
"mysqlPort": "3306",
"mysqlPorts": {},
"language": "tr"
}
+5 -5
View File
@@ -32,13 +32,13 @@ export function registerIpcHandlers(): void {
ipcMain.handle('services:start', async (_event, serviceName: string) => {
try {
const settings = configService.getSettings()
const binDir = path.join(app.getPath('userData'), 'bin')
const binDir = path.join(configService.getBasePath(), 'bin')
if (serviceName === 'nginx') {
const rootPath = path.join(app.getAppPath(), 'www')
const nginxDir = path.join(binDir, 'nginx')
const binPath = findExecutable(nginxDir, 'nginx.exe')
const logDir = path.join(app.getPath('userData'), 'logs')
const logDir = path.join(configService.getBasePath(), 'logs')
if (!binPath) return { success: false, message: 'Nginx executable bulunamadı. Lütfen indirin.' }
@@ -121,8 +121,8 @@ export function registerIpcHandlers(): void {
const mysqlId = mysqlVersion ? `mysql-${mysqlVersion}` : null
const dataDir = mysqlId
? path.join(app.getPath('userData'), `mysql_data_${mysqlId}`)
: path.join(app.getPath('userData'), 'mysql_data')
? path.join(configService.getBasePath(), `mysql_data_${mysqlId}`)
: path.join(configService.getBasePath(), 'mysql_data')
const mysqlDir = mysqlId ? path.join(binDir, mysqlId) : binDir
const binPath = findExecutable(mysqlDir, 'mysqld.exe')
@@ -142,7 +142,7 @@ export function registerIpcHandlers(): void {
const mysqlPort = mysqlId ? (settings.mysqlPorts?.[mysqlId] || '3306') : settings.mysqlPort
const sanitizedServiceName = serviceName.replace(/:/g, '_')
const logFile = path.join(app.getPath('userData'), 'logs', `${sanitizedServiceName}_error.log`)
const logFile = path.join(configService.getBasePath(), 'logs', `${sanitizedServiceName}_error.log`)
const configPath = await configService.generateConfig('my.ini.template', `my_${mysqlId || 'default'}.ini`, {
PORT: mysqlPort,
+10 -2
View File
@@ -16,8 +16,9 @@ export class ConfigService {
constructor() {
this.templateDir = path.join(app.getAppPath(), 'config')
this.configDir = path.join(app.getPath('userData'), 'generated_configs')
this.settingsPath = path.join(app.getPath('userData'), 'settings.json')
const base = this.getBasePath()
this.configDir = path.join(base, 'generated_configs')
this.settingsPath = path.join(base, 'settings.json')
if (!fs.existsSync(this.configDir)) {
fs.mkdirSync(this.configDir, { recursive: true })
@@ -28,6 +29,13 @@ export class ConfigService {
}
}
getBasePath(): string {
if (app.isPackaged) {
return path.dirname(app.getPath('exe'))
}
return process.cwd()
}
async generateConfig(templateName: string, targetName: string, variables: Record<string, string>): Promise<string> {
const templatePath = path.join(this.templateDir, templateName)
const targetPath = path.join(this.configDir, targetName)
+3 -2
View File
@@ -2,14 +2,15 @@ import axios from 'axios'
import fs from 'fs'
import path from 'path'
import { app } from 'electron'
import { configService } from './ConfigService'
const decompress = require('decompress')
export class DownloadService {
private binDir: string
constructor() {
// Use userData for binaries to avoid permission issues in Program Files
this.binDir = path.join(app.getPath('userData'), 'bin')
// Use local base path for binaries
this.binDir = path.join(configService.getBasePath(), 'bin')
if (!fs.existsSync(this.binDir)) {
fs.mkdirSync(this.binDir, { recursive: true })
}
+1 -1
View File
@@ -20,7 +20,7 @@ export class MySqlManagerService {
private catalogPath: string
constructor() {
this.binDir = path.join(app.getPath('userData'), 'bin')
this.binDir = path.join(configService.getBasePath(), 'bin')
this.catalogPath = path.join(app.getAppPath(), 'src/main/resources/mysql_catalog.json')
if (!fs.existsSync(this.catalogPath)) {
this.catalogPath = path.join(process.cwd(), 'src/main/resources/mysql_catalog.json')
+2 -2
View File
@@ -1,6 +1,6 @@
import fs from 'fs'
import path from 'path'
import { app } from 'electron'
import { configService } from './ConfigService'
export interface NginxVersion {
id: string
@@ -15,7 +15,7 @@ export class NginxManagerService {
private binDir: string
constructor() {
this.binDir = path.join(app.getPath('userData'), 'bin')
this.binDir = path.join(configService.getBasePath(), 'bin')
this.init()
}
+2 -2
View File
@@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'path'
import { app } from 'electron'
import axios from 'axios'
import { configService } from './ConfigService'
import { portMappingService } from './PortMappingService'
export interface PhpVersion {
@@ -22,7 +22,7 @@ export class PhpManagerService {
private mainReleasesJsonUrl = 'https://www.php.net/releases/index.php?json'
constructor() {
this.binDir = path.join(app.getPath('userData'), 'bin')
this.binDir = path.join(configService.getBasePath(), 'bin')
this.init()
}
+2 -2
View File
@@ -1,6 +1,6 @@
import path from 'path'
import fs from 'fs'
import { app } from 'electron'
import { configService } from './ConfigService'
import { downloadService } from './DownloadService'
export class PhpMyAdminService {
@@ -8,7 +8,7 @@ export class PhpMyAdminService {
private downloadUrl = 'https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.zip'
constructor() {
this.pmaDir = path.join(app.getPath('userData'), 'phpmyadmin')
this.pmaDir = path.join(configService.getBasePath(), 'phpmyadmin')
}
async isInstalled(): Promise<boolean> {
+2 -2
View File
@@ -1,6 +1,6 @@
import fs from 'fs'
import path from 'path'
import { app } from 'electron'
import { configService } from './ConfigService'
interface PortMap {
php: Record<string, number>;
@@ -14,7 +14,7 @@ export class PortMappingService {
private nextMysqlPort = 3306
constructor() {
this.configFile = path.join(app.getPath('userData'), 'port_mappings.json')
this.configFile = path.join(configService.getBasePath(), 'port_mappings.json')
this.load()
}
+37 -9
View File
@@ -2,6 +2,7 @@ import { spawn, ChildProcess } from 'child_process'
import path from 'path'
import { app } from 'electron'
import fs from 'fs'
import { configService } from './ConfigService'
export class ProcessManager {
private processes: Map<string, ChildProcess> = new Map()
@@ -16,7 +17,7 @@ export class ProcessManager {
private initLogs() {
if (this.logDir) return
try {
this.logDir = path.join(app.getPath('userData'), 'logs')
this.logDir = path.join(configService.getBasePath(), 'logs')
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true })
}
@@ -152,16 +153,43 @@ export class ProcessManager {
async forceKillAll(name: string): Promise<boolean> {
return new Promise((resolve) => {
const childProcess = this.processes.get(name)
if (childProcess && childProcess.pid) {
this.addLog(name, `Killing process by PID: ${childProcess.pid}`)
const kill = spawn('taskkill', ['/F', '/PID', childProcess.pid.toString(), '/T'], {
windowsHide: true,
shell: false
})
kill.on('close', () => {
this.processes.delete(name)
resolve(true)
})
return
}
// Fallback: If it's a generic service name without version, or we just want to be sure
// For MySQL multi-instance, we should avoid global kill if possible.
const isVersionedMysql = name.startsWith('mysql:')
const executableName = name === 'nginx' ? 'nginx.exe' : name === 'php' ? 'php-cgi.exe' : 'mysqld.exe'
this.addLog(name, `Force killing all ${executableName} processes...`)
const child = spawn('taskkill', ['/F', '/IM', executableName, '/T'], {
windowsHide: true,
shell: false
})
child.on('close', () => resolve(true))
child.on('error', () => resolve(true))
if (isVersionedMysql) {
// For versioned mysql, we try to find by command line (contains data directory name)
const mysqlId = name.replace('mysql:', 'mysql-')
this.addLog(name, `Attempting to kill specific MySQL instance: ${mysqlId}`)
// Using wmic to find and kill
const wmic = spawn('wmic', ['process', 'where', `name='mysqld.exe' and commandline like '%mysql_data_${mysqlId}%'`, 'call', 'terminate'], {
windowsHide: true,
shell: false
})
wmic.on('close', () => resolve(true))
} else {
this.addLog(name, `Force killing all ${executableName} processes...`)
const tk = spawn('taskkill', ['/F', '/IM', executableName, '/T'], {
windowsHide: true,
shell: false
})
tk.on('close', () => resolve(true))
}
})
}
+2 -2
View File
@@ -1,6 +1,6 @@
import fs from 'fs'
import path from 'path'
import { app } from 'electron'
import { configService } from './ConfigService'
export interface Project {
id: string
@@ -16,7 +16,7 @@ export class ProjectService {
private projects: Project[] = []
constructor() {
this.projectsFile = path.join(app.getPath('userData'), 'projects.json')
this.projectsFile = path.join(configService.getBasePath(), 'projects.json')
this.loadProjects()
}
+1 -10
View File
@@ -530,16 +530,7 @@ function App(): JSX.Element {
<Container maxWidth="md">
{activeTab === 'settings' && (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5" sx={{ fontWeight: 'medium' }}>
Genel Ayarlar
</Typography>
{settingsTab < 3 && (
<Button startIcon={loadingVersions ? <CircularProgress size={20} /> : <RefreshIcon />} onClick={handleRefreshVersions} disabled={loadingVersions}>
Listeyi Yenile
</Button>
)}
</Box>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">