How to Close Console Application Gracefully on Win

2019-02-25 03:15发布

问题:

I am trying to close my vb.net console app gracefully when windows shutdown occurs. I have found examples that call the Win32 function SetConsoleCtrlHandler that all basically look like this:

Module Module1

Public Enum ConsoleEvent
    CTRL_C_EVENT = 0
    CTRL_BREAK_EVENT = 1
    CTRL_CLOSE_EVENT = 2
    CTRL_LOGOFF_EVENT = 5
    CTRL_SHUTDOWN_EVENT = 6
End Enum

Private Declare Function SetConsoleCtrlHandler Lib "kernel32" (ByVal handlerRoutine As ConsoleEventDelegate, ByVal add As Boolean) As Boolean
Public Delegate Function ConsoleEventDelegate(ByVal MyEvent As ConsoleEvent) As Boolean


Sub Main()

    If Not SetConsoleCtrlHandler(AddressOf Application_ConsoleEvent, True) Then
        Console.Write("Unable to install console event handler.")
    End If

    'Main loop
    Do While True
        Threading.Thread.Sleep(500)
        Console.WriteLine("Main loop executing")
    Loop

End Sub


Public Function Application_ConsoleEvent(ByVal [event] As ConsoleEvent) As Boolean

    Dim cancel As Boolean = False

    Select Case [event]

        Case ConsoleEvent.CTRL_C_EVENT
            MsgBox("CTRL+C received!")
        Case ConsoleEvent.CTRL_BREAK_EVENT
            MsgBox("CTRL+BREAK received!")
        Case ConsoleEvent.CTRL_CLOSE_EVENT
            MsgBox("Program being closed!")
        Case ConsoleEvent.CTRL_LOGOFF_EVENT
            MsgBox("User is logging off!")
        Case ConsoleEvent.CTRL_SHUTDOWN_EVENT
            MsgBox("Windows is shutting down.")
            ' My cleanup code here
    End Select

    Return cancel ' handling the event.

End Function

This works fine until I incorporate it into muy existing program when I get this exception:

CallbackOnCollectedDelegate was detected Message: A callback was made on a garbage collected delegate of type 'AISLogger!AISLogger.Module1+ConsoleEventDelegate::Invoke'. This may cause application crashes, corruption and data loss. When passing delegates to unmanaged code, they must be kept alive by the managed application until it is guaranteed that they will never be called.

Much searching indicates that the problem is caused by the the delegate object not being referenced and so is going out of scope and so being disposed of by the garbage collector. This seems to be confirmed by adding a GC.Collect into the main loop in the example above and getting the same exception when closing the console window or pressing ctrl-C. The trouble is, I don't understand what is meant by 'referencing the delegate'? This sounds to me like assigning a variable to a function??? How can I do this in VB? There are lots of C# examples of this but I can't translate them into VB.

Thanks.

回答1:

    If Not SetConsoleCtrlHandler(AddressOf Application_ConsoleEvent, True) Then

This is the statement that gets you into trouble. It creates a delegate instance on-the-fly and passes it to unmanaged code. But the garbage collector cannot see the reference held by that unmanaged code. You'll need to store it yourself so it won't be garbage collected. Make that look like this:

Private handler As ConsoleEventDelegate

Sub Main()
    handler = AddressOf Application_ConsoleEvent
    If Not SetConsoleCtrlHandler(handler, True) Then
       '' etc...

The handler variable now keeps it referenced and does so for the life of the program since it is declared in a Module.

You cannot cancel a shutdown btw, but that's another issue.



回答2:

Pointers to functions are represented in .Net as delegates. A delegate is a kind of object and, like any other object, if there are no references to it then it will be garbage collected.

The expression (AddressOf Application_ConsoleEvent) creates the delegate. SetConsoleCtrlHandler is a native function, so it doesn't understand delegates; it receives a raw function pointer. So the sequence of operations is:

  1. (AddressOf Application_ConsoleEvent) creates the delegate.
  2. SetConsoleCtrlHandler receives a raw function pointer that points to the delegate, and stores the raw pointer for later use.
  3. Time passes. Note that nothing is referencing the delegate now.
  4. Garbage collection occurs and the delegate is collected because it is unreferenced.
  5. The application is closing, so Windows tries to notify you by calling the raw function pointer. This points to a non-existent delegate so you crash.

You need to declare a variable to keep a reference to the delegate. My VB is very rusty, but something like:

Shared keepAlive As ConsoleEventDelegate

Then

keepAlive = AddressOf ConsoleEventDelegate
If Not SetConsoleCtrlHandler(keepAlive, True) Then
    'etc.


回答3:

The simplest way of doing this is probably to handle the AppDomain.ProcessExit event, which is raised when the application's parent process exits.

    For example:
        Module MyApp

        Sub Main()
            ' Attach the event handler method
            AddHandler AppDomain.CurrentDomain.ProcessExit, AddressOf MyApp_ProcessExit

            ' Do something
            ' ...

            Environment.Exit(0)
        End Sub

        Private Sub MyApp_ProcessExit(sender As Object, e As EventArgs)
            Console.WriteLine("App Is Exiting...")
        End Sub

    End Module

But calling Environment.Exit may not be the best solution to your original problem. In general, the only time it is necessary to use this method is when there might be other foreground threads running. And in that case, it's worth investigating ways of gracefully terminating those other threads without resorting to draconian measures that kill the entire process.

Environment.Exit, despite the somewhat pleasant-sounding name, is a pretty brutal measure. It's not quite as bad as clicking "End Task" in the Windows Task Manager (and note that if you do that, the ProcessExit event will not be raised, meaning that the above suggestion will not work), but it's probably not the solution you really want, either.