diff options
author | Giampaolo Rodola <g.rodola@gmail.com> | 2019-04-05 02:16:40 +0200 |
---|---|---|
committer | Giampaolo Rodola <g.rodola@gmail.com> | 2019-04-05 02:16:40 +0200 |
commit | 84448be84278b1701ea1d4163cd46c6b959bf1be (patch) | |
tree | ae7f92f6e4e91c1218669e634abfb4ce5c5968de | |
parent | 16128f7298f490d11368dad316fa8e53348ed52d (diff) | |
parent | e471e7cbad9e2d84f9fb114da86df78755836852 (diff) | |
download | psutil-84448be84278b1701ea1d4163cd46c6b959bf1be.tar.gz |
merge from masterosx-ionice
-rw-r--r-- | HISTORY.rst | 8 | ||||
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | docs/index.rst | 63 | ||||
-rw-r--r-- | psutil/__init__.py | 4 | ||||
-rw-r--r-- | psutil/_pslinux.py | 34 | ||||
-rw-r--r-- | psutil/_psutil_common.c | 2 | ||||
-rw-r--r-- | psutil/_psutil_windows.c | 71 | ||||
-rw-r--r-- | psutil/_pswindows.py | 131 | ||||
-rw-r--r-- | psutil/arch/windows/global.c | 33 | ||||
-rw-r--r-- | psutil/arch/windows/global.h | 4 | ||||
-rw-r--r-- | psutil/arch/windows/ntextapi.h | 4 | ||||
-rw-r--r-- | psutil/arch/windows/process_handles.c | 11 | ||||
-rw-r--r-- | psutil/arch/windows/process_info.c | 55 | ||||
-rwxr-xr-x | psutil/tests/runner.py | 29 | ||||
-rwxr-xr-x | psutil/tests/test_contracts.py | 7 | ||||
-rwxr-xr-x | psutil/tests/test_process.py | 151 | ||||
-rwxr-xr-x | psutil/tests/test_windows.py | 16 | ||||
-rwxr-xr-x | scripts/internal/winmake.py | 10 |
18 files changed, 394 insertions, 244 deletions
diff --git a/HISTORY.rst b/HISTORY.rst index 79963855..1c745b8b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,10 @@ the number of physical CPUs in case /proc/cpuinfo does not provide this info. - 1458_: provide coloured test output. Also show failures on KeyboardInterrupt. - 1464_: various docfixes (always point to python3 doc, fix links, etc.). +- 1473_: [Windows] process IO priority (ionice()) values are now exposed as 4 + new constants: IOPRIO_VERYLOW, IOPRIO_LOW, IOPRIO_NORMAL, IOPRIO_HIGH. + Also it was not possible to set high I/O priority (not it is). +- 1478_: add make command to re-run tests failed on last run. **Bug fixes** @@ -21,6 +25,10 @@ exist. (patch by Cedric Lamoriniere) - 1471_: [SunOS] Process name() and cmdline() can return SystemError. (patch by Daniel Beer) +- 1475_: [Windows] OSError.winerror attribute wasn't properly checked resuling + in WindowsError being raised instead of AccessDenied. +- 1477_: [Windows] wrong or absent error handling for private NTSTATUS Windows + APIs. Different process methods were affected by this. 5.6.1 ===== @@ -49,6 +49,7 @@ clean: ## Remove all build files. *.egg-info \ *\$testfn* \ .coverage \ + .failed-tests.txt \ .tox \ build/ \ dist/ \ @@ -151,6 +152,10 @@ test-by-name: ## e.g. make test-by-name ARGS=psutil.tests.test_system.TestSyste ${MAKE} install @$(TEST_PREFIX) $(PYTHON) -m unittest -v $(ARGS) +test-failed: ## Re-run tests which failed on last run + ${MAKE} install + $(TEST_PREFIX) $(PYTHON) -c "import psutil.tests.runner as r; r.run(last_failed=True)" + test-coverage: ## Run test coverage. ${MAKE} install # Note: coverage options are controlled by .coveragerc file diff --git a/docs/index.rst b/docs/index.rst index cd9c6d7c..1fc13624 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1228,7 +1228,6 @@ Process class >>> p.nice(psutil.HIGH_PRIORITY_CLASS) - .. method:: ionice(ioclass=None, value=None) Get or set process I/O niceness (priority). @@ -1239,35 +1238,33 @@ Process class I/O priority even further. Here's the possible platform-dependent *ioclass* values. - Linux: + Linux (see `ioprio_get`_ manual): - * ``IOPRIO_CLASS_RT``: (highest priority) the process gets first access - to the disk every time. Use it with care as it can starve the entire + * ``IOPRIO_CLASS_RT``: (high) the process gets first access to the disk + every time. Use it with care as it can starve the entire system. Additional priority *level* can be specified and ranges from ``0`` (highest) to ``7`` (lowest). - * ``IOPRIO_CLASS_BE``: (best effort) the default for any process that - hasn't set a specific I/O priority. Additional priority *level* ranges - from ``0`` (highest) to ``7`` (lowest). - * ``IOPRIO_CLASS_IDLE``: (lowest priority) get I/O time when no-one else - needs the disk. - * ``IOPRIO_CLASS_NONE``: this should be equal to ``IOPRIO_CLASS_RT``. + * ``IOPRIO_CLASS_BE``: (normal) the default for any process that hasn't set + a specific I/O priority. Additional priority *level* ranges from + ``0`` (highest) to ``7`` (lowest). + * ``IOPRIO_CLASS_IDLE``: (low) get I/O time when no-one else needs the disk. + No additional *value* is accepted. + * ``IOPRIO_CLASS_NONE``: returned when no priority was previously set. - macOS: + macOS (see `getiopolicy_np`_ manual): - * ``IOPOL_IMPORTANT``: highest priority - * ``IOPOL_DEFAULT``: the default + * ``IOPOL_IMPORTANT``: highest priority. + * ``IOPOL_DEFAULT``: the default. * ``IOPOL_STANDARD``, ``IOPOL_UTILITY``, ``IOPOL_THROTTLE``: - various levels of low priority (refer to man page) - * ``IOPOL_PASSIVE``: lowest priority + various levels of low priority (refer to man page). + * ``IOPOL_PASSIVE``: lowest priority. Windows: - * ``REALTIME_PRIORITY_CLASS``: highest priority - * ``HIGH_PRIORITY_CLASS``: high priority - * ``ABOVE_NORMAL_PRIORITY_CLASS``: something between high and normal - * ``NORMAL_PRIORITY_CLASS``: the default - * ``BELOW_NORMAL_PRIORITY_CLASS``: something between normal and low - * ``IDLE_PRIORITY_CLASS``: lowest possible priority + * ``IOPRIO_HIGH``: highest priority. + * ``IOPRIO_NORMAL``: default priority. + * ``IOPRIO_LOW``: low priority. + * ``IOPRIO_VERYLOW``: lowest priority. Here's an example on how to set the highest I/O priority depending on what platform you're on:: @@ -1279,13 +1276,16 @@ Process class elif psutil.MACOS: p.ionice(psutil.IOPOL_IMPORTANT) else: # Windows - p.ionice(psutil.REALTIME_PRIORITY_CLASS) + p.ionice(psutil.IOPRIO_HIGH) p.ionice() # get Availability: Linux, macOS, Windows Vista+ .. versionchanged:: 5.6.2 added macOS support + .. versionchanged:: 5.6.2 Windows accepts mew ``IOPRIO_`` constants + including new IOPRIO_HIGH. + .. method:: rlimit(resource, limits=None) Get or set process resource limits (see `man prlimit`_). *resource* is one @@ -2187,10 +2187,18 @@ Constants Availability: Linux - .. versionchanged:: - 3.0.0 on Python >= 3.4 these constants are - `enums <https://docs.python.org/3/library/enum.html#module-enum>`__ - instead of a plain integer. +.. data:: IOPRIO_VERYLOW +.. data:: IOPRIO_LOW +.. data:: IOPRIO_NORMAL +.. data:: IOPRIO_HIGH + + A set of integers representing the I/O priority of a process on Linux. + They can be used in conjunction with :meth:`psutil.Process.ionice()` to get + or set process I/O priority. + + Availability: Windows + + .. versionadded:: 5.6.2 .. _const-iopol: .. data:: IOPOL_DEFAULT @@ -2207,6 +2215,8 @@ Constants Availability: macOS + .. versionadded:: 5.6.2 + .. _const-rlimit: .. data:: RLIM_INFINITY .. data:: RLIMIT_AS @@ -2905,6 +2915,7 @@ Timeline .. _`netstat.py`: https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py. .. _`nettop.py`: https://github.com/giampaolo/psutil/blob/master/scripts/nettop.py .. _`open`: https://docs.python.org/3/library/functions.html#open +.. _`getiopolicy_np`: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/getiopolicy_np.3.html .. _`os.cpu_count`: https://docs.python.org/3/library/os.html#os.cpu_count .. _`os.getloadavg`: https://docs.python.org/3/library/os.html#os.getloadavg .. _`os.getpid`: https://docs.python.org/3/library/os.html#os.getpid diff --git a/psutil/__init__.py b/psutil/__init__.py index 18fbe71b..5bf1305a 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -146,6 +146,10 @@ elif WINDOWS: from ._psutil_windows import NORMAL_PRIORITY_CLASS # NOQA from ._psutil_windows import REALTIME_PRIORITY_CLASS # NOQA from ._pswindows import CONN_DELETE_TCB # NOQA + from ._pswindows import IOPRIO_VERYLOW # NOQA + from ._pswindows import IOPRIO_LOW # NOQA + from ._pswindows import IOPRIO_NORMAL # NOQA + from ._pswindows import IOPRIO_HIGH # NOQA elif MACOS: from . import _psosx as _psplatform diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index ecba4139..6c58cf2c 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -41,7 +41,6 @@ from ._common import supports_ipv6 from ._common import usage_percent from ._compat import b from ._compat import basestring -from ._compat import long from ._compat import PY3 if sys.version_info >= (3, 4): @@ -1987,35 +1986,12 @@ class Process(object): @wrap_exceptions def ionice_set(self, ioclass, value): - if value is not None: - if not PY3 and not isinstance(value, (int, long)): - msg = "value argument is not an integer (gor %r)" % value - raise TypeError(msg) - if not 0 <= value <= 7: - raise ValueError( - "value argument range expected is between 0 and 7") - - if ioclass in (IOPRIO_CLASS_NONE, None): - if value: - msg = "can't specify value with IOPRIO_CLASS_NONE " \ - "(got %r)" % value - raise ValueError(msg) - ioclass = IOPRIO_CLASS_NONE + if value is None: value = 0 - elif ioclass == IOPRIO_CLASS_IDLE: - if value: - msg = "can't specify value with IOPRIO_CLASS_IDLE " \ - "(got %r)" % value - raise ValueError(msg) - value = 0 - elif ioclass in (IOPRIO_CLASS_RT, IOPRIO_CLASS_BE): - if value is None: - # TODO: add comment explaining why this is 4 (?) - value = 4 - else: - # otherwise we would get OSError(EVINAL) - raise ValueError("invalid ioclass argument %r" % ioclass) - + if value and ioclass == IOPRIO_CLASS_IDLE: + raise ValueError("IOPRIO_CLASS_IDLE accepts no value") + if value < 0 or value > 7: + raise ValueError("value not in 0-7 range") return cext.proc_ioprio_set(self.pid, ioclass, value) if HAS_PRLIMIT: diff --git a/psutil/_psutil_common.c b/psutil/_psutil_common.c index 4b6ab399..c6e37bc2 100644 --- a/psutil/_psutil_common.c +++ b/psutil/_psutil_common.c @@ -62,7 +62,7 @@ PyErr_SetFromOSErrnoWithSyscall(const char *syscall) { char fullmsg[1024]; #ifdef _WIN32 - sprintf(fullmsg, "originated from %s", syscall); + sprintf(fullmsg, "(originated from %s)", syscall); PyErr_SetFromWindowsErrWithFilename(GetLastError(), fullmsg); #else PyObject *exc; diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 4dfae2d5..a1a68857 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -3,7 +3,15 @@ * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. * - * Windows platform-specific module methods for _psutil_windows + * Windows platform-specific module methods for _psutil_windows. + * + * List of undocumented Windows NT APIs which are used in here and in + * other modules: + * - NtQuerySystemInformation + * - NtQueryInformationProcess + * - NtQueryObject + * - NtSuspendProcess + * - NtResumeProcess */ // Fixes clash between winsock2.h and windows.h @@ -797,8 +805,8 @@ psutil_GetProcWsetInformation( } else { PyErr_Clear(); - psutil_debug("NtQueryVirtualMemory failed with %i", status); - PyErr_SetString(PyExc_RuntimeError, "NtQueryVirtualMemory failed"); + psutil_SetFromNTStatusErr( + status, "NtQueryVirtualMemory(MemoryWorkingSetInformation)"); } HeapFree(GetProcessHeap(), 0, buffer); return 1; @@ -946,8 +954,11 @@ psutil_per_cpu_times(PyObject *self, PyObject *args) { sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), NULL); - if (status != 0) { - PyErr_SetFromWindowsErr(0); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, + "NtQuerySystemInformation(SystemProcessorPerformanceInformation)" + ); goto error; } @@ -1025,7 +1036,7 @@ psutil_proc_cwd(PyObject *self, PyObject *args) { static PyObject * psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { long pid; - int ret; + NTSTATUS status; HANDLE hProcess; PyObject* suspend; @@ -1037,15 +1048,15 @@ psutil_proc_suspend_or_resume(PyObject *self, PyObject *args) { return NULL; if (PyObject_IsTrue(suspend)) - ret = psutil_NtSuspendProcess(hProcess); + status = psutil_NtSuspendProcess(hProcess); else - ret = psutil_NtResumeProcess(hProcess); + status = psutil_NtResumeProcess(hProcess); - if (ret != 0) { - PyErr_SetFromWindowsErr(0); + if (! NT_SUCCESS(status)) { CloseHandle(hProcess); - return NULL; + return psutil_SetFromNTStatusErr(status, "NtSuspend|ResumeProcess"); } + CloseHandle(hProcess); Py_RETURN_NONE; } @@ -1339,6 +1350,7 @@ error: // https://msdn.microsoft.com/library/aa365928.aspx +// TODO properly handle return code static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, ULONG address_family, PVOID * data, DWORD * size) @@ -1373,6 +1385,7 @@ static DWORD __GetExtendedTcpTable(_GetExtendedTcpTable call, // https://msdn.microsoft.com/library/aa365930.aspx +// TODO properly check return value static DWORD __GetExtendedUdpTable(_GetExtendedUdpTable call, ULONG address_family, PVOID * data, DWORD * size) @@ -1859,20 +1872,26 @@ psutil_proc_io_priority_get(PyObject *self, PyObject *args) { long pid; HANDLE hProcess; DWORD IoPriority; + NTSTATUS status; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; + hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == NULL) return NULL; - psutil_NtQueryInformationProcess( + + status = psutil_NtQueryInformationProcess( hProcess, ProcessIoPriority, &IoPriority, sizeof(DWORD), NULL ); + CloseHandle(hProcess); + if (! NT_SUCCESS(status)) + return psutil_SetFromNTStatusErr(status, "NtQueryInformationProcess"); return Py_BuildValue("i", IoPriority); } @@ -1885,15 +1904,17 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { long pid; DWORD prio; HANDLE hProcess; + NTSTATUS status; DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION; if (! PyArg_ParseTuple(args, "li", &pid, &prio)) return NULL; + hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) return NULL; - psutil_NtSetInformationProcess( + status = psutil_NtSetInformationProcess( hProcess, ProcessIoPriority, (PVOID)&prio, @@ -1901,6 +1922,8 @@ psutil_proc_io_priority_set(PyObject *self, PyObject *args) { ); CloseHandle(hProcess); + if (! NT_SUCCESS(status)) + return psutil_SetFromNTStatusErr(status, "NtSetInformationProcess"); Py_RETURN_NONE; } #endif @@ -3217,9 +3240,9 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { spi, ncpus * sizeof(_SYSTEM_PERFORMANCE_INFORMATION), NULL); - if (status != 0) { - PyErr_SetFromOSErrnoWithSyscall( - "NtQuerySystemInformation(SYSTEM_PERFORMANCE_INFORMATION)"); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemPerformanceInformation)"); goto error; } @@ -3236,9 +3259,9 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { InterruptInformation, ncpus * sizeof(SYSTEM_INTERRUPT_INFORMATION), NULL); - if (status != 0) { - PyErr_SetFromOSErrnoWithSyscall( - "NtQuerySystemInformation(SYSTEM_INTERRUPT_INFORMATION)"); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemInterruptInformation)"); goto error; } for (i = 0; i < ncpus; i++) { @@ -3258,9 +3281,10 @@ psutil_cpu_stats(PyObject *self, PyObject *args) { sppi, ncpus * sizeof(_SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION), NULL); - if (status != 0) { - PyErr_SetFromOSErrnoWithSyscall( - "NtQuerySystemInformation(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)"); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, + "NtQuerySystemInformation(SystemProcessorPerformanceInformation)"); goto error; } @@ -3661,7 +3685,8 @@ void init_psutil_windows(void) module, "ERROR_INVALID_NAME", ERROR_INVALID_NAME); PyModule_AddIntConstant( module, "ERROR_SERVICE_DOES_NOT_EXIST", ERROR_SERVICE_DOES_NOT_EXIST); - + PyModule_AddIntConstant( + module, "ERROR_PRIVILEGE_NOT_HELD", ERROR_PRIVILEGE_NOT_HELD); PyModule_AddIntConstant( module, "WINVER", PSUTIL_WINVER); PyModule_AddIntConstant( diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 6687770c..929e27d7 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -63,11 +63,14 @@ else: # http://msdn.microsoft.com/en-us/library/ms686219(v=vs.85).aspx __extra__all__ = [ "win_service_iter", "win_service_get", + # Process priority "ABOVE_NORMAL_PRIORITY_CLASS", "BELOW_NORMAL_PRIORITY_CLASS", - "HIGH_PRIORITY_CLASS", "IDLE_PRIORITY_CLASS", - "NORMAL_PRIORITY_CLASS", "REALTIME_PRIORITY_CLASS", - "CONN_DELETE_TCB", - "AF_LINK", + "HIGH_PRIORITY_CLASS", "IDLE_PRIORITY_CLASS", "NORMAL_PRIORITY_CLASS", + "REALTIME_PRIORITY_CLASS", + # IO priority + "IOPRIO_VERYLOW", "IOPRIO_LOW", "IOPRIO_NORMAL", "IOPRIO_HIGH", + # others + "CONN_DELETE_TCB", "AF_LINK", ] @@ -76,10 +79,6 @@ __extra__all__ = [ # ===================================================================== CONN_DELETE_TCB = "DELETE_TCB" -ACCESS_DENIED_ERRSET = frozenset([errno.EPERM, errno.EACCES, - cext.ERROR_ACCESS_DENIED]) -NO_SUCH_SERVICE_ERRSET = frozenset([cext.ERROR_INVALID_NAME, - cext.ERROR_SERVICE_DOES_NOT_EXIST]) HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_io_priority_get") @@ -116,6 +115,19 @@ if enum is not None: globals().update(Priority.__members__) +if enum is None: + IOPRIO_VERYLOW = 0 + IOPRIO_LOW = 1 + IOPRIO_NORMAL = 2 + IOPRIO_HIGH = 3 +else: + class IOPriority(enum.IntEnum): + IOPRIO_VERYLOW = 0 + IOPRIO_LOW = 1 + IOPRIO_NORMAL = 2 + IOPRIO_HIGH = 3 + globals().update(IOPriority.__members__) + pinfo_map = dict( num_handles=0, ctx_switches=1, @@ -533,14 +545,14 @@ class WindowsService(object): """ try: yield - except WindowsError as err: - if err.errno in ACCESS_DENIED_ERRSET: + except OSError as err: + if is_permission_err(err): raise AccessDenied( pid=None, name=self._name, msg="service %r is not querable (not enough privileges)" % self._name) - elif err.errno in NO_SUCH_SERVICE_ERRSET or \ - err.winerror in NO_SUCH_SERVICE_ERRSET: + elif err.winerror in (cext.ERROR_INVALID_NAME, + cext.ERROR_SERVICE_DOES_NOT_EXIST): raise NoSuchProcess( pid=None, name=self._name, msg="service %r does not exist)" % self._name) @@ -657,20 +669,35 @@ pid_exists = cext.pid_exists ppid_map = cext.ppid_map # used internally by Process.children() +def is_permission_err(exc): + """Return True if this is a permission error.""" + assert isinstance(exc, OSError), exc + # On Python 2 OSError doesn't always have 'winerror'. Sometimes + # it does, in which case the original exception was WindowsError + # (which is a subclass of OSError). + return exc.errno in (errno.EPERM, errno.EACCES) or \ + getattr(exc, "winerror", -1) in (cext.ERROR_ACCESS_DENIED, + cext.ERROR_PRIVILEGE_NOT_HELD) + + +def convert_oserror(exc, pid=None, name=None): + """Convert OSError into NoSuchProcess or AccessDenied.""" + assert isinstance(exc, OSError), exc + if is_permission_err(exc): + return AccessDenied(pid=pid, name=name) + if exc.errno == errno.ESRCH: + return NoSuchProcess(pid=pid, name=name) + raise exc + + def wrap_exceptions(fun): - """Decorator which translates bare OSError and WindowsError - exceptions into NoSuchProcess and AccessDenied. - """ + """Decorator which converts OSError into NoSuchProcess or AccessDenied.""" @functools.wraps(fun) def wrapper(self, *args, **kwargs): try: return fun(self, *args, **kwargs) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - raise AccessDenied(self.pid, self._name) - if err.errno == errno.ESRCH: - raise NoSuchProcess(self.pid, self._name) - raise + raise convert_oserror(err, pid=self.pid, name=self._name) return wrapper @@ -744,7 +771,7 @@ class Process(object): try: ret = cext.proc_cmdline(self.pid, use_peb=True) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: + if is_permission_err(err): ret = cext.proc_cmdline(self.pid, use_peb=False) else: raise @@ -772,7 +799,7 @@ class Process(object): try: return cext.proc_memory_info(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: + if is_permission_err(err): # TODO: the C ext can probably be refactored in order # to get this from cext.proc_info() info = self.oneshot_info() @@ -813,11 +840,7 @@ class Process(object): except OSError as err: # XXX - can't use wrap_exceptions decorator as we're # returning a generator; probably needs refactoring. - if err.errno in ACCESS_DENIED_ERRSET: - raise AccessDenied(self.pid, self._name) - if err.errno == errno.ESRCH: - raise NoSuchProcess(self.pid, self._name) - raise + raise convert_oserror(err, self.pid, self._name) else: for addr, perm, path, rss in raw: path = convert_dos_path(path) @@ -893,7 +916,7 @@ class Process(object): try: return cext.proc_create_time(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: + if is_permission_err(err): return self.oneshot_info()[pinfo_map['create_time']] raise @@ -915,12 +938,11 @@ class Process(object): try: user, system = cext.proc_cpu_times(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - info = self.oneshot_info() - user = info[pinfo_map['user_time']] - system = info[pinfo_map['kernel_time']] - else: + if not is_permission_err(err): raise + info = self.oneshot_info() + user = info[pinfo_map['user_time']] + system = info[pinfo_map['kernel_time']] # Children user/system times are not retrievable (set to 0). return _common.pcputimes(user, system, 0.0, 0.0) @@ -979,35 +1001,36 @@ class Process(object): if HAS_PROC_IO_PRIORITY: @wrap_exceptions def ionice_get(self): - return cext.proc_io_priority_get(self.pid) + ret = cext.proc_io_priority_get(self.pid) + if enum is not None: + ret = IOPriority(ret) + return ret @wrap_exceptions - def ionice_set(self, value, _): - if _: - raise TypeError("set_proc_ionice() on Windows takes only " - "1 argument (2 given)") - if value not in (2, 1, 0): - raise ValueError("value must be 2 (normal), 1 (low) or 0 " - "(very low); got %r" % value) - return cext.proc_io_priority_set(self.pid, value) + def ionice_set(self, ioclass, value): + if value: + raise TypeError("value argument not accepted on Windows") + if ioclass not in (IOPRIO_VERYLOW, IOPRIO_LOW, IOPRIO_NORMAL, + IOPRIO_HIGH): + raise ValueError("%s is not a valid priority" % ioclass) + cext.proc_io_priority_set(self.pid, ioclass) @wrap_exceptions def io_counters(self): try: ret = cext.proc_io_counters(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - info = self.oneshot_info() - ret = ( - info[pinfo_map['io_rcount']], - info[pinfo_map['io_wcount']], - info[pinfo_map['io_rbytes']], - info[pinfo_map['io_wbytes']], - info[pinfo_map['io_count_others']], - info[pinfo_map['io_bytes_others']], - ) - else: + if not is_permission_err(err): raise + info = self.oneshot_info() + ret = ( + info[pinfo_map['io_rcount']], + info[pinfo_map['io_wcount']], + info[pinfo_map['io_rbytes']], + info[pinfo_map['io_wbytes']], + info[pinfo_map['io_count_others']], + info[pinfo_map['io_bytes_others']], + ) return pio(*ret) @wrap_exceptions @@ -1055,7 +1078,7 @@ class Process(object): try: return cext.proc_num_handles(self.pid) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: + if is_permission_err(err): return self.oneshot_info()[pinfo_map['num_handles']] raise diff --git a/psutil/arch/windows/global.c b/psutil/arch/windows/global.c index 9ef92092..4d8526e3 100644 --- a/psutil/arch/windows/global.c +++ b/psutil/arch/windows/global.c @@ -18,6 +18,14 @@ int PSUTIL_WINVER; SYSTEM_INFO PSUTIL_SYSTEM_INFO; +#define NT_FACILITY_MASK 0xfff +#define NT_FACILITY_SHIFT 16 +#define NT_FACILITY(Status) \ + ((((ULONG)(Status)) >> NT_FACILITY_SHIFT) & NT_FACILITY_MASK) +#define NT_NTWIN32(status) (NT_FACILITY(Status) == FACILITY_WIN32) +#define WIN32_FROM_NTSTATUS(Status) (((ULONG)(Status)) & 0xffff) + + // A wrapper around GetModuleHandle and GetProcAddress. PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname) { @@ -60,6 +68,26 @@ psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname) { } +/* + * Convert a NTSTATUS value to a Win32 error code and set the proper + * Python exception. + */ +PVOID +psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall) { + ULONG err; + char fullmsg[1024]; + + if (NT_NTWIN32(Status)) + err = WIN32_FROM_NTSTATUS(Status); + else + err = psutil_RtlNtStatusToDosErrorNoTeb(Status); + // if (GetLastError() != 0) + // err = GetLastError(); + sprintf(fullmsg, "(originated from %s)", syscall); + return PyErr_SetFromWindowsErrWithFilename(err, fullmsg); +} + + static int psutil_loadlibs() { /* @@ -127,6 +155,11 @@ psutil_loadlibs() { if (! psutil_NtQueryVirtualMemory) return 1; + psutil_RtlNtStatusToDosErrorNoTeb = psutil_GetProcAddressFromLib( + "ntdll", "RtlNtStatusToDosErrorNoTeb"); + if (! psutil_RtlNtStatusToDosErrorNoTeb) + return 1; + /* * Optional. */ diff --git a/psutil/arch/windows/global.h b/psutil/arch/windows/global.h index fb24bac9..10ae6405 100644 --- a/psutil/arch/windows/global.h +++ b/psutil/arch/windows/global.h @@ -23,6 +23,7 @@ extern SYSTEM_INFO PSUTIL_SYSTEM_INFO; int psutil_load_globals(); PVOID psutil_GetProcAddress(LPCSTR libname, LPCSTR procname); PVOID psutil_GetProcAddressFromLib(LPCSTR libname, LPCSTR procname); +PVOID psutil_SetFromNTStatusErr(NTSTATUS Status, const char *syscall); _NtQuerySystemInformation \ psutil_NtQuerySystemInformation; @@ -71,3 +72,6 @@ _NtResumeProcess \ _NtQueryVirtualMemory \ psutil_NtQueryVirtualMemory; + +_RtlNtStatusToDosErrorNoTeb \ + psutil_RtlNtStatusToDosErrorNoTeb; diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 178f9866..b6f23d99 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -500,4 +500,8 @@ typedef NTSTATUS (NTAPI *_NtQueryVirtualMemory) ( PSIZE_T ReturnLength ); +typedef ULONG (WINAPI *_RtlNtStatusToDosErrorNoTeb) ( + NTSTATUS status +); + #endif // __NTEXTAPI_H__ diff --git a/psutil/arch/windows/process_handles.c b/psutil/arch/windows/process_handles.c index 8b899972..5966669e 100644 --- a/psutil/arch/windows/process_handles.c +++ b/psutil/arch/windows/process_handles.c @@ -52,6 +52,7 @@ psutil_wait_thread(LPVOID lpvParam) { while (TRUE) { WaitForSingleObject(g_hEvtStart, INFINITE); + // TODO: return code not checked g_status = psutil_NtQueryObject( g_hFile, ObjectNameInformation, @@ -159,8 +160,9 @@ psutil_get_open_files_ntqueryobject(long dwPid, HANDLE hProcess) { &dwRet)) == STATUS_INFO_LENGTH_MISMATCH); // NtQuerySystemInformation stopped giving us STATUS_INFO_LENGTH_MISMATCH - if (!NT_SUCCESS(status)) { - PyErr_SetFromWindowsErr(HRESULT_FROM_NT(status)); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemExtendedHandleInformation)"); error = TRUE; goto cleanup; } @@ -355,8 +357,9 @@ psutil_get_open_files_getmappedfilename(long dwPid, HANDLE hProcess) { &dwRet)) == STATUS_INFO_LENGTH_MISMATCH); // NtQuerySystemInformation stopped giving us STATUS_INFO_LENGTH_MISMATCH - if (!NT_SUCCESS(status)) { - PyErr_SetFromWindowsErr(HRESULT_FROM_NT(status)); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemExtendedHandleInformation)"); error = TRUE; goto cleanup; } diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index 946a01cb..3b3c677e 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -483,6 +483,7 @@ psutil_get_process_data(long pid, BOOL theyAreWow64; #endif DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; + NTSTATUS status; hProcess = psutil_handle_from_pid(pid, access); if (hProcess == NULL) @@ -491,15 +492,16 @@ psutil_get_process_data(long pid, #ifdef _WIN64 /* 64 bit case. Check if the target is a 32 bit process running in WoW64 * mode. */ - if (! NT_SUCCESS(psutil_NtQueryInformationProcess( - hProcess, - ProcessWow64Information, - &ppeb32, - sizeof(LPVOID), - NULL))) - { - PyErr_SetFromOSErrnoWithSyscall( - "NtQueryInformationProcess(ProcessWow64Information)"); + status = psutil_NtQueryInformationProcess( + hProcess, + ProcessWow64Information, + &ppeb32, + sizeof(LPVOID), + NULL); + + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessWow64Information)"); goto error; } @@ -633,18 +635,20 @@ psutil_get_process_data(long pid, PEB_ peb; RTL_USER_PROCESS_PARAMETERS_ procParameters; - if (! NT_SUCCESS(psutil_NtQueryInformationProcess( - hProcess, - ProcessBasicInformation, - &pbi, - sizeof(pbi), - NULL))) - { - PyErr_SetFromOSErrnoWithSyscall( - "NtQueryInformationProcess(ProcessBasicInformation)"); + status = psutil_NtQueryInformationProcess( + hProcess, + ProcessBasicInformation, + &pbi, + sizeof(pbi), + NULL); + + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessBasicInformation)"); goto error; } + // read peb if (!ReadProcessMemory(hProcess, pbi.PebBaseAddress, @@ -767,10 +771,12 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { NULL, 0, &bufLen); + if (status != STATUS_BUFFER_OVERFLOW && \ status != STATUS_BUFFER_TOO_SMALL && \ status != STATUS_INFO_LENGTH_MISMATCH) { - PyErr_SetFromOSErrnoWithSyscall("NtQueryInformationProcess(0)"); + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessBasicInformation)"); goto error; } @@ -789,8 +795,9 @@ psutil_cmdline_query_proc(long pid, WCHAR **pdata, SIZE_T *psize) { bufLen, &bufLen ); - if (! NT_SUCCESS(status)) { - PyErr_SetFromOSErrnoWithSyscall("NtQueryInformationProcess(withlen)"); + if (!NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQueryInformationProcess(ProcessCommandLineInformation)"); goto error; } @@ -971,9 +978,9 @@ psutil_get_proc_info(DWORD pid, PSYSTEM_PROCESS_INFORMATION *retProcess, } } - if (status != 0) { - PyErr_Format( - PyExc_RuntimeError, "NtQuerySystemInformation() syscall failed"); + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr( + status, "NtQuerySystemInformation(SystemProcessInformation)"); goto error; } diff --git a/psutil/tests/runner.py b/psutil/tests/runner.py index 9e19d198..1a28aa43 100755 --- a/psutil/tests/runner.py +++ b/psutil/tests/runner.py @@ -24,11 +24,13 @@ except ImportError: import psutil from psutil._common import memoize +from psutil.tests import safe_rmpath from psutil.tests import TOX HERE = os.path.abspath(os.path.dirname(__file__)) VERBOSITY = 1 if TOX else 2 +FAILED_TESTS_FNAME = '.failed-tests.txt' if os.name == 'posix': GREEN = 1 RED = 2 @@ -157,15 +159,38 @@ def get_suite(name=None): return suite -def run(name=None): +def get_suite_from_failed(): + # ...from previously failed test run + suite = unittest.TestSuite() + if not os.path.isfile(FAILED_TESTS_FNAME): + return suite + with open(FAILED_TESTS_FNAME, 'rt') as f: + names = f.read().split() + for n in names: + suite.addTest(unittest.defaultTestLoader.loadTestsFromName(n)) + return suite + + +def save_failed_tests(result): + if result.wasSuccessful(): + return safe_rmpath(FAILED_TESTS_FNAME) + with open(FAILED_TESTS_FNAME, 'wt') as f: + for t in result.errors + result.failures: + tname = str(t[0]) + f.write(tname + '\n') + + +def run(name=None, last_failed=False): setup_tests() runner = ColouredRunner(verbosity=VERBOSITY) + suite = get_suite_from_failed() if last_failed else get_suite(name) try: - result = runner.run(get_suite(name)) + result = runner.run(suite) except (KeyboardInterrupt, SystemExit) as err: print("received %s" % err.__class__.__name__, file=sys.stderr) runner.result.printErrors() sys.exit(1) else: + save_failed_tests(result) success = result.wasSuccessful() sys.exit(0 if success else 1) diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index d39213cb..d84c0ba3 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -625,8 +625,11 @@ class TestFetchAllProcesses(unittest.TestCase): # commented as on Linux we might get # '/foo/bar (deleted)' # assert os.path.exists(nt.path), nt.path - elif fname in ('addr', 'perms'): - assert value + elif fname == 'addr': + assert value, repr(value) + elif fname == 'perms': + if not WINDOWS: + assert value, repr(value) else: self.assertIsInstance(value, (int, long)) self.assertGreaterEqual(value, 0) diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py index f38803a0..873a6975 100755 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -43,7 +43,6 @@ from psutil.tests import create_proc_children_pair from psutil.tests import create_zombie_proc from psutil.tests import enum from psutil.tests import get_test_subprocess -from psutil.tests import get_winver from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_ENVIRON from psutil.tests import HAS_IONICE @@ -67,7 +66,6 @@ from psutil.tests import ThreadTask from psutil.tests import TRAVIS from psutil.tests import unittest from psutil.tests import wait_for_pid -from psutil.tests import WIN_VISTA # =================================================================== @@ -351,71 +349,38 @@ class TestProcess(unittest.TestCase): self.assertGreaterEqual(io2[i], 0) @unittest.skipIf(not HAS_IONICE, "not supported") - @unittest.skipIf(WINDOWS and get_winver() < WIN_VISTA, 'not supported') - def test_ionice(self): + @unittest.skipIf(not LINUX, "Linux only") + def test_ionice_linux(self): p = psutil.Process() - if LINUX: - from psutil import (IOPRIO_CLASS_NONE, IOPRIO_CLASS_RT, - IOPRIO_CLASS_BE, IOPRIO_CLASS_IDLE) - self.assertEqual(IOPRIO_CLASS_NONE, 0) - self.assertEqual(IOPRIO_CLASS_RT, 1) - self.assertEqual(IOPRIO_CLASS_BE, 2) - self.assertEqual(IOPRIO_CLASS_IDLE, 3) - try: - p.ionice(2) - ioclass, value = p.ionice() - if enum is not None: - self.assertIsInstance(ioclass, enum.IntEnum) - self.assertEqual(ioclass, 2) - self.assertEqual(value, 4) - # - p.ionice(3) - ioclass, value = p.ionice() - self.assertEqual(ioclass, 3) - self.assertEqual(value, 0) - # - p.ionice(2, 0) - ioclass, value = p.ionice() - self.assertEqual(ioclass, 2) - self.assertEqual(value, 0) - p.ionice(2, 7) - ioclass, value = p.ionice() - self.assertEqual(ioclass, 2) - self.assertEqual(value, 7) - finally: - p.ionice(IOPRIO_CLASS_NONE) - if MACOS: - names = ("IOPOL_IMPORTANT", "IOPOL_STANDARD", "IOPOL_UTILITY", - "IOPOL_THROTTLE", "IOPOL_PASSIVE") - original = p.ionice() - try: - for name in names: - if name == "IOPOL_IMPORTANT": - # XXX has no effect (?!?) - continue - value = getattr(psutil, name) - p.ionice(value) - self.assertEqual(p.ionice(), value) - finally: - p.ionice(original) - else: - original = p.ionice() - self.assertIsInstance(original, int) - try: - value = 0 # very low - if original == value: - value = 1 # low - p.ionice(value) - self.assertEqual(p.ionice(), value) - finally: - p.ionice(original) - - @unittest.skipIf(not HAS_IONICE, "not supported") - @unittest.skipIf(WINDOWS and get_winver() < WIN_VISTA, 'not supported') - def test_ionice_errs(self): - sproc = get_test_subprocess() - p = psutil.Process(sproc.pid) - if LINUX: + self.assertEqual(p.ionice()[0], psutil.IOPRIO_CLASS_NONE) + self.assertEqual(psutil.IOPRIO_CLASS_NONE, 0) + self.assertEqual(psutil.IOPRIO_CLASS_RT, 1) # high + self.assertEqual(psutil.IOPRIO_CLASS_BE, 2) # normal + self.assertEqual(psutil.IOPRIO_CLASS_IDLE, 3) # low + try: + # low + p.ionice(psutil.IOPRIO_CLASS_IDLE) + self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_IDLE, 0)) + with self.assertRaises(ValueError): # accepts no value + p.ionice(psutil.IOPRIO_CLASS_IDLE, value=7) + # normal + p.ionice(psutil.IOPRIO_CLASS_BE) + self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_BE, 0)) + p.ionice(psutil.IOPRIO_CLASS_BE, value=7) + self.assertEqual(tuple(p.ionice()), (psutil.IOPRIO_CLASS_BE, 7)) + with self.assertRaises(ValueError): + p.ionice(psutil.IOPRIO_CLASS_BE, value=8) + # high + if os.getuid() == 0: # root + p.ionice(psutil.IOPRIO_CLASS_RT) + self.assertEqual(tuple(p.ionice()), + (psutil.IOPRIO_CLASS_RT, 0)) + p.ionice(psutil.IOPRIO_CLASS_RT, value=7) + self.assertEqual(tuple(p.ionice()), + (psutil.IOPRIO_CLASS_RT, 7)) + with self.assertRaises(ValueError): + p.ionice(psutil.IOPRIO_CLASS_IDLE, value=8) + # errs self.assertRaises(ValueError, p.ionice, 2, 10) self.assertRaises(ValueError, p.ionice, 2, -1) self.assertRaises(ValueError, p.ionice, 4) @@ -429,11 +394,55 @@ class TestProcess(unittest.TestCase): self.assertRaisesRegex( ValueError, "'ioclass' argument must be specified", p.ionice, value=1) - else: - if not MACOS: - # ionice() can only be set for the current process - self.assertRaises(TypeError, p.ionice, 2, 1) - self.assertRaises(ValueError, p.ionice, 3) + finally: + p.ionice(psutil.IOPRIO_CLASS_BE) + + @unittest.skipIf(not HAS_IONICE, "not supported") + @unittest.skipIf(not WINDOWS, 'Windows only') + def test_ionice_win(self): + p = psutil.Process() + self.assertEqual(p.ionice(), psutil.IOPRIO_NORMAL) + try: + # base + p.ionice(psutil.IOPRIO_VERYLOW) + self.assertEqual(p.ionice(), psutil.IOPRIO_VERYLOW) + p.ionice(psutil.IOPRIO_LOW) + self.assertEqual(p.ionice(), psutil.IOPRIO_LOW) + try: + p.ionice(psutil.IOPRIO_HIGH) + except psutil.AccessDenied: + pass + else: + self.assertEqual(p.ionice(), psutil.IOPRIO_HIGH) + # errs + self.assertRaisesRegex( + TypeError, "value argument not accepted on Windows", + p.ionice, psutil.IOPRIO_NORMAL, value=1) + self.assertRaisesRegex( + ValueError, "is not a valid priority", + p.ionice, psutil.IOPRIO_HIGH + 1) + finally: + p.ionice(psutil.IOPRIO_NORMAL) + self.assertEqual(p.ionice(), psutil.IOPRIO_NORMAL) + + @unittest.skipIf(not HAS_IONICE, "not supported") + @unittest.skipIf(not MACOS, 'macOS only') + def test_ionice_macos(self): + names = ("IOPOL_IMPORTANT", "IOPOL_STANDARD", "IOPOL_UTILITY", + "IOPOL_THROTTLE", "IOPOL_PASSIVE") + p = psutil.Process() + original = p.ionice() + try: + for n in names: + with self.subTest(name=n): + if n == "IOPOL_IMPORTANT": + # XXX has no effect (?!?) + continue + value = getattr(psutil, n) + p.ionice(value) + self.assertEqual(p.ionice(), value) + finally: + p.ionice(original) @unittest.skipIf(not HAS_RLIMIT, "not supported") def test_rlimit_get(self): diff --git a/psutil/tests/test_windows.py b/psutil/tests/test_windows.py index a3a6b61d..70c99b4b 100755 --- a/psutil/tests/test_windows.py +++ b/psutil/tests/test_windows.py @@ -664,17 +664,15 @@ class TestDualProcessImplementation(unittest.TestCase): assert fun.called def test_cmdline(self): - from psutil._pswindows import ACCESS_DENIED_ERRSET + from psutil._pswindows import convert_oserror for pid in psutil.pids(): try: a = cext.proc_cmdline(pid, use_peb=True) b = cext.proc_cmdline(pid, use_peb=False) except OSError as err: - if err.errno in ACCESS_DENIED_ERRSET: - pass - elif err.errno == errno.ESRCH: - pass # NSP - else: + err = convert_oserror(err) + if not isinstance(err, (psutil.AccessDenied, + psutil.NoSuchProcess)): raise else: self.assertEqual(a, b) @@ -837,7 +835,8 @@ class TestServices(unittest.TestCase): # test NoSuchProcess service = psutil.win_service_get(name) exc = WindowsError( - psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST, "") + 0, "", 0, + psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST) with mock.patch("psutil._psplatform.cext.winservice_query_status", side_effect=exc): self.assertRaises(psutil.NoSuchProcess, service.status) @@ -847,7 +846,8 @@ class TestServices(unittest.TestCase): # test AccessDenied exc = WindowsError( - psutil._psplatform.cext.ERROR_ACCESS_DENIED, "") + 0, "", 0, + psutil._psplatform.cext.ERROR_ACCESS_DENIED) with mock.patch("psutil._psplatform.cext.winservice_query_status", side_effect=exc): self.assertRaises(psutil.AccessDenied, service.status) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index cbdeebdc..75b4c348 100755 --- a/scripts/internal/winmake.py +++ b/scripts/internal/winmake.py @@ -318,6 +318,7 @@ def clean(): "*.~", "*__pycache__", ".coverage", + ".failed-tests.txt", ".tox", ) safe_rmtree("build") @@ -440,6 +441,15 @@ def test_by_name(): @cmd +def test_failed(): + """Re-run tests which failed on last run.""" + install() + test_setup() + sh('%s -c "import psutil.tests.runner as r; r.run(last_failed=True)"' % ( + PYTHON)) + + +@cmd def test_script(): """Quick way to test a script""" try: |