web-dev-qa-db-ja.com

^ CをWindows上のPythonサブプロセスオブジェクトに送信する

(Cで書かれた)テスト対象のプログラムを送信してシャットダウンする必要があるテストハーネス(Pythonで書かれた) ^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)
24
zwol

Windowsstartコマンドを使用して新しいコンソールウィンドウで開始されるラッパー(Vinayが提供するリンクで説明されている)を使用することによる解決策があります。

ラッパーのコード:

_#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)コマンドを実行するラッパーを制御できるようにする必要があります。アプリケーションでは、この目的でソケットを使用しました。

説明:

事前情報

  • _CTRL_C_EVENT_は_os.kill_専用であるため、send_signal(CTRL_C_EVENT)は機能しません。 [REF1]
  • os.kill(CTRL_C_EVENT)は、現在のcmdウィンドウで実行されているすべてのプロセスに信号を送信します [REF2]
  • _CTRL_C_EVENT_はプロセスグループで無視されるため、Popen(..., creationflags=CREATE_NEW_PROCESS_GROUP)は機能しません。 [REF2] これはpythonドキュメント [REF3] のバグです

実装されたソリューション

  1. Windowsシェルコマンドstartを使用して、プログラムを別のコマンドウィンドウで実行します。
  2. コントロールアプリケーションとCTRL-C信号を取得するアプリケーションの間にCTRL-Cリクエストラッパーを追加します。ラッパーは、CTRL-C信号を取得する必要があるアプリケーションと同じcmdウィンドウで実行されます。
  3. ラッパーはそれ自体と、cmdウィンドウのすべてのプロセスにCTRL_C_EVENTを送信することによってCTRL-C信号を取得する必要があるプログラムをシャットダウンします。
  4. 制御プログラムは、CTRL-C信号を送信するようにラッパーに要求できる必要があります。これは、IPCは、ソケットなどを意味します。

役立つ投稿は次のとおりです。

私は新しいユーザーであり、3つ以上のリンクを投稿することは許可されていないため、リンクの前にあるhttpを削除する必要がありました。

更新:IPC based CTRL-C Wrapper

ここでは、ソケットベースのIPCを含むCTRL-Cラッピングを提供する自己記述型のpythonモジュールを見つけることができます。構文は、サブプロセスモジュールと非常に似ています。

使用法:

_>>> 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)
_
13
blablub

GenerateConsoleCtrlEventを使用して ctypes 関数を呼び出してみてください。新しいプロセスグループを作成するとき、プロセスグループIDはpidと同じである必要があります。だから、

import ctypes

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

動作するはずです。

更新:そうです、詳細の一部を逃しました。ここに 投稿 これは少し厄介ですが、可能な解決策を提案します。詳細は この回答 にあります。

7
Vinay Sajip

以下は、ターゲットスクリプトを変更する必要がない完全に機能する例です

これはsitecustomizeモジュールを上書きするので、すべてのシナリオに適しているとは限りません。ただし、この場合、サイトパッケージ内の* .pthファイルを使用して、サブプロセスの起動時にコードを実行できます( https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html を参照)。

Editこれは、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
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)
2
schlamar

以下の利点を持つ単一ファイルソリューションがあります。-外部ライブラリなし。 (ctypes以外)-特定の方法でプロセスを開く必要はありません。

解決策は このスタックオーバーフローの投稿 から変更されていますが、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)
1
MHDante

私のソリューションにはラッパースクリプトも含まれていますが、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という名前を付けました。

1
Siyuan Ren

私はこれを試しましたが、何らかの理由で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のバージョンを実行してctrl + cをpythonも同様に機能しましたが、ここでもネイティブウィンドウを実際に実行していません。

例:

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)
0
Gary Baird