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