fix(install): la actualizacion de Windows decia que funciono y no cambiaba nada
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>
This commit is contained in:
176
install.ps1
176
install.ps1
@@ -94,6 +94,107 @@ function Get-AgentTask {
|
|||||||
Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-NormalizedPath([string]$Path) {
|
||||||
|
<#
|
||||||
|
Normaliza para comparar rutas: la tarea puede guardarlas entrecomilladas, con otra
|
||||||
|
capitalización o con separador final. Comparar las cadenas en crudo daba falsos negativos.
|
||||||
|
#>
|
||||||
|
if (-not $Path) { return '' }
|
||||||
|
$clean = $Path.Trim().Trim('"')
|
||||||
|
try { return [System.IO.Path]::GetFullPath($clean).TrimEnd('\') } catch { return $clean.TrimEnd('\') }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SamePath([string]$A, [string]$B) {
|
||||||
|
if (-not $A -or -not $B) { return $false }
|
||||||
|
# NTFS no distingue mayúsculas: comparar sensible a caso reportaría dos rutas distintas donde
|
||||||
|
# el SO ve una sola.
|
||||||
|
return (Get-NormalizedPath $A) -ieq (Get-NormalizedPath $B)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-AgentTaskExecute {
|
||||||
|
<#
|
||||||
|
Ruta del ejecutable que la tarea programada tiene registrada en su acción.
|
||||||
|
|
||||||
|
Este es el dato que faltaba. Reemplazar el binario es una operación por RUTA, pero
|
||||||
|
arrancarlo con `Start-ScheduledTask` es una operación por NOMBRE. Si la tarea apunta a otra
|
||||||
|
carpeta se actualiza un binario y se arranca otro: el servidor se queda en la versión vieja
|
||||||
|
y la actualización parece haber funcionado.
|
||||||
|
|
||||||
|
Se lee la propiedad por reflexión porque no todas las acciones de una tarea son de tipo
|
||||||
|
Exec (una ComHandler no tiene `Execute`), y con Set-StrictMode tocar una propiedad que no
|
||||||
|
existe es un error.
|
||||||
|
#>
|
||||||
|
$task = Get-AgentTask
|
||||||
|
if (-not $task) { return '' }
|
||||||
|
foreach ($action in @($task.Actions)) {
|
||||||
|
$prop = $action.PSObject.Properties['Execute']
|
||||||
|
if ($prop -and $prop.Value) { return ([string]$prop.Value).Trim().Trim('"') }
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function Repair-AgentTaskPath {
|
||||||
|
<#
|
||||||
|
Reapunta la acción de la tarea al binario recién instalado, conservando el resto de su
|
||||||
|
definición (disparador, principal, ajustes) y los argumentos que tuviera: volver a
|
||||||
|
registrarla desde cero perdería cualquier ajuste que el operador le haya hecho.
|
||||||
|
#>
|
||||||
|
param($Task)
|
||||||
|
|
||||||
|
$actions = @()
|
||||||
|
foreach ($action in @($Task.Actions)) {
|
||||||
|
$execProp = $action.PSObject.Properties['Execute']
|
||||||
|
if (-not $execProp) { $actions += $action; continue }
|
||||||
|
|
||||||
|
$argProp = $action.PSObject.Properties['Arguments']
|
||||||
|
$arguments = if ($argProp -and $argProp.Value) { [string]$argProp.Value } else { '' }
|
||||||
|
$actions += if ($arguments) {
|
||||||
|
New-ScheduledTaskAction -Execute $dest -Argument $arguments -WorkingDirectory $Prefix
|
||||||
|
} else {
|
||||||
|
New-ScheduledTaskAction -Execute $dest -WorkingDirectory $Prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Set-ScheduledTask -TaskName $TaskName -Action $actions -ErrorAction Stop | Out-Null
|
||||||
|
Write-Ok "Tarea '$TaskName' reapuntada a $dest."
|
||||||
|
} catch {
|
||||||
|
throw ("La tarea '$TaskName' ejecuta un binario distinto del que se acaba de instalar y no " +
|
||||||
|
"se pudo corregir ($($_.Exception.Message)). Arrancarla levantaría la versión anterior " +
|
||||||
|
'y la actualización quedaría sin efecto aparentando haber funcionado, que es peor que ' +
|
||||||
|
'fallar aquí.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-AgentState {
|
||||||
|
<#
|
||||||
|
Estado del agente en una sola consulta: si hay algo vivo, si alguno corre el binario de
|
||||||
|
ESTA instalación, y desde dónde corren los que no.
|
||||||
|
|
||||||
|
`PathsReadable` separa "no corre desde aquí" de "no pude ver desde dónde corre": un proceso
|
||||||
|
de SYSTEM no expone `.Path` a una cuenta sin elevación, y tratar ese caso como "es de otra
|
||||||
|
instalación" revertiría actualizaciones correctas.
|
||||||
|
|
||||||
|
Vive en un solo sitio a propósito. Esta lógica repetida en dos funciones es exactamente
|
||||||
|
cómo se colaron las divergencias que estamos arreglando.
|
||||||
|
#>
|
||||||
|
$procs = @(Get-AgentProcess)
|
||||||
|
$paths = @()
|
||||||
|
foreach ($proc in $procs) {
|
||||||
|
try { if ($proc.Path) { $paths += $proc.Path } } catch { }
|
||||||
|
}
|
||||||
|
$fromPrefix = $false
|
||||||
|
foreach ($path in $paths) {
|
||||||
|
if (Test-SamePath $path $dest) { $fromPrefix = $true; break }
|
||||||
|
}
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Running = ($procs.Count -gt 0)
|
||||||
|
PathsReadable = (($procs.Count -eq 0) -or ($paths.Count -gt 0))
|
||||||
|
FromPrefix = $fromPrefix
|
||||||
|
Paths = $paths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Test-RestoreInProgress {
|
function Test-RestoreInProgress {
|
||||||
<#
|
<#
|
||||||
Una restauración en curso no se interrumpe. El agente no atiende señales de terminación,
|
Una restauración en curso no se interrumpe. El agente no atiende señales de terminación,
|
||||||
@@ -147,6 +248,9 @@ function Start-Agent {
|
|||||||
No se fija QT_QPA_PLATFORM: `--headless` hace que el propio binario elija el plugin
|
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
|
'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.
|
en la sesión 0, donde no hay escritorio interactivo al que asomar una ventana.
|
||||||
|
|
||||||
|
Que la acción de la tarea apunte a `$dest` lo garantiza `Sync-AgentTaskPath`, que corre
|
||||||
|
justo después de copiar el binario. Aquí ya se puede arrancar sin volver a comprobarlo.
|
||||||
#>
|
#>
|
||||||
if (Get-AgentTask) {
|
if (Get-AgentTask) {
|
||||||
Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
@@ -158,15 +262,59 @@ function Start-Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Wait-AgentAlive {
|
function Wait-AgentAlive {
|
||||||
|
<#
|
||||||
|
Espera a que el agente esté vivo, y que sea el binario de ESTA instalación.
|
||||||
|
|
||||||
|
La identidad importa: `Get-Process -Name` responde "hay un proceso con ese nombre", no
|
||||||
|
"corre el binario que instalé". Con la tarea apuntando a otra carpeta, el agente viejo
|
||||||
|
—que nunca se fue— satisfacía la comprobación por nombre y la actualización pasaba por
|
||||||
|
buena sin haber cambiado nada.
|
||||||
|
|
||||||
|
Si la ruta del proceso no es legible NO se concluye que sea de otra instalación: un proceso
|
||||||
|
de SYSTEM no expone `.Path` a una cuenta sin elevación. Se acepta por nombre y se avisa,
|
||||||
|
porque revertir una actualización correcta por falta de información es peor.
|
||||||
|
#>
|
||||||
param([int]$TimeoutSeconds = 60)
|
param([int]$TimeoutSeconds = 60)
|
||||||
|
|
||||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||||
|
$foreign = @()
|
||||||
while ((Get-Date) -lt $deadline) {
|
while ((Get-Date) -lt $deadline) {
|
||||||
if (Get-AgentProcess) { return $true }
|
$state = Get-AgentState
|
||||||
|
if ($state.Running) {
|
||||||
|
if (-not $state.PathsReadable) {
|
||||||
|
Write-Warn ("Hay $ProcName corriendo pero no se pudo leer su ruta; se acepta por " +
|
||||||
|
'nombre. La versión efectiva la confirma el panel con config\.version.')
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
if ($state.FromPrefix) { return $true }
|
||||||
|
$foreign = $state.Paths
|
||||||
|
}
|
||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Seconds 2
|
||||||
}
|
}
|
||||||
|
# Se avisa una sola vez y al final: dentro del bucle serían treinta líneas iguales.
|
||||||
|
if ($foreign.Count -gt 0) {
|
||||||
|
Write-Warn ("Hay $ProcName corriendo desde $($foreign -join ', '), pero no desde $dest.")
|
||||||
|
}
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Sync-AgentTaskPath {
|
||||||
|
<#
|
||||||
|
Alinea la acción de la tarea programada con el binario recién instalado.
|
||||||
|
|
||||||
|
Es la raíz del fallo silencioso: reemplazar el binario es una operación por RUTA, pero
|
||||||
|
`Start-ScheduledTask` es por NOMBRE y ejecuta la ruta que la tarea lleva registrada. En una
|
||||||
|
instalación fuera de la carpeta por omisión, eso significaba copiar el binario nuevo en un
|
||||||
|
sitio y arrancar el viejo desde otro: el run terminaba en verde y el servidor seguía igual.
|
||||||
|
#>
|
||||||
|
$registered = Get-AgentTaskExecute
|
||||||
|
if (-not $registered) { return }
|
||||||
|
if (Test-SamePath $registered $dest) { return }
|
||||||
|
|
||||||
|
Write-Warn "La tarea '$TaskName' ejecutaba $registered, no $dest."
|
||||||
|
Repair-AgentTaskPath -Task (Get-AgentTask)
|
||||||
|
}
|
||||||
|
|
||||||
function Copy-Binary {
|
function Copy-Binary {
|
||||||
<#
|
<#
|
||||||
Copia con reintentos. Tras terminar un proceso, el antivirus y el propio SO pueden
|
Copia con reintentos. Tras terminar un proceso, el antivirus y el propio SO pueden
|
||||||
@@ -269,6 +417,11 @@ if (Test-Path -LiteralPath $dest) {
|
|||||||
Copy-Binary -From $src -To $dest
|
Copy-Binary -From $src -To $dest
|
||||||
Write-Ok "Binario instalado en $dest"
|
Write-Ok "Binario instalado en $dest"
|
||||||
|
|
||||||
|
# El binario nuevo ya está en su sitio; falta que el arranque automático apunte AHÍ. Va aquí y no
|
||||||
|
# dentro de Start-Agent para que se corrija aunque en este momento no haya que arrancar nada: una
|
||||||
|
# tarea desalineada seguiría levantando la versión vieja en el próximo reinicio del servidor.
|
||||||
|
Sync-AgentTaskPath
|
||||||
|
|
||||||
# --- Bootstrap de config\ -----------------------------------------------------------
|
# --- Bootstrap de config\ -----------------------------------------------------------
|
||||||
# El propio binario crea config\, .env y las carpetas de trabajo al arrancar. Se corre
|
# 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.
|
# una vez acotado por timeout para que el operador ya pueda editar config\.env.
|
||||||
@@ -304,9 +457,13 @@ if (Test-Path -LiteralPath $envPath) {
|
|||||||
function Merge-EnvFile {
|
function Merge-EnvFile {
|
||||||
param([string]$Target, [hashtable]$Values)
|
param([string]$Target, [hashtable]$Values)
|
||||||
|
|
||||||
$lines = if (Test-Path -LiteralPath $Target) {
|
# El @() envuelve el `if` COMPLETO, no solo el Get-Content. Al asignar la salida de un `if`,
|
||||||
@(Get-Content -LiteralPath $Target -Encoding UTF8)
|
# PowerShell desenrolla un array de un solo elemento a escalar: con un config\.env de UNA línea
|
||||||
} else { @() }
|
# $lines quedaba como String y `$lines.Count` reventaba con Set-StrictMode. Lo destapó la
|
||||||
|
# emulación de una actualización sobre una instalación con .env mínimo.
|
||||||
|
$lines = @(
|
||||||
|
if (Test-Path -LiteralPath $Target) { Get-Content -LiteralPath $Target -Encoding UTF8 }
|
||||||
|
)
|
||||||
|
|
||||||
foreach ($key in $Values.Keys) {
|
foreach ($key in $Values.Keys) {
|
||||||
$line = "$key=$($Values[$key])"
|
$line = "$key=$($Values[$key])"
|
||||||
@@ -417,7 +574,16 @@ switch ($Mode) {
|
|||||||
$shouldBeRunning = ($Mode -eq 'service') -or $wasRunning -or $taskExisted
|
$shouldBeRunning = ($Mode -eq 'service') -or $wasRunning -or $taskExisted
|
||||||
|
|
||||||
if ($shouldBeRunning) {
|
if ($shouldBeRunning) {
|
||||||
if (-not (Get-AgentProcess)) {
|
# La condición mira la RUTA, no solo el nombre. Mientras se copiaba, la tarea pudo relanzar el
|
||||||
|
# agente desde su ruta anterior (trae RestartCount): con una comprobación por nombre eso pasaba
|
||||||
|
# por "ya está corriendo", no se arrancaba el nuevo, y la actualización quedaba sin efecto.
|
||||||
|
$state = Get-AgentState
|
||||||
|
if (-not ($state.Running -and ($state.FromPrefix -or -not $state.PathsReadable))) {
|
||||||
|
if ($state.Running) {
|
||||||
|
Write-Warn ("Hay $ProcName corriendo desde $($state.Paths -join ', '); se termina para " +
|
||||||
|
'que no compita con el binario recién instalado.')
|
||||||
|
Stop-Agent -TimeoutSeconds 15 | Out-Null
|
||||||
|
}
|
||||||
$how = Start-Agent
|
$how = Start-Agent
|
||||||
Write-Step "Rearrancando el agente ($how)"
|
Write-Step "Rearrancando el agente ($how)"
|
||||||
}
|
}
|
||||||
|
|||||||
213
scripts/emular-actualizacion-windows.ps1
Normal file
213
scripts/emular-actualizacion-windows.ps1
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<#
|
||||||
|
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
|
||||||
@@ -138,6 +138,38 @@ def test_install_ps1_respalda_y_revierte():
|
|||||||
assert "Remove-Item -LiteralPath $backup" in ps1
|
assert "Remove-Item -LiteralPath $backup" in ps1
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_ps1_alinea_la_tarea_con_el_binario_instalado():
|
||||||
|
"""
|
||||||
|
El fallo silencioso: reemplazar el binario es una operación por RUTA, pero
|
||||||
|
`Start-ScheduledTask` es por NOMBRE y ejecuta la ruta que la tarea lleva registrada. Con una
|
||||||
|
instalación fuera de la carpeta por omisión, eso copiaba el binario nuevo en un sitio y
|
||||||
|
arrancaba el viejo desde otro: el run terminaba en verde y el servidor seguía igual.
|
||||||
|
|
||||||
|
Se comprueba con `scripts/emular-actualizacion-windows.ps1`, que reproduce el escenario
|
||||||
|
completo; esto solo protege de que las piezas desaparezcan en un refactor.
|
||||||
|
"""
|
||||||
|
ps1 = (ROOT / "install.ps1").read_text(encoding="utf-8")
|
||||||
|
assert "Sync-AgentTaskPath" in ps1
|
||||||
|
assert "Get-AgentTaskExecute" in ps1
|
||||||
|
# La comparación tiene que normalizar: la tarea guarda la ruta entrecomillada y NTFS no
|
||||||
|
# distingue mayúsculas, así que comparar las cadenas en crudo da falsos negativos.
|
||||||
|
assert "Test-SamePath" in ps1
|
||||||
|
# Y la confirmación de arranque tiene que mirar la RUTA del proceso, no solo su nombre.
|
||||||
|
assert "FromPrefix" in ps1
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_env_file_no_se_desenrolla_con_una_sola_linea():
|
||||||
|
"""
|
||||||
|
`$lines = if (...) { @(Get-Content ...) } else { @() }` desenrolla un array de UN elemento a
|
||||||
|
escalar al asignarlo, así que con un config\\.env de una sola línea `$lines.Count` reventaba
|
||||||
|
bajo Set-StrictMode y la siembra de credenciales abortaba la instalación. El `@()` tiene que
|
||||||
|
envolver el `if` COMPLETO.
|
||||||
|
"""
|
||||||
|
ps1 = (ROOT / "install.ps1").read_text(encoding="utf-8")
|
||||||
|
assert "$lines = @(\n" in ps1, "el @() debe envolver el if completo, no solo el Get-Content"
|
||||||
|
assert "$lines = if (" not in ps1
|
||||||
|
|
||||||
|
|
||||||
def test_el_arranque_automatico_de_windows_pide_headless():
|
def test_el_arranque_automatico_de_windows_pide_headless():
|
||||||
"""
|
"""
|
||||||
La tarea ONSTART corre como SYSTEM, en la sesión 0, donde no hay escritorio interactivo. Es
|
La tarea ONSTART corre como SYSTEM, en la sesión 0, donde no hay escritorio interactivo. Es
|
||||||
|
|||||||
Reference in New Issue
Block a user