我曾经编写过跨平台(Windows / Unix)应用程序,在命令行启动时以相同的方式处理用户键入的 Ctrl-C 组合键,即优雅地终止应用程序。
在 Windows 上,是否可以从另一个(无关的)进程发送 Ctrl-C/SIGINT/等价信号到进程中,从而请求其优雅地终止(给予其机会清理资源等)?
我对这个主题进行了一些研究,结果比我预期的更受欢迎。KindDragon的回复是其中至关重要的一点。
我在这个主题上写了一篇较长的博客文章,并创建了一个工作演示程序,演示如何使用这种类型的系统以几种好看的方式关闭命令行应用程序。该文章还列出了我在研究中使用的外部链接。
简而言之,这些演示程序执行以下操作:
pinvoke
隐藏,运行6秒钟,使用pinvoke
显示,使用.Net停止。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){}
}
wait()
,我移除了重新启用Ctrl-C(SetConsoleCtrlHandler(null, false);
)。我进行了几个测试(多次调用,也包括已终止的进程),目前尚未发现任何副作用。 - 56ka start
。)如果您的命令行中有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
process.send_signal(signal.CTRL_C_EVENT)
在控制台上运行得非常好,但在pythonw
上却出现了OSError。这是一个解决方法!谢谢! - ewerybodyGenerateConsoleCtrlEvent()
来针对另一个进程发送事件,会返回错误。但是,你可以附加到另一个控制台应用程序,并向所有子进程发送事件。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 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;
}
感谢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
回到当前进程父进程的控制台使得事情得以顺利进行。 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);
}
}
jstack
来代替特定的问题:https://dev59.com/OnM_5IYBdhLWcg3w2XLw#47723393 - Vadzim