Compare commits

...

2 Commits

Author SHA1 Message Date
dd483da485 fix(cras-install): sonda sin .NET y registrar el LanguageMode del destino
La sonda que decide si el instalador del artefacto soporta -UpdateInPlace usaba
[regex]::IsMatch, que es una llamada estatica y por tanto de lo primero que
bloquea Constrained Language Mode — el mismo motivo por el que fallo antes
(Get-Command …).Parameters en ese servidor y no en desarrollo. Habria degradado a
'desconocido' en vez de responder: no rompe, pero tampoco sirve. Pasa a
Select-String, un cmdlet puro sin resolucion de tipos.

`-ErrorAction Stop` NO es opcional, y esto lo destapo probar la ruta de FALLO con
PowerShell real, no las pruebas unitarias: Select-String emite un error NO
TERMINANTE cuando no puede leer el archivo, asi que sin el, el catch no se
dispara, -Quiet devuelve falso y un fallo de lectura se reporta como 'no' —
exactamente el defecto que esta funcion existe para eliminar.

Verificado contra PowerShell real los cuatro casos: instalador nuevo -> si,
instalador del zip 1.1.2 publicado -> no, archivo inexistente -> desconocido,
fallo de lectura -> desconocido.

Se anade probeWindowsLanguageMode y se registra SIEMPRE en el paso
precondiciones, con un aviso aparte cuando no es FullLanguage. Es el dato que
faltaba para dejar de diagnosticar a ciegas: install.ps1 todavia usa cinco
construcciones .NET que en modo restringido no funcionarian, y sin verlo en la
bitacora cada fallo raro en Windows empieza con una ronda de suposiciones.

La prueba nueva prohibe cualquier `[Tipo]::` en el script de la sonda. Se ha
caido dos veces en lo mismo; que lo impida una prueba y no la memoria.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 14:41:39 -06:00
bc1fcd8508 fix(cras-install): no confundir "no pude averiguarlo" con "no lo soporta"
El panel rechazaba actualizar con 1.1.4 diciendo que su install.ps1 no declara
-UpdateInPlace. Era falso: el zip trae UpdateInPlace 8 veces y Sync-AgentTaskPath
3. El bloqueo lo causaba la sonda.

    const supportsInPlace = supportsProbe.stdout.trim() === 'si';

Cualquier fallo —una excepcion, un codigo de salida distinto de cero, stdout
vacio— colapsaba a false. Mientras eso solo elegia entre -UpdateInPlace y
-Service, un falso negativo degradaba la instalacion; al convertirlo en un 409
duro paso a ser un candado permanente sobre un artefacto correcto.

Por que fallaba en ese servidor y no en la maquina donde se probo no se sabe con
certeza. El candidato mas probable es Constrained Language Mode (AppLocker/WDAC
son habituales en servidores endurecidos): restringe el acceso a propiedades de
objetos .NET como .Parameters mientras `& install.ps1` sigue funcionando, que es
justo lo que se observaba. Pero el arreglo no es adivinarlo:

- probeInstallerUpdateSupport lee el TEXTO del script y busca la declaracion
  `[switch]$UpdateInPlace` con [regex]::IsMatch. No compila nada, no toca
  reflexion y ninguna politica lo bloquea. Anclado a la declaracion y no a una
  mencion suelta, para que un comentario no de un falso positivo.
- Tres estados en vez de dos. Solo un "no" CONFIRMADO bloquea; "desconocido"
  sigue adelante con el comportamiento anterior y deja asentado POR QUE no se
  pudo determinar. Un fallo de diagnostico no puede impedir el trabajo.
- El estado se decide tambien con el codigo de salida y stderr, no solo con
  stdout: un execRemote que falla deja stdout vacio y eso se leia como respuesta.

Auditado el mismo patron en las otras sondas de Windows, porque dos gobiernan
fallos duros: probeWindowsAgentProcess (502 "no esta corriendo"),
probeWindowsElevation (409 por privilegios) y probeWindowsTask leian "el comando
no respondio" como "la respuesta es no". Ahora todas distinguen los dos casos.

Verificado con PowerShell real, y esta vez tambien las rutas de FALLO —que es lo
que falto la vez pasada y por lo que esto llego a produccion—: instalador nuevo
-> si, instalador del zip 1.1.2 publicado -> no, archivo inexistente ->
desconocido, y fallo de lectura -> desconocido.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:27:14 -06:00
2 changed files with 265 additions and 23 deletions

View File

@@ -31,6 +31,7 @@ import {
alignWindowsTask, alignWindowsTask,
checkWindowsArtifactUpdateSupport, checkWindowsArtifactUpdateSupport,
normalizeWindowsPath, normalizeWindowsPath,
probeInstallerUpdateSupport,
probeWindowsAgentProcess, probeWindowsAgentProcess,
probeWindowsElevation, probeWindowsElevation,
probeWindowsTask, probeWindowsTask,
@@ -481,9 +482,13 @@ describe('comparación de rutas de Windows', () => {
* agente viejo y sin agente nuevo. * agente viejo y sin agente nuevo.
*/ */
describe('checkWindowsArtifactUpdateSupport', () => { describe('checkWindowsArtifactUpdateSupport', () => {
const SI = { answer: 'si', detail: '' } as const;
const NO = { answer: 'no', detail: '' } as const;
const NOSE = { answer: 'desconocido', detail: 'Get-Content: acceso denegado' } as const;
it('ACTUALIZAR con un instalador antiguo se rechaza en vez de romper el servidor', () => { it('ACTUALIZAR con un instalador antiguo se rechaza en vez de romper el servidor', () => {
try { try {
checkWindowsArtifactUpdateSupport('update', false, '1.1.3', 'Principal'); checkWindowsArtifactUpdateSupport('update', NO, '1.1.3', 'Principal');
throw new Error('se esperaba que lanzara'); throw new Error('se esperaba que lanzara');
} catch (e) { } catch (e) {
const err = e as InstallError; const err = e as InstallError;
@@ -500,14 +505,93 @@ describe('checkWindowsArtifactUpdateSupport', () => {
it('INSTALAR limpio con el mismo artefacto solo avisa', () => { it('INSTALAR limpio con el mismo artefacto solo avisa', () => {
// Ahí no hay agente en marcha que perder, así que negarse solo estorbaría. // Ahí no hay agente en marcha que perder, así que negarse solo estorbaría.
const aviso = checkWindowsArtifactUpdateSupport('install', false, '1.1.3', 'Principal'); const aviso = checkWindowsArtifactUpdateSupport('install', NO, '1.1.3', 'Principal');
expect(aviso).toContain('1.1.3'); expect(aviso).toContain('1.1.3');
expect(aviso).toMatch(/Se instala igual/); expect(aviso).toMatch(/Se instala igual/);
}); });
it('un artefacto con los arreglos no dice nada, en ninguno de los dos modos', () => { it('un artefacto con los arreglos no dice nada, en ninguno de los dos modos', () => {
expect(checkWindowsArtifactUpdateSupport('update', true, '1.1.4', 'Principal')).toBe(''); expect(checkWindowsArtifactUpdateSupport('update', SI, '1.1.4', 'Principal')).toBe('');
expect(checkWindowsArtifactUpdateSupport('install', true, '1.1.4', 'Principal')).toBe(''); expect(checkWindowsArtifactUpdateSupport('install', SI, '1.1.4', 'Principal')).toBe('');
});
it('NO SABER no bloquea: es la prueba que faltaba y la que dejó 1.1.4 sin poder instalarse', () => {
// La sonda anterior colapsaba cualquier fallo en "no lo soporta", y sobre esa lectura se
// decidía un 409. Con un artefacto correcto, eso era un candado permanente.
const aviso = checkWindowsArtifactUpdateSupport('update', NOSE, '1.1.4', 'Principal');
expect(aviso).toMatch(/No se pudo determinar/);
// Y dice POR QUÉ no se pudo, que es lo que evita adivinar la próxima vez.
expect(aviso).toContain('acceso denegado');
});
it('tampoco bloquea al instalar cuando no se pudo determinar', () => {
expect(checkWindowsArtifactUpdateSupport('install', NOSE, '1.1.4', 'Principal')).toMatch(
/No se pudo determinar/
);
});
});
/**
* La sonda que lee el instalador del artefacto.
*
* Lo que se protege aquí es la distinción entre "no lo declara" y "no pude leerlo". La versión
* anterior preguntaba por `(Get-Command …).Parameters`, funcionaba en una máquina de desarrollo y
* fallaba en el servidor real; como cualquier fallo se leía como "no", el panel acabó rechazando un
* artefacto correcto.
*/
describe('probeInstallerUpdateSupport', () => {
const RUTA = 'C:\\Temp\\cras\\CloudRestoreAS\\install.ps1';
it('lee el texto del script sin resolver un solo tipo .NET', async () => {
const { sftp, commands } = fakeSftp(() => ({ code: 0, stdout: 'si' }));
const r = await probeInstallerUpdateSupport(sftp as never, RUTA);
expect(r.answer).toBe('si');
const script = Buffer.from(
commands[0].split('-EncodedCommand ')[1] ?? '',
'base64'
).toString('utf16le');
// Nada de reflexión ni de .NET: en Constrained Language Mode se prohíbe resolver tipos y
// llamar métodos estáticos, mientras los cmdlets siguen funcionando. Esta prueba es la que
// impide volver a caer ahí — primero con (Get-Command …).Parameters, después con
// [regex]::IsMatch, las dos veces por el mismo motivo.
expect(script).not.toContain('Get-Command');
expect(script).not.toContain('.Parameters');
expect(script).not.toContain('[regex]');
expect(script).not.toMatch(/\[[A-Za-z][A-Za-z0-9.]*\]::/);
expect(script).toContain('Select-String');
// Anclado a la DECLARACIÓN, para que un comentario que nombre el parámetro no cuente.
expect(script).toContain('switch');
});
it('un "no" limpio sigue siendo un no', async () => {
const { sftp } = fakeSftp(() => ({ code: 0, stdout: 'no' }));
expect((await probeInstallerUpdateSupport(sftp as never, RUTA)).answer).toBe('no');
});
it('código de salida distinto de cero es DESCONOCIDO, nunca "no"', async () => {
const { sftp } = fakeSftp(() => ({ code: 1, stdout: '', stderr: 'AmsiScanBuffer bloqueó' }));
const r = await probeInstallerUpdateSupport(sftp as never, RUTA);
expect(r.answer).toBe('desconocido');
expect(r.detail).toContain('AmsiScanBuffer');
});
it('stdout vacío con código 0 también es DESCONOCIDO', async () => {
// Es la forma exacta en que fallaba la sonda vieja: sin salida, y leído como respuesta.
const { sftp } = fakeSftp(() => ({ code: 0, stdout: ' ' }));
expect((await probeInstallerUpdateSupport(sftp as never, RUTA)).answer).toBe('desconocido');
});
it('el archivo que no existe se distingue de un instalador antiguo', async () => {
const { sftp } = fakeSftp(() => ({ code: 0, stdout: 'desconocido|no existe el archivo' }));
const r = await probeInstallerUpdateSupport(sftp as never, RUTA);
expect(r.answer).toBe('desconocido');
expect(r.detail).toBe('no existe el archivo');
});
it('una salida inesperada no se interpreta como respuesta', async () => {
const { sftp } = fakeSftp(() => ({ code: 0, stdout: 'WARNING: algo raro' }));
expect((await probeInstallerUpdateSupport(sftp as never, RUTA)).answer).toBe('desconocido');
}); });
}); });
@@ -539,12 +623,22 @@ describe('sondas de ruta en Windows', () => {
it('probeWindowsTask saca la ruta y la cuenta de la tarea', async () => { it('probeWindowsTask saca la ruta y la cuenta de la tarea', async () => {
const { sftp } = psFake('si|C:\\Otra\\CloudRestoreAS.exe|SYSTEM'); const { sftp } = psFake('si|C:\\Otra\\CloudRestoreAS.exe|SYSTEM');
expect(await probeWindowsTask(sftp as never)).toEqual({ expect(await probeWindowsTask(sftp as never)).toEqual({
answered: true,
exists: true, exists: true,
execute: 'C:\\Otra\\CloudRestoreAS.exe', execute: 'C:\\Otra\\CloudRestoreAS.exe',
principal: 'SYSTEM' principal: 'SYSTEM'
}); });
}); });
it('una sonda que no responde no significa "no hay tarea"', async () => {
// Con `answered: false` el llamador sabe que no se sabe. Antes, un comando fallido se leía
// como "no hay tarea registrada" y de ahí salían decisiones sobre elevación y realineación.
const { sftp } = fakeSftp(() => ({ code: 1, stdout: '' }));
const r = await probeWindowsTask(sftp as never);
expect(r.answered).toBe(false);
expect(r.exists).toBe(false);
});
it('probeWindowsTask sin tarea registrada', async () => { it('probeWindowsTask sin tarea registrada', async () => {
const { sftp } = psFake('no||'); const { sftp } = psFake('no||');
const r = await probeWindowsTask(sftp as never); const r = await probeWindowsTask(sftp as never);

View File

@@ -1041,7 +1041,7 @@ async function verifyLinuxDeployment(
// Windows // Windows
// ============================================================================ // ============================================================================
export type WindowsElevation = 'admin' | 'token-filtrado' | 'limitado'; export type WindowsElevation = 'admin' | 'token-filtrado' | 'limitado' | 'desconocido';
export interface WindowsPrivilege { export interface WindowsPrivilege {
elevation: WindowsElevation; elevation: WindowsElevation;
@@ -1050,6 +1050,28 @@ export interface WindowsPrivilege {
detail: string; detail: string;
} }
/**
* Modo de lenguaje de PowerShell en el destino.
*
* Existe porque en `ConstrainedLanguage` —lo que imponen AppLocker o WDAC, habituales en servidores
* endurecidos— PowerShell prohíbe resolver tipos .NET y llamar a métodos estáticos, mientras los
* cmdlets y `& script.ps1` siguen funcionando. Eso convierte fallos en sitios sorprendentes: fue la
* hipótesis principal cuando `(Get-Command …).Parameters` respondía en una máquina de desarrollo y
* no en el servidor real, y `install.ps1` todavía usa varias construcciones que ahí no funcionarían
* (`[System.IO.Path]::GetFullPath`, `New-Object System.Text.UTF8Encoding`, `[TimeSpan]::Zero`…).
*
* Se registra en la bitácora del run SIEMPRE, cueste lo que cueste averiguarlo, para dejar de
* diagnosticar a ciegas: sin este dato cada fallo raro en Windows arranca con una ronda de
* suposiciones. Devuelve cadena vacía si ni siquiera esto se pudo consultar.
*/
export async function probeWindowsLanguageMode(sftp: SftpClient): Promise<string> {
const probe = await execRemote(
sftp,
psEncoded('Write-Output $ExecutionContext.SessionState.LanguageMode')
);
return probe.code === 0 ? probe.stdout.trim() : '';
}
/** /**
* Sonda de elevación en Windows. * Sonda de elevación en Windows.
* *
@@ -1075,7 +1097,22 @@ export async function probeWindowsElevation(sftp: SftpClient): Promise<WindowsPr
) )
); );
switch (probe.stdout.trim()) { const salida = probe.stdout.trim();
// Sin respuesta no se afirma que la cuenta sea limitada: ese veredicto gobierna un 409, y
// deducirlo de un comando que no contestó rechazaría instalaciones perfectamente válidas.
if (probe.code !== 0 || !salida) {
return {
elevation: 'desconocido',
label: 'privilegios no determinados',
detail:
`la sonda de privilegios no respondió (código ${probe.code})` +
(probe.stderr ? `: ${truncate(probe.stderr)}` : '') +
'. Se continúa: el propio instalador fallará con un mensaje claro si de verdad ' +
'faltan permisos.'
};
}
switch (salida) {
case 'admin': case 'admin':
return { elevation: 'admin', label: 'administrador', detail: '' }; return { elevation: 'admin', label: 'administrador', detail: '' };
case 'token-filtrado': case 'token-filtrado':
@@ -1140,6 +1177,12 @@ export function windowsAgentExe(prefix: string): string {
} }
export interface WindowsProcessProbe { export interface WindowsProcessProbe {
/**
* La sonda obtuvo una respuesta del destino. En false NO se sabe si hay agente corriendo, y
* quien decida un fallo duro debe exigir una respuesta afirmativa en vez de asumirla por
* ausencia: leer "el comando no respondió" como "no hay proceso" tumba actualizaciones buenas.
*/
answered: boolean;
running: boolean; running: boolean;
/** Rutas de los ejecutables vivos. Vacío si corren pero no se pudo leer su ruta. */ /** Rutas de los ejecutables vivos. Vacío si corren pero no se pudo leer su ruta. */
paths: string[]; paths: string[];
@@ -1173,13 +1216,19 @@ export async function probeWindowsAgentProcess(
); );
const out = probe.stdout.trim(); const out = probe.stdout.trim();
// Sin respuesta no se sabe nada. Antes esto se leía como "detenido", que es una afirmación que
// la sonda nunca hizo.
if (probe.code !== 0 || !out) {
return { answered: false, running: false, paths: [], fromPrefix: false };
}
if (!out.startsWith('corriendo')) { if (!out.startsWith('corriendo')) {
return { running: false, paths: [], fromPrefix: false }; return { answered: true, running: false, paths: [], fromPrefix: false };
} }
const joined = out.split('|')[1] ?? ''; const joined = out.split('|')[1] ?? '';
const paths = joined.split(';').map((p) => p.trim()).filter(Boolean); const paths = joined.split(';').map((p) => p.trim()).filter(Boolean);
const wanted = windowsAgentExe(prefix); const wanted = windowsAgentExe(prefix);
return { return {
answered: true,
running: true, running: true,
paths, paths,
fromPrefix: paths.some((p) => sameWindowsPath(p, wanted)) fromPrefix: paths.some((p) => sameWindowsPath(p, wanted))
@@ -1187,6 +1236,8 @@ export async function probeWindowsAgentProcess(
} }
export interface WindowsTaskProbe { export interface WindowsTaskProbe {
/** La sonda respondió. En false, `exists: false` significa "no sé", no "no hay tarea". */
answered: boolean;
exists: boolean; exists: boolean;
/** Ruta del ejecutable en la acción de la tarea. Es DÓNDE arranca el agente de verdad. */ /** Ruta del ejecutable en la acción de la tarea. Es DÓNDE arranca el agente de verdad. */
execute: string; execute: string;
@@ -1221,14 +1272,93 @@ export async function probeWindowsTask(sftp: SftpClient): Promise<WindowsTaskPro
) )
); );
const [flag, execute, principal] = probe.stdout.trim().split('|'); const salida = probe.stdout.trim();
if (probe.code !== 0 || !salida) {
return { answered: false, exists: false, execute: '', principal: '' };
}
const [flag, execute, principal] = salida.split('|');
return { return {
answered: true,
exists: flag === 'si', exists: flag === 'si',
execute: (execute ?? '').trim(), execute: (execute ?? '').trim(),
principal: (principal ?? '').trim() principal: (principal ?? '').trim()
}; };
} }
/**
* Respuesta de una sonda que puede no saber.
*
* `desconocido` NO es `no`. Confundirlos es lo que dejó bloqueadas las actualizaciones con un
* artefacto perfectamente bueno: la sonda anterior colapsaba cualquier fallo —una excepción, un
* código de salida distinto de cero, stdout vacío— en "el instalador no lo soporta", y sobre esa
* lectura se decidía un 409. Mientras solo elegía entre dos banderas el falso negativo degradaba;
* en cuanto gobernó un rechazo, se volvió un candado.
*/
export interface ProbeAnswer {
answer: 'si' | 'no' | 'desconocido';
/** Por qué no se pudo determinar. Vacío cuando hay respuesta. */
detail: string;
}
/**
* ¿El `install.ps1` del artefacto declara `-UpdateInPlace`?
*
* Se pregunta leyendo el TEXTO del script, no pidiéndole a PowerShell que lo compile y exponga sus
* parámetros. `(Get-Command …).Parameters` funcionaba en una máquina de desarrollo y fallaba en el
* servidor real; el candidato más probable es Constrained Language Mode (AppLocker/WDAC son
* habituales en servidores endurecidos), que restringe el acceso a propiedades de objetos .NET
* mientras `& install.ps1` sigue funcionando — justo lo que se observaba. Un `-match` sobre el
* contenido no compila nada, no toca reflexión y ninguna política lo bloquea.
*
* Se ancla a la DECLARACIÓN (`[switch]$UpdateInPlace`) y no a una mención suelta, para que un
* comentario que nombre el parámetro no dé un falso positivo.
*/
export async function probeInstallerUpdateSupport(
sftp: SftpClient,
installerPath: string
): Promise<ProbeAnswer> {
const probe = await execRemote(
sftp,
psEncoded(
`$p = '${installerPath}'; ` +
"if (-not (Test-Path -LiteralPath $p)) { Write-Output 'desconocido|no existe el archivo'; exit 0 }; " +
'try { ' +
// Select-String y nada más: un cmdlet puro, sin resolución de tipos .NET.
// La versión anterior usaba [regex]::IsMatch, que es una llamada estática y por
// tanto de lo primero que bloquea Constrained Language Mode — el modo que se
// sospecha en el servidor donde falló `(Get-Command …).Parameters`. Habría
// degradado a 'desconocido' en vez de responder, que no rompe nada pero tampoco
// sirve de nada.
// `-ErrorAction Stop` NO es opcional: Select-String emite un error NO TERMINANTE
// cuando no puede leer el archivo, así que sin él el catch no se dispara, -Quiet
// devuelve falso y el fallo de lectura se reporta como 'no'. Es decir, exactamente
// el defecto que esta función existe para eliminar. Lo destapó probar la ruta de
// fallo con PowerShell real, no las pruebas unitarias.
"if (Select-String -LiteralPath $p -Pattern '\\[switch\\]\\s*\\$UpdateInPlace' " +
'-Quiet -ErrorAction Stop) ' +
"{ Write-Output 'si' } else { Write-Output 'no' } } " +
"catch { Write-Output ('desconocido|' + $_.Exception.Message) }"
)
);
const salida = probe.stdout.trim();
// El código de salida y stderr cuentan: un execRemote que falla deja stdout vacío, y leer eso
// como una respuesta es precisamente el error que se está corrigiendo.
if (probe.code !== 0 || !salida) {
return {
answer: 'desconocido',
detail:
`la sonda no respondió (código ${probe.code})` +
(probe.stderr ? `: ${truncate(probe.stderr)}` : '')
};
}
if (salida === 'si' || salida === 'no') return { answer: salida, detail: '' };
return {
answer: 'desconocido',
detail: salida.startsWith('desconocido|') ? salida.slice(12) : truncate(salida)
};
}
/** /**
* ¿Se puede desplegar este artefacto en Windows sin romper el servidor? * ¿Se puede desplegar este artefacto en Windows sin romper el servidor?
* *
@@ -1250,11 +1380,22 @@ export async function probeWindowsTask(sftp: SftpClient): Promise<WindowsTaskPro
*/ */
export function checkWindowsArtifactUpdateSupport( export function checkWindowsArtifactUpdateSupport(
mode: InstallMode, mode: InstallMode,
supportsInPlace: boolean, soporte: ProbeAnswer,
version: string, version: string,
targetName: string targetName: string
): string { ): string {
if (supportsInPlace) return ''; if (soporte.answer === 'si') return '';
// No saber NO es motivo para bloquear. Un fallo de diagnóstico no puede impedir el trabajo: se
// sigue con el comportamiento anterior y se deja dicho por qué no se pudo determinar, que es el
// dato que convierte la próxima sorpresa en un diagnóstico de diez segundos.
if (soporte.answer === 'desconocido') {
return (
`No se pudo determinar si el install.ps1 de ${version} soporta -UpdateInPlace ` +
`(${soporte.detail || 'sin detalle'}). Se instala con el modo de arranque normal en vez ` +
'de bloquear: no saberlo no es lo mismo que saber que no.'
);
}
const aviso = const aviso =
`El artefacto ${version} trae un install.ps1 anterior a los arreglos de actualización ` + `El artefacto ${version} trae un install.ps1 anterior a los arreglos de actualización ` +
@@ -1497,12 +1638,27 @@ async function installWindows(
if (needsElevation && privileged.elevation === 'token-filtrado') { if (needsElevation && privileged.elevation === 'token-filtrado') {
await appendInstallStep(runId, 'aviso-elevacion', true, privileged.detail); await appendInstallStep(runId, 'aviso-elevacion', true, privileged.detail);
} }
// El modo de lenguaje se registra SIEMPRE, aunque todo vaya bien. Es barato y es el dato que
// faltaba: en ConstrainedLanguage varias construcciones de install.ps1 no funcionan, y sin
// verlo en la bitácora cada fallo raro en Windows empieza con una ronda de suposiciones.
const languageMode = await probeWindowsLanguageMode(sftp);
await appendInstallStep( await appendInstallStep(
runId, runId,
'precondiciones', 'precondiciones',
true, true,
`${systemEvidence}, privilegios=${privileged.label}` `${systemEvidence}, privilegios=${privileged.label}` +
`, PowerShell=${languageMode || 'modo no determinado'}`
); );
if (languageMode && languageMode !== 'FullLanguage') {
await appendInstallStep(
runId,
'aviso-modo-restringido',
true,
`PowerShell corre en ${languageMode}. Ahí se prohíbe resolver tipos .NET, y install.ps1 ` +
'usa varias construcciones que dependen de eso, así que puede fallar en sitios ' +
'inesperados aunque el artefacto sea correcto.'
);
}
if (request.mode === 'update') { if (request.mode === 'update') {
await assertExistingInstall(sftp, 'windows', targetPath, reportedInstallPath); await assertExistingInstall(sftp, 'windows', targetPath, reportedInstallPath);
@@ -1602,21 +1758,13 @@ async function installWindows(
// //
// Pero solo si el instalador DEL ARTEFACTO lo declara: el parámetro existe desde 1.1.3, e // Pero solo si el instalador DEL ARTEFACTO lo declara: el parámetro existe desde 1.1.3, e
// install.ps1 usa [CmdletBinding()], así que pasárselo a uno anterior falla con // install.ps1 usa [CmdletBinding()], así que pasárselo a uno anterior falla con
// NamedParameterNotFound SIN ejecutar una sola línea. Se le pregunta a PowerShell por el // NamedParameterNotFound SIN ejecutar una sola línea.
// `param()` del propio script en vez de mantener una tabla de versiones aquí.
const installerPs1 = `${remoteDir}\\CloudRestoreAS\\install.ps1`; const installerPs1 = `${remoteDir}\\CloudRestoreAS\\install.ps1`;
const supportsProbe = await execRemote( const soporte = await probeInstallerUpdateSupport(sftp, installerPs1);
sftp,
psEncoded(
`if ((Get-Command '${installerPs1}').Parameters.ContainsKey('UpdateInPlace')) ` +
"{'si'} else {'no'}"
)
);
const supportsInPlace = supportsProbe.stdout.trim() === 'si';
const avisoArtefacto = checkWindowsArtifactUpdateSupport( const avisoArtefacto = checkWindowsArtifactUpdateSupport(
request.mode, request.mode,
supportsInPlace, soporte,
release.version, release.version,
target.name target.name
); );
@@ -1624,7 +1772,7 @@ async function installWindows(
await appendInstallStep(runId, 'instalador-antiguo', true, avisoArtefacto); await appendInstallStep(runId, 'instalador-antiguo', true, avisoArtefacto);
} }
const inPlaceUpdate = request.mode === 'update' && supportsInPlace; const inPlaceUpdate = request.mode === 'update' && soporte.answer === 'si';
const flag = inPlaceUpdate const flag = inPlaceUpdate
? ' -UpdateInPlace' ? ' -UpdateInPlace'
: autostart === 'none' : autostart === 'none'