4

I am making an interactive benchmarking program that works in console mode, and would like to be able to interrupt current computation by pressing Ctrl+C or Ctrl+Break once, while subsequent pressing should act as usual, terminating the program. However, when no computation is currently performing, the very first Ctrl+C press should cause termination.

At first, this looked like a simple task. I created a static class with obvious EnableHandler() and DisableHandler() routines, and a handler function with behavior dependent on previous state: if interruption was already requested, then just stand down and return, otherwise set arg.Cancel and raise interruption request flag. Thus, when a benchmark starts, it enables the handler and then checks for interruption flag on each major cycle; when finished, it disables the handler and propagates interruption request up to the topmost caller, if needed.

However, this approach works only once: after the handler was removed for the first time (no matter was it ever fired or not), installing it again afterwards has no effect anymore — the operation doesn't produce errors, but the handler never receives control when event occurs. Is this an expected behavior in .NET event handling?

There are numerous topics regarding Console.CancelKeyPress on SO and other forums, but virtually none of them considers removing the handler, thus no wonder they didn't run into trouble. Nevertheless, in “How to use ConsoleCancelEventHandler several times” some similar(?) issue was mentioned, but that application was a complex GUI frontend for several external console utilities launched on demand, and the issue was clearly associated with explicit exception raised when trying to add handler the second time, — which is not my case.

Having been informed of previously existing bugs related to console operation in various .NET versions, I won't be surprised if such a behavior is abnormal and caused by .NET malfunction or is specific to Visual Basic (I'm not sure whether AddHandler and RemoveHandler in VB.NET are totally equivalent to += and -= event operators in C#). I'm currently using .NET 4.5 and Visual Basic 2012 on Windows 7 x86-64 with all available updates installed.

As a workaround, I actually don't remove the handler in DisableHandler() routine, but simply toggle a flag: when this flag is cleared, the handler returns immediately, as if not installed. However, this approach looks ugly to me, and I hope to resolve the issue and achieve the goal in proper way.

PS. Current code as requested by Chris Dunaway:

' Usage example
Dim fAbort As Boolean = False
BreakHandler.Enable()
For Each oImplementation As Implementation In oCompetition.Implementations
    Benchmark(oImplementation)
    If BreakHandler.AbortRequested() Then fAbort = True : Exit For
Next
BreakHandler.Disable()
Return Not fAbort


Public Class BreakHandler

    Protected Shared AbortFlag As Boolean = False
    Protected Shared HandlerInstalled As Boolean = False
    Protected Shared HandlerEnabled As Boolean = False

    Public Shared ReadOnly Property AbortRequested() As Boolean
        Get
            If AbortFlag Then
                AbortFlag = False
                Return True
            Else
                Return False
            End If
        End Get
    End Property

    Public Shared Sub Enable()
        If HandlerEnabled Then Return
        If Not HandlerInstalled Then
            AddHandler Console.CancelKeyPress, AddressOf Handler
            HandlerInstalled = True
        End If
        HandlerEnabled = True
    End Sub

    Public Shared Sub Disable()
        AbortFlag = False
        If Not HandlerEnabled Then Return
        ' This is where the handler was removed originally.
        'RemoveHandler Console.CancelKeyPress, AddressOf Handler
        HandlerEnabled = False
    End Sub

    Protected Shared Sub Handler(ByVal sender As Object, ByVal args As ConsoleCancelEventArgs)
        If (Not HandlerEnabled) OrElse AbortFlag Then
            Return  ' Stand down, allow complete abortion.
        Else
            Console.Out.WriteLine("Will abort on next cycle. Press Break again to quit.")
            AbortFlag = True
            args.Cancel = True
        End If
    End Sub

End Class
Community
  • 1
  • 1
Anton Samsonov
  • 1,380
  • 17
  • 34
  • Can you post a short but complete program which demonstrates the problem? Without some code, it will be very difficult to help you. – Chris Dunaway Mar 28 '14 at 14:21

1 Answers1

5

Yes, this is a bug in the Console class. Present in all .NET Framework versions afaict, including 4.5.1. It is a rather silly bug, the first time you register an event handler it will install a callback so Windows gets the event to be raised. When you remove the event handler, and no other event handlers are left, it uninstalls the callback. But forgets to reset the internal "I've installed to callback" status variable. So when you call AddHandler again, it doesn't re-install the callback.

You can report the bug at connect.microsoft.com, let me know if you don't want to take the time and I'll take care of it.

There's a silly workaround for it, you just need to prevent it from uninstalling the callback again. Which you can do by registering a dummy event handler. Put this line of code at the top of your Main() method:

    AddHandler Console.CancelKeyPress, Sub(s, e)
                                       End Sub

But of course your workaround is fine as well. Do note that your code has other problems, a Boolean variable is not a synchronization object. The CancelKeyPress event runs on another thread so there is no guarantee that your main thread can see the value change. This can malfunction when you run your program in the Release build and use the x86 jitter. A minimum requirement is to declare the Boolean variable as volatile so the jitter doesn't store the variable in a CPU register but always reloads it from memory, but the VB.NET language doesn't have syntax for that. You should use a real synchronization object, a ManualResetEvent is the proper one.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Bug #524889 in Microsoft Connect is marked as fixed back in 2010. If this is another one, then, well, I'm not feeling potent enough to influence MS guys. As for the sync issues, I'm aware of the topic in general, but I thought that 32-bit ops are always atomic, and even MSVC (not to mention VB.NET) saves register values back to memory immediately, so the worst case here would be to miss near break request and wait for another turn. Or am I mistaken? (Anyway, I'll do as you suggest, as it would provide greater transparency without a significant overhead.) – Anton Samsonov Mar 28 '14 at 16:07
  • 1
    Atomicity is not the problem here. A routine optimization is to store a variable value in a CPU register. An important one since memory is slow. Which gives the *volatile* keyword a meaning both in C and C#. It goes wrong when the loop is tight with little code that needs the register for another duty. No, #524889 is not related. – Hans Passant Mar 28 '14 at 16:17
  • 2
    Filed as https://connect.microsoft.com/VisualStudio/feedback/details/842578/console-cancelkeypress-event-doesnt-fire – Hans Passant Mar 28 '14 at 16:29