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

498 lines
23 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
}
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.' }
try {
// Re-generate config
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] // Simplify: use first installed
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')
// For reload, we need the same prefix and config
const success = await processManager.runOnce(binPath, ['-p', nginxPrefix, '-c', configPath, '-s', 'reload'], 'nginx')
return { success, message: success ? 'Nginx yapılandırması yeniden yüklendi.' : 'Nginx reload hatası (Nginx çalışmıyor olabilir).' }
} catch (error: any) {
return { success: false, message: `Reload hatası: ${error.message}` }
}
})
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) {
return { success: false, message: 'Nginx konfigürasyon hatası! Log sekmesini kontrol edin.' }
}
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) => {
return projectService.addProject(project)
})
ipcMain.handle('projects:remove', async (_event, id) => {
return projectService.removeProject(id)
})
ipcMain.handle('projects:update', async (_event, project) => {
return projectService.updateProject(project)
})
// Config management
ipcMain.handle('config:get', async () => {
return configService.getSettings()
})
ipcMain.handle('config:save', async (_event, config: any) => {
return { success: true, settings: configService.updateSettings(config) }
})
// 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}` }
}
})
}