feat: implement PHP version management and extension configuration service with IPC support

This commit is contained in:
Ümit Tunç
2026-03-30 17:09:06 +03:00
parent 8d687a159f
commit 4dfc7c0834
3 changed files with 35 additions and 14 deletions
+13 -2
View File
@@ -110,6 +110,9 @@ async function restartNginx(): Promise<{ success: boolean; message: string }> {
// 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)
@@ -187,9 +190,13 @@ export function registerIpcHandlers(): void {
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 = ''
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')
const extMatches = localIniContent.match(/^\s*extension\s*=.*$/gm)
@@ -197,8 +204,10 @@ export function registerIpcHandlers(): void {
extensionsList = extMatches.join('\n')
}
} else {
// Fallback to minimal set if missing
extensionsList = 'extension=curl\nextension=mbstring\nextension=openssl\nextension=mysqli\nextension=pdo_mysql'
// 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`, {
@@ -209,6 +218,8 @@ export function registerIpcHandlers(): void {
EXTENSIONS: extensionsList
})
// 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) {
+15 -5
View File
@@ -212,23 +212,33 @@ export class PhpManagerService {
let found = false
const newLines = lines.map(line => {
const trimmedLine = line.trim()
// Pattern matches: ;extension=php_mysql.dll, extension=php_mysql.dll, ; extension = php_mysql.dll
const pattern = new RegExp(`^\\s*;?\\s*extension\\s*=\\s*(?:php_)?${extName}(?:\\.dll)?\\s*$`, 'i')
if (pattern.test(trimmedLine)) {
found = true
return enabled ? `extension=php_${extName}.dll` : `;extension=php_${extName}.dll`
// 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
if (version.startsWith('5.') || version.startsWith('7.')) {
extValue = `php_${extName}.dll`
}
return enabled ? `extension=${extValue}` : `;extension=${extValue}`
}
return line
})
if (!found && enabled) {
// If not found in file, append it under [Extension] or at the end
let extValue = extName
if (version.startsWith('5.') || version.startsWith('7.')) {
extValue = `php_${extName}.dll`
}
const newLine = `extension=${extValue}`
const extSectionIndex = newLines.findIndex(l => l.trim().toLowerCase() === '; dynamic extensions')
if (extSectionIndex !== -1) {
newLines.splice(extSectionIndex + 1, 0, `extension=php_${extName}.dll`)
newLines.splice(extSectionIndex + 1, 0, newLine)
} else {
newLines.push(`extension=php_${extName}.dll`)
newLines.push(newLine)
}
}
+7 -7
View File
@@ -1211,21 +1211,21 @@ function App(): JSX.Element {
}
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: '#39A7FF' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2, gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, minWidth: 0 }}>
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: '#39A7FF', flexShrink: 0 }}>
<FolderIcon />
</Avatar>
<Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: '#fff', lineHeight: 1.2 }}>
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Typography variant="h6" noWrap sx={{ fontWeight: 700, color: '#fff', lineHeight: 1.2, textOverflow: 'ellipsis' }}>
{project.name}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.4)', fontFamily: 'monospace' }}>
<Typography variant="caption" noWrap sx={{ color: 'rgba(255, 255, 255, 0.4)', fontFamily: 'monospace', display: 'block', textOverflow: 'ellipsis' }}>
{project.path.length > 40 ? '...' + project.path.slice(-37) : project.path}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 0.5 }}>
<Box sx={{ display: 'flex', gap: 0.5, flexShrink: 0 }}>
<Tooltip title="Düzenle">
<IconButton size="small" onClick={() => {
setNewProject(project)