mirror of
https://github.com/PowerShell/PSReadLine
synced 2026-06-08 12:12:13 +00:00
1121 lines
45 KiB
C#
1121 lines
45 KiB
C#
/********************************************************************++
|
|
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<T>(Func<T> 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<SafeFileHandle> _inputHandle = new Lazy<SafeFileHandle>(() => OpenConsoleHandle("CONIN$"));
|
|
private static readonly Lazy<SafeFileHandle> _outputHandle = new Lazy<SafeFileHandle>(() => 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<int, Action> VTColorAction = new Dictionary<int, Action> {
|
|
{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, <ESC>[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<PROCESS_BASIC_INFORMATION>(), 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<ITaskbarList3> _taskbarInstance = new Lazy<ITaskbarList3>(() => (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<uint> _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<Process> 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<Process>();
|
|
|
|
// 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;
|
|
|
|
/// <remarks>
|
|
/// This method helps to find the owner thread of the terminal window used by this pwsh instance,
|
|
/// by looking for a parent process whose <see cref="Process.MainWindowHandle"/>) 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).
|
|
/// </remarks>
|
|
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);
|
|
}
|