Skip to content

Fix -WindowStyle Hidden console window flash#27111

Open
SufficientDaikon wants to merge 8 commits intoPowerShell:masterfrom
SufficientDaikon:fix/windowstyle-hidden-console-flash
Open

Fix -WindowStyle Hidden console window flash#27111
SufficientDaikon wants to merge 8 commits intoPowerShell:masterfrom
SufficientDaikon:fix/windowstyle-hidden-console-flash

Conversation

@SufficientDaikon
Copy link
Copy Markdown

@SufficientDaikon SufficientDaikon commented Mar 28, 2026

PR Summary

Eliminate the console window flash when launching pwsh -WindowStyle Hidden on Windows 11 24H2+ by using the consoleAllocationPolicy=detached manifest element and AllocConsoleWithOptions kernel32 API.

Fixes #3028

Important

Progressive enhancement: On older Windows the manifest is ignored and behavior is identical to the current release. On newer Windows (build 26100+) the console window is never created in the first place.

On Windows 11 build 26100+, the OS no longer auto-creates a console window for pwsh.exe — PowerShell allocates it programmatically at the top of Main():

  • -WindowStyle HiddenAllocConsoleWithOptions(NoWindow): console I/O works, no window appears
  • Normal launchAllocConsoleWithOptions(Default): respects DETACHED_PROCESS from the parent (per DHowett's review)
  • Older Windows — API not available → TryAlloc* returns false via catch (EntryPointNotFoundException) → no-op (OS already auto-allocated the console)

No AllocConsole() fallback in EarlyConsoleInit — plain AllocConsole() would override DETACHED_PROCESS from the parent. On older Windows the manifest is ignored, so the OS auto-allocates the console before Main() runs and GetConsoleWindow() returns non-zero — EarlyConsoleInit is a no-op.

pwsh.exe stays a CUI subsystem binary. No new executables, no breaking changes, no new public API.

Changes

File Change
assets/pwsh.manifest Add consoleAllocationPolicy=detached in a new <application> block (asm.v3 xmlns required)
engine/Interop/Windows/AllocConsoleWithOptions.cs New — P/Invoke for AllocConsoleWithOptions, shared TryAllocConsoleWithMode() helper, checks AllocConsoleResult != NoConsole
host/msh/ManagedEntrance.cs New EarlyConsoleInit(args) + EarlyCheckForHiddenWindowStyle(args)
host/msh/ConsoleControl.cs SetConsoleMode() guards against null handle — graceful return instead of Dbg.Assert
engine/NativeCommandProcessor.cs AllocateHiddenConsole() prefers AllocConsoleWithOptions(NoWindow) with AllocConsole() fallback (native commands always need a console)
test/powershell/Host/WindowStyleHidden.Tests.ps1 New — 13 Pester tests (Pester 4 compatible)

Behavior by launch context

Context Old Windows Windows 26100+
Terminal / cmd / pwsh Console inherited — unchanged Console inherited — unchanged
Explorer double-click OS creates console — unchanged AllocConsoleWithOptions(Default)
CreateProcess(DETACHED_PROCESS) No console — unchanged Default mode respects detached intent
CreateProcess(CREATE_NO_WINDOW) Console session, no window — unchanged AllocConsoleWithOptions returns ExistingConsole (no-op)
DETACHED_PROCESS + -WindowStyle Hidden No console (crash) NoWindow creates invisible console — user explicitly requested working I/O
Task Scheduler + -WindowStyle Hidden Flash then hide No flash
Shortcut + -WindowStyle Hidden Flash then hide No flash
  public static int Start(string[] args, int argc)
  {
      ArgumentNullException.ThrowIfNull(args);
+
+ #if !UNIX
+     // Allocate console before anything touches CONOUT$/CONIN$ handles.
+     EarlyConsoleInit(args);
+ #endif
+
  #if DEBUG

Warning

Ordering constraint: EarlyConsoleInit() must run before EarlyStartup.Init(). The Lazy<ConsoleHandle> in ConsoleControl.cs opens CONOUT$ via CreateFile — it's a once-only Lazy initializer, so if accessed before a console exists, the HostException is cached permanently. The console must exist before any background warmup (AMSI, Compiler init) touches it.

Root cause and how the fix works
flowchart TD
    A["pwsh.exe launched with\n-WindowStyle Hidden"] --> B{"Windows checks\nPE subsystem"}

    B -->|"CUI (current behavior)"| C["OS creates visible\nconsole window"]
    C --> D["Main() starts"]
    D --> E["PowerShell parses\n-WindowStyle Hidden"]
    E --> F["ShowWindow(SW_HIDE)"]
    F --> G["Window already\nflashed on screen"]

    B -->|"CUI + detached manifest\n(this PR)"| H["OS skips\nconsole allocation"]
    H --> I["Main() starts"]
    I --> J["EarlyConsoleInit()\nscans args immediately"]
    J --> K["AllocConsoleWithOptions\n(NoWindow)"]
    K --> L["Console I/O works\nNo window ever appeared"]
Loading

The key insight: previous fixes tried to hide the window faster. This fix prevents the window from being created at all by shifting console allocation from the OS to PowerShell.

How EarlyConsoleInit decides what to do

flowchart LR
    A["EarlyConsoleInit(args)"] --> B{"GetConsoleWindow()\n!= 0?"}
    B -->|"Yes (inherited /\nolder Windows)"| C["return\n(leave to SetConsoleMode)"]

    B -->|"No (detached policy /\nDETACHED_PROCESS /\nCREATE_NO_WINDOW)"| F{"-WindowStyle\nHidden?"}
    F -->|Yes| G["TryAllocConsoleNoWindow()"]
    F -->|No| H["TryAllocConsoleDefault()"]

    G -->|"API missing\n(older Windows)"| I["no-op\n(preserve existing behavior)"]
    H -->|"API missing\n(older Windows)"| J["no-op\n(preserve existing behavior)"]
Loading
Early arg scan design

EarlyCheckForHiddenWindowStyle(args) runs before CommandLineParameterParser. Uses ReadOnlySpan<char> for zero heap allocations.

Matches:

  • Single dash: -w, -wi, -win, ..., -windowstyle
  • Double dash: --w, --windowstyle (strips second dash, matching GetSwitchKey)
  • Forward slash: /w, /windowstyle
  • Case-insensitive via StringComparison.OrdinalIgnoreCase

Design tradeoffs:

  • False positives acceptable — worst case is allocating a hidden console that the full parser later makes visible
  • False negatives fall through to the existing SetConsoleMode path which handles -WindowStyle via ShowWindow
  • Colon syntax (-windowstyle:hidden) intentionally unhandled — the full parser's MatchSwitch also does not split on colons for this parameter
Why no AllocConsole() fallback in EarlyConsoleInit

Per DHowett's review: plain AllocConsole() overrides DETACHED_PROCESS from the parent's CreateProcess call and force-creates a console. AllocConsoleWithOptions with Default mode respects the parent's intent.

The manifest (consoleAllocationPolicy=detached) and the API (AllocConsoleWithOptions) were designed and shipped together in Windows 11 build 26100. Therefore:

  • API available → manifest is effective → TryAlloc* handles everything
  • API not available → manifest is ignored → OS auto-allocated the console → GetConsoleWindow() != 0EarlyConsoleInit returns early (no-op)
  • DETACHED_PROCESS on older Windows → only path where TryAlloc* returns false and no console exists → existing behavior is no console I/O; a plain AllocConsole() fallback would defeat DETACHED_PROCESS

NativeCommandProcessor.AllocateHiddenConsole() retains its AllocConsole() fallback because native commands unconditionally need a console for I/O — that's a different contract than startup allocation.

Both TryAllocConsoleNoWindow() and TryAllocConsoleDefault() funnel through a shared TryAllocConsoleWithMode() helper that checks result != AllocConsoleResult.NoConsole.

DETACHED_PROCESS + -WindowStyle Hidden

daxian-dbw raised an inconsistency: with -WindowStyle Hidden, NoWindow mode creates a console even under DETACHED_PROCESS, while without -WindowStyle Hidden, Default mode respects DETACHED_PROCESS and creates none.

This is intentional. Per the API docs, ALLOC_CONSOLE_MODE_NO_WINDOW "allocates a console session without a window, even if this process was created with DETACHED_PROCESS." When the user explicitly passes -WindowStyle Hidden, they are requesting invisible PowerShell with working I/O (Write-Host, native commands, pipeline). The alternative — honoring DETACHED_PROCESS and providing no console, causing stdin/stdout crashes — is strictly worse.

Launch flags Without -WindowStyle Hidden With -WindowStyle Hidden
Normal Default → new visible console NoWindow → new invisible console
DETACHED_PROCESS DefaultNoConsole (respected) NoWindow → new invisible console (user asked for I/O)
CREATE_NO_WINDOW DefaultExistingConsole (no-op) NoWindowExistingConsole (no-op)
API reference
API Docs Notes
AllocConsoleWithOptions learn.microsoft.com Returns HRESULT, takes ALLOC_CONSOLE_OPTIONS* and ALLOC_CONSOLE_RESULT*
ALLOC_CONSOLE_OPTIONS learn.microsoft.com { ALLOC_CONSOLE_MODE mode; BOOL useShowWindow; WORD showWindow; }

Minimum: Windows 11 24H2 (build 26100) / Windows Server 2025

Test coverage

13 Pester tests in WindowStyleHidden.Tests.ps1 (Pester 4 compatible — -Skip on It blocks, not Context):

Context Tests
Manifest verification PE embedded manifest extraction
Output capture -WindowStyle Hidden -Command, pipeline, Write-Host, exit codes
Arg prefix variants -w, -win, --windowstyle, /windowstyle, -WINDOWSTYLE
API probe AllocConsoleWithOptions availability detection
Regression Normal startup, -WindowStyle Normal
Known limitations
  • Unix: All changes are #if !UNIX guarded — no effect, no risk.
  • Windows Server Core / RDP sessions: Not explicitly tested. Progressive enhancement design means no regression risk, but behavior under headless or remote display contexts has not been verified.
  • -NonInteractive interaction: The early arg scan only looks for -WindowStyle Hidden. It does not check -NonInteractive. This matches existing behavior (the full parser handles -NonInteractive separately).
  • pwsh-preview.exe: No separate pwsh-preview.manifest exists in the repo. If the preview binary shares the same manifest, the fix applies automatically.
  • Colon syntax (-windowstyle:hidden): Intentionally unhandled — the full parser's MatchSwitch also does not split on colons for this parameter. Open to guidance if reviewers feel the early scan should handle it.

PR Context

Issue #3028 has been open since January 2017 with 160+ upvotes. Two previous PRs attempted to fix it:

PR Approach Result
#10962 Separate pwshw.exe (WinExe subsystem) Closed — 127 test failures; Write-Host, Read-Host, native commands broke
#10965 Parser-level changes Closed — 66 test failures

Both failed because pwsh.exe is a CUI binary and Windows creates a visible console window before Main() runs. The flash cannot be eliminated by optimizing managed code startup — the window is created by the Windows loader. The fix required an OS-level mechanism: consoleAllocationPolicy and AllocConsoleWithOptions, shipped in Windows 11 build 26100. The Windows Console team (DHowett — designer/implementer of consoleAllocationPolicy) recommended this approach in issue #3028.

📚 Interactive walkthrough of this fix — visual before/after comparisons, annotated source changes, and quizzes

PR Checklist

Use consoleAllocationPolicy=detached manifest and AllocConsoleWithOptions
to prevent the OS from auto-allocating a visible console window on newer
Windows. On older Windows the manifest is ignored and behavior is unchanged.

On Windows 11 build 26100+, the detached policy stops the OS from
creating a console window before any code runs. PowerShell now allocates
the console itself at the earliest point in startup — visibly for
interactive use, or invisibly via AllocConsoleWithOptions(NoWindow) when
-WindowStyle Hidden is specified.

This approach was recommended by @DHowett (Windows Console team) in
PowerShell#3028 (comment)

Fix PowerShell#3028
@SufficientDaikon SufficientDaikon requested a review from a team as a code owner March 28, 2026 12:42
- Replace Substring(1) with AsSpan(1) for zero-alloc early arg parsing
- Extract TryAllocConsoleNoWindow() into Interop.Windows (DRY)
- Add XML doc comments to AllocConsoleWithOptions enums and struct
- Document colon-syntax and false-positive behavior in arg scanner
- Change test tag from CI to Feature (matches existing WindowStyle tests)
- Remove hardcoded build number from API probe test
- Add -ErrorAction Stop to Add-Type in tests
- Use double quotes consistently in test file
@DHowett
Copy link
Copy Markdown

DHowett commented Mar 28, 2026

This sounds awesome! I haven't been able to give it an in-depth review, but I do have a note from reading the PR description.

If you are on a version of Windows which has AllocConsoleWithOptions, you should always use it (when coupled with the "detached" policy). Using AllocConsole will override a CreateProcess(... DETACHED_PROCESS), whereas AllocConsoleWithOptions with ALLOC_CONSOLE_MODE_DEFAULT will respect DETACHED_PROCESS.

Per DHowett's feedback: plain AllocConsole() overrides DETACHED_PROCESS
from the parent's CreateProcess call. AllocConsoleWithOptions with
Default mode respects it. Extract shared TryAllocConsoleWithMode() and
add TryAllocConsoleDefault() alongside TryAllocConsoleNoWindow().

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@SufficientDaikon
Copy link
Copy Markdown
Author

SufficientDaikon commented Mar 28, 2026

Thanks @DHowett! Great catch — you're absolutely right.

I've pushed a fix in 1a86fe6: normal (non-hidden) launches now use AllocConsoleWithOptions(Default) instead of plain AllocConsole(), so DETACHED_PROCESS from the parent's CreateProcess call is properly respected.

The logic is now:

  • -WindowStyle HiddenAllocConsoleWithOptions(NoWindow) — invisible console session
  • Normal launchAllocConsoleWithOptions(Default) — respects parent's detach intent
  • Older Windows (API missing) → falls back to AllocConsole() via catch (EntryPointNotFoundException)

Both helpers go through a shared TryAllocConsoleWithMode() to keep it DRY.

generated by opus.

@DHowett
Copy link
Copy Markdown

DHowett commented Mar 28, 2026

Great catch — you're absolutely right.

It is rude to respond to people with AI-generated text.

@SufficientDaikon
Copy link
Copy Markdown
Author

SufficientDaikon commented Mar 28, 2026

@DHowett i'm sorry, won't happen again.

i automated the process too much, i'm working on dialing it down and adding rules to prevent comments all together because that's not at all the kind of automation i'm looking for.

but yes, thank you so much for your reply, and i really do appreciate it because the powershell repo is waaay to big for me to understand quickly enough for my PRs to not have mistakes, so i need feedback so i can keep working on iterating on the PRs i'm making quickly so they get merged as cleanly as easily as possible.

again, i apologize, that is incrediblly rude, i didn't mean for that to happen at all, i also acknowledge that i should've been more attentive of what my powershell agent was doing.

@DHowett
Copy link
Copy Markdown

DHowett commented Mar 28, 2026

@SufficientDaikon hey thanks for saying all that :) it's really cool that the tools of the modern day allow you to take on a daunting task like this! I know everyone's still figuring out their whole workflow, so don't feel too bad!

@SufficientDaikon
Copy link
Copy Markdown
Author

SufficientDaikon commented Mar 28, 2026

@DHowett
omg i was like panicking thinking i made a huge error, i appreciate that you're so understanding, that's actually so nice, i'm not worried now of actaully making the PRs i want because this whole time i'm just trying to fix my problems but they're way too big to be reviewed in a timely manner 😅

but yea, i'll keep working on this and tag you when i need input.

thank you again, i appreciate your understanding ❤️

The early arg scan only stripped one leading dash, so --windowstyle
hidden was not detected (the key became "-windowstyle" with length 12,
failing the <= "windowstyle".Length check). Add double-dash stripping
to match the full parser's GetSwitchKey behavior.

Remove misleading comment claiming colon syntax is handled by the full
parser (GetSwitchKey does not split on colons for windowstyle).

Rewrite manifest test to extract embedded manifest from PE binary
instead of checking a source file that doesn't exist in $PSHOME.

Add 5 early arg scan variant tests: -w, -win, --windowstyle,
/windowstyle, UPPERCASE. Total: 12 Pester tests.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@kilasuit kilasuit added WG-Engine core PowerShell engine, interpreter, and runtime CL-Engine Indicates that a PR should be marked as an engine change in the Change Log WG-NeedsReview Needs a review by the labeled Working Group labels Mar 28, 2026
@daxian-dbw daxian-dbw requested a review from Copilot March 30, 2026 17:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to eliminate the visible console window “flash” when launching pwsh -WindowStyle Hidden on newer Windows builds by opting into detached console allocation via the app manifest and proactively allocating the console in Main() using AllocConsoleWithOptions.

Changes:

  • Add consoleAllocationPolicy=detached to pwsh’s embedded manifest to prevent OS auto-allocating a visible console on Win11 24H2+.
  • Add AllocConsoleWithOptions interop helpers and use them during early console initialization and hidden-console allocation.
  • Add a new Pester test file intended to validate manifest and -WindowStyle Hidden behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
assets/pwsh.manifest Opts into detached console allocation policy on newer Windows builds.
src/System.Management.Automation/engine/Interop/Windows/AllocConsoleWithOptions.cs Introduces P/Invoke + helpers for AllocConsoleWithOptions.
src/Microsoft.PowerShell.ConsoleHost/host/msh/ManagedEntrance.cs Adds early console allocation logic prior to EarlyStartup.Init().
src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleControl.cs Makes SetConsoleMode resilient when no console window handle exists.
src/System.Management.Automation/engine/NativeCommandProcessor.cs Prefers allocating a hidden console via AllocConsoleWithOptions(NoWindow) when needed.
test/powershell/Host/WindowStyleHidden.Tests.ps1 Adds tests for manifest presence and -WindowStyle Hidden behavior.

Comment on lines +13 to +15
$pwshExe = Join-Path -Path $PSHOME -ChildPath "pwsh.exe"
$manifest = [System.Reflection.Assembly]::LoadFile($pwshExe).GetManifestResourceStream("pwsh.exe.manifest")
if ($null -eq $manifest) {
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[System.Reflection.Assembly]::LoadFile($pwshExe) will throw for pwsh.exe because it’s not a managed assembly in typical builds; this will fail the test before the fallback logic runs. Wrap this in try/catch (e.g., BadImageFormatException) and fall back to mt.exe/source-manifest extraction when the load fails.

Copilot uses AI. Check for mistakes.
$content | Should -Match "detached"
} else {
# If mt.exe is unavailable, check the source manifest as last resort.
$srcManifest = Join-Path -Path (Split-Path $PSHOME) -ChildPath "assets/pwsh.manifest"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback path uses Split-Path $PSHOME to locate assets/pwsh.manifest, but $PSHOME points to the built/published output, not the repo root, so this path will almost always be missing and the test will be skipped. Use a repo-relative path (for example based on $PSScriptRoot) or a known build variable instead.

Suggested change
$srcManifest = Join-Path -Path (Split-Path $PSHOME) -ChildPath "assets/pwsh.manifest"
$repoRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent
$srcManifest = Join-Path -Path $repoRoot -ChildPath "assets/pwsh.manifest"

Copilot uses AI. Check for mistakes.
$content | Should -Match "consoleAllocationPolicy"
$content | Should -Match "detached"
} else {
Set-ItResult -Skipped -Because "cannot extract embedded manifest (mt.exe unavailable)"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After calling Set-ItResult -Skipped, the test should return to prevent any further execution in the It block (Pester guidance) and to avoid future edits accidentally running assertions after the skip.

Suggested change
Set-ItResult -Skipped -Because "cannot extract embedded manifest (mt.exe unavailable)"
Set-ItResult -Skipped -Because "cannot extract embedded manifest (mt.exe unavailable)"
return

Copilot uses AI. Check for mistakes.
Comment on lines +2492 to +2516
// Try AllocConsoleWithOptions with NoWindow mode first to avoid the flash
// that the AllocConsole() + ShowWindow(SW_HIDE) pattern causes.
bool allocated = Interop.Windows.TryAllocConsoleNoWindow();

bool returnValue;
if (hwnd == nint.Zero)
if (!allocated)
{
returnValue = false;
}
else
{
returnValue = true;
// Fallback for older Windows: allocate and then hide.
Interop.Windows.AllocConsole();
hwnd = Interop.Windows.GetConsoleWindow();
if (hwnd == nint.Zero)
{
return false;
}

Interop.Windows.ShowWindow(hwnd, Interop.Windows.SW_HIDE);
AlwaysCaptureApplicationIO = true;
}

AlwaysCaptureApplicationIO = true;

if (savedForeground != nint.Zero && Interop.Windows.GetForegroundWindow() != savedForeground)
{
Interop.Windows.SetForegroundWindow(savedForeground);
}

return returnValue;
return true;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AllocateHiddenConsole() now returns true whenever TryAllocConsoleNoWindow() returns true, without verifying that a console was actually allocated/attached. Previously the code validated by checking GetConsoleWindow() after allocation and returned false if it was still zero. To preserve the method’s contract, consider validating console allocation after the NoWindow call and falling back (or returning false) if no console is present.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +90
int hr = AllocConsoleWithOptions(ref options, out _);
return hr >= 0; // S_OK
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryAllocConsoleWithMode treats any HRESULT >= 0 as success and discards the AllocConsoleResult. If the API can succeed while returning NoConsole (for example in Default mode when respecting DETACHED_PROCESS), callers may need to distinguish “API succeeded but no console was allocated” from “console allocated/attached”. Consider returning the AllocConsoleResult (or a richer result type) so call sites can make the right decision.

Suggested change
int hr = AllocConsoleWithOptions(ref options, out _);
return hr >= 0; // S_OK
int hr = AllocConsoleWithOptions(ref options, out AllocConsoleResult result);
return hr >= 0 && result != AllocConsoleResult.NoConsole; // S_OK and console allocated/attached

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +172
// No console exists. This means the detached policy is active (newer Windows)
// and we were launched without console inheritance (e.g. from Explorer, Task Scheduler).
if (EarlyCheckForHiddenWindowStyle(args))
{
// Hidden: allocate an invisible console session so CONOUT$/CONIN$ work
// (Write-Host, native commands, etc.) but no window is ever shown.
if (!Interop.Windows.TryAllocConsoleNoWindow())
{
// Fallback (should not happen since we only reach here on newer Windows,
// but be defensive): alloc + hide.
Interop.Windows.AllocConsole();
nint hwnd = Interop.Windows.GetConsoleWindow();
if (hwnd != nint.Zero)
{
Interop.Windows.ShowWindow(hwnd, Interop.Windows.SW_HIDE);
}
}
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In EarlyConsoleInit, the Hidden path assumes TryAllocConsoleNoWindow() implies usable console I/O, but doesn’t verify that a console was actually created/attached. If the call returns success while allocating no console, later code that opens CONOUT$/CONIN$ can still fail. Consider verifying allocation (or using the AllocConsoleResult out value) and falling back when no console was created.

Copilot uses AI. Check for mistakes.
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe "WindowStyle Hidden console flash fix (Issue #3028)" -Tag "Feature" {
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The top-level Describe is tagged only as "Feature". CI test runs exclude the 'Feature' tag by default (tools/ci.psm1 sets ExcludeTag to Slow/Feature/Scenario for TagSet=CI), so these tests likely won’t run in CI and won’t protect this change. Consider tagging as CI (or CI + Feature) so the coverage runs in standard CI.

Suggested change
Describe "WindowStyle Hidden console flash fix (Issue #3028)" -Tag "Feature" {
Describe "WindowStyle Hidden console flash fix (Issue #3028)" -Tag @('CI','Feature') {

Copilot uses AI. Check for mistakes.
CI uses Pester 4.x which does not support -Skip on Context blocks,
causing 'A parameter cannot be found that matches parameter name Skip'
on all three platforms.

Move -Skip:(!$IsWindows) to individual It blocks (standard pattern).

Also addresses Copilot review feedback:
- Add CI tag so tests run in standard CI (not just Others)
- Remove Assembly::LoadFile path (pwsh.exe is native, not managed)
- Use repo-relative path via $PSScriptRoot for manifest fallback
- Add return after Set-ItResult -Skipped per Pester guidance

Co-authored-by: Copilot <[email protected]>
// Fallback for older Windows: allocate and then hide.
Interop.Windows.AllocConsole();
hwnd = Interop.Windows.GetConsoleWindow();
if (hwnd == nint.Zero)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new code doesn't try restoring the foreground windows before return. Could it be possible that the foreground window focus changes even if GetConsoleWindow returns IntPtr.Zero?

<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the xmlns="urn:schemas-microsoft-com:asm.v3" attribute required for the <application> node? We have the xmlns="urn:schemas-microsoft-com:asm.v1" at the <assembly> node above.

Comment on lines +144 to +150
// Console already exists (inherited from parent or auto-allocated on older Windows).
// If -WindowStyle Hidden was requested, hide the window at the earliest possible moment
// to minimize the flash on older Windows where the detached policy is not supported.
if (EarlyCheckForHiddenWindowStyle(args))
{
Interop.Windows.ShowWindow(existingConsole, Interop.Windows.SW_HIDE);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say in this case, we leave -WindowStyle to be handled in its original code path (within SetConsoleMode), and in this method, we only deal with the scenarios where existingConsole == nint.Zero.

Comment on lines +163 to +170
// Fallback (should not happen since we only reach here on newer Windows,
// but be defensive): alloc + hide.
Interop.Windows.AllocConsole();
nint hwnd = Interop.Windows.GetConsoleWindow();
if (hwnd != nint.Zero)
{
Interop.Windows.ShowWindow(hwnd, Interop.Windows.SW_HIDE);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this can happen on old Windows when the current process is created by CreateProcess with either the CREATE_NO_WINDOW or DETACHED_PROCESS flag.

Currently (without changes in this PR), when pwsh starts with the DETACHED_PROCESS flag, it silently crashes because it uses the stdin/stdout which doesn't exist in that case. I'd say we stick to that behavior to keep the code simple.

However, with the new manifest change, we cannot tell the difference between "it's a normal start and OS doesn't automatically allocate console session for pwsh" and "it's started by CreateProcess with DETACHED_PROCESS flag". For the latter case, on build 26100 or later, we will end up creating a console session if -WindowStyle hidden is specified, but no console session is created otherwise (TryAllocConsoleDefault will respect DETACHED_PROCESS), which seems inconsistent.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cannot tell the difference between "it's a normal start and OS doesn't automatically allocate console session for pwsh" and "it's started by CreateProcess with DETACHED_PROCESS flag".

That's what AllocConsoleWithOptions(... DEFAULT ...) does. It gives you a console if you were started in a way where you would have gotten a console on previous versions, and it does not give you a console if you were started DETACHED.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. AllocConsoleWithOptions literally lets you detect whether "earlier versions of windows" would have given you a console.

Copy link
Copy Markdown
Member

@daxian-dbw daxian-dbw Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When EarlyCheckForHiddenWindowStyle(args) returns true, ideally, we only want to call AllocConsoleWithOptions(No_Window) if the currently process starts normally (not by CreateProcess with DETACHED_PROCESS). But if we need to call AllocConsoleWithOptions(Default) to determine if it's a "DETACHED_PROCESS" situation, the Console Window would already show up in the normal startup case and there will be no point to call AllocConsoleWithOptions(No_Window), right?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see what you mean. Yes, AllocConsoleWithOptions(... NO_WINDOW ...) will create a console session if the parent requested a detached session.

However: I think that fulfills the contract just fine. The user gave conflicting information: "Spawn powershell with no console and a hidden console window"? The ambiguity should resolve in favor of the thing-hosted-within-PowerShell having access to console APIs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(In my opinion, somebody calling CreateProcess("powershell", ... DETACHED_PROCESS) who chooses to pass -WindowStyle is only harming themselves. They chose this. They did that.)

/// a console for CUI apps. On older Windows, AllocConsole() returns false (no-op).
/// </summary>
private static void EarlyConsoleInit(string[] args)
{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check on the build number of Windows here to simply do nothing for build prior to 26100? Let PowerShell fall back to the original code path for WindowStyle handling on old Windows.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version checks are always the wrong choice.

APIs can be backported--and this one very nearly was!--to earlier versions. The only correct way to handle this is to check whether it is available either by calling it and having it fail or by looking it up at runtime.

@DHowett
Copy link
Copy Markdown

DHowett commented Mar 30, 2026

(FWIW, as a note for the author -- I'm not just the person who recommended consoleAllocationPolicy, but the person who designed and implemented it. 🙂 )

Comment on lines +141 to +142
nint existingConsole = Interop.Windows.GetConsoleWindow();
if (existingConsole != nint.Zero)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DHowett If the process is started by CreateProcess with the CREATE_NO_WINDOW flag, it may inherit the existing console session but have no console window created for it, right?

If so, existingConsole would be nint.Zero in that case, and we will call AllocConsoleWithOptions in the new code path. I guess AllocConsoleWithOptions(Default) will return that existing console session, but what will AllocConsoleWithOptions(No_Window) do in that case?

@SufficientDaikon
Copy link
Copy Markdown
Author

(FWIW, as a note for the author -- I'm not just the person who recommended consoleAllocationPolicy, but the person who designed and implemented it. 🙂 )

Wow, how does one go about designing and architecting something that'll be used by millions? where do you even start so you make sure people's computers don't like... explode? xD

@daxian-dbw thank you so much for feedback, very very helpful, you're mentioning things i haven't even thought of.

@SufficientDaikon SufficientDaikon force-pushed the fix/windowstyle-hidden-console-flash branch 2 times, most recently from 4a930c9 to 4050f51 Compare March 31, 2026 07:14
EarlyConsoleInit: remove early ShowWindow(SW_HIDE) for existing consoles
per daxian-dbw — leave -WindowStyle handling to the existing SetConsoleMode
code path. EarlyConsoleInit now only handles the no-console case.

TryAllocConsoleWithMode: check AllocConsoleResult instead of discarding it.
Return false when result is NoConsole (DETACHED_PROCESS respected) so
callers know a console was not actually allocated.

NativeCommandProcessor: add comment clarifying foreground window restore
runs for both NoWindow and fallback paths.

Manifest: add XML comment explaining why asm.v3 xmlns is required on
the application element (distinct from root asm.v1 namespace).

Co-authored-by: Copilot <[email protected]>
@SufficientDaikon SufficientDaikon force-pushed the fix/windowstyle-hidden-console-flash branch from 4050f51 to 6214139 Compare March 31, 2026 07:43
@DHowett
Copy link
Copy Markdown

DHowett commented Mar 31, 2026

where do you even start so you make sure people's computers don't like... explode? xD

Hah, well, the jury is still out on whether I've made any computers explode. :)

Honestly though... this has been a problem that bugged me for years and years. I wrote a spec (here), forgot about it, sent it to people to get their opinions, promoted it on Twitter, got a few responses, let it sit for multiple years (it was written in 2020 and completed in 2023!)... and then when it seemed like everything was agreed, started writing the code.

Then we had a Microsoft-internal "API review" and the AllocConsoleWithOptions API changed a good amount between what I originally wrote and what we finally shipped.

If you want to see all the gory discussion details, they're over here: microsoft/terminal#7337

@SufficientDaikon
Copy link
Copy Markdown
Author

If you want to see all the gory discussion details, they're over here: microsoft/terminal#7337

You really didn't have to ask, i love this stuff, looks like I have reading material for tonight, thanks for sharing!

I'm always interested in learning how engineers solve the problems they find often times there's a fun story that comes a along with it, and it always fun to see how people even come up with soultions in the first place it been helping me in thinking how to solve my own problems, even none software people have problems to solve and they come up with soultions that I wouldn't even think to consider, so yeah I'm really excited to read that pr.

@htcfreek
Copy link
Copy Markdown

htcfreek commented Apr 2, 2026

Nice. ♥️ Will this get backported to PowerShell 5.1?

@SufficientDaikon
Copy link
Copy Markdown
Author

SufficientDaikon commented Apr 2, 2026

Nice. ♥️ Will this get backported to PowerShell 5.1?

Idk, I made this for me, but if there's a lot of people still on older versions I don't mind doing the work to backport. Lemme lookup how many powershell users are still on older versions

Edit: ooof okay, yea I'll see what I can do xD

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-Engine Indicates that a PR should be marked as an engine change in the Change Log WG-Engine core PowerShell engine, interpreter, and runtime WG-NeedsReview Needs a review by the labeled Working Group

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Powershell -WindowStyle Hidden still shows a window briefly

6 participants