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