From eb24d7498f3e34586fee24209f5630a58bb1a04b Mon Sep 17 00:00:00 2001 From: Brian Curtin Date: Mon, 12 Apr 2010 17:16:38 +0000 Subject: Port #1220212 (os.kill for Win32) to py3k. --- Doc/library/os.rst | 9 +++++- Doc/library/signal.rst | 14 +++++++++ Doc/library/subprocess.rst | 5 ++-- Lib/subprocess.py | 4 +++ Lib/test/test_os.py | 66 +++++++++++++++++++++++++++++++++++++++-- Lib/test/win_console_handler.py | 43 +++++++++++++++++++++++++++ Lib/unittest/test/test_break.py | 2 ++ Modules/posixmodule.c | 48 ++++++++++++++++++++++++++++++ Modules/signalmodule.c | 13 ++++++++ PC/_subprocess.c | 2 ++ 10 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 Lib/test/win_console_handler.py diff --git a/Doc/library/os.rst b/Doc/library/os.rst index dacf87a..e328399 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -1491,7 +1491,14 @@ written in Python, such as a mail server's external command delivery program. Send signal *sig* to the process *pid*. Constants for the specific signals available on the host platform are defined in the :mod:`signal` module. - Availability: Unix. + + Windows: The :data:`signal.CTRL_C_EVENT` and + :data:`signal.CTRL_BREAK_EVENT` signals are special signals which can + only be sent to console processes which share a common console window, + e.g., some subprocesses. Any other value for *sig* will cause the process + to be unconditionally killed by the TerminateProcess API, and the exit code + will be set to *sig*. The Windows version of :func:`kill` additionally takes + process handles to be killed. .. function:: killpg(pgid, sig) diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst index c6a3fe4..45b78a2 100644 --- a/Doc/library/signal.rst +++ b/Doc/library/signal.rst @@ -74,6 +74,20 @@ The variables defined in the :mod:`signal` module are: the system are defined by this module. +.. data:: CTRL_C_EVENT + + The signal corresponding to the CTRL+C keystroke event. + + Availability: Windows. + + +.. data:: CTRL_BREAK_EVENT + + The signal corresponding to the CTRL+BREAK keystroke event. + + Availability: Windows. + + .. data:: NSIG One more than the number of the highest signal number. diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst index 13698e7..4b19761 100644 --- a/Doc/library/subprocess.rst +++ b/Doc/library/subprocess.rst @@ -373,8 +373,9 @@ Instances of the :class:`Popen` class have the following methods: .. note:: - On Windows only SIGTERM is supported so far. It's an alias for - :meth:`terminate`. + On Windows, SIGTERM is an alias for :meth:`terminate`. CTRL_C_EVENT and + CTRL_BREAK_EVENT can be sent to processes started with a `creationflags` + parameter which includes `CREATE_NEW_PROCESS_GROUP`. .. method:: Popen.terminate() diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 89cf9bf..ec391cb 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -980,6 +980,10 @@ class Popen(object): """ if sig == signal.SIGTERM: self.terminate() + elif sig == signal.CTRL_C_EVENT: + os.kill(self.pid, signal.CTRL_C_EVENT) + elif sig == signal.CTRL_BREAK_EVENT: + os.kill(self.pid, signal.CTRL_BREAK_EVENT) else: raise ValueError("Only SIGTERM is supported on Windows") diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 395402b..705bdc7 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -7,9 +7,13 @@ import errno import unittest import warnings import sys +import signal +import subprocess +import time import shutil from test import support + # Tests creating TESTFN class FileTests(unittest.TestCase): def setUp(self): @@ -739,7 +743,6 @@ if sys.platform != 'win32': def test_setreuid_neg1(self): # Needs to accept -1. We run this in a subprocess to avoid # altering the test runner's process state (issue8045). - import subprocess subprocess.check_call([ sys.executable, '-c', 'import os,sys;os.setreuid(-1,-1);sys.exit(0)']) @@ -754,7 +757,6 @@ if sys.platform != 'win32': def test_setregid_neg1(self): # Needs to accept -1. We run this in a subprocess to avoid # altering the test runner's process state (issue8045). - import subprocess subprocess.check_call([ sys.executable, '-c', 'import os,sys;os.setregid(-1,-1);sys.exit(0)']) @@ -798,6 +800,63 @@ else: class Pep383Tests(unittest.TestCase): pass +@unittest.skipUnless(sys.platform == "win32", "Win32 specific tests") +class Win32KillTests(unittest.TestCase): + def _kill(self, sig, *args): + # Send a subprocess a signal (or in some cases, just an int to be + # the return value) + proc = subprocess.Popen(*args) + os.kill(proc.pid, sig) + self.assertEqual(proc.wait(), sig) + + def test_kill_sigterm(self): + # SIGTERM doesn't mean anything special, but make sure it works + self._kill(signal.SIGTERM, [sys.executable]) + + def test_kill_int(self): + # os.kill on Windows can take an int which gets set as the exit code + self._kill(100, [sys.executable]) + + def _kill_with_event(self, event, name): + # Run a script which has console control handling enabled. + proc = subprocess.Popen([sys.executable, + os.path.join(os.path.dirname(__file__), + "win_console_handler.py")], + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) + # Let the interpreter startup before we send signals. See #3137. + time.sleep(0.5) + os.kill(proc.pid, event) + # proc.send_signal(event) could also be done here. + # Allow time for the signal to be passed and the process to exit. + time.sleep(0.5) + if not proc.poll(): + # Forcefully kill the process if we weren't able to signal it. + os.kill(proc.pid, signal.SIGINT) + self.fail("subprocess did not stop on {}".format(name)) + + @unittest.skip("subprocesses aren't inheriting CTRL+C property") + def test_CTRL_C_EVENT(self): + from ctypes import wintypes + import ctypes + + # Make a NULL value by creating a pointer with no argument. + NULL = ctypes.POINTER(ctypes.c_int)() + SetConsoleCtrlHandler = ctypes.windll.kernel32.SetConsoleCtrlHandler + SetConsoleCtrlHandler.argtypes = (ctypes.POINTER(ctypes.c_int), + wintypes.BOOL) + SetConsoleCtrlHandler.restype = wintypes.BOOL + + # Calling this with NULL and FALSE causes the calling process to + # handle CTRL+C, rather than ignore it. This property is inherited + # by subprocesses. + SetConsoleCtrlHandler(NULL, 0) + + self._kill_with_event(signal.CTRL_C_EVENT, "CTRL_C_EVENT") + + def test_CTRL_BREAK_EVENT(self): + self._kill_with_event(signal.CTRL_BREAK_EVENT, "CTRL_BREAK_EVENT") + + def test_main(): support.run_unittest( ArgTests, @@ -812,7 +871,8 @@ def test_main(): Win32ErrorTests, TestInvalidFD, PosixUidGidTests, - Pep383Tests + Pep383Tests, + Win32KillTests ) if __name__ == "__main__": diff --git a/Lib/test/win_console_handler.py b/Lib/test/win_console_handler.py new file mode 100644 index 0000000..17bbe1a --- /dev/null +++ b/Lib/test/win_console_handler.py @@ -0,0 +1,43 @@ +"""Script used to test os.kill on Windows, for issue #1220212 + +This script is started as a subprocess in test_os and is used to test the +CTRL_C_EVENT and CTRL_BREAK_EVENT signals, which requires a custom handler +to be written into the kill target. + +See http://msdn.microsoft.com/en-us/library/ms685049%28v=VS.85%29.aspx for a +similar example in C. +""" + +from ctypes import wintypes +import signal +import ctypes + +# Function prototype for the handler function. Returns BOOL, takes a DWORD. +HandlerRoutine = wintypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD) + +def _ctrl_handler(sig): + """Handle a sig event and return 0 to terminate the process""" + if sig == signal.CTRL_C_EVENT: + pass + elif sig == signal.CTRL_BREAK_EVENT: + pass + else: + print("UNKNOWN EVENT") + return 0 + +ctrl_handler = HandlerRoutine(_ctrl_handler) + + +SetConsoleCtrlHandler = ctypes.windll.kernel32.SetConsoleCtrlHandler +SetConsoleCtrlHandler.argtypes = (HandlerRoutine, wintypes.BOOL) +SetConsoleCtrlHandler.restype = wintypes.BOOL + +if __name__ == "__main__": + # Add our console control handling function with value 1 + if not SetConsoleCtrlHandler(ctrl_handler, 1): + print("Unable to add SetConsoleCtrlHandler") + exit(-1) + + # Do nothing but wait for the signal + while True: + pass diff --git a/Lib/unittest/test/test_break.py b/Lib/unittest/test/test_break.py index e3fae34..9108a2c 100644 --- a/Lib/unittest/test/test_break.py +++ b/Lib/unittest/test/test_break.py @@ -1,6 +1,7 @@ import gc import io import os +import sys import signal import weakref @@ -8,6 +9,7 @@ import unittest @unittest.skipUnless(hasattr(os, 'kill'), "Test requires os.kill") +@unittest.skipIf(sys.platform =="win32", "Test cannot run on Windows") class TestBreak(unittest.TestCase): def setUp(self): diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 5076d3b..e6ef410 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4171,6 +4171,53 @@ posix_killpg(PyObject *self, PyObject *args) } #endif +#ifdef MS_WINDOWS +PyDoc_STRVAR(win32_kill__doc__, +"kill(pid, sig)\n\n\ +Kill a process with a signal."); + +static PyObject * +win32_kill(PyObject *self, PyObject *args) +{ + PyObject *result, handle_obj; + DWORD pid, sig, err; + HANDLE handle; + + if (!PyArg_ParseTuple(args, "kk:kill", &pid, &sig)) + return NULL; + + /* Console processes which share a common console can be sent CTRL+C or + CTRL+BREAK events, provided they handle said events. */ + if (sig == CTRL_C_EVENT || sig == CTRL_BREAK_EVENT) { + if (GenerateConsoleCtrlEvent(sig, pid) == 0) { + err = GetLastError(); + PyErr_SetFromWindowsErr(err); + } + else + Py_RETURN_NONE; + } + + /* If the signal is outside of what GenerateConsoleCtrlEvent can use, + attempt to open and terminate the process. */ + handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); + if (handle == NULL) { + err = GetLastError(); + return PyErr_SetFromWindowsErr(err); + } + + if (TerminateProcess(handle, sig) == 0) { + err = GetLastError(); + result = PyErr_SetFromWindowsErr(err); + } else { + Py_INCREF(Py_None); + result = Py_None; + } + + CloseHandle(handle); + return result; +} +#endif /* MS_WINDOWS */ + #ifdef HAVE_PLOCK #ifdef HAVE_SYS_LOCK_H @@ -7200,6 +7247,7 @@ static PyMethodDef posix_methods[] = { #endif /* HAVE_PLOCK */ #ifdef MS_WINDOWS {"startfile", win32_startfile, METH_VARARGS, win32_startfile__doc__}, + {"kill", win32_kill, METH_VARARGS, win32_kill__doc__}, #endif #ifdef HAVE_SETUID {"setuid", posix_setuid, METH_VARARGS, posix_setuid__doc__}, diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c index 13806c7..0039777 100644 --- a/Modules/signalmodule.c +++ b/Modules/signalmodule.c @@ -7,6 +7,7 @@ #include "intrcheck.h" #ifdef MS_WINDOWS +#include #ifdef HAVE_PROCESS_H #include #endif @@ -805,6 +806,18 @@ PyInit_signal(void) PyDict_SetItemString(d, "ItimerError", ItimerError); #endif +#ifdef CTRL_C_EVENT + x = PyLong_FromLong(CTRL_C_EVENT); + PyDict_SetItemString(d, "CTRL_C_EVENT", x); + Py_DECREF(x); +#endif + +#ifdef CTRL_BREAK_EVENT + x = PyLong_FromLong(CTRL_BREAK_EVENT); + PyDict_SetItemString(d, "CTRL_BREAK_EVENT", x); + Py_DECREF(x); +#endif + if (PyErr_Occurred()) { Py_DECREF(m); m = NULL; diff --git a/PC/_subprocess.c b/PC/_subprocess.c index d0c9843..d91ced3 100644 --- a/PC/_subprocess.c +++ b/PC/_subprocess.c @@ -599,5 +599,7 @@ PyInit__subprocess() defint(d, "INFINITE", INFINITE); defint(d, "WAIT_OBJECT_0", WAIT_OBJECT_0); defint(d, "CREATE_NEW_CONSOLE", CREATE_NEW_CONSOLE); + defint(d, "CREATE_NEW_PROCESS_GROUP", CREATE_NEW_PROCESS_GROUP); + return m; } -- cgit v0.12