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
|
||||
}
|
||||
|
||||
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 {
|
||||
<#
|
||||
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
|
||||
'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.
|
||||
|
||||
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) {
|
||||
Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
@@ -158,15 +262,59 @@ function Start-Agent {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
$foreign = @()
|
||||
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
|
||||
}
|
||||
# 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
|
||||
}
|
||||
|
||||
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 {
|
||||
<#
|
||||
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
|
||||
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\ -----------------------------------------------------------
|
||||
# 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.
|
||||
@@ -304,9 +457,13 @@ if (Test-Path -LiteralPath $envPath) {
|
||||
function Merge-EnvFile {
|
||||
param([string]$Target, [hashtable]$Values)
|
||||
|
||||
$lines = if (Test-Path -LiteralPath $Target) {
|
||||
@(Get-Content -LiteralPath $Target -Encoding UTF8)
|
||||
} else { @() }
|
||||
# El @() envuelve el `if` COMPLETO, no solo el Get-Content. Al asignar la salida de un `if`,
|
||||
# PowerShell desenrolla un array de un solo elemento a escalar: con un config\.env de UNA línea
|
||||
# $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) {
|
||||
$line = "$key=$($Values[$key])"
|
||||
@@ -417,7 +574,16 @@ switch ($Mode) {
|
||||
$shouldBeRunning = ($Mode -eq 'service') -or $wasRunning -or $taskExisted
|
||||
|
||||
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
|
||||
Write-Step "Rearrancando el agente ($how)"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user