/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using Microsoft.PowerShell; using Microsoft.PowerShell.Internal; using Microsoft.Win32.SafeHandles; static class PlatformWindows { public enum ConsoleBreakSignal : uint { CtrlC = 0, CtrlBreak = 1, Close = 2, Logoff = 5, Shutdown = 6, None = 255, } public enum StandardHandleId : uint { Error = unchecked((uint)-12), Output = unchecked((uint)-11), Input = unchecked((uint)-10), } [Flags] internal enum AccessQualifiers : uint { // From winnt.h GenericRead = 0x80000000, GenericWrite = 0x40000000 } internal enum CreationDisposition : uint { // From winbase.h CreateNew = 1, CreateAlways = 2, OpenExisting = 3, OpenAlways = 4, TruncateExisting = 5 } [Flags] internal enum ShareModes : uint { // From winnt.h ShareRead = 0x00000001, ShareWrite = 0x00000002 } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern IntPtr CreateFile ( string fileName, uint desiredAccess, uint ShareModes, IntPtr securityAttributes, uint creationDisposition, uint flagsAndAttributes, IntPtr templateFileWin32Handle ); [DllImport("kernel32.dll", SetLastError = true)] internal static extern int GetFileType(IntPtr handle); internal const int FILE_TYPE_CHAR = 0x0002; internal static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); // WinBase.h [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern IntPtr GetStdHandle(uint handleId); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add); delegate bool BreakHandler(ConsoleBreakSignal ConsoleBreakSignal); private static bool OnBreak(ConsoleBreakSignal signal) { if (signal == ConsoleBreakSignal.Close || signal == ConsoleBreakSignal.Shutdown) { // Set the event so ReadKey throws an exception to unwind. _singleton?._closingWaitHandle?.Set(); } return false; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct CONSOLE_FONT_INFO_EX { internal int cbSize; internal int nFont; internal short FontWidth; internal short FontHeight; internal FontFamily FontFamily; internal uint FontWeight; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] internal string FontFace; } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool GetCurrentConsoleFontEx(IntPtr consoleOutput, bool bMaximumWindow, ref CONSOLE_FONT_INFO_EX consoleFontInfo); [Flags] internal enum FontFamily : uint { // If this bit is set the font is a variable pitch font. // If this bit is clear the font is a fixed pitch font. TMPF_FIXED_PITCH = 0x01, TMPF_VECTOR = 0x02, TMPF_TRUETYPE = 0x04, TMPF_DEVICE = 0x08, LOWORDER_BITS = TMPF_FIXED_PITCH | TMPF_VECTOR | TMPF_TRUETYPE | TMPF_DEVICE, } internal static bool IsUsingRasterFont() { var handle = _outputHandle.Value.DangerousGetHandle(); var fontInfo = new CONSOLE_FONT_INFO_EX { cbSize = Marshal.SizeOf(typeof(CONSOLE_FONT_INFO_EX)) }; bool result = GetCurrentConsoleFontEx(handle, false, ref fontInfo); // From https://docs.microsoft.com/windows/win32/api/wingdi/ns-wingdi-textmetrica // tmPitchAndFamily - A monospace bitmap font has all of these low-order bits clear; return result && (fontInfo.FontFamily & FontFamily.LOWORDER_BITS) == 0; } private static PSConsoleReadLine _singleton; internal static IConsole OneTimeInit(PSConsoleReadLine singleton) { _singleton = singleton; var breakHandlerGcHandle = GCHandle.Alloc(new BreakHandler(OnBreak)); SetConsoleCtrlHandler((BreakHandler)breakHandlerGcHandle.Target, true); _enableVtOutput = !Console.IsOutputRedirected && SetConsoleOutputVirtualTerminalProcessing(); _terminalOwnerThreadId = GetTerminalOwnerThreadId(); return _enableVtOutput ? new VirtualTerminal() : new LegacyWin32Console(); } // Input modes const uint ENABLE_PROCESSED_INPUT = 0x0001; const uint ENABLE_LINE_INPUT = 0x0002; const uint ENABLE_WINDOW_INPUT = 0x0008; const uint ENABLE_MOUSE_INPUT = 0x0010; const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; // Output modes const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; static uint _prePSReadLineConsoleInputMode; // Need to remember this decision for CallUsingOurInputMode. static bool _enableVtInput; static bool _enableVtOutput; internal static void Init(ref ICharMap charMap) { if (_enableVtOutput) { // This is needed because PowerShell does not restore the console mode // after running external applications, and some popular applications // clear VT, e.g. git. SetConsoleOutputVirtualTerminalProcessing(); } // If input is redirected, we can't use console APIs and have to use VT input. if (IsHandleRedirected(stdin: true)) { EnableAnsiInput(ref charMap); } else { _prePSReadLineConsoleInputMode = GetConsoleInputMode(); // This envvar will force VT mode on or off depending on the setting 1 or 0. var overrideVtInput = Environment.GetEnvironmentVariable("PSREADLINE_VTINPUT"); if (overrideVtInput == "1") { _enableVtInput = true; } else if (overrideVtInput == "0") { _enableVtInput = false; } else { // If the console was already in VT mode, use the appropriate CharMap. // This handles the case where input was not redirected and the user // didn't specify a preference. The default is to use the pre-existing // console mode. _enableVtInput = (_prePSReadLineConsoleInputMode & ENABLE_VIRTUAL_TERMINAL_INPUT) == ENABLE_VIRTUAL_TERMINAL_INPUT; } if (_enableVtInput) { EnableAnsiInput(ref charMap); } // Is the TerminateOrphanedConsoleApps feature enabled? if (_allowedPids != null) { // We are about to disable Ctrl+C signals... so if there are still any // console-attached children, the shell will be broken until they are // gone, so we'll get rid of them: TerminateStragglers(); } SetOurInputMode(); } } internal static void SetOurInputMode() { // Clear a couple flags so we can actually receive certain keys: // ENABLE_PROCESSED_INPUT - enables Ctrl+C // ENABLE_LINE_INPUT - enables Ctrl+S // Also clear a couple flags so we don't mask the input that we ignore: // ENABLE_MOUSE_INPUT - mouse events // ENABLE_WINDOW_INPUT - window resize events var mode = _prePSReadLineConsoleInputMode & ~(ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT); if (_enableVtInput) { // If we're using VT input mode in the console, need to enable that too. // Since redirected input was handled above, this just handles the case // where the user requested VT input with the environment variable. // In this case the CharMap has already been set above. mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; } else { // We haven't enabled the ANSI escape processor, so turn this off so // the console doesn't spew escape sequences all over. mode &= ~ENABLE_VIRTUAL_TERMINAL_INPUT; } SetConsoleInputMode(mode); } private static void EnableAnsiInput(ref ICharMap charMap) { charMap = new WindowsAnsiCharMap(PSConsoleReadLine.GetOptions().AnsiEscapeTimeout); } internal static void Complete() { if (!IsHandleRedirected(stdin: true)) { SetConsoleInputMode(_prePSReadLineConsoleInputMode); } } internal static T CallPossibleExternalApplication(Func func) { if (IsHandleRedirected(stdin: true)) { // Don't bother with console modes if we're not in the console. return func(); } uint psReadLineConsoleMode = GetConsoleInputMode(); try { SetConsoleInputMode(_prePSReadLineConsoleInputMode); return func(); } finally { SetConsoleInputMode(psReadLineConsoleMode); } } internal static void CallUsingOurInputMode(Action a) { if (IsHandleRedirected(stdin: true)) { // Don't bother with console modes if we're not in the console. a(); return; } uint psReadLineConsoleMode = GetConsoleInputMode(); try { SetOurInputMode(); a(); } finally { SetConsoleInputMode(psReadLineConsoleMode); } } private static SafeFileHandle OpenConsoleHandle(string name) { // We use CreateFile here instead of GetStdWin32Handle, as GetStdWin32Handle will return redirected handles var handle = CreateFile( name, (uint)(AccessQualifiers.GenericRead | AccessQualifiers.GenericWrite), (uint)ShareModes.ShareWrite, (IntPtr)0, (uint)CreationDisposition.OpenExisting, 0, (IntPtr)0); if (handle == INVALID_HANDLE_VALUE) { int err = Marshal.GetLastWin32Error(); Win32Exception innerException = new Win32Exception(err); throw new Exception($"Failed to retrieve the console handle ({name}).", innerException); } return new SafeFileHandle(handle, true); } private static readonly Lazy _inputHandle = new Lazy(() => OpenConsoleHandle("CONIN$")); private static readonly Lazy _outputHandle = new Lazy(() => OpenConsoleHandle("CONOUT$")); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool GetConsoleMode(IntPtr hConsole, out uint dwMode); private static uint GetConsoleInputMode() { var handle = _inputHandle.Value.DangerousGetHandle(); GetConsoleMode(handle, out var result); return result; } private static uint GetConsoleOutputMode() { var handle = _outputHandle.Value.DangerousGetHandle(); GetConsoleMode(handle, out var result); return result; } [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool SetConsoleMode(IntPtr hConsole, uint dwMode); private static void SetConsoleInputMode(uint mode) { var handle = _inputHandle.Value.DangerousGetHandle(); SetConsoleMode(handle, mode); } private static bool SetConsoleOutputVirtualTerminalProcessing() { var handle = _outputHandle.Value.DangerousGetHandle(); return GetConsoleMode(handle, out uint mode) && SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); } internal static bool IsConsoleInput() { var handle = GetStdHandle((uint)StandardHandleId.Input); return GetFileType(handle) == FILE_TYPE_CHAR; } private static bool IsHandleRedirected(bool stdin) { var handle = GetStdHandle((uint)(stdin ? StandardHandleId.Input : StandardHandleId.Output)); // If handle is not to a character device, we must be redirected: if (GetFileType(handle) != FILE_TYPE_CHAR) return true; // Char device - if GetConsoleMode succeeds, we are NOT redirected. return !GetConsoleMode(handle, out var unused); } public static bool IsConsoleApiAvailable(bool input, bool output) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return false; } // If both input and output are false, we don't care about a specific // stream, just whether we're in a console or not. if (!input && !output) { return !IsHandleRedirected(stdin: true) || !IsHandleRedirected(stdin: false); } // Otherwise, we need to check the specific stream(s) that were requested. if (input && IsHandleRedirected(stdin: true)) { return false; } if (output && IsHandleRedirected(stdin: false)) { return false; } return true; } internal class LegacyWin32Console : VirtualTerminal { private static readonly ConsoleColor InitialFG = Console.ForegroundColor; private static readonly ConsoleColor InitialBG = Console.BackgroundColor; private static int maxTop = 0; private static readonly Dictionary VTColorAction = new Dictionary { {40, () => Console.BackgroundColor = ConsoleColor.Black}, {44, () => Console.BackgroundColor = ConsoleColor.DarkBlue }, {42, () => Console.BackgroundColor = ConsoleColor.DarkGreen}, {46, () => Console.BackgroundColor = ConsoleColor.DarkCyan}, {41, () => Console.BackgroundColor = ConsoleColor.DarkRed}, {45, () => Console.BackgroundColor = ConsoleColor.DarkMagenta}, {43, () => Console.BackgroundColor = ConsoleColor.DarkYellow}, {47, () => Console.BackgroundColor = ConsoleColor.Gray}, {49, () => Console.BackgroundColor = InitialBG}, {100, () => Console.BackgroundColor = ConsoleColor.DarkGray}, {104, () => Console.BackgroundColor = ConsoleColor.Blue}, {102, () => Console.BackgroundColor = ConsoleColor.Green}, {106, () => Console.BackgroundColor = ConsoleColor.Cyan}, {101, () => Console.BackgroundColor = ConsoleColor.Red}, {105, () => Console.BackgroundColor = ConsoleColor.Magenta}, {103, () => Console.BackgroundColor = ConsoleColor.Yellow}, {107, () => Console.BackgroundColor = ConsoleColor.White}, {30, () => Console.ForegroundColor = ConsoleColor.Black}, {34, () => Console.ForegroundColor = ConsoleColor.DarkBlue}, {32, () => Console.ForegroundColor = ConsoleColor.DarkGreen}, {36, () => Console.ForegroundColor = ConsoleColor.DarkCyan}, {31, () => Console.ForegroundColor = ConsoleColor.DarkRed}, {35, () => Console.ForegroundColor = ConsoleColor.DarkMagenta}, {33, () => Console.ForegroundColor = ConsoleColor.DarkYellow}, {37, () => Console.ForegroundColor = ConsoleColor.Gray}, {39, () => Console.ForegroundColor = InitialFG}, {90, () => Console.ForegroundColor = ConsoleColor.DarkGray}, {94, () => Console.ForegroundColor = ConsoleColor.Blue}, {92, () => Console.ForegroundColor = ConsoleColor.Green}, {96, () => Console.ForegroundColor = ConsoleColor.Cyan}, {91, () => Console.ForegroundColor = ConsoleColor.Red}, {95, () => Console.ForegroundColor = ConsoleColor.Magenta}, {93, () => Console.ForegroundColor = ConsoleColor.Yellow}, {97, () => Console.ForegroundColor = ConsoleColor.White}, {0, () => { Console.ForegroundColor = InitialFG; Console.BackgroundColor = InitialBG; }} }; private void WriteHelper(string s, bool line) { var from = 0; for (int i = 0; i < s.Length; i++) { // Process escapes we understand, write out (likely garbage) ones we don't. // The shortest pattern is 4 characters, [0m if (s[i] != '\x1b' || (i + 3) >= s.Length || s[i + 1] != '[') continue; var prefix = s.Substring(from, i - from); if (prefix.Length > 0) { Console.Write(prefix); maxTop = Console.CursorTop; } from = i; Action action1 = null; Action action2 = null; var j = i+2; var b = 1; var color = 0; var done = false; var invalidSequence = false; while (!done && j < s.Length) { switch (s[j]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if (b > 100) { invalidSequence = true; goto default; } color = color * b + (s[j] - '0'); b *= 10; break; case 'm': done = true; goto case ';'; case 'J': // We'll only support entire display for ED (Erase in Display) if (color == 2) { var cursorVisible = Console.CursorVisible; var left = Console.CursorLeft; var toScroll = maxTop - Console.WindowTop + 1; Console.CursorVisible = false; Console.SetCursorPosition(0, Console.WindowTop + Console.WindowHeight - 1); for (int k = 0; k < toScroll; k++) { Console.WriteLine(); } Console.SetCursorPosition(left, Console.WindowTop + toScroll - 1); Console.CursorVisible = cursorVisible; } break; case ';': if (VTColorAction.TryGetValue(color, out var action)) { if (action1 == null) action1 = action; else if (action2 == null) action2 = action; else invalidSequence = true; color = 0; b = 1; break; } else { invalidSequence = true; goto default; } default: done = true; break; } j += 1; } if (!invalidSequence) { action1?.Invoke(); action2?.Invoke(); from = j; i = j - 1; } } var tailSegment = s.Substring(from); if (line) { Console.WriteLine(tailSegment); maxTop = Console.CursorTop; } else { Console.Write(tailSegment); if (tailSegment.Length > 0) { maxTop = Console.CursorTop; } } } public override void Write(string s) { WriteHelper(s, false); } public override void WriteLine(string s) { WriteHelper(s, true); } public struct CHAR_INFO { public ushort UnicodeChar; public ushort Attributes; public CHAR_INFO(char c, ConsoleColor foreground, ConsoleColor background) { UnicodeChar = c; Attributes = (ushort)(((int)background << 4) | (int)foreground); } } public override int CursorSize { get => IsConsoleApiAvailable(input: false, output: true) ? Console.CursorSize : _unixCursorSize; set { if (IsConsoleApiAvailable(input: false, output: true)) { Console.CursorSize = value; } else { // I'm not sure the cursor is even visible, at any rate, no escape sequences supported. _unixCursorSize = value; } } } public override void BlankRestOfLine() { // This shouldn't scroll, but I'm lazy and don't feel like using a P/Invoke. var x = CursorLeft; var y = CursorTop; for (int i = 0; i < BufferWidth - x; i++) Console.Write(' '); // Last step may result in scrolling. if (CursorTop != y+1) y -= 1; SetCursorPosition(x, y); } } internal const uint SPI_GETSCREENREADER = 0x0046; [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SystemParametersInfo(uint uiAction, uint uiParam, ref bool pvParam, uint fWinIni); [StructLayout(LayoutKind.Sequential)] internal struct PROCESS_BASIC_INFORMATION { public IntPtr ExitStatus; public IntPtr PebBaseAddress; public IntPtr AffinityMask; public IntPtr BasePriority; public IntPtr UniqueProcessId; public IntPtr InheritedFromUniqueProcessId; } [DllImport("ntdll.dll")] internal static extern int NtQueryInformationProcess( IntPtr processHandle, int processInformationClass, out PROCESS_BASIC_INFORMATION processInformation, int processInformationLength, out int returnLength); internal const int InvalidProcessId = -1; internal static int GetParentPid(Process process) { // (This is how ProcessCodeMethods in pwsh does it.) var res = NtQueryInformationProcess(process.Handle, 0, out PROCESS_BASIC_INFORMATION pbi, Marshal.SizeOf(), out _); return res != 0 ? InvalidProcessId : pbi.InheritedFromUniqueProcessId.ToInt32(); } [DllImport("kernel32.dll", SetLastError = true, EntryPoint = "GetConsoleProcessList")] private static extern uint native_GetConsoleProcessList([In, Out] uint[] lpdwProcessList, uint dwProcessCount); private static uint[] GetConsoleProcessList() { int size = 100; uint[] pids = new uint[size]; uint numPids = native_GetConsoleProcessList(pids, (uint) size); if (numPids > size) { size = (int) numPids + 10; // a bit extra, since we may be racing attaches. pids = new uint[size]; numPids = native_GetConsoleProcessList(pids, (uint) size); } if (0 == numPids || numPids > size) { return null; // no TerminateOrphanedConsoleApps for you, sorry } Array.Resize(ref pids, (int) numPids); return pids; } // If the TerminateOrphanedConsoleApps option is enabled, this is the list of PIDs // that are allowed to stay attached to the console (effectively the current process // plus ancestors). private static uint[] _allowedPids; internal static void SetTerminateOrphanedConsoleApps(bool enabled) { if (enabled) { _allowedPids = GetConsoleProcessList(); } else { _allowedPids = null; } } private static bool ItLooksLikeWeAreInTerminal() { return !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("WT_SESSION")); } [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr GetConsoleWindow(); internal enum TaskbarStates { NoProgress = 0, Indeterminate = 0x1, Normal = 0x2, Error = 0x4, Paused = 0x8, } internal static class TaskbarProgress { [ComImport()] [Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface ITaskbarList3 { // ITaskbarList [PreserveSig] int HrInit(); [PreserveSig] int AddTab(IntPtr hwnd); [PreserveSig] int DeleteTab(IntPtr hwnd); [PreserveSig] int ActivateTab(IntPtr hwnd); [PreserveSig] int SetActiveAlt(IntPtr hwnd); // ITaskbarList2 [PreserveSig] int MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen); // ITaskbarList3 [PreserveSig] int SetProgressValue(IntPtr hwnd, UInt64 ullCompleted, UInt64 ullTotal); [PreserveSig] int SetProgressState(IntPtr hwnd, TaskbarStates state); // N.B. for copy/pasters: we've left out the rest of the ITaskbarList3 methods... } [ComImport()] [Guid("56fdf344-fd6d-11d0-958a-006097c9a090")] [ClassInterface(ClassInterfaceType.None)] private class TaskbarInstance { } private static Lazy _taskbarInstance = new Lazy(() => (ITaskbarList3) new TaskbarInstance()); public static int SetProgressState(IntPtr windowHandle, TaskbarStates taskbarState) { return _taskbarInstance.Value.SetProgressState(windowHandle, taskbarState); } public static int SetProgressValue(IntPtr windowHandle, int progressValue, int progressMax) { return _taskbarInstance.Value.SetProgressValue(windowHandle, (ulong) progressValue, (ulong) progressMax); } } private static readonly Lazy _myPid = new(() => { using var me = Process.GetCurrentProcess(); return (uint)me.Id; }); // Calculates what processes need to be terminated (populated into procsToTerminate), // and returns the count. A "straggler" is a console-attached process (so GUI // processes don't count) that is not in the _allowedPids list. private static int GatherStragglers(List procsToTerminate) { procsToTerminate.Clear(); // These are the processes currently attached to this console. Note that GUI // processes will not be attached to the console. uint[] currentPids = GetConsoleProcessList(); foreach (var pid in currentPids) { if (!_allowedPids.Contains(pid)) { Process proc = null; try { proc = Process.GetProcessById((int) pid); } catch (ArgumentException) { // Ignore it: process could be gone, or something else that we // likely can't do anything about it. } if (proc != null) { // Q: Why the check against the parent pid (below)? // // A: The idea is that a user could do something like this: // // $p = Start-Process pwsh -ArgumentList '-c Write-Host start $pid; sleep -seconds 30; Write-Host stop' -NoNewWindow -passThru // // Such a process *is* indeed _capable_ of wrecking the interactive prompt (all it has to do is to attempt to read input; and any output // will be interleaved with your interactive session)... but MAYBE it won't. So the idea with letting such processes live is that perhaps // the user did this on purpose, to do some sort of "background work" (even though it may not seem like the best way to do that); and we // only want to kill *actually-orphaned* processes: processes whose parent is gone, so they should be gone, too. // // We only check the immediate children processes here for simplicity. However, an immediate child process may have children that accidentally // derive the standard input (which technically is a wrong thing to do), so ideally we should check if the parent of a console-attached process // is still alive -- the parent process id points to an alive process that was created earlier. // We will wait for feedback to see if this check needs to be updated. if (GetParentPid(proc) != _myPid.Value) { procsToTerminate.Add(proc); } else { proc.Dispose(); } } } } return procsToTerminate.Count; } [DllImport("kernel32.dll")] internal static extern ulong GetTickCount64(); private static int MillisLeftUntilDeadline(ulong deadline) { long diff = (long) (deadline - GetTickCount64()); if (diff < 0) { diff = 0; } else if (diff >= (long) Int32.MaxValue) { // Should not ever actually happen... diff = DefaultGraceMillis; } return (int) diff; } private const int DefaultGraceMillis = 1000; private const int MaxRounds = 2; // // TerminateOrphanedConsoleApps // // This feature works around a bad interaction on Windows between: // * a race condition between ctrl+c and console attachment, and // * poor behavior when multiple processes want console input. // // This bad interaction is most likely to happen when the user has launched a process // that is launching many, MANY more child processes (imagine a build system, for // example): if the user types ctrl+c to cancel, all processes *currently attached* to // the console will receive the ctrl+c signal (and presumably exit). However, there // *may* have been some processes that had been created, but are not yet attached to // the console--these grandchildren will have missed the ctrl+c signal (that's the // race condition). If those grandchildren do not somehow figure out on their own that // they should exit, the console enters a highly problematic state ("the borked // state"): because pwsh's immediate child has exited, the shell will return to the // prompt and wait for input. But those straggler granchildren are ALSO attached to // the console... so when the user starts typing, who gets the input? // // It turns out that the console will just sort of randomly distribute pieces of input // between all processes who want input--a straggler grandchild process might get a // "key down" record, and then PSReadLine might get the corresponding "key up". This // is obviously untenable; it makes the shell totally unusable. (The console team has // been made aware, and there are several ideas of how to Do Better, but who knows // when any of those will come to fruition.) // // To make matters worse: when returning to the prompt, PSReadLine disables ctrl+c // signals (we prefer to handle those keys specially ourselves). So if you hit this // situation with cmd.exe as your shell, you can just mash on ctrl+c for a while and // kill all the stragglers manually; but if you have PSReadLine loaded, your shell is // borked, and you are stuck. You CAN recover, IF you can track down and kill all the // straggler processes manually. // // So when enabled, this feature does that for you: it kills all those straggler // processes, right before we disable ctrl+c signals and wait for user input, ensuring // that the user has a usable shell. // // Note that GUI processes do not attach to the console, so if you have launched // notepad, for example, TerminateOrphanedConsoleApps will never even "see" it; they // are immune from getting terminated. // // Q: But isn't terminating processes that we know nothing about kind of risky and // extreme? // // A: Perhaps so... but consider the alternative: by definition, if you get into a // situation where the TerminateOrphanedConsoleApps feature would actually kill // anything, your shell will be Completely Broken. It's "them or us": allow the // stragglers to live, but leave the user without their shell; or kill the // stragglers and give the user their shell back. There is no middle ground. So // when the TerminateOrphanedConsoleApps feature is enabled, that means the user // has opted for "give me back my shell". // // Note that we do give stragglers a small grace period before terminating them, in // case they are somehow just slow shutting down. But if you're wondering "should // we make that grace period longer?", remember that another way to think of that // period is "how long do I want the shell to potentially be unusable after // displaying the prompt?" // // Q: What if the user *didn't* type ctrl+c? // // A: We don't care. When TerminateOrphanedConsoleApps is called, all we know is that // the shell has displayed the prompt and believes it is time to wait for user // input. Whether this situation came about because of a ctrl+c, or some other // situation (for example, if the shell's immediate child crashed or was manually // killed), if there are leftover straggler processes (console-attached // grandchildren), the shell will be broken until they are gone, and thus we must // take action (if the feature is enabled). // // Q: Should this really be baked into PSReadLine, or could we leave it to some other // module to implement? (See: https://github.com/jazzdelightsme/ConsoleBouncer) // // A: We should have the option in PSReadLine. An external module can do something // very *similar* to what we do here in PSReadLine, but not quite the same, and is // strictly inferior. An external module would have to rely on receiving a ctrl+c // signal, but "there was a ctrl+c signal" is NOT equivalent to "the shell is about // to wait for input". For example, some child processes may depend on handling // ctrl+c signals, *without* exiting (kd.exe / cdb.exe, for example). In such a // case, control would not return to the shell, but an external module would have // no way to know that (hence it is inferior). That could be worked around, but // only clumsily--the user would have to have a way to tell the module "hey BTW // please don't kill these ones, even though they will *look* like stragglers". // // And in fact, an external module solution may still be attractive to some users // (and could safely be used with TerminateOrphanedConsoleApps enabled). Because // the (external solution) ConsoleBouncer module reacts to ctrl+c signals, that // makes it a bit more aggressive than what we do here: // TerminateOrphanedConsoleApps only comes into play when control has returned to // the shell, which might not be right away after the user types ctrl+c--there // might be "Terminate batch job (Y/N)?" messages, etc. So if the user understands // the limitations of the ConsoleBouncer module and has an environment where it // would be suitable, they could still opt to use it to get much more responsive // ctrl+c behavior. (A metaphor with a club: the PSReadLine built-in feature // patiently waits for the host of a private party to leave before kicking the rest // of the guests out; whereas the ConsoleBouncer, upon receipt of a ctrl+c signal, // just clears the whole place out right away (which *might* not be the right thing // to do, but you're paying them to be tough, not smart).) // private static void TerminateStragglers() { var procsToTerminate = new List(); // The theory for why more than one round might be needed is that the same race // between process creation and console attachment that could cause lingering // processes in the first place could cause us to need a second round of // cleanup... but I've never actually seen more than one round be needed. Probably // because in my specific scenario the process that was spawning processes got // taken out with the original ctrl+c signal. // // If it takes more than a few rounds of cleanup, we may be in some kind of // pathological situation, and we'll bow out. int round = 0; int killAttempts = 0; while (round++ < MaxRounds && GatherStragglers(procsToTerminate) > 0) { // We'll give them up to GracePeriodMillis for them to exit on their // own, in case they actually did receive the original ctrl+c, and are // just a tad slow shutting down. ulong deadline = GetTickCount64() + (ulong) DefaultGraceMillis; var notDeadYet = procsToTerminate.Where( (p) => !p.WaitForExit(MillisLeftUntilDeadline(deadline))); foreach (var process in notDeadYet) { try { killAttempts++; process.Kill(); } // Ignore problems; maybe it's gone already, maybe something else; // whatever. catch (InvalidOperationException) { } catch (Win32Exception) { } } foreach (var process in procsToTerminate) { process.Dispose(); } } // end retry loop // In forcible termination scenarios, if there was a child updating the terminal's // progress state, it may be left stuck that way... we can clear that out. // // The preferred way to do that is with a VT sequence, but there's no way to know // if the console we are attached to supports that sequence. If we are in Windows // Terminal, we know we can use the VT sequence; else we'll fall back to the old // (Win7-era?) COM API (which does the same thing). uint consoleMode = GetConsoleOutputMode(); if (ItLooksLikeWeAreInTerminal()) { // We can use the [semi-]standard OSC sequence: // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC if (0 != (consoleMode & (uint) ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { // Use "bell" if we actually tried to whack anything. string final = (killAttempts > 0) ? "\a" : "\x001b\\"; Console.Write("\x001b]9;4;0;0" + final); } } else { IntPtr hwnd = GetConsoleWindow(); if (hwnd != IntPtr.Zero) { int ret = TaskbarProgress.SetProgressState(hwnd, TaskbarStates.NoProgress); } } } private static uint _terminalOwnerThreadId; /// /// This method helps to find the owner thread of the terminal window used by this pwsh instance, /// by looking for a parent process whose ) is visible. /// /// The terminal process is not always the direct parent of the current process, but may be higher /// in the process tree in case this pwsh process is a child of some other console process. /// /// This works well in Windows Terminal (with profile), IntelliJ and VSCode. /// It doesn't work when PowerShell runs in conhost, or when it gets started from Start Menu with /// Windows Terminal as the default terminal application (without profile). /// private static uint GetTerminalOwnerThreadId() { try { // The window handle returned by `GetConsoleWindow` is not the correct terminal/console window for us // to query about the keyboard layout change. It's the window created for a console application, such // as `cmd` or `pwsh`, so its owner process in those cases will be `cmd` or `pwsh`. // // When we are running with conhost, this window is visible, but it's not what we want and needs to be // filtered out. When running with conhost, we want the window owned by the conhost. But unfortunately, // there is no reliable way to get the conhost process that is associated with the current pwsh, since // it's not in the parent chain of the process tree. // So, this method is supposed to always fail when running with conhost. IntPtr wrongHandle = GetConsoleWindow(); // Limit for parent process walk-up for not getting stuck in a loop (possible in case pid reuse). const int iterationLimit = 20; var process = Process.GetCurrentProcess(); for (int i = 0; i < iterationLimit; ++i) { if (process.ProcessName is "explorer") { // We've reached the root of the process tree. This can happen when PowerShell was started // from Start Menu with Windows Terminal as the default terminal application. // The `explorer` process has a visible window, but it doesn't help for getting the layout // change. Again, we need to find the terminal window owner. break; } IntPtr mainWindowHandle = process.MainWindowHandle; if (mainWindowHandle == wrongHandle) { // This can only happen when we are running with conhost. // Break early because the terminal owner process is not in the parent chain in this scenario. break; } if (mainWindowHandle != IntPtr.Zero && IsWindowVisible(mainWindowHandle)) { // The window is visible, so it's likely the terminal window. return GetWindowThreadProcessId(process.MainWindowHandle, out _); } // When reaching here, the main window of the process: // - doesn't exist, or // - exists but invisible // So, this is likely not a terminal process. // Now we get its parent process and continue with the check. int parentId = GetParentPid(process); process = Process.GetProcessById(parentId); } } catch (Exception) { // No access to the process, or the process is already dead. // Either way, we cannot determine the owner thread of the terminal window. } // We could not find the owner thread/process of the terminal window in following scenarios: // 1. pwsh is running with conhost. // This happens when conhost is set as the default terminal application, and a user starts pwsh // from the Start Menu, or with `win+r` (run code) and etc. // // 2. pwsh is running with Windows Terminal, but was not started from a Windows Terminal profile. // This happens when Windows Terminal is set as the default terminal application, and a user // starts pwsh from the Start Menu, or with `win+r` (run code) and etc. // The `WindowsTerminal` process is not in the parent process chain in this case. // // 3. pwsh's parent process chain is broken -- a parent was terminated so we cannot walk up the chain. return 0; } internal static IntPtr GetConsoleKeyboardLayout() { return GetKeyboardLayout(_terminalOwnerThreadId); } [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("User32.dll", SetLastError = true)] private static extern IntPtr GetKeyboardLayout(uint idThread); [DllImport("user32.dll", SetLastError = true)] private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint proccess); }