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

时间:2011-08-16 22:01:41

标签: python windows subprocess

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

proc.send_signal(signal.SIGINT)

完美无缺。在Windows上,会抛出错误(“不支持信号2”或类似的东西)。我正在使用Python 2.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)

7 个答案:

答案 0 :(得分:12)

使用包装器(如所提供的链接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_C_EVENT,0)命令的包装器。我在我的应用程序中使用了套接字。

<强>解释

Preinformation

  • send_signal(CTRL_C_EVENT)不起作用,因为CTRL_C_EVENT仅适用于os.kill[REF1]
  • os.kill(CTRL_C_EVENT)将信号发送到当前cmd窗口中运行的所有进程[REF2]
  • Popen(..., creationflags=CREATE_NEW_PROCESS_GROUP)不起作用,因为进程组会忽略CTRL_C_EVENT[REF2] 这是python文档[REF3]
  • 中的一个错误

实施解决方案

  1. 让您的程序使用Windows shell命令 start 在不同的cmd窗口中运行。
  2. 在控制应用程序和应该获得CTRL-C信号的应用程序之间添加一个CTRL-C请求包装器。包装器将在与应该获得CTRL-C信号的应用程序相同的cmd窗口中运行。
  3. 包装器将关闭自身以及应该通过向cmd窗口中的所有进程发送CTRL_C_EVENT来获取CTRL-C信号的程序。
  4. 控制程序应该能够请求包装器发送CTRL-C信号。这可以通过IPC方式实现,例如套接字。
  5. 有用的帖子是:

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

    更新:基于IPC的CTRL-C Wrapper

    在这里你可以找到一个自写的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()
    

    代码

    import socket
    import subprocess
    import time
    import random
    import signal, os, sys
    
    
    class Popen:
      _port = random.randint(10000, 50000)
      _connection = ''
    
      def _start_ctrl_c_wrapper(self, cmd):
        cmd_str = "start \"\" python winctrlc.py "+"\""+cmd+"\""+" "+str(self._port)
        subprocess.Popen(cmd_str, shell=True)
    
      def _create_connection(self):
        self._connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._connection.connect(('localhost', self._port))
    
      def send_ctrl_c(self):
        self._connection.send(Wrapper.TERMINATION_REQ)
        self._connection.close()
    
      def __init__(self, cmd):
        self._start_ctrl_c_wrapper(cmd)
        self._create_connection()
    
    
    class Wrapper:
      TERMINATION_REQ = "Terminate with CTRL-C"
    
      def _create_connection(self, port):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.bind(('localhost', port))
        s.listen(1)
        conn, addr = s.accept()
        return conn
    
      def _wait_on_ctrl_c_request(self, conn):
        while True:
          data = conn.recv(1024)
          if data == self.TERMINATION_REQ:
            ctrl_c_received = True
            break
          else:
            ctrl_c_received = False
        return ctrl_c_received
    
      def _cleanup_and_fire_ctrl_c(self, conn):
        conn.close()
        os.kill(signal.CTRL_C_EVENT, 0)
    
      def _signal_handler(self, signal, frame):
        time.sleep(1)
        sys.exit(0)
    
      def __init__(self, cmd, port):
        signal.signal(signal.SIGINT, self._signal_handler)
        subprocess.Popen(cmd)
        conn = self._create_connection(port)
        ctrl_c_req_received = self._wait_on_ctrl_c_request(conn)
        if ctrl_c_req_received:
          self._cleanup_and_fire_ctrl_c(conn)
        else:
          sys.exit(0)
    
    
    if __name__ == "__main__":
      command_string = sys.argv[1]
      port_no = int(sys.argv[2])
      Wrapper(command_string, port_no)
    

答案 1 :(得分:8)

尝试使用ctypes调用GenerateConsoleCtrlEvent函数。在创建新进程组时,进程组ID应与pid相同。所以,像

import ctypes

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

应该有用。

更新:你是对的,我错过了细节的那一部分。这里的a post提出了一个可能的解决方案,尽管它有点笨拙。更多详情请见this answer

答案 2 :(得分:3)

我的解决方案还涉及包装脚本,但是它不需要IPC,因此使用起来要简单得多。

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

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)

必须在单独的控制台中启动初始进程,以便Ctrl-C事件不会泄漏。例子

p = subprocess.Popen(['some_command'], creationflags=subprocess.CREATE_NEW_CONSOLE)

# Do something else

subprocess.check_call([sys.executable, 'ctrl_c.py', str(p.pid)]) # Send Ctrl-C

我在其中将包装脚本命名为ctrl_c.py

答案 3 :(得分:2)

对于那些对“快速修复”感兴趣的人,我制作了一个基于 console-ctrlSiyuan Ren's answer 包,使其更易于使用。

只需运行 pip install console-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)

答案 4 :(得分:1)

这是一个完全有效的示例,不需要在目标脚本中进行任何修改

这将覆盖sitecustomize模块,因此它可能不适用于每种情况。但是,在这种情况下,您可以在站点包中使用* .pth文件在子进程启动时执行代码(请参见https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html)。

编辑这仅适用于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()

custom-site \ sitecustomize.py

import ctypes
try:
    ctypes.windll.kernel32.SetConsoleCtrlHandler(None, False)
except Exception as e:
    print('SetConsoleCtrlHandler Error %s' % e)
else:
    print('SetConsoleCtrlHandler OK')

sub.py

import atexit
import time


def cleanup():
    print ('cleanup')

atexit.register(cleanup)


while True:
    time.sleep(1)

答案 5 :(得分:0)

  

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

为了澄清,在命令窗口中手动运行fio时,它似乎正在按预期运行。使用CTRL + BREAK中断而不按预期存储日志,CTRL + C也按预期完成写入文件。问题似乎出现在CTRL_C_EVENT的信号中。

它似乎是Python中的一个错误,但可能是Windows中的一个错误。还有一件事,我运行了一个cygwin版本,并在python中发送了ctrl + c,但是我们还没有真正运行本机窗口。

示例:

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)

答案 6 :(得分:0)

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

该解决方案改编自this stack overflow post,但我认为它在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)