Как закрыть консольное приложение изящно при выключении Windows

Я пытаюсь закрыть консольное приложение vb.net изящно, когда происходит выключение Windows. Я нашел примеры, которые вызывают функцию SetConsoleCtrlHandler Win32, которые в основном выглядят следующим образом:

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

Это работает нормально, пока я не включу его в существующую программу muy, когда я получу это исключение:

Обнаружен CallbackOnCollectedDelegate Сообщение: Обратный вызов был сделан на собранном делегатом мусора типа "AISLogger! AISLogger.Module1 + ConsoleEventDelegate:: Invoke". Это может привести к сбоям приложений, сбоям и потерям данных. При передаче делегатов неуправляемому коду они должны оставаться в живых управляемым приложением, пока не будет гарантировано, что они никогда не будут вызваны.

Значительный поиск указывает на то, что проблема вызвана тем, что объект-делегат не ссылается, и поэтому выходит из сферы действия и поэтому удаляется сборщиком мусора. Это, похоже, подтверждается добавлением GC.Collect в основной цикл в приведенном выше примере и получение того же исключения при закрытии окна консоли или нажатии ctrl-C. Беда в том, что я не понимаю, что подразумевается под "ссылкой на делегата"? Это звучит для меня как назначение переменной функции? Как я могу это сделать в VB? Есть много примеров С#, но я не могу перевести их в VB.

Спасибо.

Ответ 1

    If Not SetConsoleCtrlHandler(AddressOf Application_ConsoleEvent, True) Then

Это выражение, которое вызывает у вас неприятности. Он создает экземпляр делегата "на лету" и передает его неуправляемому коду. Но сборщик мусора не может видеть ссылку, содержащуюся в этом неуправляемом коде. Вам нужно будет хранить его самостоятельно, чтобы не собирать мусор. Сделайте это так:

Private handler As ConsoleEventDelegate

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

Теперь переменная обработчика держит ее ссылкой и делает это в течение всего срока службы программы, поскольку она объявлена ​​в модуле.

Вы не можете отменить btw shutdown, но это еще одна проблема.

Ответ 2

Указатели на функции представлены в .Net как делегаты. Делегат - это своего рода объект, и, как и любой другой объект, если нет ссылок на него, он будет собирать мусор.

Выражение (AddressOf Application_ConsoleEvent) создает делегат. SetConsoleCtrlHandler является нативной функцией, поэтому он не понимает делегатов; он получает необработанный указатель функции. Таким образом, последовательность операций:

  • (AddressOf Application_ConsoleEvent) создает делегат.
  • SetConsoleCtrlHandler получает необработанный указатель функции, указывающий на делегат, и сохраняет необработанный указатель для последующего использования.
  • Проходит время. Обратите внимание, что ничто не ссылается на делегата.
  • Сбор мусора происходит, и делегат собирается, потому что он не найден.
  • Приложение закрывается, поэтому Windows пытается уведомить вас, вызвав указатель на необработанные функции. Это указывает на несуществующего делегата, чтобы вы потерпели крах.

Вам нужно объявить переменную, чтобы сохранить ссылку на делегат. Мой VB очень ржавый, но что-то вроде:

Shared keepAlive As ConsoleEventDelegate

Тогда

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

Ответ 3

Простейший способ сделать это, вероятно, для обработки AppDomain.ProcessExit event, который возникает при выходе из родительского процесса приложения.

    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

Но вызов Environment.Exit не может быть лучшим решением вашей исходной проблемы. В общем, единственный раз, когда нужно использовать этот метод, могут быть другие потоки переднего плана. И в этом случае стоит исследовать способы изящного завершения этих других потоков, не прибегая к драконовским мерам, которые убивают весь процесс.

Environment.Exit, несмотря на несколько приятное звучание, является довольно жестокой мерой. Это не так плохо, как щелчок "Завершить задачу" в диспетчере задач Windows (и обратите внимание, что если вы это сделаете, событие ProcessExit не будет поднято, что означает, что приведенное выше предложение не будет работать), но это, вероятно, не решение, которое вы действительно хотите.