feat: implement MariaDB export wizard and backend management services

This commit is contained in:
Ümit Tunç
2026-04-02 12:04:32 +03:00
parent 0bb5108529
commit 51b3157d1b
7 changed files with 290 additions and 17 deletions
+19
View File
@@ -1043,6 +1043,25 @@ export function registerIpcHandlers(): void {
}, dbName, savePath, compress, (msg) => log(msg))
})
ipcMain.handle('mariadb:processlist', async (_event, versionId) => {
try {
return await mariaDbManagerService.getProcessList(versionId)
} catch (error: any) {
console.error('Failed to get process list:', error)
return []
}
})
ipcMain.handle('mariadb:kill-process', async (_event, { versionId, processId }) => {
try {
await mariaDbManagerService.killProcess(versionId, processId)
return { success: true }
} catch (error: any) {
console.error('Failed to kill process:', error)
return { success: false, error: error.message }
}
})
// CLI & Environment
ipcMain.handle('cli:get-config', async () => {
const settings = configService.getSettings()
+7 -9
View File
@@ -134,22 +134,18 @@ export class CliBinService {
if (process.platform !== 'win32') return
try {
// Use PowerShell to manage PATH safely.
// 1. Get current User PATH
// 2. Add binDir if not exists
// 3. Set new PATH
// 4. Broadcast WM_SETTINGCHANGE to notify system
const binDir = this.getBinDirInternal()
// We use Base64 encoding for the PS command to avoid any escaping issues with quotes, semicolons, etc.
const psScript = `
$binDir = '${binDir}'
$currentPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if ($currentPath -notlike '*${binDir}*') {
if ($currentPath -notlike "*$binDir*") {
$newPath = $currentPath
if ($newPath -and -not $newPath.EndsWith(';')) { $newPath += ';' }
$newPath += '${binDir}'
$newPath += $binDir
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
# Notify Windows of the change (WM_SETTINGCHANGE)
$signature = @'
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult);
@@ -164,7 +160,9 @@ export class CliBinService {
}
`
const { stdout } = await execAsync(`powershell -Command "${psScript.replace(/\n/g, ' ')}"`)
const encodedCommand = Buffer.from(psScript, 'utf16le').toString('base64')
const { stdout } = await execAsync(`powershell -EncodedCommand ${encodedCommand}`)
if (stdout.includes('PATH_UPDATED')) {
console.log(`Successfully added Multi-PHP bin to User PATH: ${binDir}`)
}
@@ -2,6 +2,7 @@ import fs from 'fs'
import path from 'path'
import { app } from 'electron'
import { configService } from './ConfigService'
import { mariaDbOperationService } from './MariaDbOperationService'
export interface MariaDbVersion {
id: string
@@ -156,6 +157,64 @@ export class MariaDbManagerService {
}
return null
}
async getProcessList(versionId: string) {
const version = this.versions.find(v => v.id === versionId)
if (!version) throw new Error('Version not found')
const binaryPath = await this.getMariaDbBinaryPath(version.version)
if (!binaryPath) throw new Error('MariaDB binary not found')
const settings = configService.getSettings()
const config = {
binPath: binaryPath,
host: '127.0.0.1',
port: version.port,
rootUser: 'root',
rootPass: settings.mariaDbPasswords?.[versionId] || ''
}
const result = await mariaDbOperationService.runQuery(config, 'SHOW FULL PROCESSLIST')
if (!result.success) throw new Error(result.error)
// Parse mysql tab-separated output
const lines = result.output.split('\n').filter(l => l.trim().length > 0)
if (lines.length === 0) return []
const headers = lines[0].split('\t')
const rows = lines.slice(1).map(line => {
const values = line.split('\t')
const row: any = {}
headers.forEach((h, i) => {
row[h] = values[i] === 'NULL' ? null : values[i]
})
return row
})
return rows
}
async killProcess(versionId: string, processId: string) {
const version = this.versions.find(v => v.id === versionId)
if (!version) throw new Error('Version not found')
const binaryPath = await this.getMariaDbBinaryPath(version.version)
if (!binaryPath) throw new Error('MariaDB binary not found')
const settings = configService.getSettings()
const config = {
binPath: binaryPath,
host: '127.0.0.1',
port: version.port,
rootUser: 'root',
rootPass: settings.mariaDbPasswords?.[versionId] || ''
}
const result = await mariaDbOperationService.runQuery(config, `KILL ${processId}`)
if (!result.success) throw new Error(result.error)
return true
}
}
export const mariaDbManagerService = new MariaDbManagerService()
+155 -1
View File
@@ -166,6 +166,12 @@ function App(): JSX.Element {
const [pmaProgress, setPmaProgress] = useState(0)
const [isMariaDbWizardOpen, setIsMariaDbWizardOpen] = useState(false)
const [isMariaDbExportWizardOpen, setIsMariaDbExportWizardOpen] = useState(false)
// MariaDB Process List State
const [processList, setProcessList] = useState<any[]>([])
const [isProcessListOpen, setIsProcessListOpen] = useState(false)
const [selectedMariaDbId, setSelectedMariaDbId] = useState<string | null>(null)
const [isProcessListLoading, setIsProcessListLoading] = useState(false)
const [isNginxConfigOpen, setIsNginxConfigOpen] = useState(false)
const [nginxConfig, setNginxConfig] = useState('')
const [isApacheConfigOpen, setIsApacheConfigOpen] = useState(false)
@@ -327,12 +333,20 @@ function App(): JSX.Element {
if (logDialogOpen && selectedService) {
logInterval = setInterval(() => {
fetchLogs(selectedService)
}, 1000)
}, 3000)
}
let processInterval: NodeJS.Timeout | null = null
if (isProcessListOpen && selectedMariaDbId) {
processInterval = setInterval(() => {
fetchProcessList(selectedMariaDbId)
}, 3000)
}
return () => {
clearInterval(interval)
if (logInterval) clearInterval(logInterval)
if (processInterval) clearInterval(processInterval)
if (typeof removePhpListener === 'function') removePhpListener()
if (typeof removeMariaDbListener === 'function') removeMariaDbListener()
if (typeof removeNginxListener === 'function') removeNginxListener()
@@ -866,6 +880,45 @@ function App(): JSX.Element {
setIsPhpExtensionsOpen(true)
}
const handleOpenProcessList = async (id: string) => {
setSelectedMariaDbId(id)
setIsProcessListOpen(true)
fetchProcessList(id)
}
const fetchProcessList = async (id: string) => {
if (!window.api) return
setIsProcessListLoading(true)
const list = await window.api.invoke('mariadb:processlist', id)
setProcessList(list || [])
setIsProcessListLoading(false)
}
const handleKillProcess = async (id: string, pid: string) => {
if (!window.api) return
const result = await Swal.fire({
title: t('mariadb_process_monitor.kill_confirm'),
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: t('mariadb_process_monitor.kill_btn'),
cancelButtonText: t('common.cancel'),
background: '#1a1d21',
color: '#fff'
})
if (result.isConfirmed) {
const res = await window.api.invoke('mariadb:kill-process', { versionId: id, processId: pid })
if (res.success) {
setNotification({ open: true, message: t('common.ok'), severity: 'success' })
fetchProcessList(id)
} else {
setNotification({ open: true, message: res.error, severity: 'error' })
}
}
}
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
return (
@@ -1584,6 +1637,9 @@ function App(): JSX.Element {
<IconButton onClick={() => handleOpenLogs(`mariadb:${v.version}`)} title={t('diagnostics.logs')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
<TerminalIcon fontSize="small" />
</IconButton>
<IconButton onClick={() => handleOpenProcessList(v.id)} title={t('mariadb_process_monitor.title')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}>
<ToolsIcon fontSize="small" />
</IconButton>
<IconButton onClick={() => handleResetData(`mariadb:${v.version}`)} title={t('common.reset')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'error.main' } }}>
<ResetIcon fontSize="small" />
</IconButton>
@@ -2106,8 +2162,106 @@ function App(): JSX.Element {
open={isMariaDbExportWizardOpen}
onClose={() => setIsMariaDbExportWizardOpen(false)}
mariaDbVersions={mariaDbVersions}
versionId={selectedMariaDbId || ''}
/>
{/* MariaDB Process List Dialog */}
<Dialog
open={isProcessListOpen}
onClose={() => setIsProcessListOpen(false)}
maxWidth="lg"
fullWidth
PaperProps={{
sx: {
bgcolor: '#1a1d21',
color: '#fff',
borderRadius: 2
}
}}
>
<DialogTitle sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<DbIcon color="primary" />
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{t('mariadb_process_monitor.title')} - {selectedMariaDbId}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
size="small"
startIcon={isProcessListLoading ? <CircularProgress size={16} /> : <RefreshIcon />}
onClick={() => selectedMariaDbId && fetchProcessList(selectedMariaDbId)}
disabled={isProcessListLoading}
>
{t('mariadb_process_monitor.refresh_btn')}
</Button>
<IconButton size="small" onClick={() => setIsProcessListOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
<CloseIcon />
</IconButton>
</Box>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<TableContainer sx={{ maxHeight: 500 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem' }}>{t('mariadb_process_monitor.columns.id')}</TableCell>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem' }}>{t('mariadb_process_monitor.columns.user')}</TableCell>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem' }}>{t('mariadb_process_monitor.columns.host')}</TableCell>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem' }}>{t('mariadb_process_monitor.columns.db')}</TableCell>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem' }}>{t('mariadb_process_monitor.columns.command')}</TableCell>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem' }}>{t('mariadb_process_monitor.columns.time')}</TableCell>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem' }}>{t('mariadb_process_monitor.columns.state')}</TableCell>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem' }}>{t('mariadb_process_monitor.columns.info')}</TableCell>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem' }}>{t('mariadb_process_monitor.columns.progress')}</TableCell>
<TableCell sx={{ bgcolor: '#24292e', color: 'rgba(255,255,255,0.5)', fontWeight: 'bold', fontSize: '0.75rem', textAlign: 'right' }}>{t('common.actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{processList.length === 0 ? (
<TableRow>
<TableCell colSpan={10} sx={{ textAlign: 'center', py: 4, color: 'rgba(255,255,255,0.3)', fontStyle: 'italic' }}>
{t('mariadb_process_monitor.no_processes')}
</TableCell>
</TableRow>
) : (
processList.map((p) => (
<TableRow key={p.Id} hover sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)', color: 'primary.light' }}>{p.Id}</TableCell>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)' }}>{p.User}</TableCell>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)', fontSize: '0.7rem', opacity: 0.7 }}>{p.Host}</TableCell>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)' }}>{p.db || <Typography component="span" variant="caption" sx={{ opacity: 0.3 }}>NULL</Typography>}</TableCell>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)' }}>
<Chip label={p.Command} size="small" variant="outlined" sx={{ height: 18, fontSize: '0.65rem', color: p.Command === 'Query' ? 'success.main' : 'inherit' }} />
</TableCell>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)' }}>{p.Time}s</TableCell>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)', fontSize: '0.75rem', color: p.State === 'starting' ? 'warning.main' : 'inherit' }}>{p.State}</TableCell>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)', maxWidth: 200, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontSize: '0.7rem', fontFamily: 'monospace' }} title={p.Info}>
{p.Info || ''}
</TableCell>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)', fontSize: '0.75rem' }}>
{p.Progress ? `${parseFloat(p.Progress).toFixed(2)}%` : '-'}
</TableCell>
<TableCell sx={{ borderColor: 'rgba(255,255,255,0.05)', textAlign: 'right' }}>
<Button
size="small"
color="error"
variant="text"
onClick={() => selectedMariaDbId && handleKillProcess(selectedMariaDbId, p.Id)}
sx={{ fontSize: '0.65rem', p: 0, minWidth: 'auto' }}
>
{t('mariadb_process_monitor.kill_btn')}
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</DialogContent>
</Dialog>
<Dialog
open={isNginxConfigOpen}
onClose={() => setIsNginxConfigOpen(false)}
@@ -35,9 +35,10 @@ interface MariaDbExportWizardProps {
open: boolean
onClose: () => void
mariaDbVersions: any[]
versionId?: string
}
const MariaDbExportWizard: React.FC<MariaDbExportWizardProps> = ({ open, onClose, mariaDbVersions }) => {
const MariaDbExportWizard: React.FC<MariaDbExportWizardProps> = ({ open, onClose, mariaDbVersions, versionId }) => {
const { t } = useTranslation()
const steps = [
@@ -65,10 +66,14 @@ const MariaDbExportWizard: React.FC<MariaDbExportWizardProps> = ({ open, onClose
setSavePath('')
setLogs([])
setExportResult(null)
if (versionId) {
setSelectedVersion(versionId)
} else {
const installedVersion = mariaDbVersions.find(v => v.status === 'installed')
if (installedVersion) setSelectedVersion(installedVersion.id)
}
}, [open, mariaDbVersions])
}
}, [open, mariaDbVersions, versionId])
useEffect(() => {
if (logEndRef.current) {
+19
View File
@@ -1,5 +1,6 @@
{
"common": {
"actions": "Actions",
"start": "Start",
"stop": "Stop",
"restart": "Restart",
@@ -254,5 +255,23 @@
"prev_match": "Previous (Shift+Enter)",
"next_match": "Next (Enter)"
}
},
"mariadb_process_monitor": {
"title": "MariaDB Process List",
"kill_btn": "KILL",
"kill_confirm": "Are you sure you want to kill this process?",
"refresh_btn": "Refresh",
"no_processes": "No active processes found.",
"columns": {
"id": "ID",
"user": "User",
"host": "Host",
"db": "db",
"command": "Command",
"time": "Time",
"state": "State",
"info": "Info",
"progress": "Progress"
}
}
}
+19
View File
@@ -1,5 +1,6 @@
{
"common": {
"actions": "İşlem",
"start": "Başlat",
"stop": "Durdur",
"restart": "Yeniden Başlat",
@@ -249,5 +250,23 @@
"prev_match": "Önceki (Shift+Enter)",
"next_match": "Sonraki (Enter)"
}
},
"mariadb_process_monitor": {
"title": "MariaDB Süreç Listesi (Process List)",
"kill_btn": "KILL",
"kill_confirm": "Bu süreci sonlandırmak istediğinizden emin misiniz?",
"refresh_btn": "Yenile",
"no_processes": "Aktif süreç bulunamadı.",
"columns": {
"id": "ID",
"user": "Kullanıcı",
"host": "Host",
"db": "Veritabanı",
"command": "Komut",
"time": "Süre",
"state": "Durum",
"info": "Bilgi",
"progress": "İlerleme"
}
}
}