Total Complexity | 169 |
Total Lines | 934 |
Duplicated Lines | 16.81 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like glances.plugins.processlist 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 | # |
||
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 | """Process list plugin.""" |
||
10 | |||
11 | import copy |
||
12 | import functools |
||
13 | import os |
||
14 | |||
15 | from glances.globals import WINDOWS, key_exist_value_not_none_not_v, replace_special_chars |
||
16 | from glances.logger import logger |
||
17 | from glances.outputs.glances_unicode import unicode_message |
||
18 | from glances.plugins.core import PluginModel as CorePluginModel |
||
19 | from glances.plugins.plugin.model import GlancesPluginModel |
||
20 | from glances.processes import glances_processes, sort_stats |
||
21 | |||
22 | # Fields description |
||
23 | # description: human readable description |
||
24 | # short_name: shortname to use un UI |
||
25 | # unit: unit type |
||
26 | # rate: if True then compute and add *_gauge and *_rate_per_is fields |
||
27 | # min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)... |
||
28 | fields_description = { |
||
29 | 'pid': { |
||
30 | 'description': 'Process identifier (ID)', |
||
31 | 'unit': 'number', |
||
32 | }, |
||
33 | 'name': { |
||
34 | 'description': 'Process name', |
||
35 | 'unit': 'string', |
||
36 | }, |
||
37 | 'cmdline': { |
||
38 | 'description': 'Command line with arguments', |
||
39 | 'unit': 'list', |
||
40 | }, |
||
41 | 'username': { |
||
42 | 'description': 'Process owner', |
||
43 | 'unit': 'string', |
||
44 | }, |
||
45 | 'num_threads': { |
||
46 | 'description': 'Number of threads', |
||
47 | 'unit': 'number', |
||
48 | }, |
||
49 | 'cpu_percent': { |
||
50 | 'description': 'Process CPU consumption', |
||
51 | 'unit': 'percent', |
||
52 | }, |
||
53 | 'memory_percent': { |
||
54 | 'description': 'Process memory consumption', |
||
55 | 'unit': 'percent', |
||
56 | }, |
||
57 | 'memory_info': { |
||
58 | 'description': 'Process memory information (dict with rss, vms, shared, text, lib, data, dirty keys)', |
||
59 | 'unit': 'byte', |
||
60 | }, |
||
61 | 'status': { |
||
62 | 'description': 'Process status', |
||
63 | 'unit': 'string', |
||
64 | }, |
||
65 | 'nice': { |
||
66 | 'description': 'Process nice value', |
||
67 | 'unit': 'number', |
||
68 | }, |
||
69 | 'cpu_times': { |
||
70 | 'description': 'Process CPU times (dict with user, system, iowait keys)', |
||
71 | 'unit': 'second', |
||
72 | }, |
||
73 | 'gids': { |
||
74 | 'description': 'Process group IDs (dict with real, effective, saved keys)', |
||
75 | 'unit': 'number', |
||
76 | }, |
||
77 | 'io_counters': { |
||
78 | 'description': 'Process IO counters (list with read_count, write_count, read_bytes, write_bytes, io_tag keys)', |
||
79 | 'unit': 'byte', |
||
80 | }, |
||
81 | } |
||
82 | |||
83 | |||
84 | def seconds_to_hms(input_seconds): |
||
85 | """Convert seconds to human-readable time.""" |
||
86 | minutes, seconds = divmod(input_seconds, 60) |
||
87 | hours, minutes = divmod(minutes, 60) |
||
88 | |||
89 | hours = int(hours) |
||
90 | minutes = int(minutes) |
||
91 | seconds = str(int(seconds)).zfill(2) |
||
92 | |||
93 | return hours, minutes, seconds |
||
94 | |||
95 | |||
96 | def split_cmdline(bare_process_name, cmdline): |
||
97 | """Return path, cmd and arguments for a process cmdline based on bare_process_name. |
||
98 | |||
99 | If first argument of cmdline starts with the bare_process_name then |
||
100 | cmdline will just be considered cmd and path will be empty (see https://github.com/nicolargo/glances/issues/1795) |
||
101 | |||
102 | :param bare_process_name: Name of the process from psutil |
||
103 | :param cmdline: cmdline from psutil |
||
104 | :return: Tuple with three strings, which are path, cmd and arguments of the process |
||
105 | """ |
||
106 | if cmdline[0].startswith(bare_process_name): |
||
107 | path, cmd = "", cmdline[0] |
||
108 | else: |
||
109 | path, cmd = os.path.split(cmdline[0]) |
||
110 | arguments = ' '.join(cmdline[1:]) |
||
111 | return path, cmd, arguments |
||
112 | |||
113 | |||
114 | class PluginModel(GlancesPluginModel): |
||
115 | """Glances' processes plugin. |
||
116 | |||
117 | stats is a list |
||
118 | """ |
||
119 | |||
120 | # Default list of processes stats to be grabbed / displayed |
||
121 | # Can be altered by glances_processes.disable_stats |
||
122 | enable_stats = [ |
||
123 | 'cpu_percent', |
||
124 | 'memory_percent', |
||
125 | 'memory_info', # vms and rss |
||
126 | 'pid', |
||
127 | 'username', |
||
128 | 'cpu_times', |
||
129 | 'num_threads', |
||
130 | 'nice', |
||
131 | 'status', |
||
132 | 'io_counters', # ior and iow |
||
133 | 'cmdline', |
||
134 | ] |
||
135 | |||
136 | # Define the header layout of the processes list columns |
||
137 | layout_header = { |
||
138 | 'cpu': '{:<6} ', |
||
139 | 'mem': '{:<5} ', |
||
140 | 'virt': '{:<5} ', |
||
141 | 'res': '{:<5} ', |
||
142 | 'pid': '{:>{width}} ', |
||
143 | 'user': '{:<10} ', |
||
144 | 'time': '{:>8} ', |
||
145 | 'thread': '{:<3} ', |
||
146 | 'nice': '{:>3} ', |
||
147 | 'status': '{:>1} ', |
||
148 | 'ior': '{:>4} ', |
||
149 | 'iow': '{:<4} ', |
||
150 | 'command': '{} {}', |
||
151 | } |
||
152 | |||
153 | # Define the stat layout of the processes list columns |
||
154 | layout_stat = { |
||
155 | 'cpu': '{:<6.1f}', |
||
156 | 'cpu_no_digit': '{:<6.0f}', |
||
157 | 'mem': '{:<5.1f} ', |
||
158 | 'virt': '{:<5} ', |
||
159 | 'res': '{:<5} ', |
||
160 | 'pid': '{:>{width}} ', |
||
161 | 'user': '{:<10} ', |
||
162 | 'time': '{:>8} ', |
||
163 | 'thread': '{:<3} ', |
||
164 | 'nice': '{:>3} ', |
||
165 | 'status': '{:>1} ', |
||
166 | 'ior': '{:>4} ', |
||
167 | 'iow': '{:<4} ', |
||
168 | 'command': '{}', |
||
169 | 'name': '[{}]', |
||
170 | } |
||
171 | |||
172 | def __init__(self, args=None, config=None): |
||
173 | """Init the plugin.""" |
||
174 | super().__init__(args=args, config=config, fields_description=fields_description, stats_init_value=[]) |
||
175 | |||
176 | # We want to display the stat in the curse interface |
||
177 | self.display_curse = True |
||
178 | |||
179 | # Trying to display proc time |
||
180 | self.tag_proc_time = True |
||
181 | |||
182 | # Call CorePluginModel to get the core number (needed when not in IRIX mode / Solaris mode) |
||
183 | try: |
||
184 | self.nb_log_core = CorePluginModel(args=self.args).update()["log"] |
||
185 | except Exception: |
||
186 | self.nb_log_core = 0 |
||
187 | |||
188 | # Get the max values (dict) |
||
189 | self.max_values = copy.deepcopy(glances_processes.max_values()) |
||
190 | |||
191 | # Get the maximum PID number |
||
192 | # Use to optimize space (see https://github.com/nicolargo/glances/issues/959) |
||
193 | self.pid_max = glances_processes.pid_max |
||
194 | |||
195 | # Load the config file |
||
196 | self.load(args, config) |
||
197 | |||
198 | # The default sort key could also be overwrite by command line (see #1903) |
||
199 | if args and args.sort_processes_key is not None: |
||
200 | glances_processes.set_sort_key(args.sort_processes_key, False) |
||
201 | |||
202 | # Note: 'glances_processes' is already init in the processes.py script |
||
203 | |||
204 | def load(self, args, config): |
||
205 | # Set the default sort key if it is defined in the configuration file |
||
206 | if config is None or 'processlist' not in config.as_dict(): |
||
207 | return |
||
208 | if 'sort_key' in config.as_dict()['processlist']: |
||
209 | logger.debug( |
||
210 | 'Configuration overwrites processes sort key by {}'.format(config.as_dict()['processlist']['sort_key']) |
||
211 | ) |
||
212 | glances_processes.set_sort_key(config.as_dict()['processlist']['sort_key'], False) |
||
213 | if 'export' in config.as_dict()['processlist']: |
||
214 | glances_processes.export_process_filter = config.as_dict()['processlist']['export'] |
||
215 | if args.export: |
||
216 | logger.info("Export process filter is set to: {}".format(config.as_dict()['processlist']['export'])) |
||
217 | if 'disable_stats' in config.as_dict()['processlist']: |
||
218 | logger.info( |
||
219 | 'Followings processes stats wil not be displayed: {}'.format( |
||
220 | config.as_dict()['processlist']['disable_stats'] |
||
221 | ) |
||
222 | ) |
||
223 | glances_processes.disable_stats = config.as_dict()['processlist']['disable_stats'].split(',') |
||
224 | |||
225 | def get_key(self): |
||
226 | """Return the key of the list.""" |
||
227 | return 'pid' |
||
228 | |||
229 | def update(self): |
||
230 | """Update processes stats using the input method.""" |
||
231 | # Update the stats |
||
232 | if self.input_method == 'local': |
||
233 | # Update stats using the standard system lib |
||
234 | # Note: Update is done in the processcount plugin |
||
235 | # Just return the result |
||
236 | stats = glances_processes.get_list() |
||
237 | else: |
||
238 | stats = self.get_init_value() |
||
239 | |||
240 | # Get the max values (dict) |
||
241 | # Use Deep copy to avoid change between update and display |
||
242 | self.max_values = copy.deepcopy(glances_processes.max_values()) |
||
243 | |||
244 | # Update the stats |
||
245 | self.stats = stats |
||
246 | |||
247 | return self.stats |
||
248 | |||
249 | def get_export(self): |
||
250 | """Return the processes list to export. |
||
251 | Not all the processeses are exported. |
||
252 | Only the one defined in the Glances configuration file (see #794 for details). |
||
253 | """ |
||
254 | return glances_processes.get_export() |
||
255 | |||
256 | def get_nice_alert(self, value): |
||
257 | """Return the alert relative to the Nice configuration list""" |
||
258 | value = str(value) |
||
259 | if self.get_limit('nice_critical') and value in self.get_limit('nice_critical'): |
||
260 | return 'CRITICAL' |
||
261 | if self.get_limit('nice_warning') and value in self.get_limit('nice_warning'): |
||
262 | return 'WARNING' |
||
263 | if self.get_limit('nice_careful') and value in self.get_limit('nice_careful'): |
||
264 | return 'CAREFUL' |
||
265 | |||
266 | return 'DEFAULT' |
||
267 | |||
268 | def _get_process_curses_cpu_percent(self, p, selected, args): |
||
269 | """Return process CPU curses""" |
||
270 | if key_exist_value_not_none_not_v('cpu_percent', p, ''): |
||
271 | cpu_layout = self.layout_stat['cpu'] if p['cpu_percent'] < 100 else self.layout_stat['cpu_no_digit'] |
||
272 | if args.disable_irix and self.nb_log_core != 0: |
||
273 | msg = cpu_layout.format(p['cpu_percent'] / float(self.nb_log_core)) |
||
274 | else: |
||
275 | msg = cpu_layout.format(p['cpu_percent']) |
||
276 | alert = self.get_alert( |
||
277 | p['cpu_percent'], |
||
278 | highlight_zero=False, |
||
279 | is_max=(p['cpu_percent'] == self.max_values['cpu_percent']), |
||
280 | header="cpu", |
||
281 | ) |
||
282 | ret = self.curse_add_line(msg, alert) |
||
283 | else: |
||
284 | msg = self.layout_header['cpu'].format('?') |
||
285 | ret = self.curse_add_line(msg) |
||
286 | return ret |
||
287 | |||
288 | def _get_process_curses_memory_percent(self, p, selected, args): |
||
289 | """Return process MEM curses""" |
||
290 | if key_exist_value_not_none_not_v('memory_percent', p, ''): |
||
291 | msg = self.layout_stat['mem'].format(p['memory_percent']) |
||
292 | alert = self.get_alert( |
||
293 | p['memory_percent'], |
||
294 | highlight_zero=False, |
||
295 | is_max=(p['memory_percent'] == self.max_values['memory_percent']), |
||
296 | header="mem", |
||
297 | ) |
||
298 | ret = self.curse_add_line(msg, alert) |
||
299 | else: |
||
300 | msg = self.layout_header['mem'].format('?') |
||
301 | ret = self.curse_add_line(msg) |
||
302 | return ret |
||
303 | |||
304 | def _get_process_curses_vms(self, p, selected, args): |
||
305 | """Return process VMS curses""" |
||
306 | if key_exist_value_not_none_not_v('memory_info', p, '', 1) and 'vms' in p['memory_info']: |
||
307 | msg = self.layout_stat['virt'].format(self.auto_unit(p['memory_info']['vms'], low_precision=False)) |
||
308 | ret = self.curse_add_line(msg, optional=True) |
||
309 | else: |
||
310 | msg = self.layout_header['virt'].format('?') |
||
311 | ret = self.curse_add_line(msg) |
||
312 | return ret |
||
313 | |||
314 | def _get_process_curses_rss(self, p, selected, args): |
||
315 | """Return process RSS curses""" |
||
316 | if key_exist_value_not_none_not_v('memory_info', p, '', 0) and 'rss' in p['memory_info']: |
||
317 | msg = self.layout_stat['res'].format(self.auto_unit(p['memory_info']['rss'], low_precision=False)) |
||
318 | ret = self.curse_add_line(msg, optional=True) |
||
319 | else: |
||
320 | msg = self.layout_header['res'].format('?') |
||
321 | ret = self.curse_add_line(msg) |
||
322 | return ret |
||
323 | |||
324 | def _get_process_curses_memory_info(self, p, selected, args): |
||
325 | return [ |
||
326 | self._get_process_curses_vms(p, selected, args), |
||
327 | self._get_process_curses_rss(p, selected, args), |
||
328 | ] |
||
329 | |||
330 | def _get_process_curses_pid(self, p, selected, args): |
||
331 | """Return process PID curses""" |
||
332 | # Display processes, so the PID should be displayed |
||
333 | msg = self.layout_stat['pid'].format(p['pid'], width=self._max_pid_size()) |
||
334 | return self.curse_add_line(msg) |
||
335 | |||
336 | def _get_process_curses_username(self, p, selected, args): |
||
337 | """Return process username curses""" |
||
338 | if 'username' in p: |
||
339 | # docker internal users are displayed as ints only, therefore str() |
||
340 | # Correct issue #886 on Windows OS |
||
341 | msg = self.layout_stat['user'].format(str(p['username'])[:9]) |
||
342 | else: |
||
343 | msg = self.layout_header['user'].format('?') |
||
344 | return self.curse_add_line(msg) |
||
345 | |||
346 | def _get_process_curses_cpu_times(self, p, selected, args): |
||
347 | """Return process time curses""" |
||
348 | cpu_times = p['cpu_times'] |
||
349 | try: |
||
350 | # Sum user and system time |
||
351 | user_system_time = cpu_times['user'] + cpu_times['system'] |
||
352 | except (OverflowError, TypeError, KeyError): |
||
353 | # Catch OverflowError on some Amazon EC2 server |
||
354 | # See https://github.com/nicolargo/glances/issues/87 |
||
355 | # Also catch TypeError on macOS |
||
356 | # See: https://github.com/nicolargo/glances/issues/622 |
||
357 | # Also catch KeyError (as no stats be present for processes of other users) |
||
358 | # See: https://github.com/nicolargo/glances/issues/2831 |
||
359 | # logger.debug("Cannot get TIME+ ({})".format(e)) |
||
360 | msg = self.layout_header['time'].format('?') |
||
361 | return self.curse_add_line(msg, optional=True) |
||
362 | |||
363 | hours, minutes, seconds = seconds_to_hms(user_system_time) |
||
364 | if hours > 99: |
||
365 | msg = f'{hours:<7}h' |
||
366 | elif 0 < hours < 100: |
||
367 | msg = f'{hours}h{minutes}:{seconds}' |
||
368 | else: |
||
369 | msg = f'{minutes}:{seconds}' |
||
370 | |||
371 | msg = self.layout_stat['time'].format(msg) |
||
372 | if hours > 0: |
||
373 | return self.curse_add_line(msg, decoration='CPU_TIME', optional=True) |
||
374 | |||
375 | return self.curse_add_line(msg, optional=True) |
||
376 | |||
377 | def _get_process_curses_num_threads(self, p, selected, args): |
||
378 | """Return process thread curses""" |
||
379 | if 'num_threads' in p: |
||
380 | num_threads = p['num_threads'] |
||
381 | if num_threads is None: |
||
382 | num_threads = '?' |
||
383 | msg = self.layout_stat['thread'].format(num_threads) |
||
384 | else: |
||
385 | msg = self.layout_header['thread'].format('?') |
||
386 | return self.curse_add_line(msg) |
||
387 | |||
388 | def _get_process_curses_nice(self, p, selected, args): |
||
389 | """Return process nice curses""" |
||
390 | if 'nice' in p: |
||
391 | nice = p['nice'] |
||
392 | if nice is None: |
||
393 | nice = '?' |
||
394 | msg = self.layout_stat['nice'].format(nice) |
||
395 | ret = self.curse_add_line(msg, decoration=self.get_nice_alert(nice)) |
||
396 | else: |
||
397 | msg = self.layout_header['nice'].format('?') |
||
398 | ret = self.curse_add_line(msg) |
||
399 | return ret |
||
400 | |||
401 | def _get_process_curses_status(self, p, selected, args): |
||
402 | """Return process status curses""" |
||
403 | if 'status' in p: |
||
404 | status = p['status'] |
||
405 | msg = self.layout_stat['status'].format(status) |
||
406 | if status == 'R': |
||
407 | ret = self.curse_add_line(msg, decoration='STATUS') |
||
408 | else: |
||
409 | ret = self.curse_add_line(msg) |
||
410 | else: |
||
411 | msg = self.layout_header['status'].format('?') |
||
412 | ret = self.curse_add_line(msg) |
||
413 | return ret |
||
414 | |||
415 | def _get_process_curses_io_read_write(self, p, selected, args, rorw='ior'): |
||
416 | """Return process IO Read or Write curses""" |
||
417 | if 'io_counters' in p and p['io_counters'][4] == 1 and p['time_since_update'] != 0: |
||
418 | # Display rate if stats is available and io_tag ([4]) == 1 |
||
419 | # IO |
||
420 | io = int( |
||
421 | (p['io_counters'][0 if rorw == 'ior' else 1] - p['io_counters'][2 if rorw == 'ior' else 3]) |
||
422 | / p['time_since_update'] |
||
423 | ) |
||
424 | if io == 0: |
||
425 | msg = self.layout_stat[rorw].format("0") |
||
426 | else: |
||
427 | msg = self.layout_stat[rorw].format(self.auto_unit(io, low_precision=True)) |
||
428 | ret = self.curse_add_line(msg, optional=True, additional=True) |
||
429 | else: |
||
430 | msg = self.layout_header[rorw].format("?") |
||
431 | ret = self.curse_add_line(msg, optional=True, additional=True) |
||
432 | return ret |
||
433 | |||
434 | def _get_process_curses_io_counters(self, p, selected, args): |
||
435 | return [ |
||
436 | self._get_process_curses_io_read_write(p, selected, args, rorw='ior'), |
||
437 | self._get_process_curses_io_read_write(p, selected, args, rorw='iow'), |
||
438 | ] |
||
439 | |||
440 | def _get_process_curses_cmdline(self, p, selected, args): |
||
441 | """Return process cmdline curses""" |
||
442 | ret = [] |
||
443 | # If no command line for the process is available, fallback to the bare process name instead |
||
444 | bare_process_name = p['name'] |
||
445 | cmdline = p.get('cmdline', '?') |
||
446 | try: |
||
447 | process_decoration = 'PROCESS_SELECTED' if (selected and not args.disable_cursor) else 'PROCESS' |
||
448 | if cmdline: |
||
449 | path, cmd, arguments = split_cmdline(bare_process_name, cmdline) |
||
450 | # Manage end of line in arguments (see #1692) |
||
451 | arguments = replace_special_chars(arguments) |
||
452 | if os.path.isdir(path) and not args.process_short_name: |
||
453 | msg = self.layout_stat['command'].format(path) + os.sep |
||
454 | ret.append(self.curse_add_line(msg, splittable=True)) |
||
455 | ret.append(self.curse_add_line(cmd, decoration=process_decoration, splittable=True)) |
||
456 | else: |
||
457 | msg = self.layout_stat['command'].format(cmd) |
||
458 | ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True)) |
||
459 | if arguments: |
||
460 | msg = ' ' + self.layout_stat['command'].format(arguments) |
||
461 | ret.append(self.curse_add_line(msg, splittable=True)) |
||
462 | else: |
||
463 | msg = self.layout_stat['name'].format(bare_process_name) |
||
464 | ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True)) |
||
465 | except (TypeError, UnicodeEncodeError) as e: |
||
466 | # Avoid crash after running fine for several hours #1335 |
||
467 | logger.debug(f"Can not decode command line '{cmdline}' ({e})") |
||
468 | ret.append(self.curse_add_line('', splittable=True)) |
||
469 | return ret |
||
470 | |||
471 | def get_process_curses_data(self, p, selected, args): |
||
472 | """Get curses data to display for a process. |
||
473 | |||
474 | - p is the process to display |
||
475 | - selected is a tag=True if p is the selected process |
||
476 | """ |
||
477 | ret = [self.curse_new_line()] |
||
478 | |||
479 | # When a process is selected: |
||
480 | # * display a special character at the beginning of the line |
||
481 | # * underline the command name |
||
482 | ret.append( |
||
483 | self.curse_add_line( |
||
484 | unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED' |
||
485 | ) |
||
486 | ) |
||
487 | |||
488 | for stat in [i for i in self.enable_stats if i not in glances_processes.disable_stats]: |
||
489 | msg = getattr(self, f'_get_process_curses_{stat}')(p, selected, args) |
||
490 | if isinstance(msg, list): |
||
491 | # ex: _get_process_curses_command return a list, so extend |
||
492 | ret.extend(msg) |
||
493 | else: |
||
494 | # ex: _get_process_curses_cpu return a dict, so append |
||
495 | ret.append(msg) |
||
496 | |||
497 | return ret |
||
498 | |||
499 | def is_selected_process(self, args): |
||
500 | return ( |
||
501 | args.is_standalone |
||
502 | and self.args.enable_process_extended |
||
503 | and args.cursor_position is not None |
||
504 | and glances_processes.extended_process is not None |
||
505 | ) |
||
506 | |||
507 | def msg_curse(self, args=None, max_width=None): |
||
508 | """Return the dict to display in the curse interface.""" |
||
509 | # Init the return message |
||
510 | ret = [] |
||
511 | |||
512 | # Only process if stats exist and display plugin enable... |
||
513 | if not self.stats or args.disable_process: |
||
514 | return ret |
||
515 | |||
516 | # Compute the sort key |
||
517 | process_sort_key = glances_processes.sort_key |
||
518 | processes_list_sorted = self._sort_stats(process_sort_key) |
||
519 | |||
520 | # Display extended stats for selected process |
||
521 | ############################################# |
||
522 | |||
523 | if self.is_selected_process(args): |
||
524 | self._msg_curse_extended_process(ret, glances_processes.extended_process) |
||
525 | |||
526 | # Display others processes list |
||
527 | ############################### |
||
528 | |||
529 | # Header |
||
530 | self._msg_curse_header(ret, process_sort_key, args) |
||
531 | |||
532 | # Process list |
||
533 | # Loop over processes (sorted by the sort key previously compute) |
||
534 | # This is a Glances bottleneck (see flame graph), |
||
535 | # TODO: get_process_curses_data should be optimized |
||
536 | for position, process in enumerate(processes_list_sorted): |
||
537 | ret.extend(self.get_process_curses_data(process, position == args.cursor_position, args)) |
||
538 | |||
539 | # A filter is set Display the stats summaries |
||
540 | if glances_processes.process_filter is not None: |
||
541 | if args.reset_minmax_tag: |
||
542 | args.reset_minmax_tag = not args.reset_minmax_tag |
||
543 | self._mmm_reset() |
||
544 | self._msg_curse_sum(ret, args=args) |
||
545 | self._msg_curse_sum(ret, mmm='min', args=args) |
||
546 | self._msg_curse_sum(ret, mmm='max', args=args) |
||
547 | |||
548 | # Return the message with decoration |
||
549 | return ret |
||
550 | |||
551 | def _msg_curse_extended_process(self, ret, p): |
||
552 | """Get extended curses data for the selected process (see issue #2225) |
||
553 | |||
554 | The result depends of the process type (process or thread). |
||
555 | |||
556 | Input p is a dict with the following keys: |
||
557 | {'status': 'S', |
||
558 | 'memory_info': {'rss': 466890752, 'vms': 3365347328, 'shared': 68153344, |
||
559 | 'text': 659456, 'lib': 0, 'data': 774647808, 'dirty': 0], |
||
560 | 'pid': 4980, |
||
561 | 'io_counters': [165385216, 0, 165385216, 0, 1], |
||
562 | 'num_threads': 20, |
||
563 | 'nice': 0, |
||
564 | 'memory_percent': 5.958135664449709, |
||
565 | 'cpu_percent': 0.0, |
||
566 | 'gids': {'real': 1000, 'effective': 1000, 'saved': 1000}, |
||
567 | 'cpu_times': {'user': 696.38, 'system': 119.98, 'children_user': 0.0, |
||
568 | 'children_system': 0.0, 'iowait': 0.0), |
||
569 | 'name': 'WebExtensions', |
||
570 | 'key': 'pid', |
||
571 | 'time_since_update': 2.1997854709625244, |
||
572 | 'cmdline': ['/snap/firefox/2154/usr/lib/firefox/firefox', '-contentproc', '-childID', '...'], |
||
573 | 'username': 'nicolargo', |
||
574 | 'cpu_min': 0.0, |
||
575 | 'cpu_max': 7.0, |
||
576 | 'cpu_mean': 3.2} |
||
577 | """ |
||
578 | self._msg_curse_extended_process_thread(ret, p) |
||
579 | |||
580 | def add_title_line(self, ret, prog): |
||
581 | ret.append(self.curse_add_line("Pinned thread ", "TITLE")) |
||
582 | ret.append(self.curse_add_line(prog['name'], "UNDERLINE")) |
||
583 | ret.append(self.curse_add_line(" ('e' to unpin)")) |
||
584 | |||
585 | return ret |
||
586 | |||
587 | def add_cpu_line(self, ret, prog): |
||
588 | ret.append(self.curse_new_line()) |
||
589 | ret.append(self.curse_add_line(' CPU Min/Max/Mean: ')) |
||
590 | msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(prog['cpu_min'], prog['cpu_max'], prog['cpu_mean']) |
||
591 | ret.append(self.curse_add_line(msg, decoration='INFO')) |
||
592 | |||
593 | return ret |
||
594 | |||
595 | def maybe_add_cpu_affinity_line(self, ret, prog): |
||
596 | if 'cpu_affinity' in prog and prog['cpu_affinity'] is not None: |
||
597 | ret.append(self.curse_add_line(' Affinity: ')) |
||
598 | ret.append(self.curse_add_line(str(len(prog['cpu_affinity'])), decoration='INFO')) |
||
599 | ret.append(self.curse_add_line(' cores', decoration='INFO')) |
||
600 | |||
601 | return ret |
||
602 | |||
603 | def add_ionice_line(self, headers, default): |
||
604 | def add_ionice_using_matches(msg, v): |
||
605 | return msg + headers.get(v, default(v)) |
||
606 | |||
607 | return add_ionice_using_matches |
||
608 | |||
609 | def get_headers(self, k): |
||
610 | # Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle. |
||
611 | default = {0: 'No specific I/O priority', 1: k + 'Real Time', 2: k + 'Best Effort', 3: k + 'IDLE'} |
||
612 | |||
613 | # Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low). |
||
614 | windows = {0: k + 'Very Low', 1: k + 'Low', 2: 'No specific I/O priority'} |
||
615 | |||
616 | return windows if WINDOWS else default |
||
617 | |||
618 | def maybe_add_ionice_line(self, ret, prog): |
||
619 | if 'ionice' in prog and prog['ionice'] is not None and hasattr(prog['ionice'], 'ioclass'): |
||
620 | msg = ' IO nice: ' |
||
621 | k = 'Class is ' |
||
622 | v = prog['ionice'].ioclass |
||
623 | |||
624 | def default(v): |
||
625 | return k + str(v) |
||
|
|||
626 | |||
627 | headers = self.get_headers(k) |
||
628 | msg = self.add_ionice_line(headers, default)(msg, v) |
||
629 | # value is a number which goes from 0 to 7. |
||
630 | # The higher the value, the lower the I/O priority of the process. |
||
631 | if hasattr(prog['ionice'], 'value') and prog['ionice'].value != 0: |
||
632 | msg += ' (value {}/7)'.format(str(prog['ionice'].value)) |
||
633 | ret.append(self.curse_add_line(msg, splittable=True)) |
||
634 | |||
635 | return ret |
||
636 | |||
637 | def maybe_add_memory_swap_line(self, ret, prog): |
||
638 | if 'memory_swap' in prog and prog['memory_swap'] is not None: |
||
639 | ret.append( |
||
640 | self.curse_add_line( |
||
641 | self.auto_unit(prog['memory_swap'], low_precision=False), decoration='INFO', splittable=True |
||
642 | ) |
||
643 | ) |
||
644 | ret.append(self.curse_add_line(' swap ', splittable=True)) |
||
645 | |||
646 | return ret |
||
647 | |||
648 | def add_memory_info_lines(self, ret, prog): |
||
649 | for key, val in prog['memory_info'].items(): |
||
650 | ret.append( |
||
651 | self.curse_add_line( |
||
652 | self.auto_unit(val, low_precision=False), |
||
653 | decoration='INFO', |
||
654 | splittable=True, |
||
655 | ) |
||
656 | ) |
||
657 | ret.append(self.curse_add_line(' ' + key + ' ', splittable=True)) |
||
658 | |||
659 | return ret |
||
660 | |||
661 | def add_memory_line(self, ret, prog): |
||
662 | ret.append(self.curse_new_line()) |
||
663 | ret.append(self.curse_add_line(' MEM Min/Max/Mean: ')) |
||
664 | msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(prog['memory_min'], prog['memory_max'], prog['memory_mean']) |
||
665 | ret.append(self.curse_add_line(msg, decoration='INFO')) |
||
666 | if 'memory_info' in prog and prog['memory_info'] is not None: |
||
667 | ret.append(self.curse_add_line(' Memory info: ')) |
||
668 | steps = [self.add_memory_info_lines, self.maybe_add_memory_swap_line] |
||
669 | ret = functools.reduce(lambda ret, step: step(ret, prog), steps, ret) |
||
670 | |||
671 | return ret |
||
672 | |||
673 | def add_io_and_network_lines(self, ret, prog): |
||
674 | ret.append(self.curse_new_line()) |
||
675 | ret.append(self.curse_add_line(' Open: ')) |
||
676 | for stat_prefix in ['num_threads', 'num_fds', 'num_handles', 'tcp', 'udp']: |
||
677 | if stat_prefix in prog and prog[stat_prefix] is not None: |
||
678 | ret.append(self.curse_add_line(str(prog[stat_prefix]), decoration='INFO')) |
||
679 | ret.append(self.curse_add_line(' {} '.format(stat_prefix.replace('num_', '')))) |
||
680 | return ret |
||
681 | |||
682 | def _msg_curse_extended_process_thread(self, ret, prog): |
||
683 | # `append_newlines` has dummy arguments for piping thru `functools.reduce` |
||
684 | def append_newlines(ret, prog): |
||
685 | (ret.append(self.curse_new_line()),) |
||
686 | ret.append(self.curse_new_line()) |
||
687 | |||
688 | return ret |
||
689 | |||
690 | steps = [ |
||
691 | self.add_title_line, |
||
692 | self.add_cpu_line, |
||
693 | self.maybe_add_cpu_affinity_line, |
||
694 | self.maybe_add_ionice_line, |
||
695 | self.add_memory_line, |
||
696 | self.add_io_and_network_lines, |
||
697 | append_newlines, |
||
698 | ] |
||
699 | |||
700 | functools.reduce(lambda ret, step: step(ret, prog), steps, ret) |
||
701 | |||
702 | View Code Duplication | def _msg_curse_header(self, ret, process_sort_key, args=None): |
|
703 | """Build the header and add it to the ret dict.""" |
||
704 | sort_style = 'SORT' |
||
705 | |||
706 | display_stats = [i for i in self.enable_stats if i not in glances_processes.disable_stats] |
||
707 | |||
708 | if 'cpu_percent' in display_stats: |
||
709 | if args.disable_irix and 0 < self.nb_log_core < 10: |
||
710 | msg = self.layout_header['cpu'].format('CPU%/' + str(self.nb_log_core)) |
||
711 | elif args.disable_irix and self.nb_log_core != 0: |
||
712 | msg = self.layout_header['cpu'].format('CPU%/C') |
||
713 | else: |
||
714 | msg = self.layout_header['cpu'].format('CPU%') |
||
715 | ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_percent' else 'DEFAULT')) |
||
716 | |||
717 | if 'memory_percent' in display_stats: |
||
718 | msg = self.layout_header['mem'].format('MEM%') |
||
719 | ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'memory_percent' else 'DEFAULT')) |
||
720 | if 'memory_info' in display_stats: |
||
721 | msg = self.layout_header['virt'].format('VIRT') |
||
722 | ret.append(self.curse_add_line(msg, optional=True)) |
||
723 | msg = self.layout_header['res'].format('RES') |
||
724 | ret.append(self.curse_add_line(msg, optional=True)) |
||
725 | if 'pid' in display_stats: |
||
726 | msg = self.layout_header['pid'].format('PID', width=self._max_pid_size()) |
||
727 | ret.append(self.curse_add_line(msg)) |
||
728 | if 'username' in display_stats: |
||
729 | msg = self.layout_header['user'].format('USER') |
||
730 | ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'username' else 'DEFAULT')) |
||
731 | if 'cpu_times' in display_stats: |
||
732 | msg = self.layout_header['time'].format('TIME+') |
||
733 | ret.append( |
||
734 | self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_times' else 'DEFAULT', optional=True) |
||
735 | ) |
||
736 | if 'num_threads' in display_stats: |
||
737 | msg = self.layout_header['thread'].format('THR') |
||
738 | ret.append(self.curse_add_line(msg)) |
||
739 | if 'nice' in display_stats: |
||
740 | msg = self.layout_header['nice'].format('NI') |
||
741 | ret.append(self.curse_add_line(msg)) |
||
742 | if 'status' in display_stats: |
||
743 | msg = self.layout_header['status'].format('S') |
||
744 | ret.append(self.curse_add_line(msg)) |
||
745 | if 'io_counters' in display_stats: |
||
746 | msg = self.layout_header['ior'].format('R/s') |
||
747 | ret.append( |
||
748 | self.curse_add_line( |
||
749 | msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True |
||
750 | ) |
||
751 | ) |
||
752 | msg = self.layout_header['iow'].format('W/s') |
||
753 | ret.append( |
||
754 | self.curse_add_line( |
||
755 | msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True |
||
756 | ) |
||
757 | ) |
||
758 | if args.is_standalone and not args.disable_cursor: |
||
759 | shortkey = "('e' to pin | 'k' to kill)" |
||
760 | else: |
||
761 | shortkey = "" |
||
762 | if 'cmdline' in display_stats: |
||
763 | msg = self.layout_header['command'].format("Command", shortkey) |
||
764 | ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT')) |
||
765 | |||
766 | View Code Duplication | def _msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None): |
|
767 | """ |
||
768 | Build the sum message (only when filter is on) and add it to the ret dict. |
||
769 | |||
770 | :param ret: list of string where the message is added |
||
771 | :param sep_char: define the line separation char |
||
772 | :param mmm: display min, max, mean or current (if mmm=None) |
||
773 | :param args: Glances args |
||
774 | """ |
||
775 | ret.append(self.curse_new_line()) |
||
776 | if mmm is None: |
||
777 | ret.append(self.curse_add_line(sep_char * 69)) |
||
778 | ret.append(self.curse_new_line()) |
||
779 | # CPU percent sum |
||
780 | msg = ' ' |
||
781 | msg += self.layout_stat['cpu'].format(self._sum_stats('cpu_percent', mmm=mmm)) |
||
782 | ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm))) |
||
783 | # MEM percent sum |
||
784 | msg = self.layout_stat['mem'].format(self._sum_stats('memory_percent', mmm=mmm)) |
||
785 | ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm))) |
||
786 | # VIRT and RES memory sum |
||
787 | if ( |
||
788 | 'memory_info' in self.stats[0] |
||
789 | and self.stats[0]['memory_info'] is not None |
||
790 | and self.stats[0]['memory_info'] != '' |
||
791 | ): |
||
792 | # VMS |
||
793 | msg = self.layout_stat['virt'].format( |
||
794 | self.auto_unit(self._sum_stats('memory_info', sub_key='vms', mmm=mmm), low_precision=False) |
||
795 | ) |
||
796 | ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True)) |
||
797 | # RSS |
||
798 | msg = self.layout_stat['res'].format( |
||
799 | self.auto_unit(self._sum_stats('memory_info', sub_key='rss', mmm=mmm), low_precision=False) |
||
800 | ) |
||
801 | ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True)) |
||
802 | else: |
||
803 | msg = self.layout_header['virt'].format('') |
||
804 | ret.append(self.curse_add_line(msg)) |
||
805 | msg = self.layout_header['res'].format('') |
||
806 | ret.append(self.curse_add_line(msg)) |
||
807 | # PID |
||
808 | msg = self.layout_header['pid'].format('', width=self._max_pid_size()) |
||
809 | ret.append(self.curse_add_line(msg)) |
||
810 | # USER |
||
811 | msg = self.layout_header['user'].format('') |
||
812 | ret.append(self.curse_add_line(msg)) |
||
813 | # TIME+ |
||
814 | msg = self.layout_header['time'].format('') |
||
815 | ret.append(self.curse_add_line(msg, optional=True)) |
||
816 | # THREAD |
||
817 | msg = self.layout_header['thread'].format('') |
||
818 | ret.append(self.curse_add_line(msg)) |
||
819 | # NICE |
||
820 | msg = self.layout_header['nice'].format('') |
||
821 | ret.append(self.curse_add_line(msg)) |
||
822 | # STATUS |
||
823 | msg = self.layout_header['status'].format('') |
||
824 | ret.append(self.curse_add_line(msg)) |
||
825 | # IO read/write |
||
826 | if 'io_counters' in self.stats[0] and mmm is None: |
||
827 | # IO read |
||
828 | io_rs = int( |
||
829 | (self._sum_stats('io_counters', 0) - self._sum_stats('io_counters', sub_key=2, mmm=mmm)) |
||
830 | / self.stats[0]['time_since_update'] |
||
831 | ) |
||
832 | if io_rs == 0: |
||
833 | msg = self.layout_stat['ior'].format('0') |
||
834 | else: |
||
835 | msg = self.layout_stat['ior'].format(self.auto_unit(io_rs, low_precision=True)) |
||
836 | ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True, additional=True)) |
||
837 | # IO write |
||
838 | io_ws = int( |
||
839 | (self._sum_stats('io_counters', 1) - self._sum_stats('io_counters', sub_key=3, mmm=mmm)) |
||
840 | / self.stats[0]['time_since_update'] |
||
841 | ) |
||
842 | if io_ws == 0: |
||
843 | msg = self.layout_stat['iow'].format('0') |
||
844 | else: |
||
845 | msg = self.layout_stat['iow'].format(self.auto_unit(io_ws, low_precision=True)) |
||
846 | ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True, additional=True)) |
||
847 | else: |
||
848 | msg = self.layout_header['ior'].format('') |
||
849 | ret.append(self.curse_add_line(msg, optional=True, additional=True)) |
||
850 | msg = self.layout_header['iow'].format('') |
||
851 | ret.append(self.curse_add_line(msg, optional=True, additional=True)) |
||
852 | if mmm is None: |
||
853 | msg = '< {}'.format('current') |
||
854 | ret.append(self.curse_add_line(msg, optional=True)) |
||
855 | else: |
||
856 | msg = f'< {mmm}' |
||
857 | ret.append(self.curse_add_line(msg, optional=True)) |
||
858 | msg = '(\'M\' to reset)' |
||
859 | ret.append(self.curse_add_line(msg, optional=True)) |
||
860 | |||
861 | def _mmm_deco(self, mmm): |
||
862 | """Return the decoration string for the current mmm status.""" |
||
863 | if mmm is not None: |
||
864 | return 'DEFAULT' |
||
865 | return 'FILTER' |
||
866 | |||
867 | def _mmm_reset(self): |
||
868 | """Reset the MMM stats.""" |
||
869 | self.mmm_min = {} |
||
870 | self.mmm_max = {} |
||
871 | |||
872 | def _sum_stats(self, key, sub_key=None, mmm=None): |
||
873 | """Return the sum of the stats value for the given key. |
||
874 | |||
875 | :param sub_key: If sub_key is set, get the p[key][sub_key] |
||
876 | :param mmm: display min, max, mean or current (if mmm=None) |
||
877 | """ |
||
878 | # Compute stats summary |
||
879 | ret = 0 |
||
880 | for p in self.stats: |
||
881 | if key not in p: |
||
882 | # Correct issue #1188 |
||
883 | continue |
||
884 | if p[key] is None: |
||
885 | # Correct https://github.com/nicolargo/glances/issues/1105#issuecomment-363553788 |
||
886 | continue |
||
887 | if sub_key is None: |
||
888 | ret += p[key] |
||
889 | else: |
||
890 | ret += p[key][sub_key] |
||
891 | |||
892 | # Manage Min/Max/Mean |
||
893 | mmm_key = self._mmm_key(key, sub_key) |
||
894 | if mmm == 'min': |
||
895 | try: |
||
896 | if self.mmm_min[mmm_key] > ret: |
||
897 | self.mmm_min[mmm_key] = ret |
||
898 | except AttributeError: |
||
899 | self.mmm_min = {} |
||
900 | return 0 |
||
901 | except KeyError: |
||
902 | self.mmm_min[mmm_key] = ret |
||
903 | ret = self.mmm_min[mmm_key] |
||
904 | elif mmm == 'max': |
||
905 | try: |
||
906 | if self.mmm_max[mmm_key] < ret: |
||
907 | self.mmm_max[mmm_key] = ret |
||
908 | except AttributeError: |
||
909 | self.mmm_max = {} |
||
910 | return 0 |
||
911 | except KeyError: |
||
912 | self.mmm_max[mmm_key] = ret |
||
913 | ret = self.mmm_max[mmm_key] |
||
914 | |||
915 | return ret |
||
916 | |||
917 | def _mmm_key(self, key, sub_key): |
||
918 | ret = key |
||
919 | if sub_key is not None: |
||
920 | ret += str(sub_key) |
||
921 | return ret |
||
922 | |||
923 | def _sort_stats(self, sorted_by=None): |
||
924 | """Return the stats (dict) sorted by (sorted_by).""" |
||
925 | return sort_stats(self.stats, sorted_by, reverse=glances_processes.sort_reverse) |
||
926 | |||
927 | def _max_pid_size(self): |
||
928 | """Return the maximum PID size in number of char.""" |
||
929 | if self.pid_max is not None: |
||
930 | return len(str(self.pid_max)) |
||
931 | |||
932 | # By default return 5 (corresponding to 99999 PID number) |
||
933 | return 5 |
||
934 |