Java 应用程序上的 Windows 关闭挂钩从 bat 脚本运行

我有一个运行java应用程序的bat脚本。如果我按 ctrl+c,应用程序会优雅地终止,并调用所有关机钩子。但是,如果我只是关闭 bat 脚本的 cmd 窗口,则永远不会调用关闭钩子。

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


答案 1

来自 addShutdownHook 文档:

在极少数情况下,虚拟机可能会中止,即停止运行而不完全关闭。当虚拟机在外部终止时,例如,使用 Unix 上的 SIGKILL 信号或 Microsoft Windows 上的 TerminateProcess 调用时,会发生这种情况。

所以不幸的是,我认为这里没什么可做的。


Windows Console 中的 CTRL-CLOSE 信号。似乎不可调整。

引用上面的链接:

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

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

UPD。如果本机调整是可接受的,WinAPI 函数将为抑制默认行为开辟了途径。SetConsoleCtrlHandler

UPD2.关于Java信号处理和终止的启示相对较旧的文章,但编写Java信号处理程序部分确实可能包含您需要的内容。


UPD3.我已经尝试了上面文章中的Java信号处理程序。它工作得很好,但它不是我们需要的,我决定随身携带它。结果有点复杂,可能不值得在您的项目中实现。无论如何,它可以帮助别人。SIGINTSetConsoleCtrlHandler

所以,这个想法是:

  1. 保留对关闭处理程序线程的引用。
  2. 使用 JNI 设置自定义本机控制台处理程序例程。
  3. 在信号上调用自定义 Java 方法。CTRL+CLOSE
  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"

此处提供已编译的测试二进制文件的完整源代码。


答案 2

除了上面使用SetConsoleCtrlHandler的答案之外,你也可以使用JNA来做到这一点,而不是编写自己的原生代码。

如果你愿意,你可以在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 的链接。


推荐