From a248b8750018b5c89668e594533b95e583149c4b Mon Sep 17 00:00:00 2001 From: Fernando Cerqueira Date: Mon, 10 Aug 2026 11:27:17 -0300 Subject: [PATCH 1/3] Give Ctrl+C a bounded grace period before exiting Console_CancelKeyPress cancelled the token and called Environment.Exit immediately, with no way for in-flight render/cleanup code to finish first. Add ConsolePlus.BeginCriticalRender(), a disposable scope that makes the handler wait up to 300ms after cancelling before forcing the process to exit. Co-Authored-By: Claude Sonnet 5 --- src/ConsolePlus.Startup.cs | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/ConsolePlus.Startup.cs b/src/ConsolePlus.Startup.cs index 643f5fe..be8dac1 100644 --- a/src/ConsolePlus.Startup.cs +++ b/src/ConsolePlus.Startup.cs @@ -8,6 +8,7 @@ using System; using System.Globalization; using System.Text; +using System.Threading; namespace ConsolePlusLibrary { @@ -26,6 +27,19 @@ public static partial class ConsolePlus private static readonly ProfileConsole _profile; private static bool _ctrlCPress; + private static readonly object _criticalRenderLock = new(); + private static readonly ManualResetEventSlim _criticalRenderIdle = new(true); + private static int _criticalRenderCount; + + /// + /// Upper bound on how long waits for in-flight + /// critical render sections (see ) to finish before + /// forcing the process to exit. Bounded so Ctrl+C can never hang: a stuck or crashed + /// render section simply loses its cleanup, exactly as it did before this mechanism + /// existed. + /// + private static readonly TimeSpan _criticalRenderGracePeriod = TimeSpan.FromMilliseconds(300); + private static readonly IConsole _consoledrive; private static readonly string _originalCulture; private static readonly ConsoleColor _originalForecolor; @@ -94,12 +108,67 @@ private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArg { if (!e.Cancel) { + // Cancel first so a thread inside a critical render section (see + // BeginCriticalRender) observes the token and starts its own abort cleanup + // right away -- ONLY THEN wait for it; waiting before cancelling would just + // burn the whole grace period doing nothing. LockEnvironment.Run already + // bypasses its own lock once MainToken is cancelled, so those cleanup writes + // can never deadlock against this handler thread while we wait below. Helper.MainToken.Cancel(); _ctrlCPress = true; + _criticalRenderIdle.Wait(_criticalRenderGracePeriod); Environment.Exit(Helper.ExitCode); } } + /// + /// Marks the start of a render/cleanup section (e.g. a control's abort/finish path) + /// that should get a short, bounded grace period to complete before Ctrl+C forces the + /// process to exit, instead of possibly being torn down mid-write. Dispose the + /// returned handle (typically via ) once the section completes; + /// nesting is supported. The grace period is bounded (see + /// ), so a section that never disposes its + /// scope (e.g. a genuine hang) cannot prevent Ctrl+C from eventually exiting. + /// + /// A disposable that ends the critical section when disposed. + public static IDisposable BeginCriticalRender() + { + lock (_criticalRenderLock) + { + _criticalRenderCount++; + _criticalRenderIdle.Reset(); + } + return new CriticalRenderScope(); + } + + private static void EndCriticalRender() + { + lock (_criticalRenderLock) + { + _criticalRenderCount--; + if (_criticalRenderCount <= 0) + { + _criticalRenderCount = 0; + _criticalRenderIdle.Set(); + } + } + } + + private sealed class CriticalRenderScope : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (_disposed) + { + return; + } + _disposed = true; + EndCriticalRender(); + } + } + /// /// Enables/Disable Emacs-style key bindings in the console, allowing for standard Emacs key combinations to be used for text editing and navigation. /// From dc4fcd42060007845aeda2690a200cb4c9784da9 Mon Sep 17 00:00:00 2001 From: Fernando Cerqueira Date: Mon, 10 Aug 2026 11:27:22 -0300 Subject: [PATCH 2/3] Bump version to 1.0.0-rc2 in ConsolePlus.csproj Co-Authored-By: Claude Sonnet 5 --- src/ConsolePlus.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConsolePlus.csproj b/src/ConsolePlus.csproj index de25e4c..d28c774 100644 --- a/src/ConsolePlus.csproj +++ b/src/ConsolePlus.csproj @@ -24,7 +24,7 @@ ConsolePlus.net README.md https://github.com/FRACerqueira/ConsolePlus - 1.0.0-rc1 + 1.0.0-rc2 icon.png © 2026 - Fernando Cerqueira True From 80191b28816d4135269b64b6320d6863651388a7 Mon Sep 17 00:00:00 2001 From: Fernando Cerqueira Date: Mon, 10 Aug 2026 11:59:09 -0300 Subject: [PATCH 3/3] Document BeginCriticalRender; enable NuGet package build Added documentation for ConsolePlus.BeginCriticalRender() in ConsolePlus.md, detailing its purpose, usage, and return value. Updated PromptPlus.csproj to set GeneratePackageOnBuild to true, enabling automatic NuGet package generation during build. --- docs/api/ConsolePlus.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/api/ConsolePlus.md b/docs/api/ConsolePlus.md index 9e626a8..3d45cd8 100644 --- a/docs/api/ConsolePlus.md +++ b/docs/api/ConsolePlus.md @@ -577,6 +577,26 @@ Beeps the console speaker\. public static void Beep(); ``` + + +## ConsolePlus\.BeginCriticalRender\(\) Method + +Marks the start of a render/cleanup section \(e\.g\. a control's abort/finish path\) +that should get a short, bounded grace period to complete before Ctrl\+C forces the +process to exit, instead of possibly being torn down mid\-write\. Dispose the +returned handle \(typically via [using](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using 'https://docs\.microsoft\.com/en\-us/dotnet/csharp/language\-reference/keywords/using')\) once the section completes; +nesting is supported\. The grace period is bounded \(see +[ConsolePlusLibrary\.ConsolePlus\.\_criticalRenderGracePeriod](https://learn.microsoft.com/en-us/dotnet/api/consolepluslibrary.consoleplus._criticalrendergraceperiod 'ConsolePlusLibrary\.ConsolePlus\.\_criticalRenderGracePeriod')\), so a section that never disposes its +scope \(e\.g\. a genuine hang\) cannot prevent Ctrl\+C from eventually exiting\. + +```csharp +public static System.IDisposable BeginCriticalRender(); +``` + +#### Returns +[System\.IDisposable](https://learn.microsoft.com/en-us/dotnet/api/system.idisposable 'System\.IDisposable') +A disposable that ends the critical section when disposed\. + ## ConsolePlus\.Clear\(Nullable\\) Method