diff --git a/CLAUDE.md b/CLAUDE.md index bcd5dda..717e260 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,11 +67,12 @@ Pasos de la UI (decididos): - **Despliegue en Windows**: un solo servicio. La API sirve también la web compilada (mismo origen: sin CORS y con la cookie `SameSite=strict`), con **HTTPS directo en Node** (`HTTPS_PFX_PATH` o `HTTPS_KEY_PATH`+`HTTPS_CERT_PATH`) y redirección opcional de HTTP (`HTTP_REDIRECT_PORT`). El servicio - lo administra **WinSW** (elegido sobre NSSM por su configuración en XML versionable); nginx no es - necesario. `npm run publicar` arma `release/devolucion-proveedores/` (API + web + solo dependencias de + lo administra **NSSM** (decisión del usuario; se evaluó WinSW y no se lo consideró lo bastante estable); + nginx no es necesario. NSSM 2.24 estable puede no arrancar en Windows recientes: usar la 2.24-101. `npm run publicar` arma `release/devolucion-proveedores/` (API + web + solo dependencias de producción, ~55 MB). Guía completa: `deploy/windows/DEPLOY.md`. - **Logs**: la aplicación **no escribe logs a archivo**; usa el `Logger` de Nest por consola. En - producción WinSW captura la salida y la rota (10 MB × 8). Con `NO_COLOR=1` el log sale sin códigos de + producción NSSM captura la salida y la rota a los 10 MB (no limita cuántos archivos conserva: hay que + limpiar los viejos). Con `NO_COLOR=1` el log sale sin códigos de color. Solo se registran errores 4xx/5xx, fallos o lentitud del Service Layer y eventos de negocio. - Aplicación web **independiente**. Login con el usuario de SAP B1 vía **Login del Service Layer**. - Metadatos (UDT/UDF/UDO) definidos en **JSON** con formato del Service Layer. La web tiene la @@ -109,7 +110,7 @@ devolucion-proveedores-web/ npm workspaces │ ├── web-estatico.ts sirve el build de Angular, fallback a index.html, cabeceras de seguridad │ └── https.ts lectura del certificado y redirección HTTP → HTTPS ├── scripts/publicar.mjs arma la carpeta de despliegue (release/, ignorada por git) -├── deploy/windows/ WinSW (plantilla XML), instalar/desinstalar-servicio.ps1, DEPLOY.md +├── deploy/windows/ NSSM: instalar/desinstalar-servicio.ps1, DEPLOY.md └── apps/web/ Angular 22 standalone, signals, zoneless, CSS (no SCSS) └── src/app/ ├── core/ auth.service, metadata.service, planificaciones.service, @@ -262,8 +263,8 @@ Reglas: Para repetirlo hace falta una Devolución con folio (la que emite el addon de facturación electrónica). - **Despliegue en Windows**: la carpeta publicada se probó en un Mac (API con HTTPS autofirmado, web servida por la API, rutas de Angular, 404, cabeceras, redirección 301, log sin colores). **No se ha - ejecutado en Windows**: los `.ps1` solo pasaron la comprobación de sintaxis y la generación del XML de - WinSW; falta instalar el servicio de verdad. Las banderas de la cookie de sesión están comprobadas en un + ejecutado en Windows**: los `.ps1` solo pasaron la comprobación de sintaxis y la lista de comandos de + NSSM que generan (`-SoloMostrar`); falta instalar el servicio de verdad. Las banderas de la cookie de sesión están comprobadas en un test e2e; solo falta verlo en un navegador real sobre HTTPS (iniciar sesión en el sitio desplegado). - **Estilos:** son CSS porque, en el Mac con macOS 12, el compilador nativo de Sass exige macOS 14 o superior. En Sequoia se puede volver a SCSS si se prefiere. diff --git a/apps/api/README.md b/apps/api/README.md index b0ed194..48b9d2b 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -83,9 +83,9 @@ en el mismo origen y en un solo puerto. Las rutas de Angular (sin extensión y f npm run publicar # desde la raíz: arma release/devolucion-proveedores/ ``` -Instalación en Windows (servicio con WinSW, HTTPS, firewall, actualizaciones): [`../../deploy/windows/DEPLOY.md`](../../deploy/windows/DEPLOY.md). +Instalación en Windows (servicio con NSSM, HTTPS, firewall, actualizaciones): [`../../deploy/windows/DEPLOY.md`](../../deploy/windows/DEPLOY.md). -**Logs:** la API no escribe archivos; usa el `Logger` de Nest por consola. Como servicio, WinSW captura +**Logs:** la API no escribe archivos; usa el `Logger` de Nest por consola. Como servicio, NSSM captura esa salida y la rota. Con `NO_COLOR=1` el log sale sin códigos de color. ## Estructura diff --git a/deploy/windows/DEPLOY.md b/deploy/windows/DEPLOY.md index 0cedd9c..579ea71 100644 --- a/deploy/windows/DEPLOY.md +++ b/deploy/windows/DEPLOY.md @@ -1,7 +1,7 @@ # Despliegue en Windows La aplicación se ejecuta como **un solo servicio de Windows**: la API (NestJS) sirve también la web -compilada (Angular), por HTTPS y en un único puerto. El servicio lo administra **WinSW**. +compilada (Angular), por HTTPS y en un único puerto. El servicio lo administra **NSSM**. ``` Navegador ──HTTPS :443──▶ servicio "DevolucionProveedores" (node dist\main.js) @@ -15,21 +15,26 @@ memoria de la API, **reiniciar el servicio cierra la sesión de todos los usuari > **Estado de esta guía.** La carpeta publicada, la API con HTTPS, la web servida por la API, la > redirección HTTP→HTTPS y los logs sin colores se probaron en un Mac. Los scripts de PowerShell y -> WinSW **no se han ejecutado todavía en Windows** (se comprobó su sintaxis y la generación del XML). -> Tampoco se ha probado contra HANA. Haga la primera instalación en un servidor de pruebas. +> NSSM **no se han ejecutado todavía en Windows** (se comprobó su sintaxis y los comandos de NSSM que +> generan, con `-SoloMostrar`). Tampoco se ha probado contra HANA. Haga la primera instalación en un servidor de pruebas. ## 1. Requisitos del servidor | Qué | Detalle | |---|---| | Windows | Windows Server 2016 o superior. | -| .NET Framework | 4.6.1 o superior (lo necesita WinSW; viene con Windows Server 2016+). | | Node.js | **22 LTS** o superior, instalado para todos los usuarios (`node -v` en una consola nueva). | | Red | Acceso al Service Layer de SAP (puerto 50000) y al web service de BES (HTTPS de salida). | -| WinSW | `WinSW-x64.exe` de (rama 2.x). Verifique el hash que publica el proyecto. | +| NSSM | `nssm.exe` de 64 bits (carpeta `win64` del `.zip` de ). Ver la nota de abajo sobre la versión. | | Certificado | Un `.pfx` (o `.key` + `.crt`) para el nombre con que los usuarios abrirán la página. | | Permisos | Una consola de PowerShell **como administrador**. | +> **Versión de NSSM.** Descargue la compilación **2.24-101** (*pre-release*, `nssm-2.24-101-g897c7ad.zip`, +> carpeta `win64`), no la estable. La estable (2.24, de 2014) puede fallar al iniciar servicios en Windows 10 +> y 11 y en Windows Server 2019 o 2022 (mensajes como `Unexpected status SERVICE_PAUSED`); solo debería +> servir en Windows Server 2016. Compruebe con `nssm version` (debe decir `2.24-101-g897c7ad`). La página +> de descargas no publica sumas de verificación: revise el `.zip` según la política de su empresa. + ## 2. Publicar (en su equipo de desarrollo o en el servidor) Desde la raíz del proyecto, con Node 22: @@ -55,9 +60,9 @@ C:\apps\devolucion-proveedores\ │ └── apps\api\.env ← se crea en el paso 4 ├── certificados\ ← el certificado HTTPS (paso 5) ├── logs\ ← los logs del servicio +├── servicio\ ← lo crea el instalador (copia de nssm.exe) ├── instalar-servicio.ps1 ├── desinstalar-servicio.ps1 -├── devolucion-api.xml.plantilla └── DEPLOY.md ``` @@ -86,7 +91,7 @@ HTTP_REDIRECT_PORT=80 - **El puerto es el que ponga en `PORT`**: 443 es solo el ejemplo (con él la URL no lleva `:puerto`). Puede usar cualquiera que esté libre, por ejemplo `PORT=8443`; entonces la URL es `https://servidor:8443`. - WinSW no influye: solo arranca el servicio. + NSSM no influye: solo arranca el servicio. - `HTTP_REDIRECT_PORT` es opcional. Solo es útil con el 80 (la gente escribe `http://servidor` sin puerto); con otro puerto solo redirige a quien escriba `http://servidor:`. Déjelo vacío si no lo necesita. @@ -128,7 +133,7 @@ New-NetFirewallRule -DisplayName 'Devolucion Proveedores HTTP (redireccion)' -Di ```powershell cd C:\apps\devolucion-proveedores -.\instalar-servicio.ps1 -WinSW C:\descargas\WinSW-x64.exe +.\instalar-servicio.ps1 -Nssm C:\descargas\nssm-2.24-101-g897c7ad\win64\nssm.exe ``` Opciones: @@ -138,9 +143,11 @@ Opciones: | `-Cuenta 'EMPRESA\svc-devolucion'` | Ejecutar con una cuenta propia en vez de `LocalSystem`. Pide la contraseña y no la guarda en ningún archivo. Esa cuenta necesita el derecho *Iniciar sesión como servicio* y lectura sobre la carpeta. | | `-UsarCertificadosDelSistema` | Agrega `NODE_OPTIONS=--use-system-ca`. Úselo si Fortinet (u otro firewall) inspecciona el HTTPS de salida con un certificado propio y la descarga de PDF falla por certificado. | | `-NombreServicio` | Cambia el nombre del servicio (por defecto `DevolucionProveedores`). | +| `-SoloMostrar` | Imprime los comandos de NSSM que ejecutaría y termina, sin instalar nada ni exigir administrador. Úselo para revisarlos antes. | -El script verifica Node 22, los archivos y el `.env`; genera `servicio\devolucion-api.xml` y -`servicio\devolucion-api.exe`; instala el servicio (arranque automático y reinicio si falla) y lo inicia. +El script verifica Node 22, los archivos y el `.env`; copia `nssm.exe` a `servicio\`; instala el servicio +(arranque automático, reinicio a los 10 s si Node se cae, logs a `logs\`) y lo inicia. Si algún ajuste +falla, quita el servicio a medio configurar. ## 8. Verificar @@ -156,15 +163,40 @@ En una base nueva pulse **Reconstruir** para crear las tablas, campos y el objet ## 9. Logs -WinSW guarda lo que la API escribe por consola en `logs\` (`*.out.log` y `*.err.log`), rotado por -tamaño: 10 MB y 8 archivos. La aplicación no escribe logs por su cuenta. +NSSM guarda lo que la API escribe por consola en `logs\devolucion-api.out.log` y `devolucion-api.err.log`, +y los rota al llegar a 10 MB (los rotados llevan la fecha y la hora en el nombre). La aplicación no escribe +logs por su cuenta. + +**NSSM no limita cuántos archivos rotados conserva**: se acumulan. Programe una limpieza periódica, por +ejemplo una tarea que borre los de más de 30 días: + +```powershell +Get-ChildItem C:\apps\devolucion-proveedores\logs -Filter 'devolucion-api.*-*.log' | + Where-Object LastWriteTime -lt (Get-Date).AddDays(-30) | Remove-Item +``` - Los errores 4xx/5xx de la API, los fallos y las peticiones lentas al Service Layer, y los eventos de negocio (planificación creada, PDF descargado) quedan registrados. Las peticiones correctas no. - Para seguirlo en vivo: `Get-Content C:\apps\devolucion-proveedores\logs\*.out.log -Wait -Tail 50`. - La contraseña del web service DTE (`@DTE_CONFC`) y las de los usuarios **nunca** se registran. -## 10. Actualizar a una versión nueva +## 10. Administrar el servicio + +Con `servicio\nssm.exe` (la copia que dejó el instalador): + +```powershell +cd C:\apps\devolucion-proveedores\servicio +.\nssm.exe status DevolucionProveedores +.\nssm.exe restart DevolucionProveedores +.\nssm.exe edit DevolucionProveedores # ventana gráfica con todos los ajustes +.\nssm.exe get DevolucionProveedores AppEnvironmentExtra +``` + +Para cambiar una variable de entorno del servicio con `nssm set ... AppEnvironmentExtra`, indique **todas** las +variables juntas (reemplaza la lista completa): `NODE_ENV=production NO_COLOR=1 [NODE_OPTIONS=--use-system-ca]`. +Los valores de la aplicación (puerto, SAP, HTTPS) no van aquí: se cambian en el `.env` y se reinicia el servicio. + +## 11. Actualizar a una versión nueva 1. Publique la versión nueva (paso 2) y copie la carpeta al servidor, por ejemplo a `C:\nueva\`. 2. Elija una hora en que haya pocos usuarios: **se perderán las sesiones**. @@ -182,7 +214,7 @@ Start-Service DevolucionProveedores `/MIR` borra de `app\` lo que ya no exista en la versión nueva; `/XF .env` conserva su configuración. Los certificados, los logs y el servicio no cambian. -## 11. Desinstalar +## 12. Desinstalar ```powershell cd C:\apps\devolucion-proveedores @@ -191,7 +223,7 @@ cd C:\apps\devolucion-proveedores Detiene y elimina el servicio. No borra archivos ni logs. -## 12. Seguridad +## 13. Seguridad - **No publique la aplicación en Internet.** El login es el de SAP B1; manténgala en la red interna o detrás de una VPN. @@ -199,10 +231,11 @@ Detiene y elimina el servicio. No borra archivos ni logs. - Dele al servicio la cuenta con menos permisos posible: solo lectura sobre su carpeta y escritura en `logs\`. - Restrinja la lectura del `.env` si contiene la clave del `.pfx` (paso 4). -## 13. Problemas frecuentes +## 14. Problemas frecuentes | Síntoma | Causa probable | |---|---| +| El servicio no inicia (`SERVICE_PAUSED`, `Unexpected status`) | Problema conocido de NSSM 2.24 estable en Windows recientes: use la compilación 2.24-101 (ver el paso 1) y reinstale el servicio. | | El servicio se inicia y se detiene | Mire `logs\*.err.log`. Suele ser una ruta mal escrita en el `.env` (`No se pudo leer HTTPS_PFX_PATH (…)`) o el puerto ocupado (`EADDRINUSE`). | | `WEB_DIST_DIR no contiene un index.html` | Se copió la carpeta `app` incompleta: falta `app\apps\web\dist\web\browser`. | | Puerto 443 o 80 ocupado | IIS u otro programa lo usa. Cambie `PORT` / `HTTP_REDIRECT_PORT` en el `.env` y las reglas del firewall. | diff --git a/deploy/windows/desinstalar-servicio.ps1 b/deploy/windows/desinstalar-servicio.ps1 index 8106fb5..dc69577 100644 --- a/deploy/windows/desinstalar-servicio.ps1 +++ b/deploy/windows/desinstalar-servicio.ps1 @@ -2,24 +2,28 @@ .SYNOPSIS Detiene y elimina el servicio de Windows. No borra los archivos ni los logs. #> -#Requires -RunAsAdministrator [CmdletBinding()] param([string]$NombreServicio = 'DevolucionProveedores') $ErrorActionPreference = 'Stop' -$exe = Join-Path $PSScriptRoot 'servicio\devolucion-api.exe' + +$esAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $esAdmin) { Write-Host 'ERROR: Ejecute este script en una consola de PowerShell como administrador.' -ForegroundColor Red; exit 1 } if (-not (Get-Service -Name $NombreServicio -ErrorAction SilentlyContinue)) { Write-Host "El servicio '$NombreServicio' no existe." exit 0 } -if (-not (Test-Path $exe)) { - Write-Host "No se encontró $exe; se intentará con sc.exe." - & sc.exe stop $NombreServicio | Out-Null - & sc.exe delete $NombreServicio - exit $LASTEXITCODE -} -& $exe stop -& $exe uninstall +$nssm = Join-Path $PSScriptRoot 'servicio\nssm.exe' +if (Test-Path $nssm) { + & $nssm stop $NombreServicio | Out-Null # si ya estaba detenido, no es un error + & $nssm remove $NombreServicio confirm | Out-Null +} else { + Write-Host "No se encontró $nssm; se usa sc.exe." + & sc.exe stop $NombreServicio | Out-Null + & sc.exe delete $NombreServicio | Out-Null +} +if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: no se pudo eliminar el servicio '$NombreServicio'." -ForegroundColor Red; exit 1 } + Write-Host "Servicio '$NombreServicio' eliminado. Los archivos y los logs siguen en $PSScriptRoot." diff --git a/deploy/windows/devolucion-api.xml.plantilla b/deploy/windows/devolucion-api.xml.plantilla deleted file mode 100644 index 6b6e091..0000000 --- a/deploy/windows/devolucion-api.xml.plantilla +++ /dev/null @@ -1,32 +0,0 @@ - - - __ID__ - Devolución a Proveedores - API y web de Devolución a Proveedores (SAP Business One) - - __NODE__ - dist\main.js - - __RAIZ__\app\apps\api - - - - -__NODE_OPTIONS__ - Automatic - - - - 1 hour - 15 sec - - - __RAIZ__\logs - - 10240 - 8 - - diff --git a/deploy/windows/instalar-servicio.ps1 b/deploy/windows/instalar-servicio.ps1 index ce33174..1a5465b 100644 --- a/deploy/windows/instalar-servicio.ps1 +++ b/deploy/windows/instalar-servicio.ps1 @@ -1,76 +1,118 @@ <# .SYNOPSIS - Instala la API (que también sirve la web) como servicio de Windows usando WinSW. + Instala la API (que también sirve la web) como servicio de Windows usando NSSM. .EXAMPLE - .\instalar-servicio.ps1 -WinSW C:\descargas\WinSW-x64.exe + .\instalar-servicio.ps1 -Nssm C:\descargas\nssm\win64\nssm.exe .EXAMPLE - .\instalar-servicio.ps1 -WinSW C:\descargas\WinSW-x64.exe -Cuenta 'EMPRESA\svc-devolucion' -UsarCertificadosDelSistema + .\instalar-servicio.ps1 -Nssm C:\descargas\nssm\win64\nssm.exe -Cuenta 'EMPRESA\svc-devolucion' -UsarCertificadosDelSistema + +.EXAMPLE + # Muestra los comandos que ejecutaría, sin instalar nada ni exigir administrador + .\instalar-servicio.ps1 -Nssm C:\descargas\nssm\win64\nssm.exe -SoloMostrar #> -#Requires -RunAsAdministrator [CmdletBinding()] param( - # Ruta del ejecutable de WinSW (WinSW-x64.exe, de https://github.com/winsw/winsw/releases) - [Parameter(Mandatory)][string]$WinSW, + # Ruta de nssm.exe (la versión de 64 bits, carpeta win64 del .zip de https://nssm.cc/download) + [Parameter(Mandatory)][string]$Nssm, # Cuenta con la que corre el servicio (DOMINIO\usuario). Si se omite: LocalSystem. [string]$Cuenta, [string]$NombreServicio = 'DevolucionProveedores', # Agrega NODE_OPTIONS=--use-system-ca: necesario si Fortinet u otro firewall inspecciona el tráfico # HTTPS de salida (por ejemplo hacia el web service de BES) con un certificado propio. - [switch]$UsarCertificadosDelSistema + [switch]$UsarCertificadosDelSistema, + # Solo imprime los comandos de NSSM que se ejecutarían. + [switch]$SoloMostrar ) $ErrorActionPreference = 'Stop' $raiz = $PSScriptRoot $apiDir = Join-Path $raiz 'app\apps\api' +$logsDir = Join-Path $raiz 'logs' function Fallar($mensaje) { Write-Host "ERROR: $mensaje" -ForegroundColor Red; exit 1 } +if (-not $SoloMostrar) { + $esAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + if (-not $esAdmin) { Fallar 'Ejecute este script en una consola de PowerShell como administrador.' } +} + # --- Verificaciones previas ------------------------------------------------------------------- -if (-not (Test-Path $WinSW)) { Fallar "No se encontró WinSW en '$WinSW'." } +if (-not (Test-Path $Nssm)) { Fallar "No se encontró nssm.exe en '$Nssm'." } $node = Get-Command node -ErrorAction SilentlyContinue if (-not $node) { Fallar 'Node.js no está instalado (o no está en el PATH). Instale Node 22 LTS.' } $versionNode = [version](& $node.Source --version).TrimStart('v') if ($versionNode.Major -lt 22) { Fallar "Se necesita Node 22 o superior; hay $versionNode." } -if (-not (Test-Path (Join-Path $apiDir 'dist\main.js'))) { Fallar "Falta $apiDir\dist\main.js (¿se copió la carpeta app completa?)." } -if (-not (Test-Path (Join-Path $apiDir 'node_modules')) -and -not (Test-Path (Join-Path $raiz 'app\node_modules'))) { Fallar 'Faltan las dependencias (app\node_modules).' } +if (-not (Test-Path (Join-Path $apiDir 'dist\main.js'))) { Fallar "Falta $apiDir\dist\main.js (¿se copió la carpeta app completa?)." } +if (-not (Test-Path (Join-Path $raiz 'app\node_modules'))) { Fallar 'Faltan las dependencias (app\node_modules).' } if (-not (Test-Path (Join-Path $raiz 'app\apps\web\dist\web\browser\index.html'))) { Fallar 'Falta la web compilada (app\apps\web\dist\web\browser).' } if (-not (Test-Path (Join-Path $apiDir '.env'))) { Fallar "Falta $apiDir\.env. Copie .env.example a .env y complételo (ver DEPLOY.md)." } -if (Get-Service -Name $NombreServicio -ErrorAction SilentlyContinue) { +if (-not $SoloMostrar -and (Get-Service -Name $NombreServicio -ErrorAction SilentlyContinue)) { Fallar "El servicio '$NombreServicio' ya existe. Ejecute primero .\desinstalar-servicio.ps1." } -# --- Archivos del servicio ---------------------------------------------------------------------- -$servicioDir = Join-Path $raiz 'servicio' -New-Item -ItemType Directory -Force -Path $servicioDir, (Join-Path $raiz 'logs') | Out-Null +# --- Configuración del servicio (todo lo que se le pide a NSSM, en un solo lugar) ----------------- +$variablesDeEntorno = @('NODE_ENV=production', 'NO_COLOR=1') # NO_COLOR: sin códigos de color en los logs +if ($UsarCertificadosDelSistema) { $variablesDeEntorno += 'NODE_OPTIONS=--use-system-ca' } -$exe = Join-Path $servicioDir 'devolucion-api.exe' -$xml = Join-Path $servicioDir 'devolucion-api.xml' -Copy-Item -Path $WinSW -Destination $exe -Force +$ajustes = [System.Collections.Generic.List[string[]]]::new() +function Agregar([string[]]$ajuste) { $ajustes.Add($ajuste) } -$nodeOptions = if ($UsarCertificadosDelSistema) { ' ' } else { '' } -# .Replace() sustituye texto literal (las rutas pueden tener espacios, puntos o signos que -replace trataría como patrón) -$contenido = (Get-Content (Join-Path $raiz 'devolucion-api.xml.plantilla') -Raw -Encoding UTF8). - Replace('__ID__', $NombreServicio). - Replace('__NODE__', $node.Source). - Replace('__RAIZ__', $raiz). - Replace('__NODE_OPTIONS__', $nodeOptions) -if ($contenido -match '__[A-Z_]+__') { Fallar "La plantilla tiene un marcador sin reemplazar: $($Matches[0])" } -# UTF-8 sin BOM -[System.IO.File]::WriteAllText($xml, $contenido, (New-Object System.Text.UTF8Encoding($false))) +Agregar 'DisplayName', 'Devolucion a Proveedores' +Agregar 'Description', 'API y web de Devolucion a Proveedores (SAP Business One)' +Agregar 'AppDirectory', $apiDir # de ahí se lee el archivo .env +Agregar (@('AppEnvironmentExtra') + $variablesDeEntorno) +Agregar 'Start', 'SERVICE_AUTO_START' +Agregar 'AppStdout', (Join-Path $logsDir 'devolucion-api.out.log') # lo que la API escribe por consola +Agregar 'AppStderr', (Join-Path $logsDir 'devolucion-api.err.log') +Agregar 'AppRotateFiles', '1' # rota los logs al llegar a 10 MB, +Agregar 'AppRotateOnline', '1' # incluso con el servicio en marcha +Agregar 'AppRotateBytes', '10485760' +Agregar 'AppExit', 'Default', 'Restart' # si Node se cae, lo reinicia +Agregar 'AppRestartDelay', '10000' # esperando 10 s + +$instalar = @('install', $NombreServicio, $node.Source, 'dist\main.js') + +function Texto([string[]]$partes) { + ($partes | ForEach-Object { if ($_ -match '\s') { "`"$_`"" } else { $_ } }) -join ' ' +} + +if ($SoloMostrar) { + Write-Host "Comandos que se ejecutarían (nssm = $Nssm):`n" + Write-Host "nssm $(Texto $instalar)" + foreach ($a in $ajustes) { Write-Host "nssm $(Texto (@('set', $NombreServicio) + $a))" } + Write-Host "nssm start $NombreServicio" + exit 0 +} # --- Instalación -------------------------------------------------------------------------------- +New-Item -ItemType Directory -Force -Path (Join-Path $raiz 'servicio'), $logsDir | Out-Null + +# El servicio queda apuntando a este nssm.exe, así que se guarda una copia en una carpeta estable. +$nssmExe = Join-Path $raiz 'servicio\nssm.exe' +Copy-Item -Path $Nssm -Destination $nssmExe -Force + +function Ejecutar-Nssm([string[]]$argumentos) { + & $nssmExe @argumentos | Out-Null + if ($LASTEXITCODE -ne 0) { Fallar "nssm $(Texto $argumentos) falló (código $LASTEXITCODE)." } +} + Write-Host "Instalando el servicio '$NombreServicio'..." -& $exe install -if ($LASTEXITCODE -ne 0) { Fallar 'WinSW no pudo instalar el servicio.' } +Ejecutar-Nssm $instalar +try { + foreach ($a in $ajustes) { Ejecutar-Nssm (@('set', $NombreServicio) + $a) } +} catch { + & $nssmExe remove $NombreServicio confirm | Out-Null # no dejar un servicio a medio configurar + throw +} if ($Cuenta) { # La contraseña se pide aquí y se pasa a Windows; no queda escrita en ningún archivo. - $credencial = Get-Credential -UserName $Cuenta -Message "Contraseña de la cuenta del servicio" + $credencial = Get-Credential -UserName $Cuenta -Message 'Contraseña de la cuenta del servicio' $clave = $credencial.GetNetworkCredential().Password & sc.exe config $NombreServicio obj= $credencial.UserName password= $clave | Out-Null if ($LASTEXITCODE -ne 0) { Fallar "No se pudo asignar la cuenta '$Cuenta' al servicio." } @@ -78,9 +120,9 @@ if ($Cuenta) { } Write-Host 'Iniciando...' -& $exe start +Ejecutar-Nssm @('start', $NombreServicio) Start-Sleep -Seconds 3 -& $exe status +& $nssmExe status $NombreServicio -Write-Host "`nLogs: $(Join-Path $raiz 'logs')" -ForegroundColor Green +Write-Host "`nLogs: $logsDir" -ForegroundColor Green Write-Host 'Compruebe: Invoke-WebRequest /api/health' -ForegroundColor Green