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) => {
|
ipcMain.handle('services:start', async (_event, serviceName: string) => {
|
||||||
try {
|
try {
|
||||||
const settings = configService.getSettings()
|
const settings = configService.getSettings()
|
||||||
const binDir = path.join(app.getPath('userData'), 'bin')
|
const binDir = path.join(configService.getBasePath(), 'bin')
|
||||||
|
|
||||||
if (serviceName === 'nginx') {
|
if (serviceName === 'nginx') {
|
||||||
const rootPath = path.join(app.getAppPath(), 'www')
|
const rootPath = path.join(app.getAppPath(), 'www')
|
||||||
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')
|
const logDir = path.join(configService.getBasePath(), '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.' }
|
||||||
|
|
||||||
@@ -121,8 +121,8 @@ export function registerIpcHandlers(): void {
|
|||||||
const mysqlId = mysqlVersion ? `mysql-${mysqlVersion}` : null
|
const mysqlId = mysqlVersion ? `mysql-${mysqlVersion}` : null
|
||||||
|
|
||||||
const dataDir = mysqlId
|
const dataDir = mysqlId
|
||||||
? path.join(app.getPath('userData'), `mysql_data_${mysqlId}`)
|
? path.join(configService.getBasePath(), `mysql_data_${mysqlId}`)
|
||||||
: path.join(app.getPath('userData'), 'mysql_data')
|
: path.join(configService.getBasePath(), 'mysql_data')
|
||||||
|
|
||||||
const mysqlDir = mysqlId ? path.join(binDir, mysqlId) : binDir
|
const mysqlDir = mysqlId ? path.join(binDir, mysqlId) : binDir
|
||||||
const binPath = findExecutable(mysqlDir, 'mysqld.exe')
|
const binPath = findExecutable(mysqlDir, 'mysqld.exe')
|
||||||
@@ -142,7 +142,7 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
const mysqlPort = mysqlId ? (settings.mysqlPorts?.[mysqlId] || '3306') : settings.mysqlPort
|
const mysqlPort = mysqlId ? (settings.mysqlPorts?.[mysqlId] || '3306') : settings.mysqlPort
|
||||||
const sanitizedServiceName = serviceName.replace(/:/g, '_')
|
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`, {
|
const configPath = await configService.generateConfig('my.ini.template', `my_${mysqlId || 'default'}.ini`, {
|
||||||
PORT: mysqlPort,
|
PORT: mysqlPort,
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ export class ConfigService {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.templateDir = path.join(app.getAppPath(), 'config')
|
this.templateDir = path.join(app.getAppPath(), 'config')
|
||||||
this.configDir = path.join(app.getPath('userData'), 'generated_configs')
|
const base = this.getBasePath()
|
||||||
this.settingsPath = path.join(app.getPath('userData'), 'settings.json')
|
this.configDir = path.join(base, 'generated_configs')
|
||||||
|
this.settingsPath = path.join(base, 'settings.json')
|
||||||
|
|
||||||
if (!fs.existsSync(this.configDir)) {
|
if (!fs.existsSync(this.configDir)) {
|
||||||
fs.mkdirSync(this.configDir, { recursive: true })
|
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> {
|
async generateConfig(templateName: string, targetName: string, variables: Record<string, string>): Promise<string> {
|
||||||
const templatePath = path.join(this.templateDir, templateName)
|
const templatePath = path.join(this.templateDir, templateName)
|
||||||
const targetPath = path.join(this.configDir, targetName)
|
const targetPath = path.join(this.configDir, targetName)
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import axios from 'axios'
|
|||||||
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'
|
||||||
const decompress = require('decompress')
|
const decompress = require('decompress')
|
||||||
|
|
||||||
export class DownloadService {
|
export class DownloadService {
|
||||||
private binDir: string
|
private binDir: string
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Use userData for binaries to avoid permission issues in Program Files
|
// Use local base path for binaries
|
||||||
this.binDir = path.join(app.getPath('userData'), 'bin')
|
this.binDir = path.join(configService.getBasePath(), 'bin')
|
||||||
if (!fs.existsSync(this.binDir)) {
|
if (!fs.existsSync(this.binDir)) {
|
||||||
fs.mkdirSync(this.binDir, { recursive: true })
|
fs.mkdirSync(this.binDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class MySqlManagerService {
|
|||||||
private catalogPath: string
|
private catalogPath: string
|
||||||
|
|
||||||
constructor() {
|
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')
|
this.catalogPath = path.join(app.getAppPath(), 'src/main/resources/mysql_catalog.json')
|
||||||
if (!fs.existsSync(this.catalogPath)) {
|
if (!fs.existsSync(this.catalogPath)) {
|
||||||
this.catalogPath = path.join(process.cwd(), 'src/main/resources/mysql_catalog.json')
|
this.catalogPath = path.join(process.cwd(), 'src/main/resources/mysql_catalog.json')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { app } from 'electron'
|
import { configService } from './ConfigService'
|
||||||
|
|
||||||
export interface NginxVersion {
|
export interface NginxVersion {
|
||||||
id: string
|
id: string
|
||||||
@@ -15,7 +15,7 @@ export class NginxManagerService {
|
|||||||
private binDir: string
|
private binDir: string
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.binDir = path.join(app.getPath('userData'), 'bin')
|
this.binDir = path.join(configService.getBasePath(), 'bin')
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { app } from 'electron'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { configService } from './ConfigService'
|
||||||
import { portMappingService } from './PortMappingService'
|
import { portMappingService } from './PortMappingService'
|
||||||
|
|
||||||
export interface PhpVersion {
|
export interface PhpVersion {
|
||||||
@@ -22,7 +22,7 @@ export class PhpManagerService {
|
|||||||
private mainReleasesJsonUrl = 'https://www.php.net/releases/index.php?json'
|
private mainReleasesJsonUrl = 'https://www.php.net/releases/index.php?json'
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.binDir = path.join(app.getPath('userData'), 'bin')
|
this.binDir = path.join(configService.getBasePath(), 'bin')
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { app } from 'electron'
|
import { configService } from './ConfigService'
|
||||||
import { downloadService } from './DownloadService'
|
import { downloadService } from './DownloadService'
|
||||||
|
|
||||||
export class PhpMyAdminService {
|
export class PhpMyAdminService {
|
||||||
@@ -8,7 +8,7 @@ export class PhpMyAdminService {
|
|||||||
private downloadUrl = 'https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.zip'
|
private downloadUrl = 'https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.zip'
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.pmaDir = path.join(app.getPath('userData'), 'phpmyadmin')
|
this.pmaDir = path.join(configService.getBasePath(), 'phpmyadmin')
|
||||||
}
|
}
|
||||||
|
|
||||||
async isInstalled(): Promise<boolean> {
|
async isInstalled(): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { app } from 'electron'
|
import { configService } from './ConfigService'
|
||||||
|
|
||||||
interface PortMap {
|
interface PortMap {
|
||||||
php: Record<string, number>;
|
php: Record<string, number>;
|
||||||
@@ -14,7 +14,7 @@ export class PortMappingService {
|
|||||||
private nextMysqlPort = 3306
|
private nextMysqlPort = 3306
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.configFile = path.join(app.getPath('userData'), 'port_mappings.json')
|
this.configFile = path.join(configService.getBasePath(), 'port_mappings.json')
|
||||||
this.load()
|
this.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { spawn, ChildProcess } from 'child_process'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import { configService } from './ConfigService'
|
||||||
|
|
||||||
export class ProcessManager {
|
export class ProcessManager {
|
||||||
private processes: Map<string, ChildProcess> = new Map()
|
private processes: Map<string, ChildProcess> = new Map()
|
||||||
@@ -16,7 +17,7 @@ export class ProcessManager {
|
|||||||
private initLogs() {
|
private initLogs() {
|
||||||
if (this.logDir) return
|
if (this.logDir) return
|
||||||
try {
|
try {
|
||||||
this.logDir = path.join(app.getPath('userData'), 'logs')
|
this.logDir = path.join(configService.getBasePath(), 'logs')
|
||||||
if (!fs.existsSync(this.logDir)) {
|
if (!fs.existsSync(this.logDir)) {
|
||||||
fs.mkdirSync(this.logDir, { recursive: true })
|
fs.mkdirSync(this.logDir, { recursive: true })
|
||||||
}
|
}
|
||||||
@@ -152,16 +153,43 @@ export class ProcessManager {
|
|||||||
|
|
||||||
async forceKillAll(name: string): Promise<boolean> {
|
async forceKillAll(name: string): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
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'
|
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'], {
|
if (isVersionedMysql) {
|
||||||
windowsHide: true,
|
// For versioned mysql, we try to find by command line (contains data directory name)
|
||||||
shell: false
|
const mysqlId = name.replace('mysql:', 'mysql-')
|
||||||
})
|
this.addLog(name, `Attempting to kill specific MySQL instance: ${mysqlId}`)
|
||||||
|
// Using wmic to find and kill
|
||||||
child.on('close', () => resolve(true))
|
const wmic = spawn('wmic', ['process', 'where', `name='mysqld.exe' and commandline like '%mysql_data_${mysqlId}%'`, 'call', 'terminate'], {
|
||||||
child.on('error', () => resolve(true))
|
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 fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { app } from 'electron'
|
import { configService } from './ConfigService'
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
id: string
|
id: string
|
||||||
@@ -16,7 +16,7 @@ export class ProjectService {
|
|||||||
private projects: Project[] = []
|
private projects: Project[] = []
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.projectsFile = path.join(app.getPath('userData'), 'projects.json')
|
this.projectsFile = path.join(configService.getBasePath(), 'projects.json')
|
||||||
this.loadProjects()
|
this.loadProjects()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -530,16 +530,7 @@ function App(): JSX.Element {
|
|||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
{activeTab === 'settings' && (
|
{activeTab === 'settings' && (
|
||||||
<Box>
|
<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 }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||||
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">
|
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">
|
||||||
|
|||||||
Reference in New Issue
Block a user