将^C发送到Windows上的Python子进程对象

将^C发送到Windows上的Python子进程对象,python,windows,subprocess,Python,Windows,Subprocess,我有一个测试工具(用Python编写),需要通过在Unix上发送^C来关闭被测程序(用C编写) proc.send_signal(signal.SIGINT) 工作完美。在Windows上,这会引发错误(“不支持信号2”或类似的内容)。我正在使用Python2.7 for Windows,因此我觉得我应该可以这样做 proc.send_signal(signal.CTRL_C_EVENT) 但这根本没用。我该怎么办?这是创建子流程的代码: # Windows needs an extra a

我有一个测试工具(用Python编写),需要通过在Unix上发送^C来关闭被测程序(用C编写)

proc.send_signal(signal.SIGINT)
工作完美。在Windows上,这会引发错误(“不支持信号2”或类似的内容)。我正在使用Python2.7 for Windows,因此我觉得我应该可以这样做

proc.send_signal(signal.CTRL_C_EVENT)
但这根本没用。我该怎么办?这是创建子流程的代码:

# Windows needs an extra argument passed to subprocess.Popen,
# but the constant isn't defined on Unix.
try: kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
except AttributeError: pass
proc = subprocess.Popen(argv,
                        stdin=open(os.path.devnull, "r"),
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        **kwargs)
尝试使用
ctypes
调用函数。创建新流程组时,流程组ID应与pid相同。比如

import ctypes

ctypes.windll.kernel32.GenerateConsoleCtrlEvent(0, proc.pid) # 0 => Ctrl-C
应该有用


更新:你说得对,我错过了那部分细节。这是一个可能的解决方案,虽然有点含糊不清。更多详细信息请参见。

有一种解决方案,使用包装器(如提供的链接Vinay中所述),该包装器通过Windows start命令在新控制台窗口中启动

包装器的代码:

#wrapper.py
import subprocess, time, signal, sys, os

def signal_handler(signal, frame):
  time.sleep(1)
  print 'Ctrl+C received in wrapper.py'

signal.signal(signal.SIGINT, signal_handler)
print "wrapper.py started"
subprocess.Popen("python demo.py")
time.sleep(3) #Replace with your IPC code here, which waits on a fire CTRL-C request
os.kill(signal.CTRL_C_EVENT, 0)
捕捉CTRL-C的程序代码:

#demo.py

import signal, sys, time

def signal_handler(signal, frame):
  print 'Ctrl+C received in demo.py'
  time.sleep(1)
  sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)
print 'demo.py started'
#signal.pause() # does not work under Windows
while(True):
  time.sleep(1)
启动包装器,如:

PythonPrompt> import subprocess
PythonPrompt> subprocess.Popen("start python wrapper.py", shell=True)
您需要添加一些IPC代码,它允许您控制启动os.kill(signal.CTRL\u C\u EVENT,0)命令的包装器。我在应用程序中为此使用了套接字

说明:

预先信息

  • send\u信号(CTRL\u C\u事件)
    不工作,因为
    CTRL\u C\u事件
    仅用于
    os.kill
  • os.kill(CTRL\u C\u事件)
    向当前cmd窗口中运行的所有进程发送信号
  • Popen(…,creationflags=CREATE\u NEW\u PROCESS\u GROUP)
    不起作用,因为进程组忽略了
    CTRL\u C\u事件。
    这是python文档中的一个bug
实施方案

  • 使用Windows shell命令start,让程序在不同的cmd窗口中运行
  • 在控件应用程序和应该获得CTRL-C信号的应用程序之间添加CTRL-C请求包装器。包装器将在与应用程序相同的cmd窗口中运行,应用程序应获得CTRL-C信号
  • 包装器将关闭自身和程序,该程序应通过向cmd窗口中的所有进程发送CTRL_C_事件来获取CTRL-C信号
  • 控制程序应该能够请求包装器发送CTRL-C信号。这可以通过IPC方式实现,例如插座
  • 有用的帖子有:

    我必须删除链接前面的http,因为我是新用户,不允许发布两个以上的链接

    更新:基于IPC的CTRL-C包装程序

    在这里,您可以找到一个自编python模块,它提供了一个CTRL-C包装,包括一个基于套接字的IPC。 语法与子流程模块非常相似

    用法:

    >>> import winctrlc
    >>> p1 = winctrlc.Popen("python demo.py")
    >>> p2 = winctrlc.Popen("python demo.py")
    >>> p3 = winctrlc.Popen("python demo.py")
    >>> p2.send_ctrl_c()
    >>> p1.send_ctrl_c()
    >>> p3.send_ctrl_c()
    
    代码

    我一直在尝试这个方法,但由于某种原因,ctrl+break可以工作,而ctrl+c不能。因此,使用
    os.kill(signal.CTRL\u C\u事件,0)
    失败,但执行
    os.kill(signal.CTRL\u C\u事件,1)
    有效。我被告知这与创建进程所有者是唯一可以传递ctrl c的人有关?这有意义吗

    澄清一下,当在命令窗口中手动运行fio时,它似乎正在按预期运行。使用CTRL+BREAK会按预期中断而不存储日志,并且CTRL+C也会按预期完成对文件的写入。问题似乎出在CTRL_C_事件的信号中

    这看起来几乎是Python中的一个bug,但更可能是Windows中的bug。还有一件事,我运行了一个cygwin版本,并在python中发送了ctrl+c,同样有效,但是我们在那里并没有真正运行本机windows

    例如:

    import subprocess, time, signal, sys, os
    command = '"C:\\Program Files\\fio\\fio.exe" --rw=randrw --bs=1M --numjobs=8 --iodepth=64 --direct=1 ' \
        '--sync=0 --ioengine=windowsaio --name=test --loops=10000 ' \
        '--size=99901800 --rwmixwrite=100 --do_verify=0 --filename=I\\:\\test ' \
        '--thread --output=C:\\output.txt'
    def signal_handler(signal, frame):
      time.sleep(1)
      print 'Ctrl+C received in wrapper.py'
    
    signal.signal(signal.SIGINT, signal_handler)
    print 'command Starting'
    subprocess.Popen(command)
    print 'command started'
    time.sleep(15) 
    print 'Timeout Completed'
    os.kill(signal.CTRL_C_EVENT, 0)
    

    下面是一个完全正常工作的示例,不需要对目标脚本进行任何修改

    这将覆盖
    sitecustomize
    模块,因此它可能不适用于所有场景。但是,在这种情况下,您可以使用站点包中的*.pth文件在子流程启动时执行代码(请参阅)

    编辑这仅适用于Python中的子流程。其他进程必须手动调用
    SetConsoleCtrlHandler(NULL,FALSE)

    main.py

    import os
    import signal
    import subprocess
    import sys
    import time
    
    
    def main():
        env = os.environ.copy()
        env['PYTHONPATH'] = '%s%s%s' % ('custom-site', os.pathsep,
                                        env.get('PYTHONPATH', ''))
        proc = subprocess.Popen(
            [sys.executable, 'sub.py'],
            env=env,
            creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
            )
        time.sleep(1)
        proc.send_signal(signal.CTRL_C_EVENT)
        proc.wait()
    
    
    if __name__ == '__main__':
        main()
    
    自定义站点\sitecustomize.py

    import ctypes
    import sys
    kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
    
    if not kernel32.SetConsoleCtrlHandler(None, False):
        print('SetConsoleCtrlHandler Error: ', ctypes.get_last_error(),
              file=sys.stderr)
    
    sub.py

    import atexit
    import time
    
    
    def cleanup():
        print ('cleanup')
    
    atexit.register(cleanup)
    
    
    while True:
        time.sleep(1)
    

    我有一个单文件解决方案,具有以下优点: -没有外部库。(C类型除外) -不需要以特定的方式打开流程

    该解决方案是从中改编的,但我认为它在python中要优雅得多

    import os
    import signal
    import subprocess
    import sys
    import time
    
    # Terminates a Windows console app sending Ctrl-C
    def terminateConsole(processId: int, timeout: int = None) -> bool:
        currentFilePath = os.path.abspath(__file__)
        # Call the below code in a separate process. This is necessary due to the FreeConsole call.
        try:
            code = subprocess.call('{} {} {}'.format(sys.executable, currentFilePath, processId), timeout=timeout)
            if code == 0: return True
        except subprocess.TimeoutExpired:
            pass
    
        # Backup plan
        subprocess.call('taskkill /F /PID {}'.format(processId))
    
    
    if __name__ == '__main__':
        pid = int(sys.argv[1])
    
        import ctypes
        kernel = ctypes.windll.kernel32
    
        r = kernel.FreeConsole()
        if r == 0: exit(-1)
        r = kernel.AttachConsole(pid)
        if r == 0: exit(-1)
        r = kernel.SetConsoleCtrlHandler(None, True)
        if r == 0: exit(-1)
        r = kernel.GenerateConsoleCtrlEvent(0, 0)
        if r == 0: exit(-1)
        r = kernel.FreeConsole()
        if r == 0: exit(-1)
    
        # use tasklist to wait while the process is still alive.
        while True:
            time.sleep(1)
            # We pass in stdin as PIPE because there currently is no Console, and stdin is currently invalid.
            searchOutput: bytes = subprocess.check_output('tasklist /FI "PID eq {}"'.format(pid), stdin=subprocess.PIPE)
            if str(pid) not in searchOutput.decode(): break;
    
        # The following two commands are not needed since we're about to close this script.
        # You can leave them here if you want to do more console operations.
        r = kernel.SetConsoleCtrlHandler(None, False)
        if r == 0: exit(-1)
        r = kernel.AllocConsole()
        if r == 0: exit(-1)
    
        exit(0)
    

    我的解决方案还涉及一个包装器脚本,但它不需要IPC,所以使用起来要简单得多

    包装器脚本首先将自身从任何现有控制台分离,然后连接到目标控制台,然后归档Ctrl-C事件

    导入ctypes
    导入系统
    kernel=ctypes.windell.kernel32
    pid=int(sys.argv[1])
    kernel.FreeConsole()
    附件控制台(pid)
    setConsoletrlHandler(无,1)
    kernel.generateConsoleControlEvent(0,0)
    系统出口(0)
    
    初始进程必须在单独的控制台中启动,以便Ctrl-C事件不会泄漏。范例

    p=subprocess.Popen(['some_command'],creationflags=subprocess.CREATE_NEW_控制台)
    #做点别的
    子进程检查调用([sys.executable,'ctrl_c.py',str(p.pid)])#发送ctrl-c
    
    对于那些对“快速修复”感兴趣的人,我将包装器脚本命名为
    ctrl_c.py

    ,我基于它制作了一个包,使其更易于使用

    只需运行
    pip安装控制台ctrl
    ,并在代码中:

    import console_ctrl
    import subprocess
    
    # Start some command IN A SEPARATE CONSOLE
    p = subprocess.Popen(['some_command'], creationflags=subprocess.CREATE_NEW_CONSOLE)
    # ...
    
    # Stop the target process
    console_ctrl.send_ctrl_c(p.pid)
    

    对于win32api oh或ctypes,这可能是唯一的方法。
    subprocess.kill
    将调用
    TerminateProcess
    对我来说很好,但这不会生成^C。我特别需要在控制台上伪造键入^C的行为。试试这个-。
    import console_ctrl
    import subprocess
    
    # Start some command IN A SEPARATE CONSOLE
    p = subprocess.Popen(['some_command'], creationflags=subprocess.CREATE_NEW_CONSOLE)
    # ...
    
    # Stop the target process
    console_ctrl.send_ctrl_c(p.pid)