feat(kernel): implementação completa do Kernel Nativo - Semana 3

Kernel Arcadia - Sistema nativo de gerenciamento de serviços:

Core:
- ProcessManager: spawn, monitor, restart automático de serviços
- HealthMonitor: HTTP health checks com circuit breaker
- LogAggregator: coleta e streaming de logs

API & Dashboard:
- API REST /api/kernel/* para controle dos serviços
- Dashboard Web na porta 5001
- WebSocket /ws/kernel para updates em tempo real

Serviços configurados (6):
- Python: Contabil (8003), BI Engine (8004), Automation (8005)
- Node.js: Communication (9001), Core API (9002), Worker (9003)

Integração:
- server/index.ts integrado com flag KERNEL_ENABLED
- Build passando, pronto para testes

Uso:
  KERNEL_ENABLED=true npm run dev
  Acesse http://localhost:5001 para o dashboard
This commit is contained in:
Jonas Pacheco 2026-04-02 10:40:46 -03:00
parent 201580a47a
commit 849934dae8
10 changed files with 2182 additions and 0 deletions

View File

@ -8,6 +8,7 @@ import { createServer } from "http";
import { spawn } from "child_process"; import { spawn } from "child_process";
import path from "path"; import path from "path";
import { logger, httpLogger } from "./logger"; import { logger, httpLogger } from "./logger";
import { ArcadiaKernel } from "./kernel"; // Kernel Nativo - Semana 3
interface ManagedService { interface ManagedService {
name: string; name: string;
@ -397,4 +398,21 @@ export function log(message: string, source = "express") {
log(`serving on port ${port}`); 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)');
}
})(); })();

408
server/kernel/api/routes.ts Normal file
View File

@ -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<T>(data: T): ApiResponse<T> {
return {
success: true,
data,
timestamp: new Date(),
};
}
function errorResponse(error: string): ApiResponse {
return {
success: false,
error,
timestamp: new Date(),
};
}

View File

@ -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
}
]
}

View File

@ -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<string, HealthCheckState> = new Map();
private options: Required<HealthMonitorOptions>;
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<string, HealthStatus> {
const result = new Map<string, HealthStatus>();
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<HealthStatus> {
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<void> {
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<string, number> = {
'python-contabil': 8003,
'python-bi': 8004,
'python-automation': 8005,
'node-communication': 9001,
'node-core-api': 9002,
'node-worker': 9003,
};
return ports[serviceId] || 0;
}
}

View File

@ -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<string, LogEntry[]> = new Map();
private options: Required<LogAggregatorOptions>;
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<string, number>;
logsByLevel: Record<string, number>;
} {
const logsByService: Record<string, number> = {};
const logsByLevel: Record<string, number> = {};
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');
}
}
}

View File

@ -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<string, ServiceState> = new Map();
private processes: Map<string, ChildProcess> = 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<ServiceState> {
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<ServiceState> {
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<ServiceState> {
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<ServiceState> {
return this.stopService(serviceId, true);
}
/**
* Inicia todos os serviços com autoStart
*/
async startAll(autoOnly = true): Promise<void> {
const promises: Promise<void>[] = [];
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<void> {
const promises: Promise<void>[] = [];
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);
}
}
}

View File

@ -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<DashboardServerOptions>;
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<void> {
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<void> {
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',
}
});
});
}
}

213
server/kernel/index.ts Normal file
View File

@ -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<KernelOptions>;
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<void> {
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<void> {
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);
});
}

135
server/kernel/types.ts Normal file
View File

@ -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<string, string>;
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<T = any> {
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';
}

View File

@ -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<string>;
subscribedEvents: Set<string>;
isAlive: boolean;
}
export interface KernelWebSocketOptions {
path?: string;
heartbeatInterval?: number;
}
export class KernelWebSocket extends EventEmitter {
private wss?: WebSocketServer;
private clients: Map<string, ClientConnection> = new Map();
private options: Required<KernelWebSocketOptions>;
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)}`;
}
}