fix(cras-install): realinear la tarea de Windows desde el panel, y decir por que fallo

La actualizacion ya fallaba ruidosamente en vez de mentir, pero el arreglo no
podia llegar al servidor: el install.ps1 que se ejecuta viaja DENTRO del zip, y
el artefacto 1.1.3 se construyo antes de que el instalador aprendiera a
realinear la tarea. Verificado sobre el zip: trae -UpdateInPlace y cero
Sync-AgentTaskPath. Un instalador ya publicado no se arregla hacia atras.

- alignWindowsTask realinea la accion de la tarea por SSH tras instalar el
  binario, conservando disparador, principal, ajustes y argumentos, y asienta la
  ruta ANTERIOR y la nueva para que el cambio sea reversible si la ruta declarada
  estuviera mal. Si no se puede corregir, aborta con 409 nombrando ambas: seguir
  significaria arrancar a sabiendas el binario viejo. Funciona con cualquier
  artefacto ya distribuido.
- restartWindowsAgent rearranca y espera a que corra el binario del prefijo, no
  cualquier proceso con ese nombre. Vive en cras-install y no en
  cras-agent-control porque este es el modulo de abajo; al reves habria un ciclo.

Y el diagnostico deja de ser una tarea para el operador. El error de sello
ausente decia "revisa a que binario apunta el arranque automatico" cuando el
panel YA lo sabe, y la bitacora del run donde si estaba no la encontraba nadie
("no se donde verlo"). Ahora se sondea tarea y proceso ANTES de evaluar el sello
y el mensaje nombra a que apunta la tarea, desde donde corre el proceso, y la
cola de CloudRestoreAS-crash.log — que runner.py escribe precisamente para esto
y que nunca se leia. El banner de error apunta al run por numero.

startOnWindows deja de esperar por nombre y usa la misma sonda de ruta, para que
Verificar no pueda contradecir al instalador.

PowerShell generado validado con PowerShell real: los seis scripts parsean, y el
de realineacion ejercitado contra una tarea simulada reapunta la accion Exec,
conserva los argumentos y respeta las acciones que no son Exec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 10:57:16 -06:00
parent 9a4124d636
commit 3572aa974b
4 changed files with 370 additions and 28 deletions

View File

@@ -16,6 +16,7 @@ import { getRestoreTargetSsh } from './controldesk-pg';
import {
execRemote,
probeLinuxElevation,
probeWindowsAgentProcess,
probeWindowsElevation,
psEncoded,
shQuote,
@@ -23,7 +24,11 @@ import {
} from './cras-install';
import { probeRemoteSystem } from './cras-verify';
import { listCrasTargetInventory } from './cras-releases';
import { effectiveInstallPath, DEFAULT_INSTALL_PATHS } from '$lib/cras-version';
import {
effectiveInstallPath,
DEFAULT_INSTALL_PATHS,
type CrasPlatform
} from '$lib/cras-version';
import type { ApiErrorStatus } from './api-error';
import { logger } from './logger';
@@ -65,7 +70,7 @@ async function waitAlive(check: () => Promise<boolean>): Promise<boolean> {
}
}
async function startOnWindows(sftp: SftpClient): Promise<StartAgentOutcome> {
async function startOnWindows(sftp: SftpClient, prefix: string): Promise<StartAgentOutcome> {
const privileged = await probeWindowsElevation(sftp);
if (privileged.elevation !== 'admin') {
throw new AgentControlError(
@@ -96,38 +101,51 @@ async function startOnWindows(sftp: SftpClient): Promise<StartAgentOutcome> {
);
}
// Se espera con la MISMA sonda que usa el instalador, que mira la ruta del ejecutable y no solo
// el nombre. Con la comprobación por nombre, un agente viejo corriendo desde otra carpeta hacía
// pasar por bueno un arranque que no ocurrió.
let ultima = await probeWindowsAgentProcess(sftp, prefix);
const alive = await waitAlive(async () => {
const proc = await execRemote(
sftp,
psEncoded(
"if (Get-Process -Name 'CloudRestoreAS' -ErrorAction SilentlyContinue) " +
'{"si"} else {"no"}'
)
);
return proc.stdout.trim() === 'si';
ultima = await probeWindowsAgentProcess(sftp, prefix);
// `paths` vacío con el proceso vivo es "no pude ver desde dónde corre" (un proceso de SYSTEM
// no expone su ruta sin elevación), y eso se acepta: no se afirma que sea de otra carpeta.
return ultima.fromPrefix || (ultima.running && ultima.paths.length === 0);
});
return alive
? { ok: true, detail: 'Tarea CloudRestoreAS arrancada y proceso en ejecución.' }
: {
ok: false,
detail:
'Se pidió el arranque de la tarea, pero el proceso no apareció. Revisa ' +
'config\\logs en el servidor: el agente está fallando al iniciar.'
};
if (alive) {
return {
ok: true,
detail:
'Tarea CloudRestoreAS arrancada y proceso en ejecución' +
(ultima.paths.length ? ` desde ${ultima.paths.join(', ')}.` : '.')
};
}
return {
ok: false,
detail: ultima.running
? `Hay un agente corriendo desde ${ultima.paths.join(', ')}, no desde ${prefix}: el ` +
'arranque automático apunta a otra instalación.'
: 'Se pidió el arranque de la tarea, pero el proceso no apareció. Revisa ' +
`${prefix}\\config\\logs en el servidor: el agente está fallando al iniciar.`
};
}
/**
* Ruta donde vive el agente, para poder reconocer su proceso.
*
* El ancla `^` del patrón no es opcional: sin ella, el `sh -c` que corre el propio `pgrep` lleva
* la ruta en su línea de comandos y haría match consigo mismo, reportando vivo un agente que
* nunca arrancó. Es el mismo motivo por el que install.sh ancla su patrón.
* Hace falta en las dos plataformas y por el mismo motivo: sin ella solo se puede preguntar por el
* NOMBRE del proceso, que no distingue el agente que nos interesa de otro corriendo desde otra
* carpeta. En Linux, además, el ancla `^` del patrón de `pgrep` no es opcional: sin ella el `sh -c`
* que corre el propio pgrep lleva la ruta en su línea de comandos y haría match consigo mismo,
* reportando vivo un agente que nunca arrancó.
*/
async function resolveLinuxPrefix(restoreTargetId: number): Promise<string> {
async function resolvePrefix(restoreTargetId: number, platform: CrasPlatform): Promise<string> {
const inventory = await listCrasTargetInventory();
const row = inventory.find((t) => t.restore_target_id === restoreTargetId);
return effectiveInstallPath(row?.reported_install_path ?? null, 'linux') ?? DEFAULT_INSTALL_PATHS.linux;
return (
effectiveInstallPath(row?.reported_install_path ?? null, platform) ??
DEFAULT_INSTALL_PATHS[platform]
);
}
async function startOnLinux(
@@ -203,11 +221,11 @@ export async function startCrasAgent(restoreTargetId: number): Promise<StartAgen
const system = await probeRemoteSystem(sftp);
if (system.verdict === 'windows') {
return await startOnWindows(sftp);
return await startOnWindows(sftp, await resolvePrefix(restoreTargetId, 'windows'));
}
if (system.verdict === 'linux') {
const privileged = await probeLinuxElevation(sftp, target.ssh_password);
const prefix = await resolveLinuxPrefix(restoreTargetId);
const prefix = await resolvePrefix(restoreTargetId, 'linux');
return await startOnLinux(sftp, privileged, prefix);
}
throw new AgentControlError(