Skip to content
6 changes: 6 additions & 0 deletions assets/pwsh.manifest
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
</application>
</compatibility>
<!-- The asm.v3 xmlns is required for windowsSettings; it is distinct from the root asm.v1 namespace. -->
<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.

<windowsSettings>
<consoleAllocationPolicy xmlns="http://schemas.microsoft.com/SMI/2024/WindowsSettings">detached</consoleAllocationPolicy>
</windowsSettings>
</application>
</assembly>
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,14 @@ internal enum KeyboardFlag : uint
internal static void SetConsoleMode(ProcessWindowStyle style)
{
IntPtr hwnd = GetConsoleWindow();
Dbg.Assert(hwnd != IntPtr.Zero, "Console handle should never be zero");
if (hwnd == IntPtr.Zero)
{
// No console window to modify. This can happen when running with
// consoleAllocationPolicy=detached before the console is allocated,
// or in a GUI-hosted scenario.
return;
}

switch (style)
{
case ProcessWindowStyle.Hidden:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ public static int Start([MarshalAs(UnmanagedType.LPArray, ArraySubType = Unmanag
{
ArgumentNullException.ThrowIfNull(args);

#if !UNIX
// On Windows with consoleAllocationPolicy=detached in the manifest,
// no console is auto-allocated by the OS. We must allocate one ourselves
// before anything touches CONOUT$/CONIN$ handles.
// On older Windows the manifest element is ignored and the OS auto-allocates
// a console, so GetConsoleWindow() != 0 and EarlyConsoleInit is a no-op.
EarlyConsoleInit(args);
#endif

#if DEBUG
if (args.Length > 0 && !string.IsNullOrEmpty(args[0]) && args[0]!.Equals("-isswait", StringComparison.OrdinalIgnoreCase))
{
Expand Down Expand Up @@ -120,5 +129,95 @@ public static int Start([MarshalAs(UnmanagedType.LPArray, ArraySubType = Unmanag

return exitCode;
}

#if !UNIX
/// <summary>
/// Allocates a console early in startup to support consoleAllocationPolicy=detached.
/// On newer Windows (with the detached policy active), the OS does not auto-allocate
/// a console for CUI apps. On older Windows the manifest is ignored, so the OS
/// auto-allocates a console and GetConsoleWindow() returns non-zero (early return).
/// </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.

nint existingConsole = Interop.Windows.GetConsoleWindow();
if (existingConsole != nint.Zero)
Comment on lines +142 to +143
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?

{
// Console already exists (inherited from parent or auto-allocated on older Windows).
// Leave -WindowStyle handling to the existing SetConsoleMode code path.
return;
}

// No console exists (GetConsoleWindow() == 0). This means either:
// (a) The detached manifest policy is active (newer Windows), or
// (b) DETACHED_PROCESS — no console at all, or
// (c) CREATE_NO_WINDOW — console session exists but no window.
//
// For (c), AllocConsoleWithOptions returns ExistingConsole (no-op).
// For (a) and (b), behavior depends on the mode:
// Default mode: allocates if the parent would have given us a console
// on prior Windows versions, returns NoConsole for DETACHED_PROCESS.
// NoWindow mode: always creates a console session (overrides DETACHED).
//
// When -WindowStyle Hidden is specified, we intentionally use NoWindow
// even though it overrides DETACHED_PROCESS — the user explicitly asked
// for invisible PowerShell with working I/O, and the alternative (no
// console, crashing on stdin/stdout access) is strictly worse.
//
// If the API is not available (older Windows), TryAlloc* returns false.
// On older Windows the manifest is ignored and the OS auto-allocates
// a console, so GetConsoleWindow() would have returned non-zero above.
// The only older-Windows path here is DETACHED_PROCESS, where the
// existing behavior is no console I/O; we preserve that by not
// falling back to plain AllocConsole() (per DHowett's guidance).
if (EarlyCheckForHiddenWindowStyle(args))
{
Interop.Windows.TryAllocConsoleNoWindow();
}
else
{
Interop.Windows.TryAllocConsoleDefault();
}
}

/// <summary>
/// Minimal early scan for -WindowStyle Hidden in command line args.
/// Matches any unambiguous prefix of "windowstyle" starting from "w"
/// (e.g. -w, -wi, -win, ..., -windowstyle, --windowstyle) followed by "hidden".
/// This is a best-effort check that runs before the full parser. False positives
/// (e.g. a hypothetical future -w parameter) are acceptable because the worst case
/// is allocating a hidden console that the full parser would later show.
/// </summary>
private static bool EarlyCheckForHiddenWindowStyle(string[] args)
{
for (int i = 0; i < args.Length - 1; i++)
{
string arg = args[i];
if (arg.Length >= 2 && (arg[0] == '-' || arg[0] == '/'))
{
int start = 1;

// Strip second dash for --windowstyle (matches full parser behavior).
if (arg.Length >= 3 && arg[0] == '-' && arg[1] == '-')
{
start = 2;
}

ReadOnlySpan<char> key = arg.AsSpan(start);
if (key.Length >= 1
&& key.Length <= "windowstyle".Length
&& "windowstyle".AsSpan().StartsWith(key, StringComparison.OrdinalIgnoreCase))
{
if (args[i + 1].Equals("hidden", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
}

return false;
}

#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable

using System;
using System.Runtime.InteropServices;

internal static partial class Interop
{
internal static unsafe partial class Windows
{
/// <summary>Console allocation mode for AllocConsoleWithOptions.</summary>
internal enum AllocConsoleMode : int
{
/// <summary>Allocate only if the parent process requested it.</summary>
Default = 0,

/// <summary>Force allocation of a console with a visible window.</summary>
NewWindow = 1,

/// <summary>Allocate console I/O handles without creating a visible window.</summary>
NoWindow = 2,
}

/// <summary>Result of an AllocConsoleWithOptions call.</summary>
internal enum AllocConsoleResult : int
{
/// <summary>No console was allocated.</summary>
NoConsole = 0,

/// <summary>A new console session was created.</summary>
NewConsole = 1,

/// <summary>An existing console session was attached.</summary>
ExistingConsole = 2,
}

/// <summary>Options struct passed to AllocConsoleWithOptions.</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct AllocConsoleOptions
{
/// <summary>The allocation mode (Default, NewWindow, or NoWindow).</summary>
public AllocConsoleMode Mode;

/// <summary>If non-zero, the ShowWindow field is used as the initial show state.</summary>
public int UseShowWindow;

/// <summary>The initial show state (e.g. SW_HIDE) when UseShowWindow is set.</summary>
public ushort ShowWindow;
}

[LibraryImport("kernel32.dll")]
internal static partial int AllocConsoleWithOptions(
ref AllocConsoleOptions allocOptions,
out AllocConsoleResult result);

/// <summary>
/// Attempts to allocate a console without a visible window using AllocConsoleWithOptions.
/// Returns false if the API is not available (older Windows), the call fails,
/// or no console was allocated (e.g. process was started with DETACHED_PROCESS).
/// </summary>
internal static bool TryAllocConsoleNoWindow()
{
return TryAllocConsoleWithMode(AllocConsoleMode.NoWindow);
}

/// <summary>
/// Attempts to allocate a console using AllocConsoleWithOptions with Default mode.
/// Default mode respects DETACHED_PROCESS from the parent's CreateProcess call:
/// it returns NoConsole if the parent intended this process to run without a console,
/// whereas plain AllocConsole() would override that and force-create a console.
/// Returns false if the API is not available (older Windows), the call fails,
/// or no console was allocated.
/// </summary>
internal static bool TryAllocConsoleDefault()
{
return TryAllocConsoleWithMode(AllocConsoleMode.Default);
}

private static bool TryAllocConsoleWithMode(AllocConsoleMode mode)
{
try
{
var options = new AllocConsoleOptions
{
Mode = mode,
UseShowWindow = 0,
ShowWindow = 0,
};

int hr = AllocConsoleWithOptions(ref options, out AllocConsoleResult result);
return hr >= 0 && result != AllocConsoleResult.NoConsole;
}
catch (EntryPointNotFoundException)
{
return false;
}
}
}
}
30 changes: 16 additions & 14 deletions src/System.Management.Automation/engine/NativeCommandProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2489,30 +2489,32 @@ internal static bool AllocateHiddenConsole()
// save the foreground window since allocating a console window might remove focus from it
IntPtr savedForeground = Interop.Windows.GetForegroundWindow();

// Since there is no console window, allocate and then hide it...
// Suppress the PreFAST warning about not using Marshal.GetLastWin32Error() to
// get the error code.
Interop.Windows.AllocConsole();
hwnd = Interop.Windows.GetConsoleWindow();
// 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)
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?

{
return false;
}

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

AlwaysCaptureApplicationIO = true;

// Restore foreground window if focus changed during console allocation.
if (savedForeground != nint.Zero && Interop.Windows.GetForegroundWindow() != savedForeground)
{
Interop.Windows.SetForegroundWindow(savedForeground);
}

return returnValue;
return true;
Comment on lines +2492 to +2517
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.
#endif
}
}
Expand Down
Loading
Loading