feat: implement Nginx and PHP service management IPC handlers with dynamic configuration generation
This commit is contained in:
+1
-11
@@ -47,17 +47,7 @@ sys_temp_dir = "{{TEMP_DIR}}"
|
|||||||
extension_dir = "{{EXT_DIR}}"
|
extension_dir = "{{EXT_DIR}}"
|
||||||
|
|
||||||
[ExtensionList]
|
[ExtensionList]
|
||||||
extension=curl
|
{{EXTENSIONS}}
|
||||||
extension=fileinfo
|
|
||||||
extension={{GD_EXT}}
|
|
||||||
extension=gettext
|
|
||||||
extension=mbstring
|
|
||||||
extension=exif
|
|
||||||
extension=mysqli
|
|
||||||
extension=openssl
|
|
||||||
extension=pdo_mysql
|
|
||||||
extension=pdo_sqlite
|
|
||||||
extension=sqlite3
|
|
||||||
|
|
||||||
[Date]
|
[Date]
|
||||||
date.timezone = UTC
|
date.timezone = UTC
|
||||||
|
|||||||
+39
-1
@@ -187,11 +187,26 @@ 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 })
|
||||||
|
|
||||||
|
// --- 1. Get extensions from local php.ini ---
|
||||||
|
let extensionsList = ''
|
||||||
|
const localIniPath = path.join(phpDir, 'php.ini')
|
||||||
|
if (fs.existsSync(localIniPath)) {
|
||||||
|
const localIniContent = fs.readFileSync(localIniPath, 'utf8')
|
||||||
|
const extMatches = localIniContent.match(/^\s*extension\s*=.*$/gm)
|
||||||
|
if (extMatches) {
|
||||||
|
extensionsList = extMatches.join('\n')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to minimal set if missing
|
||||||
|
extensionsList = 'extension=curl\nextension=mbstring\nextension=openssl\nextension=mysqli\nextension=pdo_mysql'
|
||||||
|
}
|
||||||
|
|
||||||
const configPath = await configService.generateConfig('php.ini.template', `php_${phpVersion || 'default'}.ini`, {
|
const configPath = await configService.generateConfig('php.ini.template', `php_${phpVersion || 'default'}.ini`, {
|
||||||
EXT_DIR: extDir,
|
EXT_DIR: extDir,
|
||||||
GD_EXT: gdExtName,
|
GD_EXT: gdExtName,
|
||||||
SESSION_DIR: sessionDir,
|
SESSION_DIR: sessionDir,
|
||||||
TEMP_DIR: tempDir
|
TEMP_DIR: tempDir,
|
||||||
|
EXTENSIONS: extensionsList
|
||||||
})
|
})
|
||||||
|
|
||||||
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)
|
||||||
@@ -370,6 +385,29 @@ export function registerIpcHandlers(): void {
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('php:config:get', async (_event, version: string) => {
|
||||||
|
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${version}`)
|
||||||
|
const iniPath = path.join(phpDir, 'php.ini')
|
||||||
|
if (fs.existsSync(iniPath)) {
|
||||||
|
return fs.readFileSync(iniPath, 'utf8')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('php:config:save', async (_event, { version, content }) => {
|
||||||
|
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${version}`)
|
||||||
|
const iniPath = path.join(phpDir, 'php.ini')
|
||||||
|
fs.writeFileSync(iniPath, content)
|
||||||
|
|
||||||
|
// Return a result that indicates a restart is needed
|
||||||
|
const serviceName = `php:${version}`
|
||||||
|
const isRunning = processManager.isServiceRunning(serviceName)
|
||||||
|
if (isRunning) {
|
||||||
|
await processManager.stopService(serviceName)
|
||||||
|
}
|
||||||
|
return { success: true, message: `php.ini saved.${isRunning ? ' Current PHP service stopped. Please restart to apply.' : ''}` }
|
||||||
|
})
|
||||||
|
|
||||||
// MariaDB Versions
|
// MariaDB Versions
|
||||||
ipcMain.handle('mariadb:versions', async () => {
|
ipcMain.handle('mariadb:versions', async () => {
|
||||||
return await mariaDbManagerService.getVersions()
|
return await mariaDbManagerService.getVersions()
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Chip,
|
Chip,
|
||||||
IconButton,
|
IconButton
|
||||||
Alert
|
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { Search as SearchIcon, Close as CloseIcon, Refresh as RefreshIcon } from '@mui/icons-material'
|
import { Search as SearchIcon, Close as CloseIcon, Refresh as RefreshIcon, EditNote as RawIcon, Save as SaveIcon } from '@mui/icons-material'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
|
|
||||||
interface PhpExtension {
|
interface PhpExtension {
|
||||||
@@ -38,6 +37,9 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [saving, setSaving] = useState<string | null>(null)
|
const [saving, setSaving] = useState<string | null>(null)
|
||||||
|
const [rawConfig, setRawConfig] = useState('')
|
||||||
|
const [isRawEditorOpen, setIsRawEditorOpen] = useState(false)
|
||||||
|
const [isSavingRaw, setIsSavingRaw] = useState(false)
|
||||||
|
|
||||||
const fetchExtensions = async () => {
|
const fetchExtensions = async () => {
|
||||||
if (!phpVersion) return
|
if (!phpVersion) return
|
||||||
@@ -52,6 +54,39 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchRawConfig = async () => {
|
||||||
|
if (!phpVersion) return
|
||||||
|
try {
|
||||||
|
const data = await window.api.invoke('php:config:get', phpVersion)
|
||||||
|
setRawConfig(data)
|
||||||
|
setIsRawEditorOpen(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch raw config:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveRaw = async () => {
|
||||||
|
setIsSavingRaw(true)
|
||||||
|
try {
|
||||||
|
const result = await window.api.invoke('php:config:save', { version: phpVersion, content: rawConfig })
|
||||||
|
if (result.success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Başarılı',
|
||||||
|
text: result.message,
|
||||||
|
icon: 'success',
|
||||||
|
background: '#1e1e1e',
|
||||||
|
color: '#fff'
|
||||||
|
})
|
||||||
|
setIsRawEditorOpen(false)
|
||||||
|
fetchExtensions() // Refresh extension list too
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
Swal.fire({ title: 'Hata', text: error.message, icon: 'error', background: '#1e1e1e', color: '#fff' })
|
||||||
|
} finally {
|
||||||
|
setIsSavingRaw(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && phpVersion) {
|
if (open && phpVersion) {
|
||||||
fetchExtensions()
|
fetchExtensions()
|
||||||
@@ -101,106 +136,171 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<>
|
||||||
open={open}
|
<Dialog
|
||||||
onClose={onClose}
|
open={open}
|
||||||
maxWidth="sm"
|
onClose={onClose}
|
||||||
fullWidth
|
maxWidth="sm"
|
||||||
PaperProps={{
|
fullWidth
|
||||||
sx: {
|
PaperProps={{
|
||||||
bgcolor: '#1e1e1e',
|
sx: {
|
||||||
color: '#fff',
|
bgcolor: '#1e1e1e',
|
||||||
backgroundImage: 'none',
|
color: '#fff',
|
||||||
borderRadius: 2,
|
backgroundImage: 'none',
|
||||||
border: '1px solid rgba(255,255,255,0.1)'
|
borderRadius: 2,
|
||||||
}
|
border: '1px solid rgba(255,255,255,0.1)'
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
>
|
||||||
<Box>
|
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Typography variant="h6">PHP {phpVersion} Eklentileri</Typography>
|
<Box>
|
||||||
<Typography variant="caption" color="text.secondary">php.ini üzerinden eklentileri yönetin</Typography>
|
<Typography variant="h6">PHP {phpVersion} Eklentileri</Typography>
|
||||||
</Box>
|
<Typography variant="caption" color="text.secondary">php.ini üzerinden eklentileri yönetin</Typography>
|
||||||
<IconButton onClick={onClose} sx={{ color: 'text.secondary' }}>
|
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent dividers sx={{ borderColor: 'rgba(255,255,255,0.1)', p: 0 }}>
|
|
||||||
<Box sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.03)' }}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
placeholder="Eklenti ara... (mysql, gd, mbstring...)"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
InputProps={{
|
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position="start">
|
|
||||||
<SearchIcon sx={{ color: 'text.secondary' }} />
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
sx: { color: '#fff', bgcolor: 'rgba(0,0,0,0.2)' }
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
<IconButton onClick={onClose} sx={{ color: 'text.secondary' }}>
|
||||||
<List sx={{ maxHeight: 400, overflow: 'auto' }}>
|
<CloseIcon />
|
||||||
{filteredExtensions.length === 0 ? (
|
</IconButton>
|
||||||
<ListItem>
|
</DialogTitle>
|
||||||
<ListItemText
|
<DialogContent dividers sx={{ borderColor: 'rgba(255,255,255,0.1)', p: 0 }}>
|
||||||
primary={searchTerm ? "Eklenti bulunamadı." : "Hiç eklenti yok."}
|
<Box sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.03)', display: 'flex', gap: 1 }}>
|
||||||
sx={{ textAlign: 'center', color: 'text.secondary' }}
|
<TextField
|
||||||
/>
|
fullWidth
|
||||||
</ListItem>
|
size="small"
|
||||||
) : (
|
placeholder="Eklenti ara... (mysql, gd, mbstring...)"
|
||||||
filteredExtensions.map((ext) => (
|
value={searchTerm}
|
||||||
<ListItem
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
key={ext.name}
|
InputProps={{
|
||||||
divider
|
startAdornment: (
|
||||||
sx={{ borderColor: 'rgba(255,255,255,0.05)' }}
|
<InputAdornment position="start">
|
||||||
secondaryAction={
|
<SearchIcon sx={{ color: 'text.secondary' }} />
|
||||||
saving === ext.name ? (
|
</InputAdornment>
|
||||||
<CircularProgress size={24} />
|
),
|
||||||
) : (
|
sx: { color: '#fff', bgcolor: 'rgba(0,0,0,0.2)' }
|
||||||
<Switch
|
}}
|
||||||
edge="end"
|
/>
|
||||||
checked={ext.enabled}
|
<Button
|
||||||
onChange={(e) => handleToggle(ext.name, e.target.checked)}
|
variant="outlined"
|
||||||
color="primary"
|
startIcon={<RawIcon />}
|
||||||
/>
|
onClick={fetchRawConfig}
|
||||||
)
|
sx={{ whiteSpace: 'nowrap', borderColor: 'rgba(255,255,255,0.2)', color: '#fff' }}
|
||||||
}
|
>
|
||||||
>
|
RAW EDİTÖR
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<List sx={{ maxHeight: 400, overflow: 'auto' }}>
|
||||||
|
{filteredExtensions.length === 0 ? (
|
||||||
|
<ListItem>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={searchTerm ? "Eklenti bulunamadı." : "Hiç eklenti yok."}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
sx={{ textAlign: 'center', color: 'text.secondary' }}
|
||||||
{ext.name}
|
|
||||||
{ext.enabled && <Chip label="Aktif" size="small" color="success" sx={{ height: 18, fontSize: '0.65rem' }} />}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
secondary={ext.dll}
|
|
||||||
secondaryTypographyProps={{ sx: { color: 'text.secondary', fontSize: '0.75rem' } }}
|
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))
|
) : (
|
||||||
)}
|
filteredExtensions.map((ext) => (
|
||||||
</List>
|
<ListItem
|
||||||
)}
|
key={ext.name}
|
||||||
</DialogContent>
|
divider
|
||||||
<DialogActions sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)' }}>
|
sx={{ borderColor: 'rgba(255,255,255,0.05)' }}
|
||||||
<Button onClick={fetchExtensions} startIcon={<RefreshIcon />} sx={{ mr: 'auto' }}>
|
secondaryAction={
|
||||||
YENİLE
|
saving === ext.name ? (
|
||||||
</Button>
|
<CircularProgress size={24} />
|
||||||
<Button onClick={onClose} variant="contained" color="primary">
|
) : (
|
||||||
KAPAT
|
<Switch
|
||||||
</Button>
|
edge="end"
|
||||||
</DialogActions>
|
checked={ext.enabled}
|
||||||
</Dialog>
|
onChange={(e) => handleToggle(ext.name, e.target.checked)}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{ext.name}
|
||||||
|
{ext.enabled && <Chip label="Aktif" size="small" color="success" sx={{ height: 18, fontSize: '0.65rem' }} />}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
secondary={ext.dll}
|
||||||
|
secondaryTypographyProps={{ sx: { color: 'text.secondary', fontSize: '0.75rem' } }}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)' }}>
|
||||||
|
<Button onClick={fetchExtensions} startIcon={<RefreshIcon />} sx={{ mr: 'auto' }}>
|
||||||
|
YENİLE
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose} variant="contained" color="primary">
|
||||||
|
KAPAT
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isRawEditorOpen}
|
||||||
|
onClose={() => setIsRawEditorOpen(false)}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
bgcolor: '#1a1a1a',
|
||||||
|
color: '#fff',
|
||||||
|
borderRadius: 2,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography variant="h6">php.ini Düzenle ({phpVersion})</Typography>
|
||||||
|
<IconButton onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent sx={{ p: 0 }}>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
fullWidth
|
||||||
|
rows={20}
|
||||||
|
value={rawConfig}
|
||||||
|
onChange={(e) => setRawConfig(e.target.value)}
|
||||||
|
variant="filled"
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
color: '#d4d4d4',
|
||||||
|
bgcolor: '#1e1e1e',
|
||||||
|
'& textarea': { p: 2 }
|
||||||
|
},
|
||||||
|
disableUnderline: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ p: 2, bgcolor: 'rgba(0,0,0,0.2)' }}>
|
||||||
|
<Button onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||||
|
İPTAL
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveRaw}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={isSavingRaw ? <CircularProgress size={18} color="inherit" /> : <SaveIcon />}
|
||||||
|
disabled={isSavingRaw}
|
||||||
|
>
|
||||||
|
{isSavingRaw ? 'KAYDEDİLİYOR...' : 'KAYDET VE SUNUCUYU DURDUR'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user