# frozen_string_literal: true # Dron::Ejecutor — Módulo atómico para ejecución de comandos # # Responsabilidad única: Ejecutar un comando y reportar resultado require_relative 'base' require_relative '../../db/core/dron_db' module Dron class Ejecutor DEFAULT_TIMEOUT = 3600 HEARTBEAT_INTERVAL = 30 class << self def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil) Base.logger.info("🛠️ Dron Ejecutor iniciado: #{Process.pid}") dron_id = ADN::DB::DronDB.generar_dron_id ADN::DB::DronDB.registrar_inicio(tipo: 'ejecutor', cmd: cmd, flujo_id: flujo_id, metadata: { nota: nota, evento_id: evento_id, timeout: timeout }) evento_id = registrar_bitacora(nota, evento_id, 'inicio') if nota resultado = ejecutar_comando(cmd, dron_id, timeout) ADN::DB::DronDB.registrar_fin(dron_id, resultado[:exit_code], resultado[:output]) registrar_bitacora(nota, evento_id, 'fin', resultado) if evento_id Base.logger.info("🛠️ Dron Ejecutor completado: #{dron_id} - #{resultado[:exit_code] == 0 ? '✅' : '❌'}") { dron_id: dron_id, exit_code: resultado[:exit_code], duration: resultado[:duration], evento_id: evento_id } rescue StandardError => e Base.logger.error("🛠️ Dron Ejecutor falló: #{e.message}") ADN::DB::DronDB.registrar_fin(dron_id, 1, e.message) if dron_id { dron_id: dron_id, exit_code: 1, output: e.message, duration: 0, error: e.message } end private def ejecutar_comando(cmd, dron_id, timeout) start_time = Time.now output = [] heartbeat_thread = Thread.new do loop do sleep(HEARTBEAT_INTERVAL) ADN::DB::DronDB.actualizar_heartbeat(dron_id) end end begin Timeout.timeout(timeout) do require 'open3' Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr| output << stdout.readline.chomp while (line = stdout.readline) && output << line output << stderr.readline.chomp while (line = stderr.readline) && output << "[ERR] #{line}" rescue EOFError break end exit_code = wait_thr.value.exitstatus end rescue Timeout::Error exit_code = 124 output << "\n[TIMEOUT] #{timeout}s excedidos" rescue StandardError => e exit_code = 1 output << "\n[ERROR] #{e.message}" ensure heartbeat_thread.exit end { exit_code: exit_code, output: output.join("\n"), duration: (Time.now - start_time).round(2) } end def registrar_bitacora(nota, evento_id, tipo, resultado = nil) return nil unless nota require_relative '../../db/core/bitacora_db' if tipo == 'inicio' descripcion = "🛸 dron: #{nota}" BitacorasDB::BitacoraDB.crear_evento(nodo_id: 1, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳') else estado = resultado[:exit_code] == 0 ? '✅' : '❌' descripcion = "🛸 dron: #{nota} — #{estado} (#{Base.formato_duracion(resultado[:duration])})" BitacorasDB::BitacoraDB.actualizar_evento(evento_id, descripcion: descripcion, fin: Time.now.strftime('%H:%M')) end rescue StandardError => e Base.logger.warn("⚠️ Error en bitácora: #{e.message}") nil end end end end