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

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

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

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

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

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

Спасибо.


person Guy    schedule 09.03.2013    source источник


Ответы (3)


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

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

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

person Hans Passant    schedule 09.03.2013
comment
Спасибо, Ганс, это прекрасно. Я полдня пытался понять, что я делал не так! - person Guy; 10.03.2013

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

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

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

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

Shared keepAlive As ConsoleEventDelegate

потом

keepAlive = AddressOf ConsoleEventDelegate
If Not SetConsoleCtrlHandler(keepAlive, True) Then
    'etc.
person arx    schedule 09.03.2013
comment
Спасибо, arx. Я принял ответ Ханса только потому, что он оказался первым и его код VB был неудачным, но ваш ответ определенно устранил бы мою проблему. - person Guy; 10.03.2013

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

    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 не будет вызвано, а это означает, что указанное выше предложение не сработает), но, вероятно, это тоже не то решение, которое вам действительно нужно.

person Kanji Patel    schedule 06.06.2014