Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/api/ConsolePlus.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,26 @@ Beeps the console speaker\.
public static void Beep();
```

<a name='ConsolePlusLibrary.ConsolePlus.BeginCriticalRender()'></a>

## 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\.

<a name='ConsolePlusLibrary.ConsolePlus.Clear(System.Nullable_ConsolePlusLibrary.Color_)'></a>

## ConsolePlus\.Clear\(Nullable\<Color\>\) Method
Expand Down
69 changes: 69 additions & 0 deletions src/ConsolePlus.Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System;
using System.Globalization;
using System.Text;
using System.Threading;

namespace ConsolePlusLibrary
{
Expand All @@ -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;

/// <summary>
/// Upper bound on how long <see cref="Console_CancelKeyPress"/> waits for in-flight
/// critical render sections (see <see cref="BeginCriticalRender"/>) 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.
/// </summary>
private static readonly TimeSpan _criticalRenderGracePeriod = TimeSpan.FromMilliseconds(300);

private static readonly IConsole _consoledrive;
private static readonly string _originalCulture;
private static readonly ConsoleColor _originalForecolor;
Expand Down Expand Up @@ -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);
}
}

/// <summary>
/// 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 <see langword="using"/>) once the section completes;
/// nesting is supported. The grace period is bounded (see
/// <see cref="_criticalRenderGracePeriod"/>), so a section that never disposes its
/// scope (e.g. a genuine hang) cannot prevent Ctrl+C from eventually exiting.
/// </summary>
/// <returns>A disposable that ends the critical section when disposed.</returns>
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();
}
}

/// <summary>
/// Enables/Disable Emacs-style key bindings in the console, allowing for standard Emacs key combinations to be used for text editing and navigation.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/ConsolePlus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<PackageId>ConsolePlus.net</PackageId>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://github.com/FRACerqueira/ConsolePlus</PackageProjectUrl>
<Version>1.0.0-rc1</Version>
<Version>1.0.0-rc2</Version>
<PackageIcon>icon.png</PackageIcon>
<Copyright>© 2026 - Fernando Cerqueira</Copyright>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
Expand Down
Loading