Test Failed
Push — develop ( 24a3ae...5f48d5 )
by Nicolas
02:20 queued 14s
created

glances.processes._sort_lambda()   A

Complexity

Conditions 3

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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