feat: implement process management service with IPC and configuration templates
This commit is contained in:
@@ -9,14 +9,14 @@ http {
|
|||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
sendfile on;
|
sendfile on;
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
server_names_hash_bucket_size 64;
|
|
||||||
|
{{PROJECTS}}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen {{LISTEN_ADDRESS}}:{{PORT}};
|
listen {{LISTEN_ADDRESS}}:{{PORT}};
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
root "{{ROOT}}";
|
root "{{ROOT}}";
|
||||||
index index.php index.html index.htm;
|
index index.php index.html index.htm;
|
||||||
autoindex on;
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# Try global root first, fallback to @project_fallback if not found
|
# Try global root first, fallback to @project_fallback if not found
|
||||||
@@ -38,10 +38,8 @@ http {
|
|||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Subdirectory access for projects (legacy support)
|
|
||||||
{{PROJECT_LOCATIONS}}
|
{{PROJECT_LOCATIONS}}
|
||||||
|
|
||||||
|
|
||||||
location /phpmyadmin {
|
location /phpmyadmin {
|
||||||
alias "{{PHPMYADMIN_DIR}}";
|
alias "{{PHPMYADMIN_DIR}}";
|
||||||
index index.php;
|
index index.php;
|
||||||
@@ -60,8 +58,7 @@ http {
|
|||||||
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
|
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||||
|
include "{{CONF_DIR}}/fastcgi_params";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{{PROJECTS}}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ sys_temp_dir = "{{TEMP_DIR}}"
|
|||||||
extension_dir = "{{EXT_DIR}}"
|
extension_dir = "{{EXT_DIR}}"
|
||||||
fastcgi.impersonate = 1
|
fastcgi.impersonate = 1
|
||||||
cgi.force_redirect = 0
|
cgi.force_redirect = 0
|
||||||
cgi.fix_pathinfo = 1
|
cgi.fix_pathinfo = 0
|
||||||
|
|
||||||
[ExtensionList]
|
[ExtensionList]
|
||||||
{{EXTENSIONS}}
|
{{EXTENSIONS}}
|
||||||
|
|||||||
+1263
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"nginxPort": "8081",
|
"nginxPort": "8081",
|
||||||
"apachePort": "8080",
|
"apachePort": "8082",
|
||||||
"serverType": "nginx",
|
"serverType": "nginx",
|
||||||
"phpPort": "9001",
|
"phpPort": "9001",
|
||||||
"mariaDbPort": "3306",
|
"mariaDbPort": "3306",
|
||||||
|
|||||||
+67
-35
@@ -76,7 +76,7 @@ async function rebuildNginxConfig(forceProjects: boolean = false): Promise<{ bin
|
|||||||
|
|
||||||
// Ensure directories exist
|
// Ensure directories exist
|
||||||
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects')
|
const projectsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects')
|
||||||
const locationsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects_locations')
|
const locationsConfDir = path.join(configService.getBasePath(), 'generated_configs', 'projects_nginx')
|
||||||
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
|
if (!fs.existsSync(projectsConfDir)) fs.mkdirSync(projectsConfDir, { recursive: true })
|
||||||
if (!fs.existsSync(locationsConfDir)) fs.mkdirSync(locationsConfDir, { recursive: true })
|
if (!fs.existsSync(locationsConfDir)) fs.mkdirSync(locationsConfDir, { recursive: true })
|
||||||
|
|
||||||
@@ -128,8 +128,9 @@ async function rebuildNginxConfig(forceProjects: boolean = false): Promise<{ bin
|
|||||||
PORT: settings.nginxPort
|
PORT: settings.nginxPort
|
||||||
})
|
})
|
||||||
|
|
||||||
// Also generate the subdirectory/alias-style config for localhost fallback
|
// Use the correct location-based template for subdirectories
|
||||||
await configService.generateConfig('nginx_project_location.conf.template', `projects_locations/${p.host}.conf`, {
|
// This prevents the 'server directive not allowed here' error
|
||||||
|
await configService.generateConfig('nginx_project_location.conf.template', `projects_nginx/${p.host}.conf`, {
|
||||||
HOST: p.host, // Use simple host slug for location path
|
HOST: p.host, // Use simple host slug for location path
|
||||||
PATH: normalizedPath,
|
PATH: normalizedPath,
|
||||||
PHP_PORT: phpPort.toString(),
|
PHP_PORT: phpPort.toString(),
|
||||||
@@ -459,6 +460,11 @@ export function registerIpcHandlers(): void {
|
|||||||
return { success: false, message: `Nginx başlatılamadı.\n\n${errorLogs.join('\n')}` }
|
return { success: false, message: `Nginx başlatılamadı.\n\n${errorLogs.join('\n')}` }
|
||||||
}
|
}
|
||||||
const ready = await waitForPort(settings.nginxPort, 5000);
|
const ready = await waitForPort(settings.nginxPort, 5000);
|
||||||
|
if (ready) {
|
||||||
|
processManager.setServiceState('nginx', 'running')
|
||||||
|
} else {
|
||||||
|
processManager.setServiceState('nginx', 'stopped')
|
||||||
|
}
|
||||||
return { success: ready, message: ready ? 'Nginx başlatıldı.' : 'Nginx başlatıldı ancak port cevabı alınamadı.' }
|
return { success: ready, message: ready ? 'Nginx başlatıldı.' : 'Nginx başlatıldı ancak port cevabı alınamadı.' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +479,11 @@ export function registerIpcHandlers(): void {
|
|||||||
return { success: false, message: `Apache başlatılamadı.\n\n${errorLogs.join('\n')}` }
|
return { success: false, message: `Apache başlatılamadı.\n\n${errorLogs.join('\n')}` }
|
||||||
}
|
}
|
||||||
const ready = await waitForPort(parseInt(settings.apachePort || '8080'), 5000);
|
const ready = await waitForPort(parseInt(settings.apachePort || '8080'), 5000);
|
||||||
|
if (ready) {
|
||||||
|
processManager.setServiceState('apache', 'running')
|
||||||
|
} else {
|
||||||
|
processManager.setServiceState('apache', 'stopped')
|
||||||
|
}
|
||||||
return { success: ready, message: ready ? 'Apache başlatıldı.' : 'Apache başlatıldı ancak port cevabı alınamadı.' }
|
return { success: ready, message: ready ? 'Apache başlatıldı.' : 'Apache başlatıldı ancak port cevabı alınamadı.' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +547,7 @@ export function registerIpcHandlers(): void {
|
|||||||
extensionsList = mustHaves.map(m => `extension=${prefix}${m}${suffix}`).join('\n')
|
extensionsList = mustHaves.map(m => `extension=${prefix}${m}${suffix}`).join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
const configPath = await configService.generateConfig('php.ini.template', `php_${phpVersion || 'default'}.ini`, {
|
const configPath = await configService.generateConfig('php.ini.template', `php/${phpVersion || 'default'}/php.ini`, {
|
||||||
EXT_DIR: extDir,
|
EXT_DIR: extDir,
|
||||||
GD_EXT: gdExtName,
|
GD_EXT: gdExtName,
|
||||||
SESSION_DIR: sessionDir,
|
SESSION_DIR: sessionDir,
|
||||||
@@ -548,6 +559,7 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
// Add a small delay to ensure previous instance (if any) is fully killed
|
// Add a small delay to ensure previous instance (if any) is fully killed
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
|
// Set PHPRC to the directory containing the project-specific php.ini
|
||||||
const success = await processManager.startService(serviceName, binPath, ['-b', `127.0.0.1:${phpPort}`, '-c', configPath], false, { PHPRC: path.dirname(configPath) })
|
const success = await processManager.startService(serviceName, binPath, ['-b', `127.0.0.1:${phpPort}`, '-c', configPath], false, { PHPRC: path.dirname(configPath) })
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@@ -556,6 +568,11 @@ export function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ready = await waitForPort(phpPort, 5000);
|
const ready = await waitForPort(phpPort, 5000);
|
||||||
|
if (ready) {
|
||||||
|
processManager.setServiceState(serviceName, 'running')
|
||||||
|
} else {
|
||||||
|
processManager.setServiceState(serviceName, 'stopped')
|
||||||
|
}
|
||||||
return { success: ready, message: ready ? `${phpVersion || ''} PHP başlatıldı.` : `${phpVersion || ''} PHP başlatıldı ancak port cevabı alınamadı.` }
|
return { success: ready, message: ready ? `${phpVersion || ''} PHP başlatıldı.` : `${phpVersion || ''} PHP başlatıldı ancak port cevabı alınamadı.` }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,17 +652,19 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// Quick health check
|
// Quick health check
|
||||||
await new Promise(r => setTimeout(r, 1500))
|
const ready = await waitForPort(parseInt(dbPort), 7000);
|
||||||
if (!processManager.isServiceRunning(serviceName)) {
|
if (!ready || !processManager.isServiceRunning(serviceName)) {
|
||||||
const errorLogs = await processManager.getLastErrorLogs(serviceName)
|
const errorLogs = await processManager.getLastErrorLogs(serviceName)
|
||||||
return { success: false, message: `${dbVersion || ''} MariaDB başlatıldı ancak çalışma hatası verdi (ExitCode: 1).\n\n${errorLogs.length > 0 ? 'Önemli Hata Logları:\n' + errorLogs.join('\n') : 'Lütfen log sekmesini kontrol edin.'}` }
|
processManager.setServiceState(serviceName, 'stopped')
|
||||||
|
return { success: false, message: `${dbVersion || ''} MariaDB başlatıldı ancak çalışma hatası verdi.\n\n${errorLogs.length > 0 ? 'Önemli Hata Logları:\n' + errorLogs.join('\n') : 'Lütfen log sekmesini kontrol edin.'}` }
|
||||||
}
|
}
|
||||||
|
processManager.setServiceState(serviceName, 'running')
|
||||||
|
return { success: true, message: `${dbVersion || ''} MariaDB başlatıldı.` }
|
||||||
} else {
|
} else {
|
||||||
const errorLogs = await processManager.getLastErrorLogs(serviceName)
|
const errorLogs = await processManager.getLastErrorLogs(serviceName)
|
||||||
|
processManager.setServiceState(serviceName, 'stopped')
|
||||||
return { success: false, message: `${dbVersion || ''} MariaDB başlatılamadı.\n\n${errorLogs.join('\n')}` }
|
return { success: false, message: `${dbVersion || ''} MariaDB başlatılamadı.\n\n${errorLogs.join('\n')}` }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success, message: success ? `${dbVersion || ''} MariaDB başlatıldı.` : `${dbVersion || ''} MariaDB devreye alınamadı.` }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, message: 'Bilinmeyen servis.' }
|
return { success: false, message: 'Bilinmeyen servis.' }
|
||||||
@@ -656,8 +675,12 @@ export function registerIpcHandlers(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('services:stop', async (_event, serviceName: string) => {
|
ipcMain.handle('services:stop', async (_event, serviceName: string) => {
|
||||||
const success = await processManager.stopService(serviceName)
|
try {
|
||||||
return { success, message: success ? `${serviceName} durduruldu.` : `${serviceName} zaten durmuş.` }
|
const success = await processManager.stopService(serviceName)
|
||||||
|
return { success, message: success ? `${serviceName} durduruldu.` : `${serviceName} durdurulurken hata oluştu veya zaten durmuş.` }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, message: `Durdurma hatası: ${error.message}` }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('services:status', async () => {
|
ipcMain.handle('services:status', async () => {
|
||||||
@@ -769,15 +792,23 @@ export function registerIpcHandlers(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('php:config:get', async (_event, version: string) => {
|
ipcMain.handle('php:config:get', async (_event, version: string) => {
|
||||||
const phpIniPath = path.join(configService.getBasePath(), 'generated_configs', `php_${version}.ini`)
|
const phpIniPath = path.join(configService.getBasePath(), 'generated_configs', 'php', version, 'php.ini')
|
||||||
if (fs.existsSync(phpIniPath)) {
|
if (fs.existsSync(phpIniPath)) {
|
||||||
return { content: fs.readFileSync(phpIniPath, 'utf8') }
|
return { content: fs.readFileSync(phpIniPath, 'utf8') }
|
||||||
}
|
}
|
||||||
|
// Fallback to old path for migration support
|
||||||
|
const oldPath = path.join(configService.getBasePath(), 'generated_configs', `php_${version}.ini`)
|
||||||
|
if (fs.existsSync(oldPath)) {
|
||||||
|
return { content: fs.readFileSync(oldPath, 'utf8') }
|
||||||
|
}
|
||||||
return { content: '' }
|
return { content: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('php:config:save', async (_event, { version, content }) => {
|
ipcMain.handle('php:config:save', async (_event, { version, content }) => {
|
||||||
const phpIniPath = path.join(configService.getBasePath(), 'generated_configs', `php_${version}.ini`)
|
const phpIniDir = path.join(configService.getBasePath(), 'generated_configs', 'php', version)
|
||||||
|
if (!fs.existsSync(phpIniDir)) fs.mkdirSync(phpIniDir, { recursive: true })
|
||||||
|
const phpIniPath = path.join(phpIniDir, 'php.ini')
|
||||||
|
|
||||||
// Add manual edit marker to prevent overwriting
|
// Add manual edit marker to prevent overwriting
|
||||||
let finalContent = content
|
let finalContent = content
|
||||||
if (!finalContent.includes('; MANUAL_EDIT')) {
|
if (!finalContent.includes('; MANUAL_EDIT')) {
|
||||||
@@ -1017,39 +1048,40 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
ipcMain.handle('project:get-config', async (_event, { host, serverType }) => {
|
ipcMain.handle('project:get-config', async (_event, { host, serverType }) => {
|
||||||
const projectsDir = serverType === 'apache' ? 'projects_apache' : 'projects'
|
const projectsDir = serverType === 'apache' ? 'projects_apache' : 'projects'
|
||||||
const baseDir = path.join(configService.getBasePath(), 'generated_configs', projectsDir)
|
|
||||||
|
|
||||||
// Try both host.conf and host.local.conf
|
// Prefer projects_nginx for Nginx as requested by user, fallback to projects
|
||||||
const paths = [
|
const dirs = serverType === 'nginx' ? ['projects_nginx', 'projects'] : [projectsDir]
|
||||||
path.join(baseDir, `${host}.conf`),
|
const baseDir = configService.getBasePath()
|
||||||
path.join(baseDir, `${host}.local.conf`)
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const p of paths) {
|
for (const dir of dirs) {
|
||||||
if (fs.existsSync(p)) {
|
const pathToCheck = path.join(baseDir, 'generated_configs', dir, `${host}.conf`)
|
||||||
return { content: fs.readFileSync(p, 'utf8'), path: p }
|
if (fs.existsSync(pathToCheck)) {
|
||||||
|
return { content: fs.readFileSync(pathToCheck, 'utf8'), path: pathToCheck }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try .local.conf as well
|
||||||
|
const localPath = path.join(baseDir, 'generated_configs', dir, `${host}.local.conf`)
|
||||||
|
if (fs.existsSync(localPath)) {
|
||||||
|
return { content: fs.readFileSync(localPath, 'utf8'), path: localPath }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { content: '', message: `Konfigürasyon dosyası bulunamadı. Aranan yollar: ${paths.join(', ')}` }
|
return { content: '', message: `Konfigürasyon dosyası bulunamadı. Aranan klasörler: ${dirs.join(', ')}` }
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('project:save-config', async (_event, { host, serverType, content }) => {
|
ipcMain.handle('project:save-config', async (_event, { host, serverType, content }) => {
|
||||||
const projectsDir = serverType === 'apache' ? 'projects_apache' : 'projects'
|
const projectsDir = serverType === 'apache' ? 'projects_apache' : 'projects'
|
||||||
const baseDir = path.join(configService.getBasePath(), 'generated_configs', projectsDir)
|
const dirs = serverType === 'nginx' ? ['projects_nginx', 'projects'] : [projectsDir]
|
||||||
|
const baseDir = configService.getBasePath()
|
||||||
|
|
||||||
// Try to find existing file first (to overwrite the right one)
|
// Find existing file to overwrite, or default to the first folder
|
||||||
const paths = [
|
let configPath = path.join(baseDir, 'generated_configs', dirs[0], `${host}.conf`)
|
||||||
path.join(baseDir, `${host}.conf`),
|
|
||||||
path.join(baseDir, `${host}.local.conf`)
|
|
||||||
]
|
|
||||||
|
|
||||||
let configPath = paths[0]
|
for (const dir of dirs) {
|
||||||
for (const p of paths) {
|
const p1 = path.join(baseDir, 'generated_configs', dir, `${host}.conf`)
|
||||||
if (fs.existsSync(p)) {
|
const p2 = path.join(baseDir, 'generated_configs', dir, `${host}.local.conf`)
|
||||||
configPath = p
|
if (fs.existsSync(p1)) { configPath = p1; break; }
|
||||||
break
|
if (fs.existsSync(p2)) { configPath = p2; break; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let finalContent = content
|
let finalContent = content
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ export class ConfigService {
|
|||||||
content = content.replace(new RegExp(`{{${key}}}`, 'g'), () => escapedValue)
|
content = content.replace(new RegExp(`{{${key}}}`, 'g'), () => escapedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetDir = path.dirname(targetPath)
|
||||||
|
if (!fs.existsSync(targetDir)) {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFileSync(targetPath, content)
|
fs.writeFileSync(targetPath, content)
|
||||||
return targetPath
|
return targetPath
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import { t } from '../utils/i18n'
|
|||||||
|
|
||||||
export class ProcessManager {
|
export class ProcessManager {
|
||||||
private processes: Map<string, ChildProcess> = new Map()
|
private processes: Map<string, ChildProcess> = new Map()
|
||||||
|
private states: Map<string, 'starting' | 'running' | 'stopping' | 'stopped'> = new Map()
|
||||||
private logs: Map<string, string[]> = new Map()
|
private logs: Map<string, string[]> = new Map()
|
||||||
private logDir: string
|
private logDir: string
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.processes = new Map()
|
this.processes = new Map()
|
||||||
|
this.states = new Map()
|
||||||
this.logDir = ''
|
this.logDir = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +69,15 @@ export class ProcessManager {
|
|||||||
return this.logs.get(name) || []
|
return this.logs.get(name) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setServiceState(name: string, state: 'starting' | 'running' | 'stopping' | 'stopped') {
|
||||||
|
console.log(`[STATE] ${name} -> ${state}`)
|
||||||
|
this.states.set(name, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
getServiceState(name: string): string {
|
||||||
|
return this.states.get(name) || (this.processes.has(name) ? 'running' : 'stopped')
|
||||||
|
}
|
||||||
|
|
||||||
async runOnce(binPath: string, args: string[], name?: string): Promise<{ success: boolean; output: string }> {
|
async runOnce(binPath: string, args: string[], name?: string): Promise<{ success: boolean; output: string }> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = spawn(binPath, args, { stdio: 'pipe', shell: false, windowsHide: true })
|
const child = spawn(binPath, args, { stdio: 'pipe', shell: false, windowsHide: true })
|
||||||
@@ -102,15 +113,18 @@ export class ProcessManager {
|
|||||||
|
|
||||||
async startService(name: string, binPath: string, args: string[], shell: boolean = true, envVars: Record<string, string> = {}): Promise<boolean> {
|
async startService(name: string, binPath: string, args: string[], shell: boolean = true, envVars: Record<string, string> = {}): Promise<boolean> {
|
||||||
if (this.processes.has(name)) {
|
if (this.processes.has(name)) {
|
||||||
|
this.states.set(name, 'running')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(binPath)) {
|
if (!fs.existsSync(binPath)) {
|
||||||
this.addLog(name, `${t('common.error')}: ${t('common.executable_not_found')} (${binPath})`)
|
this.addLog(name, `${t('common.error')}: ${t('common.executable_not_found')} (${binPath})`)
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.states.set(name, 'starting')
|
||||||
this.addLog(name, `${t('common.starting')}... (${binPath})`)
|
this.addLog(name, `${t('common.starting')}... (${binPath})`)
|
||||||
|
|
||||||
const serviceEnv = { ...process.env, ...envVars }
|
const serviceEnv = { ...process.env, ...envVars }
|
||||||
@@ -129,6 +143,11 @@ export class ProcessManager {
|
|||||||
env: serviceEnv
|
env: serviceEnv
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!child.pid) {
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[PROCESS] Started ${name} with PID ${child.pid}`)
|
console.log(`[PROCESS] Started ${name} with PID ${child.pid}`)
|
||||||
this.addLog(name, `${t('common.running')} (PID: ${child.pid})`)
|
this.addLog(name, `${t('common.running')} (PID: ${child.pid})`)
|
||||||
serviceEventLogger.log({ service: name, action: 'start', status: 'success' })
|
serviceEventLogger.log({ service: name, action: 'start', status: 'success' })
|
||||||
@@ -140,6 +159,7 @@ export class ProcessManager {
|
|||||||
this.addLog(name, `Failed to start: ${err.message}`)
|
this.addLog(name, `Failed to start: ${err.message}`)
|
||||||
serviceEventLogger.log({ service: name, action: 'start', status: 'failure', details: err.message })
|
serviceEventLogger.log({ service: name, action: 'start', status: 'failure', details: err.message })
|
||||||
this.processes.delete(name)
|
this.processes.delete(name)
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
})
|
})
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
child.on('exit', (code) => {
|
||||||
@@ -150,38 +170,72 @@ export class ProcessManager {
|
|||||||
this.checkErrorLogs(name)
|
this.checkErrorLogs(name)
|
||||||
}
|
}
|
||||||
this.processes.delete(name)
|
this.processes.delete(name)
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
})
|
})
|
||||||
|
|
||||||
this.processes.set(name, child)
|
this.processes.set(name, child)
|
||||||
|
// State remains 'starting' until the IPC handler confirms port listener
|
||||||
return true
|
return true
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.addLog(name, `Spawn Error: ${error.message}`)
|
this.addLog(name, `Spawn Error: ${error.message}`)
|
||||||
serviceEventLogger.log({ service: name, action: 'start', status: 'failure', details: error.message })
|
serviceEventLogger.log({ service: name, action: 'start', status: 'failure', details: error.message })
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopService(name: string): Promise<boolean> {
|
async stopService(name: string): Promise<boolean> {
|
||||||
|
this.states.set(name, 'stopping')
|
||||||
const child = this.processes.get(name)
|
const child = this.processes.get(name)
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
this.addLog(name, `${t('common.stopping')}...`)
|
this.addLog(name, `${t('common.stopping')}...`)
|
||||||
serviceEventLogger.log({ service: name, action: 'stop', status: 'info' })
|
serviceEventLogger.log({ service: name, action: 'stop', status: 'info' })
|
||||||
try {
|
|
||||||
if (process.platform === 'win32') {
|
return new Promise((resolve) => {
|
||||||
child.kill()
|
let resolved = false
|
||||||
} else {
|
const timer = setTimeout(() => {
|
||||||
// On Unix, try SIGTERM first, then SIGKILL if needed
|
if (!resolved) {
|
||||||
child.kill('SIGTERM')
|
this.addLog(name, 'Stop timeout, forcing kill...')
|
||||||
|
this.forceKillAll(name).then(() => {
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
resolved = true
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
child.on('exit', () => {
|
||||||
|
if (!resolved) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
this.processes.delete(name)
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
|
resolve(true)
|
||||||
|
resolved = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
child.kill()
|
||||||
|
} else {
|
||||||
|
child.kill('SIGTERM')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.addLog(name, `Stop signal error: ${e instanceof Error ? e.message : String(e)}`)
|
||||||
|
this.forceKillAll(name).then(() => {
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
resolved = true
|
||||||
}
|
}
|
||||||
} catch (e) {
|
})
|
||||||
this.addLog(name, `Stop error: ${e instanceof Error ? e.message : String(e)}`)
|
|
||||||
}
|
|
||||||
this.processes.delete(name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also force kill lingering processes
|
// Also force kill lingering processes even if not in map
|
||||||
return this.forceKillAll(name)
|
const result = await this.forceKillAll(name)
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async forceKillAll(name: string): Promise<boolean> {
|
async forceKillAll(name: string): Promise<boolean> {
|
||||||
@@ -210,13 +264,11 @@ export class ProcessManager {
|
|||||||
.on('close', () => resolve(true))
|
.on('close', () => resolve(true))
|
||||||
.on('error', () => resolve(true))
|
.on('error', () => resolve(true))
|
||||||
} else {
|
} else {
|
||||||
// Unix: pkill -f matches full command line (data directory path usually contains the dbId)
|
|
||||||
spawn('pkill', ['-f', dbId], { windowsHide: true })
|
spawn('pkill', ['-f', dbId], { windowsHide: true })
|
||||||
.on('close', () => resolve(true))
|
.on('close', () => resolve(true))
|
||||||
.on('error', () => resolve(true))
|
.on('error', () => resolve(true))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const isWindows = process.platform === 'win32';
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
const executableName = name === 'nginx' ? 'nginx.exe' : name === 'apache' ? 'httpd.exe' : name === 'php' ? 'php-cgi.exe' : 'mariadbd.exe'
|
const executableName = name === 'nginx' ? 'nginx.exe' : name === 'apache' ? 'httpd.exe' : name === 'php' ? 'php-cgi.exe' : 'mariadbd.exe'
|
||||||
spawn('taskkill', ['/F', '/IM', executableName, '/T'], { windowsHide: true })
|
spawn('taskkill', ['/F', '/IM', executableName, '/T'], { windowsHide: true })
|
||||||
@@ -230,7 +282,6 @@ export class ProcessManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// macOS / Linux
|
|
||||||
if (childProcess && childProcess.pid) {
|
if (childProcess && childProcess.pid) {
|
||||||
try { process.kill(childProcess.pid, 'SIGKILL') } catch (e) { }
|
try { process.kill(childProcess.pid, 'SIGKILL') } catch (e) { }
|
||||||
this.processes.delete(name)
|
this.processes.delete(name)
|
||||||
@@ -286,36 +337,51 @@ export class ProcessManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isServiceRunning(name: string): boolean {
|
isServiceRunning(name: string): boolean {
|
||||||
return this.processes.has(name)
|
const child = this.processes.get(name)
|
||||||
|
return !!(child && child.pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatuses(): Promise<Record<string, { status: string; cpu?: number; memory?: number }>> {
|
async getStatuses(): Promise<Record<string, { status: string; cpu?: number; memory?: number }>> {
|
||||||
const statuses: Record<string, { status: string; cpu?: number; memory?: number }> = {}
|
const statuses: Record<string, { status: string; cpu?: number; memory?: number }> = {}
|
||||||
|
|
||||||
|
// Add all tracked states first
|
||||||
|
for (const [name, state] of this.states.entries()) {
|
||||||
|
statuses[name] = { status: state }
|
||||||
|
}
|
||||||
|
|
||||||
const entries = Array.from(this.processes.entries())
|
const entries = Array.from(this.processes.entries())
|
||||||
for (const [name, child] of entries) {
|
for (const [name, child] of entries) {
|
||||||
|
const currentState = this.states.get(name)
|
||||||
if (child.pid) {
|
if (child.pid) {
|
||||||
try {
|
try {
|
||||||
const stats = await pidusage(child.pid)
|
const stats = await pidusage(child.pid)
|
||||||
statuses[name] = {
|
statuses[name] = {
|
||||||
status: 'running',
|
status: currentState === 'stopping' ? 'stopping' : 'running',
|
||||||
cpu: Math.round(stats.cpu * 10) / 10, // Round to 1 decimal
|
cpu: Math.round(stats.cpu * 10) / 10,
|
||||||
memory: stats.memory
|
memory: stats.memory
|
||||||
}
|
}
|
||||||
|
if (currentState !== 'stopping' && currentState !== 'starting') {
|
||||||
|
this.states.set(name, 'running')
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
statuses[name] = { status: 'running' }
|
if (currentState !== 'starting' && currentState !== 'stopping') {
|
||||||
// Clean up pidusage if error (usually process died)
|
statuses[name] = { status: 'stopped' }
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
|
}
|
||||||
pidusage.clear()
|
pidusage.clear()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
statuses[name] = { status: 'running' }
|
if (currentState !== 'starting' && currentState !== 'stopping') {
|
||||||
|
statuses[name] = { status: 'stopped' }
|
||||||
|
this.states.set(name, 'stopped')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure defaults are present if stopped
|
// Ensure defaults are present
|
||||||
const defaultServices = ['nginx', 'apache', 'mariadb']
|
const defaultServices = ['nginx', 'apache', 'mariadb']
|
||||||
defaultServices.forEach(s => {
|
defaultServices.forEach(s => {
|
||||||
if (!statuses[s]) statuses[s] = { status: 'stopped' }
|
if (!statuses[s]) statuses[s] = { status: this.states.get(s) || 'stopped' }
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
@@ -353,7 +419,6 @@ export class ProcessManager {
|
|||||||
this.initLogs()
|
this.initLogs()
|
||||||
const foundLogs: string[] = []
|
const foundLogs: string[] = []
|
||||||
|
|
||||||
// 1. Check in-memory logs for [CORE ERROR] or other error markers
|
|
||||||
const memLogs = this.logs.get(name) || []
|
const memLogs = this.logs.get(name) || []
|
||||||
const errorMarkers = ['ERROR', 'failed', 'Fatal', 'error']
|
const errorMarkers = ['ERROR', 'failed', 'Fatal', 'error']
|
||||||
const recentErrors = memLogs.filter(line =>
|
const recentErrors = memLogs.filter(line =>
|
||||||
@@ -362,7 +427,6 @@ export class ProcessManager {
|
|||||||
|
|
||||||
foundLogs.push(...recentErrors)
|
foundLogs.push(...recentErrors)
|
||||||
|
|
||||||
// 2. Check physical log files if memory logs are insufficient
|
|
||||||
const sanitizedName = name.replace(/:/g, '_')
|
const sanitizedName = name.replace(/:/g, '_')
|
||||||
const possibleFiles = [
|
const possibleFiles = [
|
||||||
path.join(this.logDir, `${sanitizedName}.log`),
|
path.join(this.logDir, `${sanitizedName}.log`),
|
||||||
@@ -381,7 +445,6 @@ export class ProcessManager {
|
|||||||
if (!foundLogs.includes(l)) foundLogs.push(l)
|
if (!foundLogs.includes(l)) foundLogs.push(l)
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore errors
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ export default function ProjectDetails({
|
|||||||
|
|
||||||
<Box sx={{ p: 0, flexGrow: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ p: 0, flexGrow: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||||
{projectDetailTab === 0 && (
|
{projectDetailTab === 0 && (
|
||||||
<Box sx={{ p: 4 }}>
|
<Box sx={{ p: 4, overflowY: 'auto', flexGrow: 1 }}>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<Box sx={{ mb: 4 }}>
|
<Box sx={{ mb: 4 }}>
|
||||||
@@ -313,7 +313,7 @@ export default function ProjectDetails({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{projectDetailTab === 1 && (
|
{projectDetailTab === 1 && (
|
||||||
<Box sx={{ p: 4 }}>
|
<Box sx={{ p: 4, overflowY: 'auto', flexGrow: 1 }}>
|
||||||
<Box sx={{ maxWidth: 600 }}>
|
<Box sx={{ maxWidth: 600 }}>
|
||||||
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
|
<Box sx={{ p: 3, border: '1px solid rgba(255,255,255,0.05)', borderRadius: 2, bgcolor: 'rgba(255,255,255,0.01)' }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user