diff --git a/server/index.ts b/server/index.ts index f980fb9..df3bc11 100644 --- a/server/index.ts +++ b/server/index.ts @@ -8,6 +8,7 @@ import { createServer } from "http"; import { spawn } from "child_process"; import path from "path"; import { logger, httpLogger } from "./logger"; +import { ArcadiaKernel } from "./kernel"; // Kernel Nativo - Semana 3 interface ManagedService { name: string; @@ -397,4 +398,21 @@ export function log(message: string, source = "express") { log(`serving on port ${port}`); }, ); + + // Inicia Arcadia Kernel (Dashboard porta 5001) - Semana 3 + // Só inicia se KERNEL_ENABLED estiver setado + if (process.env.KERNEL_ENABLED === 'true') { + try { + const kernel = new ArcadiaKernel({ + dashboard: { enabled: true, port: 5001, host: '0.0.0.0' }, + autoStart: process.env.KERNEL_AUTOSTART === 'true', + }); + await kernel.start(); + log('Arcadia Kernel iniciado em http://0.0.0.0:5001'); + } catch (error) { + log(`Erro ao iniciar Kernel: ${error instanceof Error ? error.message : String(error)}`); + } + } else { + log('Kernel desabilitado (set KERNEL_ENABLED=true para ativar)'); + } })(); diff --git a/server/kernel/api/routes.ts b/server/kernel/api/routes.ts new file mode 100644 index 0000000..414878b --- /dev/null +++ b/server/kernel/api/routes.ts @@ -0,0 +1,408 @@ +/** + * API Routes - Rotas REST do Kernel + * Endpoints para controle e monitoramento dos serviços + */ + +import { Router, Request, Response } from 'express'; +import { ProcessManager } from '../core/ProcessManager'; +import { HealthMonitor } from '../core/HealthMonitor'; +import { LogAggregator } from '../core/LogAggregator'; +import { ApiResponse, ServiceActionRequest, ServiceLogsRequest } from '../types'; + +export function createKernelRoutes( + processManager: ProcessManager, + healthMonitor: HealthMonitor, + logAggregator: LogAggregator +): Router { + const router = Router(); + + // Middleware para JSON + router.use(expressJson()); + + // ========================================== + // SERVICES ENDPOINTS + // ========================================== + + /** + * GET /api/kernel/services + * Lista todos os serviços com seu estado atual + */ + router.get('/services', (req: Request, res: Response) => { + const states = processManager.getAllStates(); + + // Adiciona health status de cada serviço + const servicesWithHealth = states.map(state => ({ + ...state, + health: healthMonitor.getHealth(state.config.id), + })); + + res.json(successResponse(servicesWithHealth)); + }); + + /** + * GET /api/kernel/services/:id + * Detalhes de um serviço específico + */ + router.get('/services/:id', (req: Request, res: Response) => { + const { id } = req.params; + const state = processManager.getServiceState(id); + + if (!state) { + return res.status(404).json(errorResponse(`Serviço ${id} não encontrado`)); + } + + res.json(successResponse({ + ...state, + health: healthMonitor.getHealth(id), + })); + }); + + /** + * POST /api/kernel/services/:id/start + * Inicia um serviço + */ + router.post('/services/:id/start', async (req: Request, res: Response) => { + const { id } = req.params; + + try { + const state = await processManager.startService(id); + healthMonitor.startMonitoring(id, state); + res.json(successResponse(state)); + } catch (error) { + res.status(500).json(errorResponse( + error instanceof Error ? error.message : 'Erro ao iniciar serviço' + )); + } + }); + + /** + * POST /api/kernel/services/:id/stop + * Para um serviço + */ + router.post('/services/:id/stop', async (req: Request, res: Response) => { + const { id } = req.params; + const { force } = req.body as ServiceActionRequest; + + try { + const state = await processManager.stopService(id, force); + healthMonitor.stopMonitoring(id); + res.json(successResponse(state)); + } catch (error) { + res.status(500).json(errorResponse( + error instanceof Error ? error.message : 'Erro ao parar serviço' + )); + } + }); + + /** + * POST /api/kernel/services/:id/restart + * Reinicia um serviço + */ + router.post('/services/:id/restart', async (req: Request, res: Response) => { + const { id } = req.params; + + try { + const state = await processManager.restartService(id); + healthMonitor.startMonitoring(id, state); + res.json(successResponse(state)); + } catch (error) { + res.status(500).json(errorResponse( + error instanceof Error ? error.message : 'Erro ao reiniciar serviço' + )); + } + }); + + /** + * POST /api/kernel/services/:id/kill + * Mata um serviço (force) + */ + router.post('/services/:id/kill', async (req: Request, res: Response) => { + const { id } = req.params; + + try { + const state = await processManager.killService(id); + healthMonitor.stopMonitoring(id); + res.json(successResponse(state)); + } catch (error) { + res.status(500).json(errorResponse( + error instanceof Error ? error.message : 'Erro ao matar serviço' + )); + } + }); + + // ========================================== + // LOGS ENDPOINTS + // ========================================== + + /** + * GET /api/kernel/services/:id/logs + * Logs de um serviço específico + */ + router.get('/services/:id/logs', (req: Request, res: Response) => { + const { id } = req.params; + const { lines = '100', level } = req.query; + + const logs = logAggregator.getServiceLogs( + id, + parseInt(lines as string, 10) || 100 + ); + + // Filtra por nível se especificado + let filteredLogs = logs; + if (level) { + const levels = ['debug', 'info', 'warn', 'error']; + const minLevel = levels.indexOf(level as string); + filteredLogs = logs.filter(l => levels.indexOf(l.level) >= minLevel); + } + + res.json(successResponse(filteredLogs)); + }); + + /** + * GET /api/kernel/logs + * Logs de todos os serviços + */ + router.get('/logs', (req: Request, res: Response) => { + const { lines = '100', service, level } = req.query; + + const logs = logAggregator.getAll({ + lines: parseInt(lines as string, 10) || 100, + serviceId: service as string | undefined, + level: level as any, + }); + + res.json(successResponse(logs)); + }); + + /** + * DELETE /api/kernel/logs + * Limpa logs + */ + router.delete('/logs', (req: Request, res: Response) => { + const { service } = req.query; + + if (service) { + logAggregator.clearService(service as string); + } else { + logAggregator.clearAll(); + } + + res.json(successResponse({ cleared: true })); + }); + + // ========================================== + // HEALTH ENDPOINTS + // ========================================== + + /** + * GET /api/kernel/health + * Health check geral do kernel + */ + router.get('/health', (req: Request, res: Response) => { + const states = processManager.getAllStates(); + const allHealth = healthMonitor.getAllHealth(); + + const healthy = Array.from(allHealth.values()).filter(h => h === 'healthy').length; + const unhealthy = Array.from(allHealth.values()).filter(h => h === 'unhealthy').length; + const running = states.filter(s => s.status === 'running').length; + const total = states.length; + + const health = { + status: unhealthy === 0 ? 'healthy' : 'unhealthy', + summary: { + total, + running, + healthy, + unhealthy, + unknown: total - healthy - unhealthy, + }, + services: states.map(s => ({ + id: s.config.id, + name: s.config.name, + status: s.status, + health: allHealth.get(s.config.id) || 'unknown', + pid: s.pid, + uptime: s.startTime + ? Date.now() - s.startTime.getTime() + : 0, + })), + }; + + res.json(successResponse(health)); + }); + + /** + * GET /api/kernel/services/:id/health + * Health check de um serviço específico + */ + router.get('/services/:id/health', async (req: Request, res: Response) => { + const { id } = req.params; + + // Força um check imediato + const health = await healthMonitor.forceCheck(id); + + res.json(successResponse({ + serviceId: id, + health, + timestamp: new Date(), + })); + }); + + // ========================================== + // ACTIONS ENDPOINTS + // ========================================== + + /** + * POST /api/kernel/actions/start-all + * Inicia todos os serviços + */ + router.post('/actions/start-all', async (req: Request, res: Response) => { + const { auto = true } = req.body; + + res.json(successResponse({ + message: 'Iniciando serviços...', + auto, + })); + + // Executa em background + processManager.startAll(auto).catch(() => {}); + }); + + /** + * POST /api/kernel/actions/stop-all + * Para todos os serviços + */ + router.post('/actions/stop-all', async (req: Request, res: Response) => { + const { force = false } = req.body; + + await processManager.stopAll(force); + healthMonitor.stopAll(); + + res.json(successResponse({ + message: 'Todos os serviços parados', + force, + })); + }); + + /** + * POST /api/kernel/actions/restart-all + * Reinicia todos os serviços + */ + router.post('/actions/restart-all', async (req: Request, res: Response) => { + const states = processManager.getAllStates(); + + res.json(successResponse({ + message: 'Reiniciando serviços...', + count: states.filter(s => s.status === 'running').length, + })); + + // Executa em background + for (const state of states) { + if (state.status === 'running') { + try { + await processManager.restartService(state.config.id); + } catch (error) { + // Ignora erros individuais + } + } + } + }); + + // ========================================== + // STATS ENDPOINTS + // ========================================== + + /** + * GET /api/kernel/stats + * Estatísticas do kernel + */ + router.get('/stats', (req: Request, res: Response) => { + const states = processManager.getAllStates(); + const logStats = logAggregator.getStats(); + + const stats = { + services: { + total: states.length, + running: states.filter(s => s.status === 'running').length, + stopped: states.filter(s => s.status === 'stopped').length, + error: states.filter(s => s.status === 'error').length, + restarting: states.filter(s => s.status === 'restarting').length, + }, + health: { + healthy: Array.from(healthMonitor.getAllHealth().values()) + .filter(h => h === 'healthy').length, + unhealthy: Array.from(healthMonitor.getAllHealth().values()) + .filter(h => h === 'unhealthy').length, + unknown: Array.from(healthMonitor.getAllHealth().values()) + .filter(h => h === 'unknown').length, + }, + logs: logStats, + totalRestarts: states.reduce((sum, s) => sum + s.restartCount, 0), + }; + + res.json(successResponse(stats)); + }); + + // ========================================== + // MISC ENDPOINTS + // ========================================== + + /** + * GET /api/kernel/config + * Configuração do kernel + */ + router.get('/config', (req: Request, res: Response) => { + const states = processManager.getAllStates(); + + res.json(successResponse({ + services: states.map(s => ({ + id: s.config.id, + name: s.config.name, + type: s.config.type, + port: s.config.port, + autoStart: s.config.autoStart, + maxRestarts: s.config.maxRestarts, + })), + })); + }); + + return router; +} + +// Helper para JSON middleware (evita import do express inteiro) +function expressJson() { + return (req: Request, res: Response, next: any) => { + if (req.headers['content-type']?.includes('application/json')) { + let data = ''; + req.on('data', chunk => data += chunk); + req.on('end', () => { + try { + (req as any).body = data ? JSON.parse(data) : {}; + } catch { + (req as any).body = {}; + } + next(); + }); + } else { + (req as any).body = {}; + next(); + } + }; +} + +// Helpers de resposta +function successResponse(data: T): ApiResponse { + return { + success: true, + data, + timestamp: new Date(), + }; +} + +function errorResponse(error: string): ApiResponse { + return { + success: false, + error, + timestamp: new Date(), + }; +} diff --git a/server/kernel/config/services.json b/server/kernel/config/services.json new file mode 100644 index 0000000..2261bba --- /dev/null +++ b/server/kernel/config/services.json @@ -0,0 +1,139 @@ +{ + "services": [ + { + "id": "python-contabil", + "name": "Contabil Service", + "description": "Serviço de contabilidade e integração fiscal", + "type": "python", + "command": "python3", + "args": ["-m", "uvicorn", "contabil.main:app", "--host", "0.0.0.0", "--port", "8003"], + "cwd": "./python-service", + "port": 8003, + "healthCheck": { + "path": "/health", + "interval": 30000, + "timeout": 5000, + "retries": 3 + }, + "env": { + "PYTHONPATH": ".", + "SERVICE_NAME": "contabil", + "LOG_LEVEL": "info" + }, + "maxRestarts": 5, + "autoStart": true + }, + { + "id": "python-bi", + "name": "BI Engine", + "description": "Motor de Business Intelligence e análise de dados", + "type": "python", + "command": "python3", + "args": ["-m", "uvicorn", "bi_engine.main:app", "--host", "0.0.0.0", "--port", "8004"], + "cwd": "./python-service", + "port": 8004, + "healthCheck": { + "path": "/health", + "interval": 30000, + "timeout": 5000, + "retries": 3 + }, + "env": { + "PYTHONPATH": ".", + "SERVICE_NAME": "bi", + "LOG_LEVEL": "info" + }, + "maxRestarts": 5, + "autoStart": true + }, + { + "id": "python-automation", + "name": "Automation Service", + "description": "Serviço de automações e workflows", + "type": "python", + "command": "python3", + "args": ["-m", "uvicorn", "automation.main:app", "--host", "0.0.0.0", "--port", "8005"], + "cwd": "./python-service", + "port": 8005, + "healthCheck": { + "path": "/health", + "interval": 30000, + "timeout": 5000, + "retries": 3 + }, + "env": { + "PYTHONPATH": ".", + "SERVICE_NAME": "automation", + "LOG_LEVEL": "info" + }, + "maxRestarts": 5, + "autoStart": true + }, + { + "id": "node-communication", + "name": "Communication Service", + "description": "Serviço de comunicação WhatsApp e outros canais", + "type": "node", + "command": "node", + "args": ["dist/services/communication/index.js"], + "port": 9001, + "healthCheck": { + "path": "/health", + "interval": 30000, + "timeout": 5000, + "retries": 3 + }, + "env": { + "NODE_ENV": "production", + "SERVICE_NAME": "communication", + "PORT": "9001" + }, + "maxRestarts": 5, + "autoStart": false + }, + { + "id": "node-core-api", + "name": "Core API Service", + "description": "API Core adicional para processamento", + "type": "node", + "command": "node", + "args": ["dist/services/core-api/index.js"], + "port": 9002, + "healthCheck": { + "path": "/health", + "interval": 30000, + "timeout": 5000, + "retries": 3 + }, + "env": { + "NODE_ENV": "production", + "SERVICE_NAME": "core-api", + "PORT": "9002" + }, + "maxRestarts": 5, + "autoStart": false + }, + { + "id": "node-worker", + "name": "Worker Service", + "description": "Processamento de jobs e tarefas em background", + "type": "node", + "command": "node", + "args": ["dist/services/worker/index.js"], + "port": 9003, + "healthCheck": { + "path": "/health", + "interval": 30000, + "timeout": 5000, + "retries": 3 + }, + "env": { + "NODE_ENV": "production", + "SERVICE_NAME": "worker", + "PORT": "9003" + }, + "maxRestarts": 5, + "autoStart": false + } + ] +} diff --git a/server/kernel/core/HealthMonitor.ts b/server/kernel/core/HealthMonitor.ts new file mode 100644 index 0000000..8817dac --- /dev/null +++ b/server/kernel/core/HealthMonitor.ts @@ -0,0 +1,241 @@ +/** + * HealthMonitor - Monitor de saúde dos serviços + * Faz health checks HTTP periódicos nos serviços + */ + +import { EventEmitter } from 'events'; +import { ServiceState, HealthStatus, HealthCheckConfig } from '../types'; + +export interface HealthMonitorOptions { + defaultInterval?: number; + defaultTimeout?: number; + defaultRetries?: number; + onHealthChange?: (serviceId: string, health: HealthStatus, lastError?: string) => void; +} + +interface HealthCheckState { + serviceId: string; + config: HealthCheckConfig; + status: HealthStatus; + consecutiveFailures: number; + consecutiveSuccesses: number; + lastCheck?: Date; + lastError?: string; + timer?: NodeJS.Timeout; +} + +export class HealthMonitor extends EventEmitter { + private checks: Map = new Map(); + private options: Required; + + constructor(options: HealthMonitorOptions = {}) { + super(); + this.options = { + defaultInterval: 30000, + defaultTimeout: 5000, + defaultRetries: 3, + ...options, + }; + } + + /** + * Inicia monitoramento de um serviço + */ + startMonitoring(serviceId: string, state: ServiceState): void { + if (!state.config.healthCheck) { + return; + } + + // Para monitoramento anterior se existir + this.stopMonitoring(serviceId); + + const checkState: HealthCheckState = { + serviceId, + config: { + interval: this.options.defaultInterval, + timeout: this.options.defaultTimeout, + retries: this.options.defaultRetries, + ...state.config.healthCheck, + }, + status: 'unknown', + consecutiveFailures: 0, + consecutiveSuccesses: 0, + }; + + this.checks.set(serviceId, checkState); + + // Primeiro check imediato + this.performCheck(checkState); + + // Agenda checks periódicos + this.scheduleNextCheck(checkState); + + this.emit('monitoring:started', { serviceId }); + } + + /** + * Para monitoramento de um serviço + */ + stopMonitoring(serviceId: string): void { + const check = this.checks.get(serviceId); + if (check) { + if (check.timer) { + clearTimeout(check.timer); + } + this.checks.delete(serviceId); + this.emit('monitoring:stopped', { serviceId }); + } + } + + /** + * Para todos os monitoramentos + */ + stopAll(): void { + for (const [serviceId] of this.checks) { + this.stopMonitoring(serviceId); + } + } + + /** + * Retorna status de saúde de um serviço + */ + getHealth(serviceId: string): HealthStatus { + return this.checks.get(serviceId)?.status || 'unknown'; + } + + /** + * Retorna todos os status + */ + getAllHealth(): Map { + const result = new Map(); + for (const [serviceId, check] of this.checks) { + result.set(serviceId, check.status); + } + return result; + } + + /** + * Força um health check imediato + */ + async forceCheck(serviceId: string): Promise { + const check = this.checks.get(serviceId); + if (!check) { + return 'unknown'; + } + + await this.performCheck(check); + return check.status; + } + + /** + * Executa um health check + */ + private async performCheck(check: HealthCheckState): Promise { + const { serviceId, config } = check; + const url = `http://localhost:${this.getServicePort(serviceId)}${config.path}`; + + check.lastCheck = new Date(); + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), config.timeout || 5000); + + const response = await fetch(url, { + method: 'GET', + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (response.ok) { + this.handleSuccess(check); + } else { + this.handleFailure(check, `HTTP ${response.status}`); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.handleFailure(check, message); + } + } + + /** + * Trata sucesso do health check + */ + private handleSuccess(check: HealthCheckState): void { + check.consecutiveSuccesses++; + check.consecutiveFailures = 0; + check.lastError = undefined; + + const requiredSuccesses = 2; // Necessário 2 sucessos consecutivos para ficar healthy + + if (check.status !== 'healthy' && check.consecutiveSuccesses >= requiredSuccesses) { + const oldStatus = check.status; + check.status = 'healthy'; + this.emitHealthChange(check, oldStatus); + } + } + + /** + * Trata falha do health check + */ + private handleFailure(check: HealthCheckState, error: string): void { + check.consecutiveFailures++; + check.consecutiveSuccesses = 0; + check.lastError = error; + + const maxFailures = check.config.retries || 3; + + if (check.status !== 'unhealthy' && check.consecutiveFailures >= maxFailures) { + const oldStatus = check.status; + check.status = 'unhealthy'; + this.emitHealthChange(check, oldStatus); + } + } + + /** + * Emite mudança de saúde + */ + private emitHealthChange(check: HealthCheckState, oldStatus: HealthStatus): void { + this.emit('health:changed', { + serviceId: check.serviceId, + status: check.status, + oldStatus, + lastError: check.lastError, + }); + + if (this.options.onHealthChange) { + this.options.onHealthChange(check.serviceId, check.status, check.lastError); + } + } + + /** + * Agenda próximo check + */ + private scheduleNextCheck(check: HealthCheckState): void { + if (check.timer) { + clearTimeout(check.timer); + } + + check.timer = setTimeout(() => { + this.performCheck(check).then(() => { + this.scheduleNextCheck(check); + }); + }, check.config.interval); + } + + /** + * Obtém porta do serviço (placeholder - deve ser integrado com ProcessManager) + */ + private getServicePort(serviceId: string): number { + // TODO: Integrar com ProcessManager ou config + const ports: Record = { + 'python-contabil': 8003, + 'python-bi': 8004, + 'python-automation': 8005, + 'node-communication': 9001, + 'node-core-api': 9002, + 'node-worker': 9003, + }; + return ports[serviceId] || 0; + } +} diff --git a/server/kernel/core/LogAggregator.ts b/server/kernel/core/LogAggregator.ts new file mode 100644 index 0000000..901b06a --- /dev/null +++ b/server/kernel/core/LogAggregator.ts @@ -0,0 +1,221 @@ +/** + * LogAggregator - Agregador de logs do Kernel + * Coleta, armazena e disponibiliza logs dos serviços + */ + +import { EventEmitter } from 'events'; +import { LogEntry, ServiceConfig } from '../types'; + +export interface LogAggregatorOptions { + maxLines?: number; + retentionHours?: number; +} + +export class LogAggregator extends EventEmitter { + private logs: LogEntry[] = []; + private serviceBuffers: Map = new Map(); + private options: Required; + + constructor(options: LogAggregatorOptions = {}) { + super(); + this.options = { + maxLines: 10000, + retentionHours: 24, + ...options, + }; + + // Limpa logs antigos periodicamente + setInterval(() => this.cleanup(), 60000); + } + + /** + * Adiciona uma entrada de log + */ + add(entry: LogEntry): void { + // Adiciona ao buffer global + this.logs.push(entry); + + // Adiciona ao buffer do serviço + if (!this.serviceBuffers.has(entry.serviceId)) { + this.serviceBuffers.set(entry.serviceId, []); + } + this.serviceBuffers.get(entry.serviceId)!.push(entry); + + // Limita tamanho do buffer global + if (this.logs.length > this.options.maxLines) { + this.logs.shift(); + } + + // Limita tamanho do buffer por serviço + const serviceLogs = this.serviceBuffers.get(entry.serviceId)!; + if (serviceLogs.length > this.options.maxLines / 10) { + serviceLogs.shift(); + } + + this.emit('log', entry); + } + + /** + * Adiciona log de sistema + */ + system(message: string, level: 'info' | 'warn' | 'error' = 'info'): void { + this.add({ + timestamp: new Date(), + serviceId: 'kernel', + level, + message, + source: 'system', + }); + } + + /** + * Retorna todos os logs + */ + getAll(options: { + lines?: number; + since?: Date; + level?: 'info' | 'warn' | 'error' | 'debug'; + serviceId?: string; + } = {}): LogEntry[] { + let logs = options.serviceId + ? this.serviceBuffers.get(options.serviceId) || [] + : [...this.logs]; + + if (options.since) { + logs = logs.filter(l => l.timestamp >= options.since!); + } + + if (options.level) { + const levels = ['debug', 'info', 'warn', 'error']; + const minLevel = levels.indexOf(options.level); + logs = logs.filter(l => levels.indexOf(l.level) >= minLevel); + } + + if (options.lines) { + logs = logs.slice(-options.lines); + } + + return logs; + } + + /** + * Retorna logs de um serviço específico + */ + getServiceLogs(serviceId: string, lines = 100): LogEntry[] { + const logs = this.serviceBuffers.get(serviceId) || []; + return logs.slice(-lines); + } + + /** + * Retorna logs recentes (últimos N) + */ + getRecent(count = 50): LogEntry[] { + return this.logs.slice(-count); + } + + /** + * Retorna logs como string formatada + */ + formatLogs(logs: LogEntry[]): string { + return logs.map(log => this.formatEntry(log)).join('\n'); + } + + /** + * Formata uma entrada de log + */ + formatEntry(entry: LogEntry): string { + const time = entry.timestamp.toISOString().split('T')[1].slice(0, 12); + const level = entry.level.toUpperCase().padStart(5); + const service = entry.serviceId.padEnd(20); + return `[${time}] [${level}] [${service}] ${entry.message}`; + } + + /** + * Limpa logs de um serviço + */ + clearService(serviceId: string): void { + this.serviceBuffers.delete(serviceId); + this.logs = this.logs.filter(l => l.serviceId !== serviceId); + } + + /** + * Limpa todos os logs + */ + clearAll(): void { + this.logs = []; + this.serviceBuffers.clear(); + this.system('Logs limpos', 'info'); + } + + /** + * Retorna estatísticas de logs + */ + getStats(): { + totalLogs: number; + logsByService: Record; + logsByLevel: Record; + } { + const logsByService: Record = {}; + const logsByLevel: Record = {}; + + for (const log of this.logs) { + logsByService[log.serviceId] = (logsByService[log.serviceId] || 0) + 1; + logsByLevel[log.level] = (logsByLevel[log.level] || 0) + 1; + } + + return { + totalLogs: this.logs.length, + logsByService, + logsByLevel, + }; + } + + /** + * Stream de logs em tempo real (retorna handler para unsubscribe) + */ + stream(callback: (entry: LogEntry) => void, options: { + serviceId?: string; + level?: 'info' | 'warn' | 'error' | 'debug'; + } = {}): () => void { + const handler = (entry: LogEntry) => { + if (options.serviceId && entry.serviceId !== options.serviceId) { + return; + } + + if (options.level) { + const levels = ['debug', 'info', 'warn', 'error']; + if (levels.indexOf(entry.level) < levels.indexOf(options.level)) { + return; + } + } + + callback(entry); + }; + + this.on('log', handler); + + return () => { + this.off('log', handler); + }; + } + + /** + * Limpa logs antigos + */ + private cleanup(): void { + const cutoff = new Date(); + cutoff.setHours(cutoff.getHours() - this.options.retentionHours); + + const initialCount = this.logs.length; + this.logs = this.logs.filter(l => l.timestamp >= cutoff); + + for (const [serviceId, logs] of this.serviceBuffers) { + this.serviceBuffers.set(serviceId, logs.filter(l => l.timestamp >= cutoff)); + } + + const removed = initialCount - this.logs.length; + if (removed > 0) { + this.system(`Cleanup: ${removed} logs antigos removidos`, 'debug'); + } + } +} diff --git a/server/kernel/core/ProcessManager.ts b/server/kernel/core/ProcessManager.ts new file mode 100644 index 0000000..318c7a9 --- /dev/null +++ b/server/kernel/core/ProcessManager.ts @@ -0,0 +1,346 @@ +/** + * ProcessManager - Gerenciador de processos do Kernel + * Responsável por spawn, monitoramento e controle de serviços + */ + +import { spawn, ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; +import path from 'path'; +import { + ServiceConfig, + ServiceState, + ServiceStatus, + HealthStatus, + LogEntry, +} from '../types'; + +export interface ProcessManagerOptions { + onLog?: (entry: LogEntry) => void; + onStateChange?: (serviceId: string, state: ServiceState) => void; + onHealthChange?: (serviceId: string, health: HealthStatus) => void; +} + +export class ProcessManager extends EventEmitter { + private services: Map = new Map(); + private processes: Map = new Map(); + private options: ProcessManagerOptions; + + constructor(options: ProcessManagerOptions = {}) { + super(); + this.options = options; + } + + /** + * Registra um serviço no manager (sem iniciar) + */ + registerService(config: ServiceConfig): ServiceState { + const state: ServiceState = { + config, + status: 'stopped', + health: 'unknown', + restartCount: 0, + }; + + this.services.set(config.id, state); + this.emit('service:registered', { serviceId: config.id, state }); + + return state; + } + + /** + * Inicia um serviço + */ + async startService(serviceId: string): Promise { + const state = this.services.get(serviceId); + if (!state) { + throw new Error(`Serviço ${serviceId} não encontrado`); + } + + if (state.status === 'running' || state.status === 'starting') { + return state; + } + + state.status = 'starting'; + this.emitStateChange(serviceId, state); + + try { + const process = this.spawnProcess(state.config); + this.processes.set(serviceId, process); + + state.pid = process.pid; + state.status = 'running'; + state.startTime = new Date(); + state.lastError = undefined; + + this.setupProcessHandlers(serviceId, process); + this.emit('service:started', { serviceId, state }); + + return state; + } catch (error) { + state.status = 'error'; + state.lastError = error instanceof Error ? error.message : String(error); + this.emitStateChange(serviceId, state); + throw error; + } + } + + /** + * Para um serviço + */ + async stopService(serviceId: string, force = false): Promise { + const state = this.services.get(serviceId); + if (!state) { + throw new Error(`Serviço ${serviceId} não encontrado`); + } + + if (state.status === 'stopped' || state.status === 'stopping') { + return state; + } + + state.status = 'stopping'; + this.emitStateChange(serviceId, state); + + const process = this.processes.get(serviceId); + if (process) { + if (force) { + process.kill('SIGKILL'); + } else { + process.kill('SIGTERM'); + + // Aguarda 5s antes de forçar + setTimeout(() => { + if (!process.killed) { + process.kill('SIGKILL'); + } + }, 5000); + } + } + + return state; + } + + /** + * Reinicia um serviço + */ + async restartService(serviceId: string): Promise { + const state = this.services.get(serviceId); + if (!state) { + throw new Error(`Serviço ${serviceId} não encontrado`); + } + + state.status = 'restarting'; + this.emitStateChange(serviceId, state); + + await this.stopService(serviceId); + + // Aguarda processo parar completamente + await new Promise(resolve => setTimeout(resolve, 1000)); + + return this.startService(serviceId); + } + + /** + * Mata um serviço (force kill) + */ + async killService(serviceId: string): Promise { + return this.stopService(serviceId, true); + } + + /** + * Inicia todos os serviços com autoStart + */ + async startAll(autoOnly = true): Promise { + const promises: Promise[] = []; + + for (const [serviceId, state] of this.services) { + if (autoOnly && !state.config.autoStart) { + continue; + } + + promises.push( + this.startService(serviceId) + .then(() => { + this.log('system', serviceId, 'info', `Serviço ${state.config.name} iniciado`); + }) + .catch(error => { + this.log('system', serviceId, 'error', `Falha ao iniciar ${state.config.name}: ${error.message}`); + }) + ); + } + + await Promise.all(promises); + } + + /** + * Para todos os serviços + */ + async stopAll(force = false): Promise { + const promises: Promise[] = []; + + for (const [serviceId, state] of this.services) { + if (state.status === 'running' || state.status === 'starting') { + promises.push( + this.stopService(serviceId, force) + .catch(error => { + this.log('system', serviceId, 'error', `Erro ao parar serviço: ${error.message}`); + }) + ); + } + } + + await Promise.all(promises); + } + + /** + * Retorna estado de um serviço + */ + getServiceState(serviceId: string): ServiceState | undefined { + return this.services.get(serviceId); + } + + /** + * Retorna todos os estados + */ + getAllStates(): ServiceState[] { + return Array.from(this.services.values()); + } + + /** + * Retorna processo de um serviço + */ + getProcess(serviceId: string): ChildProcess | undefined { + return this.processes.get(serviceId); + } + + /** + * Verifica se serviço está rodando + */ + isRunning(serviceId: string): boolean { + const state = this.services.get(serviceId); + return state?.status === 'running'; + } + + /** + * Atualiza health status de um serviço + */ + setHealth(serviceId: string, health: HealthStatus): void { + const state = this.services.get(serviceId); + if (state && state.health !== health) { + state.health = health; + this.emit('service:health_changed', { serviceId, health }); + + if (this.options.onHealthChange) { + this.options.onHealthChange(serviceId, health); + } + } + } + + /** + * Spawna um processo + */ + private spawnProcess(config: ServiceConfig): ChildProcess { + const cwd = config.cwd + ? path.resolve(process.cwd(), config.cwd) + : process.cwd(); + + const env = { + ...process.env, + ...config.env, + }; + + const process_ = spawn(config.command, config.args, { + cwd, + env, + detached: false, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + if (!process_.pid) { + throw new Error(`Falha ao spawnar processo ${config.name}`); + } + + return process_; + } + + /** + * Configura handlers de processo + */ + private setupProcessHandlers(serviceId: string, process: ChildProcess): void { + const state = this.services.get(serviceId)!; + + // stdout + process.stdout?.on('data', (data: Buffer) => { + const message = data.toString().trim(); + if (message) { + state.lastLog = message; + this.log('stdout', serviceId, 'info', message); + } + }); + + // stderr + process.stderr?.on('data', (data: Buffer) => { + const message = data.toString().trim(); + if (message) { + this.log('stderr', serviceId, 'error', message); + } + }); + + // exit + process.on('exit', (code: number | null, signal: string | null) => { + this.processes.delete(serviceId); + + const wasRunning = state.status === 'running'; + state.status = 'stopped'; + state.pid = undefined; + + this.emitStateChange(serviceId, state); + this.emit('service:stopped', { serviceId, code, signal }); + + // Auto-restart se saiu com erro + if (wasRunning && code !== 0 && state.restartCount < (state.config.maxRestarts || 3)) { + state.restartCount++; + this.log('system', serviceId, 'warn', `Serviço parou (código ${code}). Reiniciando... (tentativa ${state.restartCount})`); + + setTimeout(() => { + this.startService(serviceId).catch(() => {}); + }, 2000); + } + }); + + // error + process.on('error', (error: Error) => { + state.lastError = error.message; + this.log('system', serviceId, 'error', `Erro no processo: ${error.message}`); + }); + } + + /** + * Emite log + */ + private log(source: 'stdout' | 'stderr' | 'system', serviceId: string, level: 'info' | 'warn' | 'error' | 'debug', message: string): void { + const entry: LogEntry = { + timestamp: new Date(), + serviceId, + level, + message, + source, + }; + + this.emit('log', entry); + + if (this.options.onLog) { + this.options.onLog(entry); + } + } + + /** + * Emite mudança de estado + */ + private emitStateChange(serviceId: string, state: ServiceState): void { + this.emit('service:state_changed', { serviceId, state }); + + if (this.options.onStateChange) { + this.options.onStateChange(serviceId, state); + } + } +} diff --git a/server/kernel/dashboard/DashboardServer.ts b/server/kernel/dashboard/DashboardServer.ts new file mode 100644 index 0000000..2eb4aff --- /dev/null +++ b/server/kernel/dashboard/DashboardServer.ts @@ -0,0 +1,117 @@ +/** + * DashboardServer - Servidor do Dashboard do Kernel + * Interface web na porta 5001 para gerenciamento dos serviços + */ + +import express, { Application, Request, Response } from 'express'; +import { createServer, Server } from 'http'; +import { ProcessManager } from '../core/ProcessManager'; +import { HealthMonitor } from '../core/HealthMonitor'; +import { LogAggregator } from '../core/LogAggregator'; +import { KernelWebSocket } from '../websocket/KernelWebSocket'; +import { createKernelRoutes } from '../api/routes'; + +export interface DashboardServerOptions { + port?: number; + host?: string; +} + +export class DashboardServer { + private app: Application; + private server?: Server; + private webSocket?: KernelWebSocket; + private options: Required; + + constructor( + private processManager: ProcessManager, + private healthMonitor: HealthMonitor, + private logAggregator: LogAggregator, + options: DashboardServerOptions = {} + ) { + this.options = { + port: 5001, + host: '0.0.0.0', + ...options, + }; + + this.app = express(); + this.setupMiddleware(); + this.setupRoutes(); + } + + async start(): Promise { + return new Promise((resolve, reject) => { + this.server = createServer(this.app); + + this.webSocket = new KernelWebSocket( + this.processManager, + this.healthMonitor, + this.logAggregator + ); + this.webSocket.attach(this.server); + + this.server.listen(this.options.port, this.options.host, () => { + console.log(`[Kernel] Dashboard em http://${this.options.host}:${this.options.port}`); + resolve(); + }); + + this.server.on('error', reject); + }); + } + + async stop(): Promise { + return new Promise((resolve) => { + this.webSocket?.stop(); + this.server?.close(() => { + console.log('[Kernel] Dashboard parado'); + resolve(); + }); + }); + } + + isRunning(): boolean { + return this.server?.listening || false; + } + + private setupMiddleware(): void { + this.app.use(express.json()); + + this.app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + + if (req.method === 'OPTIONS') { + res.sendStatus(200); + } else { + next(); + } + }); + } + + private setupRoutes(): void { + this.app.get('/health', (req: Request, res: Response) => { + res.json({ status: 'healthy', service: 'arcadia-kernel', timestamp: new Date() }); + }); + + const kernelRoutes = createKernelRoutes( + this.processManager, + this.healthMonitor, + this.logAggregator + ); + this.app.use('/api/kernel', kernelRoutes); + + this.app.get('/', (req: Request, res: Response) => { + res.json({ + name: 'Arcadia Kernel Dashboard', + version: '1.0.0', + endpoints: { + dashboard: '/', + health: '/health', + api: '/api/kernel', + websocket: '/ws/kernel', + } + }); + }); + } +} diff --git a/server/kernel/index.ts b/server/kernel/index.ts new file mode 100644 index 0000000..56b6c95 --- /dev/null +++ b/server/kernel/index.ts @@ -0,0 +1,213 @@ +/** + * Arcadia Kernel - Entry Point + * Sistema nativo de gerenciamento de serviços + */ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { ProcessManager } from './core/ProcessManager'; +import { HealthMonitor } from './core/HealthMonitor'; +import { LogAggregator } from './core/LogAggregator'; +import { DashboardServer } from './dashboard/DashboardServer'; +import { ServiceConfig, ServiceState } from './types'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export interface KernelOptions { + configPath?: string; + dashboard?: { + enabled?: boolean; + port?: number; + host?: string; + }; + autoStart?: boolean; +} + +export class ArcadiaKernel { + public processManager: ProcessManager; + public healthMonitor: HealthMonitor; + public logAggregator: LogAggregator; + public dashboard?: DashboardServer; + + private options: Required; + private services: ServiceConfig[] = []; + private started = false; + + constructor(options: KernelOptions = {}) { + this.options = { + configPath: join(__dirname, 'config', 'services.json'), + dashboard: { + enabled: true, + port: 5001, + host: '0.0.0.0', + }, + autoStart: false, + ...options, + }; + + // Inicializa componentes + this.logAggregator = new LogAggregator({ maxLines: 10000 }); + + this.processManager = new ProcessManager({ + onLog: (entry) => this.logAggregator.add(entry), + onStateChange: (serviceId, state) => { + console.log(`[Kernel] ${serviceId}: ${state.status}`); + }, + onHealthChange: (serviceId, health) => { + console.log(`[Kernel] ${serviceId} health: ${health}`); + }, + }); + + this.healthMonitor = new HealthMonitor({ + onHealthChange: (serviceId, health, error) => { + this.processManager.setHealth(serviceId, health); + if (error) { + console.log(`[Kernel] ${serviceId} health error: ${error}`); + } + }, + }); + + // Carrega configurações + this.loadConfig(); + } + + /** + * Inicia o Kernel + */ + async start(): Promise { + if (this.started) { + return; + } + + console.log('[Kernel] Iniciando Arcadia Kernel...'); + console.log(`[Kernel] ${this.services.length} serviços registrados`); + + // Registra serviços no ProcessManager + for (const config of this.services) { + this.processManager.registerService(config); + } + + // Inicia Dashboard + if (this.options.dashboard.enabled) { + this.dashboard = new DashboardServer( + this.processManager, + this.healthMonitor, + this.logAggregator, + { + port: this.options.dashboard.port, + host: this.options.dashboard.host, + } + ); + await this.dashboard.start(); + } + + // Auto-start serviços + if (this.options.autoStart) { + console.log('[Kernel] Auto-iniciando serviços...'); + await this.processManager.startAll(true); + + // Inicia health monitoring para serviços em execução + for (const state of this.processManager.getAllStates()) { + if (state.status === 'running') { + this.healthMonitor.startMonitoring(state.config.id, state); + } + } + } + + this.started = true; + console.log('[Kernel] Kernel iniciado com sucesso!'); + + // Eventos de shutdown + this.setupShutdownHandlers(); + } + + /** + * Para o Kernel + */ + async stop(): Promise { + if (!this.started) { + return; + } + + console.log('[Kernel] Parando Kernel...'); + + // Para health monitor + this.healthMonitor.stopAll(); + + // Para todos os serviços + await this.processManager.stopAll(true); + + // Para dashboard + await this.dashboard?.stop(); + + this.started = false; + console.log('[Kernel] Kernel parado.'); + } + + /** + * Retorna se o kernel está rodando + */ + isRunning(): boolean { + return this.started; + } + + /** + * Carrega configurações dos serviços + */ + private loadConfig(): void { + try { + const configData = readFileSync(this.options.configPath, 'utf-8'); + const config = JSON.parse(configData); + this.services = config.services || []; + } catch (error) { + console.error('[Kernel] Erro ao carregar config:', error); + this.services = []; + } + } + + /** + * Configura handlers de shutdown + */ + private setupShutdownHandlers(): void { + const shutdown = async (signal: string) => { + console.log(`\n[Kernel] Sinal ${signal} recebido. Desligando...`); + await this.stop(); + process.exit(0); + }; + + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('SIGINT', () => shutdown('SIGINT')); + + process.on('uncaughtException', (error) => { + console.error('[Kernel] Uncaught Exception:', error); + }); + + process.on('unhandledRejection', (reason) => { + console.error('[Kernel] Unhandled Rejection:', reason); + }); + } +} + +// Exporta componentes individuais +export { ProcessManager } from './core/ProcessManager'; +export { HealthMonitor } from './core/HealthMonitor'; +export { LogAggregator } from './core/LogAggregator'; +export { DashboardServer } from './dashboard/DashboardServer'; +export { KernelWebSocket } from './websocket/KernelWebSocket'; +export { createKernelRoutes } from './api/routes'; +export * from './types'; + +// Entry point para execução direta +if (import.meta.url === `file://${process.argv[1]}`) { + const kernel = new ArcadiaKernel({ + dashboard: { enabled: true, port: 5001 }, + autoStart: true, + }); + + kernel.start().catch((error) => { + console.error('[Kernel] Falha ao iniciar:', error); + process.exit(1); + }); +} diff --git a/server/kernel/types.ts b/server/kernel/types.ts new file mode 100644 index 0000000..000dc5a --- /dev/null +++ b/server/kernel/types.ts @@ -0,0 +1,135 @@ +/** + * Tipos e Interfaces do Arcadia Kernel + * Sistema de gerenciamento nativo de serviços + */ + +export type ServiceType = 'python' | 'node'; +export type ServiceStatus = 'stopped' | 'starting' | 'running' | 'stopping' | 'error' | 'restarting'; +export type HealthStatus = 'healthy' | 'unhealthy' | 'unknown' | 'checking'; + +export interface HealthCheckConfig { + path: string; + interval: number; // ms + timeout?: number; // ms + retries?: number; +} + +export interface ServiceConfig { + id: string; + name: string; + type: ServiceType; + command: string; + args: string[]; + cwd?: string; + port: number; + env?: Record; + healthCheck?: HealthCheckConfig; + maxRestarts?: number; + autoStart?: boolean; + description?: string; +} + +export interface ServiceState { + config: ServiceConfig; + status: ServiceStatus; + health: HealthStatus; + pid?: number; + startTime?: Date; + restartCount: number; + lastError?: string; + lastLog?: string; + cpu?: number; // CPU usage % + memory?: number; // Memory in MB +} + +export interface LogEntry { + timestamp: Date; + serviceId: string; + level: 'info' | 'warn' | 'error' | 'debug'; + message: string; + source: 'stdout' | 'stderr' | 'system'; +} + +export interface KernelStats { + totalServices: number; + runningServices: number; + healthyServices: number; + errorServices: number; + totalRestarts: number; + uptime: number; // ms since kernel start + cpuUsage: number; + memoryUsage: number; +} + +export interface TenantStats { + tenantId: number; + name: string; + activeUsers: number; + totalRequests: number; + lastActivity: Date; +} + +export interface DashboardData { + services: ServiceState[]; + stats: KernelStats; + tenants: TenantStats[]; + recentLogs: LogEntry[]; + timestamp: Date; +} + +export interface KernelConfig { + dashboard: { + port: number; + host: string; + authEnabled: boolean; + }; + websocket: { + path: string; + heartbeatInterval: number; + }; + healthCheck: { + defaultInterval: number; + defaultTimeout: number; + defaultRetries: number; + }; + logging: { + maxLines: number; + retentionHours: number; + }; +} + +// WebSocket event types +export type KernelEventType = + | 'service:started' + | 'service:stopped' + | 'service:restarted' + | 'service:error' + | 'service:health_changed' + | 'log:new' + | 'stats:update' + | 'tenant:activity'; + +export interface KernelEvent { + type: KernelEventType; + timestamp: Date; + data: any; +} + +// API Response types +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + timestamp: Date; +} + +export interface ServiceActionRequest { + action: 'start' | 'stop' | 'restart' | 'kill'; + force?: boolean; +} + +export interface ServiceLogsRequest { + lines?: number; + since?: Date; + level?: 'info' | 'warn' | 'error' | 'debug'; +} diff --git a/server/kernel/websocket/KernelWebSocket.ts b/server/kernel/websocket/KernelWebSocket.ts new file mode 100644 index 0000000..2ae3276 --- /dev/null +++ b/server/kernel/websocket/KernelWebSocket.ts @@ -0,0 +1,344 @@ +/** + * KernelWebSocket - WebSocket para atualizações em tempo real + * Transmite logs, status e eventos do kernel + */ + +import { WebSocketServer, WebSocket } from 'ws'; +import { Server } from 'http'; +import { EventEmitter } from 'events'; +import { ProcessManager } from '../core/ProcessManager'; +import { HealthMonitor } from '../core/HealthMonitor'; +import { LogAggregator } from '../core/LogAggregator'; +import { LogEntry, ServiceState, KernelEvent, HealthStatus } from '../types'; + +interface ClientConnection { + ws: WebSocket; + id: string; + subscribedServices: Set; + subscribedEvents: Set; + isAlive: boolean; +} + +export interface KernelWebSocketOptions { + path?: string; + heartbeatInterval?: number; +} + +export class KernelWebSocket extends EventEmitter { + private wss?: WebSocketServer; + private clients: Map = new Map(); + private options: Required; + private heartbeatTimer?: NodeJS.Timeout; + + constructor( + private processManager: ProcessManager, + private healthMonitor: HealthMonitor, + private logAggregator: LogAggregator, + options: KernelWebSocketOptions = {} + ) { + super(); + this.options = { + path: '/ws/kernel', + heartbeatInterval: 30000, + ...options, + }; + } + + /** + * Inicia o WebSocket server + */ + attach(server: Server): void { + this.wss = new WebSocketServer({ + server, + path: this.options.path, + }); + + this.wss.on('connection', (ws, req) => { + this.handleConnection(ws, req); + }); + + // Inicia heartbeat + this.startHeartbeat(); + + // Subscribe aos eventos do kernel + this.subscribeToKernelEvents(); + + console.log(`[Kernel] WebSocket ativo em ${this.options.path}`); + } + + /** + * Para o WebSocket server + */ + stop(): void { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + } + + // Fecha todas as conexões + for (const client of this.clients.values()) { + client.ws.close(); + } + this.clients.clear(); + + this.wss?.close(); + } + + /** + * Broadcast para todos os clientes + */ + broadcast(event: string, data: any): void { + const message = JSON.stringify({ + type: event, + data, + timestamp: new Date(), + }); + + for (const client of this.clients.values()) { + if (client.ws.readyState === WebSocket.OPEN) { + client.ws.send(message); + } + } + } + + /** + * Broadcast para clientes inscritos em um serviço + */ + broadcastToService(serviceId: string, event: string, data: any): void { + const message = JSON.stringify({ + type: event, + serviceId, + data, + timestamp: new Date(), + }); + + for (const client of this.clients.values()) { + if ( + client.ws.readyState === WebSocket.OPEN && + (client.subscribedServices.has(serviceId) || client.subscribedServices.has('*')) + ) { + client.ws.send(message); + } + } + } + + /** + * Retorna número de clientes conectados + */ + getClientCount(): number { + return this.clients.size; + } + + /** + * Handler de nova conexão + */ + private handleConnection(ws: WebSocket, req: any): void { + const clientId = this.generateClientId(); + const client: ClientConnection = { + ws, + id: clientId, + subscribedServices: new Set(), + subscribedEvents: new Set(), + isAlive: true, + }; + + this.clients.set(clientId, client); + console.log(`[Kernel] Cliente WebSocket conectado: ${clientId}`); + + // Envia estado inicial + this.sendInitialState(client); + + // Handlers de mensagens + ws.on('message', (data) => { + this.handleMessage(client, data.toString()); + }); + + // Heartbeat + ws.on('pong', () => { + client.isAlive = true; + }); + + // Cleanup na desconexão + ws.on('close', () => { + this.clients.delete(clientId); + console.log(`[Kernel] Cliente WebSocket desconectado: ${clientId}`); + }); + + // Erro + ws.on('error', (error) => { + console.error(`[Kernel] Erro WebSocket ${clientId}:`, error); + }); + } + + /** + * Handler de mensagens do cliente + */ + private handleMessage(client: ClientConnection, message: string): void { + try { + const data = JSON.parse(message); + const { action, payload } = data; + + switch (action) { + case 'subscribe': + // Inscreve em serviços específicos ou '*' para todos + if (payload.services) { + for (const serviceId of payload.services) { + client.subscribedServices.add(serviceId); + } + } + if (payload.events) { + for (const event of payload.events) { + client.subscribedEvents.add(event); + } + } + this.send(client, 'subscribed', { + services: Array.from(client.subscribedServices), + events: Array.from(client.subscribedEvents), + }); + break; + + case 'unsubscribe': + if (payload.services) { + for (const serviceId of payload.services) { + client.subscribedServices.delete(serviceId); + } + } + if (payload.events) { + for (const event of payload.events) { + client.subscribedEvents.delete(event); + } + } + break; + + case 'getLogs': + // Retorna logs de um serviço + const logs = this.logAggregator.getServiceLogs( + payload.serviceId, + payload.lines || 50 + ); + this.send(client, 'logs', { + serviceId: payload.serviceId, + logs, + }); + break; + + case 'ping': + this.send(client, 'pong', { timestamp: new Date() }); + break; + + default: + this.send(client, 'error', { message: `Ação desconhecida: ${action}` }); + } + } catch (error) { + this.send(client, 'error', { + message: error instanceof Error ? error.message : 'Erro ao processar mensagem' + }); + } + } + + /** + * Envia mensagem para um cliente + */ + private send(client: ClientConnection, type: string, data: any): void { + if (client.ws.readyState === WebSocket.OPEN) { + client.ws.send(JSON.stringify({ + type, + data, + timestamp: new Date(), + })); + } + } + + /** + * Envia estado inicial para novo cliente + */ + private sendInitialState(client: ClientConnection): void { + const states = this.processManager.getAllStates(); + const allHealth = this.healthMonitor.getAllHealth(); + + this.send(client, 'initial', { + services: states.map(s => ({ + id: s.config.id, + name: s.config.name, + status: s.status, + health: allHealth.get(s.config.id) || 'unknown', + pid: s.pid, + startTime: s.startTime, + })), + timestamp: new Date(), + }); + } + + /** + * Inscreve nos eventos do kernel + */ + private subscribeToKernelEvents(): void { + // Eventos de serviço + this.processManager.on('service:started', ({ serviceId, state }) => { + this.broadcastToService(serviceId, 'service:started', { + serviceId, + state: { + id: state.config.id, + name: state.config.name, + status: state.status, + pid: state.pid, + startTime: state.startTime, + }, + }); + }); + + this.processManager.on('service:stopped', ({ serviceId, code, signal }) => { + this.broadcastToService(serviceId, 'service:stopped', { + serviceId, + code, + signal, + }); + }); + + this.processManager.on('service:state_changed', ({ serviceId, state }) => { + this.broadcastToService(serviceId, 'service:state_changed', { + serviceId, + status: state.status, + restartCount: state.restartCount, + }); + }); + + // Eventos de health + this.healthMonitor.on('health:changed', ({ serviceId, status, oldStatus }) => { + this.broadcastToService(serviceId, 'service:health_changed', { + serviceId, + health: status, + previousHealth: oldStatus, + }); + }); + + // Eventos de log + this.logAggregator.on('log', (entry: LogEntry) => { + this.broadcastToService(entry.serviceId, 'log:new', entry); + }); + } + + /** + * Inicia heartbeat para detectar conexões mortas + */ + private startHeartbeat(): void { + this.heartbeatTimer = setInterval(() => { + for (const client of this.clients.values()) { + if (!client.isAlive) { + client.ws.terminate(); + this.clients.delete(client.id); + continue; + } + + client.isAlive = false; + client.ws.ping(); + } + }, this.options.heartbeatInterval); + } + + /** + * Gera ID único para cliente + */ + private generateClientId(): string { + return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } +}