glances.processes   F
last analyzed

Complexity

Total Complexity 111

Size/Duplication

Total Lines 700
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 362
dl 0
loc 700
rs 2
c 0
b 0
f 0
wmc 111

38 Methods

Rating   Name   Duplication   Size   Complexity  
A GlancesProcesses.get_count() 0 3 1
A GlancesProcesses.processes_count() 0 4 1
A GlancesProcesses.is_selected_extended_process() 0 11 1
A GlancesProcesses.export_process_filter() 0 4 1
A GlancesProcesses.process_filter_input() 0 4 1
A GlancesProcesses.process_filter() 0 4 1
A GlancesProcesses.update_list() 0 6 3
B GlancesProcesses.__get_min_max_mean() 0 26 6
B GlancesProcesses.get_extended_stats() 0 46 5
A GlancesProcesses.max_processes() 0 4 1
A GlancesProcesses.max_values() 0 3 1
A GlancesProcesses.set_sort_key() 0 8 2
A GlancesProcesses.set_max_values() 0 3 1
A GlancesProcesses.enable_extended() 0 4 1
A GlancesProcesses.nice_decrease() 0 11 2
F GlancesProcesses.update() 0 148 20
A GlancesProcesses.disable() 0 3 1
A GlancesProcesses.process_filter_re() 0 4 1
A GlancesProcesses.sort_key() 0 4 1
A GlancesProcesses.__get_extended_memory_swap() 0 15 4
A GlancesProcesses.reset_processcount() 0 3 1
A GlancesProcesses.get_export() 0 3 1
B GlancesProcesses.__init__() 0 78 5
A GlancesProcesses.update_export_list() 0 6 3
A GlancesProcesses.get_max_values() 0 3 1
A GlancesProcesses.disable_extended() 0 3 1
A GlancesProcesses.process_filter_key() 0 4 1
A GlancesProcesses.sort_reverse() 0 7 3
A GlancesProcesses.kill() 0 7 1
A GlancesProcesses.disable_kernel_threads() 0 3 1
A GlancesProcesses.nice_increase() 0 11 2
A GlancesProcesses.set_args() 0 3 1
A GlancesProcesses.update_processcount() 0 12 3
A GlancesProcesses.reset_max_values() 0 5 2
A GlancesProcesses.enable() 0 4 1
A GlancesProcesses.__get_extended_connections() 0 17 3
A GlancesProcesses.get_list() 0 8 2
A GlancesProcesses.pid_max() 0 32 4

5 Functions

Rating   Name   Duplication   Size   Complexity  
A _sort_lambda() 0 8 3
A weighted() 0 3 2
C sort_stats() 0 35 10
A _sort_cpu_times() 0 9 1
A _sort_io_counters() 0 6 1

How to fix   Complexity   

Complexity

Complex classes like glances.processes often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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