diff --git a/src/lib/server/cras-install.test.ts b/src/lib/server/cras-install.test.ts index 1f412f9..9a1f17c 100644 --- a/src/lib/server/cras-install.test.ts +++ b/src/lib/server/cras-install.test.ts @@ -29,9 +29,11 @@ import { probeInPlaceUpdate, probeLinuxElevation, alignWindowsTask, + normalizeWindowsPath, probeWindowsAgentProcess, probeWindowsElevation, probeWindowsTask, + sameWindowsPath, psEncoded, shQuote, verifyWindowsDeployment, @@ -429,11 +431,71 @@ describe('probeWindowsElevation', () => { * que lo único que se actualiza se identifica por RUTA. En cuanto las dos no coincidían se * reemplazaba un binario y se arrancaba otro, y ni el instalador ni el panel se enteraban. */ +/** + * Rutas de instalación personalizadas. + * + * `C:\Aduanasoft\CloudRestoreAS-win` es el peor caso posible y existe en producción: la ruta por + * omisión `C:\Aduanasoft\CloudRestoreAS` es **prefijo de cadena** de ella. Cualquier comparación + * hecha con `startsWith` daría por iguales dos instalaciones distintas, y el resultado sería + * exactamente el fallo que se estuvo persiguiendo: actualizar una carpeta y arrancar la otra. + * + * Las pruebas de más abajo usaban nombres sin relación entre sí (`C:\Declarada` contra `C:\Otra`), + * así que un `startsWith` mal puesto las pasaba todas. + */ +describe('comparación de rutas de Windows', () => { + const DEFECTO = 'C:\\Aduanasoft\\CloudRestoreAS'; + const PERSONALIZADA = 'C:\\Aduanasoft\\CloudRestoreAS-win'; + + it('NO confunde la ruta por omisión con una que la tiene como prefijo', () => { + expect(sameWindowsPath(DEFECTO, PERSONALIZADA)).toBe(false); + // Y en el otro sentido: da igual cuál sea la instalada. + expect(sameWindowsPath(PERSONALIZADA, DEFECTO)).toBe(false); + expect( + sameWindowsPath(`${DEFECTO}\\CloudRestoreAS.exe`, `${PERSONALIZADA}\\CloudRestoreAS.exe`) + ).toBe(false); + }); + + it('normaliza comillas, barras finales y mayúsculas', () => { + // La acción de una tarea guarda la ruta entrecomillada; una ruta capturada a mano suele + // traer barra final; y NTFS no distingue caso. + expect(sameWindowsPath(`"${PERSONALIZADA}"`, PERSONALIZADA)).toBe(true); + expect(sameWindowsPath(`${PERSONALIZADA}\\`, PERSONALIZADA)).toBe(true); + expect(sameWindowsPath(PERSONALIZADA.toUpperCase(), PERSONALIZADA)).toBe(true); + expect(normalizeWindowsPath(' ""C:\\X\\"" ')).toBe('c:\\x'); + }); + + it('una ruta vacía no es igual a nada, ni a otra vacía', () => { + // Si lo fuera, un `execute` sin leer pasaría por "ya está alineada" y no se corregiría nada. + expect(sameWindowsPath('', '')).toBe(false); + expect(sameWindowsPath('', PERSONALIZADA)).toBe(false); + }); +}); + describe('sondas de ruta en Windows', () => { + const DEFECTO = 'C:\\Aduanasoft\\CloudRestoreAS'; + const PERSONALIZADA = 'C:\\Aduanasoft\\CloudRestoreAS-win'; + function psFake(stdout: string) { return fakeSftp(() => ({ code: 0, stdout })); } + it('el proceso de la ruta por omisión no cuenta como el de la personalizada', async () => { + const { sftp } = psFake(`corriendo|${DEFECTO}\\CloudRestoreAS.exe`); + const r = await probeWindowsAgentProcess(sftp as never, PERSONALIZADA); + expect(r.running).toBe(true); + expect(r.fromPrefix).toBe(false); + }); + + it('ni al contrario: el de la personalizada no cuenta como el de la de omisión', async () => { + const { sftp } = psFake(`corriendo|${PERSONALIZADA}\\CloudRestoreAS.exe`); + expect((await probeWindowsAgentProcess(sftp as never, DEFECTO)).fromPrefix).toBe(false); + }); + + it('y el de la personalizada sí cuenta cuando esa es la instalación', async () => { + const { sftp } = psFake(`corriendo|${PERSONALIZADA}\\CloudRestoreAS.exe`); + expect((await probeWindowsAgentProcess(sftp as never, PERSONALIZADA)).fromPrefix).toBe(true); + }); + it('probeWindowsTask saca la ruta y la cuenta de la tarea', async () => { const { sftp } = psFake('si|C:\\Otra\\CloudRestoreAS.exe|SYSTEM'); expect(await probeWindowsTask(sftp as never)).toEqual({ @@ -533,6 +595,20 @@ describe('alignWindowsTask', () => { expect(commands).toHaveLength(0); }); + it('reapunta cuando la tarea usa la ruta por omisión y la instalación es la personalizada', async () => { + // El servidor real: instalado en ...-win, tarea apuntando a la carpeta por omisión. Con una + // comparación por prefijo esto se daría por alineado y no se corregiría nada. + const { sftp, commands } = psFake('ok'); + const cambio = await alignWindowsTask( + sftp as never, + 1, + 'C:\\Aduanasoft\\CloudRestoreAS\\CloudRestoreAS.exe', + 'C:\\Aduanasoft\\CloudRestoreAS-win' + ); + expect(cambio).toBe(true); + expect(commands).toHaveLength(1); + }); + it('si no se puede corregir, falla nombrando las dos rutas', async () => { const { sftp } = psFake('error|Acceso denegado'); await expect( diff --git a/src/lib/server/cras-install.ts b/src/lib/server/cras-install.ts index d9cf6ba..6b39608 100644 --- a/src/lib/server/cras-install.ts +++ b/src/lib/server/cras-install.ts @@ -1100,6 +1100,45 @@ export async function probeWindowsElevation(sftp: SftpClient): Promise p.trim()).filter(Boolean); - const wanted = `${prefix.replace(/\\+$/, '')}\\CloudRestoreAS.exe`.toLowerCase(); + const wanted = windowsAgentExe(prefix); return { running: true, paths, - fromPrefix: paths.some((p) => p.replace(/^"|"$/g, '').toLowerCase() === wanted) + fromPrefix: paths.some((p) => sameWindowsPath(p, wanted)) }; } @@ -1190,15 +1229,6 @@ export async function probeWindowsTask(sftp: SftpClient): Promise { if (!taskExecute) return false; - const wanted = `${targetPath.replace(/\\+$/, '')}\\CloudRestoreAS.exe`; - if (normalizeWindowsPath(taskExecute) === normalizeWindowsPath(wanted)) return false; + const wanted = windowsAgentExe(targetPath); + if (sameWindowsPath(taskExecute, wanted)) return false; // `$argumentos` y no `$args`: `$args` es una variable automática de PowerShell y asignarla // dentro de un bloque de script es un error. @@ -1279,7 +1309,7 @@ export async function restartWindowsAgent( prefix: string, timeoutMs = 60_000 ): Promise { - const exe = `${prefix.replace(/\\+$/, '')}\\CloudRestoreAS.exe`; + const exe = windowsAgentExe(prefix); await execRemote( sftp, psEncoded( diff --git a/src/lib/server/cras-verify.ts b/src/lib/server/cras-verify.ts index 6e095cc..2e439f1 100644 --- a/src/lib/server/cras-verify.ts +++ b/src/lib/server/cras-verify.ts @@ -24,8 +24,10 @@ import { probeWindowsTask, psEncoded, readUnitProps, + sameWindowsPath, shQuote, - unitUserOrRoot + unitUserOrRoot, + windowsAgentExe } from './cras-install'; import { listCrasTargetInventory } from './cras-releases'; import { DEFAULT_INSTALL_PATHS, effectiveInstallPath, type CrasPlatform } from '$lib/cras-version'; @@ -827,9 +829,10 @@ async function inspectWindows( // apunta a otra carpeta hace que actualizar no surta efecto sin que nada falle: se reemplaza el // binario de una y se arranca el de otra. Aquí se ve, en vez de descubrirse después. const taskProbe = await probeWindowsTask(sftp); - const esperado = `${prefix.replace(/\\+$/, '')}\\CloudRestoreAS.exe`; - const taskAlineada = - !taskProbe.execute || taskProbe.execute.toLowerCase() === esperado.toLowerCase(); + const esperado = windowsAgentExe(prefix); + // Misma comparación que usa el instalador. Tenerla duplicada aquí con otro criterio dejaba a las + // dos pantallas capaces de discrepar sobre el mismo servidor. + const taskAlineada = !taskProbe.execute || sameWindowsPath(taskProbe.execute, esperado); if (taskProbe.exists) { checks.push( check( diff --git a/src/routes/versiones-cras/+page.svelte b/src/routes/versiones-cras/+page.svelte index 3e2809a..66f0937 100644 --- a/src/routes/versiones-cras/+page.svelte +++ b/src/routes/versiones-cras/+page.svelte @@ -5,7 +5,7 @@ import type { SubmitFunction } from '@sveltejs/kit'; import AppShell from '$lib/components/AppShell.svelte'; import Spinner from '$lib/components/Spinner.svelte'; - import { effectiveInstallPath, platformLabel } from '$lib/cras-version'; + import { effectiveInstallPath, isDefaultInstallPath, platformLabel } from '$lib/cras-version'; import { INSTALL_STEP_LABEL, currentPhaseLabel, @@ -1346,6 +1346,29 @@ {/if} + +
+

+ Se instalará en {targetInstallPath} +

+

+ {#if installTarget?.reported_install_path} + Ruta registrada para {installTarget.name}. Se cambia con el lápiz que está + junto al nombre del servidor, en la tabla. + {:else if installTarget?.platform && isDefaultInstallPath(targetInstallPath, installTarget.platform)} + Ruta por omisión: este servidor no tiene ninguna registrada. Si el agente + vive en otra carpeta, captúrala primero con el lápiz de la tabla — instalar + aquí crearía una segunda instalación. + {:else} + Sin ruta registrada para este servidor. + {/if} +

+
+