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 // Force kill Nginx to ensure it picks up new config on Windows
await processManager.forceKillAll('nginx') await processManager.forceKillAll('nginx')
await processManager.stopService('nginx')
// Give OS time to release the port
await new Promise(resolve => setTimeout(resolve, 1000))
// Start service // Start service
const success = await processManager.startService('nginx', binPath, ['-p', nginxPrefix, '-c', configPath], false) 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(sessionDir)) fs.mkdirSync(sessionDir, { recursive: true })
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { 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 --- // --- 1. Get extensions from local php.ini ---
let extensionsList = '' let extensionsList = ''
const localIniPath = path.join(phpDir, 'php.ini') 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)) { if (fs.existsSync(localIniPath)) {
const localIniContent = fs.readFileSync(localIniPath, 'utf8') const localIniContent = fs.readFileSync(localIniPath, 'utf8')
const extMatches = localIniContent.match(/^\s*extension\s*=.*$/gm) const extMatches = localIniContent.match(/^\s*extension\s*=.*$/gm)
@@ -197,8 +204,10 @@ export function registerIpcHandlers(): void {
extensionsList = extMatches.join('\n') extensionsList = extMatches.join('\n')
} }
} else { } else {
// Fallback to minimal set if missing // Fallback formatting based on version
extensionsList = 'extension=curl\nextension=mbstring\nextension=openssl\nextension=mysqli\nextension=pdo_mysql' 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`, { const configPath = await configService.generateConfig('php.ini.template', `php_${phpVersion || 'default'}.ini`, {
@@ -209,6 +218,8 @@ export function registerIpcHandlers(): void {
EXTENSIONS: extensionsList 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) const success = await processManager.startService(serviceName, binPath, ['-b', `127.0.0.1:${phpPort}`, '-c', configPath], false)
if (!success) { if (!success) {
+15 -5
View File
@@ -212,23 +212,33 @@ export class PhpManagerService {
let found = false let found = false
const newLines = lines.map(line => { const newLines = lines.map(line => {
const trimmedLine = line.trim() 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') const pattern = new RegExp(`^\\s*;?\\s*extension\\s*=\\s*(?:php_)?${extName}(?:\\.dll)?\\s*$`, 'i')
if (pattern.test(trimmedLine)) { if (pattern.test(trimmedLine)) {
found = true 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 return line
}) })
if (!found && enabled) { 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') const extSectionIndex = newLines.findIndex(l => l.trim().toLowerCase() === '; dynamic extensions')
if (extSectionIndex !== -1) { if (extSectionIndex !== -1) {
newLines.splice(extSectionIndex + 1, 0, `extension=php_${extName}.dll`) newLines.splice(extSectionIndex + 1, 0, newLine)
} else { } 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', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2, gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, minWidth: 0 }}>
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: '#39A7FF' }}> <Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: '#39A7FF', flexShrink: 0 }}>
<FolderIcon /> <FolderIcon />
</Avatar> </Avatar>
<Box> <Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 700, color: '#fff', lineHeight: 1.2 }}> <Typography variant="h6" noWrap sx={{ fontWeight: 700, color: '#fff', lineHeight: 1.2, textOverflow: 'ellipsis' }}>
{project.name} {project.name}
</Typography> </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} {project.path.length > 40 ? '...' + project.path.slice(-37) : project.path}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 0.5 }}> <Box sx={{ display: 'flex', gap: 0.5, flexShrink: 0 }}>
<Tooltip title="Düzenle"> <Tooltip title="Düzenle">
<IconButton size="small" onClick={() => { <IconButton size="small" onClick={() => {
setNewProject(project) setNewProject(project)