我(过去)编写过跨平台(Windows/Unix)应用程序,当从命令行启动时,以相同的方式处理用户键入Ctrl的C组合(即干净地终止应用程序)。
在 Windows 上是否可以从另一个(不相关的)进程向进程发送Ctrl- C/SIGINT/equivalent 请求它干净地终止(给它一个整理资源等的机会)?
我围绕这个主题做了一些研究,结果证明它比我预期的更受欢迎。KindDragon 的回答是关键点之一。
我就该主题写了一篇较长的博客文章并创建了一个工作演示程序,该程序演示了使用这种类型的系统以几种不错的方式关闭命令行应用程序。那篇文章还列出了我在研究中使用的外部链接。
简而言之,这些演示程序执行以下操作:
pinvoke
,运行 6 秒,使用 显示,使用.Netpinvoke
停止。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){}
}
我最接近解决方案的是SendSignal 3rd 方应用程序。作者列出了源代码和可执行文件。我已经验证它可以在 64 位 Windows 下工作(作为 32 位程序运行,杀死另一个 32 位程序),但我还没有弄清楚如何将代码嵌入到 Windows 程序中(32 位或 64 位)。
这个怎么运作:
在调试器中进行大量挖掘之后,我发现实际执行与 ctrl-break 等信号相关的行为的入口点是 kernel32!CtrlRoutine。该函数与 ThreadProc 具有相同的原型,因此可以直接与 CreateRemoteThread 一起使用,而无需注入代码。但是,这不是导出的符号!它在不同版本的 Windows 上位于不同的地址(甚至有不同的名称)。该怎么办?
这是我最终想出的解决方案。我为我的应用程序安装了一个控制台 ctrl 处理程序,然后为我的应用程序生成一个 ctrl-break 信号。当我的处理程序被调用时,我回头查看堆栈顶部以找出传递给 kernel32!BaseThreadStart 的参数。我抓住第一个参数,这是线程想要的起始地址,也就是 kernel32!CtrlRoutine 的地址。然后我从我的处理程序返回,表明我已经处理了信号并且我的应用程序不应该被终止。回到主线程,我等到 kernel32!CtrlRoutine 的地址被检索到。一旦我得到它,我在目标进程中创建一个带有发现的起始地址的远程线程。这会导致评估目标进程中的 ctrl 处理程序,就像按下 ctrl-break 一样!
好处是只有目标进程受到影响,任何进程(甚至是窗口进程)都可以成为目标。一个缺点是我的小应用程序不能在批处理文件中使用,因为它会在发送 ctrl-break 事件以发现 kernel32!CtrlRoutine 的地址时杀死它。
(start
如果在批处理文件中运行它,则在它之前。)
我想我在这个问题上有点晚了,但无论如何我都会为遇到同样问题的人写一些东西。这与我对这个问题的回答相同。
我的问题是我希望我的应用程序是一个 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() 的工作原理也有点奇怪,所以如果有人知道更多细节,请分享。
如果您为另一个进程调用它会以某种方式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
}
编辑:
对于 GUI 应用程序,在 Windows 开发中处理此问题的“正常”方式是向进程的主窗口发送 WM_CLOSE 消息。
对于控制台应用程序,您需要使用SetConsoleCtrlHandler添加一个CTRL_C_EVENT
.
如果应用程序不遵守这一点,您可以调用TerminateProcess。
这是我在 C++ 应用程序中使用的代码。
积极点:
缺点:
// Inspired from http://stackoverflow.com/a/15281070/1529139
// 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;
}
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);
}
}
感谢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
返回到当前进程的父进程的控制台。
应该说得很清楚,因为目前还不是。 SendSignal 有一个修改和编译的版本来发送 Ctrl-C(默认情况下它只发送 Ctrl+Break)。以下是一些二进制文件:
(2014-3-7):我用 Ctrl-C 构建了 32 位和 64 位版本,它被称为 SendSignalCtrlC.exe,您可以在以下位置下载它:https ://dl.dropboxusercontent.com/u/49065779/ sendsignalctrlc/x86/SendSignalCtrlC.exe https://dl.dropboxusercontent.com/u/49065779/sendsignalctrlc/x86_64/SendSignalCtrlC.exe -- Juraj Michalak
我还镜像了这些文件以防万一:
32 位版本:https
://www.dropbox.com/s/r96jxglhkm4sjz2/SendSignalCtrlC.exe?dl=0
64 位版本:https ://www.dropbox.com /s/hhe0io7mcgcle1c/SendSignalCtrlC64.exe?dl=0
免责声明:我没有构建这些文件。未对编译的原始文件进行任何修改。唯一测试的平台是 64 位 Windows 7。建议改编http://www.latenighthacking.com/projects/2003/sendSignal/上的可用源并自己编译。
在 Java 中,将 JNA 与 Kernel32.dll 库一起使用,类似于 C++ 解决方案。将 CtrlCSender 主方法作为 Process 运行,它只是获取进程的控制台以将 Ctrl+C 事件发送到并生成事件。由于它在没有控制台的情况下单独运行,因此不需要禁用并再次启用 Ctrl+C 事件。
CtrlCSender.java - 基于Nemo1024和KindDragon 的答案。
给定一个已知的进程 ID,这个无控制台应用程序将附加目标进程的控制台并在其上生成一个 CTRL+C 事件。
import com.sun.jna.platform.win32.Kernel32;
public class CtrlCSender {
public static void main(String args[]) {
int processId = Integer.parseInt(args[0]);
Kernel32.INSTANCE.AttachConsole(processId);
Kernel32.INSTANCE.GenerateConsoleCtrlEvent(Kernel32.CTRL_C_EVENT, 0);
}
}
主应用程序- 将 CtrlCSender 作为单独的无控制台进程运行
ProcessBuilder pb = new ProcessBuilder();
pb.command("javaw", "-cp", System.getProperty("java.class.path", "."), CtrlCSender.class.getName(), processId);
pb.redirectErrorStream();
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
Process ctrlCProcess = pb.start();
ctrlCProcess.waitFor();
是的。该windows-kill
项目完全符合您的要求:
windows-kill -SIGINT 1234
如果您的命令行中有可用的 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 项目:https ://github.com/alirdn/windows-kill
我发现这一切都太复杂了,并使用SendKeys将CTRL-C击键发送到命令行窗口(即 cmd.exe 窗口)作为一种解决方法。
我的一个朋友提出了一种完全不同的解决问题的方法,它对我有用。使用如下所示的 vbscript。它启动并应用程序,让它运行 7 秒,然后使用ctrl+c关闭它。
'VBScript 示例
Set WshShell = WScript.CreateObject("WScript.Shell")
WshShell.Run "notepad.exe"
WshShell.AppActivate "notepad"
WScript.Sleep 7000
WshShell.SendKeys "^C"
// Send [CTRL-C] to interrupt a batch file running in a Command Prompt window, even if the Command Prompt window is not visible,
// without bringing the Command Prompt window into focus.
// [CTRL-C] will have an effect on the batch file, but not on the Command Prompt window itself -- in other words,
// [CTRL-C] will not have the same visible effect on a Command Prompt window that isn't running a batch file at the moment
// as bringing a Command Prompt window that isn't running a batch file into focus and pressing [CTRL-C] on the keyboard.
ulong ulProcessId = 0UL;
// hwC = Find Command Prompt window HWND
GetWindowThreadProcessId (hwC, (LPDWORD) &ulProcessId);
AttachConsole ((DWORD) ulProcessId);
SetConsoleCtrlHandler (NULL, TRUE);
GenerateConsoleCtrlEvent (CTRL_C_EVENT, 0UL);
SetConsoleCtrlHandler (NULL, FALSE);
FreeConsole ();
SIGINT 可以使用windows-kill发送到程序,通过语法windows-kill -SIGINT PID
,PID
可以通过微软的pslist获得。
关于捕获 SIGINT,如果您的程序使用 Python,那么您可以像在此解决方案中那样实现 SIGINT 处理/捕获。
基于进程ID,我们可以向进程发送信号以强制或优雅终止或任何其他信号。
列出所有进程:
C:\>tasklist
要杀死进程:
C:\>Taskkill /IM firefox.exe /F
or
C:\>Taskkill /PID 26356 /F
细节:
http://tweaks.com/windows/39559/kill-processes-from-command-prompt/