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