如何在Windows关闭时优雅地关闭控制台应用程序

3

当 Windows 关机时,我想要优雅地关闭我的 VB.NET 控制台应用程序。我已经找到了一些调用 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

这段代码运行良好,但在将其合并到现有程序时,会出现以下异常:

检测到已收集的委托回调 消息:在类型为‘AISLogger!AISLogger.Module1+ ConsoleEventDelegate :: Invoke’的垃圾已收集委托上进行了回调。 这可能导致应用程序崩溃,损坏和数据丢失。 在将委托传递给非托管代码时,必须由托管应用程序保持活动状态,直到确保永远不会调用它们。

经过多次搜索,发现问题是由于委托对象没有被引用而导致过期被垃圾收集器处理。这似乎得到了证实,因为在上面的示例中,在主循环中添加GC.Collect并在关闭控制台窗口或按下ctrl-C时获取相同的异常。问题是,我不理解“引用委托”的含义?这对我来说听起来像是将变量分配给函数?如何在VB中执行此操作?有许多C#示例,但我无法将其翻译成VB。

谢谢。
3个回答

4
    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变量现在保持对它的引用,并且由于它是在模块中声明的,因此在程序的生命周期内都这样做。

顺便提一下,您不能取消关机,但这是另一个问题。


谢谢,汉斯,太完美了。我已经花了半天时间试图找出我的错误在哪里! - Guy

1
函数指针在.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.

谢谢你,arx。我之所以接受了Hans的答案,是因为他似乎是第一个给出准确VB代码的人,但你的答案肯定也能解决我的问题。 - Guy

-1

最简单的方法可能是处理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事件,这意味着上述建议将无法奏效),但它可能也不是您真正想要的解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接