Sending ^C to Python subprocess objects on Windows Sending ^C to Python subprocess objects on Windows windows windows

Sending ^C to Python subprocess objects on Windows


There is a solution by using a wrapper (as described in the link Vinay provided) which is started in a new console window with the Windows start command.

Code of the wrapper:

#wrapper.pyimport subprocess, time, signal, sys, osdef 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 requestos.kill(signal.CTRL_C_EVENT, 0)

Code of the program catching CTRL-C:

#demo.pyimport signal, sys, timedef 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 Windowswhile(True):  time.sleep(1)

Launch the wrapper like e.g.:

PythonPrompt> import subprocessPythonPrompt> subprocess.Popen("start python wrapper.py", shell=True)

You need to add some IPC code which allows you to control the wrapper firing the os.kill(signal.CTRL_C_EVENT, 0) command. I used sockets for this purpose in my application.

Explanation:

Preinformation

  • send_signal(CTRL_C_EVENT) does not work because CTRL_C_EVENT is only for os.kill. [REF1]
  • os.kill(CTRL_C_EVENT) sends the signal to all processes running in the current cmd window [REF2]
  • Popen(..., creationflags=CREATE_NEW_PROCESS_GROUP) does not work because CTRL_C_EVENT is ignored for process groups. [REF2]This is a bug in the python documentation [REF3]

Implemented solution

  1. Let your program run in a different cmd window with the Windows shell command start.
  2. Add a CTRL-C request wrapper between your control application and the application which should get the CTRL-C signal. The wrapper will run in the same cmd window as the application which should get the CTRL-C signal.
  3. The wrapper will shutdown itself and the program which should get the CTRL-C signal by sending all processes in the cmd window the CTRL_C_EVENT.
  4. The control program should be able to request the wrapper to send the CTRL-C signal. This might be implemnted trough IPC means, e.g. sockets.

Helpful posts were:

I had to remove the http in front of the links because I'm a new user and are not allowed to post more than two links.

Update: IPC based CTRL-C Wrapper

Here you can find a selfwritten python module providing a CTRL-C wrapping including a socket based IPC.The syntax is quite similiar to the subprocess module.

Usage:

>>> 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()

Code

import socketimport subprocessimport timeimport randomimport signal, os, sysclass 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)


Try calling the GenerateConsoleCtrlEvent function using ctypes. As you are creating a new process group, the process group ID should be the same as the pid. So, something like

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

should work.

Update: You're right, I missed that part of the detail. Here's a post which suggests a possible solution, though it's a bit kludgy. More details are in this answer.


My solution also involves a wrapper script, but it does not need IPC, so it is far simpler to use.

The wrapper script first detaches itself from any existing console, then attach to the target console, then files the Ctrl-C event.

import ctypesimport syskernel = ctypes.windll.kernel32pid = int(sys.argv[1])kernel.FreeConsole()kernel.AttachConsole(pid)kernel.SetConsoleCtrlHandler(None, 1)kernel.GenerateConsoleCtrlEvent(0, 0)sys.exit(0)

The initial process must be launched in a separate console so that the Ctrl-C event will not leak. Example

p = subprocess.Popen(['some_command'], creationflags=subprocess.CREATE_NEW_CONSOLE)# Do something elsesubprocess.check_call([sys.executable, 'ctrl_c.py', str(p.pid)]) # Send Ctrl-C

where I named the wrapper script as ctrl_c.py.