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