<# 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 :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