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

zwo*_*wol 22 python windows subprocess

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

proc.send_signal(signal.SIGINT)
Run Code Online (Sandbox Code Playgroud)

工作得很好.在Windows上,会抛出错误("不支持信号2"或类似的东西).我正在使用Python 2.7 for Windows,所以我觉得我应该能够做到

proc.send_signal(signal.CTRL_C_EVENT)
Run Code Online (Sandbox Code Playgroud)

但这根本不起作用.我需要做什么?这是创建子进程的代码:

# 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)
Run Code Online (Sandbox Code Playgroud)

小智 12

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

包装器代码:

#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)
Run Code Online (Sandbox Code Playgroud)

捕获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)
Run Code Online (Sandbox Code Playgroud)

启动包装器,例如:

PythonPrompt> import subprocess
PythonPrompt> subprocess.Popen("start python wrapper.py", shell=True)
Run Code Online (Sandbox Code Playgroud)

您需要添加一些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方式实现,例如套接字.

有用的帖子是:

我不得不删除链接前面的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()
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

  • 参数的顺序是错误的.它应该是`os.kill(pid,sig)`而不是`os.kill(sig,pid)`.虽然`os.kill(0,signal.CTRL_C_EVENT)`不会在使用Windows 7的vm中中断对Python 3.5的输入()调用(目的是将Ctrl + C发送到共享控制台的所有进程) (2认同)

Vin*_*jip 8

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

import ctypes

ctypes.windll.kernel32.GenerateConsoleCtrlEvent(0, proc.pid) # 0 => Ctrl-C
Run Code Online (Sandbox Code Playgroud)

应该管用.

更新:你是对的,我错过了那部分细节.这里有一篇文章提出了一个可能的解决方案,尽管它有点笨拙.更多细节在这个答案中.


Siy*_*Ren 6

我的解决方案还涉及一个包装脚本,但它不需要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)
Run Code Online (Sandbox Code Playgroud)

初始进程必须在单独的控制台中启动,以便 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
Run Code Online (Sandbox Code Playgroud)

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