Enhance process "extended stats" display (in Curses interface) #2225

This commit is contained in:
nicolargo 2023-03-11 17:34:05 +01:00
commit e0a052c53e
8 changed files with 539 additions and 415 deletions

BIN
docs/_static/processlist-extended.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -15,10 +15,15 @@ Filtered view:
.. image:: ../_static/processlist-filter.png
Extended view:
.. image:: ../_static/processlist-extended.png
The process view consists of 3 parts:
- Processes summary
- Monitored processes list (optional)
- Monitored processes list (optional, only in standalone mode)
- Extended stats for the selected process (optional)
- Processes list
The processes summary line displays:
@ -56,12 +61,15 @@ You can also set the sort key in the UI:
* - c
- --sort-processes cpu_percent
- Sort by CPU
* - e
- N/A
- Pin the process and display extended stats
* - i
- --sort-processes io_counters
- Sort by DISK I/O
* - j
- --programs
- Accumulate processes by program
- Accumulate processes by program (extended stats disable in this mode)
* - m
- --sort-processes memory_percent
- Sort by MEM

View File

@ -74,7 +74,7 @@ Get plugin stats::
"refresh": 3.0,
"regex": True,
"result": None,
"timer": 0.8202648162841797},
"timer": 0.8183016777038574},
{"count": 0,
"countmax": 20.0,
"countmin": None,
@ -83,7 +83,7 @@ Get plugin stats::
"refresh": 3.0,
"regex": True,
"result": None,
"timer": 0.820155143737793}]
"timer": 0.8182027339935303}]
Get a specific field::
@ -101,7 +101,7 @@ Get a specific item when field matchs the given value::
"refresh": 3.0,
"regex": True,
"result": None,
"timer": 0.8202648162841797}]}
"timer": 0.8183016777038574}]}
GET core
--------
@ -131,19 +131,19 @@ Get plugin stats::
"ctx_switches": 0,
"guest": 0.0,
"guest_nice": 0.0,
"idle": 71.8,
"idle": 70.9,
"interrupts": 0,
"iowait": 0.2,
"iowait": 0.0,
"irq": 0.0,
"nice": 0.0,
"soft_interrupts": 0,
"softirq": 0.0,
"steal": 0.0,
"syscalls": 0,
"system": 3.9,
"system": 4.0,
"time_since_update": 1,
"total": 29.2,
"user": 24.1}
"total": 29.8,
"user": 25.2}
Fields descriptions:
@ -166,7 +166,7 @@ Fields descriptions:
Get a specific field::
# curl http://localhost:61208/api/3/cpu/total
{"total": 29.2}
{"total": 29.8}
GET diskio
----------
@ -211,58 +211,29 @@ GET docker
Get plugin stats::
# curl http://localhost:61208/api/3/docker
{"containers": [{"Command": ["/whoamI"],
"Id": "12b96990d8485d7cb37dbb54a3fb28207169654191701575f9d6bcf2cfc4c2e4",
"Image": ["emilevauge/whoami:latest"],
"Status": "running",
"Uptime": "1 weeks",
"cpu": {"total": 0.0},
"cpu_percent": 0.0,
"io": {},
"io_r": None,
"io_w": None,
"key": "name",
"memory": {},
"memory_usage": None,
"name": "docker-compose_whoami_1",
"network": {},
"network_rx": None,
"network_tx": None},
{"Command": ["/bin/sh",
"-c",
"python3 -m glances -C /etc/glances.conf "
"$GLANCES_OPT"],
"Id": "ca7c2a050209ed12bdc8de20719423b4b6bc819e7f02e7ef0f44ff4e2f3ba0e3",
"Image": ["nicolargo/glances:dev"],
"Status": "running",
"Uptime": "1 weeks",
"cpu": {"total": 0.0},
"cpu_percent": 0.0,
"io": {},
"io_r": None,
"io_w": None,
"key": "name",
"memory": {},
"memory_usage": None,
"name": "docker-compose_monitoring_1",
"network": {},
"network_rx": None,
"network_tx": None},
{"Command": ["docker-entrypoint.sh", "mongod"],
{"containers": [{"Command": ["docker-entrypoint.sh", "mongod"],
"Id": "c3a1bb27858df965e1c524c6ef33c0fd26d765cae5bcd90fbe9e662b703a52aa",
"Image": ["mongo:latest"],
"Status": "running",
"Uptime": "1 months",
"cpu": {"total": 0.0},
"cpu_percent": 0.0,
"io": {},
"io": {"cumulative_ior": 847872,
"cumulative_iow": 53813248,
"time_since_update": 1},
"io_r": None,
"io_w": None,
"key": "name",
"memory": {},
"memory_usage": None,
"memory": {"cache": None,
"limit": 7836196864,
"max_usage": None,
"rss": None,
"usage": 23642112},
"memory_usage": 23642112,
"name": "docker-mongo_mongo_1",
"network": {},
"network": {"cumulative_rx": 37744307,
"cumulative_tx": 28244833,
"time_since_update": 1},
"network_rx": None,
"network_tx": None},
{"Command": ["tini",
@ -340,13 +311,13 @@ Get plugin stats::
# curl http://localhost:61208/api/3/fs
[{"device_name": "/dev/mapper/ubuntu--gnome--vg-root",
"free": 53523116032,
"free": 53470863360,
"fs_type": "ext4",
"key": "mnt_point",
"mnt_point": "/",
"percent": 76.8,
"size": 243334156288,
"used": 177423585280},
"used": 177475837952},
{"device_name": "zsfpool",
"free": 41811968,
"fs_type": "zfs",
@ -365,13 +336,13 @@ Get a specific item when field matchs the given value::
# curl http://localhost:61208/api/3/fs/mnt_point//
{"/": [{"device_name": "/dev/mapper/ubuntu--gnome--vg-root",
"free": 53523116032,
"free": 53470863360,
"fs_type": "ext4",
"key": "mnt_point",
"mnt_point": "/",
"percent": 76.8,
"size": 243334156288,
"used": 177423585280}]}
"used": 177475837952}]}
GET ip
------
@ -397,7 +368,10 @@ GET load
Get plugin stats::
# curl http://localhost:61208/api/3/load
{"cpucore": 4, "min1": 0.63525390625, "min15": 1.3359375, "min5": 1.08349609375}
{"cpucore": 4,
"min1": 0.79833984375,
"min15": 0.7431640625,
"min5": 0.8056640625}
Fields descriptions:
@ -409,7 +383,7 @@ Fields descriptions:
Get a specific field::
# curl http://localhost:61208/api/3/load/min1
{"min1": 0.63525390625}
{"min1": 0.79833984375}
GET mem
-------
@ -417,16 +391,16 @@ GET mem
Get plugin stats::
# curl http://localhost:61208/api/3/mem
{"active": 2453286912,
"available": 3052646400,
"buffers": 129122304,
"cached": 3469373440,
"free": 3052646400,
"inactive": 4119760896,
"percent": 61.0,
"shared": 455708672,
{"active": 2350882816,
"available": 2422988800,
"buffers": 131072000,
"cached": 2795134976,
"free": 2422988800,
"inactive": 3522777088,
"percent": 69.1,
"shared": 685944832,
"total": 7836196864,
"used": 4783550464}
"used": 5413208064}
Fields descriptions:
@ -453,13 +427,13 @@ GET memswap
Get plugin stats::
# curl http://localhost:61208/api/3/memswap
{"free": 5701988352,
"percent": 29.5,
"sin": 8590413824,
"sout": 14424719360,
{"free": 5875109888,
"percent": 27.3,
"sin": 8760786944,
"sout": 14506393600,
"time_since_update": 1,
"total": 8082419712,
"used": 2380431360}
"used": 2207309824}
Fields descriptions:
@ -483,29 +457,29 @@ Get plugin stats::
# curl http://localhost:61208/api/3/network
[{"alias": None,
"cumulative_cx": 1100113578,
"cumulative_rx": 550056789,
"cumulative_tx": 550056789,
"cx": 7802,
"cumulative_cx": 1140515614,
"cumulative_rx": 570257807,
"cumulative_tx": 570257807,
"cx": 5342,
"interface_name": "lo",
"is_up": True,
"key": "interface_name",
"rx": 3901,
"rx": 2671,
"speed": 0,
"time_since_update": 1,
"tx": 3901},
"tx": 2671},
{"alias": None,
"cumulative_cx": 35336641046,
"cumulative_rx": 34139736267,
"cumulative_tx": 1196904779,
"cx": 32563,
"cumulative_cx": 36011739106,
"cumulative_rx": 34790353651,
"cumulative_tx": 1221385455,
"cx": 24457,
"interface_name": "wlp2s0",
"is_up": True,
"key": "interface_name",
"rx": 22311,
"rx": 18960,
"speed": 0,
"time_since_update": 1,
"tx": 10252}]
"tx": 5497}]
Fields descriptions:
@ -535,24 +509,23 @@ Get a specific field::
"br-ef0a06c4e10f",
"veth9910148",
"veth5e7315e",
"veth24ae93e",
"vethb59ceaa"]}
"veth24ae93e"]}
Get a specific item when field matchs the given value::
# curl http://localhost:61208/api/3/network/interface_name/lo
{"lo": [{"alias": None,
"cumulative_cx": 1100113578,
"cumulative_rx": 550056789,
"cumulative_tx": 550056789,
"cx": 7802,
"cumulative_cx": 1140515614,
"cumulative_rx": 570257807,
"cumulative_tx": 570257807,
"cx": 5342,
"interface_name": "lo",
"is_up": True,
"key": "interface_name",
"rx": 3901,
"rx": 2671,
"speed": 0,
"time_since_update": 1,
"tx": 3901}]}
"tx": 2671}]}
GET now
-------
@ -560,7 +533,7 @@ GET now
Get plugin stats::
# curl http://localhost:61208/api/3/now
"2023-03-11 10:42:32 CET"
"2023-03-11 17:30:43 CET"
GET percpu
----------
@ -571,28 +544,28 @@ Get plugin stats::
[{"cpu_number": 0,
"guest": 0.0,
"guest_nice": 0.0,
"idle": 21.0,
"idle": 22.0,
"iowait": 0.0,
"irq": 0.0,
"key": "cpu_number",
"nice": 0.0,
"softirq": 0.0,
"steal": 0.0,
"system": 3.0,
"total": 79.0,
"user": 68.0},
"system": 1.0,
"total": 78.0,
"user": 67.0},
{"cpu_number": 1,
"guest": 0.0,
"guest_nice": 0.0,
"idle": 83.0,
"idle": 81.0,
"iowait": 0.0,
"irq": 0.0,
"key": "cpu_number",
"nice": 0.0,
"softirq": 0.0,
"steal": 0.0,
"system": 0.0,
"total": 17.0,
"system": 2.0,
"total": 19.0,
"user": 7.0}]
Get a specific field::
@ -612,7 +585,7 @@ Get plugin stats::
"port": 0,
"refresh": 30,
"rtt_warning": None,
"status": 0.004165,
"status": 0.003449,
"timeout": 3}]
Get a specific field::
@ -629,7 +602,7 @@ Get a specific item when field matchs the given value::
"port": 0,
"refresh": 30,
"rtt_warning": None,
"status": 0.004165,
"status": 0.003449,
"timeout": 3}]}
GET processcount
@ -638,12 +611,12 @@ GET processcount
Get plugin stats::
# curl http://localhost:61208/api/3/processcount
{"pid_max": 0, "running": 1, "sleeping": 326, "thread": 1637, "total": 396}
{"pid_max": 0, "running": 1, "sleeping": 319, "thread": 1542, "total": 387}
Get a specific field::
# curl http://localhost:61208/api/3/processcount/total
{"total": 396}
{"total": 387}
GET processlist
---------------
@ -653,15 +626,15 @@ Get plugin stats::
# curl http://localhost:61208/api/3/processlist
[{"cmdline": ["/snap/firefox/2391/usr/lib/firefox/firefox"],
"cpu_percent": 0.0,
"cpu_times": [77.73, 21.94, 28.88, 6.46, 0.0],
"cpu_times": [1287.11, 419.09, 1097.08, 148.64, 0.0],
"gids": [1000, 1000, 1000],
"io_counters": [351327232, 423739392, 0, 0, 0],
"io_counters": [502609920, 1333940224, 0, 0, 0],
"key": "pid",
"memory_info": [448016384, 3489435648, 179822592, 626688, 0, 589619200, 0],
"memory_percent": 5.71726810563191,
"memory_info": [509206528, 21605502976, 176431104, 626688, 0, 833679360, 0],
"memory_percent": 6.498133429231825,
"name": "firefox",
"nice": 0,
"num_threads": 104,
"num_threads": 117,
"pid": 846720,
"status": "S",
"time_since_update": 1,
@ -686,12 +659,12 @@ Get plugin stats::
"true",
"tab"],
"cpu_percent": 0.0,
"cpu_times": [28.89, 5.0, 0.0, 0.0, 0.0],
"cpu_times": [185.15, 34.08, 0.0, 0.0, 0.0],
"gids": [1000, 1000, 1000],
"io_counters": [4248576, 0, 0, 0, 0],
"io_counters": [9207808, 0, 0, 0, 0],
"key": "pid",
"memory_info": [405463040, 3183603712, 85282816, 626688, 0, 623808512, 0],
"memory_percent": 5.174232437456028,
"memory_info": [435576832, 3219697664, 100659200, 626688, 0, 613511168, 0],
"memory_percent": 5.558523344418112,
"name": "WebExtensions",
"nice": 0,
"num_threads": 20,
@ -705,181 +678,176 @@ Get a specific field::
# curl http://localhost:61208/api/3/processlist/pid
{"pid": [846720,
846991,
4150,
847043,
255919,
4150,
847035,
255919,
255685,
847039,
847858,
859882,
836010,
255737,
847051,
849456,
836010,
847733,
849713,
285410,
847007,
258480,
4473,
847007,
285410,
422,
875536,
255627,
849995,
850278,
850213,
850165,
255751,
847507,
258480,
6074,
255751,
876641,
876390,
847507,
876962,
255664,
850348,
62850,
877142,
2512,
4544,
4035,
846970,
4544,
847508,
256270,
422,
258481,
2512,
596789,
255769,
596789,
285072,
95798,
829345,
2721,
4585,
255672,
4413,
789543,
860282,
285950,
286011,
4248,
789544,
255906,
3955,
286011,
255906,
255976,
829380,
4932,
790774,
2721,
62850,
829424,
1635,
1,
789543,
4325,
4223,
829380,
829424,
14455,
1,
4325,
4331,
4561,
4223,
4332,
4561,
4214,
3351,
4625,
1660,
1681,
3351,
1876,
5299,
3944,
219792,
14458,
286108,
1876,
4327,
3934,
3944,
5299,
4182,
4130,
4659,
4263,
4445,
4327,
286108,
4339,
4233,
4214,
4137,
2239,
1682,
129087,
4182,
4352,
129101,
4261,
255632,
3968,
830552,
823483,
4130,
17189,
4377,
129097,
3968,
1634,
4352,
1442,
255633,
1777,
17189,
1682,
2179,
117714,
59511,
4330,
4452,
1643,
706956,
17205,
1655,
219792,
4659,
4524,
3700,
706252,
3966,
4229,
3971,
1873,
117714,
4330,
96102,
2179,
4201,
285853,
790786,
1643,
4192,
1673,
4334,
4392,
3966,
4155,
706252,
4329,
4443,
1617,
790709,
4328,
3700,
4573,
1675,
4573,
4485,
285970,
1661,
4337,
4050,
285853,
1631,
4178,
1666,
4334,
4166,
1666,
4348,
4324,
4335,
285913,
4045,
4347,
4443,
285970,
285913,
4173,
4192,
4212,
468,
4212,
3976,
1885,
1676,
1626,
1676,
4314,
1670,
9703,
1646,
4119,
1443,
255649,
850150,
877118,
3952,
129099,
49191,
2020,
255649,
1633,
4162,
1441,
14505,
49179,
3953,
3701,
3354,
129100,
1449,
@ -887,32 +855,30 @@ Get a specific field::
4579,
1450,
2472,
3701,
219931,
49182,
1618,
843176,
850347,
877141,
1804,
49194,
1803,
790829,
4323,
2480,
3707,
4018,
255635,
285883,
285777,
285877,
285883,
872737,
285835,
285843,
285784,
285905,
3945,
285898,
4820,
839994,
285898,
2475,
1447,
2503,
@ -1068,58 +1034,56 @@ Get a specific field::
220752,
220756,
285941,
818594,
818595,
818597,
826942,
829047,
829048,
829051,
829052,
829054,
829104,
840207,
840738,
841704,
841808,
843409,
844084,
844992,
846598,
848860,
848861,
849001,
849187,
850107,
850250,
850251,
850252,
850253,
850254,
850255,
850273,
850308]}
859825,
860163,
860164,
860166,
860167,
860169,
860220,
868345,
871910,
872852,
873104,
873258,
873643,
874420,
874573,
874834,
874866,
875114,
875396,
875564,
876077,
876089,
876121,
876528,
876924,
877134]}
Get a specific item when field matchs the given value::
# curl http://localhost:61208/api/3/processlist/pid/846720
{"846720": [{"cmdline": ["/snap/firefox/2391/usr/lib/firefox/firefox"],
"cpu_percent": 0.0,
"cpu_times": [77.73, 21.94, 28.88, 6.46, 0.0],
"cpu_times": [1287.11, 419.09, 1097.08, 148.64, 0.0],
"gids": [1000, 1000, 1000],
"io_counters": [351327232, 423739392, 0, 0, 0],
"io_counters": [502609920, 1333940224, 0, 0, 0],
"key": "pid",
"memory_info": [448016384,
3489435648,
179822592,
"memory_info": [509206528,
21605502976,
176431104,
626688,
0,
589619200,
833679360,
0],
"memory_percent": 5.71726810563191,
"memory_percent": 6.498133429231825,
"name": "firefox",
"nice": 0,
"num_threads": 104,
"num_threads": 117,
"pid": 846720,
"status": "S",
"time_since_update": 1,
@ -1139,25 +1103,51 @@ GET quicklook
Get plugin stats::
# curl http://localhost:61208/api/3/quicklook
{"cpu": 29.2,
{"cpu": 29.8,
"cpu_hz": 3000000000.0,
"cpu_hz_current": 1619193499.9999998,
"cpu_hz_current": 1604295750.0,
"cpu_name": "Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz",
"mem": 61.0,
"mem": 69.1,
"percpu": [{"cpu_number": 0,
"guest": 0.0,
"guest_nice": 0.0,
"idle": 21.0,
"idle": 22.0,
"iowait": 0.0,
"irq": 0.0,
"key": "cpu_number",
"nice": 0.0,
"softirq": 0.0,
"steal": 0.0,
"system": 3.0,
"total": 79.0,
"user": 68.0},
"system": 1.0,
"total": 78.0,
"user": 67.0},
{"cpu_number": 1,
"guest": 0.0,
"guest_nice": 0.0,
"idle": 81.0,
"iowait": 0.0,
"irq": 0.0,
"key": "cpu_number",
"nice": 0.0,
"softirq": 0.0,
"steal": 0.0,
"system": 2.0,
"total": 19.0,
"user": 7.0},
{"cpu_number": 2,
"guest": 0.0,
"guest_nice": 0.0,
"idle": 70.0,
"iowait": 0.0,
"irq": 0.0,
"key": "cpu_number",
"nice": 0.0,
"softirq": 0.0,
"steal": 0.0,
"system": 2.0,
"total": 30.0,
"user": 20.0},
{"cpu_number": 3,
"guest": 0.0,
"guest_nice": 0.0,
"idle": 83.0,
@ -1167,41 +1157,15 @@ Get plugin stats::
"nice": 0.0,
"softirq": 0.0,
"steal": 0.0,
"system": 0.0,
"system": 5.0,
"total": 17.0,
"user": 7.0},
{"cpu_number": 2,
"guest": 0.0,
"guest_nice": 0.0,
"idle": 73.0,
"iowait": 1.0,
"irq": 0.0,
"key": "cpu_number",
"nice": 0.0,
"softirq": 0.0,
"steal": 0.0,
"system": 3.0,
"total": 27.0,
"user": 15.0},
{"cpu_number": 3,
"guest": 0.0,
"guest_nice": 0.0,
"idle": 82.0,
"iowait": 0.0,
"irq": 0.0,
"key": "cpu_number",
"nice": 0.0,
"softirq": 0.0,
"steal": 0.0,
"system": 3.0,
"total": 18.0,
"user": 6.0}],
"swap": 29.5}
"swap": 27.3}
Get a specific field::
# curl http://localhost:61208/api/3/quicklook/cpu
{"cpu": 29.2}
{"cpu": 29.8}
GET sensors
-----------
@ -1272,7 +1236,7 @@ GET uptime
Get plugin stats::
# curl http://localhost:61208/api/3/uptime
"55 days, 17:31:34"
"56 days, 0:19:45"
GET all stats
-------------
@ -1288,33 +1252,33 @@ GET stats history
History of a plugin::
# curl http://localhost:61208/api/3/cpu/history
{"system": [["2023-03-11T10:42:33.484532", 3.9],
["2023-03-11T10:42:34.566094", 3.9],
["2023-03-11T10:42:35.669575", 1.0]],
"user": [["2023-03-11T10:42:33.484526", 24.1],
["2023-03-11T10:42:34.566090", 24.1],
["2023-03-11T10:42:35.669571", 3.0]]}
{"system": [["2023-03-11T17:30:44.653473", 4.0],
["2023-03-11T17:30:45.724303", 4.0],
["2023-03-11T17:30:46.822979", 0.9]],
"user": [["2023-03-11T17:30:44.653466", 25.2],
["2023-03-11T17:30:45.724298", 25.2],
["2023-03-11T17:30:46.822974", 5.1]]}
Limit history to last 2 values::
# curl http://localhost:61208/api/3/cpu/history/2
{"system": [["2023-03-11T10:42:34.566094", 3.9],
["2023-03-11T10:42:35.669575", 1.0]],
"user": [["2023-03-11T10:42:34.566090", 24.1],
["2023-03-11T10:42:35.669571", 3.0]]}
{"system": [["2023-03-11T17:30:45.724303", 4.0],
["2023-03-11T17:30:46.822979", 0.9]],
"user": [["2023-03-11T17:30:45.724298", 25.2],
["2023-03-11T17:30:46.822974", 5.1]]}
History for a specific field::
# curl http://localhost:61208/api/3/cpu/system/history
{"system": [["2023-03-11T10:42:33.484532", 3.9],
["2023-03-11T10:42:34.566094", 3.9],
["2023-03-11T10:42:35.669575", 1.0]]}
{"system": [["2023-03-11T17:30:44.653473", 4.0],
["2023-03-11T17:30:45.724303", 4.0],
["2023-03-11T17:30:46.822979", 0.9]]}
Limit history for a specific field to last 2 values::
# curl http://localhost:61208/api/3/cpu/system/history
{"system": [["2023-03-11T10:42:34.566094", 3.9],
["2023-03-11T10:42:35.669575", 1.0]]}
{"system": [["2023-03-11T17:30:45.724303", 4.0],
["2023-03-11T17:30:46.822979", 0.9]]}
GET limits (used for thresholds)
--------------------------------

View File

@ -236,6 +236,13 @@ Examples of use:
dest='enable_separator',
help='enable separator in the UI',
),
parser.add_argument(
'--disable-cursor',
action='store_true',
default=False,
dest='disable_cursor',
help='disable cursor (process selection) in the UI',
),
# Sort processes list
parser.add_argument(
'--sort-processes',
@ -700,6 +707,11 @@ Examples of use:
logger.critical("Process filter is only available in standalone mode")
sys.exit(2)
# Cursor option is only available in standalone mode
if not args.disable_cursor and not self.is_standalone():
logger.critical("Cursor is only available in standalone mode")
sys.exit(2)
# Disable HDDTemp if sensors are disabled
if getattr(self.args, 'disable_sensors', False):
disable(self.args, 'hddtemp')

View File

@ -268,6 +268,7 @@ class _GlancesCurses(object):
self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
self.ifWARNING_color2 = curses.color_pair(5) | A_BOLD
self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
self.ifINFO_color = curses.color_pair(8)
self.filter_color = A_BOLD
self.selected_color = A_BOLD
@ -301,6 +302,7 @@ class _GlancesCurses(object):
self.ifCAREFUL_color2 = curses.A_UNDERLINE
self.ifWARNING_color2 = A_BOLD
self.ifCRITICAL_color2 = curses.A_REVERSE
self.ifINFO_color = A_BOLD
self.filter_color = A_BOLD
self.selected_color = A_BOLD
@ -328,6 +330,7 @@ class _GlancesCurses(object):
'CRITICAL_LOG': self.ifCRITICAL_color,
'PASSWORD': curses.A_PROTECT,
'SELECTED': self.selected_color,
'INFO': self.ifINFO_color
}
def set_cursor(self, value):
@ -407,13 +410,15 @@ class _GlancesCurses(object):
elif self.pressedkey == ord('9'):
# '9' > Theme from black to white and reverse
self._init_colors()
elif self.pressedkey == ord('e'):
elif self.pressedkey == ord('e') and not self.args.programs:
# 'e' > Enable/Disable process extended
self.args.enable_process_extended = not self.args.enable_process_extended
if not self.args.enable_process_extended:
glances_processes.disable_extended()
else:
glances_processes.enable_extended()
# When a process is selected (and only in standalone mode), disable the cursor
self.args.disable_cursor = self.args.enable_process_extended and self.args.is_standalone
elif self.pressedkey == ord('E'):
# 'E' > Erase the process filter
glances_processes.process_filter = None
@ -427,7 +432,7 @@ class _GlancesCurses(object):
elif self.pressedkey == ord('-'):
# '+' > Decrease process nice level
self.decrease_nice_process = not self.decrease_nice_process
elif self.pressedkey == ord('k'):
elif self.pressedkey == ord('k') and not self.args.disable_cursor:
# 'k' > Kill selected process (after confirmation)
self.kill_process = not self.kill_process
elif self.pressedkey == ord('w'):
@ -451,11 +456,11 @@ class _GlancesCurses(object):
# ">" (right arrow) navigation through process sort
next_sort = (self.loop_position() + 1) % len(self._sort_loop)
glances_processes.set_sort_key(self._sort_loop[next_sort], False)
elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65:
elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65 and not self.args.disable_cursor:
# 'UP' > Up in the server list
if self.args.cursor_position > 0:
self.args.cursor_position -= 1
elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66:
elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66 and not self.args.disable_cursor:
# 'DOWN' > Down in the server list
# if self.args.cursor_position < glances_processes.max_processes - 2:
if self.args.cursor_position < glances_processes.processes_count:

View File

@ -19,7 +19,7 @@ from glances.processes import glances_processes, sort_stats
from glances.outputs.glances_unicode import unicode_message
from glances.plugins.glances_core import Plugin as CorePlugin
from glances.plugins.glances_plugin import GlancesPlugin
from glances.programs import processes_to_programs
from glances.outputs.glances_bars import Bar
def seconds_to_hms(input_seconds):
@ -146,9 +146,10 @@ class Plugin(GlancesPlugin):
# Update stats using the standard system lib
# Note: Update is done in the processcount plugin
# Just return the processes list
stats = glances_processes.getlist()
if self.args.programs:
stats = processes_to_programs(stats)
stats = glances_processes.getlist(as_programs=True)
else:
stats = glances_processes.getlist()
elif self.input_method == 'snmp':
# No SNMP grab for processes
@ -353,11 +354,11 @@ class Plugin(GlancesPlugin):
- selected is a tag=True if p is the selected process
"""
ret = [self.curse_new_line()]
# When a process is selected:
# * display a special character at the beginning of the line
# * underline the command name
if args.is_standalone:
ret.append(self.curse_add_line(unicode_message('PROCESS_SELECTOR') if selected else ' ', 'SELECTED'))
ret.append(self.curse_add_line(unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED'))
# CPU
ret.append(self._get_process_curses_cpu(p, selected, args))
@ -404,7 +405,7 @@ class Plugin(GlancesPlugin):
cmdline = p.get('cmdline', '?')
try:
process_decoration = 'PROCESS_SELECTED' if (selected and args.is_standalone) else 'PROCESS'
process_decoration = 'PROCESS_SELECTED' if (selected and not args.disable_cursor) else 'PROCESS'
if cmdline:
path, cmd, arguments = split_cmdline(bare_process_name, cmdline)
# Manage end of line in arguments (see #1692)
@ -428,74 +429,14 @@ class Plugin(GlancesPlugin):
logger.debug("Can not decode command line '{}' ({})".format(cmdline, e))
ret.append(self.curse_add_line('', splittable=True))
# Add extended stats but only for the top processes
if args.cursor_position == 0 and 'extended_stats' in p and args.enable_process_extended:
# Left padding
xpad = ' ' * 13
# First line is CPU affinity
if 'cpu_affinity' in p and p['cpu_affinity'] is not None:
ret.append(self.curse_new_line())
msg = xpad + 'CPU affinity: ' + str(len(p['cpu_affinity'])) + ' cores'
ret.append(self.curse_add_line(msg, splittable=True))
# Second line is memory info
if 'memory_info' in p and p['memory_info'] is not None:
ret.append(self.curse_new_line())
msg = '{}Memory info: {}'.format(xpad, p['memory_info'])
if 'memory_swap' in p and p['memory_swap'] is not None:
msg += ' swap ' + self.auto_unit(p['memory_swap'], low_precision=False)
ret.append(self.curse_add_line(msg, splittable=True))
# Third line is for open files/network sessions
msg = ''
if 'num_threads' in p and p['num_threads'] is not None:
msg += str(p['num_threads']) + ' threads '
if 'num_fds' in p and p['num_fds'] is not None:
msg += str(p['num_fds']) + ' files '
if 'num_handles' in p and p['num_handles'] is not None:
msg += str(p['num_handles']) + ' handles '
if 'tcp' in p and p['tcp'] is not None:
msg += str(p['tcp']) + ' TCP '
if 'udp' in p and p['udp'] is not None:
msg += str(p['udp']) + ' UDP'
if msg != '':
ret.append(self.curse_new_line())
msg = xpad + 'Open: ' + msg
ret.append(self.curse_add_line(msg, splittable=True))
# Fourth line is IO nice level (only Linux and Windows OS)
if 'ionice' in p and p['ionice'] is not None and hasattr(p['ionice'], 'ioclass'):
ret.append(self.curse_new_line())
msg = xpad + 'IO nice: '
k = 'Class is '
v = p['ionice'].ioclass
# Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
# Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low).
if WINDOWS:
if v == 0:
msg += k + 'Very Low'
elif v == 1:
msg += k + 'Low'
elif v == 2:
msg += 'No specific I/O priority'
else:
msg += k + str(v)
else:
if v == 0:
msg += 'No specific I/O priority'
elif v == 1:
msg += k + 'Real Time'
elif v == 2:
msg += k + 'Best Effort'
elif v == 3:
msg += k + 'IDLE'
else:
msg += k + str(v)
# value is a number which goes from 0 to 7.
# The higher the value, the lower the I/O priority of the process.
if hasattr(p['ionice'], 'value') and p['ionice'].value != 0:
msg += ' (value %s/7)' % str(p['ionice'].value)
ret.append(self.curse_add_line(msg, splittable=True))
return ret
def is_selected_process(self, args):
return args.is_standalone and \
self.args.enable_process_extended and \
args.cursor_position is not None and \
glances_processes.extended_process is not None
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
@ -507,6 +448,16 @@ class Plugin(GlancesPlugin):
# Compute the sort key
process_sort_key = glances_processes.sort_key
processes_list_sorted = self.__sort_stats(process_sort_key)
# Display extended stats for selected process
#############################################
if self.is_selected_process(args):
self.__msg_curse_extended_process(ret, glances_processes.extended_process)
# Display others processes list
###############################
# Header
self.__msg_curse_header(ret, process_sort_key, args)
@ -515,10 +466,10 @@ class Plugin(GlancesPlugin):
# Loop over processes (sorted by the sort key previously compute)
# This is a Glances bottleneck (see flame graph),
# get_process_curses_data should be optimzed
i = 0
for p in self.__sort_stats(process_sort_key):
ret.extend(self.get_process_curses_data(p, i == args.cursor_position, args))
i += 1
for position, process in enumerate(processes_list_sorted):
ret.extend(self.get_process_curses_data(process,
position == args.cursor_position,
args))
# A filter is set Display the stats summaries
if glances_processes.process_filter is not None:
@ -532,6 +483,116 @@ class Plugin(GlancesPlugin):
# Return the message with decoration
return ret
def __msg_curse_extended_process(self, ret, p):
"""Get extended curses data for the selected process (see issue #2225)
The result depends of the process type (process or thread).
Input p is a dict with the following keys:
{'status': 'S',
'memory_info': pmem(rss=466890752, vms=3365347328, shared=68153344, text=659456, lib=0, data=774647808, dirty=0),
'pid': 4980,
'io_counters': [165385216, 0, 165385216, 0, 1],
'num_threads': 20,
'nice': 0,
'memory_percent': 5.958135664449709,
'cpu_percent': 0.0,
'gids': pgids(real=1000, effective=1000, saved=1000),
'cpu_times': pcputimes(user=696.38, system=119.98, children_user=0.0, children_system=0.0, iowait=0.0),
'name': 'WebExtensions',
'key': 'pid',
'time_since_update': 2.1997854709625244,
'cmdline': ['/snap/firefox/2154/usr/lib/firefox/firefox', '-contentproc', '-childID', '...'],
'username': 'nicolargo',
'cpu_min': 0.0,
'cpu_max': 7.0,
'cpu_mean': 3.2}
"""
if self.args.programs:
self.__msg_curse_extended_process_program(ret, p)
else:
self.__msg_curse_extended_process_thread(ret, p)
def __msg_curse_extended_process_program(self, ret, p):
# Title
msg = "Pinned program {} ('e' to unpin)".format(p['name'])
ret.append(self.curse_add_line(msg, "TITLE"))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
def __msg_curse_extended_process_thread(self, ret, p):
# Title
ret.append(self.curse_add_line("Pinned thread ", "TITLE"))
ret.append(self.curse_add_line(p['name'], "UNDERLINE"))
ret.append(self.curse_add_line(" ('e' to unpin)"))
# First line is CPU affinity
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' CPU Min/Max/Mean: '))
msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['cpu_min'], p['cpu_max'], p['cpu_mean'])
ret.append(self.curse_add_line(msg, decoration='INFO'))
if 'cpu_affinity' in p and p['cpu_affinity'] is not None:
ret.append(self.curse_add_line(' Affinity: '))
ret.append(self.curse_add_line(str(len(p['cpu_affinity'])), decoration='INFO'))
ret.append(self.curse_add_line(' cores', decoration='INFO'))
if 'ionice' in p and p['ionice'] is not None and hasattr(p['ionice'], 'ioclass'):
msg = ' IO nice: '
k = 'Class is '
v = p['ionice'].ioclass
# Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
# Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low).
if WINDOWS:
if v == 0:
msg += k + 'Very Low'
elif v == 1:
msg += k + 'Low'
elif v == 2:
msg += 'No specific I/O priority'
else:
msg += k + str(v)
else:
if v == 0:
msg += 'No specific I/O priority'
elif v == 1:
msg += k + 'Real Time'
elif v == 2:
msg += k + 'Best Effort'
elif v == 3:
msg += k + 'IDLE'
else:
msg += k + str(v)
# value is a number which goes from 0 to 7.
# The higher the value, the lower the I/O priority of the process.
if hasattr(p['ionice'], 'value') and p['ionice'].value != 0:
msg += ' (value %s/7)' % str(p['ionice'].value)
ret.append(self.curse_add_line(msg, splittable=True))
# Second line is memory info
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' MEM Min/Max/Mean: '))
msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['memory_min'], p['memory_max'], p['memory_mean'])
ret.append(self.curse_add_line(msg, decoration='INFO'))
if 'memory_info' in p and p['memory_info'] is not None:
ret.append(self.curse_add_line(' Memory info: '))
for k in p['memory_info']._asdict():
ret.append(self.curse_add_line(self.auto_unit(p['memory_info']._asdict()[k], low_precision=False), decoration='INFO', splittable=True))
ret.append(self.curse_add_line(' ' + k + ' ', splittable=True))
if 'memory_swap' in p and p['memory_swap'] is not None:
ret.append(self.curse_add_line(self.auto_unit(p['memory_swap'], low_precision=False), decoration='INFO', splittable=True))
ret.append(self.curse_add_line(' swap ', splittable=True))
# Third line is for open files/network sessions
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' Open: '))
for stat_prefix in ['num_threads', 'num_fds', 'num_handles', 'tcp', 'udp']:
if stat_prefix in p and p[stat_prefix] is not None:
ret.append(self.curse_add_line(str(p[stat_prefix]), decoration='INFO'))
ret.append(self.curse_add_line(' {} '.format(stat_prefix.replace('num_', ''))))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
def __msg_curse_header(self, ret, process_sort_key, args=None):
"""Build the header and add it to the ret dict."""
sort_style = 'SORT'
@ -578,10 +639,15 @@ class Plugin(GlancesPlugin):
msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
)
)
if not self.args.programs:
msg = self.layout_header['command'].format('Command', "('k' to kill)" if args.is_standalone else "")
if args.is_standalone and not args.disable_cursor:
if self.args.programs:
shortkey = "('k' to kill)"
else:
shortkey = "('e' to pin | 'k' to kill)"
else:
msg = self.layout_header['command'].format('Programs', "('k' to kill)" if args.is_standalone else "")
shortkey = ""
msg = self.layout_header['command'].format("Programs" if self.args.programs else "Command",
shortkey)
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT'))
def __msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None):

View File

@ -13,6 +13,7 @@ from glances.compat import iterkeys
from glances.globals import BSD, LINUX, MACOS, WINDOWS
from glances.timer import Timer, getTimeSinceLastUpdate
from glances.filter import GlancesFilter
from glances.programs import processes_to_programs
from glances.logger import logger
import psutil
@ -37,6 +38,10 @@ class GlancesProcesses(object):
def __init__(self, cache_timeout=60):
"""Init the class to collect stats about processes."""
# Init the args, coming from the GlancesStandalone class
# Should be set by the set_args method
self.args = None
# Add internals caches because psutil do not cache all the stats
# See: https://github.com/giampaolo/psutil/issues/462
self.username_cache = {}
@ -70,6 +75,7 @@ class GlancesProcesses(object):
# Extended stats for top process is enable by default
self.disable_extended_tag = False
self.extended_process = None
# Test if the system can grab io_counters
try:
@ -109,6 +115,10 @@ class GlancesProcesses(object):
self._max_values = {}
self.reset_max_values()
def set_args(self, args):
"""Set args."""
self.args = args
def reset_processcount(self):
"""Reset the global process count"""
self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None}
@ -247,6 +257,91 @@ class GlancesProcesses(object):
for k in self._max_values_list:
self._max_values[k] = 0.0
def get_extended_stats(self, proc):
"""Get the extended stats for the given PID."""
# - cpu_affinity (Linux, Windows, FreeBSD)
# - ionice (Linux and Windows > Vista)
# - num_ctx_switches (not available on Illumos/Solaris)
# - num_fds (Unix-like)
# - num_handles (Windows)
# - memory_maps (only swap, Linux)
# https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
# - connections (TCP and UDP)
# - CPU min/max/mean
# Set the extended stats list (OS dependant)
extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches']
if LINUX:
# num_fds only available on Unix system (see issue #1351)
extended_stats += ['num_fds']
if WINDOWS:
extended_stats += ['num_handles']
ret = {}
try:
# Get the extended stats
selected_process = psutil.Process(proc['pid'])
ret = selected_process.as_dict(attrs=extended_stats, ad_value=None)
if LINUX:
try:
ret['memory_swap'] = sum([v.swap for v in selected_process.memory_maps()])
except (psutil.NoSuchProcess, KeyError):
# (KeyError catch for issue #1551)
pass
except (psutil.AccessDenied, NotImplementedError):
# NotImplementedError: /proc/${PID}/smaps file doesn't exist
# on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
# is not enabled (see psutil #533/glances #413).
ret['memory_swap'] = None
try:
ret['tcp'] = len(selected_process.connections(kind="tcp"))
ret['udp'] = len(selected_process.connections(kind="udp"))
except (psutil.AccessDenied, psutil.NoSuchProcess):
# Manage issue1283 (psutil.AccessDenied)
ret['tcp'] = None
ret['udp'] = None
except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
logger.error('Can not grab extended stats ({})'.format(e))
self.extended_process = None
ret['extended_stats'] = False
else:
logger.debug('Grab extended stats for process {}'.format(proc['pid']))
# Compute CPU and MEM min/max/mean
for stat_prefix in ['cpu', 'memory']:
if stat_prefix + '_min' not in self.extended_process:
ret[stat_prefix + '_min'] = proc[stat_prefix + '_percent']
else:
ret[stat_prefix + '_min'] = proc[stat_prefix + '_percent'] if proc[stat_prefix + '_min'] > proc[stat_prefix + '_percent'] else proc[stat_prefix + '_min']
if stat_prefix + '_max' not in self.extended_process:
ret[stat_prefix + '_max'] = proc[stat_prefix + '_percent']
else:
ret[stat_prefix + '_max'] = proc[stat_prefix + '_percent'] if proc[stat_prefix + '_max'] < proc[stat_prefix + '_percent'] else proc[stat_prefix + '_max']
if stat_prefix + '_mean_sum' not in self.extended_process:
ret[stat_prefix + '_mean_sum'] = proc[stat_prefix + '_percent']
else:
ret[stat_prefix + '_mean_sum'] = proc[stat_prefix + '_mean_sum'] + proc[stat_prefix + '_percent']
if stat_prefix + '_mean_counter' not in self.extended_process:
ret[stat_prefix + '_mean_counter'] = 1
else:
ret[stat_prefix + '_mean_counter'] = proc[stat_prefix + '_mean_counter'] + 1
ret[stat_prefix + '_mean'] = ret[stat_prefix + '_mean_sum'] / ret[stat_prefix + '_mean_counter']
ret['extended_stats'] = True
return ret
def is_selected_extended_process(self, position):
"""Return True if the process is the selected one for extended stats."""
return hasattr(self.args, 'programs') and \
not self.args.programs and \
hasattr(self.args, 'enable_process_extended') and \
self.args.enable_process_extended and \
not self.disable_extended_tag and \
hasattr(self.args, 'cursor_position') and \
position == self.args.cursor_position and \
not self.args.disable_cursor
def update(self):
"""Update the processes stats."""
# Reset the stats
@ -305,59 +400,25 @@ class GlancesProcesses(object):
# Update the processcount
self.update_processcount(self.processlist)
# Loop over processes and add metadata
first = True
for proc in self.processlist:
# Get extended stats, only for top processes (see issue #403).
if first and not self.disable_extended_tag:
# - cpu_affinity (Linux, Windows, FreeBSD)
# - ionice (Linux and Windows > Vista)
# - num_ctx_switches (not available on Illumos/Solaris)
# - num_fds (Unix-like)
# - num_handles (Windows)
# - memory_maps (only swap, Linux)
# https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
# - connections (TCP and UDP)
extended = {}
try:
top_process = psutil.Process(proc['pid'])
extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches']
if LINUX:
# num_fds only available on Unix system (see issue #1351)
extended_stats += ['num_fds']
if WINDOWS:
extended_stats += ['num_handles']
# Loop over processes and :
# - add extended stats for selected process
# - add metadata
for position, proc in enumerate(self.processlist):
# Extended stats
################
# Get the extended stats
extended = top_process.as_dict(attrs=extended_stats, ad_value=None)
# Get the selected process when the 'e' key is pressed
if self.is_selected_extended_process(position):
self.extended_process = proc
if LINUX:
try:
extended['memory_swap'] = sum([v.swap for v in top_process.memory_maps()])
except (psutil.NoSuchProcess, KeyError):
# (KeyError catch for issue #1551)
pass
except (psutil.AccessDenied, NotImplementedError):
# NotImplementedError: /proc/${PID}/smaps file doesn't exist
# on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
# is not enabled (see psutil #533/glances #413).
extended['memory_swap'] = None
try:
extended['tcp'] = len(top_process.connections(kind="tcp"))
extended['udp'] = len(top_process.connections(kind="udp"))
except (psutil.AccessDenied, psutil.NoSuchProcess):
# Manage issue1283 (psutil.AccessDenied)
extended['tcp'] = None
extended['udp'] = None
except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
logger.error('Can not grab extended stats ({})'.format(e))
extended['extended_stats'] = False
else:
logger.debug('Grab extended stats for process {}'.format(proc['pid']))
extended['extended_stats'] = True
proc.update(extended)
first = False
# /End of extended stats
# Grab extended stats only for the selected process (see issue #2225)
if self.extended_process is not None and \
proc['pid'] == self.extended_process['pid']:
proc.update(self.get_extended_stats(self.extended_process))
self.extended_process = proc
# Meta data
###########
# PID is the key
proc['key'] = 'pid'
@ -424,9 +485,14 @@ class GlancesProcesses(object):
"""Get the number of processes."""
return self.processcount
def getlist(self, sorted_by=None):
"""Get the processlist."""
return self.processlist
def getlist(self, sorted_by=None, as_programs=False):
"""Get the processlist.
By default, return the list of threads.
If as_programs is True, return the list of programs."""
if as_programs:
return processes_to_programs(self.processlist)
else:
return self.processlist
@property
def sort_key(self):

View File

@ -50,6 +50,9 @@ class GlancesStandalone(object):
self.display_modules_list()
sys.exit(0)
# The args is needed to get the selected process in the process list (Curses mode)
glances_processes.set_args(args)
# If process extended stats is disabled by user
if not args.enable_process_extended:
logger.debug("Extended stats for top process are disabled")