]> granicus.if.org Git - python/commitdiff
Implement #1220212. Add os.kill support for Windows.
authorBrian Curtin <brian.curtin@gmail.com>
Fri, 2 Apr 2010 23:26:06 +0000 (23:26 +0000)
committerBrian Curtin <brian.curtin@gmail.com>
Fri, 2 Apr 2010 23:26:06 +0000 (23:26 +0000)
os.kill takes one of two newly added signals, CTRL_C_EVENT and
CTRL_BREAK_EVENT, or any integer value. The events are a special case
which work with subprocess console applications which implement a
special console control handler. Any other value but those two will
cause os.kill to use TerminateProcess, outright killing the process.

This change adds win_console_handler.py, which is a script to implement
SetConsoleCtrlHandler and applicable handler function, using ctypes.

subprocess also gets another attribute which is a necessary flag to
creationflags in Popen in order to send the CTRL events.

Doc/library/os.rst
Doc/library/signal.rst
Doc/library/subprocess.rst
Lib/subprocess.py
Lib/test/test_os.py
Lib/test/win_console_handler.py [new file with mode: 0644]
Lib/unittest/test/test_break.py
Modules/posixmodule.c
Modules/signalmodule.c
PC/_subprocess.c

index b4e39a425d78ff01fd149105b7757513a451d840..11d9607a7d589db86fb4a41a8413c3d1b7505450 100644 (file)
@@ -1719,7 +1719,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)
index 84f08b3336d6ac68ec5f31d9fd9f6706d08e3766..300c71762bffff71dd32e6275db727c50b6cf5da 100644 (file)
@@ -75,6 +75,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.
index b557bcd4c93677d601ffe0ca25653c6022d0208a..439a46da769206c9e0c5464913d7a3529c49c26a 100644 (file)
@@ -320,8 +320,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`.
 
    .. versionadded:: 2.6
 
index 59ed60c16a2f47391e5db3907d4c4ddbfc84771f..e4c843d54bacbaa67392e6c0dd64b8f3ee3a708b 100644 (file)
@@ -990,6 +990,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")
 
index 4e21dd8de351044829e6e6700215f198f685ac74..2fc0d07ccbd2a05f511c4feacdf186c0b4a51f3d 100644 (file)
@@ -7,6 +7,9 @@ import errno
 import unittest
 import warnings
 import sys
+import signal
+import subprocess
+import time
 from test import test_support
 
 warnings.filterwarnings("ignore", "tempnam", RuntimeWarning, __name__)
@@ -649,7 +652,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)'])
@@ -664,7 +666,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)'])
@@ -672,6 +673,63 @@ else:
     class PosixUidGidTests(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.1)
+        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.1)
+        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():
     test_support.run_unittest(
         FileTests,
@@ -684,7 +742,8 @@ def test_main():
         URandomTests,
         Win32ErrorTests,
         TestInvalidFD,
-        PosixUidGidTests
+        PosixUidGidTests,
+        Win32KillTests
     )
 
 if __name__ == "__main__":
diff --git a/Lib/test/win_console_handler.py b/Lib/test/win_console_handler.py
new file mode 100644 (file)
index 0000000..e4190d4
--- /dev/null
@@ -0,0 +1,42 @@
+"""Script used to test os.kill on Windows, for issue #1220212\r
+\r
+This script is started as a subprocess in test_os and is used to test the\r
+CTRL_C_EVENT and CTRL_BREAK_EVENT signals, which requires a custom handler\r
+to be written into the kill target.\r
+\r
+See http://msdn.microsoft.com/en-us/library/ms685049%28v=VS.85%29.aspx for a\r
+similar example in C.\r
+"""\r
+\r
+from ctypes import wintypes\r
+import signal\r
+import ctypes\r
+\r
+# Function prototype for the handler function. Returns BOOL, takes a DWORD.\r
+HandlerRoutine = wintypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD)\r
+\r
+def _ctrl_handler(sig):\r
+    """Handle a sig event and return 0 to terminate the process"""\r
+    if sig == signal.CTRL_C_EVENT:\r
+        pass\r
+    elif sig == signal.CTRL_BREAK_EVENT:\r
+        pass\r
+    else:\r
+        print("UNKNOWN EVENT")\r
+    return 0\r
+\r
+ctrl_handler = HandlerRoutine(_ctrl_handler)\r
+\r
+\r
+SetConsoleCtrlHandler = ctypes.windll.kernel32.SetConsoleCtrlHandler\r
+SetConsoleCtrlHandler.argtypes = (HandlerRoutine, wintypes.BOOL)\r
+SetConsoleCtrlHandler.restype = wintypes.BOOL\r
+\r
+if __name__ == "__main__":\r
+    # Add our console control handling function with value 1\r
+    if not SetConsoleCtrlHandler(ctrl_handler, 1):\r
+        print("Unable to add SetConsoleCtrlHandler")\r
+        exit(-1)\r
+\r
+    # Do nothing but wait for the signal\r
+    input()\r
index 1cd75b8d565889311d947f040c11c549cba3d044..c1c5e6dc1664ccafcac563053978874f27090bb5 100644 (file)
@@ -1,5 +1,6 @@
 import gc
 import os
+import sys
 import signal
 import weakref
 
@@ -10,6 +11,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):
index 8fb7aaab2c2265f7a586da4884a42b6ede495a9f..53eb2376b759791a09142387a2da39be2309e7ac 100644 (file)
@@ -4075,6 +4075,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
@@ -8660,6 +8707,7 @@ static PyMethodDef posix_methods[] = {
        {"popen3",      win32_popen3, METH_VARARGS},
        {"popen4",      win32_popen4, METH_VARARGS},
        {"startfile",   win32_startfile, METH_VARARGS, win32_startfile__doc__},
+       {"kill",    win32_kill, METH_VARARGS, win32_kill__doc__},
 #else
 #if defined(PYOS_OS2) && defined(PYCC_GCC)
        {"popen2",      os2emx_popen2, METH_VARARGS},
index cecb2bef975a8732060b156f3e0fd2bf282ad5d1..bb7e4b0dcb070a77668dd0836bc7645da1832c92 100644 (file)
@@ -7,6 +7,7 @@
 #include "intrcheck.h"
 
 #ifdef MS_WINDOWS
+#include <Windows.h>
 #ifdef HAVE_PROCESS_H
 #include <process.h>
 #endif
@@ -793,6 +794,18 @@ initsignal(void)
        PyDict_SetItemString(d, "ItimerError", ItimerError);
 #endif
 
+#ifdef CTRL_C_EVENT
+    x = PyInt_FromLong(CTRL_C_EVENT);
+    PyDict_SetItemString(d, "CTRL_C_EVENT", x);
+    Py_DECREF(x);
+#endif
+
+#ifdef CTRL_BREAK_EVENT
+    x = PyInt_FromLong(CTRL_BREAK_EVENT);
+    PyDict_SetItemString(d, "CTRL_BREAK_EVENT", x);
+    Py_DECREF(x);
+#endif
+
         if (!PyErr_Occurred())
                 return;
 
index bb240e107e84d89e0ee8cd1c1e3ae6f147ba3755..b4680a77d5e1e922662616f1770d0c8f1839f65c 100644 (file)
@@ -586,4 +586,5 @@ init_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);
 }