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

605 lines
28 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 } 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 { mySqlManagerService } from '../services/MySqlManagerService'
import { nginxManagerService } from '../services/NginxManagerService'
import { phpMyAdminService } from '../services/PhpMyAdminService'
import { portMappingService } from '../services/PortMappingService'
import path from 'path'
import { mySqlOperationService } from '../services/MySqlOperationService'
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 reloadNginx(): Promise<{ success: boolean; message: string }> {
try {
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 binPath = path.join(nginxPrefix, 'nginx.exe')
const configPath = path.join(configService.getBasePath(), 'generated_configs', 'nginx.conf')
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 {
console.error('Nginx reload failed:', result.output)
return { success: false, message: result.output || 'Nginx reload failed' }
}
} catch (error: any) {
return { success: false, message: `Reload hatası: ${error.message}` }
}
}
export function registerIpcHandlers(): void {
// Service management
ipcMain.handle('services:reload', async (_event, serviceName: string) => {
if (serviceName !== 'nginx') return { success: false, message: 'Yalnızca Nginx için reload desteklenmektedir.' }
return await reloadNginx()
})
ipcMain.handle('services:start', async (_event, serviceName: string) => {
try {
const settings = configService.getSettings()
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(configService.getBasePath(), 'logs')
if (!binPath) return { success: false, message: 'Nginx executable bulunamadı. Lütfen indirin.' }
// Nginx prefix = directory containing nginx.exe
const nginxPrefix = path.dirname(binPath)
const confDir = path.join(nginxPrefix, 'conf')
// Ensure temp directories exist (Nginx needs these)
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 })
}
}
const projects = await projectService.getProjects()
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects')
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
// Clean up old project configs
const existingFiles = fs.readdirSync(projectsConfDir)
for (const file of existingFiles) {
fs.unlinkSync(path.join(projectsConfDir, file))
}
for (const p of projects) {
const phpPort = portMappingService.getPhpPort(p.phpVersion)
const normalizedPath = p.path.replace(/\\/g, '/')
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,
PHP_PORT: settings.phpPort,
ROOT: rootPath,
LOG_DIR: logDir.replace(/\\/g, '/'),
CONF_DIR: confDir.replace(/\\/g, '/'),
PHPMYADMIN_DIR: phpMyAdminService.getPath().replace(/\\/g, '/'),
PROJECTS: projectsInclusion
})
// Test config first with prefix
const testResult = await processManager.runOnce(binPath, ['-t', '-p', nginxPrefix, '-c', configPath], 'nginx')
if (!testResult.success) {
console.error('Nginx config test failed before reload:', testResult.output)
return { success: false, message: `Nginx config error: ${testResult.output}` }
}
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.startsWith('php')) {
const versionMatch = serviceName.match(/^php:(.+)$/)
const phpVersion = versionMatch ? versionMatch[1] : null
const phpId = phpVersion ? `php-${phpVersion}` : null
const phpDir = phpId ? path.join(binDir, phpId) : binDir
const binPath = findExecutable(phpDir, 'php-cgi.exe')
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 configPath = await configService.generateConfig('php.ini.template', `php_${phpVersion || 'default'}.ini`, {
EXT_DIR: extDir,
GD_EXT: gdExtName,
SESSION_DIR: sessionDir,
TEMP_DIR: tempDir
})
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('mysql')) {
const versionMatch = serviceName.match(/^mysql:(.+)$/)
const mysqlVersion = versionMatch ? versionMatch[1] : null
const mysqlId = mysqlVersion ? `mysql-${mysqlVersion}` : null
const dataDir = mysqlId
? path.join(configService.getBasePath(), 'data', mysqlId)
: path.join(configService.getBasePath(), 'data', 'default')
const mysqlDir = mysqlId ? path.join(binDir, mysqlId) : binDir
const binPath = findExecutable(mysqlDir, 'mysqld.exe')
if (!binPath) return { success: false, message: 'MySQL executable bulunamadı. Lütfen indirin.' }
const mysqlVersions = await mySqlManagerService.getVersions()
const mysqlInfo = mysqlVersions.find(v => v.id === mysqlId)
const mysqlPort = mysqlInfo?.port || (mysqlId ? '3306' : settings.mysqlPort)
// 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, mysqlPort)
// Initialize if data directory is empty
if (!fs.existsSync(dataDir) || fs.readdirSync(dataDir).length === 0) {
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
console.log(`Initializing MySQL data directory for ${serviceName}...`)
try {
// Try modern initialization first (5.7+)
await processManager.runOnce(binPath, ['--initialize-insecure', `--datadir=${dataDir.replace(/\\/g, '/')}`])
} catch (err) {
console.log(`Modern init failed, trying template copy for ${serviceName}...`)
// Fallback for 5.6: Copy default 'data' folder from binary dir if it exists
const templateDataDir = path.join(mysqlDir, 'data')
if (fs.existsSync(templateDataDir)) {
// Recursively copy template data (mysql, performance_schema, etc.)
fs.cpSync(templateDataDir, dataDir, { recursive: true })
console.log(`Successfully copied template data for ${serviceName}`)
} else {
throw new Error(`MySQL 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 configPath = await configService.generateConfig('my.ini.template', `my_${mysqlId || 'default'}.ini`, {
PORT: mysqlPort,
DATADIR: dataDir.replace(/\\/g, '/'),
SOCKET: `/tmp/${sanitizedServiceName}.sock`,
LOG_FILE: logFile.replace(/\\/g, '/')
})
const success = await processManager.startService(serviceName, binPath, ['--defaults-file=' + configPath], false)
if (success) {
// Quick health check for MySQL
await new Promise(r => setTimeout(r, 1500))
if (!processManager.isServiceRunning(serviceName)) {
const errorLogs = await processManager.getLastErrorLogs(serviceName)
return { success: false, message: `${mysqlVersion || ''} MySQL 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: `${mysqlVersion || ''} MySQL başlatılamadı.\n\n${errorLogs.join('\n')}` }
}
return { success, message: success ? `${mysqlVersion || ''} MySQL başlatıldı.` : `${mysqlVersion || ''} MySQL 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')
return { success: true, message: 'İndirme tamamlandı.' }
} catch (error: any) {
phpManagerService.updateProgress(id, 0, 'available')
return { success: false, message: `Hata: ${error.message}` }
}
})
// MySQL Versions
ipcMain.handle('mysql:versions', async () => {
return await mySqlManagerService.getVersions()
})
ipcMain.handle('mysql:download', async (event, id: string) => {
const versions = await mySqlManagerService.getVersions()
const version = versions.find(v => v.id === id)
if (!version) return { success: false, message: 'Geçersiz MySQL sürümü.' }
try {
mySqlManagerService.updateProgress(id, 0, 'downloading')
event.sender.send('mysql:progress', { id, progress: 0 })
await downloadService.downloadAndExtract(version.url, id, (p) => {
mySqlManagerService.updateProgress(id, p)
event.sender.send('mysql:progress', { id, progress: p })
})
mySqlManagerService.updateProgress(id, 100, 'installed')
return { success: true, message: 'MySQL indirme tamamlandı.' }
} catch (error: any) {
mySqlManagerService.updateProgress(id, 0, 'available')
return { success: false, message: `MySQL 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('binaries:download', async (_event, { name, url }) => {
return downloadService.downloadAndExtract(url, name, (p) => {
// TODO: Emit progress to renderer via webContents.send
console.log(`Download progress for ${name}: ${p}%`)
})
})
// Project management
ipcMain.handle('projects:list', async () => {
return projectService.getProjects()
})
ipcMain.handle('projects:add', async (_event, project) => {
const result = await projectService.addProject(project)
// Trigger Nginx reload
await ipcMain.emit('services:reload', null, 'nginx')
return result
})
ipcMain.handle('projects:remove', async (_event, id) => {
const result = projectService.removeProject(id)
// Trigger Nginx reload
await ipcMain.emit('services:reload', null, 'nginx')
return result
})
ipcMain.handle('projects:update', async (_event, project) => {
const result = projectService.updateProject(project)
// Trigger Nginx reload
await ipcMain.emit('services:reload', null, 'nginx')
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)
// Reload Nginx after save
return await reloadNginx()
})
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 }
}
})
// 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`)
fs.writeFileSync(configPath, content)
// Reload Nginx after save
return await reloadNginx()
})
// phpMyAdmin
ipcMain.handle('pma:status', async () => {
return await phpMyAdminService.isInstalled()
})
ipcMain.handle('pma:setup', async (event, mysqlPort: string) => {
return await phpMyAdminService.setup(mysqlPort, (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(/^mysql:(.+)$/)
const mysqlVersion = versionMatch ? versionMatch[1] : null
const mysqlId = mysqlVersion ? `mysql-${mysqlVersion}` : null
if (!mysqlId) return { success: false, message: 'Geçersiz servis adı.' }
const dataDir = path.join(configService.getBasePath(), 'data', mysqlId)
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}` }
}
})
ipcMain.handle('mysql:import-wizard:check-db', async (_event, { versionId, dbName, rootPass }) => {
const versions = await mySqlManagerService.getVersions()
const v = versions.find(v => v.id === versionId)
if (!v) return { exists: false, error: 'MySQL version not found' }
const binDir = path.join(configService.getBasePath(), 'bin', versionId)
const binPath = findExecutable(binDir, 'mysql.exe')
if (!binPath) return { exists: false, error: 'mysql.exe not found' }
const exists = await mySqlOperationService.checkDatabaseExists({
binPath,
host: '127.0.0.1',
port: v.port,
rootUser: 'root',
rootPass: rootPass || ''
}, dbName)
return { exists }
})
ipcMain.handle('mysql:import-wizard:start', async (event, { versionId, filePath, dbName, user, pass, rootPass, overwrite }) => {
const versions = await mySqlManagerService.getVersions()
const v = versions.find(v => v.id === versionId)
if (!v) return { success: false, message: 'MySQL version not found' }
const binDir = path.join(configService.getBasePath(), 'bin', versionId)
const mysqlBin = findExecutable(binDir, 'mysql.exe')
if (!mysqlBin) return { success: false, message: 'mysql.exe not found' }
const config = {
binPath: mysqlBin,
host: '127.0.0.1',
port: v.port,
rootUser: 'root',
rootPass: rootPass || ''
}
const log = (msg: string) => event.sender.send('mysql:import:log', msg)
try {
if (overwrite) {
log(`Dropping existing database: ${dbName}`)
await mySqlOperationService.dropDatabase(config, dbName)
}
log(`Creating database and user: ${dbName} / ${user}`)
const createRes = await mySqlOperationService.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'), `mysql_import_${Date.now()}`)
sqlFiles = await mySqlOperationService.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 mySqlOperationService.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}` }
}
})
}