Files
multiphp/src/main/ipc/index.ts
T

1146 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ipcMain, app, dialog, shell } from 'electron'
import fs from 'fs'
import { processManager } from '../services/ProcessManager'
import { configService } from '../services/ConfigService'
import { downloadService } from '../services/DownloadService'
import { projectService } from '../services/ProjectService'
import { phpManagerService } from '../services/PhpManagerService'
import { mariaDbManagerService } from '../services/MariaDbManagerService'
import { nginxManagerService } from '../services/NginxManagerService'
import { apacheManagerService } from '../services/ApacheManagerService'
import { phpMyAdminService } from '../services/PhpMyAdminService'
import { portMappingService } from '../services/PortMappingService'
import path from 'path'
import { mariaDbOperationService } from '../services/MariaDbOperationService'
import { cliAliasService } from '../services/CliAliasService'
import { cliBinService } from '../services/CliBinService'
function findExecutable(dir: string, name: string): string | null {
if (!fs.existsSync(dir)) return null
const files = fs.readdirSync(dir)
for (const file of files) {
const fullPath = path.join(dir, file)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
const found = findExecutable(fullPath, name)
if (found) return found
} else if (file.toLowerCase() === name.toLowerCase()) {
return fullPath
}
}
return null
}
async function rebuildNginxConfig(forceProjects: boolean = false): Promise<{ binPath: string; nginxPrefix: string; configPath: string }> {
const settings = configService.getSettings()
const binDir = path.join(configService.getBasePath(), 'bin')
const nginxDir = path.join(binDir, 'nginx')
const nginxVersions = fs.readdirSync(nginxDir).filter(f => fs.statSync(path.join(nginxDir, f)).isDirectory())
const activeVersion = nginxVersions[0]
if (!activeVersion) throw new Error('Nginx sürümü bulunamadı.')
const nginxPrefix = path.join(nginxDir, activeVersion)
const isWindows = process.platform === 'win32';
const binPath = path.join(nginxPrefix, isWindows ? 'nginx.exe' : 'nginx')
const confDir = path.join(nginxPrefix, 'conf')
const logDir = path.join(configService.getBasePath(), 'logs')
const rootPath = path.join(configService.getBasePath(), 'www')
if (!fs.existsSync(rootPath)) fs.mkdirSync(rootPath, { recursive: true })
// Ensure directories exist
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects')
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
const tempDirs = ['temp', 'temp/client_body_temp', 'temp/proxy_temp', 'temp/fastcgi_temp', 'temp/uwsgi_temp', 'temp/scgi_temp', 'logs']
for (const dir of tempDirs) {
const fullDir = path.join(nginxPrefix, dir)
if (!fs.existsSync(fullDir)) fs.mkdirSync(fullDir, { recursive: true })
}
// Generate project configs
const allProjects = await projectService.getProjects()
const projects = allProjects.filter(p => !p.serverType || p.serverType === 'nginx')
for (const p of projects) {
const projectConfPath = path.join(projectsConfDir, `${p.host}.conf`)
let shouldGenerate = forceProjects || !fs.existsSync(projectConfPath)
// If file exists, check if it was manually edited
if (!shouldGenerate && fs.existsSync(projectConfPath)) {
const currentContent = fs.readFileSync(projectConfPath, 'utf8')
if (!currentContent.includes('# MANUAL_EDIT')) {
// Overwrite old, broken configs that don't have the marker
shouldGenerate = true
}
}
if (shouldGenerate) {
const phpPort = portMappingService.getPhpPort(p.phpVersion)
// Ensure path ends with a slash for alias compatibility
let normalizedPath = p.path.replace(/\\/g, '/')
if (!normalizedPath.endsWith('/')) {
normalizedPath += '/'
}
await configService.generateConfig('nginx_project.conf.template', `projects/${p.host}.conf`, {
HOST: p.host,
PATH: normalizedPath,
PHP_PORT: phpPort.toString(),
CONF_DIR: confDir.replace(/\\/g, '/')
})
}
}
const projectsInclusion = projects.length > 0
? `include "${projectsConfDir.replace(/\\/g, '/')}/*.conf";`
: ''
const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', {
PORT: settings.nginxPort,
LISTEN_ADDRESS: settings.allowRemoteAccess ? '0.0.0.0' : '127.0.0.1',
PHP_PORT: settings.phpPort,
ROOT: rootPath,
LOG_DIR: logDir.replace(/\\/g, '/'),
CONF_DIR: confDir.replace(/\\/g, '/'),
PHPMYADMIN_DIR: phpMyAdminService.getPath().replace(/\\/g, '/'),
PROJECTS: projectsInclusion
})
return { binPath, nginxPrefix, configPath }
}
async function restartNginx(): Promise<{ success: boolean; message: string }> {
try {
const { binPath, nginxPrefix, configPath } = await rebuildNginxConfig(false)
// Force kill Nginx to ensure it picks up new config on Windows
await processManager.forceKillAll('nginx')
await processManager.stopService('nginx')
// Give OS time to release the port
await new Promise(resolve => setTimeout(resolve, 1000))
// Start service
const success = await processManager.startService('nginx', binPath, ['-p', nginxPrefix, '-c', configPath], false)
if (success) {
return { success: true, message: 'Nginx restart successful' }
} else {
const errorLogs = await processManager.getLastErrorLogs('nginx')
return { success: false, message: `Nginx restart failed:\n${errorLogs.join('\n')}` }
}
} catch (error: any) {
return { success: false, message: `Restart hatası: ${error.message}` }
}
}
async function reloadNginx(): Promise<{ success: boolean; message: string }> {
try {
const { binPath, nginxPrefix, configPath } = await rebuildNginxConfig(false)
const result = await processManager.runOnce(binPath, ['-p', nginxPrefix, '-c', configPath, '-s', 'reload'], 'nginx')
if (result.success) {
console.log('Nginx reload successful')
return { success: true, message: 'Nginx reloaded' }
} else {
// Fallback to full restart if reload fails (common on Windows)
return await restartNginx()
}
} catch (error: any) {
return { success: false, message: `Reload hatası: ${error.message}` }
}
}
async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ binPath: string; apacheDir: string; configPath: string }> {
const settings = configService.getSettings()
const binDir = path.join(configService.getBasePath(), 'bin')
const apacheDir = path.join(binDir, 'apache')
const isWindows = process.platform === 'win32';
const binPath = findExecutable(apacheDir, isWindows ? 'httpd.exe' : 'httpd')
if (!binPath) throw new Error('Apache executable bulunamadı.')
const apacheRoot = path.dirname(path.dirname(binPath)) // bin'in üstü
const logDir = path.join(configService.getBasePath(), 'logs')
const rootPath = path.join(configService.getBasePath(), 'www')
if (!fs.existsSync(rootPath)) fs.mkdirSync(rootPath, { recursive: true })
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'httpd.conf')
// Check if manual edit exists for global config
let skipGlobal = false
if (fs.existsSync(configPath) && !forceProjects) {
const content = fs.readFileSync(configPath, 'utf8')
if (content.includes('# MANUAL_EDIT')) {
console.log('Skipping Apache global config generation (Manual Edit detected)')
skipGlobal = true
}
}
// Ensure directories exist
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects_apache')
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
// Generate project configs
const allProjects = await projectService.getProjects()
const projects = allProjects.filter(p => p.serverType === 'apache')
for (const p of projects) {
const projectConfPath = path.join(projectsConfDir, `${p.host}.conf`)
let shouldGenerate = forceProjects || !fs.existsSync(projectConfPath)
// If file exists, check if it was manually edited (though we don't have a UI for this yet, good for future)
if (!shouldGenerate && fs.existsSync(projectConfPath)) {
const currentContent = fs.readFileSync(projectConfPath, 'utf8')
if (!currentContent.includes('# MANUAL_EDIT')) {
shouldGenerate = true
}
}
if (shouldGenerate) {
const phpPort = portMappingService.getPhpPort(p.phpVersion)
let normalizedPath = p.path.replace(/\\/g, '/')
if (!normalizedPath.endsWith('/')) normalizedPath += '/'
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${p.phpVersion}`)
const phpCgiBinary = findExecutable(phpDir, process.platform === 'win32' ? 'php-cgi.exe' : 'php-cgi') || ''
await configService.generateConfig('apache_project.conf.template', `projects_apache/${p.host}.conf`, {
HOST: p.host,
PATH: normalizedPath,
PHP_PORT: phpPort.toString(),
PHP_DIR: phpDir.replace(/\\/g, '/'),
PHP_CGI_PATH: phpCgiBinary.replace(/\\/g, '/'),
PHP_BIN_URL: `/php-bin-${p.host}`
})
}
}
if (!skipGlobal) {
const projectsInclusion = projects.length > 0
? `Include "${projectsConfDir.replace(/\\/g, '/')}/*.conf"`
: ''
await configService.generateConfig('httpd.conf.template', 'httpd.conf', {
PORT: settings.apachePort || '8080',
LISTEN_ADDRESS: settings.allowRemoteAccess ? '0.0.0.0' : '127.0.0.1',
PHP_PORT: settings.phpPort,
ROOT: rootPath.replace(/\\/g, '/'),
SRVROOT: apacheRoot.replace(/\\/g, '/'),
LOG_DIR: logDir.replace(/\\/g, '/'),
PHPMYADMIN_DIR: phpMyAdminService.getPath().replace(/\\/g, '/'),
PROJECTS: projectsInclusion
})
}
return { binPath, apacheDir, configPath }
}
async function restartApache(): Promise<{ success: boolean; message: string }> {
try {
const { binPath, configPath } = await rebuildApacheConfig(false)
const settings = configService.getSettings()
await processManager.forceKillAll('apache')
await processManager.stopService('apache')
// Force kill port to be absolutely sure
await processManager.killByPort('apache', settings.apachePort || '8080')
await new Promise(resolve => setTimeout(resolve, 1000))
const success = await processManager.startService('apache', binPath, ['-f', configPath], false)
if (success) {
return { success: true, message: 'Apache restart successful' }
} else {
const errorLogs = await processManager.getLastErrorLogs('apache')
return { success: false, message: `Apache restart failed:\n${errorLogs.join('\n')}` }
}
} catch (error: any) {
return { success: false, message: `Restart hatası: ${error.message}` }
}
}
export function registerIpcHandlers(): void {
ipcMain.handle('mariadb:processlist', async (_event, versionId) => {
try {
return await mariaDbManagerService.getProcessList(versionId)
} catch (error: any) {
console.error('Failed to get process list:', error)
return []
}
})
ipcMain.handle('mariadb:kill-process', async (_event, { versionId, processId }) => {
try {
await mariaDbManagerService.killProcess(versionId, processId)
return { success: true }
} catch (error: any) {
console.error('Failed to kill process:', error)
return { success: false, error: error.message }
}
})
// CLI & Environment
ipcMain.handle('cli:get-config', async () => {
const settings = configService.getSettings()
return {
enabled: settings.cliEnvEnabled !== false,
defaultPhp: settings.defaultPhpVersion,
defaultMariaDb: settings.defaultMariaDbVersion,
binDir: cliBinService.getBinDir(),
aliases: cliBinService.getAllAliases()
}
})
ipcMain.handle('cli:set-config', async (_event, config: any) => {
const settings = configService.getSettings()
configService.updateSettings({
...settings,
cliEnvEnabled: config.enabled,
defaultPhpVersion: config.defaultPhp,
defaultMariaDbVersion: config.defaultMariaDb
})
await cliAliasService.generateAliases()
return { success: true }
})
ipcMain.handle('cli:sync', async () => {
await cliAliasService.generateAliases()
return { success: true }
})
// Service management
ipcMain.handle('services:reload', async (_event, serviceName: string) => {
if (serviceName === 'nginx') return await reloadNginx()
if (serviceName === 'apache') return await restartApache() // Apache doesn't have a simple reload here
return { success: false, message: 'Bu servis için reload desteklenmiyor.' }
})
ipcMain.handle('services:start', async (_event, serviceName: string) => {
try {
const settings = configService.getSettings()
const binDir = path.join(configService.getBasePath(), 'bin')
if (serviceName === 'nginx') {
const { binPath, nginxPrefix, configPath } = await rebuildNginxConfig(false)
const success = await processManager.startService('nginx', binPath, ['-p', nginxPrefix, '-c', configPath], false)
if (!success) {
const errorLogs = await processManager.getLastErrorLogs('nginx')
return { success: false, message: `Nginx başlatılamadı.\n\n${errorLogs.join('\n')}` }
}
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx devreye alınamadı.' }
}
if (serviceName === 'apache') {
const { binPath, configPath } = await rebuildApacheConfig(false)
const settings = configService.getSettings()
// Ensure port is free
await processManager.killByPort('apache', settings.apachePort || '8080')
const success = await processManager.startService('apache', binPath, ['-f', configPath], false)
if (!success) {
const errorLogs = await processManager.getLastErrorLogs('apache')
return { success: false, message: `Apache başlatılamadı.\n\n${errorLogs.join('\n')}` }
}
return { success, message: success ? 'Apache başlatıldı.' : 'Apache devreye alınamadı.' }
}
if (serviceName.startsWith('php')) {
const versionMatch = serviceName.match(/^php:(.+)$/)
const phpVersion = versionMatch ? versionMatch[1] : null
const phpId = phpVersion ? `php-${phpVersion}` : null
const isWindows = process.platform === 'win32';
const phpDir = phpId ? path.join(binDir, phpId) : binDir
const phpBinary = isWindows ? 'php-cgi.exe' : 'php-cgi';
const binPath = findExecutable(phpDir, phpBinary)
if (!binPath) return { success: false, message: `${phpVersion || ''} PHP executable bulunamadı. Lütfen indirin.` }
const phpPort = phpVersion ? portMappingService.getPhpPort(phpVersion) : settings.phpPort
const extDir = path.join(path.dirname(binPath), 'ext').replace(/\\/g, '/')
const gdExtName = fs.existsSync(path.join(extDir, 'php_gd2.dll')) ? 'gd2' : 'gd'
const sessionDir = path.join(configService.getBasePath(), 'data', 'sessions').replace(/\\/g, '/')
const tempDir = path.join(configService.getBasePath(), 'data', 'temp').replace(/\\/g, '/')
if (!fs.existsSync(sessionDir)) fs.mkdirSync(sessionDir, { recursive: true })
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true })
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// --- 1. Get extensions from local php.ini ---
let extensionsList = ''
let errorReporting = 'E_ALL'
let displayErrors = 'On'
const localIniPath = path.join(phpDir, 'php.ini')
const mustHaves = ['curl', 'mbstring', 'openssl', 'mysqli', 'pdo_mysql', 'gd', 'fileinfo', 'exif', 'intl', 'bz2', 'gettext', 'soap', 'xsl']
if (fs.existsSync(localIniPath)) {
const localIniContent = fs.readFileSync(localIniPath, 'utf8')
// 1. Get extensions (and deduplicate them)
const extMatches = localIniContent.match(/^\s*extension\s*=.*$/gm)
if (extMatches) {
const uniqueExts = new Set<string>()
extMatches.forEach(match => {
// Extract just the dll name or extension name to prevent duplicates
const extValue = match.split('=')[1].trim()
uniqueExts.add(extValue)
})
extensionsList = Array.from(uniqueExts).map(ext => `extension=${ext}`).join('\n')
}
// 2. Get error settings
const erMatch = localIniContent.match(/^\s*error_reporting\s*=\s*(.+)$/m)
if (erMatch) errorReporting = erMatch[1].trim()
const deMatch = localIniContent.match(/^\s*display_errors\s*=\s*(.+)$/m)
if (deMatch) displayErrors = deMatch[1].trim()
} else {
// Fallback formatting based on version
const prefix = (phpVersion?.startsWith('5.') || phpVersion?.startsWith('7.')) ? 'php_' : ''
const suffix = (phpVersion?.startsWith('5.') || phpVersion?.startsWith('7.')) ? '.dll' : ''
extensionsList = mustHaves.map(m => `extension=${prefix}${m}${suffix}`).join('\n')
}
const configPath = await configService.generateConfig('php.ini.template', `php_${phpVersion || 'default'}.ini`, {
EXT_DIR: extDir,
GD_EXT: gdExtName,
SESSION_DIR: sessionDir,
TEMP_DIR: tempDir,
EXTENSIONS: extensionsList,
ERROR_REPORTING: errorReporting,
DISPLAY_ERRORS: displayErrors
})
// Add a small delay to ensure previous instance (if any) is fully killed
await sleep(1000)
const success = await processManager.startService(serviceName, binPath, ['-b', `127.0.0.1:${phpPort}`, '-c', configPath], false)
if (!success) {
const errorLogs = await processManager.getLastErrorLogs(serviceName)
return { success: false, message: `${phpVersion || ''} PHP başlatılamadı.\n\n${errorLogs.join('\n')}` }
}
return { success, message: success ? `${phpVersion || ''} PHP başlatıldı.` : `${phpVersion || ''} PHP devreye alınamadı.` }
}
if (serviceName.startsWith('mariadb') || serviceName.startsWith('mysql')) {
const versionMatch = serviceName.match(/^(?:mariadb|mysql):(.+)$/)
const dbVersion = versionMatch ? versionMatch[1] : null
const dbId = dbVersion ? `mariadb-${dbVersion}` : 'mariadb-default'
const dataDir = path.join(configService.getBasePath(), 'data', dbId)
const dbBinDir = dbVersion ? path.join(binDir, `mariadb-${dbVersion}`) : binDir
const isWindows = process.platform === 'win32';
const dbBinary = isWindows ? 'mariadbd.exe' : 'mariadbd';
const installBinary = isWindows ? 'mariadb-install-db.exe' : 'mariadb-install-db';
const mysqlInstallBinary = isWindows ? 'mysql_install_db.exe' : 'mysql_install_db';
let binPath = findExecutable(dbBinDir, dbBinary) || findExecutable(dbBinDir, isWindows ? 'mysqld.exe' : 'mysqld');
let installPath = findExecutable(dbBinDir, installBinary) || findExecutable(dbBinDir, mysqlInstallBinary);
if (!binPath) return { success: false, message: 'MariaDB executable bulunamadı. Lütfen indirin.' }
const dbVersions = await mariaDbManagerService.getVersions()
const dbInfo = dbVersions.find(v => v.id === `mariadb-${dbVersion}`)
const dbPort = dbInfo?.port || (dbVersion ? portMappingService.getMariaDbPort(dbVersion).toString() : settings.mariaDbPort)
// Force kill this specific version if running or generic mysql
await processManager.forceKillAll(serviceName)
// Also kill anything else listening on the same port
await processManager.killByPort(serviceName, dbPort)
// Initialize if data directory is empty or missing system tables
const systemDbDir = path.join(dataDir, 'mysql')
if (!fs.existsSync(dataDir) || fs.readdirSync(dataDir).length === 0 || !fs.existsSync(systemDbDir)) {
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
console.log(`Initializing MariaDB data directory for ${serviceName}...`)
try {
const normalizedDataDir = dataDir.replace(/\\/g, '/')
if (installPath) {
console.log(`Using install tool: ${installPath}`)
// MariaDB specific installation
await processManager.runOnce(installPath, [
`--datadir=${normalizedDataDir}`
])
} else {
console.log(`Using server init: ${binPath}`)
// MySQL 5.7+ / modern MariaDB fallback
await processManager.runOnce(binPath, ['--initialize-insecure', `--datadir=${normalizedDataDir}`])
}
} catch (err) {
console.log(`Initialization tool failed, trying template copy for ${serviceName}...`)
// Fallback: Copy default 'data' folder from binary dir if it exists
const templateDataDir = path.join(dbBinDir, 'data')
if (fs.existsSync(templateDataDir)) {
fs.cpSync(templateDataDir, dataDir, { recursive: true })
console.log(`Successfully copied template data for ${serviceName}`)
} else {
throw new Error(`MariaDB initialization failed and no template data found at ${templateDataDir}`)
}
}
}
const sanitizedServiceName = serviceName.replace(/:/g, '_')
const logFile = path.join(configService.getBasePath(), 'logs', `${sanitizedServiceName}_error.log`)
const tuning = settings.mariaDbTuning?.[dbId] || ''
const configPath = await configService.generateConfig('mariadb.ini.template', `mariadb_${dbId || 'default'}.ini`, {
PORT: dbPort,
DATADIR: dataDir.replace(/\\/g, '/'),
SOCKET: `/tmp/${sanitizedServiceName}.sock`,
LOG_FILE: logFile.replace(/\\/g, '/'),
EXTRA_CONFIG: tuning
})
const success = await processManager.startService(serviceName, binPath, ['--defaults-file=' + configPath], false)
if (success) {
// Quick health check
await new Promise(r => setTimeout(r, 1500))
if (!processManager.isServiceRunning(serviceName)) {
const errorLogs = await processManager.getLastErrorLogs(serviceName)
return { success: false, message: `${dbVersion || ''} MariaDB başlatıldı ancak çalışma hatası verdi (ExitCode: 1).\n\n${errorLogs.length > 0 ? 'Önemli Hata Logları:\n' + errorLogs.join('\n') : 'Lütfen log sekmesini kontrol edin.'}` }
}
} else {
const errorLogs = await processManager.getLastErrorLogs(serviceName)
return { success: false, message: `${dbVersion || ''} MariaDB başlatılamadı.\n\n${errorLogs.join('\n')}` }
}
return { success, message: success ? `${dbVersion || ''} MariaDB başlatıldı.` : `${dbVersion || ''} MariaDB devreye alınamadı.` }
}
return { success: false, message: 'Bilinmeyen servis.' }
} catch (error: any) {
console.error('Service start error:', error)
return { success: false, message: `Hata: ${error.message}` }
}
})
ipcMain.handle('services:stop', async (_event, serviceName: string) => {
const success = await processManager.stopService(serviceName)
return { success, message: success ? `${serviceName} durduruldu.` : `${serviceName} zaten durmuş.` }
})
ipcMain.handle('services:status', async () => {
return processManager.getStatuses()
})
ipcMain.handle('services:logs', async (_event, serviceName: string) => {
return processManager.getLogs(serviceName)
})
// Binary management
ipcMain.handle('binaries:list-php', async () => {
return phpManagerService.getVersions()
})
ipcMain.handle('binaries:download-php', async (event, id: string) => {
const versions = await phpManagerService.getVersions()
const version = versions.find(v => v.id === id)
if (!version) return { success: false, message: 'Geçersiz sürüm.' }
try {
// Signal immediate start
phpManagerService.updateProgress(id, 0, 'downloading')
event.sender.send('binaries:progress', { id, progress: 0 })
await downloadService.downloadAndExtract(version.url, id, (p) => {
phpManagerService.updateProgress(id, p)
event.sender.send('binaries:progress', { id, progress: p })
})
phpManagerService.updateProgress(id, 100, 'installed')
// Regenerate aliases after install
await cliAliasService.generateAliases()
return { success: true, message: 'İndirme tamamlandı.' }
} catch (error: any) {
phpManagerService.updateProgress(id, 0, 'available')
return { success: false, message: `Hata: ${error.message}` }
}
})
ipcMain.handle('php:extensions:get', async (_event, version: string) => {
return await phpManagerService.getExtensions(version)
})
ipcMain.handle('php:extensions:toggle', async (_event, { version, extName, enabled }) => {
const result = await phpManagerService.toggleExtension(version, extName, enabled)
if (result.success) {
const serviceName = `php:${version}`
if (processManager.isServiceRunning(serviceName)) {
await processManager.stopService(serviceName)
result.message += " (Service stopped for restart)"
}
}
return result
})
ipcMain.handle('php:config:get', async (_event, version: string) => {
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${version}`)
const iniPath = path.join(phpDir, 'php.ini')
if (fs.existsSync(iniPath)) {
return {
content: fs.readFileSync(iniPath, 'utf8'),
path: iniPath
}
}
return { content: '', path: iniPath }
})
ipcMain.handle('php:config:save', async (_event, { version, content }) => {
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${version}`)
const iniPath = path.join(phpDir, 'php.ini')
fs.writeFileSync(iniPath, content)
const serviceName = `php:${version}`
if (processManager.isServiceRunning(serviceName)) {
await processManager.stopService(serviceName)
}
return { success: true, message: 'Configuration saved.' }
})
// MariaDB Versions
ipcMain.handle('mariadb:versions', async () => {
return await mariaDbManagerService.getVersions()
})
ipcMain.handle('mariadb:download', async (event, id: string) => {
const versions = await mariaDbManagerService.getVersions()
const version = versions.find(v => v.id === id)
if (!version) return { success: false, message: 'Geçersiz MariaDB sürümü.' }
try {
mariaDbManagerService.updateProgress(id, 0, 'downloading')
event.sender.send('mariadb:progress', { id, progress: 0 })
await downloadService.downloadAndExtract(version.url, id, (p) => {
mariaDbManagerService.updateProgress(id, p)
event.sender.send('mariadb:progress', { id, progress: p })
})
mariaDbManagerService.updateProgress(id, 100, 'installed')
// Regenerate aliases after install
await cliAliasService.generateAliases()
return { success: true, message: 'MariaDB indirme tamamlandı.' }
} catch (error: any) {
mariaDbManagerService.updateProgress(id, 0, 'available')
return { success: false, message: `MariaDB hatası: ${error.message}` }
}
})
ipcMain.handle('mariadb:config:get', async (_event, versionId: string) => {
const settings = configService.getSettings()
const tuning = settings.mariaDbTuning?.[versionId] || ''
return { content: tuning }
})
ipcMain.handle('mariadb:config:save', async (_event, { versionId, content }) => {
const settings = configService.getSettings()
const mariaDbTuning = settings.mariaDbTuning || {}
mariaDbTuning[versionId] = content
configService.updateSettings({ ...settings, mariaDbTuning })
const serviceName = `mariadb:${versionId.replace('mariadb-', '')}`
if (processManager.isServiceRunning(serviceName)) {
await processManager.stopService(serviceName)
}
return { success: true, message: 'Tuning configuration saved.' }
})
ipcMain.handle('php:versions:all', async () => {
return phpManagerService.getVersions()
})
ipcMain.handle('php:config:get', async (_event, version: string) => {
const phpIniPath = path.join(configService.getBasePath(), 'generated_configs', `php_${version}.ini`)
if (fs.existsSync(phpIniPath)) {
return { content: fs.readFileSync(phpIniPath, 'utf8') }
}
return { content: '' }
})
ipcMain.handle('php:config:save', async (_event, { version, content }) => {
const phpIniPath = path.join(configService.getBasePath(), 'generated_configs', `php_${version}.ini`)
// Add manual edit marker to prevent overwriting
let finalContent = content
if (!finalContent.includes('; MANUAL_EDIT')) {
finalContent = `; MANUAL_EDIT\n${finalContent}`
}
fs.writeFileSync(phpIniPath, finalContent, 'utf8')
// Restart PHP if running
const serviceName = `php:${version}`
if (processManager.isServiceRunning(serviceName)) {
await processManager.stopService(serviceName)
// It will be started again later or manually
}
return { success: true }
})
// Apache Versions
ipcMain.handle('apache:versions', async () => {
return await apacheManagerService.getVersions()
})
ipcMain.handle('apache:download', async (event, id: string) => {
const versions = await apacheManagerService.getVersions()
const version = versions.find(v => v.id === id)
if (!version) return { success: false, message: 'Geçersiz Apache sürümü.' }
try {
apacheManagerService.updateProgress(id, 0, 'downloading')
event.sender.send('apache:progress', { id, progress: 0 })
await downloadService.downloadAndExtract(version.url, id, (p) => {
apacheManagerService.updateProgress(id, p)
event.sender.send('apache:progress', { id, progress: p })
})
apacheManagerService.updateProgress(id, 100, 'installed')
return { success: true, message: 'Apache indirme tamamlandı.' }
} catch (error: any) {
apacheManagerService.updateProgress(id, 0, 'available')
return { success: false, message: `Apache hatası: ${error.message}` }
}
})
// Nginx Versions
ipcMain.handle('nginx:versions', async () => {
return await nginxManagerService.getVersions()
})
ipcMain.handle('nginx:download', async (event, id: string) => {
const versions = await nginxManagerService.getVersions()
const version = versions.find(v => v.id === id)
if (!version) return { success: false, message: 'Geçersiz Nginx sürümü.' }
try {
nginxManagerService.updateProgress(id, 0, 'downloading')
event.sender.send('nginx:progress', { id, progress: 0 })
await downloadService.downloadAndExtract(version.url, id, (p) => {
nginxManagerService.updateProgress(id, p)
event.sender.send('nginx:progress', { id, progress: p })
})
nginxManagerService.updateProgress(id, 100, 'installed')
return { success: true, message: 'Nginx indirme tamamlandı.' }
} catch (error: any) {
nginxManagerService.updateProgress(id, 0, 'available')
return { success: false, message: `Nginx hatası: ${error.message}` }
}
})
ipcMain.handle('dialog:select-directory', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (result.canceled) return null
return result.filePaths[0]
})
ipcMain.handle('dialog:select-file', async (_event, filters: any[]) => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: filters || [
{ name: 'SQL & Archives', extensions: ['sql', 'zip', 'gz', 'tar'] }
]
})
if (result.canceled) return null
return result.filePaths[0]
})
ipcMain.handle('dialog:save-file', async (_event, filters: any[]) => {
const result = await dialog.showSaveDialog({
filters: filters || [
{ name: 'SQL File', extensions: ['sql'] },
{ name: 'Compressed SQL', extensions: ['sql.gz'] }
]
})
if (result.canceled) return null
return result.filePath
})
ipcMain.handle('shell:open-path', async (_event, pathStr: string) => {
return await shell.openPath(pathStr)
})
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
const result = await downloadService.downloadAndExtract(url, name, (p) => {
// TODO: Emit progress to renderer via webContents.send
console.log(`Download progress for ${name}: ${p}%`)
})
await cliAliasService.generateAliases()
return result
})
ipcMain.handle('cli:generate-aliases', async () => {
try {
await cliAliasService.generateAliases()
return {
success: true,
message: 'CLI aliasları başarıyla oluşturuldu.',
paths: {
powershell: configService.getGeneratedPath('aliases.ps1'),
bash: configService.getGeneratedPath('aliases.sh')
}
}
} catch (error: any) {
return { success: false, message: `Hata: ${error.message}` }
}
})
// Project management
ipcMain.handle('projects:list', async () => {
return projectService.getProjects()
})
ipcMain.handle('projects:add', async (_event, project) => {
const result = await projectService.addProject(project)
if (project.serverType === 'apache') {
await restartApache()
} else {
await restartNginx()
}
return result
})
ipcMain.handle('projects:remove', async (_event, id) => {
const projects = await projectService.getProjects()
const projectToRemove = projects.find(p => p.id === id)
const result = projectService.removeProject(id)
if (projectToRemove) {
if (projectToRemove.serverType === 'apache') {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'projects_apache', `${projectToRemove.host}.conf`)
if (fs.existsSync(configPath)) {
try { fs.unlinkSync(configPath) } catch (e) { console.error('Error deleting apache config:', e) }
}
await restartApache()
} else {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'projects', `${projectToRemove.host}.conf`)
if (fs.existsSync(configPath)) {
try { fs.unlinkSync(configPath) } catch (e) { console.error('Error deleting nginx config:', e) }
}
await restartNginx()
}
}
return result
})
ipcMain.handle('projects:update', async (_event, project) => {
// Clean up old config file if host or serverType changed
const projects = await projectService.getProjects()
const oldProject = projects.find(p => p.id === project.id)
if (oldProject && (oldProject.host !== project.host || oldProject.serverType !== project.serverType)) {
const oldTypeDir = oldProject.serverType === 'apache' ? 'projects_apache' : 'projects'
const oldConfigPath = path.join(configService.getBasePath(), 'generated_configs', oldTypeDir, `${oldProject.host}.conf`)
if (fs.existsSync(oldConfigPath)) {
try { fs.unlinkSync(oldConfigPath) } catch (e) { console.error('Error deleting old config:', e) }
}
}
const result = projectService.updateProject(project)
if (project.serverType === 'apache') {
await restartApache()
} else {
await restartNginx()
}
// If serverType changed, trigger restart for the old server as well
if (oldProject && oldProject.serverType !== project.serverType) {
if (oldProject.serverType === 'apache') await restartApache()
else await restartNginx()
}
return result
})
// Config management
ipcMain.handle('config:get', async () => {
return configService.getSettings()
})
ipcMain.handle('config:save', async (_event, config: any) => {
const settings = configService.updateSettings(config)
return { success: true, settings }
})
// Nginx Config Management
ipcMain.handle('nginx:config:get', async () => {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'nginx.conf')
if (fs.existsSync(configPath)) {
return fs.readFileSync(configPath, 'utf8')
}
return ''
})
ipcMain.handle('nginx:config:save', async (_event, content) => {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'nginx.conf')
fs.writeFileSync(configPath, content)
// Full restart after manual global save
return await restartNginx()
})
ipcMain.handle('nginx:config:test', async () => {
try {
const binDir = path.join(configService.getBasePath(), 'bin', 'nginx')
const nginxVersions = fs.readdirSync(binDir).filter(f => fs.statSync(path.join(binDir, f)).isDirectory())
const activeVersion = nginxVersions[0]
if (!activeVersion) throw new Error('Nginx not found')
const nginxPrefix = path.join(binDir, activeVersion)
const binPath = path.join(nginxPrefix, 'nginx.exe')
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'nginx.conf')
const result = await processManager.runOnce(binPath, ['-t', '-p', nginxPrefix, '-c', configPath], 'nginx')
return { success: result.success, message: result.success ? 'Configuration test successful' : result.output }
} catch (error: any) {
return { success: false, message: error.message }
}
})
// Apache Config Management
ipcMain.handle('apache:config:get', async () => {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'httpd.conf')
if (fs.existsSync(configPath)) {
return fs.readFileSync(configPath, 'utf8')
}
return ''
})
ipcMain.handle('apache:config:save', async (_event, content) => {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'httpd.conf')
// Ensure manual edit marker is present
let finalContent = content
if (!finalContent.includes('# MANUAL_EDIT')) {
finalContent = `# MANUAL_EDIT\n${finalContent}`
}
fs.writeFileSync(configPath, finalContent)
return await restartApache()
})
ipcMain.handle('apache:config:test', async () => {
try {
const binDir = path.join(configService.getBasePath(), 'bin', 'apache')
const isWindows = process.platform === 'win32';
const binPath = findExecutable(binDir, isWindows ? 'httpd.exe' : 'httpd')
if (!binPath) throw new Error('Apache executable bulunamadı.')
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'httpd.conf')
const result = await processManager.runOnce(binPath, ['-t', '-f', configPath], 'apache')
return { success: result.success, message: result.success ? 'Configuration test successful' : result.output }
} catch (error: any) {
return { success: false, message: error.message }
}
})
// Project Nginx Config
ipcMain.handle('project:nginx:get', async (_event, host) => {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'projects', `${host}.conf`)
if (fs.existsSync(configPath)) {
return fs.readFileSync(configPath, 'utf8')
}
return ''
})
ipcMain.handle('project:nginx:test', async (_event, host, content) => {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'projects', `${host}.conf`)
let originalContent = ''
if (fs.existsSync(configPath)) {
originalContent = fs.readFileSync(configPath, 'utf8')
}
try {
// 1. Save new content temporarily
fs.writeFileSync(configPath, content)
// 2. Run global test
const binDir = path.join(configService.getBasePath(), 'bin', 'nginx')
const nginxVersions = fs.readdirSync(binDir).filter(f => fs.statSync(path.join(binDir, f)).isDirectory())
const activeVersion = nginxVersions[0]
if (!activeVersion) throw new Error('Nginx sürümü bulunamadı.')
const nginxPrefix = path.join(binDir, activeVersion)
const binPath = path.join(nginxPrefix, 'nginx.exe')
const globalConfigPath = path.join(configService.getBasePath(), 'generated_configs', 'nginx.conf')
const result = await processManager.runOnce(binPath, ['-t', '-p', nginxPrefix, '-c', globalConfigPath], 'nginx')
// 3. Restore original content regardless of test result
// (The user must click "Save" to keep it)
if (originalContent) {
fs.writeFileSync(configPath, originalContent)
} else {
fs.unlinkSync(configPath)
}
return { success: result.success, message: result.success ? 'Configuration test successful' : result.output }
} catch (error: any) {
// Restore on error too
if (originalContent) fs.writeFileSync(configPath, originalContent)
return { success: false, message: error.message }
}
})
ipcMain.handle('project:nginx:save', async (_event, host, content) => {
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'projects', `${host}.conf`)
// Ensure manual edit marker is present
let finalContent = content
if (!finalContent.includes('# MANUAL_EDIT')) {
finalContent = `# MANUAL_EDIT\n${finalContent}`
}
fs.writeFileSync(configPath, finalContent)
return await restartNginx()
})
// phpMyAdmin
ipcMain.handle('pma:status', async () => {
return await phpMyAdminService.isInstalled()
})
ipcMain.handle('pma:setup', async (event, mariaDbPort: string) => {
return await phpMyAdminService.setup(mariaDbPort, (p) => {
event.sender.send('pma:progress', p)
})
})
ipcMain.handle('services:reset-data', async (_event, serviceName: string) => {
try {
await processManager.forceKillAll(serviceName)
const versionMatch = serviceName.match(/^(?:mariadb|mysql):(.+)$/)
const dbVersion = versionMatch ? versionMatch[1] : null
const dbId = dbVersion ? `mariadb-${dbVersion}` : null
if (!dbId) return { success: false, message: 'Geçersiz servis adı.' }
const dataDir = path.join(configService.getBasePath(), 'data', dbId)
if (fs.existsSync(dataDir)) {
fs.rmSync(dataDir, { recursive: true, force: true })
}
return { success: true, message: `${serviceName} verileri sıfırlandı. Tekrar başlatmayı deneyebilirsiniz.` }
} catch (error: any) {
return { success: false, message: `Sıfırlama hatası: ${error.message}` }
}
})
// MariaDB Operations
ipcMain.handle('mariadb:import-wizard:check-db', async (_event, { versionId, dbName, rootPass }) => {
const versions = await mariaDbManagerService.getVersions()
const v = versions.find(v => v.id === versionId)
if (!v) return { exists: false, error: 'MariaDB version not found' }
const binDir = path.join(configService.getBasePath(), 'bin', versionId)
const isWindows = process.platform === 'win32'
const binPath = findExecutable(binDir, isWindows ? 'mariadb.exe' : 'mariadb') || findExecutable(binDir, isWindows ? 'mysql.exe' : 'mysql')
if (!binPath) return { exists: false, error: 'mariadb/mysql executable not found' }
const exists = await mariaDbOperationService.checkDatabaseExists({
binPath,
host: '127.0.0.1',
port: v.port,
rootUser: 'root',
rootPass: rootPass || ''
}, dbName)
return { exists }
})
ipcMain.handle('mariadb:import-wizard:start', async (event, { versionId, filePath, dbName, user, pass, rootPass, overwrite }) => {
const versions = await mariaDbManagerService.getVersions()
const v = versions.find(v => v.id === versionId)
if (!v) return { success: false, message: 'MariaDB version not found' }
const binDir = path.join(configService.getBasePath(), 'bin', versionId)
const isWindows = process.platform === 'win32'
const dbBin = findExecutable(binDir, isWindows ? 'mariadb.exe' : 'mariadb') || findExecutable(binDir, isWindows ? 'mysql.exe' : 'mysql')
if (!dbBin) return { success: false, message: 'mariadb/mysql executable not found' }
const config = {
binPath: dbBin,
host: '127.0.0.1',
port: v.port,
rootUser: 'root',
rootPass: rootPass || ''
}
const log = (msg: string) => event.sender.send('mariadb:import:log', msg)
try {
if (overwrite) {
log(`Dropping existing database: ${dbName}`)
await mariaDbOperationService.dropDatabase(config, dbName)
}
log(`Creating database and user: ${dbName} / ${user}`)
const createRes = await mariaDbOperationService.createDatabaseAndUser(config, dbName, user, pass)
if (!createRes.success) return createRes
let sqlFiles: string[] = []
if (filePath.toLowerCase().endsWith('.sql')) {
sqlFiles = [filePath]
} else {
log(`Extracting archive: ${path.basename(filePath)}`)
const extractDir = path.join(app.getPath('temp'), `mariadb_import_${Date.now()}`)
sqlFiles = await mariaDbOperationService.extractArchive(filePath, extractDir)
if (sqlFiles.length === 0) return { success: false, message: 'No .sql files found in archive.' }
}
for (const sqlFile of sqlFiles) {
log(`Importing: ${path.basename(sqlFile)}`)
const importRes = await mariaDbOperationService.importSql(config, dbName, sqlFile, (msg) => log(msg))
if (!importRes.success) return importRes
}
return { success: true, message: 'Import completed successfully.' }
} catch (error: any) {
return { success: false, message: `Import error: ${error.message}` }
}
})
ipcMain.handle('mariadb:export-wizard:list-dbs', async (_event, { versionId, rootPass }) => {
const versions = await mariaDbManagerService.getVersions()
const v = versions.find(v => v.id === versionId)
if (!v) return []
const binDir = path.join(configService.getBasePath(), 'bin', versionId)
const isWindows = process.platform === 'win32'
const dbBin = findExecutable(binDir, isWindows ? 'mariadb.exe' : 'mariadb') || findExecutable(binDir, isWindows ? 'mysql.exe' : 'mysql')
if (!dbBin) return []
return await mariaDbOperationService.listDatabases({
binPath: dbBin,
host: '127.0.0.1',
port: v.port,
rootUser: 'root',
rootPass: rootPass || ''
})
})
ipcMain.handle('mariadb:export-wizard:start', async (event, { versionId, dbName, savePath, compress, rootPass }) => {
const versions = await mariaDbManagerService.getVersions()
const v = versions.find(v => v.id === versionId)
if (!v) return { success: false, message: 'MariaDB version not found' }
const binDir = path.join(configService.getBasePath(), 'bin', versionId)
const isWindows = process.platform === 'win32'
const dbBin = findExecutable(binDir, isWindows ? 'mariadb.exe' : 'mariadb') || findExecutable(binDir, isWindows ? 'mysql.exe' : 'mysql')
if (!dbBin) return { success: false, message: 'mariadb/mysql executable not found' }
const log = (msg: string) => event.sender.send('mariadb:export:log', msg)
return await mariaDbOperationService.exportSql({
binPath: dbBin,
host: '127.0.0.1',
port: v.port,
rootUser: 'root',
rootPass: rootPass || ''
}, dbName, savePath, compress, (msg) => log(msg))
})
}