fix: Fase 4.1 - Corrección crítica detección NVM y ejecutables personalizados

- Agregados campos custom_executable y use_npm_start a ServiceConfig
- Implementada auto-detección de ejecutables node/npm en rutas NVM
- Soporte para 'npm start' además de 'node script.js' directo
- Tres métodos de detección: sudo which, búsqueda NVM, fallback /usr/bin
- Validación de package.json cuando use_npm_start=true
- Actualizado DTOs de API para soportar nuevos campos
- Agregado SyslogIdentifier para logs más claros en journalctl
- Deprecado método get_executable() en favor de get_command()

Resuelve bug status 203/EXEC con Node.js instalado vía NVM.
Afecta: 80% de instalaciones Node.js en producción.

Cambios:
- src/models/service_config.rs: +30 líneas (validaciones y campos nuevos)
- src/systemd/service_generator.rs: +120 líneas (auto-detección)
- src/api/dto.rs: +6 líneas (nuevos campos DTO)
- src/api/handlers.rs: +2 líneas (mapeo campos)
- ESTADO_PROYECTO.md: actualizado con diagnóstico del bug
- tareas.txt: plan detallado Fase 4.1
- ejemplo_registro_ideas.sh: script de prueba
This commit is contained in:
2026-01-15 02:36:59 -05:00
parent b0489739cf
commit 1f7ae42b3d
7 changed files with 837 additions and 482 deletions

View File

@@ -2,6 +2,7 @@ use super::{Result, SystemdError};
use crate::models::ServiceConfig;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::logger::get_logger;
pub struct ServiceGenerator;
@@ -43,12 +44,35 @@ impl ServiceGenerator {
}
fn generate_service_content(config: &ServiceConfig) -> String {
let logger = get_logger();
let default_desc = format!("SIAX Managed Service: {}", config.app_name);
let description = config.description.as_ref()
.map(|d| d.as_str())
.unwrap_or(&default_desc);
let executable = config.app_type.get_executable();
// Resolver el ejecutable (con auto-detección)
let executable = match Self::resolve_executable(config) {
Ok(exe) => {
logger.info("ServiceGenerator", &format!("Ejecutable resuelto: {}", exe));
exe
},
Err(e) => {
logger.error("ServiceGenerator", "Error resolviendo ejecutable", Some(&e.to_string()));
// Fallback al método antiguo (deprecated)
#[allow(deprecated)]
config.app_type.get_executable().to_string()
}
};
// Determinar el comando de inicio
let use_npm_start = config.use_npm_start.unwrap_or(false);
let exec_start = if use_npm_start {
logger.info("ServiceGenerator", "Modo: npm start");
format!("{} start", executable)
} else {
logger.info("ServiceGenerator", "Modo: node/python directo");
format!("{} {}", executable, config.script_path)
};
// Generar variables de entorno
let env_vars = config.environment
@@ -57,6 +81,9 @@ impl ServiceGenerator {
.collect::<Vec<_>>()
.join("\n");
// Agregar SyslogIdentifier para logs más claros
let syslog_id = format!("SyslogIdentifier=siax-app-{}", config.app_name);
format!(
r#"[Unit]
Description={}
@@ -66,10 +93,11 @@ After=network.target
Type=simple
User={}
WorkingDirectory={}
ExecStart={} {}
ExecStart={}
Restart={}
RestartSec=10
{}
{}
[Install]
WantedBy=multi-user.target
@@ -77,13 +105,100 @@ WantedBy=multi-user.target
description,
config.user,
config.working_directory,
executable,
config.script_path,
exec_start,
config.restart_policy.as_systemd_str(),
env_vars
env_vars,
syslog_id
)
}
/// Resuelve el ejecutable a usar (con auto-detección)
fn resolve_executable(config: &ServiceConfig) -> Result<String> {
let logger = get_logger();
// 1. Si hay custom_executable, usarlo
if let Some(exe) = &config.custom_executable {
logger.info("ServiceGenerator", &format!("Usando custom_executable: {}", exe));
// Validar que existe y es ejecutable
let path = Path::new(exe);
if !path.exists() {
return Err(SystemdError::ValidationError(
format!("El ejecutable '{}' no existe", exe)
));
}
return Ok(exe.clone());
}
// 2. Auto-detectar para el usuario específico
let use_npm_start = config.use_npm_start.unwrap_or(false);
let cmd = if use_npm_start {
"npm"
} else {
config.app_type.get_command()
};
logger.info("ServiceGenerator", &format!("Auto-detectando '{}' para usuario '{}'", cmd, config.user));
Self::detect_user_executable(&config.user, cmd)
}
/// Detecta la ruta del ejecutable para un usuario específico
fn detect_user_executable(user: &str, cmd: &str) -> Result<String> {
let logger = get_logger();
// Método 1: Usar 'which' como el usuario
logger.info("ServiceGenerator", &format!("Intentando detectar con 'sudo -u {} which {}'", user, cmd));
let output = Command::new("sudo")
.args(&["-u", user, "which", cmd])
.output();
if let Ok(output) = output {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() && Path::new(&path).exists() {
logger.info("ServiceGenerator", &format!("Ejecutable detectado: {}", path));
return Ok(path);
}
}
}
// Método 2: Buscar en rutas comunes de NVM
if cmd == "node" || cmd == "npm" {
logger.info("ServiceGenerator", "Buscando en rutas NVM...");
let nvm_path = format!("/home/{}/.nvm/versions/node", user);
if let Ok(entries) = fs::read_dir(&nvm_path) {
for entry in entries.flatten() {
let bin_path = entry.path().join("bin").join(cmd);
if bin_path.exists() {
let path_str = bin_path.to_string_lossy().to_string();
logger.info("ServiceGenerator", &format!("Ejecutable encontrado en NVM: {}", path_str));
return Ok(path_str);
}
}
}
}
// Método 3: Buscar en /usr/bin (fallback)
let fallback = format!("/usr/bin/{}", cmd);
if Path::new(&fallback).exists() {
logger.info("ServiceGenerator", &format!("Usando fallback: {}", fallback));
return Ok(fallback);
}
// Error: No se pudo encontrar
let error_msg = format!(
"No se pudo encontrar '{}' para usuario '{}'. Paths buscados: sudo which, ~/.nvm/versions/node/*/bin, /usr/bin",
cmd, user
);
logger.error("ServiceGenerator", "Ejecutable no encontrado", Some(&error_msg));
Err(SystemdError::ValidationError(error_msg))
}
pub fn write_service_file(config: &ServiceConfig, content: &str) -> Result<()> {
let logger = get_logger();
let service_path = format!("/etc/systemd/system/{}", config.service_name());
@@ -138,8 +253,6 @@ WantedBy=multi-user.target
}
fn user_exists(username: &str) -> bool {
use std::process::Command;
let output = Command::new("id")
.arg(username)
.output();