Bugs corregidos (12): B1-B12 — crashes, tipos, métodos, logger Ejecutor reescrito: evento web → heartbeat 30s → cierre automático copiloto.rb y novato.rb deprecados → redirigen a dron lanzar Plan P009 v3.2 — Fase 1.5 100%% completa
311 lines
12 KiB
Ruby
311 lines
12 KiB
Ruby
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
# ==========================================================
|
|
# adn/tools/cli/copiloto.rb — Vigía de tareas largas
|
|
# ----------------------------------------------------------
|
|
# El "copiloto" 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 copiloto lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch
|
|
# ./adn/tools/run copiloto vigilar --evento 1379 --pid 12345
|
|
# ./adn/tools/run copiloto estado
|
|
# ==========================================================
|
|
|
|
require 'optparse'
|
|
require 'fileutils'
|
|
require 'time'
|
|
require 'json'
|
|
|
|
module ADN
|
|
class SubcomandoCopiloto
|
|
COPILOTO_DIR = File.join(ADN::PROJECT_ROOT, 'tmp', 'copiloto')
|
|
ADN_RUN = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'run')
|
|
|
|
def initialize(args, logger = nil)
|
|
@args = args
|
|
@logger = logger
|
|
FileUtils.mkdir_p(COPILOTO_DIR)
|
|
end
|
|
|
|
def ejecutar
|
|
puts "\n#{Color::YELLOW}⚠️ DEPRECADO: 'copiloto' ha sido reemplazado por 'dron lanzar'#{Color::RESET}"
|
|
puts ""
|
|
puts " El sistema AtomicDrones (Fase 1.5) reemplaza al copiloto 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}copiloto lanzar --evento ID -- <cmd>#{Color::RESET}"
|
|
puts " #{Color::GREEN}→ dron lanzar --evento ID --nota \"...\" -- <cmd>#{Color::RESET}"
|
|
puts ""
|
|
puts " #{Color::DIM}copiloto vigilar --evento ID --pid PID#{Color::RESET}"
|
|
puts " #{Color::GREEN}→ dron flota (para ver drones activos)#{Color::RESET}"
|
|
puts ""
|
|
puts " #{Color::DIM}copiloto estado#{Color::RESET}"
|
|
puts " #{Color::GREEN}→ dron flota#{Color::RESET}"
|
|
puts ""
|
|
|
|
# Si hay argumentos, intentar redirigir automáticamente
|
|
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 copiloto 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 copiloto lanzar --evento 1379 -- <comando>"
|
|
return
|
|
end
|
|
|
|
copiloto_args = args[0...separador]
|
|
comando_args = args[(separador + 1)..-1]
|
|
|
|
OptionParser.new do |opts|
|
|
opts.on("--evento ID", "ID del evento (o AUTO para crear uno)") { |e| evento_id = e }
|
|
opts.on("--nota TEXTO", "Nota adicional para el cierre") { |n| nota = n }
|
|
end.parse!(copiloto_args)
|
|
|
|
# AUTO: crear evento de bitácora automáticamente
|
|
if evento_id == 'AUTO'
|
|
cmd_crear = "#{ADN_RUN} db evento:crear --ambito dtic-BKPs --nodo srv-ns8"
|
|
cmd_crear += " --descripcion \"#{(nota || comando_args.join(' ')).gsub('"', '\\"')}\""
|
|
cmd_crear += " --inicio #{Time.now.strftime('%H:%M')} --modo R --ia"
|
|
output = `#{cmd_crear} 2>&1`
|
|
# Extraer ID del evento creado
|
|
if output.match(/ID\s+(\d+)/)
|
|
evento_id = output.match(/ID\s+(\d+)/)[1].to_i
|
|
@logger&.info("Copiloto: Evento AUTO ##{evento_id} creado")
|
|
else
|
|
@logger&.info("Copiloto: No se pudo crear evento AUTO, continuando sin evento")
|
|
evento_id = nil
|
|
end
|
|
elsif evento_id
|
|
evento_id = evento_id.to_i
|
|
end
|
|
|
|
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 = "copiloto_#{Time.now.strftime('%H%M%S')}_#{$$}"
|
|
log_file = File.join(COPILOTO_DIR, "#{tarea_id}.log")
|
|
meta_file = File.join(COPILOTO_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
|
|
# Copiloto vigía — auto-generado
|
|
LOGFILE="#{log_file}"
|
|
METAFILE="#{meta_file}"
|
|
EVENTO_ID="#{evento_id}"
|
|
ADN_RUN="#{ADN_RUN}"
|
|
|
|
echo "[$(date)] Copiloto: Iniciando comando..." >> "$LOGFILE"
|
|
#{comando_str} >> "$LOGFILE" 2>&1
|
|
EXIT_CODE=$?
|
|
echo "[$(date)] Copiloto: 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="- 🤖 Copiloto: ✔ Completado exitosamente (${DURACION}min)"
|
|
else
|
|
RESULTADO="- 🤖 Copiloto: ✖ 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)] Copiloto: Evento $EVENTO_ID cerrado a las $HORA_FIN ($RESULTADO)" >> "$LOGFILE"
|
|
fi
|
|
BASH
|
|
|
|
vigia_path = File.join(COPILOTO_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}✔ Copiloto 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 copiloto cerrará el evento ##{evento_id} cuando termine.#{Color::RESET}" if evento_id
|
|
puts "#{Color::DIM}Consultar: ./adn/tools/run copiloto 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(COPILOTO_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)] Copiloto: PID #{pid} terminó" >> #{log_file}
|
|
#{evento_id ? "\"#{ADN_RUN}\" db evento:actualizar #{evento_id} --fin $HORA_FIN >> #{log_file} 2>&1" : ""}
|
|
echo "[$(date)] Copiloto: 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(COPILOTO_DIR, "#{tarea_id}.json"), JSON.pretty_generate(meta))
|
|
|
|
puts "#{Color::GREEN}✔ Copiloto vigilando PID #{pid}#{Color::RESET}"
|
|
puts " Evento: #{evento_id || '(sin evento)'}"
|
|
puts " Log: #{log_file}"
|
|
end
|
|
|
|
# ─── estado: mostrar tareas del copiloto ───────────────────────
|
|
def cmd_estado
|
|
metas = Dir.glob(File.join(COPILOTO_DIR, '*.json')).sort
|
|
if metas.empty?
|
|
puts "#{Color::DIM}Sin tareas de copiloto activas.#{Color::RESET}"
|
|
return
|
|
end
|
|
|
|
puts "#{Color::BOLD}📋 Tareas del Copiloto#{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(COPILOTO_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(COPILOTO_DIR, "#{base}#{ext}"))
|
|
end
|
|
end
|
|
end
|
|
puts "#{Color::GREEN}✔ Tareas completadas limpiadas.#{Color::RESET}"
|
|
end
|
|
|
|
def mostrar_ayuda
|
|
puts <<~HELP
|
|
#{Color::BOLD}#{Color::CYAN}Copiloto — Vigía de Tareas Largas#{Color::RESET}
|
|
|
|
El copiloto 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}
|
|
copiloto lanzar --evento ID -- <comando> Lanzar y vigilar
|
|
copiloto vigilar --evento ID --pid PID Vigilar PID existente
|
|
copiloto estado Ver tareas activas
|
|
copiloto limpiar Limpiar completadas
|
|
|
|
#{Color::YELLOW}Ejemplos:#{Color::RESET}
|
|
./adn/tools/run copiloto lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch
|
|
./adn/tools/run copiloto vigilar --evento 1379 --pid 12345
|
|
./adn/tools/run copiloto estado
|
|
HELP
|
|
end
|
|
end
|
|
end
|