发送 Ctrl+C 事件终止 Windows 控制台进程

偶然在 Nuitka 的代码中发现一个未被触发的 BUG 。问题代码位于 OnefileBootstrap.ccleanupChildProcess() 函数中:

c++static void cleanupChildProcess(bool send_sigint) {

    // Cause KeyboardInterrupt in the child process.
    if (handle_process != 0) {

        if (send_sigint) {
#if defined(_NUITKA_EXPERIMENTAL_DEBUG_ONEFILE_HANDLING)
            puts("Sending CTRL-C to child\n");
#endif

#if defined(_WIN32)
            BOOL res = GenerateConsoleCtrlEvent(CTRL_C_EVENT, GetProcessId(handle_process));

            if (res == false) {
                printError("Failed to send CTRL-C to child process.", GetLastError());
                // No error exit is done, we still want to cleanup when it does exit
            }
#else
            kill(handle_process, SIGINT);
#endif
        }

        // TODO: We ought to only need to wait if there is a need to cleanup
        // files when we are on Windows, on Linux maybe exec can be used so
        // this process to exist anymore if there is nothing to do.
#if _NUITKA_ONEFILE_TEMP_BOOL == 1 || 1
        NUITKA_PRINT_TRACE("Waiting for child to exit.\n");
#if defined(_WIN32)
        if (WaitForSingleObject(handle_process, _NUITKA_ONEFILE_CHILD_GRACE_TIME_INT) != 0) {
            TerminateProcess(handle_process, 0);
        }

        CloseHandle(handle_process);
#else
        waitpid_timeout(handle_process);
        kill(handle_process, SIGKILL);
#endif
        NUITKA_PRINT_TRACE("Child is exited.\n");
#endif
    }

#if _NUITKA_ONEFILE_TEMP_BOOL == 1
    if (payload_created) {
#if _NUITKA_EXPERIMENTAL_DEBUG_ONEFILE_HANDLING
        wprintf(L"Removing payload path '%lS'\n", payload_path);
#endif
        removeDirectory(payload_path);
    }
#endif
}

这段代码中第 654 行的 GenerateConsoleCtrlEvent() 总是返回 FALSE ,导致子进程无法收到 CTRL-C 信号。不过由于 Nuitka 的代码中只有 cleanupChildProcess(false) 调用,因此这个 BUG 正常情况下不会被触发。

不幸的是,本人 fork 了 Nuitka 的项目 Nuitka-winsvc 正好触发了这个 BUG 。Nuitka-winsvc 为 Nuitka 增加了编译为 Windows 服务的选项。当停止服务时,需要向子进程发送 CTRL-C 信号来优雅结束子进程。

根据微软的官方文档, GenerateConsoleCtrlEvent() 的第二个参数应该是进程组 ID ,而不是子进程 ID 。要获得进程组 ID 必须在使用 CreateProcess() 创建子进程时设置 CREATE_NEW_PROCESS_GROUP 标志。另一种方法是关联子进程的控制台,并将 GenerateConsoleCtrlEvent() 的第二个参数设置为 0 。具体实现代码如下:

c++AttachConsole(GetProcessId(handle_process));
SetConsoleCtrlHandler(NULL, TRUE);	// 阻止当前进程被终止

BOOL res = GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);

FreeConsole();