refactor(ambitos): Migración completa dtic-DASUTEN (A04) y dtic-BKPs (A03) + herramientas ADN
Migración dtic-DASUTEN a estándar A04: - A04_dtic-DASUTEN.md: Manifiesto creado - A04.P001 a A04.P005: Planes renombrados de P2601.* - _hist/_hist_P2601_dasuten.md: Legacy archivado Migración dtic-BKPs a estándar A03: - A03_dtic-BKPs.md: Manifiesto creado - A03.P001, A03.P002: Planes migrados - docs/ambito/dtic-BKPs/: Nueva estructura Herramientas ADN nuevas: - adn/tools/bkps/: BKPs migrado a tools ADN - adn/tools/cli/dron.rb: Vigía de tareas largas - adn/tools/cli/copiloto.rb: Asistente CLI - adn/tools/cli/novato.rb: CLI de aprendizaje Limpieza: - dtic-BKPs/ → _hist/dtic-BKPs/ (legacy archivado) - adn/tools/cli/backup.rb → _hist/ (migrado a bkps.rb) Otros: - nodos/pcv-dasu1.md, srvv-data.md, srvv-nginx-rm.md: Actualizados - adn/tools/core/conciliador.rb: Mejoras - adn/tools/w-zombi/data/cmd.json: Actualizado Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
018e45e046
commit
1802c25194
@@ -0,0 +1,304 @@
|
||||
#!/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
|
||||
if @args.empty? || @args.first == 'help' || @args.first == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
accion = @args.shift
|
||||
case accion
|
||||
when 'lanzar'
|
||||
cmd_lanzar(@args)
|
||||
when 'vigilar'
|
||||
cmd_vigilar(@args)
|
||||
when 'estado'
|
||||
cmd_estado
|
||||
when 'limpiar'
|
||||
cmd_limpiar
|
||||
else
|
||||
puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}"
|
||||
mostrar_ayuda
|
||||
exit 1
|
||||
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
|
||||
@@ -0,0 +1,509 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# adn/tools/cli/dron.rb — Sistema de Drones Autónomos
|
||||
# ----------------------------------------------------------
|
||||
# Flota de drones que operan de forma autónoma:
|
||||
# - Lanzar tareas en background con auto-bitácora
|
||||
# - Vigilar procesos existentes
|
||||
# - Orquestar flota: detectar drones caídos, estancados
|
||||
# - Reportar salud del sistema en tiempo real
|
||||
#
|
||||
# Principio: Sistema Vivo — los drones operan, reportan
|
||||
# y se auto-gestionan sin intervención humana.
|
||||
#
|
||||
# Uso:
|
||||
# ./adn/tools/run dron lanzar --evento AUTO -- <comando>
|
||||
# ./adn/tools/run dron flota # Dashboard de la flota
|
||||
# ./adn/tools/run dron salud # Health check activo
|
||||
# ./adn/tools/run dron estado # Estado detallado
|
||||
# ./adn/tools/run dron limpiar # Limpiar completados
|
||||
# ==========================================================
|
||||
|
||||
require 'optparse'
|
||||
require 'fileutils'
|
||||
require 'time'
|
||||
require 'json'
|
||||
|
||||
module ADN
|
||||
class SubcomandoDron
|
||||
DRON_DIR = File.join(ADN::PROJECT_ROOT, 'tmp', 'dron')
|
||||
ADN_RUN = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'run')
|
||||
STALE_MINUTES = 120 # Dron sin actividad > 2h = posible problema
|
||||
|
||||
def initialize(args, logger = nil)
|
||||
@args = args
|
||||
@logger = logger
|
||||
FileUtils.mkdir_p(DRON_DIR)
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty? || @args.first == 'help' || @args.first == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
accion = @args.shift
|
||||
case accion
|
||||
when 'lanzar'
|
||||
cmd_lanzar(@args)
|
||||
when 'vigilar'
|
||||
cmd_vigilar(@args)
|
||||
when 'estado'
|
||||
cmd_estado
|
||||
when 'flota'
|
||||
cmd_flota
|
||||
when 'salud'
|
||||
cmd_salud
|
||||
when 'limpiar'
|
||||
cmd_limpiar
|
||||
else
|
||||
puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}"
|
||||
mostrar_ayuda
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# ─── lanzar: ejecutar comando en background y vigilar ────────
|
||||
def cmd_lanzar(args)
|
||||
evento_id = nil
|
||||
nota = nil
|
||||
|
||||
# Separar opciones del dron 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 dron lanzar --evento 1379 -- <comando>"
|
||||
return
|
||||
end
|
||||
|
||||
dron_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!(dron_args)
|
||||
|
||||
# AUTO: crear evento de bitácora automáticamente
|
||||
if evento_id == 'AUTO'
|
||||
# Primero generamos el tarea_id para incluirlo en la descripción del evento
|
||||
tarea_id = "dron_#{Time.now.strftime('%H%M%S')}_#{$$}"
|
||||
cmd_crear = "#{ADN_RUN} db evento:crear --ambito dtic-BKPs --nodo srv-ns8"
|
||||
cmd_crear += " --descripcion \"🛸 #{tarea_id}: #{(nota || comando_args.join(' ')).gsub('"', '\\"')}\""
|
||||
cmd_crear += " --inicio #{Time.now.strftime('%H:%M')} --modo R --ia"
|
||||
output = `#{cmd_crear} 2>&1`
|
||||
# El evento recién creado aparece como último en la lista de pendientes
|
||||
# Output: "⏳ [33mID 1413 [0m 17:12:00 srv-ns8 ..."
|
||||
all_ids = output.scan(/ID\s+(\d+)/).flatten.map(&:to_i)
|
||||
if output.include?('Entrada creada exitosamente') || output.include?('✅ Entrada creada')
|
||||
if all_ids.any?
|
||||
# Tomamos el ID más alto (el último creado)
|
||||
evento_id = all_ids.max
|
||||
@logger&.info("Dron: Evento AUTO ##{evento_id} creado para #{tarea_id}")
|
||||
else
|
||||
@logger&.warn("Dron: Evento creado pero no se pudo extraer ID del output")
|
||||
evento_id = nil
|
||||
end
|
||||
else
|
||||
@logger&.error("Dron: Error al crear evento AUTO: #{output}")
|
||||
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 ||= "dron_#{Time.now.strftime('%H%M%S')}_#{$$}"
|
||||
log_file = File.join(DRON_DIR, "#{tarea_id}.log")
|
||||
meta_file = File.join(DRON_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
|
||||
nota_safe = (nota || "").gsub(%q{"}, %q{\"})
|
||||
cmd_safe = comando_str.gsub(%q{"}, %q{\"})
|
||||
inicio_epoch = Time.now.to_i
|
||||
vigia_script = <<~BASH
|
||||
#!/bin/bash
|
||||
# Dron vigia — auto-generado
|
||||
LOGFILE="#{log_file}"
|
||||
METAFILE="#{meta_file}"
|
||||
EVENTO_ID="#{evento_id}"
|
||||
ADN_RUN="#{ADN_RUN}"
|
||||
TAREA_ID="#{tarea_id}"
|
||||
INICIO_EPOCH=#{inicio_epoch}
|
||||
|
||||
actualizar_evento() {
|
||||
if [ -n "$EVENTO_ID" ] && [ "$EVENTO_ID" != "" ]; then
|
||||
"$ADN_RUN" db evento:actualizar "$EVENTO_ID" --descripcion "$1" >> "$LOGFILE" 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
HORA_INICIO=$(date +%H:%M:%S)
|
||||
DIARIO="Dron: $TAREA_ID
|
||||
--
|
||||
$HORA_INICIO - Inicio de vuelo
|
||||
Mision: #{nota_safe.empty? ? cmd_safe : nota_safe}
|
||||
Ejecutando: #{cmd_safe}"
|
||||
|
||||
echo "[$HORA_INICIO] Dron $TAREA_ID: Inicio de vuelo" >> "$LOGFILE"
|
||||
actualizar_evento "$DIARIO"
|
||||
|
||||
#{comando_str} >> "$LOGFILE" 2>&1
|
||||
EXIT_CODE=$?
|
||||
HORA_FIN=$(date +%H:%M:%S)
|
||||
DURACION=$(( ($(date +%s) - $INICIO_EPOCH) / 60 ))
|
||||
echo "[$HORA_FIN] Dron $TAREA_ID: Termino con codigo $EXIT_CODE" >> "$LOGFILE"
|
||||
|
||||
if [ "$EXIT_CODE" -eq 0 ]; then
|
||||
DIARIO="$DIARIO
|
||||
OK $HORA_FIN - Completado (${DURACION}min)
|
||||
Aterrizaje limpio"
|
||||
else
|
||||
DIARIO="$DIARIO
|
||||
FALLO $HORA_FIN - Codigo $EXIT_CODE (${DURACION}min)
|
||||
Aterrizaje de emergencia"
|
||||
fi
|
||||
|
||||
ruby -rjson -e '
|
||||
f = ARGV[0]
|
||||
m = JSON.parse(File.read(f))
|
||||
m["fin"] = Time.now.iso8601
|
||||
m["exit_code"] = ARGV[1].to_i
|
||||
m["estado"] = ARGV[1].to_i == 0 ? "completado" : "fallido"
|
||||
File.write(f, JSON.pretty_generate(m))
|
||||
' "$METAFILE" "$EXIT_CODE"
|
||||
|
||||
if [ -n "$EVENTO_ID" ] && [ "$EVENTO_ID" != "" ]; then
|
||||
HORA_FIN_HM=$(date +%H:%M)
|
||||
"$ADN_RUN" db evento:actualizar "$EVENTO_ID" --fin "$HORA_FIN_HM" --descripcion "$DIARIO" >> "$LOGFILE" 2>&1
|
||||
echo "[$HORA_FIN] Dron $TAREA_ID: Evento $EVENTO_ID cerrado" >> "$LOGFILE"
|
||||
fi
|
||||
BASH
|
||||
|
||||
vigia_path = File.join(DRON_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}✔ Dron 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 dron cerrará el evento ##{evento_id} cuando termine.#{Color::RESET}" if evento_id
|
||||
puts "#{Color::DIM}Consultar: ./adn/tools/run dron 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(DRON_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)] Dron: PID #{pid} terminó" >> #{log_file}
|
||||
#{evento_id ? "\"#{ADN_RUN}\" db evento:actualizar #{evento_id} --fin $HORA_FIN >> #{log_file} 2>&1" : ""}
|
||||
echo "[$(date)] Dron: 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(DRON_DIR, "#{tarea_id}.json"), JSON.pretty_generate(meta))
|
||||
|
||||
puts "#{Color::GREEN}✔ Dron vigilando PID #{pid}#{Color::RESET}"
|
||||
puts " Evento: #{evento_id || '(sin evento)'}"
|
||||
puts " Log: #{log_file}"
|
||||
end
|
||||
|
||||
# ─── estado: mostrar tareas del dron ───────────────────────
|
||||
def cmd_estado
|
||||
metas = Dir.glob(File.join(DRON_DIR, '*.json')).sort
|
||||
if metas.empty?
|
||||
puts "#{Color::DIM}Sin tareas de dron activas.#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
puts "#{Color::BOLD}📋 Tareas del Dron#{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
|
||||
|
||||
# ─── flota: dashboard compacto de la flota ─────────────────────
|
||||
def cmd_flota
|
||||
drones = cargar_todos
|
||||
|
||||
if drones.empty?
|
||||
puts "#{Color::DIM}🛸 Flota vacía — sin drones activos.#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
activos = drones.select { |d| d['estado'] == 'vigilando' }
|
||||
completados = drones.select { |d| d['estado'] == 'completado' }
|
||||
fallidos = drones.select { |d| d['estado'] == 'fallido' }
|
||||
|
||||
puts "\n#{Color::BOLD}#{Color::CYAN}🛸 Flota de Drones#{Color::RESET}\n"
|
||||
puts "─" * 70
|
||||
|
||||
# Activos
|
||||
if activos.any?
|
||||
puts "#{Color::YELLOW}⏳ En vuelo (#{activos.length})#{Color::RESET}"
|
||||
activos.each do |d|
|
||||
pid = d['pid'] || d['pid_vigilado']
|
||||
vivo = pid && system("kill -0 #{pid} 2>/dev/null")
|
||||
elapsed = tiempo_transcurrido(d['inicio'])
|
||||
comando_corto = (d['comando'] || d['nota'] || '?')[0..55]
|
||||
estado_pid = vivo ? "#{Color::GREEN}●#{Color::RESET}" : "#{Color::RED}●#{Color::RESET}"
|
||||
puts " #{estado_pid} #{d['tarea_id']} #{elapsed.rjust(8)} #{comando_corto}"
|
||||
end
|
||||
puts ""
|
||||
end
|
||||
|
||||
# Completados
|
||||
if completados.any?
|
||||
puts "#{Color::GREEN}✔ Completados (#{completados.length})#{Color::RESET}"
|
||||
completados.last(5).each do |d|
|
||||
duracion = calcular_duracion(d['inicio'], d['fin'])
|
||||
comando_corto = (d['nota'] || d['comando'] || '?')[0..55]
|
||||
puts " ✔ #{d['tarea_id']} #{duracion.rjust(8)} #{comando_corto}"
|
||||
end
|
||||
puts ""
|
||||
end
|
||||
|
||||
# Fallidos
|
||||
if fallidos.any?
|
||||
puts "#{Color::RED}✖ Fallidos (#{fallidos.length})#{Color::RESET}"
|
||||
fallidos.each do |d|
|
||||
comando_corto = (d['nota'] || d['comando'] || '?')[0..55]
|
||||
puts " ✖ #{d['tarea_id']} exit=#{d['exit_code']} #{comando_corto}"
|
||||
end
|
||||
puts ""
|
||||
end
|
||||
|
||||
puts "─" * 70
|
||||
puts "#{Color::BOLD}Total: #{drones.length} drones | " \
|
||||
"#{Color::YELLOW}⏳#{activos.length}#{Color::RESET} | " \
|
||||
"#{Color::GREEN}✔#{completados.length}#{Color::RESET} | " \
|
||||
"#{Color::RED}✖#{fallidos.length}#{Color::RESET}"
|
||||
end
|
||||
|
||||
# ─── salud: health check activo ────────────────────────────────
|
||||
def cmd_salud
|
||||
drones = cargar_todos
|
||||
problemas = []
|
||||
|
||||
if drones.empty?
|
||||
puts "#{Color::GREEN}✔ Sin drones — nada que verificar.#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
puts "\n#{Color::BOLD}🏥 Diagnóstico de Salud#{Color::RESET}\n"
|
||||
puts "─" * 70
|
||||
|
||||
drones.each do |d|
|
||||
next unless d['estado'] == 'vigilando'
|
||||
|
||||
pid = d['pid'] || d['pid_vigilado']
|
||||
vivo = pid && system("kill -0 #{pid} 2>/dev/null")
|
||||
elapsed_min = minutos_transcurridos(d['inicio'])
|
||||
|
||||
# Detectar zombie: meta dice vigilando pero PID muerto
|
||||
if !vivo
|
||||
problemas << { dron: d['tarea_id'], tipo: '💀 Zombie', detalle: "PID #{pid} muerto pero estado=vigilando", meta: d }
|
||||
puts " #{Color::RED}💀 ZOMBIE#{Color::RESET} #{d['tarea_id']} — PID #{pid} muerto"
|
||||
|
||||
# Auto-reparar: actualizar metadata
|
||||
meta_path = File.join(DRON_DIR, "#{d['tarea_id']}.json")
|
||||
if File.exist?(meta_path)
|
||||
d['estado'] = 'fallido'
|
||||
d['fin'] = Time.now.iso8601
|
||||
d['exit_code'] = -1
|
||||
d['nota_salud'] = 'Auto-detectado como zombie por dron salud'
|
||||
File.write(meta_path, JSON.pretty_generate(d))
|
||||
puts " #{Color::DIM}→ Auto-reparado: marcado como fallido#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
|
||||
# Detectar estancado: lleva mucho tiempo
|
||||
if vivo && elapsed_min > STALE_MINUTES
|
||||
problemas << { dron: d['tarea_id'], tipo: '🐌 Estancado', detalle: "#{elapsed_min}min sin finalizar" }
|
||||
puts " #{Color::YELLOW}🐌 ESTANCADO#{Color::RESET} #{d['tarea_id']} — #{elapsed_min}min activo"
|
||||
|
||||
# Verificar si el log sigue creciendo
|
||||
log_path = File.join(DRON_DIR, "#{d['tarea_id']}.log")
|
||||
if File.exist?(log_path)
|
||||
log_age = ((Time.now - File.mtime(log_path)) / 60).to_i
|
||||
if log_age > 30
|
||||
puts " #{Color::DIM}→ Log sin actualizar hace #{log_age}min#{Color::RESET}"
|
||||
else
|
||||
puts " #{Color::DIM}→ Log activo (última escritura hace #{log_age}min)#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Sano
|
||||
if vivo && elapsed_min <= STALE_MINUTES
|
||||
puts " #{Color::GREEN}💚 SANO#{Color::RESET} #{d['tarea_id']} — #{elapsed_min}min, PID #{pid} activo"
|
||||
end
|
||||
end
|
||||
|
||||
# Eventos huérfanos: drones con evento_id pero sin PID vivo
|
||||
eventos_pendientes = drones.select { |d| d['estado'] == 'fallido' && d['evento_id'] }
|
||||
if eventos_pendientes.any?
|
||||
puts "\n#{Color::YELLOW}⚠ Eventos con drones fallidos:#{Color::RESET}"
|
||||
eventos_pendientes.each do |d|
|
||||
puts " Evento ##{d['evento_id']} — #{d['tarea_id']} (exit=#{d['exit_code']})"
|
||||
end
|
||||
end
|
||||
|
||||
puts ""
|
||||
puts "─" * 70
|
||||
if problemas.empty?
|
||||
puts "#{Color::GREEN}#{Color::BOLD}✔ Flota saludable — sin problemas detectados.#{Color::RESET}"
|
||||
else
|
||||
puts "#{Color::RED}#{Color::BOLD}⚠ #{problemas.length} problema(s) detectado(s).#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
|
||||
# ─── limpiar: borrar tareas completadas ──────────────────────
|
||||
def cmd_limpiar
|
||||
Dir.glob(File.join(DRON_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(DRON_DIR, "#{base}#{ext}"))
|
||||
end
|
||||
end
|
||||
end
|
||||
puts "#{Color::GREEN}✔ Tareas completadas limpiadas.#{Color::RESET}"
|
||||
end
|
||||
|
||||
# ─── Helpers ─────────────────────────────────────────────────
|
||||
def cargar_todos
|
||||
Dir.glob(File.join(DRON_DIR, '*.json')).sort.map do |f|
|
||||
JSON.parse(File.read(f)) rescue nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
def tiempo_transcurrido(inicio_str)
|
||||
return '?' unless inicio_str
|
||||
mins = ((Time.now - Time.parse(inicio_str)) / 60).to_i
|
||||
if mins < 60
|
||||
"#{mins}min"
|
||||
else
|
||||
"#{mins / 60}h#{mins % 60}m"
|
||||
end
|
||||
end
|
||||
|
||||
def calcular_duracion(inicio_str, fin_str)
|
||||
return '?' unless inicio_str && fin_str
|
||||
mins = ((Time.parse(fin_str) - Time.parse(inicio_str)) / 60).to_i
|
||||
if mins < 60
|
||||
"#{mins}min"
|
||||
else
|
||||
"#{mins / 60}h#{mins % 60}m"
|
||||
end
|
||||
end
|
||||
|
||||
def minutos_transcurridos(inicio_str)
|
||||
return 0 unless inicio_str
|
||||
((Time.now - Time.parse(inicio_str)) / 60).to_i
|
||||
end
|
||||
|
||||
def mostrar_ayuda
|
||||
puts <<~HELP
|
||||
#{Color::BOLD}#{Color::CYAN}🛸 Dron — Sistema de Drones Autónomos#{Color::RESET}
|
||||
|
||||
Flota de drones que operan de forma autónoma: ejecutan tareas
|
||||
en background, registran en bitácora, detectan problemas y
|
||||
se auto-gestionan. Sistema vivo, no estático.
|
||||
|
||||
#{Color::YELLOW}Comandos:#{Color::RESET}
|
||||
dron lanzar --evento ID -- <cmd> Lanzar dron de trabajo
|
||||
dron flota Dashboard compacto de la flota
|
||||
dron salud Health check (detecta zombies/estancados)
|
||||
dron estado Estado detallado de cada dron
|
||||
dron vigilar --evento ID --pid PID Vigilar PID existente
|
||||
dron limpiar Limpiar completados/fallidos
|
||||
|
||||
#{Color::YELLOW}Evento AUTO:#{Color::RESET}
|
||||
--evento AUTO crea un evento de bitácora automáticamente.
|
||||
--nota "texto" define la descripción del evento.
|
||||
|
||||
#{Color::YELLOW}Ejemplos:#{Color::RESET}
|
||||
./adn/tools/run dron lanzar --evento AUTO --nota "Backup srvv-sitio" -- ./adn/tools/run bkps backup srvv-sitio
|
||||
./adn/tools/run dron flota
|
||||
./adn/tools/run dron salud
|
||||
HELP
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,286 @@
|
||||
#!/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
|
||||
if @args.empty? || @args.first == 'help' || @args.first == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
accion = @args.shift
|
||||
case accion
|
||||
when 'lanzar'
|
||||
cmd_lanzar(@args)
|
||||
when 'vigilar'
|
||||
cmd_vigilar(@args)
|
||||
when 'estado'
|
||||
cmd_estado
|
||||
when 'limpiar'
|
||||
cmd_limpiar
|
||||
else
|
||||
puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}"
|
||||
mostrar_ayuda
|
||||
exit 1
|
||||
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 -- <comando>"
|
||||
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 -- <comando> 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
|
||||
Reference in New Issue
Block a user