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

Posted

技术标签:

【中文标题】在 Windows 上将 ^C 发送到 Python 子进程对象【英文标题】:Sending ^C to Python subprocess objects on Windows 【发布时间】:2011-10-28 11:23:08 【问题描述】:

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

proc.send_signal(signal.SIGINT)

完美运行。在 Windows 上,这会引发错误(“不支持信号 2”或类似的东西)。我在 Windows 上使用 Python 2.7,所以我觉得我应该可以这样做

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)

【问题讨论】:

这可能是唯一的方法 - code.activestate.com/recipes/… 使用 win32api oh 或 ctypes。 subprocess.kill 会为我调用 TerminateProcess 就好了,但这不会生成 ^C。我特别需要伪造在控制台输入 ^C 的行为。 试试这个 - rutherfurd.net/python/sendkeys 。显然 SendKeys.SendKeys("^c") 应该这样做。 那行不通;它与 活动窗口 进行通信,该窗口可能不是运行进程的控制台窗口——如果它恰好是活动窗口,它将为 其中运行的每个进程,包括测试工具本身。我想要的效果是GenerateConsoleCtrlEvent 的效果(这就是subprocess.send_signal(signal.CTRL_C_EVENT) 在Python 2.7 中的记录...) 【参考方案1】:

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

import ctypes

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

应该可以。

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

【讨论】:

这也不起作用,所以我重新阅读了 MSDN 页面并意识到它明确表示“您不能将 CTRL_C_EVENT 发送到进程组,它没有效果”。相反,发送 CTRL_BREAK_EVENT 确实有效(甚至没有 ctypes),并且在玩具测试程序中完全符合我的要求,但是当我在我的真实被测程序中使用它时,我得到“遇到问题并需要关闭”对话框盒子一遍又一遍。有什么想法吗? @zwol:文档说 “无法为进程组生成此信号。”os.kill(0, signal.CTRL_C_EVENT) 在 Windows 控制台中为我在 ipython 中生成 KeyboardInterrupt 为如果我手动按下了Ctrl+C 即,您可以将CTRL_C_EVENT0 一起使用(“信号在所有共享调用进程控制台的进程中生成。”)。 MSDN 出错,进程组无法接收 Ctrl+C。这是一个特殊的说法,因为每个进程要么是新进程组的领导者,要么继承其父进程组。默认情况下,新组中的第一个进程在其ProcessParameters->ConsoleFlags 中禁用了Ctrl+C,该ProcessParameters->ConsoleFlags 由子进程继承。控制台本身没有注意到这个标志;它由控制线程启动函数CtrlRoutine在kernelbase.dll中处理。需要 Ctrl+C 支持的进程应通过 SetConsoleCtrlHandler(NULL, FALSE) 覆盖继承/初始标志值。【参考方案2】:

有一个解决方案是使用包装器(如 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) 命令。为此,我在我的应用程序中使用了套接字。

说明:

预告

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]中的一个错误

实施的解决方案

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

有用的帖子是:

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

http://social.msdn.microsoft.com/Forums/en-US/windowsgeneraldevelopmentissues/thread/dc9586ab-1ee8-41aa-a775-cf4828ac1239/#6589714f-12a7-447e-b214-27372f31ca11 Can I send a ctrl-C (SIGINT) to an application on Windows? Sending SIGINT to a subprocess of python http://bugs.python.org/issue9524 http://ss64.com/nt/start.html http://objectmix.com/python/387639-sending-cntrl-c.html#post1443948

更新:基于 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)

【讨论】:

这种技术(父级向自身及其相关进程发送 Ctrl+C)确实有效!我是独立到达的,但我发现应该在父进程中等待,直到处理 SIGINT,以避免信号中断,例如到达后系统调用。 参数顺序错误。它应该是 os.kill(pid, sig) 而不是 os.kill(sig, pid)。虽然os.kill(0, signal.CTRL_C_EVENT) 不会中断input() 在 Windows 7 中的 vm 中对 Python 3.5 的调用(目的是将 Ctrl+C 发送到共享控制台的所有进程) send_signal(CTRL_C_EVENT) 工作正常,前提是子进程是进程组的领导者并通过SetConsoleCtrlHandler(NULL, FALSE) 手动启用 Ctrl+C,这将由其自己的子进程继承。文档声称“CTRL_C_EVENT 被进程组忽略”是无稽之谈。每个进程都在一个进程组中。一个新组最初禁用了 Ctrl+C。 os.kill(0, CTRL_C_EVENT)GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0) 是火腿。它将事件发送到连接到控制台的所有进程,包括祖先。使用进程组,如果禁用 Ctrl+C,则使用 Ctrl+Break。如果它不处理 Ctrl+Break,那么它也可能不处理关闭控制台窗口的 Ctrl+Close,因为这两个事件都映射到 CSIGBREAK。如果可能的话,这是一个疏忽,应该作为错误报告提交。所有需要特殊处理才能彻底关闭的控制台应用程序都应处理 Ctrl+Break 和 Ctrl+Close。【参考方案3】:

我一直在尝试这个,但由于某种原因 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)

【讨论】:

【参考方案4】:

这是一个完整的工作示例,不需要对目标脚本进行任何修改

这会覆盖sitecustomize 模块,因此它可能并不适合所有场景。但是,在这种情况下,您可以使用 site-packages 中的 *.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()

自定义站点\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)

【讨论】:

这不是检查SetConsoleCtrlHandler 是否失败。 ctypes 不会为失败的函数调用引发 Python 异常。这必须手动完成,或者使用 ctypes errcheck 函数自动完成。无论如何,在这种情况下我们不需要引发异常,因为没有任何事情要做,但我们应该检查并记录失败。使用kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)。然后,例如,if not kernel32.SetConsoleCtrlHandler(None, False): print('SetConsoleCtrlHandler Error: ', ctypes.get_last_error(), file=sys.stderr) 也就是说,如果我们控制子进程,我们还应该有一个 C SIGBREAK 处理程序来彻底关闭。这将为 CTRL_BREAK_EVENTCTRL_CLOSE_EVENT 调用(即 Ctrl+Break 或关闭控制台窗口)。不幸的是,Python 解释器阻止处理由关闭控制台生成的SIGBREAK,因为它的 C 信号处理程序立即返回,因此 csrss.exe 在 Python 处理程序执行之前终止进程。作为一种解决方法,我们可以通过 SetConsoleCtrlHandler 设置一个 ctypes 回调处理程序,它绕过 Python 的 C 处理程序。【参考方案5】:

我有一个具有以下优点的单文件解决方案: - 没有外部库。 (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)

【讨论】:

【参考方案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)

初始进程必须在单独的控制台中启动,这样 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

【讨论】:

非常有用的例子!谢谢!我没有使用 ctrl_c.py 作为文件,而是编写了程序并使用 multiprocessing.Process() 运行它完美工作。 重要:要杀死的进程必须有一个控制台,所以它应该以creationflags=CREATE_NEW_CONSOLE, startupinfo=STARTUPINFO(dwFlags=STARTF_USESHOWWINDOW, wShowWindow=SW_HIDE)开始【参考方案7】:

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

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

【讨论】:

【参考方案8】:

(这应该是Siyuan Ren's answer 下的评论,但我没有足够的代表,所以这里有一个稍长的版本。)

如果您不想创建任何可以使用的帮助脚本:

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

# Do something else

subprocess.run([
    sys.executable,
    "-c",
    "import ctypes, 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)",
    str(p.pid)
]) # Send Ctrl-C

但是如果你使用 PyInstaller 它将不起作用 - sys.executable 指向你的可执行文件,而不是 Python 解释器。为了解决这个问题,我为 Windows 创建了一个小型实用程序:https://github.com/anadius/ctrlc

现在您可以使用以下命令发送 Ctrl+C 事件:

subprocess.run(["ctrlc", str(p.pid)])

【讨论】:

以上是关于在 Windows 上将 ^C 发送到 Python 子进程对象的主要内容,如果未能解决你的问题,请参考以下文章

在 Windows 7 上将“django-admin.py”路径添加到命令行

在 Windows 上将 Python 添加到 PATH

我应该在 Windows 上将 Eclipse 安装到哪个文件夹?

如何在 Windows 上将 c++11 线程关联设置为 NUMA 节点?

在Windows 10上将.NET Core控制台应用程序切换到全屏模式

在 Windows 上将 TagLib 编译成 Qt C++ 项目