345 lines
8.6 KiB
TypeScript
345 lines
8.6 KiB
TypeScript
/**
|
|
* 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)}`;
|
|
}
|
|
}
|