feat: implement MariaDB export wizard and backend management services
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -134,25 +134,21 @@ 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);
|
||||
[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);
|
||||
'@
|
||||
$type = Add-Type -MemberDefinition $signature -Name "Win32" -Namespace "Native" -PassThru -ErrorAction SilentlyContinue
|
||||
if ($type) {
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"common": {
|
||||
"actions": "İşlem",
|
||||
"start": "Başlat",
|
||||
"stop": "Durdur",
|
||||
"restart": "Yeniden Başlat",
|
||||
@@ -79,7 +80,7 @@
|
||||
"updated": "Proje başarıyla güncellendi.",
|
||||
"deleted": "Proje silindi.",
|
||||
"delete_confirm": "Bu projeyi silmek istediğinize emin misiniz? Dosyalarınız silinmez, sadece listeden kaldırılır.",
|
||||
"browse" : "Gözat"
|
||||
"browse": "Gözat"
|
||||
},
|
||||
"settings": {
|
||||
"general": "Genel Ayarlar",
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user