From 0ab5b4f68c81d950043632d1e672b2fee28709cd Mon Sep 17 00:00:00 2001 From: Exequiel Aramburu Date: Tue, 29 Sep 2026 23:32:03 -0300 Subject: [PATCH] se adecua el servicio --- build/Deb64/DEBIAN/postinst | 18 +- build/Deb64/DEBIAN/postrm | 12 +- build/Deb64/DEBIAN/preinst | 35 ++- build/Deb64/DEBIAN/prerm | 10 + build/Deb64/etc/init.d/emalibre | 332 ++++++++++++----------------- build/controlDeb/control_arm_web | 2 +- build/controlDeb/control_linux_web | 2 +- generarDeb64.sh | 63 ++++-- src/package.json | 6 +- test/init-service.test.py | 206 ++++++++++++++++++ 10 files changed, 463 insertions(+), 223 deletions(-) create mode 100755 build/Deb64/DEBIAN/prerm create mode 100644 test/init-service.test.py diff --git a/build/Deb64/DEBIAN/postinst b/build/Deb64/DEBIAN/postinst index 3dd2bed..05c3f25 100755 --- a/build/Deb64/DEBIAN/postinst +++ b/build/Deb64/DEBIAN/postinst @@ -2,9 +2,14 @@ # postinst script for EmaLibre set -e printf "\033[1;4m3) Se aplican los permisos necesarios.\033[0m" -chmod -R 777 /usr/share/emalibre +if [ -d /usr/share/emalibre/gui_web ]; then + # El servicio corre como root: sus fuentes no deben ser editables por todos. + chmod -R u+rwX,go+rX,go-w /usr/share/emalibre +else + chmod -R 777 /usr/share/emalibre +fi chmod a-x /usr/share/menu/emalibre -chmod 777 /usr/share/applications/emalibre.desktop +chmod 644 /usr/share/applications/emalibre.desktop printf "\033[1;32m --> OK. \033[0m\n" printf "\033[1;4m4) Se actualizan los menus del sistema.\033[0m" if [ "$1" = "configure" ] && [ -x /usr/bin/update-menus ]; then update-menus ; fi @@ -16,7 +21,7 @@ if [ -f "/tmp/ConfiguracionGeneral.js" ]; then printf "\033[1;4m6) Se restaura el archivo de configuracion.\033[0m" if [ -d "/usr/share/emalibre/conf/" ]; then mv -f /tmp/ConfiguracionGeneral.js /usr/share/emalibre/conf/ - chmod 777 /usr/share/emalibre/conf/ConfiguracionGeneral.js || true + chmod 644 /usr/share/emalibre/conf/ConfiguracionGeneral.js || true printf "\033[1;32m --> OK. \033[0m\n" elif [ -d "/usr/share/emalibre/package.nw/conf/" ]; then mv -f /tmp/ConfiguracionGeneral.js /usr/share/emalibre/package.nw/conf/ @@ -34,4 +39,11 @@ else printf "\033[1;36m --> /usr/share/emalibre/conf/ConfiguracionGeneral.js. \033[0m\n" echo "" fi +if [ "$1" = configure ] && [ -x /etc/init.d/emalibre ]; then + update-rc.d emalibre defaults + if [ -d /run/systemd/system ]; then + systemctl daemon-reload + fi + invoke-rc.d emalibre start +fi exit 0 diff --git a/build/Deb64/DEBIAN/postrm b/build/Deb64/DEBIAN/postrm index a2b4290..fe887d5 100755 --- a/build/Deb64/DEBIAN/postrm +++ b/build/Deb64/DEBIAN/postrm @@ -1,4 +1,12 @@ #!/bin/sh set -e -echo "Se actualiza los menus y reglas de control..." -if [ "$1" = "configure" ] && [ -x /usr/bin/update-menus ]; then update-menus ; fi +case "$1" in + remove|purge) + if [ -x /usr/bin/update-menus ]; then update-menus; fi + if [ -d /run/systemd/system ]; then systemctl daemon-reload; fi + ;; +esac +if [ "$1" = purge ] && [ ! -e /etc/init.d/emalibre ]; then + update-rc.d emalibre remove +fi +exit 0 diff --git a/build/Deb64/DEBIAN/preinst b/build/Deb64/DEBIAN/preinst index 8ebc98a..b27d09d 100755 --- a/build/Deb64/DEBIAN/preinst +++ b/build/Deb64/DEBIAN/preinst @@ -6,8 +6,6 @@ # * `upgrade' # * `abort-upgrade' set -e -pidFile="/var/run/emalibre.pid" -pidMonitorFile="/var/run/monitor_emalibre.pid" echo "" printf "\033[1;35m***************************************************************\n" printf "Gracias por instalar EMA Libre Cliente (Carpincho)!!.\n" @@ -19,14 +17,31 @@ case "$1" in printf "\033[1;10m * 1.0) Se verifica y detiene si existe EMA Libre corriendo.\033[0m" # 0. Se detiene emalibre si existe instancia corriendo. - if [ -f $pidFile ]; then - rm -f /var/run/emalibre.pid - rm -f /var/run/emalibre.log - fi - if [ -f $pidMonitorFile ]; then - rm -f /var/run/monitor_emalibre.pid - rm -f /var/run/monitor_emalibre.log - fi + # Versiones anteriores usaban stopall y borraban el PID sin parar. + # Detener únicamente EMA_Web, incluso durante la primera migración. + if [ -f /usr/share/emalibre/node_modules/forever/lib/forever.js ]; then + NODE=/usr/share/emalibre/node_modules/node/bin/node + # Compatibilidad al actualizar una instalación antigua sin runtime incluido. + if [ ! -x "$NODE" ]; then + NODE=$(command -v node || true) + fi + if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then + echo "No se encontró Node para detener la instalación anterior." >&2 + exit 1 + fi + HOME=/root FOREVER_ROOT=/root/.forever "$NODE" - <<'JS' +const forever = require('/usr/share/emalibre/node_modules/forever'); +const timer = setTimeout(() => process.exit(1), 15000); +forever.list(false, (err, processes) => { + if (err) { console.error(err); process.exit(1); } + if (!(processes || []).some(p => p.uid === 'EMA_Web')) { + clearTimeout(timer); process.exit(0); + } + forever.stop('EMA_Web').on('stop', () => process.exit(0)) + .on('error', err => { console.error(err); process.exit(1); }); +}); +JS + fi printf "\033[1;32m --> OK. \033[0m\n" # 1. Se consulta si existe el archivo de configuracion if [ -f "/usr/share/emalibre/conf/ConfiguracionGeneral.js" ]; then diff --git a/build/Deb64/DEBIAN/prerm b/build/Deb64/DEBIAN/prerm new file mode 100755 index 0000000..ffbf1ae --- /dev/null +++ b/build/Deb64/DEBIAN/prerm @@ -0,0 +1,10 @@ +#!/bin/sh +set -e +case "$1" in + remove|upgrade|deconfigure) + if [ -x /etc/init.d/emalibre ]; then + invoke-rc.d emalibre stop + fi + ;; +esac +exit 0 diff --git a/build/Deb64/etc/init.d/emalibre b/build/Deb64/etc/init.d/emalibre index 0809888..58229f0 100755 --- a/build/Deb64/etc/init.d/emalibre +++ b/build/Deb64/etc/init.d/emalibre @@ -1,207 +1,159 @@ #!/bin/bash ### BEGIN INIT INFO -# Provides: EMA Libre Carpincho -# Required-Start: $syslog -# Required-Stop: $syslog +# Provides: emalibre +# Required-Start: $remote_fs $network $syslog +# Required-Stop: $remote_fs $network $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 -# Short-Description: EMA Libre Carpincho demonio -# Description: Start/stop/Restart EMA Libre Carpincho +# Short-Description: EMA Libre Carpincho web ### END INIT INFO -### CONFIG ZONE -# Get function from functions library -#set -e +PATH=/usr/sbin:/usr/bin:/sbin:/bin +export PATH . /lib/lsb/init-functions -RETVAL=0 - -# Nombre del servicio. -serviceName="emalibre" - -# Ubicacion de la APP -appFilePath="/usr/share/emalibre" - -# Archivo de Inicio. -web="gui_web/express.js" - -### END CONFIG ZONE - -# Ubicacion de logs y del pid. -pidFile="/var/run/$serviceName.pid" -logFile="/var/run/$serviceName.log" - -# Ubicacion de node como modulo de carpincho. -nodemodule="$appFilePath/node_modules/node/bin" - -# Ubicacion de forever como modulo de carpincho. +appFilePath=/usr/share/emalibre +pidFile=/run/emalibre.pid +logDir=/var/log/emalibre +modeFile=/etc/emalibre_modo foreverApp="$appFilePath/node_modules/forever/bin/forever" +# Mantener el registro de instalaciones anteriores iniciadas como root. +export HOME=/root +export FOREVER_ROOT=/root/.forever +nodeBin="$appFilePath/node_modules/node/bin/node" +export PATH="$appFilePath/node_modules/node/bin:$PATH" -# Ubicacion de logs de EMA Libre -emaLogs="$appFilePath/logs/debug.log" - -export PATH=$PATH:$nodemodule - +fail() { echo "emalibre: $*" >&2; return 1; } +require_root() { + [ "$(id -u)" -eq 0 ] || { fail "Este comando requiere root."; return 4; } +} +check_runtime() { + [ -x "$nodeBin" ] || { fail "Falta el Node incluido en $nodeBin (Node.js >= 20)."; return 5; } + "$nodeBin" -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 20 ? 0 : 1)' || { + fail "Se requiere Node.js >= 20."; return 5; + } + [ -f "$foreverApp" ] && [ -f "$appFilePath/gui_web/express.js" ] || { + fail "La instalación web está incompleta."; return 5; + } +} +node_run() { "$nodeBin" "$@" 9>&-; } +forever_cmd() { node_run "$foreverApp" "$@"; } +# Consultar el supervisor, sin confiar en archivos PID residuales. +# 0: activo; 3: detenido; 4: no se pudo consultar. +process_status() { + "$nodeBin" - "$appFilePath" <<'JS' +const root = process.argv[2]; +const timer = setTimeout(() => process.exit(4), 5000); +try { + require(root + '/node_modules/forever').list(false, (err, processes) => { + clearTimeout(timer); + if (err) { console.error(err.message); process.exit(4); } + const active = (processes || []).some(p => p.uid === 'EMA_Web'); + process.exit(active ? 0 : 3); + }); +} catch (err) { console.error(err.message); process.exit(4); } +JS +} start() { - if [ ! -f /etc/emalibre_modo ] - then - `echo "web" > /etc/emalibre_modo` - fi - MODO=`cat /etc/emalibre_modo` - if [ -f $pidFile ]; then - log_daemon_msg "EMA libre Carpincho se encuentra corriendo." - RETVAL=$? - else - if [ $MODO = 'web' ] - then - log_daemon_msg "Comenzando EMA Libre Carpincho en modo web" - echo "$(date '+%d/%m/%Y %H:%M:%S') - 0|SERVICIO|Inicio el servicio en modo web." >> $emaLogs - $foreverApp start --uid "EMA_Web" --id "EMA_Web" --minUptime 5000 --spinSleepTime 5000 --sourceDir $appFilePath --workingDir $appFilePath --pidFile $pidFile -l $logFile -a -d -c node "$web" iniciar - RETVAL=$? - fi - fi - RETVAL=$? + local state mode + process_status; state=$? + case "$state" in + 0) echo "EMA Libre ya está ejecutándose."; return 0 ;; + 3) ;; + *) fail "No se pudo consultar Forever."; return "$state" ;; + esac + if [ ! -f "$modeFile" ]; then + printf 'web\n' > "$modeFile" || return 1 + fi + mode=$(tr -d '[:space:]' < "$modeFile") || return 1 + [ "$mode" = web ] || { fail "Modo inválido: '$mode'; se esperaba web."; return 2; } + mkdir -p "$logDir" || return 1 + rm -f "$pidFile" || return 1 + # Forever concatena sourceDir y script: el script debe ser relativo. + forever_cmd start --uid EMA_Web --id EMA_Web --minUptime 5000 --spinSleepTime 5000 \ + --sourceDir "$appFilePath" --workingDir "$appFilePath" --pidFile "$pidFile" \ + -l "$logDir/forever.log" -o "$logDir/stdout.log" -e "$logDir/stderr.log" \ + -a -c "$nodeBin" "gui_web/express.js" iniciar || return $? + # Forever arranca en segundo plano: esperar a que registre el supervisor. + local attempt + for attempt in {1..10}; do + process_status; state=$? + [ "$state" -eq 0 ] && { echo "EMA Libre iniciado."; return 0; } + [ "$state" -eq 3 ] || return "$state" + sleep 1 + done + fail "El supervisor no arrancó; revise $logDir." } - -restart() { - log_daemon_msg "Reiniciando EMA Libre Carpincho" - echo "$(date '+%d/%m/%Y %H:%M:%S') - 0|SERVICIO|Reiniciando el servicio." >> $emaLogs - $foreverApp restart $nodemodule - RETVAL=$? -} - stop() { - if [ -f $pidFile ]; then - log_daemon_msg "Deteniendo EMA Libre Carpincho." - echo "$(date '+%d/%m/%Y %H:%M:%S') - 0|SERVICIO|Detenido el servicio." >> $emaLogs - $foreverApp stopall - RETVAL=$? - else - log_daemon_msg "EMA libre Carpincho no se encuentra ejecutandose." - fi - RETVAL=$? + local state + process_status; state=$? + case "$state" in + 0) forever_cmd stop EMA_Web || return $? ;; + 3) echo "EMA Libre ya está detenido." ;; + *) fail "No se pudo consultar Forever."; return "$state" ;; + esac + process_status; state=$? + [ "$state" -eq 3 ] || { fail "No se pudo confirmar la parada."; return 1; } + rm -f "$pidFile" } - - status() { - log_daemon_msg "Mostrando estado nativo del servicio." - RETVAL=$? + local state + process_status; state=$? + case "$state" in + 0) echo "EMA Libre: supervisor activo." ;; + 3) echo "EMA Libre: detenido." ;; + *) fail "Estado desconocido; no se pudo consultar Forever." ;; + esac + return "$state" +} +set_boot() { + # El arranque del servicio es independiente del inicio del recolector. + if [ -d /run/systemd/system ]; then + systemctl "$1" emalibre + else + update-rc.d emalibre defaults && update-rc.d emalibre "$1" + fi +} +view_conf() { + printf 'Modo: '; cat "$modeFile" || return 1 + if [ -d /run/systemd/system ]; then + systemctl is-enabled emalibre + else + echo 'Inicio del servicio: consultar enlaces /etc/rc[2-5].d/*emalibre' + fi + "$nodeBin" - "$appFilePath/conf/ConfiguracionGeneral.js" <<'JS' +const cfg = require(process.argv[2]).ConfGeneral; +for (const key of ['Tiempo_de_Recoleccion', 'UTC', 'SistemaMetrico', 'debug', + 'debugTamanio', 'NombreEstacion', 'Modo', 'inicio_automatico', 'Audio', + 'notificar_cambios', 'audioVoces', 'IP', 'Puerto', 'Ruta']) { + console.log(key + ': ' + cfg[key]); +} +JS } - -uptime() { - echo "$(date '+%d/%m/%Y %H:%M:%S') - 0|SERVICIO|Mostrando el uptime del servicio (consola)." >> $emaLogs - $foreverApp list - RETVAL=$? -} - -reporte() { - echo "$(date '+%d/%m/%Y %H:%M:%S') - 0|SERVICIO|Mostrando el reporte de consola." >> $emaLogs - pushd "$appFilePath" - "$nodemodule/node" core/reporte.js - RETVAL=$? -} - -if [ -d /usr/share/emalibre/gui_web ] -then - # SI EXISTE EL MODULO GUI_WEB se habilitan algunas opciones - case "$1" in - start) - start - - ;; - stop) - stop - - ;; - status) - status - - ;; - restart) - restart - - ;; - reporte) - reporte - - ;; - uptime) - uptime - - ;; - set-inicio-enabled) - log_daemon_msg "Se Habilito el servicio para inicio automatico." - systemctl enable emalibre - "$nodemodule/node" -e " - const fs = require('fs'); - const cfgPath = '/usr/share/emalibre/conf/ConfiguracionGeneral.js'; - const cfg = require(cfgPath); - cfg.ConfGeneral.inicio_automatico = true; - fs.writeFileSync( - cfgPath, - 'module.exports = ' + JSON.stringify(cfg, null, 2) + ';\n' - ); - " - - ;; - set-inicio-disable) - log_daemon_msg "Se Deshabilito el inicio automatico del servicio." - systemctl disable emalibre - "$nodemodule/node" -e " - const fs = require('fs'); - const cfgPath = '/usr/share/emalibre/conf/ConfiguracionGeneral.js'; - const cfg = require(cfgPath); - cfg.ConfGeneral.inicio_automatico = false; - fs.writeFileSync( - cfgPath, - 'module.exports = ' + JSON.stringify(cfg, null, 2) + ';\n' - ); - " - ;; - view-conf) - log_daemon_msg "Configuración de EMA Libre Carpincho" - log_daemon_msg "-------------------------------------" - CFG=/usr/share/emalibre/conf/ConfiguracionGeneral.js - if [ -f /etc/emalibre_modo ] - then - log_daemon_msg "Modo actual: `cat /etc/emalibre_modo` " - else - `echo "consola" > /etc/emalibre_modo` - log_daemon_msg "Modo actual: `cat /etc/emalibre_modo` " - fi - log_daemon_msg "Inicio Automatico (servicio): `systemctl is-enabled emalibre 2> echo`" - log_daemon_msg "" - log_daemon_msg "----------------------" - log_daemon_msg "Configuracion Global:" - log_daemon_msg "->`grep "Version" /usr/share/emalibre/conf/ConfiguracionGlobal.js`" - log_daemon_msg "->`grep "Nombre" /usr/share/emalibre/conf/ConfiguracionGlobal.js`" - log_daemon_msg "----------------------" - log_daemon_msg "Configuracion General:" - log_daemon_msg "" - CFG=/usr/share/emalibre/conf/ConfiguracionGeneral.js - log_daemon_msg "-> Tiempo de Recolección: $(sed -nE 's/^[[:space:]]*"Tiempo_de_Recoleccion"[[:space:]]*:[[:space:]]*([0-9]+),?/\1/p' $CFG)" - log_daemon_msg "-> UTC: $(sed -nE 's/^[[:space:]]*"UTC"[[:space:]]*:[[:space:]]*(-?[0-9]+),?/\1/p' $CFG)" - log_daemon_msg "-> Sistema Métrico: $(sed -nE 's/^[[:space:]]*"SistemaMetrico"[[:space:]]*:[[:space:]]*"([^"]+)",?/\1/p' $CFG)" - log_daemon_msg "-> Nivel Debug: $(sed -nE 's/^[[:space:]]*"debug"[[:space:]]*:[[:space:]]*([0-9]+),?/\1/p' $CFG)" - log_daemon_msg "-> Tamaño Debug: $(sed -nE 's/^[[:space:]]*"debugTamanio"[[:space:]]*:[[:space:]]*([0-9]+),?/\1/p' $CFG)" - log_daemon_msg "-> Nombre Estación: $(sed -nE 's/^[[:space:]]*"NombreEstacion"[[:space:]]*:[[:space:]]*"([^"]+)",?/\1/p' $CFG)" - log_daemon_msg "-> Modo: $(sed -nE 's/^[[:space:]]*"Modo"[[:space:]]*:[[:space:]]*"([^"]+)",?/\1/p' $CFG)" - log_daemon_msg "-> Llave R: $(sed -nE 's/^[[:space:]]*"Llave_R"[[:space:]]*:[[:space:]]*"([^"]+)",?/\1/p' $CFG)" - log_daemon_msg "-> Inicio automático: $(sed -nE 's/^[[:space:]]*"inicio_automatico"[[:space:]]*:[[:space:]]*(true|false),?/\1/p' $CFG)" - log_daemon_msg "-> Audio: $(sed -nE 's/^[[:space:]]*"Audio"[[:space:]]*:[[:space:]]*(true|false),?/\1/p' $CFG)" - log_daemon_msg "-> Notificar cambios: $(sed -nE 's/^[[:space:]]*"notificar_cambios"[[:space:]]*:[[:space:]]*(true|false),?/\1/p' $CFG)" - log_daemon_msg "-> Audio Voces: $(sed -nE 's/^[[:space:]]*"audioVoces"[[:space:]]*:[[:space:]]*"([^"]+)",?/\1/p' $CFG)" - log_daemon_msg "-> IP: $(sed -nE 's/^[[:space:]]*"IP"[[:space:]]*:[[:space:]]*"([^"]+)",?/\1/p' $CFG)" - log_daemon_msg "-> Puerto: $(sed -nE 's/^[[:space:]]*"Puerto"[[:space:]]*:[[:space:]]*"([^"]+)",?/\1/p' $CFG)" - log_daemon_msg "-> Ruta: $(sed -nE 's/^[[:space:]]*"Ruta"[[:space:]]*:[[:space:]]*"([^"]+)",?/\1/p' $CFG)" - ;; - *) - echo "Utilice: {start|stop|status|restart|set-inicio-enabled|set-inicio-disable|view-conf|reporte|uptime}" - exit 1 - - ;; - esac -fi - -exit $RETVAL +case "${1:-}" in + start|stop|restart|force-reload|status|uptime|reporte|view-conf|set-inicio-enabled|set-inicio-disable) ;; + *) echo "Uso: $0 {start|stop|restart|force-reload|status|uptime|reporte|view-conf|set-inicio-enabled|set-inicio-disable}" >&2; exit 2 ;; +esac +require_root || exit $? +check_runtime || exit $? +case "$1" in + start|stop|restart|force-reload|set-inicio-enabled|set-inicio-disable) + # Evitar arranques/paradas simultáneos. Cerrar el descriptor en Node + # impide que el daemon herede y retenga el bloqueo. + exec 9>/run/lock/emalibre.lock || exit 1 + flock -w 30 9 || exit 1 + ;; +esac +case "$1" in + start) start ;; + stop) stop ;; + restart|force-reload) stop && start ;; + status) status ;; + uptime) forever_cmd list ;; + reporte) (cd "$appFilePath" && node_run core/reporte.js) ;; + view-conf) view_conf ;; + set-inicio-enabled) set_boot enable ;; + set-inicio-disable) set_boot disable ;; +esac +exit $? diff --git a/build/controlDeb/control_arm_web b/build/controlDeb/control_arm_web index db926e9..0aa23af 100644 --- a/build/controlDeb/control_arm_web +++ b/build/controlDeb/control_arm_web @@ -5,7 +5,7 @@ Priority: optional Section: utils Maintainer: Laboratorio de Investigacion Gugler Homepage: https://ema.gugler.com.ar -Depends: nodejs (>= 14), libusb-1.0-0, libudev1 +Depends: lsb-base, init-system-helpers, util-linux, libusb-1.0-0, libudev1 Conflicts: emalibre-gui Description: EMA Libre Cliente (Carpincho) Software Libre en modo web y consola para Estaciones Meteorológicas Automáticas. diff --git a/build/controlDeb/control_linux_web b/build/controlDeb/control_linux_web index 66dca5f..642fee9 100644 --- a/build/controlDeb/control_linux_web +++ b/build/controlDeb/control_linux_web @@ -5,7 +5,7 @@ Priority: optional Section: application Maintainer: Laboratorio de Investigacion Gugler Homepage: https://ema.gugler.com.ar -Depends: +Depends: lsb-base, init-system-helpers, util-linux, libusb-1.0-0, libudev1 Conflicts: emalibre-gui Description: EMA Libre Cliente (Carpincho) - Software Libre en modo web y consola para Estaciones Meteorológica Automáticas. EMA Libre es Libre y Gratuito desarrollado por el Laboratorio de Investigación Gugler diff --git a/generarDeb64.sh b/generarDeb64.sh index 72a4921..cad1f08 100755 --- a/generarDeb64.sh +++ b/generarDeb64.sh @@ -48,6 +48,12 @@ function spinner() { printf " \b\b\b\b" } +# El entorno de compilación requiere Node 20; el DEB incluye su propio runtime. +if ! node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 20 ? 0 : 1)'; then + log_error "Se requiere Node.js >= 20 para construir el paquete web." + exit 1 +fi + # ======================= # VERSIONES # ======================= @@ -70,7 +76,7 @@ echo "" # LIMPIEZA Y PREPARACIÓN # ======================= log_info "1) Limpiando compilación anterior." -mkdir -p "$DEB_DIR/usr/bin" "$DEB_DIR/usr/share/emalibre" "$DEBS_OUT" +mkdir -p "$DEB_DIR/usr/bin" "$DEB_DIR/usr/share/emalibre" "$DEBS_OUT" "$TMP_DIR" sudo rm -rf "$EMALIBRE_DIR"/* "$TMP_DIR/emalibre_nogui" "$DEB_DIR/usr/bin/"* "$DEB_DIR/usr/share/emalibre/"* log_ok " --> OK." @@ -134,7 +140,21 @@ log_ok " --> OK." log_info "7) Generando node_modules con NPM del proyecto." cd "$EMALIBRE_DIR/package.nw" -npm ci --only=production --no-audit --silent 2>>"$LOG_FILE" 1>/dev/null +# El lockfile no está versionado: sincronizarlo con package.json antes de npm ci. +npm install --package-lock-only --ignore-scripts --no-audit --silent 2>>"$LOG_FILE" 1>/dev/null +npm ci --omit=dev --no-audit --silent 2>>"$LOG_FILE" 1>/dev/null +# npm instala el binario de Node para la arquitectura de compilación dentro del paquete. +BUNDLED_NODE="$PWD/node_modules/node/bin/node" +if [ ! -x "$BUNDLED_NODE" ] || ! "$BUNDLED_NODE" -e ' + const expected = require("./package.json").dependencies.node; + process.exit(process.versions.node === expected && process.platform === "linux" && process.arch === "x64" ? 0 : 1); +'; then + log_error "Falta el runtime Node incluido o no corresponde a la versión/arquitectura del DEB." + exit 1 +fi +# Compilar/cargar las dependencias nativas con el mismo Node que usará el servicio. +PATH="$(dirname "$BUNDLED_NODE"):$PATH" npm rebuild node-hid --no-audit >>"$NODE_HID_LOG" 2>&1 +"$BUNDLED_NODE" -e 'require("node-hid")' log_ok " --> OK." # ======================= @@ -197,7 +217,7 @@ log_ok " --> OK." log_info "4) Aplicando permisos necesarios." sudo chmod +x "$DEB_DIR/usr/share/emalibre/emalibre" -sudo chmod 0755 "$DEB_DIR/DEBIAN" "$DEB_DIR/DEBIAN/postrm" "$DEB_DIR/DEBIAN/postinst" "$DEB_DIR/DEBIAN/preinst" +sudo chmod 0755 "$DEB_DIR/DEBIAN" "$DEB_DIR/DEBIAN/postrm" "$DEB_DIR/DEBIAN/postinst" "$DEB_DIR/DEBIAN/preinst" "$DEB_DIR/DEBIAN/prerm" sudo chown -R root:root "$DEB_DIR" log_ok " --> OK." @@ -206,21 +226,36 @@ sudo ln -sf ../share/emalibre/emalibre "$DEB_DIR/usr/bin/emalibre" log_ok " --> OK." log_info "6) Eliminando node/web/express/forever/servicio para modo GUI y la interfaz para el modo web." -sudo mv "$DEB_DIR/etc/init.d" "$TMP_DIR" 2>/dev/null || true -sudo mv "$DEB_DIR/etc/emalibre_modo" "$TMP_DIR" 2>/dev/null || true -sudo rm -rf "$DEB_DIR/usr/share/emalibre/node_modules/node" \ - "$DEB_DIR/usr/share/emalibre/node_modules/express" \ - "$DEB_DIR/usr/share/emalibre/node_modules/forever" \ - "$DEB_DIR/usr/share/emalibre/node_modules/forever-agent" \ - "$DEB_DIR/usr/share/emalibre/node_modules/forever-monitor" \ - "$DEB_DIR/usr/share/emalibre/gui_web" +# Restaurar las plantillas incluso si falla la construcción del paquete GUI. +SERVICE_BACKUP=$(mktemp -d "$(pwd)/$TMP_DIR/service.XXXXXX") +SERVICE_ETC="$(pwd)/$DEB_DIR/etc" +restore_service() { + if [ -f "$SERVICE_BACKUP/emalibre" ]; then + sudo mv "$SERVICE_BACKUP/emalibre" "$SERVICE_ETC/init.d/emalibre" || return + fi + if [ -f "$SERVICE_BACKUP/emalibre_modo" ]; then + sudo mv "$SERVICE_BACKUP/emalibre_modo" "$SERVICE_ETC/emalibre_modo" || return + fi + rmdir "$SERVICE_BACKUP" +} +trap restore_service EXIT +sudo mv "$SERVICE_ETC/init.d/emalibre" "$SERVICE_BACKUP/emalibre" +sudo mv "$SERVICE_ETC/emalibre_modo" "$SERVICE_BACKUP/emalibre_modo" +sudo rm -rf "$DEB_DIR/usr/share/emalibre/package.nw/node_modules/.bin/node" \ + "$DEB_DIR/usr/share/emalibre/package.nw/node_modules/node-bin-setup" \ + "$DEB_DIR/usr/share/emalibre/package.nw/node_modules/node" \ + "$DEB_DIR/usr/share/emalibre/package.nw/node_modules/express" \ + "$DEB_DIR/usr/share/emalibre/package.nw/node_modules/forever" \ + "$DEB_DIR/usr/share/emalibre/package.nw/node_modules/forever-agent" \ + "$DEB_DIR/usr/share/emalibre/package.nw/node_modules/forever-monitor" \ + "$DEB_DIR/usr/share/emalibre/package.nw/gui_web" log_ok " --> OK." log_info "7) Generando Paquete DEB para GUI 64 bits Linux." #sudo dpkg -b Deb64 debs/"emalibre-gui-linux_$VERSION.deb" sudo dpkg-deb -Zxz -b "$DEB_DIR" "$DEBS_OUT/emalibre-gui-linux_$VERSION.deb" -sudo mv "$TMP_DIR/init.d" "$DEB_DIR/etc" 2>/dev/null || true -sudo mv "$TMP_DIR/emalibre_modo" "$DEB_DIR/etc/emalibre_modo" 2>/dev/null || true +restore_service +trap - EXIT log_final " Finalizó con éxito, build/debs/emalibre-gui-linux_$VERSION.deb." log_ok " --> OK." @@ -261,7 +296,7 @@ sudo sed -i 's|/emalibre/package.nw/gui_common/|/emalibre/gui_common/|' "$DEB_DI log_ok " --> OK." log_info "4) Aplicando permisos necesarios." -sudo chmod 0755 "$DEB_DIR/DEBIAN" "$DEB_DIR/DEBIAN/postrm" "$DEB_DIR/DEBIAN/postinst" "$DEB_DIR/DEBIAN/preinst" +sudo chmod 0755 "$DEB_DIR/DEBIAN" "$DEB_DIR/DEBIAN/postrm" "$DEB_DIR/DEBIAN/postinst" "$DEB_DIR/DEBIAN/preinst" "$DEB_DIR/DEBIAN/prerm" sudo chown -R root:root "$DEB_DIR" log_ok " --> OK." diff --git a/src/package.json b/src/package.json index 3984638..7312003 100644 --- a/src/package.json +++ b/src/package.json @@ -58,9 +58,11 @@ "datatables.net": "1.13.11", "datatables.net-bs5": "1.13.11", "datatables.net-buttons": "2.4.3", - "datatables.net-buttons-bs5": "2.4.3" + "datatables.net-buttons-bs5": "2.4.3", + "node": "22.23.3" }, "allowScripts": { - "node-hid@3.4.0": true + "node-hid@3.4.0": true, + "node@22.23.3": true } } diff --git a/test/init-service.test.py b/test/init-service.test.py new file mode 100644 index 0000000..0ee2420 --- /dev/null +++ b/test/init-service.test.py @@ -0,0 +1,206 @@ +"""Exercise an isolated copy of the init script; never launch real daemons.""" +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +REPO = Path(__file__).resolve().parents[1] +NODE = shutil.which('node', path='/usr/bin:/bin') + +@unittest.skipUnless(NODE, 'Node is required for the fake Forever module') +class InitServiceTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='emalibre-test-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.app = self.root / 'app' + self.module = self.app / 'node_modules/forever' + (self.module / 'bin').mkdir(parents=True) + (self.app / 'gui_web').mkdir() + (self.app / 'gui_web/express.js').touch() + (self.app / 'conf').mkdir() + self.config = self.app / 'conf/ConfiguracionGeneral.js' + self.config.write_text('module.exports = {ConfGeneral: {inicio_automatico: false}};') + self.state = self.root / 'state.json' + self.state.write_text('[]') + self.calls = self.root / 'calls.jsonl' + self.pid = self.root / 'emalibre.pid' + self.mode = self.root / 'mode' + self.mode.write_text('web\n') + self.module.joinpath('index.js').write_text(''' +const fs = require('fs'); +exports.stop = uid => { + const emitter = new (require('events').EventEmitter)(); + process.nextTick(() => { + if (process.env.COMMAND_FAIL === 'stop') return emitter.emit('error', Error('stop failed')); + const state = JSON.parse(fs.readFileSync(process.env.TEST_STATE)); + fs.writeFileSync(process.env.TEST_STATE, JSON.stringify(state.filter(p => p.uid !== uid))); + emitter.emit('stop'); + }); + return emitter; +}; +exports.list = (_, cb) => cb(process.env.LIST_FAIL ? Error('query failed') : null, + JSON.parse(fs.readFileSync(process.env.TEST_STATE))); +''') + self.module.joinpath('bin/forever').write_text(''' +const fs = require('fs'); +const args = process.argv.slice(2); +fs.appendFileSync(process.env.TEST_CALLS, JSON.stringify(args) + '\\n'); +if (process.env.COMMAND_FAIL === args[0]) process.exit(7); +let state = JSON.parse(fs.readFileSync(process.env.TEST_STATE)); +if (args[0] === 'start') { + // Match Forever's tryStart: path.join(sourceDir, script), not path.resolve. + const script = require('path').join(args[args.indexOf('--sourceDir') + 1], args[args.length - 2]); + if (!fs.existsSync(script)) { + console.error('script ' + script + ' does not exist'); + process.exit(255); + } + state.push({uid: 'EMA_Web'}); + fs.writeFileSync(args[args.indexOf('--pidFile') + 1], '123456'); +} +if (args[0] === 'stop') state = state.filter(p => p.uid !== args[1]); +fs.writeFileSync(process.env.TEST_STATE, JSON.stringify(state)); +''') + wrapper = self.app / 'node_modules/node/bin/node' + wrapper.parent.mkdir(parents=True) + # Allow tests on older host Node; production still enforces Node >= 20. + wrapper.write_text('#!/bin/sh\nif [ "$1" = -e ]; then exit "${VERSION_FAIL:-0}"; fi\nexec ' + NODE + ' "$@"\n') + wrapper.chmod(0o755) + script = (REPO / 'build/Deb64/etc/init.d/emalibre').read_text() + for old, new in { + '/usr/share/emalibre': str(self.app), + '/run/emalibre.pid': str(self.pid), + '/var/log/emalibre': str(self.root / 'logs'), + '/etc/emalibre_modo': str(self.mode), + '/root/.forever': str(self.root / 'forever'), + 'HOME=/root': 'HOME=' + str(self.root), + '/run/lock/emalibre.lock': str(self.root / 'lock'), + '/run/systemd/system': str(self.root / 'systemd'), + '$(id -u)': '${TEST_UID:-0}', + }.items(): + script = script.replace(old, new) + # Both service managers are mocks; no system configuration is touched. + script = script.replace('set_boot() {', ''' +systemctl() { echo "$*" >> "${TEST_BOOT}"; return "${BOOT_FAIL:-0}"; } +update-rc.d() { echo "$*" >> "${TEST_BOOT}"; return "${BOOT_FAIL:-0}"; } +set_boot() {''') + self.script = self.root / 'service' + self.script.write_text(script) + self.env = dict(os.environ, TEST_STATE=str(self.state), TEST_CALLS=str(self.calls), + TEST_BOOT=str(self.root / 'boot')) + + def run_service(self, action, expected=0, **env): + result = subprocess.run(['bash', str(self.script), action], env=dict(self.env, **env), + capture_output=True, text=True, timeout=20) + self.assertEqual(result.returncode, expected, result.stdout + result.stderr) + return result + + def commands(self): + return [json.loads(line) for line in self.calls.read_text().splitlines()] if self.calls.exists() else [] + + def test_start_and_idempotency(self): + self.run_service('start') + self.run_service('start') + self.assertEqual(len(self.commands()), 1) + self.assertEqual(self.commands()[0][-1], 'iniciar') + args = self.commands()[0] + self.assertEqual(args[args.index('-c') + 1], str(self.app / 'node_modules/node/bin/node')) + + def test_stale_pid_does_not_block_start(self): + self.pid.write_text('123456') + self.run_service('start') + self.assertEqual(self.commands()[0][0], 'start') + + def test_start_error_is_preserved(self): + self.run_service('start', 7, COMMAND_FAIL='start') + + def test_stop_only_ema_and_without_pid(self): + self.state.write_text('[{"uid":"EMA_Web"},{"uid":"other"}]') + self.run_service('stop') + self.assertEqual(self.commands(), [['stop', 'EMA_Web']]) + self.assertEqual(json.loads(self.state.read_text()), [{'uid': 'other'}]) + + def test_stop_idempotency_and_stale_pid(self): + self.pid.write_text('123456') + self.run_service('stop') + self.run_service('stop') + self.assertFalse(self.pid.exists()) + self.assertEqual(self.commands(), []) + + def test_stop_error_is_preserved(self): + self.state.write_text('[{"uid":"EMA_Web"}]') + self.run_service('stop', 7, COMMAND_FAIL='stop') + + def test_restart_and_stop_failure(self): + self.state.write_text('[{"uid":"EMA_Web"}]') + self.run_service('restart', 7, COMMAND_FAIL='stop') + self.assertEqual(len(self.commands()), 1) + self.run_service('restart') + self.assertEqual([a[0] for a in self.commands()], ['stop', 'stop', 'start']) + + def test_status(self): + self.run_service('status', 3) + self.state.write_text('[{"uid":"EMA_Web"}]') + self.run_service('status') + self.run_service('status', 4, LIST_FAIL='1') + + def test_query_failure_prevents_start(self): + self.run_service('start', 4, LIST_FAIL='1') + self.assertEqual(self.commands(), []) + + def test_mode_validation_and_default(self): + for value in ['', 'consola', 'web invalid']: + self.mode.write_text(value) + self.run_service('start', 2) + self.mode.unlink() + self.run_service('start') + self.assertEqual(self.mode.read_text(), 'web\n') + + def test_missing_bundled_node_does_not_use_system_node(self): + (self.app / 'node_modules/node/bin/node').unlink() + self.run_service('start', 5) + self.assertEqual(self.commands(), []) + + def test_missing_runtime_and_app(self): + self.run_service('start', 5, VERSION_FAIL='1') + (self.app / 'gui_web/express.js').unlink() + self.run_service('start', 5) + + def test_usage_and_permissions(self): + self.run_service('invalid', 2) + self.run_service('start', 4, TEST_UID='1000') + + def test_boot_independent_from_collection(self): + before = self.config.read_text() + self.run_service('set-inicio-enabled') + self.assertEqual((self.root / 'boot').read_text(), 'emalibre defaults\nemalibre enable\n') + (self.root / 'systemd').mkdir() + self.run_service('set-inicio-disable') + self.run_service('set-inicio-enabled', 9, BOOT_FAIL='9') + self.assertEqual(self.config.read_text(), before) + + def test_migration_stops_only_ema(self): + source = (REPO / 'build/Deb64/DEBIAN/preinst').read_text() + js = source.split("<<'JS'\n", 1)[1].split('\nJS', 1)[0] + js = js.replace('/usr/share/emalibre', str(self.app)) + for initial, expected, code, extra in [ + ([{'uid': 'EMA_Web'}, {'uid': 'other'}], [{'uid': 'other'}], 0, {}), + ([{'uid': 'other'}], [{'uid': 'other'}], 0, {}), + ([{'uid': 'EMA_Web'}], [{'uid': 'EMA_Web'}], 1, {'COMMAND_FAIL': 'stop'}), + ]: + self.state.write_text(json.dumps(initial)) + result = subprocess.run([NODE, '-'], input=js, text=True, capture_output=True, + env=dict(self.env, **extra), timeout=20) + self.assertEqual(result.returncode, code, result.stderr) + self.assertEqual(json.loads(self.state.read_text()), expected) + + def test_view_does_not_create_mode(self): + self.mode.unlink() + self.run_service('view-conf', 1) + self.assertFalse(self.mode.exists()) + +if __name__ == '__main__': + unittest.main()