我能否在Windows上向一个应用程序发送ctrl-C(SIGINT)信号?

110

我曾经编写过跨平台(Windows / Unix)应用程序,在命令行启动时以相同的方式处理用户键入的 Ctrl-C 组合键,即优雅地终止应用程序。

在 Windows 上,是否可以从另一个(无关的)进程发送 Ctrl-C/SIGINT/等价信号到进程中,从而请求其优雅地终止(给予其机会清理资源等)?


2
我有兴趣发送Ctrl-C到Java服务进程以获取线程转储。似乎可以可靠地使用jstack来代替特定的问题:https://dev59.com/OnM_5IYBdhLWcg3w2XLw#47723393 - Vadzim
https://learn.microsoft.com/en-us/windows/console/generateconsolectrlevent - Hans Passant
17个回答

70

我对这个主题进行了一些研究,结果比我预期的更受欢迎。KindDragon的回复是其中至关重要的一点。

我在这个主题上写了一篇较长的博客文章,并创建了一个工作演示程序,演示如何使用这种类型的系统以几种好看的方式关闭命令行应用程序。该文章还列出了我在研究中使用的外部链接。

简而言之,这些演示程序执行以下操作:

  • 使用.Net启动具有可见窗口的程序,使用pinvoke隐藏,运行6秒钟,使用pinvoke显示,使用.Net停止。
  • 使用.Net启动没有窗口的程序,运行6秒钟,通过附加控制台并发出ConsoleCtrlEvent停止。

编辑:@KindDragon的修改解决方案,供那些此时此地对代码感兴趣的人使用。如果您计划在停止第一个进程后启动其他程序,则应重新启用CTRL+C处理,否则下一个进程将继承父进程的禁用状态,并且将不会响应CTRL+C

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AttachConsole(uint dwProcessId);

[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern bool FreeConsole();

[DllImport("kernel32.dll")]
static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add);

delegate bool ConsoleCtrlDelegate(CtrlTypes CtrlType);

// Enumerated type for the control messages sent to the handler routine
enum CtrlTypes : uint
{
    CTRL_C_EVENT = 0,
    CTRL_BREAK_EVENT,
    CTRL_CLOSE_EVENT,
    CTRL_LOGOFF_EVENT = 5,
    CTRL_SHUTDOWN_EVENT
}

[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);

public void StopProgram(Process proc)
{
    //This does not require the console window to be visible.
    if (AttachConsole((uint)proc.Id))
    {
    // Disable Ctrl-C handling for our program
    SetConsoleCtrlHandler(null, true); 
    GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0);

    //Moved this command up on suggestion from Timothy Jannace (see comments below)
    FreeConsole();
                
    // Must wait here. If we don't and re-enable Ctrl-C
    // handling below too fast, we might terminate ourselves.
    proc.WaitForExit(2000);
                
    //Re-enable Ctrl-C handling or any subsequently started
    //programs will inherit the disabled state.
    SetConsoleCtrlHandler(null, false); 
    }
}

此外,如果 AttachConsole() 或发送的信号失败,建议制定备用方案,例如休眠再执行以下操作:

if (!proc.HasExited)
{
    try
    {
    proc.Kill();
    }
    catch (InvalidOperationException e){}
}

4
受此启发,我制作了一个“独立”的 CPP 应用程序来实现它:https://gist.github.com/rdp/f51fb274d69c5c31b6be ,以防有用。是的,这种方法可以向“任何” PID 发送 Ctrl+C 或 Ctrl+Break 信号。 - rogerdpack
优秀的帖子。优秀的博客。我建议在你的实验程序中完成StopProgramWithInvisibleWindowUsingPinvoke函数。我差点放弃这个想法,认为你没有找到解决方案。 - Wilmer SH
为避免wait(),我移除了重新启用Ctrl-C(SetConsoleCtrlHandler(null, false);)。我进行了几个测试(多次调用,也包括已终止的进程),目前尚未发现任何副作用。 - 56ka
在发送GenerateConsoleCtrlEvent之后应立即调用FreeConsole。在调用之前,您的应用程序将附加到其他控制台。如果另一个控制台在完成关闭之前被终止,您的应用程序也将被终止! - Timothy Jannace
@56ka:如果您不打算从主进程启动新进程,那么这将是一个选项。我已经有一段时间没有使用这个项目了,但是如注释所示,未能重新启用处理程序意味着下一个启动的进程将无法监听Crtl-C事件。 - Stanislav
显示剩余3条评论

32
我所找到的最接近解决方案的是第三方应用程序SendSignal。作者提供了源代码和可执行文件。我已经验证它可以在64位Windows下工作(以32位程序运行,关闭另一个32位程序),但我还没有弄清楚如何将代码嵌入到Windows程序中(无论是32位还是64位)。
它是如何工作的:
在调试器中挖掘了很多后,我发现实际上执行类似于ctrl-break信号的行为的入口点是kernel32!CtrlRoutine。该函数与ThreadProc具有相同的原型,因此可以直接使用CreateRemoteThread,而无需注入代码。但是,那不是导出的符号!在不同版本的Windows上,它在不同的地址上(甚至有不同的名称)。怎么办?
这是我最后想出的解决方案。我为我的应用程序安装了一个控制台控制处理程序,然后为我的应用程序生成一个ctrl-break信号。当我的处理程序被调用时,我回顾一下堆栈顶部,以查找传递给kernel32!BaseThreadStart的参数。我抓取第一个参数,这是线程的期望起始地址,也就是kernel32!CtrlRoutine的地址。然后我从我的处理程序返回,表明我已处理了信号,我的应用程序不应该被终止。回到主线程,在kernel32!CtrlRoutine的地址被检索出来之前,我等待。一旦我获得它,我就在目标进程中使用发现的起始地址创建一个远程线程。这会导致目标进程中的控制处理程序被评估,就像按下了ctrl-break键一样!
好处是只有目标进程受到影响,并且可以针对任何进程(甚至是窗口进程)。缺点之一是我的小应用程序不能在批处理文件中使用,因为当它发送ctrl-break事件以发现kernel32!CtrlRoutine的地址时,它将终止它。
(如果在批处理文件中运行它,请在其前面加上 start。)

2
+1 - 链接的代码使用 Ctrl-Break 而不是 Ctrl-C,但就其外观而言(查看 GenerateConsoleCtrlEvent 的文档),它可以适应 Ctrl-C。 - Matthew Murdoch
8
其实有一个函数可以帮你做到这一点,哈哈,“GenerateConsoleCtrlEvent”。详见:http://msdn.microsoft.com/en-us/library/ms683155(v=vs.85).aspx 还可以看看我在这里的回复:http://serverfault.com/a/501045/10023 - Triynko
1
与上面的答案相关的Github链接: https://github.com/walware/statet/tree/master/de.walware.statet.r.console.core/win32 - meawoppl
3
令人难以置信的是,Windows 的历史如此悠久,但微软却没有为控制台应用程序A提供一个机制,使其可以侦听来自控制台应用程序B的信号,以便控制台应用程序B可以通知控制台应用程序A进行清理关闭。无论如何,撇开对微软的抨击不谈,我已经尝试了发送信号(SendSignal),但得到了“CreateRemoteThread failed with 0x00000005”的错误。还有其他什么编码方式能让应用程序A接收信号(任何方便的信号),并如何发送信号呢? - David I. McIntosh
3
@DavidI.McIntosh,听起来你想在两个进程中创建一个命名事件;然后你就可以从一个进程向另一个进程发送信号。请查阅CreateEvent的文档。 - arolson101

20
我想我对这个问题有点晚了,但为了任何遇到同样问题的人,我还是想写点什么。这与我在此处提供的答案相同。
我的问题是,我希望我的应用程序是一个GUI应用程序,但执行的进程应该在后台运行,而不附加任何交互式控制台窗口。我认为这个解决方案也适用于父进程是控制台进程的情况。但你可能需要删除“CREATE_NO_WINDOW”标志。
我使用GenerateConsoleCtrlEvent()和包装器应用程序解决了这个问题。棘手的部分只是文档并没有清楚地说明它的使用方式和其中的陷阱。
我的解决方案基于这里所述的内容。但那并没有真正解释所有细节,并且会出现错误,因此这里是如何使其工作的详细信息。
创建一个新的辅助应用程序“Helper.exe”。这个应用程序将位于您的应用程序(父级)和您想要能够关闭的子进程之间。它还将创建实际的子进程。你必须有这个“中间人”进程,否则GenerateConsoleCtrlEvent()将失败。
使用某种IPC机制,从父进程向助手进程通信,让助手关闭子进程。当助手接收到此事件时,它会调用“GenerateConsoleCtrlEvent(CTRL_BREAK,0)”,这将关闭它自己和子进程。我自己使用了一个事件对象来完成这个操作,当父进程想要取消子进程时,它会完成该事件。
创建Helper.exe时,请使用CREATE_NO_WINDOW和CREATE_NEW_PROCESS_GROUP选项。创建子进程时不要使用任何标志(0),这意味着它将从其父进程继承控制台。如果没有这样做,它将忽略该事件。
每个步骤都按照这种方式执行非常重要。我尝试了各种组合,但这种组合是唯一有效的。您无法发送CTRL_C事件。它会返回成功,但进程将忽略它。CTRL_BREAK是唯一有效的。这并不重要,因为它们最终都会调用ExitProcess()。
您也不能直接使用子进程ID作为进程组ID调用GenerateConsoleCtrlEvent(),以允许助手进程继续运行。这也会失败。
我花了一整天的时间来使其工作。这个解决方案对我有用,但如果有人有其他东西要补充,请随时分享。我在网络上找到了很多遇到类似问题的人,但没有明确的解决方案。GenerateConsoleCtrlEvent()的工作方式也有点奇怪,所以如果有人知道更多细节,请分享。

7
谢谢你的解决方案!这又是Windows API混乱不堪的又一例。 - vitaut

10

如果您的命令行中有Python 3.x可用,那么我发现的解决方案非常简单。首先,保存一个名为ctrl_c.py的文件,其中包含以下内容:

import ctypes
import sys
  
kernel = ctypes.windll.kernel32
    
pid = int(sys.argv[1])
kernel.FreeConsole()
kernel.AttachConsole(pid)
kernel.SetConsoleCtrlHandler(None, 1)
kernel.GenerateConsoleCtrlEvent(0, 0)
sys.exit(0)

然后调用:
python ctrl_c.py 12345

如果那不起作用,我建议尝试windows-kill项目:

1
非常抱歉,我知道这可能有些不必要,但我真的想说谢谢! :) 我已经花了相当多的时间来研究这个问题,并希望ffmpeg能够优雅地退出。使用内置的process.send_signal(signal.CTRL_C_EVENT)在控制台上运行得非常好,但在pythonw上却出现了OSError。这是一个解决方法!谢谢! - ewerybody
你应该得到比3个更多的赞。这是我找到的唯一能够处理在不同控制台打开子进程的解决方案。 - polortiz40

9
一些情况下,如果你调用GenerateConsoleCtrlEvent()来针对另一个进程发送事件,会返回错误。但是,你可以附加到另一个控制台应用程序,并向所有子进程发送事件。
void SendControlC(int pid)
{
    AttachConsole(pid); // attach to process console
    SetConsoleCtrlHandler(NULL, TRUE); // disable Control+C handling for our app
    GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); // generate Control+C event
}

如何返回控制权?然后我发送控制-C,停止程序,但我无法手动操作。 - Yaroslav L.
控制权应该返回到哪里? - KindDragon
我认为你可以调用SetConsoleCtrlHandler(NULL, FALSE)来移除你的处理程序,参考其他不同的响应。 - rogerdpack

8

编辑:

对于 GUI 应用程序,在 Windows 开发中处理这个问题的"正常"方法是向进程的主窗口发送 WM_CLOSE 消息。

对于控制台应用程序,您需要使用 SetConsoleCtrlHandler 添加一个 CTRL_C_EVENT

如果应用程序不遵守该操作,您可以调用TerminateProcess


我想在命令行应用程序中执行此操作(无窗口)。TerminateProcess是一种强制终止机制(类似于SIGKILL),因此不会给终止的应用程序任何清理的机会。 - Matthew Murdoch
@Matthew Murdoch:更新了如何在控制台应用程序中处理此问题的信息。您还可以通过相同的机制捕获其他事件,包括Windows关闭或控制台关闭等。有关完整详细信息,请参阅MSDN。 - Reed Copsey
@Reed Copsey: 但我想从一个单独的进程开始这个过程。我已经查看了GenerateConsoleCtrlEvent(从SetConsoleCtrlHandler引用),但这似乎只有在被终止的进程与执行终止操作的进程属于同一“进程组”时才起作用...有什么想法吗? - Matthew Murdoch
1
Matthew:我唯一的想法是找到进程,找到它的父进程(控制台),获取父进程的主窗口句柄(控制台),然后使用SendKeys直接将Ctrl+C发送给它。这应该会导致您的控制台进程接收到CTRL_C_EVENT。不过我还没有尝试过,不确定它能否正常工作。 - Reed Copsey
这里有一个C++版本的SendKeys等效代码: https://dev59.com/AlnUa4cB1Zd3GeqPcaRs 另请参阅:https://dev59.com/kGkv5IYBdhLWcg3wtS8N,尽管即使在那种情况下也不能保证它能正常工作... - rogerdpack

8

以下是我在C++应用程序中使用的代码。

优点:

  • 可从控制台应用程序运行
  • 可从Windows服务运行
  • 无需延迟
  • 不关闭当前应用程序

缺点:

  • 主控制台被丢失并创建了一个新的控制台(参见FreeConsole
  • 控制台切换会产生奇怪的结果...

// Inspired from https://dev59.com/lXRA5IYBdhLWcg3w0xfk#15281070
// and http://stackoverflow.com/q/40059902/1529139
bool signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent)
{
    bool success = false;
    DWORD thisConsoleId = GetCurrentProcessId();
    // Leave current console if it exists
    // (otherwise AttachConsole will return ERROR_ACCESS_DENIED)
    bool consoleDetached = (FreeConsole() != FALSE);

    if (AttachConsole(dwProcessId) != FALSE)
    {
        // Add a fake Ctrl-C handler for avoid instant kill is this console
        // WARNING: do not revert it or current program will be also killed
        SetConsoleCtrlHandler(nullptr, true);
        success = (GenerateConsoleCtrlEvent(dwCtrlEvent, 0) != FALSE);
        FreeConsole();
    }

    if (consoleDetached)
    {
        // Create a new console if previous was deleted by OS
        if (AttachConsole(thisConsoleId) == FALSE)
        {
            int errorCode = GetLastError();
            if (errorCode == 31) // 31=ERROR_GEN_FAILURE
            {
                AllocConsole();
            }
        }
    }
    return success;
}

使用示例:

DWORD dwProcessId = ...;
if (signalCtrl(dwProcessId, CTRL_C_EVENT))
{
    cout << "Signal sent" << endl;
}

5

感谢jimhark的答案和其他答案,我找到了在PowerShell中完成它的方法:

$ProcessID = 1234
$MemberDefinition = '
    [DllImport("kernel32.dll")]public static extern bool FreeConsole();
    [DllImport("kernel32.dll")]public static extern bool AttachConsole(uint p);
    [DllImport("kernel32.dll")]public static extern bool GenerateConsoleCtrlEvent(uint e, uint p);
    public static void SendCtrlC(uint p) {
        FreeConsole();
        if (AttachConsole(p)) {
            GenerateConsoleCtrlEvent(0, p);
            FreeConsole();
        }
        AttachConsole(uint.MaxValue);
    }'
Add-Type -Name 'dummyName' -Namespace 'dummyNamespace' -MemberDefinition $MemberDefinition
[dummyNamespace.dummyName]::SendCtrlC($ProcessID) 


发送 GenerateConsoleCtrlEvent 给目标进程组而非发送给调用进程所共享的所有控制台进程,并通过AttachConsole 回到当前进程父进程的控制台使得事情得以顺利进行。

这个脚本中的结尾}需要被移除才能正常工作。当我试图修复时,出现了“建议编辑队列已满”的提示。 - Johan Walles

4
        void SendSIGINT( HANDLE hProcess )
        {
            DWORD pid = GetProcessId(hProcess);
            FreeConsole();
            if (AttachConsole(pid))
            {
                // Disable Ctrl-C handling for our program
                SetConsoleCtrlHandler(NULL, true);

                GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); // SIGINT

                //Re-enable Ctrl-C handling or any subsequently started
                //programs will inherit the disabled state.
                SetConsoleCtrlHandler(NULL, false);

                WaitForSingleObject(hProcess, 10000);
            }
        }

3

这个项目包含可供下载的预编译二进制文件,可以在此处获取:https://github.com/alirdn/windows-kill/releases - 这个回答应该有更多的赞。 - boly38
代码库似乎是空的。 - dothebart
1
@dothebart:是的,我刚刚意识到了。我更新了链接。 - BullyWiiPlaza
这似乎是该项目的新正确链接:https://github.com/ElyDotDev/windows-kill - Tim Ludwinski

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