Files
devolucion_proveedores/deploy/windows/instalar-servicio.ps1
T
georgeandClaude Sonnet 5.5 699dd983d0 Despliegue en Windows con NSSM en lugar de WinSW
El servicio de Windows pasa a administrarse con NSSM (decisión del usuario: WinSW no se
consideró lo bastante estable).

- instalar-servicio.ps1: recibe -Nssm (ruta de nssm.exe) y configura el servicio con
  `nssm install` / `nssm set` (arranque automático, reinicio a los 10 s, logs a logs\ rotados
  a los 10 MB). Nueva opción -SoloMostrar que imprime los comandos sin ejecutarlos ni exigir
  administrador. Si un ajuste falla, quita el servicio a medio configurar.
- desinstalar-servicio.ps1: usa `nssm stop` y `nssm remove`.
- Se elimina la plantilla XML de WinSW.
- DEPLOY.md: pasos con NSSM, sección para administrar el servicio, aviso de usar la
  compilación 2.24-101 en Windows 10/11 y Server 2019/2022 (la 2.24 estable puede no arrancar),
  y limpieza de logs rotados (NSSM no limita cuántos conserva). Ya no se requiere .NET Framework.
- CLAUDE.md y README de la API actualizados.

Sin probar aún en Windows: solo se comprobó la sintaxis y los comandos generados (-SoloMostrar).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 08:55:29 -05:00

129 lines
6.2 KiB
PowerShell

<#
.SYNOPSIS
Instala la API (que también sirve la web) como servicio de Windows usando NSSM.
.EXAMPLE
.\instalar-servicio.ps1 -Nssm C:\descargas\nssm\win64\nssm.exe
.EXAMPLE
.\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
#>
[CmdletBinding()]
param(
# 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,
# 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 $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 $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 (-not $SoloMostrar -and (Get-Service -Name $NombreServicio -ErrorAction SilentlyContinue)) {
Fallar "El servicio '$NombreServicio' ya existe. Ejecute primero .\desinstalar-servicio.ps1."
}
# --- 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' }
$ajustes = [System.Collections.Generic.List[string[]]]::new()
function Agregar([string[]]$ajuste) { $ajustes.Add($ajuste) }
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'..."
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'
$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." }
Write-Host "Cuenta del servicio: $($credencial.UserName). Recuerde darle 'Iniciar sesión como servicio' y permiso de lectura sobre $raiz."
}
Write-Host 'Iniciando...'
Ejecutar-Nssm @('start', $NombreServicio)
Start-Sleep -Seconds 3
& $nssmExe status $NombreServicio
Write-Host "`nLogs: $logsDir" -ForegroundColor Green
Write-Host 'Compruebe: Invoke-WebRequest <su-url>/api/health' -ForegroundColor Green