Actualizar un servidor con el agente en una carpeta NO estandar terminaba en verde y lo dejaba con la version anterior. Todo el camino de Windows identificaba al agente por NOMBRE, mientras que lo unico que se actualiza se identifica por RUTA; en cuanto las dos no coincidian, nada fallaba y nada cambiaba. - Se alinea la tarea programada con el binario instalado. `Start-ScheduledTask` ejecuta la ruta registrada en su accion, no el -Prefix: si difieren, se copiaba el binario nuevo en un sitio y se arrancaba el viejo del otro. Ahora se reapunta conservando disparador, principal, ajustes y argumentos; si no se puede corregir, FALLA — arrancar a sabiendas el binario anterior es peor. - La confirmacion de arranque mira la RUTA del proceso. Un agente viejo que nunca se detuvo satisfacia igual de bien un `Get-Process -Name`. Si la ruta no es legible (un proceso de SYSTEM no la expone sin elevacion) se acepta por nombre y se avisa, en vez de revertir una actualizacion correcta por falta de informacion. - Corregido Merge-EnvFile con un config\.env de UNA linea: al asignar la salida de un `if`, PowerShell desenrolla un array de un elemento a escalar, asi que $lines.Count reventaba con Set-StrictMode y la siembra abortaba la instalacion. Nuevo scripts/emular-actualizacion-windows.ps1: monta un agente falso (un .exe real que se queda vivo), una instalacion en una carpeta y una tarea apuntando a otra, corre el instalador y dice si la actualizacion surtio efecto. Sin elevacion y sin tocar la instalacion real de la maquina. Es lo que destapo los dos defectos: contra el instalador anterior reproduce el sintoma exacto —codigo de salida 0 y "El agente esta corriendo con el binario nuevo" sobre un servidor intacto— y contra este confirma que ya surte efecto, sin tocar la tarea cuando ya estaba bien. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
214 lines
9.4 KiB
PowerShell
214 lines
9.4 KiB
PowerShell
<#
|
|
Emulación end-to-end en Windows de la actualización que "completa y no hace nada".
|
|
|
|
Reproduce el caso del servidor real: el agente vive en la carpeta que el panel tiene declarada,
|
|
pero la tarea programada ejecuta el binario de OTRA carpeta. Reemplazar el binario de la primera
|
|
y arrancar la tarea levanta el de la segunda.
|
|
|
|
Qué es REAL aquí: el agente es un .exe compilado que se queda vivo, los archivos y carpetas, la
|
|
copia del binario, la identidad del proceso por ruta, el sello config\.version y la reversión.
|
|
El "viejo" NO escribe el sello (igual que 1.1.0) y el "nuevo" sí (igual que 1.1.3) — esa
|
|
diferencia es la que hacía que la verificación del panel tolerara el sello vacío.
|
|
|
|
Qué está simulado y por qué: los cmdlets *-ScheduledTask. Registrar una tarea exige elevación, y
|
|
la gracia de esta emulación es poder correrla sin ella. Los sustitutos respetan la semántica que
|
|
provoca el fallo: Start-ScheduledTask lanza LA RUTA REGISTRADA EN LA TAREA, no la que se acaba de
|
|
instalar. Como install.ps1 se invoca con &, sus llamadas resuelven a estas funciones del ámbito
|
|
padre. Si quieres la prueba con una tarea de verdad, corre esto en PowerShell elevado tras
|
|
quitar los sustitutos.
|
|
|
|
Uso:
|
|
.\emular-actualizacion-windows.ps1 -Installer ..\install.ps1
|
|
.\emular-actualizacion-windows.ps1 -Installer ..\install.ps1 -Escenario alineada
|
|
|
|
Encontró dos defectos que las pruebas de unidad no veían: el rearranque del binario equivocado,
|
|
y que Merge-EnvFile reventaba con un config\.env de una sola línea.
|
|
#>
|
|
param(
|
|
# install.ps1 a poner a prueba. Para comparar contra una versión anterior:
|
|
# git show <commit>:install.ps1 > install-antes.ps1
|
|
[Parameter(Mandatory = $true)][string]$Installer,
|
|
[string]$Etiqueta = 'instalador',
|
|
# 'desalineada' reproduce el servidor que falla; 'alineada' es el caso normal, para comprobar
|
|
# que el arreglo no lo rompe (una tarea que ya apunta bien no debe tocarse).
|
|
[ValidateSet('desalineada', 'alineada')][string]$Escenario = 'desalineada',
|
|
[string]$Carpeta = 'C:\Users\Public\cras-e2e'
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$base = $Carpeta
|
|
$declarada = Join-Path $base 'declarada' # donde el panel cree que está (y está)
|
|
$otra = Join-Path $base 'otra-carpeta' # a donde apunta la tarea
|
|
$pkg = Join-Path $base 'pkg'
|
|
|
|
function Nuevo-AgenteFalso {
|
|
param([string]$Destino, [string]$Version, [switch]$EscribeSello)
|
|
|
|
# Here-strings LITERALES: el C# lleva comillas dobles y en una cadena interpolada de PowerShell
|
|
# habría que escaparlas con backtick, no con barra invertida.
|
|
$plantilla = @'
|
|
using System; using System.IO; using System.Threading;
|
|
class P { static void Main() {
|
|
string d = AppDomain.CurrentDomain.BaseDirectory;
|
|
__CUERPO__
|
|
Thread.Sleep(Timeout.Infinite); } }
|
|
'@
|
|
$cuerpo = if ($EscribeSello) {
|
|
@'
|
|
Directory.CreateDirectory(Path.Combine(d, "config"));
|
|
File.WriteAllText(Path.Combine(d, "config", ".version"), "__VER__");
|
|
'@
|
|
} else {
|
|
' // 1.1.0 no escribia el sello de version'
|
|
}
|
|
$src = $plantilla.Replace('__CUERPO__', $cuerpo).Replace('__VER__', $Version)
|
|
New-Item -ItemType Directory -Path (Split-Path -Parent $Destino) -Force | Out-Null
|
|
Add-Type -TypeDefinition $src -OutputAssembly $Destino -OutputType ConsoleApplication
|
|
}
|
|
|
|
function Matar-Agentes {
|
|
Get-Process -Name 'CloudRestoreAS' -ErrorAction SilentlyContinue |
|
|
Stop-Process -Force -ErrorAction SilentlyContinue
|
|
Start-Sleep -Milliseconds 800
|
|
}
|
|
|
|
# ------------------------------------------------ tarea programada simulada (estado + semántica)
|
|
$global:tareaExiste = $true
|
|
$global:tareaExecute = if ($Escenario -eq 'alineada') {
|
|
Join-Path $declarada 'CloudRestoreAS.exe'
|
|
} else {
|
|
Join-Path $otra 'CloudRestoreAS.exe'
|
|
}
|
|
$global:tareaArgs = '--start-engine --headless'
|
|
$global:reapuntadaVeces = 0
|
|
|
|
function Get-ScheduledTask {
|
|
[CmdletBinding()] param([string]$TaskName)
|
|
if (-not $global:tareaExiste) { return $null }
|
|
[pscustomobject]@{
|
|
TaskName = 'CloudRestoreAS'
|
|
State = 'Running'
|
|
Actions = @([pscustomobject]@{
|
|
Execute = $global:tareaExecute
|
|
Arguments = $global:tareaArgs
|
|
})
|
|
}
|
|
}
|
|
|
|
function Set-ScheduledTask {
|
|
[CmdletBinding()] param([string]$TaskName, $Action)
|
|
$global:tareaExecute = $Action[0].Execute
|
|
$global:tareaArgs = $Action[0].Arguments
|
|
$global:reapuntadaVeces++
|
|
return $true
|
|
}
|
|
|
|
function New-ScheduledTaskAction {
|
|
[CmdletBinding()] param([string]$Execute, [string]$Argument, [string]$WorkingDirectory)
|
|
[pscustomobject]@{ Execute = $Execute; Arguments = $Argument }
|
|
}
|
|
|
|
function Start-ScheduledTask {
|
|
# AQUÍ está el corazón del fallo: se lanza la ruta REGISTRADA EN LA TAREA.
|
|
[CmdletBinding()] param([string]$TaskName)
|
|
Start-Process -FilePath $global:tareaExecute `
|
|
-WorkingDirectory (Split-Path -Parent $global:tareaExecute) -WindowStyle Hidden | Out-Null
|
|
}
|
|
|
|
function Stop-ScheduledTask {
|
|
[CmdletBinding()] param([string]$TaskName)
|
|
Matar-Agentes
|
|
}
|
|
|
|
# ---------------------------------------------------------------- montaje del escenario
|
|
Matar-Agentes
|
|
Remove-Item $base -Recurse -Force -ErrorAction SilentlyContinue
|
|
Write-Host "=== Montando escenario: $Etiqueta ===" -ForegroundColor Cyan
|
|
|
|
Nuevo-AgenteFalso -Destino (Join-Path $declarada 'CloudRestoreAS.exe') -Version '1.1.0'
|
|
New-Item -ItemType Directory -Path (Join-Path $declarada 'config') -Force | Out-Null
|
|
# Multilínea, como el que escribe el bootstrap del agente. Con UNA sola línea se destapa un bug
|
|
# aparte de Merge-EnvFile (el array se desenrolla a escalar y $lines.Count revienta con StrictMode),
|
|
# que el instalador viejo todavía tiene y aquí taparía el fallo que se quiere medir.
|
|
Set-Content -Path (Join-Path $declarada 'config\.env') -Value @(
|
|
'# Configuracion de CloudRestoreAS',
|
|
'CLOUDRESTORE_PANEL_API_URL=http://ejemplo.local:3000',
|
|
'CLOUDRESTORE_AUTO_START=true'
|
|
)
|
|
|
|
Nuevo-AgenteFalso -Destino (Join-Path $otra 'CloudRestoreAS.exe') -Version '1.1.0'
|
|
|
|
New-Item -ItemType Directory -Path $pkg -Force | Out-Null
|
|
Copy-Item $Installer (Join-Path $pkg 'install.ps1') -Force
|
|
Nuevo-AgenteFalso -Destino (Join-Path $pkg 'dist\CloudRestoreAS.exe') -Version '1.1.3' -EscribeSello
|
|
|
|
$panelEnv = Join-Path $base 'panel.env'
|
|
Set-Content -Path $panelEnv -Value @(
|
|
'CLOUDRESTORE_PANEL_API_URL=http://ejemplo.local:3000',
|
|
'CLOUDRESTORE_PANEL_API_TOKEN=token-de-prueba-no-real',
|
|
'CLOUDRESTORE_PANEL_INSTANCE_KEY=servidor-de-prueba'
|
|
)
|
|
|
|
# El agente viejo corriendo, lanzado por la tarea desde su carpeta.
|
|
Start-ScheduledTask -TaskName 'CloudRestoreAS'
|
|
Start-Sleep -Seconds 2
|
|
|
|
Write-Host " instalacion declarada : $declarada"
|
|
Write-Host " tarea ejecuta : $global:tareaExecute"
|
|
$antes = @(Get-Process -Name 'CloudRestoreAS' -ErrorAction SilentlyContinue |
|
|
ForEach-Object { $_.Path } | Select-Object -Unique)
|
|
Write-Host " proceso corriendo de : $($antes -join ', ')"
|
|
|
|
# ---------------------------------------------------------------- la actualización
|
|
Write-Host ''
|
|
Write-Host '=== Actualizacion, tal como la lanza el panel ===' -ForegroundColor Cyan
|
|
$codigo = 0
|
|
try {
|
|
& (Join-Path $pkg 'install.ps1') -UpdateInPlace -Prefix $declarada -PanelEnvFile $panelEnv
|
|
$codigo = if ($null -eq $LASTEXITCODE) { 0 } else { $LASTEXITCODE }
|
|
} catch {
|
|
$codigo = 1
|
|
Write-Host " [el instalador lanzo] $($_.Exception.Message)" -ForegroundColor Yellow
|
|
}
|
|
Write-Host "--- codigo de salida: $codigo"
|
|
|
|
# ---------------------------------------------------------------- veredicto
|
|
Start-Sleep -Seconds 3
|
|
Write-Host ''
|
|
Write-Host '=== RESULTADO ===' -ForegroundColor Cyan
|
|
$esperado = Join-Path $declarada 'CloudRestoreAS.exe'
|
|
$procAhora = @(Get-Process -Name 'CloudRestoreAS' -ErrorAction SilentlyContinue |
|
|
ForEach-Object { try { $_.Path } catch { '(ilegible)' } } | Select-Object -Unique)
|
|
$selloPath = Join-Path $declarada 'config\.version'
|
|
$sello = if (Test-Path $selloPath) { (Get-Content $selloPath -Raw).Trim() } else { '(no existe)' }
|
|
|
|
Write-Host " tarea ejecuta : $global:tareaExecute (reapuntada $global:reapuntadaVeces vez/veces)"
|
|
Write-Host " proceso corriendo de : $($procAhora -join ', ')"
|
|
Write-Host " config\.version : $sello"
|
|
|
|
$fallos = @()
|
|
if ($global:tareaExecute -ne $esperado) { $fallos += "la tarea sigue apuntando a $global:tareaExecute" }
|
|
if ($procAhora -notcontains $esperado) { $fallos += 'el proceso vivo no es el de la instalacion declarada' }
|
|
if ($sello -ne '1.1.3') { $fallos += "el sello dice '$sello', se esperaba 1.1.3" }
|
|
# Una tarea que ya apuntaba bien no debe reescribirse: hacerlo sin necesidad arriesga perder
|
|
# ajustes que el operador le haya hecho.
|
|
if ($Escenario -eq 'alineada' -and $global:reapuntadaVeces -ne 0) {
|
|
$fallos += "se reescribio una tarea que ya estaba bien ($global:reapuntadaVeces vez/veces)"
|
|
}
|
|
|
|
Write-Host ''
|
|
if ($fallos.Count -eq 0 -and $codigo -eq 0) {
|
|
Write-Host 'VEREDICTO: la actualizacion SI surtio efecto' -ForegroundColor Green
|
|
} elseif ($fallos.Count -eq 0) {
|
|
Write-Host "VEREDICTO: surtio efecto pero el instalador salio con $codigo" -ForegroundColor Yellow
|
|
} else {
|
|
Write-Host 'VEREDICTO: la actualizacion NO surtio efecto' -ForegroundColor Red
|
|
$fallos | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
|
|
if ($codigo -eq 0) {
|
|
Write-Host ' *** y el instalador dijo que TODO BIEN (codigo 0) ***' -ForegroundColor Red
|
|
}
|
|
}
|
|
|
|
Matar-Agentes
|
|
Remove-Item $base -Recurse -Force -ErrorAction SilentlyContinue
|