From b751895fb1a9d19d35002a0f0c782ed46fe63927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Tue, 31 Mar 2026 00:09:10 +0300 Subject: [PATCH] feat: implement IPC handlers for Nginx and Apache service management and project configuration generation --- .gitignore | 1 + src/main/ipc/index.ts | 93 +++++++++++++--- src/main/services/PhpManagerService.ts | 10 +- src/renderer/src/App.tsx | 105 +++++++++++++++++- .../src/components/ApacheCodeEditor.tsx | 57 ++++++++++ 5 files changed, 245 insertions(+), 21 deletions(-) create mode 100644 src/renderer/src/components/ApacheCodeEditor.tsx diff --git a/.gitignore b/.gitignore index 24a8876..50d760f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ bin/ *.exe *.dll projects.json +www/ diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 94081b7..da6b9a6 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -290,10 +290,16 @@ export function registerIpcHandlers(): void { if (fs.existsSync(localIniPath)) { const localIniContent = fs.readFileSync(localIniPath, 'utf8') - // 1. Get extensions + // 1. Get extensions (and deduplicate them) const extMatches = localIniContent.match(/^\s*extension\s*=.*$/gm) if (extMatches) { - extensionsList = extMatches.join('\n') + const uniqueExts = new Set() + 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 @@ -658,38 +664,62 @@ export function registerIpcHandlers(): void { ipcMain.handle('projects:add', async (_event, project) => { const result = await projectService.addProject(project) - // Trigger Nginx restart to generate initial config and apply - await restartNginx() + if (project.serverType === 'apache') { + await restartApache() + } else { + await restartNginx() + } return result }) ipcMain.handle('projects:remove', async (_event, id) => { - const result = projectService.removeProject(id) - // Trigger Nginx restart to remove config from glob inclusion indirectly - // Actually we might want to delete the .conf file too const projects = await projectService.getProjects() const projectToRemove = projects.find(p => p.id === id) + const result = projectService.removeProject(id) + if (projectToRemove) { - const configPath = path.join(configService.getBasePath(), 'generated_configs', 'projects', `${projectToRemove.host}.conf`) - if (fs.existsSync(configPath)) fs.unlinkSync(configPath) + 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() + } } - await restartNginx() return result }) ipcMain.handle('projects:update', async (_event, project) => { - // Clean up old config file if host changed - const projects = projectService.getProjects() + // 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) { - const oldConfigPath = path.join(configService.getBasePath(), 'generated_configs', 'projects', `${oldProject.host}.conf`) + 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) - // Trigger Nginx restart - await restartNginx() + + 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 }) @@ -737,6 +767,37 @@ export function registerIpcHandlers(): void { } }) + // 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') + fs.writeFileSync(configPath, content) + 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`) diff --git a/src/main/services/PhpManagerService.ts b/src/main/services/PhpManagerService.ts index 46a3b90..1500f97 100644 --- a/src/main/services/PhpManagerService.ts +++ b/src/main/services/PhpManagerService.ts @@ -219,9 +219,10 @@ export class PhpManagerService { // Keep the same format as found if possible, but for enabled we enforce a clean one // For Windows, extension=php_name.dll is safest even in newer versions, // but extension=name is cleaner. We'll use a slightly smarter approach: - let extValue = extName + let cleanExt = extName.replace(/^php_/i, '').replace(/\.dll$/i, '') + let extValue = cleanExt if (version.startsWith('5.') || version.startsWith('7.')) { - extValue = `php_${extName}.dll` + extValue = `php_${cleanExt}.dll` } return enabled ? `extension=${extValue}` : `;extension=${extValue}` } @@ -229,9 +230,10 @@ export class PhpManagerService { }) if (!found && enabled) { - let extValue = extName + let cleanExt = extName.replace(/^php_/i, '').replace(/\.dll$/i, '') + let extValue = cleanExt if (version.startsWith('5.') || version.startsWith('7.')) { - extValue = `php_${extName}.dll` + extValue = `php_${cleanExt}.dll` } const newLine = `extension=${extValue}` const extSectionIndex = newLines.findIndex(l => l.trim().toLowerCase() === '; dynamic extensions') diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 1c71cf6..0ddf6e7 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -75,6 +75,7 @@ import { useTranslation } from 'react-i18next' import MariaDbImportWizard from './components/MariaDbImportWizard' import MariaDbExportWizard from './components/MariaDbExportWizard' import NginxCodeEditor from './components/NginxCodeEditor' +import ApacheCodeEditor from './components/ApacheCodeEditor' import PhpExtensionManager from './components/PhpExtensionManager' declare global { @@ -145,6 +146,8 @@ function App(): JSX.Element { const [isMariaDbExportWizardOpen, setIsMariaDbExportWizardOpen] = useState(false) const [isNginxConfigOpen, setIsNginxConfigOpen] = useState(false) const [nginxConfig, setNginxConfig] = useState('') + const [isApacheConfigOpen, setIsApacheConfigOpen] = useState(false) + const [apacheConfig, setApacheConfig] = useState('') const [isProjectNginxConfigOpen, setIsProjectNginxConfigOpen] = useState(false) const [projectNginxConfig, setProjectNginxConfig] = useState('') const [selectedProjectHost, setSelectedProjectHost] = useState('') @@ -515,6 +518,46 @@ function App(): JSX.Element { } } + const handleOpenApacheConfig = async () => { + if (window.api && window.api.invoke) { + const config = await window.api.invoke('apache:config:get') + setApacheConfig(config) + setIsApacheConfigOpen(true) + } + } + + const handleSaveApacheConfig = async () => { + if (window.api && window.api.invoke) { + const result = await window.api.invoke('apache:config:save', apacheConfig) + if (result.success) { + setNotification({ open: true, message: 'Apache yapılandırması kaydedildi ve yeniden yüklendi.', severity: 'success' }) + setIsApacheConfigOpen(false) + } + } + } + + const handleTestApacheConfig = async () => { + if (window.api && window.api.invoke) { + const result = await window.api.invoke('apache:config:test') + const formattedMessage = result.message + .replace(/failed/g, 'failed') + .replace(/successful/g, 'successful') + + Swal.fire({ + title: result.success ? 'Başarılı' : 'Yapılandırma Hatası', + html: ` +
+ ${formattedMessage} +
+ `, + icon: result.success ? 'success' : 'error', + background: '#1e1e1e', + color: '#fff', + width: '600px' + }) + } + } + const handleTestNginxConfig = async () => { if (window.api && window.api.invoke) { const result = await window.api.invoke('nginx:config:test') @@ -926,7 +969,12 @@ function App(): JSX.Element { )} {v.status === 'installed' && ( - + + + + )} @@ -1103,6 +1151,14 @@ function App(): JSX.Element { handleOpenLogs('apache')} title="Hata Logları" size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}> + + + + setIsApacheConfigOpen(false)} + maxWidth="md" + fullWidth + PaperProps={{ + sx: { + bgcolor: '#1e1e1e', + color: '#fff', + borderRadius: 3, + border: '1px solid rgba(255,255,255,0.1)' + } + }} + > + + Apache (httpd.conf) Yapılandırması + setIsApacheConfigOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}> + + + + + + Düzenlediğiniz bu dosya Apache sunucusunun ana (global) yapılandırma dosyasıdır. + + setApacheConfig(val)} + /> + + + + + + + + setIsPhpExtensionsOpen(false)} diff --git a/src/renderer/src/components/ApacheCodeEditor.tsx b/src/renderer/src/components/ApacheCodeEditor.tsx new file mode 100644 index 0000000..9b796b4 --- /dev/null +++ b/src/renderer/src/components/ApacheCodeEditor.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import Editor from 'react-simple-code-editor' +import Prism from 'prismjs' +import 'prismjs/components/prism-apacheconf' +import 'prismjs/themes/prism-tomorrow.css' + +interface ApacheCodeEditorProps { + value: string + onChange: (value: string) => void +} + +const ApacheCodeEditor: React.FC = ({ value, onChange }) => { + return ( +
+ onChange(code)} + highlight={code => Prism.highlight(code, Prism.languages.apacheconf, 'apacheconf')} + padding={15} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 14, + minHeight: '400px', + outline: 'none' + }} + textareaClassName="apache-editor-textarea" + preClassName="apache-editor-pre" + /> + +
+ ) +} + +export default ApacheCodeEditor