feat: implement process management service and IPC bridge with initial renderer UI structure
This commit is contained in:
@@ -41,6 +41,7 @@ max_file_uploads = 20
|
|||||||
allow_url_fopen = On
|
allow_url_fopen = On
|
||||||
allow_url_include = Off
|
allow_url_include = Off
|
||||||
default_socket_timeout = 60
|
default_socket_timeout = 60
|
||||||
|
extension_dir = "{{EXT_DIR}}"
|
||||||
|
|
||||||
[ExtensionList]
|
[ExtensionList]
|
||||||
extension=curl
|
extension=curl
|
||||||
|
|||||||
+38
-4
@@ -6,6 +6,7 @@ import { downloadService } from '../services/DownloadService'
|
|||||||
import { projectService } from '../services/ProjectService'
|
import { projectService } from '../services/ProjectService'
|
||||||
import { phpManagerService } from '../services/PhpManagerService'
|
import { phpManagerService } from '../services/PhpManagerService'
|
||||||
import { mySqlManagerService } from '../services/MySqlManagerService'
|
import { mySqlManagerService } from '../services/MySqlManagerService'
|
||||||
|
import { nginxManagerService } from '../services/NginxManagerService'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
function findExecutable(dir: string, name: string): string | null {
|
function findExecutable(dir: string, name: string): string | null {
|
||||||
@@ -49,14 +50,16 @@ export function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (serviceName === 'php') {
|
if (serviceName === 'php') {
|
||||||
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {})
|
|
||||||
// Find any installed PHP version
|
|
||||||
const binPath = findExecutable(binDir, 'php-cgi.exe')
|
const binPath = findExecutable(binDir, 'php-cgi.exe')
|
||||||
|
|
||||||
if (!binPath) return { success: false, message: 'PHP executable bulunamadı. Lütfen en az bir sürüm indirin.' }
|
if (!binPath) return { success: false, message: 'PHP executable bulunamadı. Lütfen en az bir sürüm indirin.' }
|
||||||
|
|
||||||
|
const extDir = path.join(path.dirname(binPath), 'ext')
|
||||||
|
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {
|
||||||
|
EXT_DIR: extDir.replace(/\\/g, '\\\\') // PHP ini needs escaped backslashes or forward slashes
|
||||||
|
})
|
||||||
|
|
||||||
const success = await processManager.startService('php', binPath, ['-b', `127.0.0.1:${settings.phpPort}`, '-c', configPath])
|
const success = await processManager.startService('php', binPath, ['-b', `127.0.0.1:${settings.phpPort}`, '-c', configPath])
|
||||||
return { success, message: success ? 'PHP başlatıldı.' : 'PHP dosyaları bulunamadı veya port meşgul.' }
|
return { success, message: success ? 'PHP başlatıldı.' : 'PHP devreye alınamadı (Hata loglarını kontrol edin).' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceName === 'mysql') {
|
if (serviceName === 'mysql') {
|
||||||
@@ -98,6 +101,10 @@ export function registerIpcHandlers(): void {
|
|||||||
return processManager.getStatuses()
|
return processManager.getStatuses()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('services:logs', async (_event, serviceName: string) => {
|
||||||
|
return processManager.getLogs(serviceName)
|
||||||
|
})
|
||||||
|
|
||||||
// Binary management
|
// Binary management
|
||||||
ipcMain.handle('binaries:list-php', async () => {
|
ipcMain.handle('binaries:list-php', async () => {
|
||||||
return phpManagerService.getVersions()
|
return phpManagerService.getVersions()
|
||||||
@@ -153,6 +160,33 @@ export function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Nginx Versions
|
||||||
|
ipcMain.handle('nginx:versions', async () => {
|
||||||
|
return await nginxManagerService.getVersions()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('nginx:download', async (event, id: string) => {
|
||||||
|
const versions = await nginxManagerService.getVersions()
|
||||||
|
const version = versions.find(v => v.id === id)
|
||||||
|
if (!version) return { success: false, message: 'Geçersiz Nginx sürümü.' }
|
||||||
|
|
||||||
|
try {
|
||||||
|
nginxManagerService.updateProgress(id, 0, 'downloading')
|
||||||
|
event.sender.send('nginx:progress', { id, progress: 0 })
|
||||||
|
|
||||||
|
await downloadService.downloadAndExtract(version.url, id, (p) => {
|
||||||
|
nginxManagerService.updateProgress(id, p)
|
||||||
|
event.sender.send('nginx:progress', { id, progress: p })
|
||||||
|
})
|
||||||
|
|
||||||
|
nginxManagerService.updateProgress(id, 100, 'installed')
|
||||||
|
return { success: true, message: 'Nginx indirme tamamlandı.' }
|
||||||
|
} catch (error: any) {
|
||||||
|
nginxManagerService.updateProgress(id, 0, 'available')
|
||||||
|
return { success: false, message: `Nginx hatası: ${error.message}` }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('dialog:select-directory', async () => {
|
ipcMain.handle('dialog:select-directory', async () => {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
properties: ['openDirectory']
|
properties: ['openDirectory']
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
|
export interface NginxVersion {
|
||||||
|
id: string
|
||||||
|
version: string
|
||||||
|
url: string
|
||||||
|
status: 'available' | 'downloading' | 'installed'
|
||||||
|
progress: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NginxManagerService {
|
||||||
|
private versions: NginxVersion[] = []
|
||||||
|
private binDir: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.binDir = path.join(app.getPath('userData'), 'bin')
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
if (!fs.existsSync(this.binDir)) {
|
||||||
|
fs.mkdirSync(this.binDir, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVersions(): Promise<NginxVersion[]> {
|
||||||
|
// For Nginx, we can stick to one stable version or a small list
|
||||||
|
const stableVersion = '1.26.2'
|
||||||
|
const url = `https://nginx.org/download/nginx-${stableVersion}.zip`
|
||||||
|
|
||||||
|
const id = 'nginx'
|
||||||
|
const targetDir = path.join(this.binDir, id)
|
||||||
|
|
||||||
|
// Check if installed (look for nginx.exe anywhere in the zip extraction folder)
|
||||||
|
const isInstalled = this.isInstalled(targetDir)
|
||||||
|
|
||||||
|
const v: NginxVersion = {
|
||||||
|
id,
|
||||||
|
version: stableVersion,
|
||||||
|
url,
|
||||||
|
status: isInstalled ? 'installed' : 'available',
|
||||||
|
progress: isInstalled ? 100 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintain internal state for progress updates
|
||||||
|
const existing = this.versions.find(ev => ev.id === id)
|
||||||
|
if (existing && existing.status === 'downloading') {
|
||||||
|
return [existing]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.versions = [v]
|
||||||
|
return this.versions
|
||||||
|
}
|
||||||
|
|
||||||
|
private isInstalled(dir: string): boolean {
|
||||||
|
if (!fs.existsSync(dir)) return false
|
||||||
|
|
||||||
|
const findNginx = (d: string): boolean => {
|
||||||
|
const files = fs.readdirSync(d)
|
||||||
|
for (const f of files) {
|
||||||
|
const p = path.join(d, f)
|
||||||
|
const s = fs.statSync(p)
|
||||||
|
if (s.isDirectory()) {
|
||||||
|
if (findNginx(p)) return true
|
||||||
|
} else if (f.toLowerCase() === 'nginx.exe') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return findNginx(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(id: string, progress: number, status: NginxVersion['status'] = 'downloading') {
|
||||||
|
const v = this.versions.find(ev => ev.id === id)
|
||||||
|
if (v) {
|
||||||
|
v.progress = progress
|
||||||
|
v.status = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const nginxManagerService = new NginxManagerService()
|
||||||
@@ -1,22 +1,80 @@
|
|||||||
import { spawn, ChildProcess } from 'child_process'
|
import { spawn, ChildProcess, exec } from 'child_process'
|
||||||
import { join } from 'path'
|
import path from 'path'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
export class ProcessManager {
|
export class ProcessManager {
|
||||||
private processes: Map<string, ChildProcess> = new Map()
|
private processes: Map<string, ChildProcess> = new Map()
|
||||||
|
private logs: Map<string, string[]> = new Map()
|
||||||
|
private logDir: string
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.processes = new Map()
|
this.processes = new Map()
|
||||||
|
this.logDir = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
private initLogs() {
|
||||||
|
if (this.logDir) return
|
||||||
|
try {
|
||||||
|
this.logDir = path.join(app.getPath('userData'), 'logs')
|
||||||
|
if (!fs.existsSync(this.logDir)) {
|
||||||
|
fs.mkdirSync(this.logDir, { recursive: true })
|
||||||
|
}
|
||||||
|
// Initialize with default logs
|
||||||
|
['nginx', 'php', 'mysql'].forEach(s => {
|
||||||
|
if (!this.logs.has(s)) {
|
||||||
|
this.logs.set(s, [`[${new Date().toLocaleTimeString()}] Sistem hazır. İşlem bekleniyor...`])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to init logs:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addLog(name: string, data: string) {
|
||||||
|
this.initLogs()
|
||||||
|
if (!this.logs.has(name)) this.logs.set(name, [])
|
||||||
|
const serviceLogs = this.logs.get(name)!
|
||||||
|
const lines = data.split(/\r?\n/).filter(l => l.trim())
|
||||||
|
const timestamp = new Date().toLocaleTimeString()
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
const formattedLine = `[${timestamp}] ${line}`
|
||||||
|
console.log(`[LOG:${name}] ${formattedLine}`)
|
||||||
|
serviceLogs.push(formattedLine)
|
||||||
|
// Persist to file
|
||||||
|
if (this.logDir) {
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(path.join(this.logDir, `${name}.log`), formattedLine + '\n')
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore write errors to file if it happens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Keep last 100 lines in memory
|
||||||
|
if (serviceLogs.length > 100) {
|
||||||
|
this.logs.set(name, serviceLogs.slice(-100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogs(name: string): string[] {
|
||||||
|
this.initLogs()
|
||||||
|
return this.logs.get(name) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
async runOnce(binPath: string, args: string[]): Promise<boolean> {
|
async runOnce(binPath: string, args: string[]): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = spawn(binPath, args, { stdio: 'inherit' })
|
const child = spawn(binPath, args, { stdio: 'pipe' })
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => console.log(`[INIT] ${data}`))
|
||||||
|
child.stderr?.on('data', (data) => console.error(`[INIT ERROR] ${data}`))
|
||||||
|
|
||||||
child.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
resolve(code === 0)
|
resolve(code === 0)
|
||||||
})
|
})
|
||||||
child.on('error', () => {
|
child.on('error', (err) => {
|
||||||
|
console.error('RunOnce error:', err)
|
||||||
resolve(false)
|
resolve(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -24,43 +82,47 @@ export class ProcessManager {
|
|||||||
|
|
||||||
async startService(name: string, binPath: string, args: string[]): Promise<boolean> {
|
async startService(name: string, binPath: string, args: string[]): Promise<boolean> {
|
||||||
if (this.processes.has(name)) {
|
if (this.processes.has(name)) {
|
||||||
console.log(`Service ${name} is already running.`)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(binPath)) {
|
if (!fs.existsSync(binPath)) {
|
||||||
console.warn(`[DEV] Executable not found: ${binPath}. Mocking service ${name}.`)
|
this.addLog(name, `Executable not found: ${binPath}`)
|
||||||
const mockChild = {
|
return false
|
||||||
kill: () => { },
|
|
||||||
on: () => { },
|
|
||||||
unref: () => { }
|
|
||||||
} as unknown as ChildProcess
|
|
||||||
this.processes.set(name, mockChild)
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.addLog(name, `Starting service with: ${binPath} ${args.join(' ')}`)
|
||||||
|
|
||||||
const child = spawn(binPath, args, {
|
const child = spawn(binPath, args, {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore'
|
shell: true,
|
||||||
|
windowsHide: true,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
cwd: path.dirname(binPath)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log(`[PROCESS] Started ${name} with PID ${child.pid}`)
|
||||||
|
this.addLog(name, `Process started (PID: ${child.pid})`)
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => this.addLog(name, data.toString()))
|
||||||
|
child.stderr?.on('data', (data) => this.addLog(name, data.toString()))
|
||||||
|
|
||||||
child.on('error', (err) => {
|
child.on('error', (err) => {
|
||||||
console.error(`Failed to start service ${name}:`, err)
|
this.addLog(name, `Failed to start: ${err.message}`)
|
||||||
this.processes.delete(name)
|
this.processes.delete(name)
|
||||||
})
|
})
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
child.on('exit', (code) => {
|
||||||
console.log(`Service ${name} exited with code ${code}`)
|
this.addLog(name, `Exited with code ${code}`)
|
||||||
this.processes.delete(name)
|
this.processes.delete(name)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.processes.set(name, child)
|
this.processes.set(name, child)
|
||||||
child.unref() // Allow the parent to exit independently
|
child.unref()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(`Error starting service ${name}:`, error)
|
this.addLog(name, `Spawn Error: ${error.message}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,11 +130,25 @@ export class ProcessManager {
|
|||||||
async stopService(name: string): Promise<boolean> {
|
async stopService(name: string): Promise<boolean> {
|
||||||
const child = this.processes.get(name)
|
const child = this.processes.get(name)
|
||||||
if (child) {
|
if (child) {
|
||||||
|
this.addLog(name, 'Stopping service...')
|
||||||
child.kill()
|
child.kill()
|
||||||
this.processes.delete(name)
|
this.processes.delete(name)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
// Also force kill lingering processes
|
||||||
|
return this.forceKillAll(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
async forceKillAll(name: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const executableName = name === 'nginx' ? 'nginx.exe' : name === 'php' ? 'php-cgi.exe' : 'mysqld.exe'
|
||||||
|
this.addLog(name, `Force killing all ${executableName} processes...`)
|
||||||
|
|
||||||
|
exec(`taskkill /F /IM ${executableName} /T`, (_err) => {
|
||||||
|
// If it fails, usually it means the process isn't running, which is fine
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isServiceRunning(name: string): boolean {
|
isServiceRunning(name: string): boolean {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const api = {
|
|||||||
startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name),
|
startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name),
|
||||||
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
|
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
|
||||||
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
|
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
|
||||||
|
getServiceLogs: (name: string) => electronAPI.ipcRenderer.invoke('services:logs'),
|
||||||
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
|
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
|
||||||
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config),
|
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config),
|
||||||
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
|
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
|
||||||
@@ -19,7 +20,10 @@ const api = {
|
|||||||
onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)),
|
onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)),
|
||||||
selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory'),
|
selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory'),
|
||||||
getSettings: () => electronAPI.ipcRenderer.invoke('config:get'),
|
getSettings: () => electronAPI.ipcRenderer.invoke('config:get'),
|
||||||
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings)
|
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings),
|
||||||
|
getNginxVersions: () => electronAPI.ipcRenderer.invoke('nginx:versions'),
|
||||||
|
downloadNginxVersion: (id: string) => electronAPI.ipcRenderer.invoke('nginx:download', id),
|
||||||
|
onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
+190
-13
@@ -53,7 +53,9 @@ import {
|
|||||||
Refresh as RefreshIcon,
|
Refresh as RefreshIcon,
|
||||||
FolderOpen as FolderOpenIcon,
|
FolderOpen as FolderOpenIcon,
|
||||||
Save as SaveIcon,
|
Save as SaveIcon,
|
||||||
Terminal as TerminalIcon
|
LanguageOutlined as WebIcon,
|
||||||
|
Terminal as TerminalIcon,
|
||||||
|
ContentCopy as CopyIcon
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@@ -90,6 +92,7 @@ function App(): JSX.Element {
|
|||||||
const [projects, setProjects] = useState<any[]>([])
|
const [projects, setProjects] = useState<any[]>([])
|
||||||
const [phpVersions, setPhpVersions] = useState<any[]>([])
|
const [phpVersions, setPhpVersions] = useState<any[]>([])
|
||||||
const [mySqlVersions, setMySqlVersions] = useState<any[]>([])
|
const [mySqlVersions, setMySqlVersions] = useState<any[]>([])
|
||||||
|
const [nginxVersions, setNginxVersions] = useState<any[]>([])
|
||||||
const [loadingVersions, setLoadingVersions] = useState(false)
|
const [loadingVersions, setLoadingVersions] = useState(false)
|
||||||
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
|
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
|
||||||
const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' })
|
const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' })
|
||||||
@@ -105,6 +108,11 @@ function App(): JSX.Element {
|
|||||||
severity: 'success'
|
severity: 'success'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Log States
|
||||||
|
const [logDialogOpen, setLogDialogOpen] = useState(false)
|
||||||
|
const [selectedService, setSelectedService] = useState('')
|
||||||
|
const [logs, setLogs] = useState<string[]>([])
|
||||||
|
|
||||||
const fetchStatus = async () => {
|
const fetchStatus = async () => {
|
||||||
if (window.api && window.api.getServiceStatus) {
|
if (window.api && window.api.getServiceStatus) {
|
||||||
const currentStatus = await window.api.getServiceStatus()
|
const currentStatus = await window.api.getServiceStatus()
|
||||||
@@ -140,10 +148,24 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchNginxVersions = async () => {
|
||||||
|
if (window.api && window.api.getNginxVersions) {
|
||||||
|
const list = await window.api.getNginxVersions()
|
||||||
|
setNginxVersions(list || [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchLogs = async (service: string) => {
|
||||||
|
if (window.api && window.api.getServiceLogs) {
|
||||||
|
const list = await window.api.getServiceLogs(service)
|
||||||
|
setLogs(list || [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleRefreshVersions = async () => {
|
const handleRefreshVersions = async () => {
|
||||||
setLoadingVersions(true)
|
setLoadingVersions(true)
|
||||||
try {
|
try {
|
||||||
await Promise.all([fetchPhpVersions(), fetchMySqlVersions()])
|
await Promise.all([fetchPhpVersions(), fetchMySqlVersions(), fetchNginxVersions()])
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingVersions(false)
|
setLoadingVersions(false)
|
||||||
}
|
}
|
||||||
@@ -165,6 +187,7 @@ function App(): JSX.Element {
|
|||||||
fetchProjects()
|
fetchProjects()
|
||||||
fetchPhpVersions()
|
fetchPhpVersions()
|
||||||
fetchMySqlVersions()
|
fetchMySqlVersions()
|
||||||
|
fetchNginxVersions()
|
||||||
fetchSettings()
|
fetchSettings()
|
||||||
|
|
||||||
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
|
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
|
||||||
@@ -179,19 +202,35 @@ function App(): JSX.Element {
|
|||||||
setMySqlVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
|
setMySqlVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
|
||||||
}) : null
|
}) : null
|
||||||
|
|
||||||
|
const removeNginxListener = window.api.onNginxProgress ? window.api.onNginxProgress((data) => {
|
||||||
|
if (!data) return
|
||||||
|
const { id, progress } = data
|
||||||
|
setNginxVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
|
||||||
|
}) : null
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchStatus()
|
fetchStatus()
|
||||||
fetchProjects()
|
fetchProjects()
|
||||||
fetchPhpVersions()
|
fetchPhpVersions()
|
||||||
fetchMySqlVersions()
|
fetchMySqlVersions()
|
||||||
|
fetchNginxVersions()
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
|
let logInterval: NodeJS.Timeout | null = null
|
||||||
|
if (logDialogOpen && selectedService) {
|
||||||
|
logInterval = setInterval(() => {
|
||||||
|
fetchLogs(selectedService)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
|
if (logInterval) clearInterval(logInterval)
|
||||||
if (typeof removePhpListener === 'function') removePhpListener()
|
if (typeof removePhpListener === 'function') removePhpListener()
|
||||||
if (typeof removeMySqlListener === 'function') removeMySqlListener()
|
if (typeof removeMySqlListener === 'function') removeMySqlListener()
|
||||||
|
if (typeof removeNginxListener === 'function') removeNginxListener()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [logDialogOpen, selectedService])
|
||||||
|
|
||||||
const handleSelectPath = async () => {
|
const handleSelectPath = async () => {
|
||||||
if (window.api && window.api.selectDirectory) {
|
if (window.api && window.api.selectDirectory) {
|
||||||
@@ -204,10 +243,19 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
const handleToggleService = async (service: keyof ServiceStatus) => {
|
const handleToggleService = async (service: keyof ServiceStatus) => {
|
||||||
if (!window.api) return
|
if (!window.api) return
|
||||||
|
|
||||||
|
const isStarting = status[service] === 'stopped'
|
||||||
|
if (isStarting) {
|
||||||
|
setStatus(prev => ({ ...prev, [service]: 'starting' }))
|
||||||
|
}
|
||||||
|
|
||||||
const result = status[service] === 'running'
|
const result = status[service] === 'running'
|
||||||
? await window.api.stopService(service)
|
? await window.api.stopService(service)
|
||||||
: await window.api.startService(service)
|
: await window.api.startService(service)
|
||||||
|
|
||||||
|
// Refresh status after operation
|
||||||
|
fetchStatus()
|
||||||
|
|
||||||
setNotification({
|
setNotification({
|
||||||
open: true,
|
open: true,
|
||||||
message: result.message,
|
message: result.message,
|
||||||
@@ -215,6 +263,12 @@ function App(): JSX.Element {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenLogs = async (service: string) => {
|
||||||
|
setSelectedService(service)
|
||||||
|
setLogDialogOpen(true)
|
||||||
|
await fetchLogs(service)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSaveSettings = async () => {
|
const handleSaveSettings = async () => {
|
||||||
if (window.api && window.api.saveSettings) {
|
if (window.api && window.api.saveSettings) {
|
||||||
const result = await window.api.saveSettings(settings)
|
const result = await window.api.saveSettings(settings)
|
||||||
@@ -232,11 +286,17 @@ function App(): JSX.Element {
|
|||||||
const handleStartAll = async () => {
|
const handleStartAll = async () => {
|
||||||
if (!window.api) return
|
if (!window.api) return
|
||||||
const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql']
|
const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql']
|
||||||
|
for (const s of services) {
|
||||||
|
if (status[s] === 'stopped') {
|
||||||
|
setStatus(prev => ({ ...prev, [s]: 'starting' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const s of services) {
|
for (const s of services) {
|
||||||
if (status[s] !== 'running') {
|
if (status[s] !== 'running') {
|
||||||
await window.api.startService(s)
|
await window.api.startService(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fetchStatus()
|
||||||
setNotification({ open: true, message: 'Tüm servisler başlatıldı.', severity: 'success' })
|
setNotification({ open: true, message: 'Tüm servisler başlatıldı.', severity: 'success' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,10 +308,14 @@ function App(): JSX.Element {
|
|||||||
await window.api.stopService(s)
|
await window.api.stopService(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fetchStatus()
|
||||||
setNotification({ open: true, message: 'Tüm servisler durduruldu.', severity: 'success' })
|
setNotification({ open: true, message: 'Tüm servisler durduruldu.', severity: 'success' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusChip = (s: string) => {
|
const getStatusChip = (s: string) => {
|
||||||
|
if (s === 'starting') {
|
||||||
|
return <Chip label="BAŞLATILIYOR..." color="warning" size="small" sx={{ fontWeight: 'bold' }} icon={<CircularProgress size={14} color="inherit" />} />
|
||||||
|
}
|
||||||
const color = s === 'running' ? 'success' : s === 'stopped' ? 'error' : 'warning'
|
const color = s === 'running' ? 'success' : s === 'stopped' ? 'error' : 'warning'
|
||||||
return <Chip label={s.toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
|
return <Chip label={s.toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
|
||||||
}
|
}
|
||||||
@@ -294,6 +358,17 @@ function App(): JSX.Element {
|
|||||||
fetchMySqlVersions()
|
fetchMySqlVersions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDownloadNginx = async (id: string) => {
|
||||||
|
if (!window.api) return
|
||||||
|
const result = await window.api.downloadNginxVersion(id)
|
||||||
|
setNotification({
|
||||||
|
open: true,
|
||||||
|
message: result.message,
|
||||||
|
severity: result.success ? 'success' : 'error'
|
||||||
|
})
|
||||||
|
fetchNginxVersions()
|
||||||
|
}
|
||||||
|
|
||||||
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
|
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -359,7 +434,7 @@ function App(): JSX.Element {
|
|||||||
<Typography variant="h5" sx={{ fontWeight: 'medium' }}>
|
<Typography variant="h5" sx={{ fontWeight: 'medium' }}>
|
||||||
Genel Ayarlar
|
Genel Ayarlar
|
||||||
</Typography>
|
</Typography>
|
||||||
{settingsTab < 2 && (
|
{settingsTab < 3 && (
|
||||||
<Button startIcon={loadingVersions ? <CircularProgress size={20} /> : <RefreshIcon />} onClick={handleRefreshVersions} disabled={loadingVersions}>
|
<Button startIcon={loadingVersions ? <CircularProgress size={20} /> : <RefreshIcon />} onClick={handleRefreshVersions} disabled={loadingVersions}>
|
||||||
Listeyi Yenile
|
Listeyi Yenile
|
||||||
</Button>
|
</Button>
|
||||||
@@ -368,8 +443,9 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||||
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">
|
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto">
|
||||||
<Tab label="PHP Sürümleri" icon={<PhpIcon />} iconPosition="start" />
|
<Tab label="PHP" icon={<PhpIcon />} iconPosition="start" />
|
||||||
<Tab label="MySQL Sürümleri" icon={<DbIcon />} iconPosition="start" />
|
<Tab label="MySQL" icon={<DbIcon />} iconPosition="start" />
|
||||||
|
<Tab label="Nginx" icon={<WebIcon />} iconPosition="start" />
|
||||||
<Tab label="Sunucu Ayarları" icon={<ServerIcon />} iconPosition="start" />
|
<Tab label="Sunucu Ayarları" icon={<ServerIcon />} iconPosition="start" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -464,6 +540,44 @@ function App(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{settingsTab === 2 && (
|
{settingsTab === 2 && (
|
||||||
|
<Paper sx={{ bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
|
||||||
|
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>Nginx Web Sunucusu</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">Sunucu için gerekli Nginx binary dosyalarını buradan indirebilirsiniz.</Typography>
|
||||||
|
</Box>
|
||||||
|
<List>
|
||||||
|
{nginxVersions.map((v) => (
|
||||||
|
<ListItem key={v.id} sx={{ py: 2 }}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{v.status === 'installed' ? <InstalledIcon color="success" /> : <AvailableIcon color="action" />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={`Nginx ${v.version} (Stable)`}
|
||||||
|
secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'}
|
||||||
|
sx={{ flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
<Box sx={{ minWidth: 120, textAlign: 'right' }}>
|
||||||
|
{v.status === 'available' && (
|
||||||
|
<Button variant="outlined" size="small" startIcon={<DownloadIcon />} onClick={() => handleDownloadNginx(v.id)}>
|
||||||
|
İNDİR
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{v.status === 'downloading' && (
|
||||||
|
<Box sx={{ width: '100%', mt: 1 }}>
|
||||||
|
<LinearProgress variant="determinate" value={v.progress} sx={{ height: 8, borderRadius: 5 }} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{v.status === 'installed' && (
|
||||||
|
<Chip label="HAZIR" color="success" variant="outlined" size="small" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{settingsTab === 3 && (
|
||||||
<Box component={Stack} spacing={3}>
|
<Box component={Stack} spacing={3}>
|
||||||
<Paper sx={{ p: 3, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
|
<Paper sx={{ p: 3, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
|
||||||
<Typography variant="h6" gutterBottom>Port Ayarları</Typography>
|
<Typography variant="h6" gutterBottom>Port Ayarları</Typography>
|
||||||
@@ -511,29 +625,50 @@ function App(): JSX.Element {
|
|||||||
<Paper elevation={4} sx={{ mb: 4, borderRadius: 3, overflow: 'hidden', bgcolor: 'rgba(255, 255, 255, 0.03)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
<Paper elevation={4} sx={{ mb: 4, borderRadius: 3, overflow: 'hidden', bgcolor: 'rgba(255, 255, 255, 0.03)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||||
<List dense>
|
<List dense>
|
||||||
<ListItem sx={{ py: 1.5 }}>
|
<ListItem sx={{ py: 1.5 }}>
|
||||||
<ListItemIcon><ServerIcon color="primary" /></ListItemIcon>
|
<ListItemIcon><WebIcon color="primary" /></ListItemIcon>
|
||||||
<ListItemText primary="Nginx Web Sunucusu" secondary={`Port: ${settings.nginxPort}`} />
|
<ListItemText primary="Nginx Web Sunucusu" secondary={`Port: ${settings.nginxPort}`} />
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<IconButton size="small" onClick={() => handleOpenLogs('nginx')} title="Logları Görüntüle">
|
||||||
|
<TerminalIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
{getStatusChip(status.nginx)}
|
{getStatusChip(status.nginx)}
|
||||||
<Switch checked={status.nginx === 'running'} onChange={() => handleToggleService('nginx')} />
|
<Switch
|
||||||
|
checked={status.nginx === 'running' || status.nginx === 'starting'}
|
||||||
|
onChange={() => handleToggleService('nginx')}
|
||||||
|
disabled={status.nginx === 'starting'}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" />
|
<Divider variant="inset" />
|
||||||
<ListItem sx={{ py: 1.5 }}>
|
<ListItem sx={{ py: 1.5 }}>
|
||||||
<ListItemIcon><PhpIcon color="primary" /></ListItemIcon>
|
<ListItemIcon><PhpIcon color="primary" /></ListItemIcon>
|
||||||
<ListItemText primary="PHP FastCGI" secondary={`Port: ${settings.phpPort}`} />
|
<ListItemText primary="PHP FastCGI" secondary={`Port: ${settings.phpPort}`} />
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<IconButton size="small" onClick={() => handleOpenLogs('php')} title="Logları Görüntüle">
|
||||||
|
<TerminalIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
{getStatusChip(status.php)}
|
{getStatusChip(status.php)}
|
||||||
<Switch checked={status.php === 'running'} onChange={() => handleToggleService('php')} />
|
<Switch
|
||||||
|
checked={status.php === 'running' || status.php === 'starting'}
|
||||||
|
onChange={() => handleToggleService('php')}
|
||||||
|
disabled={status.php === 'starting'}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" />
|
<Divider variant="inset" />
|
||||||
<ListItem sx={{ py: 1.5 }}>
|
<ListItem sx={{ py: 1.5 }}>
|
||||||
<ListItemIcon><DbIcon color="primary" /></ListItemIcon>
|
<ListItemIcon><DbIcon color="primary" /></ListItemIcon>
|
||||||
<ListItemText primary="MySQL / MariaDB" secondary={`Port: ${settings.mysqlPort}`} />
|
<ListItemText primary="MySQL / MariaDB" secondary={`Port: ${settings.mysqlPort}`} />
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<IconButton size="small" onClick={() => handleOpenLogs('mysql')} title="Logları Görüntüle">
|
||||||
|
<TerminalIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
{getStatusChip(status.mysql)}
|
{getStatusChip(status.mysql)}
|
||||||
<Switch checked={status.mysql === 'running'} onChange={() => handleToggleService('mysql')} />
|
<Switch
|
||||||
|
checked={status.mysql === 'running' || status.mysql === 'starting'}
|
||||||
|
onChange={() => handleToggleService('mysql')}
|
||||||
|
disabled={status.mysql === 'starting'}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
@@ -662,6 +797,48 @@ function App(): JSX.Element {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Log Dialog */}
|
||||||
|
<Dialog open={logDialogOpen} onClose={() => setLogDialogOpen(false)} fullWidth maxWidth="md">
|
||||||
|
<DialogTitle sx={{ bgcolor: '#1e1e1e', color: '#fff', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<TerminalIcon />
|
||||||
|
{selectedService.toUpperCase()} Konsol Çıktısı
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<IconButton size="small" sx={{ color: '#fff' }} onClick={() => navigator.clipboard.writeText(logs.join('\n'))}>
|
||||||
|
<CopyIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent sx={{ bgcolor: '#000', p: 0 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
fontFamily: 'Consolas, monospace',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
color: '#00ff00',
|
||||||
|
height: '400px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<Typography variant="caption" sx={{ color: '#666' }}>Henüz log kaydı yok...</Typography>
|
||||||
|
) : (
|
||||||
|
logs.map((line, i) => (
|
||||||
|
<Box key={i} sx={{ mb: 0.5, borderLeft: '2px solid #333', pl: 1 }}>
|
||||||
|
{line}
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ bgcolor: '#1e1e1e' }}>
|
||||||
|
<Button onClick={() => setLogDialogOpen(false)} variant="contained" size="small">Kapat</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user