mirror of
https://github.com/python/cpython.git
synced 2025-09-26 10:19:53 +00:00
Implement #1220212. Add os.kill support for Windows.
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.
This commit is contained in:
parent
a04c7a0f16
commit
e5aa886b44
10 changed files with 197 additions and 6 deletions
|
@ -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
|
Send signal *sig* to the process *pid*. Constants for the specific signals
|
||||||
available on the host platform are defined in the :mod:`signal` module.
|
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)
|
.. function:: killpg(pgid, sig)
|
||||||
|
|
|
@ -75,6 +75,20 @@ The variables defined in the :mod:`signal` module are:
|
||||||
the system are defined by this module.
|
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
|
.. data:: NSIG
|
||||||
|
|
||||||
One more than the number of the highest signal number.
|
One more than the number of the highest signal number.
|
||||||
|
|
|
@ -320,8 +320,9 @@ Instances of the :class:`Popen` class have the following methods:
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
On Windows only SIGTERM is supported so far. It's an alias for
|
On Windows, SIGTERM is an alias for :meth:`terminate`. CTRL_C_EVENT and
|
||||||
:meth:`terminate`.
|
CTRL_BREAK_EVENT can be sent to processes started with a `creationflags`
|
||||||
|
parameter which includes `CREATE_NEW_PROCESS_GROUP`.
|
||||||
|
|
||||||
.. versionadded:: 2.6
|
.. versionadded:: 2.6
|
||||||
|
|
||||||
|
|
|
@ -990,6 +990,10 @@ class Popen(object):
|
||||||
"""
|
"""
|
||||||
if sig == signal.SIGTERM:
|
if sig == signal.SIGTERM:
|
||||||
self.terminate()
|
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:
|
else:
|
||||||
raise ValueError("Only SIGTERM is supported on Windows")
|
raise ValueError("Only SIGTERM is supported on Windows")
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,9 @@ import errno
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
import sys
|
import sys
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
from test import test_support
|
from test import test_support
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", "tempnam", RuntimeWarning, __name__)
|
warnings.filterwarnings("ignore", "tempnam", RuntimeWarning, __name__)
|
||||||
|
@ -649,7 +652,6 @@ if sys.platform != 'win32':
|
||||||
def test_setreuid_neg1(self):
|
def test_setreuid_neg1(self):
|
||||||
# Needs to accept -1. We run this in a subprocess to avoid
|
# Needs to accept -1. We run this in a subprocess to avoid
|
||||||
# altering the test runner's process state (issue8045).
|
# altering the test runner's process state (issue8045).
|
||||||
import subprocess
|
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
sys.executable, '-c',
|
sys.executable, '-c',
|
||||||
'import os,sys;os.setreuid(-1,-1);sys.exit(0)'])
|
'import os,sys;os.setreuid(-1,-1);sys.exit(0)'])
|
||||||
|
@ -664,7 +666,6 @@ if sys.platform != 'win32':
|
||||||
def test_setregid_neg1(self):
|
def test_setregid_neg1(self):
|
||||||
# Needs to accept -1. We run this in a subprocess to avoid
|
# Needs to accept -1. We run this in a subprocess to avoid
|
||||||
# altering the test runner's process state (issue8045).
|
# altering the test runner's process state (issue8045).
|
||||||
import subprocess
|
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
sys.executable, '-c',
|
sys.executable, '-c',
|
||||||
'import os,sys;os.setregid(-1,-1);sys.exit(0)'])
|
'import os,sys;os.setregid(-1,-1);sys.exit(0)'])
|
||||||
|
@ -672,6 +673,63 @@ else:
|
||||||
class PosixUidGidTests(unittest.TestCase):
|
class PosixUidGidTests(unittest.TestCase):
|
||||||
pass
|
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():
|
def test_main():
|
||||||
test_support.run_unittest(
|
test_support.run_unittest(
|
||||||
FileTests,
|
FileTests,
|
||||||
|
@ -684,7 +742,8 @@ def test_main():
|
||||||
URandomTests,
|
URandomTests,
|
||||||
Win32ErrorTests,
|
Win32ErrorTests,
|
||||||
TestInvalidFD,
|
TestInvalidFD,
|
||||||
PosixUidGidTests
|
PosixUidGidTests,
|
||||||
|
Win32KillTests
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
42
Lib/test/win_console_handler.py
Normal file
42
Lib/test/win_console_handler.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
"""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
|
||||||
|
input()
|
|
@ -1,5 +1,6 @@
|
||||||
import gc
|
import gc
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import signal
|
import signal
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ import unittest
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(hasattr(os, 'kill'), "Test requires os.kill")
|
@unittest.skipUnless(hasattr(os, 'kill'), "Test requires os.kill")
|
||||||
|
@unittest.skipIf(sys.platform =="win32", "Test cannot run on Windows")
|
||||||
class TestBreak(unittest.TestCase):
|
class TestBreak(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -4075,6 +4075,53 @@ posix_killpg(PyObject *self, PyObject *args)
|
||||||
}
|
}
|
||||||
#endif
|
#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_PLOCK
|
||||||
|
|
||||||
#ifdef HAVE_SYS_LOCK_H
|
#ifdef HAVE_SYS_LOCK_H
|
||||||
|
@ -8660,6 +8707,7 @@ static PyMethodDef posix_methods[] = {
|
||||||
{"popen3", win32_popen3, METH_VARARGS},
|
{"popen3", win32_popen3, METH_VARARGS},
|
||||||
{"popen4", win32_popen4, METH_VARARGS},
|
{"popen4", win32_popen4, METH_VARARGS},
|
||||||
{"startfile", win32_startfile, METH_VARARGS, win32_startfile__doc__},
|
{"startfile", win32_startfile, METH_VARARGS, win32_startfile__doc__},
|
||||||
|
{"kill", win32_kill, METH_VARARGS, win32_kill__doc__},
|
||||||
#else
|
#else
|
||||||
#if defined(PYOS_OS2) && defined(PYCC_GCC)
|
#if defined(PYOS_OS2) && defined(PYCC_GCC)
|
||||||
{"popen2", os2emx_popen2, METH_VARARGS},
|
{"popen2", os2emx_popen2, METH_VARARGS},
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "intrcheck.h"
|
#include "intrcheck.h"
|
||||||
|
|
||||||
#ifdef MS_WINDOWS
|
#ifdef MS_WINDOWS
|
||||||
|
#include <Windows.h>
|
||||||
#ifdef HAVE_PROCESS_H
|
#ifdef HAVE_PROCESS_H
|
||||||
#include <process.h>
|
#include <process.h>
|
||||||
#endif
|
#endif
|
||||||
|
@ -793,6 +794,18 @@ initsignal(void)
|
||||||
PyDict_SetItemString(d, "ItimerError", ItimerError);
|
PyDict_SetItemString(d, "ItimerError", ItimerError);
|
||||||
#endif
|
#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())
|
if (!PyErr_Occurred())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -586,4 +586,5 @@ init_subprocess()
|
||||||
defint(d, "INFINITE", INFINITE);
|
defint(d, "INFINITE", INFINITE);
|
||||||
defint(d, "WAIT_OBJECT_0", WAIT_OBJECT_0);
|
defint(d, "WAIT_OBJECT_0", WAIT_OBJECT_0);
|
||||||
defint(d, "CREATE_NEW_CONSOLE", CREATE_NEW_CONSOLE);
|
defint(d, "CREATE_NEW_CONSOLE", CREATE_NEW_CONSOLE);
|
||||||
|
defint(d, "CREATE_NEW_PROCESS_GROUP", CREATE_NEW_PROCESS_GROUP);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue