Skip to content

fix(hook): show why a captured program failed to start in PowerShell - #62

Merged
NaNIsaac merged 2 commits into
mainfrom
fix/hook-native-launch
Nov 5, 2026
Merged

NaNIsaac merged 2 commits into
mainfrom
fix/hook-native-launch

Conversation

@NaNIsaac

@NaNIsaac NaNIsaac commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Problem

Under the PowerShell hook, a program that fails to start reports

Program 'x.exe' failed to run: StandardOutputEncoding is only supported when standard output is redirected.

instead of the reason. Seen launching powershell -ExecutionPolicy Bypass -c "irm ... | iex" in a pwsh 7.6 session over ssh.

Cause

pwsh engine bug PowerShell/PowerShell#28225 (open). In NativeCommandProcessor.InitNativeProcess, a Win32Exception from Process.Start on Windows desktops falls back to ShellExecute for a solo command: it clears RedirectStandardOutput but leaves the StandardOutputEncoding set by GetProcessStartInfo when output is redirected, so Process.Start throws InvalidOperationException. The hook runs every captured line as . $block *>&1 | __cdgRecord, so every native command in it has its output redirected and any start failure goes down this path. Without the hook the same command is not redirected, gets the ShellExecute fallback, and reports the real error when that fails too. This has been the case since the capture pipe was added; the $^/$$ change is unrelated.

Repro without the hook: Set-Content notpe.exe x; $x = & .\notpe.exe.

Fix

When a captured line fails this way (ApplicationFailedException wrapping an InvalidOperationException from Process.Start, pointing at the line itself) and the line is that one program alone with plain-value arguments, nothing in it ran, so __cdg runs it again without the pipe. It starts as it would without the hook, or fails with the real reason. That line is not captured. Lines that did more (a; b.exe, b.exe $(...), a function that calls the program) keep the old behavior, since running them again would repeat work.

Tests

  • test_pwsh_program_that_cannot_start_says_why: a non-PE .exe reports the real reason with and without the hook (fails on main); a line that did more is not run again.
  • test_pwsh_captured_line_starts_windows_powershell_and_pwsh: powershell.exe -c, pwsh -c, and an installer-shaped multi-line script (& { ... }, $ErrorActionPreference = 'Stop', a native call) piped to iex in this shell, in powershell -c and in pwsh -c all run and are captured.

Full suite: 311 passed, 5 skipped, 1 xfailed locally (Windows, pwsh 7.6.9); ruff check and format clean.

When a native program cannot be started while its output is piped, pwsh
retries through ShellExecute without the pipe but leaves
StandardOutputEncoding set, so the retry fails with "StandardOutputEncoding
is only supported when standard output is redirected" and the real reason is
lost (PowerShell/PowerShell#28225). Every captured line pipes its output, so
under the hook any start failure surfaced this way, and the ShellExecute
fallback a plain shell gets never ran.

When the failing line is that one program alone, with plain-value
arguments, nothing in it ran, so the hook now runs it again without the
pipe: it starts as it would without the hook, or fails with the real reason.
Lines that did more than start the program keep the old behavior.

Tests: a file that is not a program reports the same error with and without
the hook; powershell.exe, pwsh and an iex-piped installer script (in this
shell and in a child one) start and are captured.
@NaNIsaac
NaNIsaac merged commit d30f3ca into main Nov 5, 2026
8 checks passed
@NaNIsaac
NaNIsaac deleted the fix/hook-native-launch branch November 5, 2026 06:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant