diff --git a/config/php.ini.template b/config/php.ini.template index 6225177..6a3da4f 100644 --- a/config/php.ini.template +++ b/config/php.ini.template @@ -41,6 +41,7 @@ max_file_uploads = 20 allow_url_fopen = On allow_url_include = Off default_socket_timeout = 60 +extension_dir = "{{EXT_DIR}}" [ExtensionList] extension=curl diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 30456c1..478e00e 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -6,6 +6,7 @@ import { downloadService } from '../services/DownloadService' import { projectService } from '../services/ProjectService' import { phpManagerService } from '../services/PhpManagerService' import { mySqlManagerService } from '../services/MySqlManagerService' +import { nginxManagerService } from '../services/NginxManagerService' import path from 'path' function findExecutable(dir: string, name: string): string | null { @@ -49,14 +50,16 @@ export function registerIpcHandlers(): void { } 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') - 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]) - 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') { @@ -98,6 +101,10 @@ export function registerIpcHandlers(): void { return processManager.getStatuses() }) + ipcMain.handle('services:logs', async (_event, serviceName: string) => { + return processManager.getLogs(serviceName) + }) + // Binary management ipcMain.handle('binaries:list-php', async () => { 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 () => { const result = await dialog.showOpenDialog({ properties: ['openDirectory'] diff --git a/src/main/services/NginxManagerService.ts b/src/main/services/NginxManagerService.ts new file mode 100644 index 0000000..3df9627 --- /dev/null +++ b/src/main/services/NginxManagerService.ts @@ -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 { + // 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() diff --git a/src/main/services/ProcessManager.ts b/src/main/services/ProcessManager.ts index e36946a..aac164b 100644 --- a/src/main/services/ProcessManager.ts +++ b/src/main/services/ProcessManager.ts @@ -1,22 +1,80 @@ -import { spawn, ChildProcess } from 'child_process' -import { join } from 'path' +import { spawn, ChildProcess, exec } from 'child_process' +import path from 'path' import { app } from 'electron' import fs from 'fs' export class ProcessManager { private processes: Map = new Map() + private logs: Map = new Map() + private logDir: string constructor() { 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 { 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) => { resolve(code === 0) }) - child.on('error', () => { + child.on('error', (err) => { + console.error('RunOnce error:', err) resolve(false) }) }) @@ -24,43 +82,47 @@ export class ProcessManager { async startService(name: string, binPath: string, args: string[]): Promise { if (this.processes.has(name)) { - console.log(`Service ${name} is already running.`) return true } if (!fs.existsSync(binPath)) { - console.warn(`[DEV] Executable not found: ${binPath}. Mocking service ${name}.`) - const mockChild = { - kill: () => { }, - on: () => { }, - unref: () => { } - } as unknown as ChildProcess - this.processes.set(name, mockChild) - return true + this.addLog(name, `Executable not found: ${binPath}`) + return false } try { + this.addLog(name, `Starting service with: ${binPath} ${args.join(' ')}`) + const child = spawn(binPath, args, { 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) => { - console.error(`Failed to start service ${name}:`, err) + this.addLog(name, `Failed to start: ${err.message}`) this.processes.delete(name) }) 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.set(name, child) - child.unref() // Allow the parent to exit independently + child.unref() return true - } catch (error) { - console.error(`Error starting service ${name}:`, error) + } catch (error: any) { + this.addLog(name, `Spawn Error: ${error.message}`) return false } } @@ -68,11 +130,25 @@ export class ProcessManager { async stopService(name: string): Promise { const child = this.processes.get(name) if (child) { + this.addLog(name, 'Stopping service...') child.kill() this.processes.delete(name) - return true } - return false + + // Also force kill lingering processes + return this.forceKillAll(name) + } + + async forceKillAll(name: string): Promise { + 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 { diff --git a/src/preload/index.ts b/src/preload/index.ts index e8172a6..909afda 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -6,6 +6,7 @@ const api = { startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name), stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name), getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'), + getServiceLogs: (name: string) => electronAPI.ipcRenderer.invoke('services:logs'), getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key), saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config), getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'), @@ -19,7 +20,10 @@ const api = { onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)), selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory'), 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 diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 40f516e..236e59b 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -53,7 +53,9 @@ import { Refresh as RefreshIcon, FolderOpen as FolderOpenIcon, Save as SaveIcon, - Terminal as TerminalIcon + LanguageOutlined as WebIcon, + Terminal as TerminalIcon, + ContentCopy as CopyIcon } from '@mui/icons-material' import { useTranslation } from 'react-i18next' @@ -90,6 +92,7 @@ function App(): JSX.Element { const [projects, setProjects] = useState([]) const [phpVersions, setPhpVersions] = useState([]) const [mySqlVersions, setMySqlVersions] = useState([]) + const [nginxVersions, setNginxVersions] = useState([]) const [loadingVersions, setLoadingVersions] = useState(false) const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false) const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' }) @@ -105,6 +108,11 @@ function App(): JSX.Element { severity: 'success' }) + // Log States + const [logDialogOpen, setLogDialogOpen] = useState(false) + const [selectedService, setSelectedService] = useState('') + const [logs, setLogs] = useState([]) + const fetchStatus = async () => { if (window.api && 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 () => { setLoadingVersions(true) try { - await Promise.all([fetchPhpVersions(), fetchMySqlVersions()]) + await Promise.all([fetchPhpVersions(), fetchMySqlVersions(), fetchNginxVersions()]) } finally { setLoadingVersions(false) } @@ -165,6 +187,7 @@ function App(): JSX.Element { fetchProjects() fetchPhpVersions() fetchMySqlVersions() + fetchNginxVersions() fetchSettings() 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)) }) : 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(() => { fetchStatus() fetchProjects() fetchPhpVersions() fetchMySqlVersions() + fetchNginxVersions() }, 5000) + let logInterval: NodeJS.Timeout | null = null + if (logDialogOpen && selectedService) { + logInterval = setInterval(() => { + fetchLogs(selectedService) + }, 1000) + } + return () => { clearInterval(interval) + if (logInterval) clearInterval(logInterval) if (typeof removePhpListener === 'function') removePhpListener() if (typeof removeMySqlListener === 'function') removeMySqlListener() + if (typeof removeNginxListener === 'function') removeNginxListener() } - }, []) + }, [logDialogOpen, selectedService]) const handleSelectPath = async () => { if (window.api && window.api.selectDirectory) { @@ -204,10 +243,19 @@ function App(): JSX.Element { const handleToggleService = async (service: keyof ServiceStatus) => { if (!window.api) return + + const isStarting = status[service] === 'stopped' + if (isStarting) { + setStatus(prev => ({ ...prev, [service]: 'starting' })) + } + const result = status[service] === 'running' ? await window.api.stopService(service) : await window.api.startService(service) + // Refresh status after operation + fetchStatus() + setNotification({ open: true, 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 () => { if (window.api && window.api.saveSettings) { const result = await window.api.saveSettings(settings) @@ -232,11 +286,17 @@ function App(): JSX.Element { const handleStartAll = async () => { if (!window.api) return 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) { if (status[s] !== 'running') { await window.api.startService(s) } } + fetchStatus() 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) } } + fetchStatus() setNotification({ open: true, message: 'Tüm servisler durduruldu.', severity: 'success' }) } const getStatusChip = (s: string) => { + if (s === 'starting') { + return } /> + } const color = s === 'running' ? 'success' : s === 'stopped' ? 'error' : 'warning' return } @@ -294,6 +358,17 @@ function App(): JSX.Element { 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') return ( @@ -359,7 +434,7 @@ function App(): JSX.Element { Genel Ayarlar - {settingsTab < 2 && ( + {settingsTab < 3 && ( @@ -368,8 +443,9 @@ function App(): JSX.Element { setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto"> - } iconPosition="start" /> - } iconPosition="start" /> + } iconPosition="start" /> + } iconPosition="start" /> + } iconPosition="start" /> } iconPosition="start" /> @@ -464,6 +540,44 @@ function App(): JSX.Element { )} {settingsTab === 2 && ( + + + Nginx Web Sunucusu + Sunucu için gerekli Nginx binary dosyalarını buradan indirebilirsiniz. + + + {nginxVersions.map((v) => ( + + + {v.status === 'installed' ? : } + + + + {v.status === 'available' && ( + + )} + {v.status === 'downloading' && ( + + + + )} + {v.status === 'installed' && ( + + )} + + + ))} + + + )} + + {settingsTab === 3 && ( Port Ayarları @@ -511,29 +625,50 @@ function App(): JSX.Element { - + - + + handleOpenLogs('nginx')} title="Logları Görüntüle"> + + {getStatusChip(status.nginx)} - handleToggleService('nginx')} /> + handleToggleService('nginx')} + disabled={status.nginx === 'starting'} + /> - + + handleOpenLogs('php')} title="Logları Görüntüle"> + + {getStatusChip(status.php)} - handleToggleService('php')} /> + handleToggleService('php')} + disabled={status.php === 'starting'} + /> - + + handleOpenLogs('mysql')} title="Logları Görüntüle"> + + {getStatusChip(status.mysql)} - handleToggleService('mysql')} /> + handleToggleService('mysql')} + disabled={status.mysql === 'starting'} + /> @@ -662,6 +797,48 @@ function App(): JSX.Element { + + {/* Log Dialog */} + setLogDialogOpen(false)} fullWidth maxWidth="md"> + + + + {selectedService.toUpperCase()} Konsol Çıktısı + + + navigator.clipboard.writeText(logs.join('\n'))}> + + + + + + + {logs.length === 0 ? ( + Henüz log kaydı yok... + ) : ( + logs.map((line, i) => ( + + {line} + + )) + )} + + + + + + ) }