Esa ruta es el peor caso posible y existe en produccion: la ruta por omision C:\Aduanasoft\CloudRestoreAS es PREFIJO DE CADENA de ella, asi que cualquier comparacion hecha con startsWith daria por iguales dos instalaciones distintas — y el resultado seria el fallo silencioso otra vez, actualizar una carpeta y arrancar la otra. El flujo ya la manejaba bien (Test-SamePath compara por igualdad exacta tras normalizar), pero nada lo probaba: la emulacion usaba declarada/otra-carpeta, nombres sin relacion entre si, que un startsWith mal puesto pasaria sin problema. - Escenario `sufijo` en emular-actualizacion-windows.ps1: instalacion en ...\CloudRestoreAS-win y tarea apuntando a ...\CloudRestoreAS. Verificado en Windows: reapunta la tarea, el proceso queda corriendo desde -win y el sello en la version nueva. - Nuevo scripts/probar-funciones-install.ps1: extrae las funciones del instalador por AST y las ejercita contra una tarea simulada, sin elevacion. Cubre los casos limite de la comparacion de rutas (el par de prefijo en ambos sentidos, comillas, barra final, mayusculas, `..`, ruta vacia) y que Sync-AgentTaskPath falle cuando no puede corregir. BUILD.md documenta por que se compara por igualdad exacta y no por prefijo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
228 lines
10 KiB
PowerShell
228 lines
10 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).
|
|
#
|
|
# 'sufijo' es el caso PELIGROSO y existe en producción: la instalación está en
|
|
# ...\CloudRestoreAS-win y la tarea apunta a ...\CloudRestoreAS, que es prefijo de cadena de la
|
|
# anterior. Cualquier comparación de rutas hecha con `startsWith` las daría por iguales, no
|
|
# corregiría la tarea, y la actualización volvería a no surtir efecto.
|
|
[ValidateSet('desalineada', 'alineada', 'sufijo')][string]$Escenario = 'desalineada',
|
|
[string]$Carpeta = 'C:\Users\Public\cras-e2e'
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$base = $Carpeta
|
|
$pkg = Join-Path $base 'pkg'
|
|
|
|
# En el escenario 'sufijo' los nombres NO son arbitrarios: reproducen el par que se da en producción,
|
|
# donde el nombre de una carpeta es prefijo de cadena del de la otra. Con nombres sin relación entre
|
|
# sí (declarada / otra-carpeta) un `startsWith` mal puesto pasaría la prueba sin problema.
|
|
if ($Escenario -eq 'sufijo') {
|
|
$declarada = Join-Path $base 'CloudRestoreAS-win' # la instalación real
|
|
$otra = Join-Path $base 'CloudRestoreAS' # a donde apunta la tarea
|
|
} else {
|
|
$declarada = Join-Path $base 'declarada' # donde el panel cree que está (y está)
|
|
$otra = Join-Path $base 'otra-carpeta' # a donde apunta la tarea
|
|
}
|
|
|
|
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
|