diff --git a/Makefile b/Makefile index 890c6e413f..ff71d9c7fc 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ DEPS = \ pyperf \ requests \ setuptools \ - sphinx \ + sphinx==2.3.1 \ twine \ unittest2 \ virtualenv \ diff --git a/docs/index.rst b/docs/index.rst index ea5f15bd12..82b4498ffc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -493,6 +493,23 @@ Disks .. versionchanged:: 4.0.0 NetBSD no longer has *read_time* and *write_time* fields. +.. function:: disk_swaps() + + Enumerate swap partitions and swap files as a list of namedtuples including: + + - **path**: the path of the swap partition/file on disk + - **total**: total swap partition/file size + - **used**: used swap partition/file size + - **fstype** (Linux): either "partition" or "swapfile" + - **priority** (Linux): makes sense when multiple swap files are in use. + The lower the priority, the more likely the swap file is o be used. + - **peak** (Windows): the highest peak usage over time. + + Availability: Linux, Windows, FreeBSD + + .. versionadded:: 5.7.1 + + Network ------- diff --git a/psutil/__init__.py b/psutil/__init__.py index e74bb109ce..81f1cee966 100644 --- a/psutil/__init__.py +++ b/psutil/__init__.py @@ -220,6 +220,7 @@ "net_io_counters", "net_connections", "net_if_addrs", # network "net_if_stats", "disk_io_counters", "disk_partitions", "disk_usage", # disk + # "disk_swaps" # "sensors_temperatures", "sensors_battery", "sensors_fans" # sensors "users", "boot_time", # others ] @@ -2081,6 +2082,25 @@ def disk_io_counters(perdisk=False, nowrap=True): disk_io_counters.cache_clear.__doc__ = "Clears nowrap argument cache" +# Linux +if hasattr(_psplatform, "disk_swaps"): + + def disk_swaps(): + """Enumerate swap partitions and swap files as a list of namedtuples: + + - path: the path of the swap partition/file on disk + - total: total swap partition/file size + - used: used swap partition/file size + - fstype (Linux): either "partition" or "swapfile" + - priority (Linux): makes sense when multiple swap files are in + use. The lower the priority, the more likely the swap file is + to be used. + """ + return _psplatform.disk_swaps() + + __all__.append("disk_swaps") + + # ===================================================================== # --- network related functions # ===================================================================== diff --git a/psutil/_common.py b/psutil/_common.py index 9306cd1556..efa8e36c09 100644 --- a/psutil/_common.py +++ b/psutil/_common.py @@ -176,6 +176,8 @@ class BatteryTime(enum.IntEnum): 'read_time', 'write_time']) # psutil.disk_partitions() sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts']) +# psutil.disk_swaps() +sdiskswap = namedtuple('sdiskswap', ['path', 'total', 'used']) # psutil.net_io_counters() snetio = namedtuple('snetio', ['bytes_sent', 'bytes_recv', 'packets_sent', 'packets_recv', diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index 49ad1e995c..f83c401229 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -336,6 +336,10 @@ def disk_partitions(all=False): disk_usage = _psposix.disk_usage disk_io_counters = cext.disk_io_counters +if hasattr(cext, "disk_swaps"): + def disk_swaps(): + """Return disk page files information.""" + return [_common.sdiskswap(*x) for x in cext.disk_swaps()] # ===================================================================== diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py index 9e32f25e7b..f745d61a22 100644 --- a/psutil/_pslinux.py +++ b/psutil/_pslinux.py @@ -182,6 +182,9 @@ class IOPriority(enum.IntEnum): 'read_time', 'write_time', 'read_merged_count', 'write_merged_count', 'busy_time']) +# psutil.disk_swaps() +sdiskswap = namedtuple( + 'sdiskswap', _common.sdiskswap._fields + ('fstype', 'priority')) # psutil.Process().open_files() popenfile = namedtuple( 'popenfile', ['path', 'fd', 'position', 'mode', 'flags']) @@ -1182,6 +1185,37 @@ def disk_partitions(all=False): return retlist +def disk_swaps(): + """Return swap partitions (or swap files).""" + retlist = [] + try: + f = open_text("%s/swaps" % get_procfs_path()) + except FileNotFoundError: + return retlist + else: + with f: + lines = f.readlines() + lines.pop(0) # header + for line in lines: + # Format is confusing: + # Filename Type\tSize Used Priority + # /dev/nvme0n1p3 partition\t11718652 2724 -2 + line = line.strip() + name_and_type, _, other_fields = line.partition('\t') + fstype = name_and_type.split()[-1] + # "/dev/nvme0n1p3 partition" -> "/dev/nvme0n1p3" + path = name_and_type.rstrip(fstype).strip() + # The priority column is useful when multiple swap + # files are in use. The lower the priority, the + # more likely the swap file is to be used. + total, used, priority = map(int, other_fields.split('\t')) + total *= 1024 + used *= 1024 + nt = sdiskswap(path, total, used, fstype, priority) + retlist.append(nt) + return retlist + + # ===================================================================== # --- sensors # ===================================================================== diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index 953fcd083c..798550339c 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -691,6 +691,55 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } +#define NSWAP 16 + +/* + * Enumerate swap locations. + */ +static PyObject * +psutil_disk_swaps(PyObject *self, PyObject *args) { + kvm_t *kd; + int n; + int i; + struct kvm_swap kswap[NSWAP]; + int pagesize = getpagesize(); + PyObject *py_tuple; + PyObject *py_retlist; + + n = kvm_getswapinfo( + kd, kswap, sizeof kswap / sizeof kswap[0], SWIF_DEV_PREFIX); + if (n == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + // For some reason if we don't create the list here kvm_getswapinfo() + // returns 0. + py_retlist = PyList_New(0); + if (! py_retlist) + goto error; + + for (i = 0; i < n; ++i) { + py_tuple = Py_BuildValue( + "sII", + kswap[i].ksw_devname, + kswap[i].ksw_total * pagesize, + kswap[i].ksw_used * pagesize + ); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + } + + return py_retlist; + +error: + return NULL; +} + + /* * Return a Python list of named tuples with overall network I/O information */ @@ -976,6 +1025,8 @@ static PyMethodDef mod_methods[] = { {"disk_partitions", psutil_disk_partitions, METH_VARARGS, "Return a list of tuples including device, mount point and " "fs type for all partitions mounted on the system."}, + {"disk_swaps", psutil_disk_swaps, METH_VARARGS, + "Enumerate swap partitions/files."}, {"net_io_counters", psutil_net_io_counters, METH_VARARGS, "Return dict of tuples of networks I/O information."}, {"disk_io_counters", psutil_disk_io_counters, METH_VARARGS, diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 82fa518eeb..f63c6a35e4 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -1604,6 +1604,8 @@ PsutilMethods[] = { "Return a list of currently connected users."}, {"disk_partitions", psutil_disk_partitions, METH_VARARGS, "Return disk partitions."}, + {"disk_swaps", psutil_disk_swaps, METH_VARARGS, + "Return information about the disk page files as a list."}, {"net_connections", psutil_net_connections, METH_VARARGS, "Return system-wide connections"}, {"net_if_addrs", psutil_net_if_addrs, METH_VARARGS, diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py index 99d5d71499..0446656c97 100644 --- a/psutil/_pswindows.py +++ b/psutil/_pswindows.py @@ -165,6 +165,9 @@ class IOPriority(enum.IntEnum): ['user', 'system', 'idle', 'interrupt', 'dpc']) # psutil.virtual_memory() svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) +# psutil.disk_swaps() +sdiskswap = namedtuple( + 'sdiskswap', _common.sdiskswap._fields + ('peak', )) # psutil.Process.memory_info() pmem = namedtuple( 'pmem', ['rss', 'vms', @@ -196,6 +199,9 @@ def convert_dos_path(s): into: "C:\Windows\systemew\file.txt" """ + # "\??\" refers to \GLOBAL??\. Just remove it. + if s.startswith("\\??\\"): + return s[4:] rawdrive = '\\'.join(s.split('\\')[:3]) driveletter = cext.win32_QueryDosDevice(rawdrive) remainder = s[len(rawdrive):] @@ -274,6 +280,17 @@ def disk_partitions(all): return [_common.sdiskpart(*x) for x in rawlist] +def disk_swaps(): + """Return disk page files information.""" + ret = [] + rawlist = cext.disk_swaps() + for dospath, total, used, peak in rawlist: + path = convert_dos_path(dospath) + nt = sdiskswap(path, total, used, peak) + ret.append(nt) + return ret + + # ===================================================================== # --- CPU # ===================================================================== diff --git a/psutil/arch/windows/disk.c b/psutil/arch/windows/disk.c index 45e0ee1e62..0cf9533188 100644 --- a/psutil/arch/windows/disk.c +++ b/psutil/arch/windows/disk.c @@ -344,6 +344,89 @@ psutil_disk_partitions(PyObject *self, PyObject *args) { } +/* + * Return information about the disk page files as a list. + * A page file is basically the same thing as a swap partition. + */ +PyObject * +psutil_disk_swaps(PyObject *self, PyObject *args) { + NTSTATUS status; + PVOID buffer = NULL; + ULONG bufferSize = 0x200; + PSYSTEM_PAGEFILE_INFORMATION pInfo; + PyObject *py_tuple = NULL; + PyObject *py_path = NULL; + PyObject *py_retlist = PyList_New(0); + + if (! py_retlist) + return NULL; + + // Enumerate page files. + buffer = MALLOC_ZERO(bufferSize); + while ((status = NtQuerySystemInformation( + SystemPageFileInformation, + buffer, + bufferSize, + NULL)) == STATUS_INFO_LENGTH_MISMATCH) + { + FREE(buffer); + bufferSize *= 2; + buffer = MALLOC_ZERO(bufferSize); + } + + if (! NT_SUCCESS(status)) { + psutil_SetFromNTStatusErr(status, "NtQuerySystemInformation"); + goto error; + } + + // Traverse the resulting struct. + // A TotalSize of 0 is used to indicate that there are no pagefiles. + pInfo = (SYSTEM_PAGEFILE_INFORMATION *)buffer; + if (pInfo->TotalSize != 0) { + while (TRUE) { + // construct python list + py_path = PyUnicode_FromWideChar( + pInfo->PageFileName.Buffer, + wcslen(pInfo->PageFileName.Buffer)); + if (! py_path) + goto error; + + py_tuple = Py_BuildValue( + "Okkk", + py_path, + pInfo->TotalSize * PSUTIL_SYSTEM_INFO.dwPageSize, + pInfo->TotalInUse * PSUTIL_SYSTEM_INFO.dwPageSize, + pInfo->PeakUsage * PSUTIL_SYSTEM_INFO.dwPageSize + ); + if (!py_tuple) + goto error; + if (PyList_Append(py_retlist, py_tuple)) + goto error; + Py_CLEAR(py_tuple); + Py_CLEAR(py_path); + + // end of list + if (pInfo->NextEntryOffset == 0) + break; + // set pointer to the next pInfo struct + pInfo = (SYSTEM_PAGEFILE_INFORMATION *) \ + ((BYTE *)pInfo + pInfo->NextEntryOffset); + } + } + + FREE(buffer); + return py_retlist; + +error: + if (buffer != NULL) + FREE(buffer); + Py_XDECREF(py_tuple); + Py_XDECREF(py_path); + Py_DECREF(py_retlist); + return NULL; +} + + /* Accept a filename's drive in native format like "\Device\HarddiskVolume1\" and return the corresponding drive letter (e.g. "C:\\"). diff --git a/psutil/arch/windows/disk.h b/psutil/arch/windows/disk.h index 298fb6ba0e..aa22ad9539 100644 --- a/psutil/arch/windows/disk.h +++ b/psutil/arch/windows/disk.h @@ -9,4 +9,5 @@ PyObject *psutil_disk_io_counters(PyObject *self, PyObject *args); PyObject *psutil_disk_partitions(PyObject *self, PyObject *args); PyObject *psutil_disk_usage(PyObject *self, PyObject *args); +PyObject *psutil_disk_swaps(PyObject *self, PyObject *args); PyObject *psutil_win32_QueryDosDevice(PyObject *self, PyObject *args); diff --git a/psutil/arch/windows/ntextapi.h b/psutil/arch/windows/ntextapi.h index 8cb00430e2..d585c6786e 100644 --- a/psutil/arch/windows/ntextapi.h +++ b/psutil/arch/windows/ntextapi.h @@ -35,6 +35,8 @@ typedef LONG NTSTATUS; #define ProcessWow64Information 26 #undef SystemProcessIdInformation #define SystemProcessIdInformation 88 +#undef SystemPageFileInformation +#define SystemPageFileInformation 18 // process suspend() / resume() typedef enum _KTHREAD_STATE { @@ -370,6 +372,14 @@ typedef struct _SYSTEM_PROCESS_ID_INFORMATION { UNICODE_STRING ImageName; } SYSTEM_PROCESS_ID_INFORMATION, *PSYSTEM_PROCESS_ID_INFORMATION; +typedef struct _SYSTEM_PAGEFILE_INFORMATION { + ULONG NextEntryOffset; + ULONG TotalSize; + ULONG TotalInUse; + ULONG PeakUsage; + UNICODE_STRING PageFileName; +} SYSTEM_PAGEFILE_INFORMATION, *PSYSTEM_PAGEFILE_INFORMATION; + // ==================================================================== // PEB structs for cmdline(), cwd(), environ() // ==================================================================== diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 3e4dc88066..78258f412d 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -171,6 +171,7 @@ HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") HAS_RLIMIT = hasattr(psutil.Process, "rlimit") HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") +HAS_DISK_SWAPS = hasattr(psutil, "disk_swaps") try: HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) except Exception: diff --git a/psutil/tests/test_bsd.py b/psutil/tests/test_bsd.py index e525e66724..69fa9eee16 100755 --- a/psutil/tests/test_bsd.py +++ b/psutil/tests/test_bsd.py @@ -240,17 +240,17 @@ def test_cpu_times(self): @unittest.skipIf(not FREEBSD, "FREEBSD only") class FreeBSDSystemTestCase(unittest.TestCase): - @staticmethod - def parse_swapinfo(): + @unittest.skipIf(not which('swapinfo'), "swapinfo util not available") + def parse_swapinfo(self): # the last line is always the total - output = sh("swapinfo -k").splitlines()[-1] - parts = re.split(r'\s+', output) - - if not parts: - raise ValueError("Can't parse swapinfo: %s" % output) - - # the size is in 1k units, so multiply by 1024 - total, used, free = (int(p) * 1024 for p in parts[1:4]) + total = used = free = 0 + lines = sh("swapinfo -k").splitlines() + lines.pop(0) # header + for line in lines: + path, total_, used_, free_ = line.split()[:4] + total += int(total_) * 1024 + used += int(used_) * 1024 + free += int(free_) * 1024 return total, used, free def test_cpu_frequency_against_sysctl(self): @@ -473,6 +473,27 @@ def test_sensors_temperatures_against_sysctl(self): psutil.sensors_temperatures()["coretemp"][cpu].high, sysctl_result) + # --- disks + + @unittest.skipIf(not which('swapinfo'), "swapinfo util not available") + def test_disk_swaps(self): + total, used, free = self.parse_swapinfo() + self.assertAlmostEqual( + sum([x.total for x in psutil.disk_swaps()]), + total, delta=MEMORY_TOLERANCE) + self.assertAlmostEqual( + sum([x.used for x in psutil.disk_swaps()]), + used, delta=MEMORY_TOLERANCE) + + lines = sh("swapinfo -k").splitlines() + lines.pop(0) # header + paths = [] + for i, line in enumerate(lines): + paths.append(line.split()[0]) + self.assertEqual(sorted(paths), + sorted([x.path for x in psutil.disk_swaps()])) + + # ===================================================================== # --- OpenBSD # ===================================================================== diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 5e258b3015..85f9c00d2c 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -133,6 +133,10 @@ def test_battery(self): self.assertEqual(hasattr(psutil, "sensors_battery"), LINUX or WINDOWS or FREEBSD or MACOS) + def test_disk_swaps(self): + hasit = LINUX or WINDOWS or FREEBSD + self.assertEqual(hasattr(psutil, "disk_swaps"), hasit) + class TestAvailProcessAPIs(unittest.TestCase): diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 97946a0bbd..872ddf50e6 100755 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -1204,6 +1204,20 @@ def exists(path): self.assertRaises(NotImplementedError, psutil.disk_io_counters) +@unittest.skipIf(not LINUX, "LINUX only") +class TestDiskSwaps(unittest.TestCase): + + @unittest.skipIf(not which("swapon"), "swapon utility not available") + def test_against_swapon(self): + swaps = psutil.disk_swaps() + lines = sh("swapon --bytes").split('\n') + lines.pop(0) # header + for i, line in enumerate(lines): + path, fstype, total, used, prio = line.split() + self.assertEqual((path, int(total), int(used), fstype, int(prio)), + tuple(swaps[i])) + + # ===================================================================== # --- misc # ===================================================================== diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 31a632a478..4da9fea5c7 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -40,6 +40,7 @@ from psutil.tests import get_test_subprocess from psutil.tests import HAS_CPU_AFFINITY from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_DISK_SWAPS from psutil.tests import HAS_ENVIRON from psutil.tests import HAS_IONICE from psutil.tests import HAS_MEMORY_MAPS @@ -515,6 +516,10 @@ def test_disk_partitions(self): def test_disk_io_counters(self): self.execute(psutil.disk_io_counters, nowrap=False) + @unittest.skipIf(not HAS_DISK_SWAPS, "not supported") + def test_disk_swaps(self): + self.execute(psutil.disk_swaps) + # --- proc @skip_if_linux() diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py index 0d3f43753e..b0cc64df41 100755 --- a/psutil/tests/test_system.py +++ b/psutil/tests/test_system.py @@ -39,6 +39,7 @@ from psutil.tests import get_test_subprocess from psutil.tests import HAS_BATTERY from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_DISK_SWAPS from psutil.tests import HAS_GETLOADAVG from psutil.tests import HAS_NET_IO_COUNTERS from psutil.tests import HAS_SENSORS_BATTERY @@ -716,6 +717,35 @@ def test_disk_io_counters_no_disks(self): self.assertEqual(psutil.disk_io_counters(perdisk=True), {}) assert m.called + @unittest.skipIf(not HAS_DISK_SWAPS, "not supported") + def test_disk_swaps(self): + ls = psutil.disk_swaps() + self.assertIsInstance(ls, list) + if not ls: + raise self.skipTest("no swap locations") + + for swap in ls: + assert os.path.exists(swap.path), swap.path + self.assertGreaterEqual(swap.total, 0) + self.assertGreaterEqual(swap.used, 0) + if LINUX: + fields = ('path', 'total', 'used', 'fstype', 'priority') + self.assertEqual(swap._fields, fields) + self.assertIn(swap.fstype, ("partition", "swapfile")) + self.assertIsInstance(swap.priority, int) + elif WINDOWS: + fields = ('path', 'total', 'used', 'peak') + self.assertEqual(swap._fields, fields) + self.assertGreaterEqual(swap.peak, 0) + + if not WINDOWS: + self.assertEqual( + psutil.swap_memory().total, + sum([x.total for x in psutil.disk_swaps()])) + self.assertEqual( + psutil.swap_memory().used, + sum([x.used for x in psutil.disk_swaps()])) + class TestNetAPIs(unittest.TestCase): diff --git a/scripts/disk_usage.py b/scripts/disk_usage.py index 1860401fda..2edfdbe01a 100755 --- a/scripts/disk_usage.py +++ b/scripts/disk_usage.py @@ -19,6 +19,7 @@ import os import psutil from psutil._common import bytes2human +from psutil._common import usage_percent def main(): @@ -41,6 +42,16 @@ def main(): int(usage.percent), part.fstype, part.mountpoint)) + if hasattr(psutil, "disk_swaps"): + for swap in psutil.disk_swaps(): + print(templ % ( + swap.path, + bytes2human(swap.total), + bytes2human(swap.used), + bytes2human(swap.total - swap.used), + int(usage_percent(swap.used, swap.total)), + "swap", + "")) if __name__ == '__main__':