1
|
|
|
# |
2
|
|
|
# This file is part of Glances. |
3
|
|
|
# |
4
|
|
|
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <[email protected]> |
5
|
|
|
# |
6
|
|
|
# SPDX-License-Identifier: LGPL-3.0-only |
7
|
|
|
# |
8
|
|
|
|
9
|
|
|
import os |
10
|
|
|
|
11
|
|
|
import psutil |
12
|
|
|
|
13
|
|
|
from glances.filter import GlancesFilter, GlancesFilterList |
14
|
|
|
from glances.globals import ( |
15
|
|
|
BSD, |
16
|
|
|
LINUX, |
17
|
|
|
MACOS, |
18
|
|
|
WINDOWS, |
19
|
|
|
dictlist_first_key_value, |
20
|
|
|
iterkeys, |
21
|
|
|
list_of_namedtuple_to_list_of_dict, |
22
|
|
|
namedtuple_to_dict, |
23
|
|
|
) |
24
|
|
|
from glances.logger import logger |
25
|
|
|
from glances.programs import processes_to_programs |
26
|
|
|
from glances.timer import Timer, getTimeSinceLastUpdate |
27
|
|
|
|
28
|
|
|
psutil_version_info = tuple([int(num) for num in psutil.__version__.split('.')]) |
29
|
|
|
|
30
|
|
|
# This constant defines the list of mandatory processes stats. Thoses stats can not be disabled by the user |
31
|
|
|
mandatory_processes_stats_list = ['pid', 'name'] |
32
|
|
|
|
33
|
|
|
# This constant defines the list of available processes sort key |
34
|
|
|
sort_processes_stats_list = ['cpu_percent', 'memory_percent', 'username', 'cpu_times', 'io_counters', 'name'] |
35
|
|
|
|
36
|
|
|
# Sort dictionary for human |
37
|
|
|
sort_for_human = { |
38
|
|
|
'io_counters': 'disk IO', |
39
|
|
|
'cpu_percent': 'CPU consumption', |
40
|
|
|
'memory_percent': 'memory consumption', |
41
|
|
|
'cpu_times': 'process time', |
42
|
|
|
'username': 'user name', |
43
|
|
|
'name': 'processs name', |
44
|
|
|
None: 'None', |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
|
48
|
|
|
class GlancesProcesses: |
49
|
|
|
"""Get processed stats using the psutil library.""" |
50
|
|
|
|
51
|
|
|
def __init__(self, cache_timeout=60): |
52
|
|
|
"""Init the class to collect stats about processes.""" |
53
|
|
|
# Init the args, coming from the classes derived from GlancesMode |
54
|
|
|
# Should be set by the set_args method |
55
|
|
|
self.args = None |
56
|
|
|
|
57
|
|
|
# The internals caches will be cleaned each 'cache_timeout' seconds |
58
|
|
|
self.cache_timeout = cache_timeout |
59
|
|
|
# First iteration, no cache |
60
|
|
|
self.cache_timer = Timer(0) |
61
|
|
|
|
62
|
|
|
# Init the io_old dict used to compute the IO bitrate |
63
|
|
|
# key = pid |
64
|
|
|
# value = [ read_bytes_old, write_bytes_old ] |
65
|
|
|
self.io_old = {} |
66
|
|
|
|
67
|
|
|
# Init stats |
68
|
|
|
self.auto_sort = None |
69
|
|
|
self._sort_key = None |
70
|
|
|
# Default processes sort key is 'auto' |
71
|
|
|
# Can be overwrite from the configuration file (issue#1536) => See glances_processlist.py init |
72
|
|
|
self.set_sort_key('auto', auto=True) |
73
|
|
|
self.processlist = [] |
74
|
|
|
self.reset_processcount() |
75
|
|
|
|
76
|
|
|
# Cache is a dict with key=pid and value = dict of cached value |
77
|
|
|
self.processlist_cache = {} |
78
|
|
|
|
79
|
|
|
# List of processes stats to export |
80
|
|
|
# Only process matching one of the filter will be exported |
81
|
|
|
self._filter_export = GlancesFilterList() |
82
|
|
|
self.processlist_export = [] |
83
|
|
|
|
84
|
|
|
# Tag to enable/disable the processes stats (to reduce the Glances CPU consumption) |
85
|
|
|
# Default is to enable the processes stats |
86
|
|
|
self.disable_tag = False |
87
|
|
|
|
88
|
|
|
# Extended stats for top process is enable by default |
89
|
|
|
self.disable_extended_tag = False |
90
|
|
|
self.extended_process = None |
91
|
|
|
|
92
|
|
|
# Tests (and disable if not available) optionals features |
93
|
|
|
self._test_grab() |
94
|
|
|
|
95
|
|
|
# Maximum number of processes showed in the UI (None if no limit) |
96
|
|
|
self._max_processes = None |
97
|
|
|
|
98
|
|
|
# Process filter |
99
|
|
|
self._filter = GlancesFilter() |
100
|
|
|
|
101
|
|
|
# Whether or not to hide kernel threads |
102
|
|
|
self.no_kernel_threads = False |
103
|
|
|
|
104
|
|
|
# Store maximums values in a dict |
105
|
|
|
# Used in the UI to highlight the maximum value |
106
|
|
|
self._max_values_list = ('cpu_percent', 'memory_percent') |
107
|
|
|
# { 'cpu_percent': 0.0, 'memory_percent': 0.0 } |
108
|
|
|
self._max_values = {} |
109
|
|
|
self.reset_max_values() |
110
|
|
|
|
111
|
|
|
# Set the key's list be disabled in order to only display specific attribute in the process list |
112
|
|
|
self.disable_stats = [] |
113
|
|
|
|
114
|
|
|
def _test_grab(self): |
115
|
|
|
"""Test somes optionals features""" |
116
|
|
|
# Test if the system can grab io_counters |
117
|
|
|
try: |
118
|
|
|
p = psutil.Process() |
119
|
|
|
p.io_counters() |
120
|
|
|
except Exception as e: |
121
|
|
|
logger.warning(f'PsUtil can not grab processes io_counters ({e})') |
122
|
|
|
self.disable_io_counters = True |
123
|
|
|
else: |
124
|
|
|
logger.debug('PsUtil can grab processes io_counters') |
125
|
|
|
self.disable_io_counters = False |
126
|
|
|
|
127
|
|
|
# Test if the system can grab gids |
128
|
|
|
try: |
129
|
|
|
p = psutil.Process() |
130
|
|
|
p.gids() |
131
|
|
|
except Exception as e: |
132
|
|
|
logger.warning(f'PsUtil can not grab processes gids ({e})') |
133
|
|
|
self.disable_gids = True |
134
|
|
|
else: |
135
|
|
|
logger.debug('PsUtil can grab processes gids') |
136
|
|
|
self.disable_gids = False |
137
|
|
|
|
138
|
|
|
def set_args(self, args): |
139
|
|
|
"""Set args.""" |
140
|
|
|
self.args = args |
141
|
|
|
|
142
|
|
|
def reset_internal_cache(self): |
143
|
|
|
"""Reset the internal cache.""" |
144
|
|
|
self.cache_timer = Timer(0) |
145
|
|
|
self.processlist_cache = {} |
146
|
|
|
if hasattr(psutil.process_iter, 'cache_clear'): |
147
|
|
|
# Cache clear only available in PsUtil 6 or higher |
148
|
|
|
psutil.process_iter.cache_clear() |
149
|
|
|
|
150
|
|
|
def reset_processcount(self): |
151
|
|
|
"""Reset the global process count""" |
152
|
|
|
self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None} |
153
|
|
|
|
154
|
|
|
def update_processcount(self, plist): |
155
|
|
|
"""Update the global process count from the current processes list""" |
156
|
|
|
# Update the maximum process ID (pid) number |
157
|
|
|
self.processcount['pid_max'] = self.pid_max |
158
|
|
|
# For each key in the processcount dict |
159
|
|
|
# count the number of processes with the same status |
160
|
|
|
for k in iterkeys(self.processcount): |
161
|
|
|
self.processcount[k] = len(list(filter(lambda v: v.get('status', '?') is k, plist))) |
|
|
|
|
162
|
|
|
# Compute thread |
163
|
|
|
try: |
164
|
|
|
self.processcount['thread'] = sum(i['num_threads'] for i in plist if i['num_threads'] is not None) |
165
|
|
|
except KeyError: |
166
|
|
|
self.processcount['thread'] = None |
167
|
|
|
# Compute total |
168
|
|
|
self.processcount['total'] = len(plist) |
169
|
|
|
|
170
|
|
|
def enable(self): |
171
|
|
|
"""Enable process stats.""" |
172
|
|
|
self.disable_tag = False |
173
|
|
|
self.update() |
174
|
|
|
|
175
|
|
|
def disable(self): |
176
|
|
|
"""Disable process stats.""" |
177
|
|
|
self.disable_tag = True |
178
|
|
|
|
179
|
|
|
def enable_extended(self): |
180
|
|
|
"""Enable extended process stats.""" |
181
|
|
|
self.disable_extended_tag = False |
182
|
|
|
self.update() |
183
|
|
|
|
184
|
|
|
def disable_extended(self): |
185
|
|
|
"""Disable extended process stats.""" |
186
|
|
|
self.disable_extended_tag = True |
187
|
|
|
|
188
|
|
|
@property |
189
|
|
|
def pid_max(self): |
190
|
|
|
""" |
191
|
|
|
Get the maximum PID value. |
192
|
|
|
|
193
|
|
|
On Linux, the value is read from the `/proc/sys/kernel/pid_max` file. |
194
|
|
|
|
195
|
|
|
From `man 5 proc`: |
196
|
|
|
The default value for this file, 32768, results in the same range of |
197
|
|
|
PIDs as on earlier kernels. On 32-bit platforms, 32768 is the maximum |
198
|
|
|
value for pid_max. On 64-bit systems, pid_max can be set to any value |
199
|
|
|
up to 2^22 (PID_MAX_LIMIT, approximately 4 million). |
200
|
|
|
|
201
|
|
|
If the file is unreadable or not available for whatever reason, |
202
|
|
|
returns None. |
203
|
|
|
|
204
|
|
|
Some other OSes: |
205
|
|
|
- On FreeBSD and macOS the maximum is 99999. |
206
|
|
|
- On OpenBSD >= 6.0 the maximum is 99999 (was 32766). |
207
|
|
|
- On NetBSD the maximum is 30000. |
208
|
|
|
|
209
|
|
|
:returns: int or None |
210
|
|
|
""" |
211
|
|
|
if LINUX: |
212
|
|
|
# XXX: waiting for https://github.com/giampaolo/psutil/issues/720 |
213
|
|
|
try: |
214
|
|
|
with open('/proc/sys/kernel/pid_max', 'rb') as f: |
215
|
|
|
return int(f.read()) |
216
|
|
|
except OSError: |
217
|
|
|
return None |
218
|
|
|
else: |
219
|
|
|
return None |
220
|
|
|
|
221
|
|
|
@property |
222
|
|
|
def processes_count(self): |
223
|
|
|
"""Get the current number of processes showed in the UI.""" |
224
|
|
|
return min(self._max_processes - 2, glances_processes.processcount['total'] - 1) |
225
|
|
|
|
226
|
|
|
@property |
227
|
|
|
def max_processes(self): |
228
|
|
|
"""Get the maximum number of processes showed in the UI.""" |
229
|
|
|
return self._max_processes |
230
|
|
|
|
231
|
|
|
@max_processes.setter |
232
|
|
|
def max_processes(self, value): |
233
|
|
|
"""Set the maximum number of processes showed in the UI.""" |
234
|
|
|
self._max_processes = value |
235
|
|
|
|
236
|
|
|
@property |
237
|
|
|
def disable_stats(self): |
238
|
|
|
"""Set disable_stats list""" |
239
|
|
|
return self._disable_stats |
240
|
|
|
|
241
|
|
|
@disable_stats.setter |
242
|
|
|
def disable_stats(self, stats_list): |
243
|
|
|
"""Set disable_stats list""" |
244
|
|
|
self._disable_stats = [i for i in stats_list if i not in mandatory_processes_stats_list] |
245
|
|
|
|
246
|
|
|
@property |
247
|
|
|
def process_filter_input(self): |
248
|
|
|
"""Get the process filter (given by the user).""" |
249
|
|
|
return self._filter.filter_input |
250
|
|
|
|
251
|
|
|
@property |
252
|
|
|
def process_filter(self): |
253
|
|
|
"""Get the process filter (current apply filter).""" |
254
|
|
|
return self._filter.filter |
255
|
|
|
|
256
|
|
|
@process_filter.setter |
257
|
|
|
def process_filter(self, value): |
258
|
|
|
"""Set the process filter.""" |
259
|
|
|
self._filter.filter = value |
260
|
|
|
|
261
|
|
|
@property |
262
|
|
|
def process_filter_key(self): |
263
|
|
|
"""Get the process filter key.""" |
264
|
|
|
return self._filter.filter_key |
265
|
|
|
|
266
|
|
|
@property |
267
|
|
|
def process_filter_re(self): |
268
|
|
|
"""Get the process regular expression compiled.""" |
269
|
|
|
return self._filter.filter_re |
270
|
|
|
|
271
|
|
|
# Export filter |
272
|
|
|
|
273
|
|
|
@property |
274
|
|
|
def export_process_filter(self): |
275
|
|
|
"""Get the export process filter (current export process filter list).""" |
276
|
|
|
return self._filter_export.filter |
277
|
|
|
|
278
|
|
|
@export_process_filter.setter |
279
|
|
|
def export_process_filter(self, value): |
280
|
|
|
"""Set the export process filter list.""" |
281
|
|
|
self._filter_export.filter = value |
282
|
|
|
|
283
|
|
|
# Kernel threads |
284
|
|
|
|
285
|
|
|
def disable_kernel_threads(self): |
286
|
|
|
"""Ignore kernel threads in process list.""" |
287
|
|
|
self.no_kernel_threads = True |
288
|
|
|
|
289
|
|
|
@property |
290
|
|
|
def sort_reverse(self): |
291
|
|
|
"""Return True to sort processes in reverse 'key' order, False instead.""" |
292
|
|
|
if self.sort_key == 'name' or self.sort_key == 'username': |
293
|
|
|
return False |
294
|
|
|
|
295
|
|
|
return True |
296
|
|
|
|
297
|
|
|
def max_values(self): |
298
|
|
|
"""Return the max values dict.""" |
299
|
|
|
return self._max_values |
300
|
|
|
|
301
|
|
|
def get_max_values(self, key): |
302
|
|
|
"""Get the maximum values of the given stat (key).""" |
303
|
|
|
return self._max_values[key] |
304
|
|
|
|
305
|
|
|
def set_max_values(self, key, value): |
306
|
|
|
"""Set the maximum value for a specific stat (key).""" |
307
|
|
|
self._max_values[key] = value |
308
|
|
|
|
309
|
|
|
def reset_max_values(self): |
310
|
|
|
"""Reset the maximum values dict.""" |
311
|
|
|
self._max_values = {} |
312
|
|
|
for k in self._max_values_list: |
313
|
|
|
self._max_values[k] = 0.0 |
314
|
|
|
|
315
|
|
|
def set_extended_stats(self, proc): |
316
|
|
|
"""Set the extended stats for the given PID.""" |
317
|
|
|
# - cpu_affinity (Linux, Windows, FreeBSD) |
318
|
|
|
# - ionice (Linux and Windows > Vista) |
319
|
|
|
# - num_ctx_switches (not available on Illumos/Solaris) |
320
|
|
|
# - num_fds (Unix-like) |
321
|
|
|
# - num_handles (Windows) |
322
|
|
|
# - memory_maps (only swap, Linux) |
323
|
|
|
# https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/ |
324
|
|
|
# - connections (TCP and UDP) |
325
|
|
|
# - CPU min/max/mean |
326
|
|
|
|
327
|
|
|
# Set the extended stats list (OS dependent) |
328
|
|
|
extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches'] |
329
|
|
|
if LINUX: |
330
|
|
|
# num_fds only available on Unix system (see issue #1351) |
331
|
|
|
extended_stats += ['num_fds'] |
332
|
|
|
if WINDOWS: |
333
|
|
|
extended_stats += ['num_handles'] |
334
|
|
|
|
335
|
|
|
ret = {} |
336
|
|
|
try: |
337
|
|
|
logger.debug('Grab extended stats for process {}'.format(proc['pid'])) |
338
|
|
|
|
339
|
|
|
# Get PID of the selected process |
340
|
|
|
selected_process = psutil.Process(proc['pid']) |
341
|
|
|
|
342
|
|
|
# Get the extended stats for the selected process |
343
|
|
|
ret = selected_process.as_dict(attrs=extended_stats, ad_value=None) |
344
|
|
|
|
345
|
|
|
# Get memory swap for the selected process (Linux Only) |
346
|
|
|
ret['memory_swap'] = self.__get_extended_memory_swap(selected_process) |
347
|
|
|
|
348
|
|
|
# Get number of TCP and UDP network connections for the selected process |
349
|
|
|
ret['tcp'], ret['udp'] = self.__get_extended_connections(selected_process) |
350
|
|
|
except (psutil.NoSuchProcess, ValueError, AttributeError) as e: |
351
|
|
|
logger.error(f'Can not grab extended stats ({e})') |
352
|
|
|
self.extended_process = None |
353
|
|
|
ret['extended_stats'] = False |
354
|
|
|
else: |
355
|
|
|
# Compute CPU and MEM min/max/mean |
356
|
|
|
# Merge the returned dict with the current on |
357
|
|
|
ret.update(self.__get_min_max_mean(proc)) |
358
|
|
|
self.extended_process = ret |
359
|
|
|
ret['extended_stats'] = True |
360
|
|
|
return namedtuple_to_dict(ret) |
361
|
|
|
|
362
|
|
|
def get_extended_stats(self): |
363
|
|
|
"""Return the extended stats. |
364
|
|
|
|
365
|
|
|
Return the process stat when extended_stats = True |
366
|
|
|
""" |
367
|
|
|
for p in self.processlist: |
368
|
|
|
if p.get('extended_stats'): |
369
|
|
|
return p |
370
|
|
|
return None |
371
|
|
|
|
372
|
|
|
def __get_min_max_mean(self, proc, prefix=['cpu', 'memory']): |
373
|
|
|
"""Return the min/max/mean for the given process""" |
374
|
|
|
ret = {} |
375
|
|
|
for stat_prefix in prefix: |
376
|
|
|
min_key = stat_prefix + '_min' |
377
|
|
|
max_key = stat_prefix + '_max' |
378
|
|
|
mean_sum_key = stat_prefix + '_mean_sum' |
379
|
|
|
mean_counter_key = stat_prefix + '_mean_counter' |
380
|
|
|
if min_key not in self.extended_process: |
381
|
|
|
ret[min_key] = proc[stat_prefix + '_percent'] |
382
|
|
|
else: |
383
|
|
|
ret[min_key] = min(proc[stat_prefix + '_percent'], self.extended_process[min_key]) |
384
|
|
|
if max_key not in self.extended_process: |
385
|
|
|
ret[max_key] = proc[stat_prefix + '_percent'] |
386
|
|
|
else: |
387
|
|
|
ret[max_key] = max(proc[stat_prefix + '_percent'], self.extended_process[max_key]) |
388
|
|
|
if mean_sum_key not in self.extended_process: |
389
|
|
|
ret[mean_sum_key] = proc[stat_prefix + '_percent'] |
390
|
|
|
else: |
391
|
|
|
ret[mean_sum_key] = self.extended_process[mean_sum_key] + proc[stat_prefix + '_percent'] |
392
|
|
|
if mean_counter_key not in self.extended_process: |
393
|
|
|
ret[mean_counter_key] = 1 |
394
|
|
|
else: |
395
|
|
|
ret[mean_counter_key] = self.extended_process[mean_counter_key] + 1 |
396
|
|
|
ret[stat_prefix + '_mean'] = ret[mean_sum_key] / ret[mean_counter_key] |
397
|
|
|
return ret |
398
|
|
|
|
399
|
|
|
def __get_extended_memory_swap(self, process): |
400
|
|
|
"""Return the memory swap for the given process""" |
401
|
|
|
if not LINUX: |
402
|
|
|
return None |
403
|
|
|
try: |
404
|
|
|
memory_swap = sum([v.swap for v in process.memory_maps()]) |
405
|
|
|
except (psutil.NoSuchProcess, KeyError): |
406
|
|
|
# (KeyError catch for issue #1551) |
407
|
|
|
pass |
408
|
|
|
except (psutil.AccessDenied, NotImplementedError): |
409
|
|
|
# NotImplementedError: /proc/${PID}/smaps file doesn't exist |
410
|
|
|
# on kernel < 2.6.14 or CONFIG_MMU kernel configuration option |
411
|
|
|
# is not enabled (see psutil #533/glances #413). |
412
|
|
|
memory_swap = None |
413
|
|
|
return memory_swap |
414
|
|
|
|
415
|
|
|
def __get_extended_connections(self, process): |
416
|
|
|
"""Return a tuple with (tcp, udp) connections count |
417
|
|
|
The code is compliant with both PsUtil<6 and Psutil>=6 |
418
|
|
|
""" |
419
|
|
|
try: |
420
|
|
|
# Hack for issue #2754 (PsUtil 6+) |
421
|
|
|
if psutil_version_info[0] >= 6: |
422
|
|
|
tcp = len(process.net_connections(kind="tcp")) |
423
|
|
|
udp = len(process.net_connections(kind="udp")) |
424
|
|
|
else: |
425
|
|
|
tcp = len(process.connections(kind="tcp")) |
426
|
|
|
udp = len(process.connections(kind="udp")) |
427
|
|
|
except (psutil.AccessDenied, psutil.NoSuchProcess): |
428
|
|
|
# Manage issue1283 (psutil.AccessDenied) |
429
|
|
|
tcp = None |
430
|
|
|
udp = None |
431
|
|
|
return tcp, udp |
432
|
|
|
|
433
|
|
|
def is_selected_extended_process(self, position): |
434
|
|
|
"""Return True if the process is the selected one for extended stats.""" |
435
|
|
|
return ( |
436
|
|
|
hasattr(self.args, 'programs') |
437
|
|
|
and not self.args.programs |
438
|
|
|
and hasattr(self.args, 'enable_process_extended') |
439
|
|
|
and self.args.enable_process_extended |
440
|
|
|
and not self.disable_extended_tag |
441
|
|
|
and hasattr(self.args, 'cursor_position') |
442
|
|
|
and position == self.args.cursor_position |
443
|
|
|
and not self.args.disable_cursor |
444
|
|
|
) |
445
|
|
|
|
446
|
|
|
def build_process_list(self, sorted_attrs): |
447
|
|
|
# Build the processes stats list (it is why we need psutil>=5.3.0) (see issue #2755) |
448
|
|
|
processlist = list( |
449
|
|
|
filter( |
450
|
|
|
lambda p: not (BSD and p.info['name'] == 'idle') |
451
|
|
|
and not (WINDOWS and p.info['name'] == 'System Idle Process') |
452
|
|
|
and not (MACOS and p.info['name'] == 'kernel_task') |
453
|
|
|
and not (self.no_kernel_threads and LINUX and p.info['gids'].real == 0), |
454
|
|
|
psutil.process_iter(attrs=sorted_attrs, ad_value=None), |
455
|
|
|
) |
456
|
|
|
) |
457
|
|
|
|
458
|
|
|
# Only get the info key |
459
|
|
|
# PsUtil 6+ no longer check PID reused #2755 so use is_running in the loop |
460
|
|
|
# Note: not sure it is realy needed but CPU consumption look the same with or without it |
461
|
|
|
processlist = [p.info for p in processlist if p.is_running()] |
462
|
|
|
|
463
|
|
|
# Sort the processes list by the current sort_key |
464
|
|
|
return sort_stats(processlist, sorted_by=self.sort_key, reverse=True) |
465
|
|
|
|
466
|
|
|
def get_sorted_attrs(self): |
467
|
|
|
defaults = ['cpu_percent', 'cpu_times', 'memory_percent', 'name', 'status', 'num_threads'] |
468
|
|
|
optional = ['io_counters'] if not self.disable_io_counters else [] |
469
|
|
|
|
470
|
|
|
return defaults + optional |
471
|
|
|
|
472
|
|
|
def get_displayed_attr(self): |
473
|
|
|
defaults = ['memory_info', 'nice', 'pid'] |
474
|
|
|
optional = ['gids'] if not self.disable_gids else [] |
475
|
|
|
|
476
|
|
|
return defaults + optional |
477
|
|
|
|
478
|
|
|
def get_cached_attrs(self): |
479
|
|
|
return ['cmdline', 'username'] |
480
|
|
|
|
481
|
|
|
def maybe_add_cached_attrs(self, sorted_attrs, cached_attrs): |
482
|
|
|
# Some stats are not sort key |
483
|
|
|
# An optimisation can be done be only grabbed displayed_attr |
484
|
|
|
# for displayed processes (but only in standalone mode...) |
485
|
|
|
sorted_attrs.extend(self.get_displayed_attr()) |
486
|
|
|
# Some stats are cached (not necessary to be refreshed every time) |
487
|
|
|
if self.cache_timer.finished(): |
488
|
|
|
sorted_attrs += cached_attrs |
489
|
|
|
self.cache_timer.set(self.cache_timeout) |
490
|
|
|
self.cache_timer.reset() |
491
|
|
|
is_cached = False |
492
|
|
|
else: |
493
|
|
|
is_cached = True |
494
|
|
|
|
495
|
|
|
return is_cached, sorted_attrs |
496
|
|
|
|
497
|
|
|
def get_pid_time_and_status(self, time_since_update, proc): |
498
|
|
|
# PID is the key |
499
|
|
|
proc['key'] = 'pid' |
500
|
|
|
|
501
|
|
|
# Time since last update (for disk_io rate computation) |
502
|
|
|
proc['time_since_update'] = time_since_update |
503
|
|
|
|
504
|
|
|
# Process status (only keep the first char) |
505
|
|
|
proc['status'] = str(proc.get('status', '?'))[:1].upper() |
506
|
|
|
|
507
|
|
|
return proc |
508
|
|
|
|
509
|
|
|
def get_io_counters(self, proc): |
510
|
|
|
# procstat['io_counters'] is a list: |
511
|
|
|
# [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] |
512
|
|
|
# If io_tag = 0 > Access denied or first time (display "?") |
513
|
|
|
# If io_tag = 1 > No access denied (display the IO rate) |
514
|
|
|
if 'io_counters' in proc and proc['io_counters'] is not None: |
515
|
|
|
io_new = [proc['io_counters'][2], proc['io_counters'][3]] |
516
|
|
|
# For IO rate computation |
517
|
|
|
# Append saved IO r/w bytes |
518
|
|
|
try: |
519
|
|
|
proc['io_counters'] = io_new + self.io_old[proc['pid']] |
520
|
|
|
io_tag = 1 |
521
|
|
|
except KeyError: |
522
|
|
|
proc['io_counters'] = io_new + [0, 0] |
523
|
|
|
io_tag = 0 |
524
|
|
|
# then save the IO r/w bytes |
525
|
|
|
self.io_old[proc['pid']] = io_new |
526
|
|
|
else: |
527
|
|
|
proc['io_counters'] = [0, 0] + [0, 0] |
528
|
|
|
io_tag = 0 |
529
|
|
|
# Append the IO tag (for display) |
530
|
|
|
proc['io_counters'] += [io_tag] |
531
|
|
|
|
532
|
|
|
return proc |
533
|
|
|
|
534
|
|
|
def maybe_add_cached_stats(self, is_cached, cached_attrs, proc): |
535
|
|
|
if is_cached: |
536
|
|
|
# Grab cached values (in case of a new incoming process) |
537
|
|
|
if proc['pid'] not in self.processlist_cache: |
538
|
|
|
try: |
539
|
|
|
self.processlist_cache[proc['pid']] = psutil.Process(pid=proc['pid']).as_dict( |
540
|
|
|
attrs=cached_attrs, ad_value=None |
541
|
|
|
) |
542
|
|
|
except psutil.NoSuchProcess: |
543
|
|
|
pass |
544
|
|
|
# Add cached value to current stat |
545
|
|
|
try: |
546
|
|
|
proc.update(self.processlist_cache[proc['pid']]) |
547
|
|
|
except KeyError: |
548
|
|
|
pass |
549
|
|
|
else: |
550
|
|
|
# Save values to cache |
551
|
|
|
try: |
552
|
|
|
self.processlist_cache[proc['pid']] = {cached: proc[cached] for cached in cached_attrs} |
553
|
|
|
except KeyError: |
554
|
|
|
pass |
555
|
|
|
|
556
|
|
|
return proc |
557
|
|
|
|
558
|
|
|
def update(self): |
559
|
|
|
"""Update the processes stats.""" |
560
|
|
|
# Init new processes stats |
561
|
|
|
processlist = [] |
562
|
|
|
|
563
|
|
|
# Do not process if disable tag is set |
564
|
|
|
if self.disable_tag: |
565
|
|
|
return processlist |
566
|
|
|
|
567
|
|
|
# Time since last update (for disk_io rate computation) |
568
|
|
|
time_since_update = getTimeSinceLastUpdate('process_disk') |
569
|
|
|
|
570
|
|
|
# Grab standard stats |
571
|
|
|
##################### |
572
|
|
|
sorted_attrs = self.get_sorted_attrs() |
573
|
|
|
|
574
|
|
|
# The following attributes are cached and only retrieve every self.cache_timeout seconds |
575
|
|
|
# Warning: 'name' can not be cached because it is used for filtering |
576
|
|
|
cached_attrs = self.get_cached_attrs() |
577
|
|
|
|
578
|
|
|
is_cached, sorted_attrs = self.maybe_add_cached_attrs(sorted_attrs, cached_attrs) |
579
|
|
|
|
580
|
|
|
# Remove attributes set by the user in the config file (see #1524) |
581
|
|
|
sorted_attrs = [i for i in sorted_attrs if i not in self.disable_stats] |
582
|
|
|
processlist = self.build_process_list(sorted_attrs) |
583
|
|
|
|
584
|
|
|
# Update the processcount |
585
|
|
|
self.update_processcount(processlist) |
586
|
|
|
|
587
|
|
|
# Loop over processes and : |
588
|
|
|
# - add extended stats for selected process |
589
|
|
|
# - add metadata |
590
|
|
|
for position, proc in enumerate(processlist): |
591
|
|
|
# Extended stats |
592
|
|
|
################ |
593
|
|
|
|
594
|
|
|
# Get the selected process when the 'e' key is pressed |
595
|
|
|
if self.is_selected_extended_process(position): |
596
|
|
|
self.extended_process = proc |
597
|
|
|
|
598
|
|
|
# Grab extended stats only for the selected process (see issue #2225) |
599
|
|
|
if self.extended_process is not None and proc['pid'] == self.extended_process['pid']: |
600
|
|
|
proc.update(self.set_extended_stats(self.extended_process)) |
601
|
|
|
self.extended_process = namedtuple_to_dict(proc) |
602
|
|
|
|
603
|
|
|
# Meta data |
604
|
|
|
########### |
605
|
|
|
proc = self.get_pid_time_and_status(time_since_update, proc) |
606
|
|
|
|
607
|
|
|
# Process IO |
608
|
|
|
proc = self.get_io_counters(proc) |
609
|
|
|
|
610
|
|
|
# Manage cached information |
611
|
|
|
proc = self.maybe_add_cached_stats(is_cached, cached_attrs, proc) |
612
|
|
|
|
613
|
|
|
# Remove non running process from the cache (avoid issue #2976) |
614
|
|
|
self.remove_non_running_procs(processlist) |
615
|
|
|
|
616
|
|
|
# Filter and transform process export list |
617
|
|
|
self.processlist_export = self.update_export_list(processlist) |
618
|
|
|
|
619
|
|
|
# Filter and transform process list |
620
|
|
|
processlist = self.update_list(processlist) |
621
|
|
|
|
622
|
|
|
# Compute the maximum value for keys in self._max_values_list: CPU, MEM |
623
|
|
|
# Useful to highlight the processes with maximum values |
624
|
|
|
self.compute_max_value(processlist) |
625
|
|
|
|
626
|
|
|
# Update the stats |
627
|
|
|
self.processlist = processlist |
628
|
|
|
|
629
|
|
|
return self.processlist |
630
|
|
|
|
631
|
|
|
def compute_max_value(self, processlist): |
632
|
|
|
for k in [i for i in self._max_values_list if i not in self.disable_stats]: |
633
|
|
|
values_list = [i[k] for i in processlist if i[k] is not None] |
634
|
|
|
if values_list: |
635
|
|
|
self.set_max_values(k, max(values_list)) |
636
|
|
|
|
637
|
|
|
def remove_non_running_procs(self, processlist): |
638
|
|
|
pids_running = [p['pid'] for p in processlist] |
639
|
|
|
pids_cached = list(self.processlist_cache.keys()).copy() |
640
|
|
|
for pid in pids_cached: |
641
|
|
|
if pid not in pids_running: |
642
|
|
|
self.processlist_cache.pop(pid, None) |
643
|
|
|
|
644
|
|
|
def update_list(self, processlist): |
645
|
|
|
"""Return the process list after filtering and transformation (namedtuple to dict).""" |
646
|
|
|
if self._filter.filter is None: |
647
|
|
|
return list_of_namedtuple_to_list_of_dict(processlist) |
648
|
|
|
ret = list(filter(lambda p: self._filter.is_filtered(p), processlist)) |
649
|
|
|
return list_of_namedtuple_to_list_of_dict(ret) |
650
|
|
|
|
651
|
|
|
def update_export_list(self, processlist): |
652
|
|
|
"""Return the process export list after filtering and transformation (namedtuple to dict).""" |
653
|
|
|
if self._filter_export.filter == []: |
654
|
|
|
return [] |
655
|
|
|
ret = list(filter(lambda p: self._filter_export.is_filtered(p), processlist)) |
656
|
|
|
return list_of_namedtuple_to_list_of_dict(ret) |
657
|
|
|
|
658
|
|
|
def get_count(self): |
659
|
|
|
"""Get the number of processes.""" |
660
|
|
|
return self.processcount |
661
|
|
|
|
662
|
|
|
def get_list(self, sorted_by=None, as_programs=False): |
663
|
|
|
"""Get the processlist. |
664
|
|
|
By default, return the list of threads. |
665
|
|
|
If as_programs is True, return the list of programs.""" |
666
|
|
|
if as_programs: |
667
|
|
|
return processes_to_programs(self.processlist) |
668
|
|
|
return self.processlist |
669
|
|
|
|
670
|
|
|
def get_export(self): |
671
|
|
|
"""Return the processlist for export.""" |
672
|
|
|
return self.processlist_export |
673
|
|
|
|
674
|
|
|
def get_stats(self, pid): |
675
|
|
|
"""Get stats for the given pid.""" |
676
|
|
|
return dictlist_first_key_value(self.processlist, 'pid', pid) |
677
|
|
|
|
678
|
|
|
@property |
679
|
|
|
def sort_key(self): |
680
|
|
|
"""Get the current sort key.""" |
681
|
|
|
return self._sort_key |
682
|
|
|
|
683
|
|
|
def set_sort_key(self, key, auto=True): |
684
|
|
|
"""Set the current sort key.""" |
685
|
|
|
if key == 'auto': |
686
|
|
|
self.auto_sort = True |
687
|
|
|
self._sort_key = 'cpu_percent' |
688
|
|
|
else: |
689
|
|
|
self.auto_sort = auto |
690
|
|
|
self._sort_key = key |
691
|
|
|
|
692
|
|
|
def nice_decrease(self, pid): |
693
|
|
|
"""Decrease nice level |
694
|
|
|
On UNIX this is a number which usually goes from -20 to 20. |
695
|
|
|
The higher the nice value, the lower the priority of the process.""" |
696
|
|
|
p = psutil.Process(pid) |
697
|
|
|
try: |
698
|
|
|
p.nice(p.nice() - 1) |
699
|
|
|
logger.info(f'Set nice level of process {pid} to {p.nice()} (higher the priority)') |
700
|
|
|
except psutil.AccessDenied: |
701
|
|
|
logger.warning(f'Can not decrease (higher the priority) the nice level of process {pid} (access denied)') |
702
|
|
|
|
703
|
|
|
def nice_increase(self, pid): |
704
|
|
|
"""Increase nice level |
705
|
|
|
On UNIX this is a number which usually goes from -20 to 20. |
706
|
|
|
The higher the nice value, the lower the priority of the process.""" |
707
|
|
|
p = psutil.Process(pid) |
708
|
|
|
try: |
709
|
|
|
p.nice(p.nice() + 1) |
710
|
|
|
logger.info(f'Set nice level of process {pid} to {p.nice()} (lower the priority)') |
711
|
|
|
except psutil.AccessDenied: |
712
|
|
|
logger.warning(f'Can not increase (lower the priority) the nice level of process {pid} (access denied)') |
713
|
|
|
|
714
|
|
|
def kill(self, pid, timeout=3): |
715
|
|
|
"""Kill process with pid""" |
716
|
|
|
assert pid != os.getpid(), "Glances can kill itself..." |
717
|
|
|
p = psutil.Process(pid) |
718
|
|
|
logger.debug(f'Send kill signal to process: {p}') |
719
|
|
|
p.kill() |
720
|
|
|
return p.wait(timeout) |
721
|
|
|
|
722
|
|
|
|
723
|
|
|
def weighted(value): |
724
|
|
|
"""Manage None value in dict value.""" |
725
|
|
|
return -float('inf') if value is None else value |
726
|
|
|
|
727
|
|
|
|
728
|
|
|
def _sort_io_counters(process, sorted_by='io_counters', sorted_by_secondary='memory_percent'): |
729
|
|
|
"""Specific case for io_counters |
730
|
|
|
|
731
|
|
|
:return: Sum of io_r + io_w |
732
|
|
|
""" |
733
|
|
|
return process[sorted_by][0] - process[sorted_by][2] + process[sorted_by][1] - process[sorted_by][3] |
734
|
|
|
|
735
|
|
|
|
736
|
|
|
def _sort_cpu_times(process, sorted_by='cpu_times', sorted_by_secondary='memory_percent'): |
737
|
|
|
"""Specific case for cpu_times |
738
|
|
|
|
739
|
|
|
Patch for "Sorting by process time works not as expected #1321" |
740
|
|
|
By default PsUtil only takes user time into account |
741
|
|
|
see (https://github.com/giampaolo/psutil/issues/1339) |
742
|
|
|
The following implementation takes user and system time into account |
743
|
|
|
""" |
744
|
|
|
return process[sorted_by][0] + process[sorted_by][1] |
745
|
|
|
|
746
|
|
|
|
747
|
|
|
def _sort_lambda(sorted_by='cpu_percent', sorted_by_secondary='memory_percent'): |
748
|
|
|
"""Return a sort lambda function for the sorted_by key""" |
749
|
|
|
ret = None |
750
|
|
|
if sorted_by == 'io_counters': |
751
|
|
|
ret = _sort_io_counters |
752
|
|
|
elif sorted_by == 'cpu_times': |
753
|
|
|
ret = _sort_cpu_times |
754
|
|
|
return ret |
755
|
|
|
|
756
|
|
|
|
757
|
|
|
def sort_stats(stats, sorted_by='cpu_percent', sorted_by_secondary='memory_percent', reverse=True): |
758
|
|
|
"""Return the stats (dict) sorted by (sorted_by). |
759
|
|
|
A secondary sort key should be specified. |
760
|
|
|
|
761
|
|
|
Reverse the sort if reverse is True. |
762
|
|
|
""" |
763
|
|
|
if sorted_by is None and sorted_by_secondary is None: |
764
|
|
|
# No need to sort... |
765
|
|
|
return stats |
766
|
|
|
|
767
|
|
|
# Check if a specific sort should be done |
768
|
|
|
sort_lambda = _sort_lambda(sorted_by=sorted_by, sorted_by_secondary=sorted_by_secondary) |
769
|
|
|
|
770
|
|
|
if sort_lambda is not None: |
771
|
|
|
# Specific sort |
772
|
|
|
try: |
773
|
|
|
stats.sort(key=sort_lambda, reverse=reverse) |
774
|
|
|
except Exception: |
775
|
|
|
# If an error is detected, fallback to cpu_percent |
776
|
|
|
stats.sort( |
777
|
|
|
key=lambda process: (weighted(process['cpu_percent']), weighted(process[sorted_by_secondary])), |
778
|
|
|
reverse=reverse, |
779
|
|
|
) |
780
|
|
|
else: |
781
|
|
|
# Standard sort |
782
|
|
|
try: |
783
|
|
|
stats.sort( |
784
|
|
|
key=lambda process: (weighted(process[sorted_by]), weighted(process[sorted_by_secondary])), |
785
|
|
|
reverse=reverse, |
786
|
|
|
) |
787
|
|
|
except (KeyError, TypeError): |
788
|
|
|
# Fallback to name |
789
|
|
|
stats.sort(key=lambda process: process['name'] if process['name'] is not None else '~', reverse=False) |
790
|
|
|
|
791
|
|
|
return stats |
792
|
|
|
|
793
|
|
|
|
794
|
|
|
glances_processes = GlancesProcesses() |
795
|
|
|
|