feat: implement service management system for Nginx, PHP, and MySQL with IPC handlers and process orchestration
This commit is contained in:
@@ -0,0 +1 @@
|
||||
60544
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"nginxPort": "80",
|
||||
"phpPort": "9001",
|
||||
"mysqlPort": "3306",
|
||||
"mysqlPorts": {},
|
||||
"language": "tr"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user