arcadiasuite/server/kernel/core/ProcessManager.ts

347 lines
8.7 KiB
TypeScript

/**
* 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);
}
}
}