GlancesProcesses.reset_processcount()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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)))
0 ignored issues
show
introduced by
The variable k does not seem to be defined in case the for loop on line 160 is not entered. Are you sure this can never be the case?
Loading history...
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