【问题标题】:How to Close Console Application Gracefully on Windows Shutdown如何在 Windows 关机时优雅地关闭控制台应用程序
【发布时间】:2013-02-25 08:38:46
【问题描述】:

我正在尝试在 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

这工作正常,直到我将它合并到许多现有程序中,当我得到这个异常时:

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

大量搜索表明问题是由未引用的委托对象引起的,因此超出了范围,因此被垃圾收集器处理掉了。这似乎可以通过在上面示例中的主循环中添加 GC.Collect 来确认,并在关闭控制台窗口或按 ctrl-C 时获得相同的异常。问题是,我不明白“引​​用代表”是什么意思?这对我来说听起来像是将变量分配给函数???我怎样才能在VB中做到这一点?有很多这样的 C# 示例,但我无法将它们翻译成 VB。

谢谢。

【问题讨论】:

    标签: vb.net winapi windows-console


    【解决方案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...
    

    handler 变量现在保持它被引用,并在程序的生命周期内这样做,因为它是在模块中声明的。

    顺便说一句,您无法取消关机,但这是另一个问题。

    【讨论】:

    • 谢谢你,汉斯,太好了。我花了半天时间试图找出我做错了什么!
    【解决方案2】:

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

    【讨论】:

    • 谢谢你。我接受 Hans 的回答只是因为他似乎是第一个,而且他的 VB 代码运行良好,但你的回答肯定会解决我的问题。
    【解决方案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 可能不是解决您最初问题的最佳方法。一般来说,the only time it is necessary to use this method is when there might be other foreground threads running。在这种情况下,值得研究如何优雅地终止那些其他线程,而无需采取会杀死整个进程的严厉措施。

    Environment.Exit,尽管名字听起来有点悦耳,但却是一个相当残酷的措施。它还没有在 Windows 任务管理器中单击“结束任务”那么糟糕(请注意,如果您这样做,将不会引发 ProcessExit 事件,这意味着上述建议将不起作用),但这也可能不是您真正想要的解决方案。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-03-11
      • 2016-06-09
      • 2011-04-30
      相关资源
      最近更新 更多