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:
georgeandClaude Sonnet 5.5 committed 2026-10-13 08:55:29 -05:00
1 parent 287b187c14
commit 699dd983d0
6 files changed
+147 -99

No files matched your search

+49 -16
View File
@@ -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 <https://github.com/winsw/winsw/releases> (rama 2.x). Verifique el hash que publica el proyecto. |
| NSSM | `nssm.exe` de 64 bits (carpeta `win64` del `.zip` de <https://nssm.cc/download>). 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:<ese puerto>`. 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. |
+14 -10
View File
@@ -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."
@@ -1,32 +0,0 @@
<!--
Configuración de WinSW. instalar-servicio.ps1 reemplaza los marcadores (nombre entre dos guiones bajos a cada lado) y guarda el
resultado como servicio\devolucion-api.xml (debe llamarse igual que el .exe de WinSW).
-->
<service>
<id>__ID__</id>
<name>Devolución a Proveedores</name>
<description>API y web de Devolución a Proveedores (SAP Business One)</description>
<executable>__NODE__</executable>
<arguments>dist\main.js</arguments>
<!-- Debe ser apps\api: de ahí se lee el archivo .env -->
<workingdirectory>__RAIZ__\app\apps\api</workingdirectory>
<env name="NODE_ENV" value="production"/>
<!-- Sin colores: en un archivo de log los códigos de color salen como basura -->
<env name="NO_COLOR" value="1"/>
__NODE_OPTIONS__
<startmode>Automatic</startmode>
<onfailure action="restart" delay="10 sec"/>
<onfailure action="restart" delay="30 sec"/>
<onfailure action="none"/>
<resetfailure>1 hour</resetfailure>
<stoptimeout>15 sec</stoptimeout>
<!-- Lo que la API escribe por consola (logs de Nest) queda aquí, rotado por tamaño -->
<logpath>__RAIZ__\logs</logpath>
<log mode="roll-by-size">
<sizeThreshold>10240</sizeThreshold>
<keepFiles>8</keepFiles>
</log>
</service>
+75 -33
View File
@@ -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