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>
This commit is contained in:
@@ -542,7 +542,7 @@ describe('checkWindowsArtifactUpdateSupport', () => {
|
|||||||
describe('probeInstallerUpdateSupport', () => {
|
describe('probeInstallerUpdateSupport', () => {
|
||||||
const RUTA = 'C:\\Temp\\cras\\CloudRestoreAS\\install.ps1';
|
const RUTA = 'C:\\Temp\\cras\\CloudRestoreAS\\install.ps1';
|
||||||
|
|
||||||
it('lee el texto del script en vez de compilarlo', async () => {
|
it('lee el texto del script sin resolver un solo tipo .NET', async () => {
|
||||||
const { sftp, commands } = fakeSftp(() => ({ code: 0, stdout: 'si' }));
|
const { sftp, commands } = fakeSftp(() => ({ code: 0, stdout: 'si' }));
|
||||||
const r = await probeInstallerUpdateSupport(sftp as never, RUTA);
|
const r = await probeInstallerUpdateSupport(sftp as never, RUTA);
|
||||||
expect(r.answer).toBe('si');
|
expect(r.answer).toBe('si');
|
||||||
@@ -551,11 +551,15 @@ describe('probeInstallerUpdateSupport', () => {
|
|||||||
commands[0].split('-EncodedCommand ')[1] ?? '',
|
commands[0].split('-EncodedCommand ')[1] ?? '',
|
||||||
'base64'
|
'base64'
|
||||||
).toString('utf16le');
|
).toString('utf16le');
|
||||||
// Nada de reflexión: en Constrained Language Mode el acceso a .Parameters puede fallar
|
// Nada de reflexión ni de .NET: en Constrained Language Mode se prohíbe resolver tipos y
|
||||||
// mientras `& install.ps1` sigue funcionando, que es lo que se observó en el servidor.
|
// 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('Get-Command');
|
||||||
expect(script).not.toContain('.Parameters');
|
expect(script).not.toContain('.Parameters');
|
||||||
expect(script).toContain('Get-Content');
|
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.
|
// Anclado a la DECLARACIÓN, para que un comentario que nombre el parámetro no cuente.
|
||||||
expect(script).toContain('switch');
|
expect(script).toContain('switch');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
@@ -1301,10 +1323,19 @@ export async function probeInstallerUpdateSupport(
|
|||||||
`$p = '${installerPath}'; ` +
|
`$p = '${installerPath}'; ` +
|
||||||
"if (-not (Test-Path -LiteralPath $p)) { Write-Output 'desconocido|no existe el archivo'; exit 0 }; " +
|
"if (-not (Test-Path -LiteralPath $p)) { Write-Output 'desconocido|no existe el archivo'; exit 0 }; " +
|
||||||
'try { ' +
|
'try { ' +
|
||||||
'$t = Get-Content -LiteralPath $p -Raw -ErrorAction Stop; ' +
|
// Select-String y nada más: un cmdlet puro, sin resolución de tipos .NET.
|
||||||
// El [regex]::IsMatch evita depender de $matches y del operador -match, que en
|
// La versión anterior usaba [regex]::IsMatch, que es una llamada estática y por
|
||||||
// modo restringido puede comportarse distinto.
|
// tanto de lo primero que bloquea Constrained Language Mode — el modo que se
|
||||||
'if ([regex]::IsMatch($t, \'\\[switch\\]\\s*\\$UpdateInPlace\')) ' +
|
// 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' } } " +
|
"{ Write-Output 'si' } else { Write-Output 'no' } } " +
|
||||||
"catch { Write-Output ('desconocido|' + $_.Exception.Message) }"
|
"catch { Write-Output ('desconocido|' + $_.Exception.Message) }"
|
||||||
)
|
)
|
||||||
@@ -1607,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);
|
||||||
|
|||||||
Reference in New Issue
Block a user