From 3f926997299c8573955cfb54d2c67df0d8facb62 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 28 Aug 2026 10:11:56 +0200 Subject: [PATCH] fix(installer): stop PS 5.1 stderr wrapping from aborting version probes Distilled from #1255 by @MasterFede5, rebased onto the single-binary installer and completed with the two review conditions. PowerShell 5.1 wraps redirected native stderr into ErrorRecords, so under the script's global ErrorActionPreference=Stop a HEALTHY binary that prints a single warning to stderr while exiting 0 aborts both `--version 2>&1` probes with "downloaded binary failed to run". Both probes now relax EAP to Continue for the probe call only, with an exception-safe restore; failure detection stays on $LASTEXITCODE. The review-required pre-seed ($global:LASTEXITCODE = 1) closes the window the relaxation would otherwise open: a binary that fails to START under Continue may leave $LASTEXITCODE untouched, and the installed-binary probe runs after earlier successful native calls, so a stale 0 would have turned a corrupt install into "Installed: ...". Seeding 1 makes start-failure read as failure on both probes. The bundle contract pins the guard's two load-bearing parts (the scoped relaxation with restore, and the pre-seed), verified failing when either is stripped. The change adds zero non-ASCII bytes; the file remains pure ASCII per the PS 5.1 ANSI-decode rule. Co-authored-by: MasterFede5 Signed-off-by: Martin Vogel --- install.ps1 | 27 +++++++++++++++++++++++++-- tests/test_windows_bundle_contract.sh | 13 +++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index 1433c926e..d63f1001d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -281,8 +281,21 @@ if ($binaryItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { } # Prove the downloaded binary runs before touching an existing installation. +# PS 5.1 wraps redirected native stderr into ErrorRecords, so under the global +# ErrorActionPreference=Stop a HEALTHY binary that prints one warning while +# exiting 0 becomes a terminating error here. Relax to Continue for the probe +# only; failure detection stays on $LASTEXITCODE, and the pre-seed guarantees +# a binary that fails to START (stale $LASTEXITCODE from an earlier native +# call) can never read as success. try { - $candidateVersion = & $DownloadedBinary --version 2>&1 + $ProbeEap = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + $global:LASTEXITCODE = 1 + $candidateVersion = & $DownloadedBinary --version 2>&1 + } finally { + $ErrorActionPreference = $ProbeEap + } if ($LASTEXITCODE -ne 0) { throw "candidate exited with $LASTEXITCODE" } Write-Host "Verified candidate: $candidateVersion" } catch { @@ -349,8 +362,18 @@ if (Test-Path -LiteralPath $DownloadedInstaller -PathType Leaf) { } # Verify +# Same PS 5.1 stderr-wrapping guard as the candidate probe above; the pre-seed +# matters MOST here, because prior successful native calls leave a stale +# $LASTEXITCODE=0 that a start-failure would otherwise inherit. try { - $ver = & $Dest --version 2>&1 + $ProbeEap = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + $global:LASTEXITCODE = 1 + $ver = & $Dest --version 2>&1 + } finally { + $ErrorActionPreference = $ProbeEap + } if ($LASTEXITCODE -ne 0) { throw "installed binary exited with $LASTEXITCODE" } Write-Host "Installed: $ver" } catch { diff --git a/tests/test_windows_bundle_contract.sh b/tests/test_windows_bundle_contract.sh index fab5023d5..a40ac3083 100644 --- a/tests/test_windows_bundle_contract.sh +++ b/tests/test_windows_bundle_contract.sh @@ -229,6 +229,19 @@ require( "install.ps1 must verify and install through the downloaded binary", ) +# PS 5.1 wraps redirected native stderr into ErrorRecords; under the global +# ErrorActionPreference=Stop a healthy binary that warns on stderr aborts the +# version probes (#1255). Pin the guard's two load-bearing parts so neither +# can be silently dropped: the probe-scoped relaxation with an exception-safe +# restore, and the $LASTEXITCODE pre-seed that keeps a start-failure from +# inheriting a stale 0 and reading as success. +require( + installer.count('$ErrorActionPreference = "Continue"') == 2 + and installer.count("$global:LASTEXITCODE = 1") == 2 + and installer.count("$ErrorActionPreference = $ProbeEap") == 2, + "install.ps1 version probes must relax EAP with restore and pre-seed LASTEXITCODE (#1255)", +) + # ── 4. Package-manager shims resolve the single Windows binary ─────────────── single_binary_contracts = { "pkg/npm/install.js": (