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 becauseCTRL_C_EVENT
is only foros.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 becauseCTRL_C_EVENT
is ignored for process groups. [REF2]This is a bug in the python documentation [REF3]
Implemented solution
- Let your program run in a different cmd window with the Windows shell command start.
- 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.
- 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.
- 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.
- 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
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
.