Repository navigation
Fix -WindowStyle Hidden console window flash #27111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
5dfe5ae
4b4a8ef
1a86fe6
b2a7b30
86735ad
87933d3
6214139
add7938
4025927
6b44701
8b2913a
29560fa
76e8650
c938459
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 this is a no-op | ||
| // (AllocConsole returns false when a console already exists). | ||
| EarlyConsoleInit(args); | ||
| #endif | ||
|
|
||
| #if DEBUG | ||
| if (args.Length > 0 && !string.IsNullOrEmpty(args[0]) && args[0]!.Equals("-isswait", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
|
|
@@ -120,5 +129,99 @@ 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, AllocConsole() returns false (no-op). | ||
| /// </summary> | ||
| private static void EarlyConsoleInit(string[] args) | ||
| { | ||
|
jshigetomi marked this conversation as resolved.
|
||
| nint existingConsole = Interop.Windows.GetConsoleWindow(); | ||
| if (existingConsole != nint.Zero) | ||
|
Comment on lines
+142
to
+143
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dustin L. Howett (@DHowett) If the process is started by If so, |
||
| { | ||
| // 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); | ||
| } | ||
|
jshigetomi marked this conversation as resolved.
Outdated
|
||
|
|
||
| return; | ||
| } | ||
|
|
||
| // 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); | ||
| } | ||
|
jshigetomi marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
jshigetomi marked this conversation as resolved.
Outdated
|
||
| else | ||
| { | ||
| // Normal interactive launch: allocate a visible console. | ||
| // Use AllocConsoleWithOptions(Default) when available — it respects | ||
| // DETACHED_PROCESS from the parent's CreateProcess call, whereas | ||
| // plain AllocConsole() would override it and force-create a console. | ||
| if (!Interop.Windows.TryAllocConsoleDefault()) | ||
| { | ||
| Interop.Windows.AllocConsole(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <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,98 @@ | ||
| // 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) or the call fails. | ||
| /// </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, | ||
| /// whereas plain AllocConsole() would override it and force-create a console. | ||
| /// Returns false if the API is not available (older Windows) or the call fails. | ||
| /// </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 _); | ||
| return hr >= 0; // S_OK | ||
|
jshigetomi marked this conversation as resolved.
Outdated
|
||
| } | ||
| catch (EntryPointNotFoundException) | ||
| { | ||
| return false; | ||
|
SufficientDaikon marked this conversation as resolved.
|
||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2489,30 +2489,31 @@ 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) | ||
|
SufficientDaikon marked this conversation as resolved.
|
||
| { | ||
| 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; | ||
|
Comment on lines
+2492
to
+2516
|
||
| #endif | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.