#!/usr/bin/env ruby # frozen_string_literal: true # ========================================================== # adn/tools/cli/novato.rb — Vigía de tareas largas # ---------------------------------------------------------- # El "novato" se encarga de tareas de bajo nivel: # - Lanzar un comando y vigilar hasta que termine # - Cerrar automáticamente el evento de bitácora al finalizar # - Registrar resultado (éxito/fallo) en la descripción # # Principio: Menos es Más — una tool reutilizable para # cualquier ámbito que necesite delegar tareas largas. # # Uso: # ./adn/tools/run novato lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch # ./adn/tools/run novato vigilar --evento 1379 --pid 12345 # ./adn/tools/run novato estado # ========================================================== require 'optparse' require 'fileutils' require 'time' require 'json' module ADN class SubcomandoNovato NOVATO_DIR = File.join(ADN::PROJECT_ROOT, 'tmp', 'novato') ADN_RUN = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'run') def initialize(args, logger = nil) @args = args @logger = logger FileUtils.mkdir_p(NOVATO_DIR) end def ejecutar puts "\n#{Color::YELLOW}⚠️ DEPRECADO: 'novato' ha sido reemplazado por 'dron lanzar'#{Color::RESET}" puts "" puts " El sistema AtomicDrones (Fase 1.5) reemplaza al novato con:" puts " - Heartbeat visible en Bitácora Web cada 30s" puts " - Timeout real implementado" puts " - Registro automático en tablas internas + events" puts "" puts " #{Color::CYAN}Equivalencias:#{Color::RESET}" puts " #{Color::DIM}novato lanzar --evento ID -- #{Color::RESET}" puts " #{Color::GREEN}→ dron lanzar --evento ID --nota \"...\" -- #{Color::RESET}" puts "" # Redirigir automáticamente si es 'lanzar' if @args.first == 'lanzar' @args.shift puts "#{Color::YELLOW}Redirigiendo a 'dron lanzar'...#{Color::RESET}\n\n" require_relative 'dron' dron = ADN::SubcomandoDron.new(['lanzar'] + @args) dron.ejecutar end end private # ─── lanzar: ejecutar comando en background y vigilar ──────── def cmd_lanzar(args) evento_id = nil nota = nil # Separar opciones del novato del comando a ejecutar separador = args.index('--') unless separador puts "#{Color::RED}✗ Falta separador '--' antes del comando a ejecutar#{Color::RESET}" puts "Uso: ./adn/tools/run novato lanzar --evento 1379 -- " return end novato_args = args[0...separador] comando_args = args[(separador + 1)..-1] OptionParser.new do |opts| opts.on("--evento ID", Integer, "ID del evento de bitácora a cerrar") { |e| evento_id = e } opts.on("--nota TEXTO", "Nota adicional para el cierre") { |n| nota = n } end.parse!(novato_args) if comando_args.empty? puts "#{Color::RED}✗ Falta el comando a ejecutar después de '--'#{Color::RESET}" return end comando_str = comando_args.join(' ') tarea_id = "novato_#{Time.now.strftime('%H%M%S')}_#{$$}" log_file = File.join(NOVATO_DIR, "#{tarea_id}.log") meta_file = File.join(NOVATO_DIR, "#{tarea_id}.json") # Guardar metadata meta = { tarea_id: tarea_id, evento_id: evento_id, comando: comando_str, nota: nota, inicio: Time.now.iso8601, pid: nil, estado: 'lanzando' } File.write(meta_file, JSON.pretty_generate(meta)) # Lanzar el proceso vigía en background vigia_script = <<~BASH #!/bin/bash # Novato vigía — auto-generado LOGFILE="#{log_file}" METAFILE="#{meta_file}" EVENTO_ID="#{evento_id}" ADN_RUN="#{ADN_RUN}" echo "[$(date)] Novato: Iniciando comando..." >> "$LOGFILE" #{comando_str} >> "$LOGFILE" 2>&1 EXIT_CODE=$? echo "[$(date)] Novato: Comando terminó con código $EXIT_CODE" >> "$LOGFILE" # Actualizar metadata ruby -rjson -e ' f = "'"$METAFILE"'" m = JSON.parse(File.read(f)) m["fin"] = Time.now.iso8601 m["exit_code"] = #{'"$EXIT_CODE".to_i'} m["estado"] = #{'"$EXIT_CODE".to_i'} == 0 ? "completado" : "fallido" File.write(f, JSON.pretty_generate(m)) ' # Cerrar evento en bitácora si hay evento_id if [ -n "$EVENTO_ID" ] && [ "$EVENTO_ID" != "" ]; then HORA_FIN=$(date +%H:%M) DURACION=$(( ($(date +%s) - #{Time.now.to_i}) / 60 )) if [ "$EXIT_CODE" -eq 0 ]; then RESULTADO="- 🤖 Novato: ✔ Completado exitosamente (${DURACION}min)" else RESULTADO="- 🤖 Novato: ✖ Falló con código $EXIT_CODE (${DURACION}min)" fi NOTA_BASE="#{(nota || '').gsub('"', '\\"')}" if [ -n "$NOTA_BASE" ]; then DESC_FINAL="${NOTA_BASE} ${RESULTADO}" else DESC_FINAL="#{comando_str} ${RESULTADO}" fi "$ADN_RUN" db evento:actualizar "$EVENTO_ID" --fin "$HORA_FIN" --descripcion "$DESC_FINAL" >> "$LOGFILE" 2>&1 echo "[$(date)] Novato: Evento $EVENTO_ID cerrado a las $HORA_FIN ($RESULTADO)" >> "$LOGFILE" fi BASH vigia_path = File.join(NOVATO_DIR, "#{tarea_id}.sh") File.write(vigia_path, vigia_script) FileUtils.chmod(0755, vigia_path) pid = spawn("nohup bash #{vigia_path} &", [:out, :err] => '/dev/null') Process.detach(pid) # Actualizar meta con PID meta[:pid] = pid meta[:estado] = 'vigilando' File.write(meta_file, JSON.pretty_generate(meta)) puts "#{Color::GREEN}✔ Novato lanzado#{Color::RESET}" puts " Tarea: #{tarea_id}" puts " PID: #{pid}" puts " Evento: #{evento_id || '(sin evento)'}" puts " Log: #{log_file}" puts " Comando: #{comando_str}" puts "" puts "#{Color::DIM}El novato cerrará el evento ##{evento_id} cuando termine.#{Color::RESET}" if evento_id puts "#{Color::DIM}Consultar: ./adn/tools/run novato estado#{Color::RESET}" end # ─── vigilar: adjuntarse a un PID existente ────────────────── def cmd_vigilar(args) evento_id = nil pid = nil OptionParser.new do |opts| opts.on("--evento ID", Integer, "ID del evento") { |e| evento_id = e } opts.on("--pid PID", Integer, "PID del proceso a vigilar") { |p| pid = p } end.parse!(args) unless pid puts "#{Color::RED}✗ Falta --pid#{Color::RESET}" return end tarea_id = "vigil_#{pid}_#{Time.now.strftime('%H%M%S')}" log_file = File.join(NOVATO_DIR, "#{tarea_id}.log") # Lanzar vigía para PID existente vigia_cmd = <<~CMD nohup bash -c ' tail --pid=#{pid} -f /dev/null 2>/dev/null HORA_FIN=$(date +%%H:%%M) echo "[$(date)] Novato: PID #{pid} terminó" >> #{log_file} #{evento_id ? "\"#{ADN_RUN}\" db evento:actualizar #{evento_id} --fin $HORA_FIN >> #{log_file} 2>&1" : ""} echo "[$(date)] Novato: Evento #{evento_id} cerrado a las $HORA_FIN" >> #{log_file} ' > /dev/null 2>&1 & CMD system(vigia_cmd) meta = { tarea_id: tarea_id, evento_id: evento_id, pid_vigilado: pid, inicio: Time.now.iso8601, estado: 'vigilando' } File.write(File.join(NOVATO_DIR, "#{tarea_id}.json"), JSON.pretty_generate(meta)) puts "#{Color::GREEN}✔ Novato vigilando PID #{pid}#{Color::RESET}" puts " Evento: #{evento_id || '(sin evento)'}" puts " Log: #{log_file}" end # ─── estado: mostrar tareas del novato ─────────────────────── def cmd_estado metas = Dir.glob(File.join(NOVATO_DIR, '*.json')).sort if metas.empty? puts "#{Color::DIM}Sin tareas de novato activas.#{Color::RESET}" return end puts "#{Color::BOLD}📋 Tareas del Novato#{Color::RESET}\n\n" metas.each do |f| m = JSON.parse(File.read(f)) icono = case m['estado'] when 'completado' then "#{Color::GREEN}✔#{Color::RESET}" when 'fallido' then "#{Color::RED}✖#{Color::RESET}" when 'vigilando' then "#{Color::YELLOW}⏳#{Color::RESET}" else "#{Color::DIM}?#{Color::RESET}" end pid_info = m['pid'] || m['pid_vigilado'] vivo = pid_info && system("kill -0 #{pid_info} 2>/dev/null") puts " #{icono} #{m['tarea_id']}" puts " Evento: #{m['evento_id'] || '-'} PID: #{pid_info || '-'}#{vivo ? " #{Color::GREEN}(vivo)#{Color::RESET}" : ''}" puts " #{m['comando'] || "Vigilando PID #{m['pid_vigilado']}"}" puts " Inicio: #{m['inicio']}#{m['fin'] ? " Fin: #{m['fin']}" : ''}" puts "" end end # ─── limpiar: borrar tareas completadas ────────────────────── def cmd_limpiar Dir.glob(File.join(NOVATO_DIR, '*.json')).each do |f| m = JSON.parse(File.read(f)) if ['completado', 'fallido'].include?(m['estado']) base = File.basename(f, '.json') ['.json', '.log', '.sh'].each do |ext| FileUtils.rm_f(File.join(NOVATO_DIR, "#{base}#{ext}")) end end end puts "#{Color::GREEN}✔ Tareas completadas limpiadas.#{Color::RESET}" end def mostrar_ayuda puts <<~HELP #{Color::BOLD}#{Color::CYAN}Novato — Vigía de Tareas Largas#{Color::RESET} El novato se encarga de las tareas de bajo nivel: ejecutar comandos largos en background y cerrar automáticamente eventos de bitácora cuando terminan. Delegar sin desperdiciar recursos. #{Color::YELLOW}Uso:#{Color::RESET} novato lanzar --evento ID -- Lanzar y vigilar novato vigilar --evento ID --pid PID Vigilar PID existente novato estado Ver tareas activas novato limpiar Limpiar completadas #{Color::YELLOW}Ejemplos:#{Color::RESET} ./adn/tools/run novato lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch ./adn/tools/run novato vigilar --evento 1379 --pid 12345 ./adn/tools/run novato estado HELP end end end