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>
This commit is contained in:
1 parent
287b187c14
commit
699dd983d0
6 files changed
+147
-99
No files matched your search
@@ -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) { ' <env name="NODE_OPTIONS" value="--use-system-ca"/>' } 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 <su-url>/api/health' -ForegroundColor Green
|
||||
Reference in new issue
Block a user