Private/NodeServiceProbe.ps1

# NodeServiceProbe.ps1
# Estado del servicio para `Publish-NodeApi -Plan` (issue #78).
#
# El plan informaba "Servicio: se creará (pm2)" sobre un servidor donde la API llevaba 4 h 34 min
# corriendo. El reporte se ejecuta por SSH no interactivo; pm2 vive bajo el gestor de versiones
# de Node, que solo se carga en shells interactivos; la invocación fallaba y el `else` concluía
# que no había servicio.
#
# El despliegue nunca tuvo el defecto: Manage-NodeProcess.sh carga nvm antes de llamar a pm2.
# Eran dos formas distintas de resolver el mismo binario, y por eso plan y apply discrepaban.
#
# Se corrigen las dos cosas:
# - el sondeo resuelve pm2 como lo hace el despliegue;
# - y si aun así no puede ejecutarlo, dice 'unknown' en vez de inventar 'not-configured'.
#
# La segunda es la que no se puede negociar: un plan existe para que se le crea, y afirmar un
# estado que no se pudo comprobar gasta esa credibilidad sin dar nada a cambio. Es la misma
# distinción que el módulo ya aplica en la verificación web, donde la ausencia de version.json
# es 'warn' y no 'error'.

function New-NodeServiceProbeScript {
    <#
    .SYNOPSIS
    Genera el bash que consulta el estado del servicio en el servidor.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)][ValidateSet('pm2', 'systemd')][string]$ProcessManager,
        [Parameter(Mandatory = $true)][string]$AppName
    )

    if ($ProcessManager -eq 'systemd') {
        # Si systemctl no está, este servidor no gestiona nada con systemd: afirmar que no hay
        # servicio sería tan falso como en el otro caso.
        return @"
if ! command -v systemctl >/dev/null 2>&1; then
    echo "SERVICE:unknown"
elif systemctl is-active --quiet $AppName 2>/dev/null; then
    echo "SERVICE:running"
elif systemctl is-enabled --quiet $AppName 2>/dev/null; then
    echo "SERVICE:stopped"
else
    echo "SERVICE:not-configured"
fi
"@

    }

    # Las dos primeras líneas son las mismas que Manage-NodeProcess.sh: si el despliegue cambia
    # su forma de resolver el binario, el sondeo tiene que cambiar con él o vuelve la
    # discrepancia. Hay una prueba que compara ambos archivos.
    return @"
export NVM_DIR="`$HOME/.nvm"
[ -s "`$NVM_DIR/nvm.sh" ] && . "`$NVM_DIR/nvm.sh" 2>/dev/null || true
 
if ! command -v pm2 >/dev/null 2>&1; then
    # No se pudo ejecutar pm2. No dice nada sobre si el servicio existe.
    echo "SERVICE:unknown"
elif pm2 describe $AppName >/dev/null 2>&1; then
    STATUS=`$(pm2 describe $AppName 2>/dev/null | grep -i status | head -1 | awk '{print `$4}')
    [ -z "`$STATUS" ] && STATUS="unknown"
    echo "SERVICE:`$STATUS"
else
    echo "SERVICE:not-configured"
fi
"@

}

function ConvertTo-NodeServiceState {
    <#
    .SYNOPSIS
    Traduce el estado sondeado a lo que el plan anuncia.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Status,
        [Parameter(Mandatory = $true)][string]$ProcessManager
    )

    $s = "$Status".Trim().ToLowerInvariant()

    # 'online' es la palabra de pm2 para 'running'. Pasarla tal cual mandaba un servicio
    # perfectamente arriba a la rama de estado desconocido, en amarillo.
    switch -Regex ($s) {
        '^(running|online)$' {
            return [pscustomobject]@{ Level = 'ok';   Text = "$ProcessManager activo (se reiniciará)" }
        }
        '^stopped$' {
            return [pscustomobject]@{ Level = 'info'; Text = "$ProcessManager detenido (se iniciará)" }
        }
        '^not-configured$' {
            return [pscustomobject]@{ Level = 'ok';   Text = "se creará ($ProcessManager)" }
        }
        '^errored$' {
            return [pscustomobject]@{ Level = 'error'; Text = "$ProcessManager reporta el servicio en estado errored" }
        }
        '^(unknown|desconocido|)$' {
            # El motivo va en el texto: un 'desconocido' sin causa manda a alguien a mirar el
            # servidor a ciegas.
            return [pscustomobject]@{
                Level = 'warn'
                Text  = "no se pudo comprobar el estado del servicio ($ProcessManager no se pudo ejecutar en la sesión no interactiva)"
            }
        }
        default {
            return [pscustomobject]@{ Level = 'warn'; Text = "estado no previsto: $s ($ProcessManager)" }
        }
    }
}