Compare commits
5 Commits
0a62b7d0aa
...
fix/instal
| Author | SHA1 | Date | |
|---|---|---|---|
| 9214a1feab | |||
| 896528734b | |||
| 9255c6277e | |||
| 399b0483d5 | |||
| 0ac899f531 |
35
BUILD.md
35
BUILD.md
@@ -222,6 +222,41 @@ Reemplazar el binario de un servidor en producción no puede dejarlo sin restaur
|
||||
3. **Se confirma que el agente volvió a arrancar** y, si no, se **revierte** al binario anterior.
|
||||
El respaldo solo se descarta tras esa confirmación.
|
||||
|
||||
### La tarea programada tiene que apuntar a lo que se instaló
|
||||
|
||||
Reemplazar el binario es una operación por **ruta**; `Start-ScheduledTask` es por **nombre** y
|
||||
ejecuta la ruta que la tarea lleva registrada en su acción. Cuando las dos no coinciden —una
|
||||
instalación fuera de la carpeta por omisión, o movida de sitio— se copiaba el binario nuevo en un
|
||||
lado y se arrancaba el viejo del otro: **el run terminaba en verde y el servidor seguía igual**.
|
||||
|
||||
`install.ps1` compara la acción de la tarea con el `-Prefix` y la **reapunta** si difieren,
|
||||
conservando disparador, principal, ajustes y argumentos. Si no puede corregirla, falla: arrancar a
|
||||
sabiendas el binario anterior es peor que abortar. Y la confirmación de arranque mira la **ruta**
|
||||
del proceso, no solo su nombre — un agente viejo que nunca se detuvo satisface igual de bien un
|
||||
`Get-Process -Name CloudRestoreAS`.
|
||||
|
||||
### Rutas de instalación personalizadas
|
||||
|
||||
`C:\Aduanasoft\CloudRestoreAS-win` es el caso a tener presente, y existe en producción: la ruta por
|
||||
omisión `C:\Aduanasoft\CloudRestoreAS` es **prefijo de cadena** de ella. Por eso las rutas se
|
||||
comparan por **igualdad exacta tras normalizar** (comillas, barra final, mayúsculas) y nunca con
|
||||
`startsWith` — que daría por iguales dos instalaciones distintas. En el panel eso vive en un solo
|
||||
sitio, `sameWindowsPath()`; en el instalador, en `Test-SamePath`.
|
||||
|
||||
Para reproducirlo y comprobarlo sin un servidor, desde WSL o Windows:
|
||||
|
||||
```powershell
|
||||
scripts\emular-actualizacion-windows.ps1 -Installer .\install.ps1 # caso roto
|
||||
scripts\emular-actualizacion-windows.ps1 -Installer .\install.ps1 -Escenario alineada # caso normal
|
||||
scripts\emular-actualizacion-windows.ps1 -Installer .\install.ps1 -Escenario sufijo # ...-win
|
||||
scripts\probar-funciones-install.ps1 # casos límite
|
||||
```
|
||||
|
||||
La emulación monta un agente falso (un `.exe` real que se queda vivo), una instalación en una
|
||||
carpeta y una tarea apuntando a otra, corre el instalador y dice si la actualización surtió efecto.
|
||||
`probar-funciones-install.ps1` extrae las funciones del instalador por AST y las ejercita contra una
|
||||
tarea simulada. Ninguno de los dos necesita elevación ni toca la instalación real de la máquina.
|
||||
|
||||
`-UpdateInPlace` además no vuelve a registrar la tarea (así no pisa ajustes hechos sobre ella) y
|
||||
se salta el bootstrap: una segunda instancia purgaría el `Temp\` de la que está viva. Es el modo
|
||||
que usa el PANEL para actualizar.
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,7 +1,62 @@
|
||||
# Changelog
|
||||
|
||||
## [1.1.5] - 2026-07-31
|
||||
|
||||
### La versión ahora es comprobable, y el sello deja de llegar tarde
|
||||
|
||||
Actualizar a 1.1.4 fallaba con *"no escribió `config\.version` tras la actualización"* aunque el
|
||||
binario nuevo sí estuviera instalado y corriendo desde la ruta correcta. La causa era el **orden**
|
||||
dentro de `ensure_runtime_layout()`: el sello de versión se escribía al final, detrás del
|
||||
re-despliegue de las dependencias embebidas (7-Zip y ODBC). Y al cambiar de versión esas deps se
|
||||
re-copian **enteras**, así que el sello quedaba por detrás de esa copia *y* del desempaquetado del
|
||||
onefile de ~254 MB, con el antivirus escaneando cada archivo. El PANEL se rendía esperándolo y daba
|
||||
por fallida una actualización que iba bien.
|
||||
|
||||
- **El sello se escribe lo primero**, en cuanto existen las carpetas. Es además más honesto sobre lo
|
||||
que significa —"qué binario está corriendo"—, que es cierto desde que el proceso arranca. Si el
|
||||
despliegue de deps fallara después, su propio sello (que sigue yendo al final) lo detecta y el
|
||||
siguiente arranque reintenta.
|
||||
- **La versión va en la primera línea del log de arranque.** Permite comprobar qué binario corre de
|
||||
verdad mirando solo `config/logs`, sin depender del sello ni del reporte al panel. Verificar una
|
||||
actualización ya no obliga a creerse lo que diga otro sistema.
|
||||
|
||||
## [1.1.4] - 2026-07-31
|
||||
|
||||
Recoge todo lo que sigue de esta sección y **sustituye a 1.1.3**, cuyo artefacto publicado se generó
|
||||
antes de los arreglos: su `install.ps1` no traía `-UpdateInPlace` y su binario llevaba el `runner.py`
|
||||
que ignoraba `--headless` en Windows. Actualizar con él mataba el agente, cambiaba la tarea
|
||||
programada a SYSTEM y la dejaba sin arrancar, porque Qt no puede crear su plataforma como SYSTEM en
|
||||
la sesión 0 sin `offscreen`.
|
||||
|
||||
Los paquetes genéricos de Gitea son inmutables, así que se quema el número en lugar de reemplazar
|
||||
1.1.3: dos contenidos distintos con la misma versión fue exactamente lo que hizo caro el
|
||||
diagnóstico. Hay que reconstruir los binarios aunque el código ya estuviera arreglado, porque
|
||||
`__version__` va compilado dentro del ejecutable y el PANEL compara el sello `config/.version`
|
||||
contra la versión que creía estar instalando.
|
||||
|
||||
## [Sin publicar]
|
||||
|
||||
### La actualización de Windows decía que funcionó y no cambiaba nada
|
||||
|
||||
Actualizar sobre un servidor con el agente en una carpeta **no estándar** terminaba en verde y
|
||||
dejaba el servidor con la versión anterior. La causa: todo el camino de Windows identificaba al
|
||||
agente por **nombre**, mientras que lo único que se actualiza se identifica por **ruta**.
|
||||
|
||||
- **`install.ps1` alinea la tarea programada con el binario instalado.** `Start-ScheduledTask`
|
||||
ejecuta la ruta registrada en la acción de la tarea, no el `-Prefix`: si difieren, se copiaba el
|
||||
binario nuevo en un sitio y se arrancaba el viejo del otro. Ahora se reapunta la tarea
|
||||
conservando disparador, principal, ajustes y argumentos; si no se puede corregir, **falla**.
|
||||
- **La confirmación de arranque mira la ruta del proceso**, no solo su nombre. Un agente viejo que
|
||||
nunca se detuvo satisfacía igual de bien un `Get-Process -Name CloudRestoreAS`. Si la ruta no es
|
||||
legible —un proceso de SYSTEM no la expone a una cuenta sin elevación— se acepta por nombre y se
|
||||
avisa, en vez de revertir una actualización correcta por falta de información.
|
||||
- **Nuevo `scripts/emular-actualizacion-windows.ps1`**: reproduce el escenario completo con un
|
||||
agente falso, sin elevación y sin tocar la instalación real de la máquina. Es lo que destapó este
|
||||
defecto y el siguiente.
|
||||
- **Corregido `Merge-EnvFile` con un `config\.env` de una sola línea.** Al asignar la salida de un
|
||||
`if`, PowerShell desenrolla un array de un elemento a escalar, así que `$lines.Count` reventaba
|
||||
bajo `Set-StrictMode` y la siembra de credenciales abortaba la instalación.
|
||||
|
||||
### Instalación y actualización desatendidas en Windows
|
||||
|
||||
El instalador de Windows nunca recibió la maquinaria de actualización segura que sí tiene
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# package-release.sh (nombres de artefacto y release.json) y el reporte al PANEL.
|
||||
# Formato obligatorio: puntos y números, monotónico creciente — el PANEL compara
|
||||
# versiones como tuplas de enteros para detectar si hay una más nueva.
|
||||
__version__ = "1.1.2"
|
||||
__version__ = "1.1.5"
|
||||
__author__ = "Aduanasoft"
|
||||
|
||||
@@ -175,16 +175,30 @@ def ensure_runtime_layout() -> None:
|
||||
):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_write_env_if_missing()
|
||||
|
||||
# Los sellos se ubican bajo el CONFIG_DIR vigente (no la ruta absoluta precalculada)
|
||||
# para que respeten el monkeypatch de las pruebas y no escriban en el config/ real.
|
||||
deps_stamp_path = CONFIG_DIR / BUNDLED_STAMP_PATH.name
|
||||
version_stamp_path = CONFIG_DIR / VERSION_PATH.name
|
||||
|
||||
# Las deps embebidas se re-despliegan cuando el build trae otras versiones. El sello
|
||||
# se escribe DESPUÉS de copiar: si la copia falla a medias, el próximo arranque lo
|
||||
# reintenta en lugar de quedar marcado como al día.
|
||||
# Sello de versión: lo lee el instalador remoto del PANEL por SFTP para verificar el
|
||||
# despliegue (en Windows el .exe es console=False y no tiene stdout confiable).
|
||||
#
|
||||
# Se escribe LO PRIMERO, en cuanto existen las carpetas. Antes iba al final, detrás del
|
||||
# re-despliegue de las deps embebidas, y eso lo hacía inservible justo cuando más importa: al
|
||||
# cambiar de versión las deps se re-copian enteras, así que el sello quedaba por detrás de esa
|
||||
# copia y del desempaquetado del onefile. El PANEL se rendía esperándolo y daba por fallida una
|
||||
# actualización que en realidad iba bien.
|
||||
#
|
||||
# Escribirlo aquí es además más honesto sobre lo que el sello significa: "qué binario está
|
||||
# corriendo", que es cierto desde que el proceso arranca. Si el despliegue de deps fallara
|
||||
# después, el sello de DEPS —que sí va al final— lo detecta y el próximo arranque reintenta.
|
||||
_write_stamp(version_stamp_path, __version__)
|
||||
|
||||
_write_env_if_missing()
|
||||
|
||||
# Las deps embebidas se re-despliegan cuando el build trae otras versiones. Su sello se
|
||||
# escribe DESPUÉS de copiar: si la copia falla a medias, el próximo arranque lo reintenta
|
||||
# en lugar de quedar marcado como al día.
|
||||
deps_stamp = _bundled_deps_stamp()
|
||||
refresh_deps = _read_stamp(deps_stamp_path) != deps_stamp
|
||||
_copy_bundled_tree(BUNDLED_SOURCE_7ZIP, SEVEN_ZIP_DIR, refresh_deps)
|
||||
@@ -193,10 +207,6 @@ def ensure_runtime_layout() -> None:
|
||||
if refresh_deps:
|
||||
_write_stamp(deps_stamp_path, deps_stamp)
|
||||
|
||||
# Sello de versión: lo lee el instalador remoto del PANEL por SFTP para verificar el
|
||||
# despliegue (en Windows el .exe es console=False y no tiene stdout confiable).
|
||||
_write_stamp(version_stamp_path, __version__)
|
||||
|
||||
from ..constants import DB_PATH
|
||||
|
||||
if not DB_PATH.exists():
|
||||
|
||||
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)"
|
||||
}
|
||||
|
||||
@@ -193,8 +193,14 @@ def _run() -> int:
|
||||
panel_ok = is_panel_configured()
|
||||
start_engine = args.start_engine or launch.start_engine
|
||||
|
||||
from app import __version__
|
||||
|
||||
app_logger.info("=" * 80)
|
||||
app_logger.info("CloudRestoreAS - Iniciando aplicación")
|
||||
# La versión va en la PRIMERA línea del arranque a propósito: es la forma de comprobar qué
|
||||
# binario está corriendo de verdad sin depender del sello ni del reporte al panel, mirando
|
||||
# solo config/logs. Sin esto, verificar una actualización obligaba a creerse lo que dijera
|
||||
# otro sistema.
|
||||
app_logger.info(f"CloudRestoreAS {__version__} - Iniciando aplicación")
|
||||
app_logger.info(f"Directorio app: {ROOT_DIR}")
|
||||
if qt_platform == "offscreen":
|
||||
app_logger.info("Modo: sin display → plataforma Qt 'offscreen' (motor headless)")
|
||||
|
||||
227
scripts/emular-actualizacion-windows.ps1
Normal file
227
scripts/emular-actualizacion-windows.ps1
Normal file
@@ -0,0 +1,227 @@
|
||||
<#
|
||||
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
|
||||
111
scripts/probar-funciones-install.ps1
Normal file
111
scripts/probar-funciones-install.ps1
Normal file
@@ -0,0 +1,111 @@
|
||||
<#
|
||||
Pruebas de las funciones de install.ps1, sin instalar nada.
|
||||
|
||||
Las funciones se extraen del script por AST y se ejercitan contra una tarea programada simulada.
|
||||
Así se prueba el COMPORTAMIENTO —no solo que el archivo parsee— sin necesitar elevación ni
|
||||
registrar una tarea 'CloudRestoreAS' de verdad en la máquina.
|
||||
|
||||
Complementa a emular-actualizacion-windows.ps1: ese cubre el flujo completo, este cubre los casos
|
||||
límite de la comparación de rutas, que es donde se esconden los fallos silenciosos.
|
||||
|
||||
Uso: .\probar-funciones-install.ps1
|
||||
#>
|
||||
param([string]$Installer = (Join-Path (Split-Path -Parent $PSScriptRoot) 'install.ps1'))
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$tokens = $null; $errores = $null
|
||||
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
|
||||
(Resolve-Path $Installer).Path, [ref]$tokens, [ref]$errores)
|
||||
if ($errores -and $errores.Count -gt 0) {
|
||||
Write-Host "install.ps1 no parsea: $($errores.Count) error(es)" -ForegroundColor Red
|
||||
$errores | ForEach-Object { Write-Host (" L" + $_.Extent.StartLineNumber + ": " + $_.Message) }
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Variables de ámbito de script que usan las funciones.
|
||||
$Prefix = 'C:\Aduanasoft\CloudRestoreAS-win'
|
||||
$dest = Join-Path $Prefix 'CloudRestoreAS.exe'
|
||||
$TaskName = 'CloudRestoreAS'
|
||||
$ProcName = 'CloudRestoreAS'
|
||||
|
||||
$queremos = @('Write-Step', 'Write-Ok', 'Write-Warn', 'Get-NormalizedPath', 'Test-SamePath',
|
||||
'Get-AgentTaskExecute', 'Repair-AgentTaskPath', 'Sync-AgentTaskPath')
|
||||
foreach ($f in $ast.FindAll({ param($n)
|
||||
$n -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) {
|
||||
if ($queremos -contains $f.Name) { . ([scriptblock]::Create($f.Extent.Text)) }
|
||||
}
|
||||
|
||||
$fallos = 0
|
||||
function Assert($cond, $msg) {
|
||||
if ($cond) { Write-Host " OK $msg" } else { Write-Host " FALLA $msg" -ForegroundColor Red; $script:fallos++ }
|
||||
}
|
||||
|
||||
Write-Host '=== Test-SamePath: rutas donde una es prefijo de cadena de la otra ==='
|
||||
# El caso que existe en produccion. Un `-like "$a*"` o un StartsWith aqui daria por iguales dos
|
||||
# instalaciones distintas, no se corregiria la tarea, y la actualizacion no surtiria efecto.
|
||||
$porOmision = 'C:\Aduanasoft\CloudRestoreAS'
|
||||
$personalizada = 'C:\Aduanasoft\CloudRestoreAS-win'
|
||||
Assert (-not (Test-SamePath $porOmision $personalizada)) 'no confunde la ruta por omision con la -win'
|
||||
Assert (-not (Test-SamePath $personalizada $porOmision)) 'ni al reves'
|
||||
Assert (-not (Test-SamePath "$porOmision\CloudRestoreAS.exe" "$personalizada\CloudRestoreAS.exe")) `
|
||||
'tampoco con el ejecutable completo'
|
||||
Assert (Test-SamePath $personalizada $personalizada) 'y la personalizada sigue siendo igual a si misma'
|
||||
|
||||
Write-Host '=== Test-SamePath: normalizacion ==='
|
||||
Assert (Test-SamePath "`"$personalizada`"" $personalizada) 'tolera comillas (la tarea las guarda asi)'
|
||||
Assert (Test-SamePath $personalizada.ToUpper() $personalizada) 'NTFS no distingue mayusculas'
|
||||
Assert (Test-SamePath "$personalizada\" $personalizada) 'ignora la barra final'
|
||||
Assert (Test-SamePath "$Prefix\..\CloudRestoreAS-win\x.exe" "$Prefix\x.exe") 'normaliza .. en la ruta'
|
||||
Assert (-not (Test-SamePath '' $personalizada)) 'una ruta vacia no es igual a nada'
|
||||
|
||||
Write-Host '=== Get-AgentTaskExecute ==='
|
||||
function Get-AgentTask { [pscustomobject]@{ Actions = @(
|
||||
[pscustomobject]@{ Execute = "`"$porOmision\CloudRestoreAS.exe`""; Arguments = '--start-engine --headless' }) } }
|
||||
Assert ((Get-AgentTaskExecute) -eq "$porOmision\CloudRestoreAS.exe") 'devuelve la ruta sin comillas'
|
||||
|
||||
# Una accion ComHandler no tiene .Execute: con StrictMode, tocarla a ciegas seria un error.
|
||||
function Get-AgentTask { [pscustomobject]@{ Actions = @(
|
||||
[pscustomobject]@{ ClassId = '{guid}' },
|
||||
[pscustomobject]@{ Execute = "$porOmision\CloudRestoreAS.exe"; Arguments = '' }) } }
|
||||
Assert ((Get-AgentTaskExecute) -eq "$porOmision\CloudRestoreAS.exe") 'se salta acciones sin Execute'
|
||||
|
||||
function Get-AgentTask { $null }
|
||||
Assert ((Get-AgentTaskExecute) -eq '') 'sin tarea devuelve cadena vacia'
|
||||
|
||||
Write-Host '=== Sync-AgentTaskPath: el fallo silencioso ==='
|
||||
$script:reapuntadoA = $null
|
||||
$script:argsPreservados = $null
|
||||
function Set-ScheduledTask { param($TaskName, $Action)
|
||||
$ejec = @($Action | Where-Object { $_.PSObject.Properties['Execute'] -and $_.Execute })
|
||||
$script:reapuntadoA = $ejec[0].Execute
|
||||
$script:argsPreservados = $ejec[0].Arguments
|
||||
return $true }
|
||||
function New-ScheduledTaskAction { param($Execute, $Argument, $WorkingDirectory)
|
||||
[pscustomobject]@{ Execute = $Execute; Arguments = $Argument } }
|
||||
|
||||
function Get-AgentTask { [pscustomobject]@{ Actions = @(
|
||||
[pscustomobject]@{ Execute = "$porOmision\CloudRestoreAS.exe"; Arguments = '--start-engine --headless' }) } }
|
||||
Sync-AgentTaskPath
|
||||
Assert ($script:reapuntadoA -eq $dest) "reapunta de la carpeta por omision a $dest"
|
||||
Assert ($script:argsPreservados -eq '--start-engine --headless') 'conserva los argumentos originales'
|
||||
|
||||
$script:reapuntadoA = $null
|
||||
function Get-AgentTask { [pscustomobject]@{ Actions = @(
|
||||
[pscustomobject]@{ Execute = $dest; Arguments = '--start-engine --headless' }) } }
|
||||
Sync-AgentTaskPath
|
||||
Assert ($null -eq $script:reapuntadoA) 'una tarea ya alineada no se toca'
|
||||
|
||||
# Si no se puede corregir tiene que FALLAR, no arrancar el binario viejo en silencio.
|
||||
function Set-ScheduledTask { param($TaskName, $Action) throw 'Acceso denegado' }
|
||||
function Get-AgentTask { [pscustomobject]@{ Actions = @(
|
||||
[pscustomobject]@{ Execute = "$porOmision\CloudRestoreAS.exe"; Arguments = '' }) } }
|
||||
$lanzo = $false; $msg = ''
|
||||
try { Sync-AgentTaskPath } catch { $lanzo = $true; $msg = $_.Exception.Message }
|
||||
Assert $lanzo 'sin permiso para corregir -> lanza en vez de seguir'
|
||||
Assert ($lanzo -and $msg -match 'sin efecto') 'el mensaje explica que la actualizacion no surtiria efecto'
|
||||
|
||||
Write-Host ''
|
||||
if ($fallos -gt 0) { Write-Host "FALLOS: $fallos" -ForegroundColor Red; exit 1 }
|
||||
Write-Host 'TODO OK' -ForegroundColor Green
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.config import bootstrap
|
||||
from app.config.bootstrap import ensure_runtime_layout
|
||||
from app.config.env_loader import (
|
||||
apply_env_overrides,
|
||||
@@ -112,6 +113,41 @@ def test_bootstrap_escribe_sello_de_version(tmp_path: Path, monkeypatch: pytest.
|
||||
assert (config_dir / ".version").read_text(encoding="utf-8").strip() == __version__
|
||||
|
||||
|
||||
def test_sello_de_version_se_escribe_antes_de_copiar_las_deps(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""
|
||||
El sello tiene que existir ANTES de re-desplegar 7-Zip y ODBC.
|
||||
|
||||
Iba al final, y eso lo volvía inservible justo cuando más importa: al cambiar de versión las
|
||||
deps embebidas se re-copian enteras, así que el sello quedaba por detrás de esa copia y del
|
||||
desempaquetado del onefile de ~254 MB con el antivirus escaneando. El PANEL se rendía
|
||||
esperándolo y reportaba como fallida una actualización que en realidad iba bien.
|
||||
"""
|
||||
from app import __version__
|
||||
|
||||
config_dir = _redirect_bootstrap(tmp_path, monkeypatch)
|
||||
|
||||
# Se observa el estado del sello EN EL MOMENTO en que empieza la copia de deps.
|
||||
visto: dict[str, str | None] = {}
|
||||
real = bootstrap._copy_bundled_tree
|
||||
|
||||
def espia(*args, **kwargs):
|
||||
sello = config_dir / ".version"
|
||||
visto.setdefault(
|
||||
"al_copiar",
|
||||
sello.read_text(encoding="utf-8").strip() if sello.exists() else None,
|
||||
)
|
||||
return real(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr("app.config.bootstrap._copy_bundled_tree", espia)
|
||||
ensure_runtime_layout()
|
||||
|
||||
assert visto["al_copiar"] == __version__, (
|
||||
"el sello de versión debe existir antes de empezar a copiar las deps embebidas"
|
||||
)
|
||||
|
||||
|
||||
def test_bootstrap_redespliega_deps_cuando_cambia_el_manifiesto(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
|
||||
@@ -138,6 +138,38 @@ def test_install_ps1_respalda_y_revierte():
|
||||
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():
|
||||
"""
|
||||
La tarea ONSTART corre como SYSTEM, en la sesión 0, donde no hay escritorio interactivo. Es
|
||||
|
||||
Reference in New Issue
Block a user