install.sh recibió la maquinaria de actualización segura y install.ps1 nunca recibió el equivalente. La asimetría se notaba en producción: actualizar desde el PANEL dejaba el servidor sin agente. - -UpdateInPlace: actualiza conservando la tarea y la configuración, sin correr el bootstrap (una segunda instancia purga el Temp\ de la que está viva). - No se interrumpe una restauración en curso: sale con 75 (EX_TEMPFAIL), que el PANEL traduce a "reintenta luego". Windows no tenía esta guarda y una reinstalación a destiempo dejaba el respaldo vetado y la base en SINGLE_USER. - Respaldo del binario anterior y reversión automática si el nuevo no arranca. - Rearranque garantizado en TODOS los modos: la detención corría siempre, pero solo -Service volvía a arrancar algo. - Espera de liberación del .exe de 5s a 30s con reintentos de la copia. - Se distingue "no es administrador" de "es administrador con el token filtrado por UAC", que es lo que recibe una sesión de OpenSSH. Se veían idénticos y el remedio es el opuesto. Y --headless deja de ser un no-op en Windows: _ensure_qt_platform() salía de inmediato en win32, así que la tarea ONSTART arrancaba como SYSTEM en la sesión 0 con el plugin Qt 'windows' intentando crear una ventana real. En Linux el unit fija QT_QPA_PLATFORM=offscreen por fuera, y esa asimetría escondió el defecto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
469 lines
21 KiB
PowerShell
469 lines
21 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Instalador Windows de CloudRestoreAS.
|
|
|
|
.DESCRIPTION
|
|
NO instala ni descarga NADA en el sistema: el binario es 100% autocontenido (Qt,
|
|
driver ODBC + Kerberos/OpenSSL, y 7-Zip van embebidos). Este script solo coloca el
|
|
.exe, hace el bootstrap de config\, opcionalmente siembra las credenciales del PANEL
|
|
y registra el arranque automático.
|
|
|
|
Por eso tampoco se usa NSSM ni ningún envoltorio de servicio descargado: el arranque
|
|
24/7 se resuelve con una tarea programada ONSTART, que ya viene en el SO.
|
|
|
|
Reemplazar el binario de un servidor en producción no puede dejarlo sin restaurador, así
|
|
que el script se sostiene sobre tres garantías, las mismas que install.sh:
|
|
1. No actúa si hay una restauración en curso (sale con 75, EX_TEMPFAIL).
|
|
2. Respalda el binario anterior antes de pisarlo.
|
|
3. Confirma que el agente volvió a arrancar y, si no, REVIERTE al binario anterior.
|
|
|
|
.PARAMETER Service
|
|
Arranque 24/7 sin sesión: tarea programada ONSTART como SYSTEM (recomendado en servidor).
|
|
|
|
.PARAMETER Desktop
|
|
Arranque al iniciar sesión. La app registra su propia tarea ONLOGON al ejecutarse.
|
|
|
|
.PARAMETER UpdateInPlace
|
|
Actualiza una instalación EXISTENTE: reemplaza el binario y vuelve a levantar el agente por
|
|
el mismo mecanismo con el que estaba, sin volver a registrar la tarea ni correr el bootstrap.
|
|
Es el modo que usa el PANEL para actualizar, porque no toca nada de la configuración vigente.
|
|
|
|
.PARAMETER Prefix
|
|
Carpeta destino. Default C:\Aduanasoft\CloudRestoreAS.
|
|
|
|
.PARAMETER PanelEnvFile
|
|
Archivo KEY=valor con CLOUDRESTORE_PANEL_API_URL / _API_TOKEN / _INSTANCE_KEY que se
|
|
fusiona en config\.env tras el bootstrap y luego se borra. Lo usa el instalador remoto
|
|
del PANEL para dejar el servidor configurado sin intervención.
|
|
|
|
.EXAMPLE
|
|
.\install.ps1 -Service
|
|
.EXAMPLE
|
|
.\install.ps1 -UpdateInPlace -Prefix 'D:\CloudRestoreAS'
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$Service,
|
|
[switch]$Desktop,
|
|
[switch]$UpdateInPlace,
|
|
[string]$Prefix = 'C:\Aduanasoft\CloudRestoreAS',
|
|
[string]$PanelEnvFile = ''
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Set-StrictMode -Version Latest
|
|
|
|
$BinName = 'CloudRestoreAS.exe'
|
|
$ProcName = 'CloudRestoreAS'
|
|
$TaskName = 'CloudRestoreAS'
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
|
|
# Código de salida reservado para "hay una restauración en curso". Es 75 (EX_TEMPFAIL) y no 1
|
|
# a propósito, igual que en install.sh: le dice al PANEL "reintenta luego", no "falló la
|
|
# instalación", y así el operador no sale a buscar una avería que no existe.
|
|
$EXIT_RESTORE_IN_PROGRESS = 75
|
|
|
|
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
|
|
function Write-Ok($msg) { Write-Host "OK: $msg" -ForegroundColor Green }
|
|
function Write-Warn($msg) { Write-Warning $msg }
|
|
|
|
$chosen = @()
|
|
if ($Service) { $chosen += '-Service' }
|
|
if ($Desktop) { $chosen += '-Desktop' }
|
|
if ($UpdateInPlace) { $chosen += '-UpdateInPlace' }
|
|
if ($chosen.Count -gt 1) {
|
|
throw "Elige un solo modo; se recibieron: $($chosen -join ', ')."
|
|
}
|
|
$Mode = if ($Service) { 'service' }
|
|
elseif ($Desktop) { 'desktop' }
|
|
elseif ($UpdateInPlace) { 'update-in-place' }
|
|
else { 'none' }
|
|
|
|
$dest = Join-Path $Prefix $BinName
|
|
# Respaldo del binario anterior. Se llena solo si hay algo que respaldar; el bloque final lo
|
|
# usa para revertir y lo borra cuando confirma que la versión nueva sí arrancó.
|
|
$backup = ''
|
|
|
|
# --- Utilidades sobre el estado del agente ------------------------------------------
|
|
|
|
function Get-AgentProcess {
|
|
Get-Process -Name $ProcName -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Get-AgentTask {
|
|
Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Test-RestoreInProgress {
|
|
<#
|
|
Una restauración en curso no se interrumpe. El agente no atiende señales de terminación,
|
|
así que matarlo a media restauración deja el job atascado —ese ZIP queda vetado en cada
|
|
escaneo posterior— y puede dejar la base en SINGLE_USER. Cualquier subcarpeta de Temp\
|
|
es un job en vuelo.
|
|
#>
|
|
$temp = Join-Path $Prefix 'Temp'
|
|
if (-not (Test-Path -LiteralPath $temp)) { return $false }
|
|
$first = Get-ChildItem -LiteralPath $temp -Force -ErrorAction SilentlyContinue |
|
|
Select-Object -First 1
|
|
return [bool]$first
|
|
}
|
|
|
|
function Stop-Agent {
|
|
<#
|
|
Detiene tarea y procesos, y espera a que el SO libere el .exe. Devuelve $true si había
|
|
algo corriendo.
|
|
|
|
La espera no es cosmética: mientras un proceso tenga el binario mapeado, Copy-Item falla
|
|
y la actualización aborta. Windows no tiene el truco que hace fácil esto en POSIX —donde
|
|
`install` desvincula el destino antes de crearlo, por lo que reemplazar un binario EN USO
|
|
funciona—, así que aquí no queda más que esperar de verdad. Los 5s de antes se quedaban
|
|
cortos con un antivirus escaneando un onefile de ~270 MB.
|
|
#>
|
|
param([int]$TimeoutSeconds = 30)
|
|
|
|
$running = [bool](Get-AgentProcess)
|
|
if (Get-AgentTask) {
|
|
Write-Step 'Deteniendo tarea programada existente'
|
|
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
}
|
|
Get-AgentProcess | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
while ((Get-AgentProcess) -and (Get-Date) -lt $deadline) {
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
if (Get-AgentProcess) {
|
|
throw ("Hay procesos $ProcName que siguen vivos tras ${TimeoutSeconds}s. No se reemplaza " +
|
|
'el binario: la copia fallaría y dejaría la instalación a medias.')
|
|
}
|
|
return $running
|
|
}
|
|
|
|
function Start-Agent {
|
|
<#
|
|
Vuelve a levantar el agente por el mismo mecanismo con el que estaba: la tarea si está
|
|
registrada, y si no, el proceso suelto. Devuelve la etiqueta del mecanismo usado.
|
|
|
|
No se fija QT_QPA_PLATFORM: `--headless` hace que el propio binario elija el plugin
|
|
'offscreen' en cualquier plataforma. Es lo que permite que el agente corra como SYSTEM
|
|
en la sesión 0, donde no hay escritorio interactivo al que asomar una ventana.
|
|
#>
|
|
if (Get-AgentTask) {
|
|
Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
return 'tarea programada'
|
|
}
|
|
Start-Process -FilePath $dest -ArgumentList '--start-engine', '--headless' `
|
|
-WorkingDirectory $Prefix -WindowStyle Hidden
|
|
return 'proceso suelto'
|
|
}
|
|
|
|
function Wait-AgentAlive {
|
|
param([int]$TimeoutSeconds = 60)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
while ((Get-Date) -lt $deadline) {
|
|
if (Get-AgentProcess) { return $true }
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
return $false
|
|
}
|
|
|
|
function Copy-Binary {
|
|
<#
|
|
Copia con reintentos. Tras terminar un proceso, el antivirus y el propio SO pueden
|
|
mantener el .exe abierto unos segundos más; reintentar sale mucho más barato que abortar
|
|
una actualización que iba bien.
|
|
#>
|
|
param([string]$From, [string]$To, [int]$Attempts = 5)
|
|
|
|
for ($i = 1; $i -le $Attempts; $i++) {
|
|
try {
|
|
Copy-Item -LiteralPath $From -Destination $To -Force -ErrorAction Stop
|
|
return
|
|
} catch {
|
|
if ($i -eq $Attempts) { throw }
|
|
Start-Sleep -Seconds 3
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Localizar el binario (mismas rutas candidatas que install.sh) -------------------
|
|
$candidates = @(
|
|
(Join-Path $ScriptDir "dist\$BinName"),
|
|
(Join-Path $ScriptDir $BinName)
|
|
)
|
|
$src = $candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
|
|
if (-not $src) {
|
|
throw "No se encontró el binario ($BinName). Ejecuta .\build.ps1 primero."
|
|
}
|
|
|
|
# -Service crea una tarea como SYSTEM: requiere elevación.
|
|
#
|
|
# Se distingue "no es administrador" de "es administrador pero UAC le dio el token FILTRADO",
|
|
# que es lo que recibe una cuenta administradora que entra por OpenSSH cuando el destino no
|
|
# tiene LocalAccountTokenFilterPolicy. Los dos casos se veían idénticos —"no eres admin"— y el
|
|
# remedio es opuesto: en el primero hay que cambiar de cuenta; en el segundo la cuenta ya es la
|
|
# correcta y lo que falta es una política del servidor.
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$currentPrincipal = [Security.Principal.WindowsPrincipal]$identity
|
|
$isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
$adminSid = New-Object Security.Principal.SecurityIdentifier 'S-1-5-32-544'
|
|
$inAdminGroup = [bool]($identity.Groups | Where-Object { $_ -eq $adminSid })
|
|
|
|
if ($Mode -eq 'service' -and -not $isAdmin) {
|
|
if ($inAdminGroup) {
|
|
throw ("La cuenta '$($identity.Name)' SÍ pertenece a Administradores, pero esta sesión " +
|
|
'recibió el token filtrado por UAC, así que no puede registrar la tarea ONSTART como ' +
|
|
'SYSTEM. No hace falta cambiar de cuenta: hay que permitir la elevación remota en el ' +
|
|
'servidor (LocalAccountTokenFilterPolicy=1 en ' +
|
|
'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System), o usar -UpdateInPlace ' +
|
|
'si solo se está actualizando una instalación que ya existe.')
|
|
}
|
|
throw ("La cuenta '$($identity.Name)' no es Administrador y -Service registra una tarea " +
|
|
'ONSTART como SYSTEM, que exige elevación.')
|
|
}
|
|
|
|
Write-Host '===============================================' -ForegroundColor Cyan
|
|
Write-Host 'CloudRestoreAS - Instalación Windows'
|
|
Write-Host " Binario : $src"
|
|
Write-Host " Destino : $Prefix"
|
|
Write-Host " Modo : $Mode"
|
|
Write-Host '===============================================' -ForegroundColor Cyan
|
|
|
|
if ($Mode -eq 'update-in-place' -and -not (Test-Path -LiteralPath $dest)) {
|
|
throw ("Se pidió -UpdateInPlace pero en $Prefix no hay una instalación ($BinName no existe). " +
|
|
'Para una instalación nueva usa -Service, -Desktop o sin modo.')
|
|
}
|
|
|
|
# --- Guarda: no interrumpir una restauración en curso -------------------------------
|
|
# Va ANTES de detener nada y en TODOS los modos: el daño lo hace matar al agente, no el modo
|
|
# que se haya pedido. Antes esta comprobación no existía en Windows y una reinstalación a
|
|
# destiempo se llevaba por delante el respaldo que estuviera restaurando.
|
|
if (Test-RestoreInProgress) {
|
|
Write-Warn "Hay una restauración en curso ($Prefix\Temp no está vacío)."
|
|
Write-Warn 'No se instala para no dejarla a medias. Reintenta cuando termine.'
|
|
exit $EXIT_RESTORE_IN_PROGRESS
|
|
}
|
|
|
|
# --- Colocar el binario -------------------------------------------------------------
|
|
Write-Step 'Instalando binario'
|
|
New-Item -ItemType Directory -Path $Prefix -Force | Out-Null
|
|
|
|
$taskExisted = [bool](Get-AgentTask)
|
|
$wasRunning = Stop-Agent
|
|
|
|
# Respaldo para poder volver atrás. Se hace siempre que haya algo que pisar, no solo al
|
|
# actualizar en sitio: el PANEL actualiza con -Service, y esa vía también reemplaza el binario
|
|
# de un servidor en producción. Sin respaldo, un binario nuevo que no arranque deja el servidor
|
|
# sin agente y sin forma de recuperarlo salvo entrando a mano.
|
|
if (Test-Path -LiteralPath $dest) {
|
|
$backup = Join-Path $Prefix ".$BinName.prev"
|
|
try {
|
|
Copy-Item -LiteralPath $dest -Destination $backup -Force -ErrorAction Stop
|
|
Write-Ok "Respaldo del binario actual en $backup"
|
|
} catch {
|
|
throw ("No se pudo respaldar el binario actual en $backup ($($_.Exception.Message)). " +
|
|
'Se aborta: actualizar sin poder revertir no es aceptable.')
|
|
}
|
|
}
|
|
|
|
Copy-Binary -From $src -To $dest
|
|
Write-Ok "Binario instalado en $dest"
|
|
|
|
# --- Bootstrap de config\ -----------------------------------------------------------
|
|
# El propio binario crea config\, .env y las carpetas de trabajo al arrancar. Se corre
|
|
# una vez acotado por timeout para que el operador ya pueda editar config\.env.
|
|
#
|
|
# Al actualizar en sitio se OMITE: el binario hace ensure_runtime_layout() en cada arranque, así
|
|
# que es redundante, y correr una segunda instancia junto a la viva es peligroso —al arrancar, el
|
|
# motor purga todas las subcarpetas de Temp, que son de la instancia en curso—.
|
|
if ($Mode -ne 'update-in-place') {
|
|
Write-Step 'Inicializando config\ (bootstrap)'
|
|
Push-Location $Prefix
|
|
try {
|
|
$env:QT_QPA_PLATFORM = 'offscreen'
|
|
$proc = Start-Process -FilePath $dest -ArgumentList '--headless' -PassThru -WindowStyle Hidden
|
|
if (-not $proc.WaitForExit(20000)) {
|
|
$proc | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
}
|
|
} finally {
|
|
Remove-Item Env:\QT_QPA_PLATFORM -ErrorAction SilentlyContinue
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
$envPath = Join-Path $Prefix 'config\.env'
|
|
if (Test-Path -LiteralPath $envPath) {
|
|
Write-Ok 'config\.env presente.'
|
|
} else {
|
|
Write-Warn 'config\.env se creará en la primera ejecución.'
|
|
}
|
|
|
|
# --- Siembra de credenciales del PANEL ----------------------------------------------
|
|
# Fusión replace-or-append: respeta el resto de config\.env y es idempotente, así que
|
|
# reinstalar no duplica claves ni pierde ajustes locales.
|
|
function Merge-EnvFile {
|
|
param([string]$Target, [hashtable]$Values)
|
|
|
|
$lines = if (Test-Path -LiteralPath $Target) {
|
|
@(Get-Content -LiteralPath $Target -Encoding UTF8)
|
|
} else { @() }
|
|
|
|
foreach ($key in $Values.Keys) {
|
|
$line = "$key=$($Values[$key])"
|
|
$idx = -1
|
|
for ($i = 0; $i -lt $lines.Count; $i++) {
|
|
if ($lines[$i] -match "^\s*$([regex]::Escape($key))\s*=") { $idx = $i; break }
|
|
}
|
|
if ($idx -ge 0) { $lines[$idx] = $line } else { $lines += $line }
|
|
}
|
|
|
|
# Se escribe SIN BOM a propósito. `Set-Content -Encoding UTF8` en PowerShell 5.1 agrega BOM
|
|
# (EF BB BF), y python-dotenv abre el archivo con encoding utf-8 (no utf-8-sig), así que el
|
|
# BOM se pega a la primera línea. Si esa primera línea es una clave —lo que pasa cuando el
|
|
# bootstrap no alcanzó a crear config\.env y este archivo se genera desde cero— la clave
|
|
# queda ilegible para el agente: se instalaría sin conectarse al panel, con toda la
|
|
# apariencia de un error de captura.
|
|
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
[System.IO.File]::WriteAllLines($Target, [string[]]$lines, $utf8NoBom)
|
|
}
|
|
|
|
if ($PanelEnvFile) {
|
|
if (-not (Test-Path -LiteralPath $PanelEnvFile)) {
|
|
throw "No existe el archivo indicado en -PanelEnvFile: $PanelEnvFile"
|
|
}
|
|
Write-Step 'Sembrando credenciales del PANEL en config\.env'
|
|
$allowed = @(
|
|
'CLOUDRESTORE_PANEL_API_URL',
|
|
'CLOUDRESTORE_PANEL_API_TOKEN',
|
|
'CLOUDRESTORE_PANEL_INSTANCE_KEY',
|
|
'CLOUDRESTORE_PANEL_VERIFY_SSL'
|
|
)
|
|
$values = @{}
|
|
foreach ($raw in Get-Content -LiteralPath $PanelEnvFile -Encoding UTF8) {
|
|
$line = $raw.Trim()
|
|
if (-not $line -or $line.StartsWith('#')) { continue }
|
|
$eq = $line.IndexOf('=')
|
|
if ($eq -lt 1) { continue }
|
|
$key = $line.Substring(0, $eq).Trim()
|
|
# Lista blanca: el archivo viene de la red, no debe poder inyectar otras claves.
|
|
if ($allowed -contains $key) {
|
|
$values[$key] = $line.Substring($eq + 1).Trim()
|
|
}
|
|
}
|
|
if ($values.Count -gt 0) {
|
|
# En update-in-place el bootstrap no corrió, así que config\.env puede no existir todavía
|
|
# si la instalación previa nunca llegó a arrancar. Merge-EnvFile lo crea.
|
|
New-Item -ItemType Directory -Path (Split-Path -Parent $envPath) -Force | Out-Null
|
|
Merge-EnvFile -Target $envPath -Values $values
|
|
Write-Ok "$($values.Count) clave(s) del PANEL escritas en config\.env"
|
|
} else {
|
|
Write-Warn 'El archivo -PanelEnvFile no traía claves CLOUDRESTORE_PANEL_* válidas.'
|
|
}
|
|
# El archivo trae el token en claro: se borra en cuanto se consumió.
|
|
Remove-Item -LiteralPath $PanelEnvFile -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# --- Arranque automático ------------------------------------------------------------
|
|
switch ($Mode) {
|
|
'service' {
|
|
Write-Step 'Registrando tarea programada ONSTART (SYSTEM)'
|
|
# Sin NSSM: una tarea ONSTART como SYSTEM cubre el 24/7 headless con lo que ya
|
|
# trae el SO. El binario elige el plugin Qt 'offscreen' por `--headless`, que es lo que
|
|
# le permite correr en la sesión 0, donde SYSTEM no tiene escritorio interactivo.
|
|
#
|
|
# Correr como SYSTEM es además lo que evita aquí el problema de propiedad que en Linux
|
|
# sí hay que resolver: allá el instalador crea el árbol como root pero el unit corre
|
|
# como un usuario común, que no podría leer su config/.env ni escribir su base local
|
|
# (de ahí el `chown -R` de install.sh). SYSTEM tiene control total sobre el sistema de
|
|
# archivos local y este script no restringe ninguna ACL del prefijo, así que hereda de
|
|
# su carpeta padre y puede leer y escribir todo lo que creó el Administrador. No hace
|
|
# falta un arreglo simétrico; si algún día se cambia el principal a una cuenta común,
|
|
# entonces sí habría que ajustar los permisos del árbol.
|
|
$action = New-ScheduledTaskAction -Execute $dest `
|
|
-Argument '--start-engine --headless' -WorkingDirectory $Prefix
|
|
$trigger = New-ScheduledTaskTrigger -AtStartup
|
|
$taskPrincipal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount `
|
|
-RunLevel Highest
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
|
|
-DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
|
|
-ExecutionTimeLimit ([TimeSpan]::Zero)
|
|
|
|
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
|
|
-Principal $taskPrincipal -Settings $settings -Force | Out-Null
|
|
Start-ScheduledTask -TaskName $TaskName
|
|
Write-Ok "Tarea '$TaskName' registrada y arrancada."
|
|
}
|
|
'desktop' {
|
|
# La app registra su propia tarea ONLOGON la primera vez que corre EN la sesión del
|
|
# usuario. Desde aquí no se puede hacer por él: esta sesión no es la suya.
|
|
Write-Host 'Modo escritorio: el autostart ONLOGON queda registrado la primera vez que la'
|
|
Write-Host 'app se abra en la sesión del usuario.'
|
|
}
|
|
'update-in-place' {
|
|
# La tarea ya está registrada y no cambia: no se vuelve a definir ni se toca su
|
|
# configuración. El binario nuevo ya está en su sitio; el bloque de abajo se encarga de
|
|
# que el agente vuelva a levantarse con él.
|
|
Write-Host 'Actualización en sitio: la tarea programada existente se conserva tal cual.'
|
|
}
|
|
'none' {
|
|
Write-Host 'Instalación sin arranque automático.'
|
|
}
|
|
}
|
|
|
|
# --- Confirmar que el agente quedó corriendo, o revertir ----------------------------
|
|
# Esto es lo que faltaba: antes SOLO el modo -Service arrancaba algo, mientras que la detención
|
|
# de arriba corría en todos los modos. Actualizar con 'desktop' o 'none' mataba el agente y se
|
|
# iba, dejando el servidor sin restaurador y sin ninguna señal de que eso había pasado.
|
|
$shouldBeRunning = ($Mode -eq 'service') -or $wasRunning -or $taskExisted
|
|
|
|
if ($shouldBeRunning) {
|
|
if (-not (Get-AgentProcess)) {
|
|
$how = Start-Agent
|
|
Write-Step "Rearrancando el agente ($how)"
|
|
}
|
|
if (Wait-AgentAlive -TimeoutSeconds 60) {
|
|
Write-Ok 'El agente está corriendo con el binario nuevo.'
|
|
if ($backup -and (Test-Path -LiteralPath $backup)) {
|
|
# Solo tras confirmar que la versión nueva corre se descarta el respaldo.
|
|
Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue
|
|
}
|
|
} else {
|
|
Write-Warn 'El agente no volvió a arrancar tras la actualización.'
|
|
$reverted = $false
|
|
if ($backup -and (Test-Path -LiteralPath $backup)) {
|
|
Write-Warn 'Revirtiendo al binario anterior...'
|
|
try {
|
|
Copy-Binary -From $backup -To $dest
|
|
Start-Agent | Out-Null
|
|
$reverted = Wait-AgentAlive -TimeoutSeconds 60
|
|
} catch {
|
|
$reverted = $false
|
|
}
|
|
}
|
|
if ($reverted) {
|
|
throw ('La versión nueva no arrancó, así que se REVIRTIÓ: el binario anterior está ' +
|
|
"corriendo de nuevo y el servidor quedó como estaba. La versión nueva NO se " +
|
|
"aplicó. Revisa $Prefix\config\logs para ver por qué no levantó.")
|
|
}
|
|
$backupNote = if ($backup) { $backup } else { '(sin respaldo)' }
|
|
throw ('La instalación no dejó al agente corriendo y no se pudo revertir. El binario ' +
|
|
"nuevo está en $dest y el anterior en $backupNote. Revisa $Prefix\config\logs.")
|
|
}
|
|
} elseif ($backup -and (Test-Path -LiteralPath $backup)) {
|
|
# No había agente corriendo ni tarea que relevar, así que no hay arranque que confirmar y el
|
|
# respaldo ya no protege de nada. Se borra: son ~270 MB, y dejarlos ahí para siempre convierte
|
|
# cada reinstalación en un cobro de disco silencioso.
|
|
Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-Host "Configuración : $envPath"
|
|
Write-Host "Logs : $Prefix\config\logs"
|
|
Write-Host 'Listo.'
|
|
|
|
# Salida explícita. El PANEL invoca este script como `& install.ps1 ...; exit $LASTEXITCODE`, y
|
|
# sin un `exit` propio esa variable queda sin fijar en el camino de éxito —aquí no corre ningún
|
|
# comando nativo que la establezca—, así que el código de salida dependería de lo que hubiera
|
|
# quedado en la sesión. Un 0 explícito no deja lugar a esa ambigüedad.
|
|
exit 0
|