From 0275649d350bcdc6953215598eca775b4882ece5 Mon Sep 17 00:00:00 2001
From: Tim Cassell <35501420+timcassell@users.noreply.github.com>
Date: Sat, 20 Jul 2024 04:16:11 -0400
Subject: [PATCH] =?UTF-8?q?=EF=BB=BFFixed=20crash=20from=20TaskbarProgress?=
=?UTF-8?q?=20when=20BuiltInComInteropSupport=20is=20disabled.=20(#2255)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixed terminal sequence printed to console in legacy console.
Include progress in error and warning states.
---
.../Helpers/Taskbar/TaskbarProgress.cs | 197 -------------
.../Helpers/TaskbarProgress.cs | 258 ++++++++++++++++++
.../Running/BenchmarkRunnerClean.cs | 3 +-
3 files changed, 259 insertions(+), 199 deletions(-)
delete mode 100644 src/BenchmarkDotNet/Helpers/Taskbar/TaskbarProgress.cs
create mode 100644 src/BenchmarkDotNet/Helpers/TaskbarProgress.cs
diff --git a/src/BenchmarkDotNet/Helpers/Taskbar/TaskbarProgress.cs b/src/BenchmarkDotNet/Helpers/Taskbar/TaskbarProgress.cs
deleted file mode 100644
index 19ad5f6ffe..0000000000
--- a/src/BenchmarkDotNet/Helpers/Taskbar/TaskbarProgress.cs
+++ /dev/null
@@ -1,197 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-
-namespace BenchmarkDotNet.Helpers
-{
- internal class TaskbarProgress : IDisposable
- {
- private static readonly bool OsVersionIsSupported = Portability.RuntimeInformation.IsWindows()
- // Must be windows 7 or greater
- && Environment.OSVersion.Version >= new Version(6, 1);
-
- private IntPtr consoleWindowHandle = IntPtr.Zero;
- private IntPtr consoleHandle = IntPtr.Zero;
-
- [DllImport("kernel32.dll")]
- private static extern IntPtr GetConsoleWindow();
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern IntPtr GetStdHandle(int nStdHandle);
-
- private const int STD_OUTPUT_HANDLE = -11;
-
- internal TaskbarProgress()
- {
- if (OsVersionIsSupported)
- {
- consoleWindowHandle = GetConsoleWindow();
- consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
- Console.CancelKeyPress += OnConsoleCancelEvent;
- }
- }
-
- internal void SetState(TaskbarProgressState state)
- {
- if (OsVersionIsSupported)
- {
- TaskbarProgressCom.SetState(consoleWindowHandle, consoleHandle, state);
- }
- }
-
- internal void SetProgress(float progressValue)
- {
- if (OsVersionIsSupported)
- {
- TaskbarProgressCom.SetValue(consoleWindowHandle, consoleHandle, progressValue);
- }
- }
-
- private void OnConsoleCancelEvent(object sender, ConsoleCancelEventArgs e)
- {
- Dispose();
- }
-
- public void Dispose()
- {
- if (OsVersionIsSupported)
- {
- TaskbarProgressCom.SetState(consoleWindowHandle, consoleHandle, TaskbarProgressState.NoProgress);
- consoleWindowHandle = IntPtr.Zero;
- consoleHandle = IntPtr.Zero;
- Console.CancelKeyPress -= OnConsoleCancelEvent;
- }
- }
- }
-
- internal enum TaskbarProgressState
- {
- NoProgress = 0,
- Indeterminate = 0x1,
- Normal = 0x2,
- Error = 0x4,
- Paused = 0x8,
- Warning = Paused
- }
-
- internal static class TaskbarProgressCom
- {
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out ConsoleModes lpMode);
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern bool SetConsoleMode(IntPtr hConsoleHandle, ConsoleModes dwMode);
-
- [Flags]
- private enum ConsoleModes : uint
- {
- ENABLE_PROCESSED_INPUT = 0x0001,
- ENABLE_LINE_INPUT = 0x0002,
- ENABLE_ECHO_INPUT = 0x0004,
- ENABLE_WINDOW_INPUT = 0x0008,
- ENABLE_MOUSE_INPUT = 0x0010,
- ENABLE_INSERT_MODE = 0x0020,
- ENABLE_QUICK_EDIT_MODE = 0x0040,
- ENABLE_EXTENDED_FLAGS = 0x0080,
- ENABLE_AUTO_POSITION = 0x0100,
-
- ENABLE_PROCESSED_OUTPUT = 0x0001,
- ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002,
- ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004,
- DISABLE_NEWLINE_AUTO_RETURN = 0x0008,
- ENABLE_LVB_GRID_WORLDWIDE = 0x0010
- }
-
- [ComImport]
- [Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface ITaskbarList3
- {
- // ITaskbarList
- [PreserveSig]
- void HrInit();
- [PreserveSig]
- void AddTab(IntPtr hwnd);
- [PreserveSig]
- void DeleteTab(IntPtr hwnd);
- [PreserveSig]
- void ActivateTab(IntPtr hwnd);
- [PreserveSig]
- void SetActiveAlt(IntPtr hwnd);
-
- // ITaskbarList2
- [PreserveSig]
- void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
-
- // ITaskbarList3
- [PreserveSig]
- void SetProgressValue(IntPtr hwnd, ulong ullCompleted, ulong ullTotal);
- [PreserveSig]
- void SetProgressState(IntPtr hwnd, TaskbarProgressState state);
- }
-
- [Guid("56FDF344-FD6D-11d0-958A-006097C9A090")]
- [ClassInterface(ClassInterfaceType.None)]
- [ComImport]
- private class TaskbarInstance
- {
- }
-
- private static readonly ITaskbarList3 s_taskbarInstance = (ITaskbarList3) new TaskbarInstance();
-
- internal static void SetState(IntPtr consoleWindowHandle, IntPtr consoleHandle, TaskbarProgressState taskbarState)
- {
- if (consoleWindowHandle != IntPtr.Zero)
- {
- s_taskbarInstance.SetProgressState(consoleWindowHandle, taskbarState);
- }
-
- if (consoleHandle != IntPtr.Zero)
- {
- // Write progress state to console for Windows Terminal (https://github.com/microsoft/terminal/issues/6700).
- GetConsoleMode(consoleHandle, out ConsoleModes previousConsoleMode);
- SetConsoleMode(consoleHandle, ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING | ConsoleModes.ENABLE_PROCESSED_OUTPUT);
- switch (taskbarState)
- {
- case TaskbarProgressState.NoProgress:
- Console.Write("\x1b]9;4;0;0\x1b\\");
- break;
- case TaskbarProgressState.Indeterminate:
- Console.Write("\x1b]9;4;3;0\x1b\\");
- break;
- case TaskbarProgressState.Normal:
- // Do nothing, this is set automatically when SetValue is called (and WT has no documented way to set this).
- break;
- case TaskbarProgressState.Error:
- Console.Write("\x1b]9;4;2;0\x1b\\");
- break;
- case TaskbarProgressState.Warning:
- Console.Write("\x1b]9;4;4;0\x1b\\");
- break;
- }
- SetConsoleMode(consoleHandle, previousConsoleMode);
- }
- }
-
- internal static void SetValue(IntPtr consoleWindowHandle, IntPtr consoleHandle, float progressValue)
- {
- bool isValidRange = progressValue >= 0 & progressValue <= 1;
- if (!isValidRange)
- {
- throw new ArgumentOutOfRangeException(nameof(progressValue), "progressValue must be between 0 and 1 inclusive.");
- }
- uint value = (uint) (progressValue * 100);
-
- if (consoleWindowHandle != IntPtr.Zero)
- {
- s_taskbarInstance.SetProgressValue(consoleWindowHandle, value, 100);
- }
-
- if (consoleHandle != IntPtr.Zero)
- {
- // Write progress sequence to console for Windows Terminal (https://github.com/microsoft/terminal/discussions/14268).
- GetConsoleMode(consoleHandle, out ConsoleModes previousConsoleMode);
- SetConsoleMode(consoleHandle, ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING | ConsoleModes.ENABLE_PROCESSED_OUTPUT);
- Console.Write($"\x1b]9;4;1;{value}\x1b\\");
- SetConsoleMode(consoleHandle, previousConsoleMode);
- }
- }
- }
-}
diff --git a/src/BenchmarkDotNet/Helpers/TaskbarProgress.cs b/src/BenchmarkDotNet/Helpers/TaskbarProgress.cs
new file mode 100644
index 0000000000..f66e21cdf6
--- /dev/null
+++ b/src/BenchmarkDotNet/Helpers/TaskbarProgress.cs
@@ -0,0 +1,258 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace BenchmarkDotNet.Helpers
+{
+ internal enum TaskbarProgressState
+ {
+ NoProgress = 0,
+ Indeterminate = 0x1,
+ Normal = 0x2,
+ Error = 0x4,
+ Paused = 0x8,
+ Warning = Paused
+ }
+
+ internal class TaskbarProgress : IDisposable
+ {
+ private static readonly bool OsVersionIsSupported = Portability.RuntimeInformation.IsWindows()
+ // Must be windows 7 or greater
+ && Environment.OSVersion.Version >= new Version(6, 1);
+
+ private Com? com;
+ private Terminal? terminal;
+
+ private bool IsEnabled => com != null || terminal != null;
+
+ internal TaskbarProgress(TaskbarProgressState initialTaskbarState)
+ {
+ if (OsVersionIsSupported)
+ {
+ com = Com.MaybeCreateInstanceAndSetInitialState(initialTaskbarState);
+ terminal = Terminal.MaybeCreateInstanceAndSetInitialState(initialTaskbarState);
+ if (IsEnabled)
+ {
+ Console.CancelKeyPress += OnConsoleCancelEvent;
+ }
+ }
+ }
+
+ internal void SetState(TaskbarProgressState state)
+ {
+ com?.SetState(state);
+ terminal?.SetState(state);
+ }
+
+ internal void SetProgress(float progressValue)
+ {
+ bool isValidRange = progressValue >= 0 & progressValue <= 1;
+ if (!isValidRange)
+ {
+ throw new ArgumentOutOfRangeException(nameof(progressValue), "progressValue must be between 0 and 1 inclusive.");
+ }
+ uint value = (uint) (progressValue * 100);
+ com?.SetValue(value);
+ terminal?.SetValue(value);
+ }
+
+ private void OnConsoleCancelEvent(object sender, ConsoleCancelEventArgs e)
+ => Dispose();
+
+ public void Dispose()
+ {
+ if (IsEnabled)
+ {
+ SetState(TaskbarProgressState.NoProgress);
+ com = null;
+ terminal = null;
+ Console.CancelKeyPress -= OnConsoleCancelEvent;
+ }
+ }
+
+ private sealed class Com
+ {
+ [ComImport]
+ [Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ private interface ITaskbarList3
+ {
+ // ITaskbarList
+ [PreserveSig]
+ void HrInit();
+ [PreserveSig]
+ void AddTab(IntPtr hwnd);
+ [PreserveSig]
+ void DeleteTab(IntPtr hwnd);
+ [PreserveSig]
+ void ActivateTab(IntPtr hwnd);
+ [PreserveSig]
+ void SetActiveAlt(IntPtr hwnd);
+
+ // ITaskbarList2
+ [PreserveSig]
+ void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
+
+ // ITaskbarList3
+ [PreserveSig]
+ void SetProgressValue(IntPtr hwnd, ulong ullCompleted, ulong ullTotal);
+ [PreserveSig]
+ void SetProgressState(IntPtr hwnd, TaskbarProgressState state);
+ }
+
+ [Guid("56FDF344-FD6D-11d0-958A-006097C9A090")]
+ [ClassInterface(ClassInterfaceType.None)]
+ [ComImport]
+ private class TaskbarInstance { }
+
+ [DllImport("kernel32.dll")]
+ private static extern IntPtr GetConsoleWindow();
+
+ private readonly ITaskbarList3 taskbarInstance;
+ private readonly IntPtr consoleWindowHandle;
+
+ private Com(IntPtr handle)
+ {
+ taskbarInstance = (ITaskbarList3) new TaskbarInstance();
+ consoleWindowHandle = handle;
+ }
+
+ internal static Com? MaybeCreateInstanceAndSetInitialState(TaskbarProgressState initialTaskbarState)
+ {
+ try
+ {
+ IntPtr handle = GetConsoleWindow();
+ if (handle == IntPtr.Zero)
+ {
+ return null;
+ }
+ var com = new Com(handle);
+ com.SetState(initialTaskbarState);
+ return com;
+ }
+ // COM may be disabled, in which case this will throw (#2253).
+ // It could be NotSupportedException or COMException, we just catch all.
+ catch
+ {
+ return null;
+ }
+ }
+
+ internal void SetState(TaskbarProgressState taskbarState)
+ => taskbarInstance.SetProgressState(consoleWindowHandle, taskbarState);
+
+ ///
+ /// Sets the progress value out of 100.
+ ///
+ internal void SetValue(uint progressValue)
+ => taskbarInstance.SetProgressValue(consoleWindowHandle, progressValue, 100);
+ }
+
+ private sealed class Terminal
+ {
+ [Flags]
+ private enum ConsoleModes : uint
+ {
+ ENABLE_PROCESSED_INPUT = 0x0001,
+ ENABLE_LINE_INPUT = 0x0002,
+ ENABLE_ECHO_INPUT = 0x0004,
+ ENABLE_WINDOW_INPUT = 0x0008,
+ ENABLE_MOUSE_INPUT = 0x0010,
+ ENABLE_INSERT_MODE = 0x0020,
+ ENABLE_QUICK_EDIT_MODE = 0x0040,
+ ENABLE_EXTENDED_FLAGS = 0x0080,
+ ENABLE_AUTO_POSITION = 0x0100,
+
+ ENABLE_PROCESSED_OUTPUT = 0x0001,
+ ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002,
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004,
+ DISABLE_NEWLINE_AUTO_RETURN = 0x0008,
+ ENABLE_LVB_GRID_WORLDWIDE = 0x0010
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out ConsoleModes lpMode);
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool SetConsoleMode(IntPtr hConsoleHandle, ConsoleModes dwMode);
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern IntPtr GetStdHandle(int nStdHandle);
+ private const int STD_OUTPUT_HANDLE = -11;
+
+ private readonly IntPtr consoleHandle;
+ private uint currentProgress;
+
+ private Terminal(IntPtr handle)
+ => consoleHandle = handle;
+
+ internal static Terminal? MaybeCreateInstanceAndSetInitialState(TaskbarProgressState initialTaskbarState)
+ {
+ IntPtr handle = GetStdHandle(STD_OUTPUT_HANDLE);
+ if (handle == IntPtr.Zero)
+ {
+ return null;
+ }
+ if (!GetConsoleMode(handle, out ConsoleModes previousConsoleMode)
+ || !SetConsoleMode(handle, ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING | ConsoleModes.ENABLE_PROCESSED_OUTPUT))
+ {
+ // If we failed to set virtual terminal processing mode, it is likely due to an older Windows version that does not support it,
+ // or legacy console. In either case the TaskbarProgressCom will take care of the progress. See https://stackoverflow.com/a/44574463/5703407.
+ // If we try to write without VT mode, the sequence will be printed for the user to see, which clutters the output.
+ return null;
+ }
+ var terminal = new Terminal(handle);
+ terminal.WriteStateSequence(initialTaskbarState);
+ SetConsoleMode(handle, previousConsoleMode);
+ return terminal;
+ }
+
+ internal void SetState(TaskbarProgressState taskbarState)
+ {
+ GetConsoleMode(consoleHandle, out ConsoleModes previousConsoleMode);
+ SetConsoleMode(consoleHandle, ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING | ConsoleModes.ENABLE_PROCESSED_OUTPUT);
+ WriteStateSequence(taskbarState);
+ SetConsoleMode(consoleHandle, previousConsoleMode);
+ }
+
+ private void WriteStateSequence(TaskbarProgressState taskbarState)
+ {
+ // Write progress state to console for Windows Terminal (https://github.com/microsoft/terminal/issues/6700).
+ switch (taskbarState)
+ {
+ case TaskbarProgressState.NoProgress:
+ currentProgress = 100;
+ Console.Write("\x1b]9;4;0;0\x1b\\");
+ break;
+ case TaskbarProgressState.Indeterminate:
+ currentProgress = 100;
+ Console.Write("\x1b]9;4;3;0\x1b\\");
+ break;
+ case TaskbarProgressState.Normal:
+ // Normal state is set when progress is set.
+ WriteProgressSequence(currentProgress);
+ break;
+ case TaskbarProgressState.Error:
+ Console.Write($"\x1b]9;4;2;{currentProgress}\x1b\\");
+ break;
+ case TaskbarProgressState.Warning:
+ Console.Write($"\x1b]9;4;4;{currentProgress}\x1b\\");
+ break;
+ }
+ }
+
+ ///
+ /// Sets the progress value out of 100.
+ ///
+ internal void SetValue(uint progressValue)
+ {
+ currentProgress = progressValue;
+ // Write progress sequence to console for Windows Terminal (https://github.com/microsoft/terminal/discussions/14268).
+ GetConsoleMode(consoleHandle, out ConsoleModes previousConsoleMode);
+ SetConsoleMode(consoleHandle, ConsoleModes.ENABLE_VIRTUAL_TERMINAL_PROCESSING | ConsoleModes.ENABLE_PROCESSED_OUTPUT);
+ WriteProgressSequence(progressValue);
+ SetConsoleMode(consoleHandle, previousConsoleMode);
+ }
+
+ private static void WriteProgressSequence(uint progressValue)
+ => Console.Write($"\x1b]9;4;1;{progressValue}\x1b\\");
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
index 7ea1932794..4a1e85eee9 100644
--- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
+++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
@@ -37,8 +37,7 @@ internal static class BenchmarkRunnerClean
internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos)
{
- using var taskbarProgress = new TaskbarProgress();
- taskbarProgress.SetState(TaskbarProgressState.Indeterminate);
+ using var taskbarProgress = new TaskbarProgress(TaskbarProgressState.Indeterminate);
var resolver = DefaultResolver;
var artifactsToCleanup = new List();