From 139b8df7b25c04e80178b0e45ed1673cc11e9d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Sat, 28 Mar 2026 22:04:21 +0300 Subject: [PATCH] feat: implement service management system for Nginx, PHP, and MySQL with IPC handlers and process orchestration --- logs/nginx.pid | 1 + logs/php | 0 port_mappings.json | 15 ++++++++ settings.json | 7 ++++ src/main/ipc/index.ts | 10 +++--- src/main/services/ConfigService.ts | 12 +++++-- src/main/services/DownloadService.ts | 5 +-- src/main/services/MySqlManagerService.ts | 2 +- src/main/services/NginxManagerService.ts | 4 +-- src/main/services/PhpManagerService.ts | 4 +-- src/main/services/PhpMyAdminService.ts | 4 +-- src/main/services/PortMappingService.ts | 4 +-- src/main/services/ProcessManager.ts | 46 +++++++++++++++++++----- src/main/services/ProjectService.ts | 4 +-- src/renderer/src/App.tsx | 11 +----- 15 files changed, 90 insertions(+), 39 deletions(-) create mode 100644 logs/nginx.pid create mode 100644 logs/php create mode 100644 port_mappings.json create mode 100644 settings.json diff --git a/logs/nginx.pid b/logs/nginx.pid new file mode 100644 index 0000000..3529c0b --- /dev/null +++ b/logs/nginx.pid @@ -0,0 +1 @@ +60544 diff --git a/logs/php b/logs/php new file mode 100644 index 0000000..e69de29 diff --git a/port_mappings.json b/port_mappings.json new file mode 100644 index 0000000..4cfc888 --- /dev/null +++ b/port_mappings.json @@ -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": {} +} \ No newline at end of file diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..9206455 --- /dev/null +++ b/settings.json @@ -0,0 +1,7 @@ +{ + "nginxPort": "80", + "phpPort": "9001", + "mysqlPort": "3306", + "mysqlPorts": {}, + "language": "tr" +} \ No newline at end of file diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 7706560..0f123bf 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -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, diff --git a/src/main/services/ConfigService.ts b/src/main/services/ConfigService.ts index ef14871..dd5d251 100644 --- a/src/main/services/ConfigService.ts +++ b/src/main/services/ConfigService.ts @@ -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): Promise { const templatePath = path.join(this.templateDir, templateName) const targetPath = path.join(this.configDir, targetName) diff --git a/src/main/services/DownloadService.ts b/src/main/services/DownloadService.ts index 4bb6bb5..c61a553 100644 --- a/src/main/services/DownloadService.ts +++ b/src/main/services/DownloadService.ts @@ -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 }) } diff --git a/src/main/services/MySqlManagerService.ts b/src/main/services/MySqlManagerService.ts index 30de8b4..7c6fb14 100644 --- a/src/main/services/MySqlManagerService.ts +++ b/src/main/services/MySqlManagerService.ts @@ -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') diff --git a/src/main/services/NginxManagerService.ts b/src/main/services/NginxManagerService.ts index 3df9627..6aea500 100644 --- a/src/main/services/NginxManagerService.ts +++ b/src/main/services/NginxManagerService.ts @@ -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() } diff --git a/src/main/services/PhpManagerService.ts b/src/main/services/PhpManagerService.ts index d7e0aa9..677d7c5 100644 --- a/src/main/services/PhpManagerService.ts +++ b/src/main/services/PhpManagerService.ts @@ -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() } diff --git a/src/main/services/PhpMyAdminService.ts b/src/main/services/PhpMyAdminService.ts index a486257..151e3fb 100644 --- a/src/main/services/PhpMyAdminService.ts +++ b/src/main/services/PhpMyAdminService.ts @@ -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 { diff --git a/src/main/services/PortMappingService.ts b/src/main/services/PortMappingService.ts index 36cf4e2..281786e 100644 --- a/src/main/services/PortMappingService.ts +++ b/src/main/services/PortMappingService.ts @@ -1,6 +1,6 @@ import fs from 'fs' import path from 'path' -import { app } from 'electron' +import { configService } from './ConfigService' interface PortMap { php: Record; @@ -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() } diff --git a/src/main/services/ProcessManager.ts b/src/main/services/ProcessManager.ts index d25884e..f1a1fd5 100644 --- a/src/main/services/ProcessManager.ts +++ b/src/main/services/ProcessManager.ts @@ -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 = 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 { 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)) + } }) } diff --git a/src/main/services/ProjectService.ts b/src/main/services/ProjectService.ts index 12eb62a..a845699 100644 --- a/src/main/services/ProjectService.ts +++ b/src/main/services/ProjectService.ts @@ -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() } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index dfdaab9..3ed4e8e 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -530,16 +530,7 @@ function App(): JSX.Element { {activeTab === 'settings' && ( - - - Genel Ayarlar - - {settingsTab < 3 && ( - - )} - + setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">