feat: implement Nginx and Apache service management with automated configuration generation and IPC handlers

This commit is contained in:
Ümit Tunç
2026-04-03 22:55:43 +03:00
parent a6967a7c4c
commit 58c84b3e2b
9 changed files with 83 additions and 18 deletions
+2 -1
View File
@@ -88,6 +88,7 @@
"asar": true, "asar": true,
"win": { "win": {
"icon": "build/icon.ico", "icon": "build/icon.ico",
"requestedExecutionLevel": "highestAvailable",
"target": [ "target": [
"nsis" "nsis"
] ]
@@ -101,7 +102,7 @@
"installerIcon": "build/icon.ico", "installerIcon": "build/icon.ico",
"uninstallerIcon": "build/icon.ico", "uninstallerIcon": "build/icon.ico",
"installerHeaderIcon": "build/icon.ico", "installerHeaderIcon": "build/icon.ico",
"perMachine": false, "perMachine": true,
"multiLanguageInstaller": true, "multiLanguageInstaller": true,
"installerLanguages": [ "installerLanguages": [
"tr_TR", "tr_TR",
+7
View File
@@ -1515,4 +1515,11 @@ export function registerIpcHandlers(): void {
return '' return ''
} }
}) })
ipcMain.handle('app:get-permissions-status', () => {
return {
hasError: configService.hasPermissionError,
path: configService.failedPath
}
})
} }
+44 -12
View File
@@ -21,25 +21,51 @@ export class ConfigService {
mariaDbTuning: {} as Record<string, string> mariaDbTuning: {} as Record<string, string>
} }
public hasPermissionError = false
public failedPath = ''
constructor() { constructor() {
this.templateDir = path.join(app.getAppPath(), 'config') this.templateDir = path.join(app.getAppPath(), 'config')
const base = this.getDataPath() const base = this.getDataPath()
this.configDir = path.join(base, 'generated_configs') this.configDir = path.join(base, 'generated_configs')
this.settingsPath = path.join(base, 'settings.json') this.settingsPath = path.join(base, 'settings.json')
console.log(`[ConfigService] Base data path: ${base}`)
console.log(`[ConfigService] Config dir: ${this.configDir}`)
// Ensure directories exist // Ensure directories exist
if (!fs.existsSync(base)) {
try {
fs.mkdirSync(base, { recursive: true })
} catch (e: any) {
if (e.code === 'EPERM' || e.code === 'EACCES') {
this.hasPermissionError = true
this.failedPath = base
}
console.error(`Failed to create base directory (${base}):`, e)
}
}
if (!fs.existsSync(this.configDir)) { if (!fs.existsSync(this.configDir)) {
try { try {
fs.mkdirSync(this.configDir, { recursive: true }) fs.mkdirSync(this.configDir, { recursive: true })
} catch (e) { } catch (e: any) {
console.error('Failed to create config directory:', e) if (e.code === 'EPERM' || e.code === 'EACCES') {
this.hasPermissionError = true
this.failedPath = this.configDir
}
console.error(`Failed to create config directory (${this.configDir}):`, e)
} }
} }
if (!fs.existsSync(this.settingsPath)) { if (!fs.existsSync(this.settingsPath)) {
try { try {
fs.writeFileSync(this.settingsPath, JSON.stringify(this.defaultSettings, null, 2)) fs.writeFileSync(this.settingsPath, JSON.stringify(this.defaultSettings, null, 2))
} catch (e) { } catch (e: any) {
if (e.code === 'EPERM' || e.code === 'EACCES') {
this.hasPermissionError = true
this.failedPath = this.settingsPath
}
console.error('Failed to create settings file:', e) console.error('Failed to create settings file:', e)
} }
} }
@@ -51,27 +77,33 @@ export class ConfigService {
* If not writable (e.g. Program Files), falls back to app.getPath('userData'). * If not writable (e.g. Program Files), falls back to app.getPath('userData').
*/ */
getDataPath(): string { getDataPath(): string {
if (!app.isPackaged) { const exeDir = path.dirname(app.getPath('exe'))
const isAsar = app.getAppPath().includes('app.asar')
// If not packaged and not running from asar, we are in development
if (!app.isPackaged && !isAsar) {
return process.cwd() return process.cwd()
} }
const exeDir = path.dirname(app.getPath('exe')) // We are in production/packaged mode.
// Priority 1: Do not even try to use Program Files if we have alternatives.
const winProgramFiles = (process.env.ProgramFiles || 'C:\\Program Files').toLowerCase()
const winProgramFilesX86 = (process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)').toLowerCase()
const currentExeDir = exeDir.toLowerCase()
// Check if we are in Program Files if (currentExeDir.startsWith(winProgramFiles) || currentExeDir.startsWith(winProgramFilesX86)) {
const isProgramFiles = exeDir.toLowerCase().includes('program files')
if (isProgramFiles) {
return app.getPath('userData') return app.getPath('userData')
} }
// Check for write access // Priority 2: Check for actual write access (Portable Mode check)
try { try {
const testFile = path.join(exeDir, '.write_test') const testFile = path.join(exeDir, `.write_test_${Date.now()}`)
fs.writeFileSync(testFile, 'test') fs.writeFileSync(testFile, 'test')
fs.unlinkSync(testFile) fs.unlinkSync(testFile)
return exeDir // Writable (Portable Mode) return exeDir // Writable (Portable Mode)
} catch (e) { } catch (e) {
return app.getPath('userData') // Not writable, use AppData // Not writable, use AppData
return app.getPath('userData')
} }
} }
+1
View File
@@ -57,6 +57,7 @@ const api = {
electronAPI.ipcRenderer.on('update-progress', listener) electronAPI.ipcRenderer.on('update-progress', listener)
return () => electronAPI.ipcRenderer.removeListener('update-progress', listener) return () => electronAPI.ipcRenderer.removeListener('update-progress', listener)
}, },
getPermissionsStatus: () => electronAPI.ipcRenderer.invoke('app:get-permissions-status'),
invoke: (channel: string, ...args: any[]) => electronAPI.ipcRenderer.invoke(channel, ...args) invoke: (channel: string, ...args: any[]) => electronAPI.ipcRenderer.invoke(channel, ...args)
} }
+18
View File
@@ -318,6 +318,23 @@ function App(): JSX.Element {
} }
} }
const checkPermissions = async () => {
if (window.api && window.api.getPermissionsStatus) {
const status = await window.api.getPermissionsStatus()
if (status.hasError) {
Swal.fire({
title: t('common.warning'),
text: t('common.admin_required'),
icon: 'warning',
confirmButtonText: t('common.ok'),
confirmButtonColor: '#3085d6',
background: '#1a1d21',
color: '#fff'
})
}
}
}
fetchStatus() fetchStatus()
fetchProjects() fetchProjects()
fetchPhpVersions() fetchPhpVersions()
@@ -327,6 +344,7 @@ function App(): JSX.Element {
fetchSettings() fetchSettings()
fetchCliConfig() fetchCliConfig()
fetchPmaStatus() fetchPmaStatus()
checkPermissions()
const removeUpdateStatusListener = window.api.onUpdateStatus ? window.api.onUpdateStatus((st: string, info: any) => { const removeUpdateStatusListener = window.api.onUpdateStatus ? window.api.onUpdateStatus((st: string, info: any) => {
setUpdateStatus(st) setUpdateStatus(st)
+3 -1
View File
@@ -45,7 +45,9 @@
"browse": "Durchsuchen", "browse": "Durchsuchen",
"reset_confirm_title": "Sind Sie sicher?", "reset_confirm_title": "Sind Sie sicher?",
"reset_confirm_text": "Möchten Sie die {{service}}-Daten zurücksetzen? Dieser Vorgang löscht alle Daten und führt eine Neuinstallation durch.", "reset_confirm_text": "Möchten Sie die {{service}}-Daten zurücksetzen? Dieser Vorgang löscht alle Daten und führt eine Neuinstallation durch.",
"reset_btn": "Ja, Zurücksetzen" "reset_btn": "Ja, Zurücksetzen",
"status": "Status",
"admin_required": "Bitte versuchen Sie, die Anwendung als Administrator auszuführen."
}, },
"dashboard": { "dashboard": {
"title": "Trunçgil Multi-PHP Server", "title": "Trunçgil Multi-PHP Server",
+3 -1
View File
@@ -51,7 +51,9 @@
"reset_confirm_text": "Do you want to reset {{service}} data? This operation deletes all data and performs a clean installation.", "reset_confirm_text": "Do you want to reset {{service}} data? This operation deletes all data and performs a clean installation.",
"reset_btn": "Yes, Reset", "reset_btn": "Yes, Reset",
"operating": "Operation in progress...", "operating": "Operation in progress...",
"please_wait": "Please wait..." "please_wait": "Please wait...",
"status": "Status",
"admin_required": "Please try running the application as an administrator."
}, },
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",
+2 -1
View File
@@ -52,7 +52,8 @@
"reset_btn": "Да, сбросить", "reset_btn": "Да, сбросить",
"operating": "Операция выполняется...", "operating": "Операция выполняется...",
"please_wait": "Пожалуйста, подождите...", "please_wait": "Пожалуйста, подождите...",
"status": "Статус" "status": "Статус",
"admin_required": "Пожалуйста, попробуйте запустить приложение от имени администратора."
}, },
"dashboard": { "dashboard": {
"title": "Панель управления", "title": "Панель управления",
+2 -1
View File
@@ -53,7 +53,8 @@
"reset_btn": "Evet, Sıfırla", "reset_btn": "Evet, Sıfırla",
"operating": "İşlem devam ediyor...", "operating": "İşlem devam ediyor...",
"please_wait": "Lütfen bekleyin...", "please_wait": "Lütfen bekleyin...",
"status": "Durum" "status": "Durum",
"admin_required": "Lütfen uygulamayı yönetici olarak çalıştırmayı deneyin."
}, },
"dashboard": { "dashboard": {
"title": "Özet", "title": "Özet",