Java应用程序在bat脚本中运行时的Windows关闭钩子

19

我有一个批处理脚本,运行一个Java应用程序。如果我按下ctrl+c,它会正常终止应用程序并调用所有的关闭钩子。然而,如果我只是关闭了bat脚本的cmd窗口,关闭钩子就永远不会被调用。

是否有一种方法来解决这个问题?也许有一种方式可以告诉bat脚本在其窗口关闭时如何终止调用的应用程序?

3个回答

22

来自addShutdownHook文档:

  

在极少数情况下,虚拟机可能会异常终止,也就是说,在没有清除关闭的情况下停止运行。 这将在外部终止虚拟机时发生,例如在Unix上使用SIGKILL信号或在Microsoft Windows上使用TerminateProcess调用。

所以我认为这里无能为力,不幸的是。


Windows控制台中的CTRL-CLOSE信号。看起来不可调整。

引用以上链接:

  

当用户关闭控制台时,系统会生成一个CTRL+CLOSE信号。附加到控制台的所有进程都会接收到该信号,从而使每个进程有机会在终止之前进行清理。当进程接收到此信号时,处理程序函数可以在执行任何清理操作后执行以下操作之一:

  • 调用ExitProcess以终止进程。
  • 返回FALSE。如果没有注册的处理程序函数返回TRUE,默认处理程序将终止进程。
  • 返回TRUE。在这种情况下,不会调用其他处理程序函数,并出现弹出对话框询问用户是否终止进程。如果用户选择不终止进程,则系统不会关闭控制台,直到进程最终终止。

UPD。如果您可以接受本地调整,则WinAPI SetConsoleCtrlHandler函数可以打开禁止默认行为的方式。

UPD2Java信号处理和终止的启示是一篇相对较旧的文章,但是其中的编写Java信号处理程序部分可能真正包含您所需的内容。


UPD3。 我尝试了上面文章中的Java信号处理程序。它很好地处理了SIGINT,但这不是我们需要的,所以我决定使用SetConsoleCtrlHandler来进行操作。结果有点复杂,可能不值得在您的项目中实现。无论如何,它可能会帮助其他人。

所以,思路是:

  1. 保留关闭处理程序线程的引用。
  2. 使用JNI设置自定义本机控制台处理程序例程。
  3. CTRL+CLOSE信号上调用自定义Java方法。
  4. 从该方法调用关闭处理程序。

Java代码:

public class TestConsoleHandler {

    private static Thread hook;

    public static void main(String[] args) {
        System.out.println("Start");
        hook = new ShutdownHook();
        Runtime.getRuntime().addShutdownHook(hook);
        replaceConsoleHandler(); // actually not "replace" but "add"

        try {
            Thread.sleep(10000); // You have 10 seconds to close console
        } catch (InterruptedException e) {}
    }

    public static void shutdown() {
        hook.run();
    }

    private static native void replaceConsoleHandler();

    static {
        System.loadLibrary("TestConsoleHandler");
    }
}

class ShutdownHook extends Thread {
    public void run() {
        try {
            // do some visible work
            new File("d:/shutdown.mark").createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("Shutdown");
    }
}

本地的 replaceConsoleHandler

JNIEXPORT void JNICALL Java_TestConsoleHandler_replaceConsoleHandler(JNIEnv *env, jclass clazz) {
    env->GetJavaVM(&jvm);
    SetConsoleCtrlHandler(&HandlerRoutine, TRUE);
}

以及处理程序本身:

BOOL WINAPI HandlerRoutine(__in DWORD dwCtrlType) {
    if (dwCtrlType == CTRL_CLOSE_EVENT) {
        JNIEnv *env;
        jint res =  jvm->AttachCurrentThread((void **)(&env), &env);
        jclass cls = env->FindClass("TestConsoleHandler");
        jmethodID mid = env->GetStaticMethodID(cls, "shutdown", "()V");
        env->CallStaticVoidMethod(cls, mid);
        jvm->DetachCurrentThread();
        return TRUE;
    }
    return FALSE;
}

这很有效。在JNI代码中,为了清除所有错误检查,都被省略了。关闭处理程序会创建一个空文件"d:\shutdown.mark",以指示正确的关闭。

完整源代码和编译的测试二进制文件在此


再次更新,附上Chris White的文章链接。 - lxbndr
这似乎不是真的,因为当我按下ctrl+c停止我的批处理脚本时,关闭挂钩被调用,但当我只是关闭窗口时,它们没有被调用。 - Zoltán
我使用SetConsoleHandler取得了积极的结果。明天我会发布详细信息。 - lxbndr
资源/二进制文件的链接已失效 :( - Igor Popov
我已重新上传文件(http://rghost.net/41278228)。不确定是否是相同或最新版本。 - lxbndr
显示剩余6条评论

5

除了编写自己的本地代码之外,您还可以使用JNA来实现上述使用SetConsoleCtrlHandler的答案。

如果您想要创建自己的kernel32接口,或者使用在此优秀框架中提供的接口: https://gitlab.com/axet/desktop

以下是一些示例代码:

import com.github.axet.desktop.os.win.GetLastErrorException;
import com.github.axet.desktop.os.win.handle.HANDLER_ROUTINE;
import com.github.axet.desktop.os.win.libs.Kernel32Ex;
...
private static HANDLER_ROUTINE handler =
  new HANDLER_ROUTINE()
  {
    @Override
    public long callback(long dwCtrlType) {
      if ((int)dwCtrlType == CTRL_CLOSE_EVENT) {
        // *** do your shutdown code here ***
        return 1;
      }
      return 0;
    }
  };

public static void assignShutdownHook() {
  if (!Kernel32Ex.INSTANCE.SetConsoleCtrlHandler(handler, true))
    throw new GetLastErrorException();
}

请注意,我将匿名类分配给了一个字段。起初我在调用SetConsoleCtrlHandler时定义了它,但我认为它可能被JVM回收了。
编辑于4/9/17: 将链接从github更新为gitlab。

你的方法在我的电脑上不起作用(Windows 10,Java 1.8)... 那个钩子在 Windows 控制台中的 CTRL+C 不起作用... - Arthur
只有在强制关闭命令窗口时,CTRL_CLOSE_EVENT才会触发。CTRL+C将优雅地结束bat/cmd文件,并不会触发该事件。如果您想处理干净的关闭,请使用Runtime.addShutdownHook(Thread hook)。http://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#addShutdownHook-java.lang.Thread- - Samah

0

尽管批处理文件可能被终止,但取决于操作系统、命令处理器以及批处理文件的执行方式(从命令提示符或通过快捷方式启动),批处理文件所在的控制台(窗口)可能会保持打开状态。

taskkill是一个很好的命令,用于结束与Windows一起分发的程序(我假设您想停止一个不同的程序,而不是批处理文件本身)。

可以通过进程ID、可执行文件名称、窗口标题、状态(即无响应)、DLL名称或服务名称来结束程序。

以下是一些示例,基于您如何在批处理文件中使用它:

强制停止“program.exe”(通常需要/f“force”标志来强制停止程序,只需通过试错检查是否需要该标志来适用于您的应用程序):

taskkill /f /im program.exe 

停止任何无响应的程序:

taskkill /fi "Status eq NOT RESPONDING"

根据窗口标题停止程序(允许使用*通配符匹配任何内容):

taskkill /fi "WindowTitle eq Please Login"

taskkill /fi "WindowTitle eq Microsoft*"

你甚至可以使用它来停止网络上另一台计算机上的程序(尽管在批处理文件中写入账户密码并不是一个好主意)。
taskkill /s JimsPC /u Jim /p James_007 /im firefox.exe

Windows中还有另一种选择,即tskill。虽然它没有那么多的选项,但命令更简单。

编辑:

我不确定是否有其他方法。我认为关闭cmd窗口类似于强制关闭应用程序,即立即终止而无需进一步通知。这是许多应用程序的行为,当它们被要求强制关闭时,实际上需要很长时间才能最终终止。这是因为在操作系统释放所有资源的努力中,某些资源(特别是某些I/O和/或文件资源)不会立即释放。

在我看来,如果Java想要做任何事情,它也无能为力。强制关闭发生在操作系统级别,在此时它会释放正在使用的内存、文件和I/O句柄等资源。Java(或任何其他程序)无法控制强制关闭。此时,操作系统接管了控制权。

如果我错了,请有人纠正我。


我并不想停止程序,这不是问题所在。问题在于,当我关闭cmd窗口(通过点击右上角的X按钮),批处理文件以一种不调用其关闭钩子的方式终止了我的Java应用程序。 - Zoltán
@Zoltán 请看我上面编辑过的评论。我愿意接受更正。 - Frankline

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