From bf74f43cd343804a560af92fdf4ee9f9535f3608 Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Mon, 24 Aug 2026 16:19:04 +0700 Subject: [PATCH] fix(processlist): index io_counters instead of testing membership in it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_sum_stats(key, sub_key)` guarded the accumulation with `sub_key in p[key]`. That is a mapping test, and `sub_key` is only a mapping key for `memory_info`. For `io_counters` it is an index — the value is the list [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] so `0 in p['io_counters']` asked whether the counters *contain the number zero*. A process doing real IO was skipped, and one whose counters happened to hold the literal 0/1/2/3 was summed instead: [[5000, 700, 2000, 300, 1], _sum_stats('io_counters', 0) -> 0 [3000, 400, 1000, 200, 1]] (expected 8000) [[5000, 700, 0, 300, 1]] _sum_stats('io_counters', 0) -> 5000 (summed only because 0 is in the list) So the R/s and W/s totals on the process-list summary row — rendered whenever a process filter is active — were not merely zero but arbitrary. The VIRT and RES totals beside them were correct, because `memory_info` really is a mapping. Branch on the container type, and bounds-check the index so a short list is skipped rather than raising. --- glances/plugins/processlist/__init__.py | 9 +++++++ tests/test_core.py | 36 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/glances/plugins/processlist/__init__.py b/glances/plugins/processlist/__init__.py index 5bd4597f..1efcc84d 100644 --- a/glances/plugins/processlist/__init__.py +++ b/glances/plugins/processlist/__init__.py @@ -1002,6 +1002,15 @@ class ProcesslistPlugin(GlancesPluginModel): continue if sub_key is None: ret += p[key] + elif isinstance(p[key], (list, tuple)): + # `sub_key` is an index here, not a key: `io_counters` is the list + # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] + # (see glances/processes.py). `sub_key in p[key]` asked whether the + # list *contains the number* 0/1/2/3, so a process doing real IO was + # skipped and one whose counters happened to hold that literal was + # summed instead. + if isinstance(sub_key, int) and -len(p[key]) <= sub_key < len(p[key]): + ret += p[key][sub_key] elif sub_key in p[key]: ret += p[key][sub_key] diff --git a/tests/test_core.py b/tests/test_core.py index ce282e91..24535d86 100755 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -384,6 +384,42 @@ class TestGlances(unittest.TestCase): print('INFO: cpu_num display formatting tests passed') + def test_010c_processlist_sum_stats_indexes_lists(self): + """The filtered process-list IO totals summed almost nothing. + + `_sum_stats('io_counters', 0)` means "index 0 of the counters list", but the + guard was `sub_key in p[key]` -- a value-membership test on a list. A process + doing real IO was skipped, and one whose counters happened to contain the + literal 0/1/2/3 was summed instead, so the R/s and W/s row was not merely + zero but arbitrary. The VIRT/RES totals beside it were right because + `memory_info` is a mapping. + """ + print('INFO: [TEST_010c] Check processlist sum of IO counters') + from glances.plugins.processlist import ProcesslistPlugin + + plugin = ProcesslistPlugin.__new__(ProcesslistPlugin) + # io_counters is [read, write, read_old, write_old, io_tag]. + plugin.stats = [ + {'io_counters': [5000, 700, 2000, 300, 1], 'memory_info': {'rss': 10, 'vms': 20}}, + {'io_counters': [3000, 400, 1000, 200, 1], 'memory_info': {'rss': 30, 'vms': 40}}, + ] + + self.assertEqual(plugin._sum_stats('io_counters', 0), 8000) + self.assertEqual(plugin._sum_stats('io_counters', sub_key=1), 1100) + self.assertEqual(plugin._sum_stats('io_counters', sub_key=2), 3000) + self.assertEqual(plugin._sum_stats('io_counters', sub_key=3), 500) + # A mapping sub_key must keep working unchanged. + self.assertEqual(plugin._sum_stats('memory_info', sub_key='rss'), 40) + self.assertEqual(plugin._sum_stats('memory_info', sub_key='absent'), 0) + + # The counters list holding the literal index must no longer be what decides. + plugin.stats = [{'io_counters': [5000, 700, 0, 300, 1]}] + self.assertEqual(plugin._sum_stats('io_counters', 0), 5000) + + # An index past the end is skipped rather than raising IndexError. + plugin.stats = [{'io_counters': [1, 2]}] + self.assertEqual(plugin._sum_stats('io_counters', 4), 0) + def test_011_folders(self): """Check File System plugin.""" # stats_to_check = [ ]