diff --git a/BUILD.md b/BUILD.md index b78156c..29717ec 100644 --- a/BUILD.md +++ b/BUILD.md @@ -235,16 +235,27 @@ sabiendas el binario anterior es peor que abortar. Y la confirmación de arranqu 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 ``` -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. No necesita -elevación ni toca la instalación real de la máquina. +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 diff --git a/app/__init__.py b/app/__init__.py index 7bc20bc..99d6410 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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.3" __author__ = "Aduanasoft" diff --git a/scripts/emular-actualizacion-windows.ps1 b/scripts/emular-actualizacion-windows.ps1 index 24bc014..b9ed92a 100644 --- a/scripts/emular-actualizacion-windows.ps1 +++ b/scripts/emular-actualizacion-windows.ps1 @@ -31,16 +31,30 @@ param( [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', + # + # '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 -$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' +# 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) diff --git a/scripts/probar-funciones-install.ps1 b/scripts/probar-funciones-install.ps1 new file mode 100644 index 0000000..471ddc3 --- /dev/null +++ b/scripts/probar-funciones-install.ps1 @@ -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