From 8f57fd36127a1586b36a26a43de701021ef09913 Mon Sep 17 00:00:00 2001 From: nicolargo Date: Sat, 19 Aug 2023 11:06:32 +0200 Subject: [PATCH 1/2] Manage error while scanning folder --- glances/folder_list.py | 58 ++++++------------------------- glances/globals.py | 26 ++++++++++++++ glances/outputs/glances_curses.py | 1 + glances/plugins/folders/model.py | 12 +++---- setup.py | 5 ++- 5 files changed, 46 insertions(+), 56 deletions(-) diff --git a/glances/folder_list.py b/glances/folder_list.py index 135531d2..2dccab93 100644 --- a/glances/folder_list.py +++ b/glances/folder_list.py @@ -13,22 +13,9 @@ from __future__ import unicode_literals import os from glances.timer import Timer -from glances.globals import nativestr +from glances.globals import nativestr, folder_size from glances.logger import logger -# Use the built-in version of scandir/walk if possible, otherwise -# use the scandir module version -scandir_tag = True -try: - # For Python 3.5 or higher - from os import scandir -except ImportError: - # For others... - try: - from scandir import scandir - except ImportError: - scandir_tag = False - class FolderList(object): @@ -62,12 +49,9 @@ class FolderList(object): self.first_grab = True if self.config is not None and self.config.has_section('folders'): - if scandir_tag: - # Process monitoring list - logger.debug("Folder list configuration detected") - self.__set_folder_list('folders') - else: - logger.error('Scandir not found. Please use Python 3.5+ or install the scandir lib') + # Process monitoring list + logger.debug("Folder list configuration detected") + self.__set_folder_list('folders') else: self.__folder_list = [] @@ -132,23 +116,6 @@ class FolderList(object): else: return None - def __folder_size(self, path): - """Return the size of the directory given by path - - path: """ - - ret = 0 - for f in scandir(path): - if f.is_dir(follow_symlinks=False) and (f.name != '.' or f.name != '..'): - ret += self.__folder_size(os.path.join(path, f.name)) - else: - try: - ret += f.stat().st_size - except OSError: - pass - - return ret - def update(self, key='path'): """Update the command result attributed.""" # Only continue if monitor list is not empty @@ -163,16 +130,13 @@ class FolderList(object): # Set the key (see issue #2327) self.__folder_list[i]['key'] = key # Get folder size - try: - self.__folder_list[i]['size'] = self.__folder_size(self.path(i)) - except OSError as e: - logger.debug('Cannot get folder size ({}). Error: {}'.format(self.path(i), e)) - if e.errno == 13: - # Permission denied - self.__folder_list[i]['size'] = '!' - else: - self.__folder_list[i]['size'] = '?' - # Reset the timer + self.__folder_list[i]['size'], self.__folder_list[i]['errno'] = folder_size(self.path(i)) + if self.__folder_list[i]['errno'] != 0: + logger.debug('Folder size ({} ~ {}) may not be correct. Error: {}'.format( + self.path(i), + self.__folder_list[i]['size'], + self.__folder_list[i]['errno'])) + # Reset the timer self.timer_folders[i].reset() # It is no more the first time... diff --git a/glances/globals.py b/glances/globals.py index c539c9b5..87a48817 100644 --- a/glances/globals.py +++ b/glances/globals.py @@ -372,3 +372,29 @@ def string_value_to_float(s): def file_exists(filename): """Return True if the file exists and is readable.""" return os.path.isfile(filename) and os.access(filename, os.R_OK) + + +def folder_size(path, errno=0): + """Return a tuple with the size of the directory given by path and the errno. + If an error occurs (for example one file or subfolder is not accessible), + errno is set to the error number. + + path: + errno: Should always be 0 when calling the function""" + ret_size = 0 + ret_err = errno + try: + f_list = os.scandir(path) + except OSError as e: + return 0, e.errno + for f in f_list: + if f.is_dir(follow_symlinks=False) and (f.name != '.' or f.name != '..'): + ret = folder_size(os.path.join(path, f.name), ret_err) + ret_size += ret[0] + ret_err = ret[1] + else: + try: + ret_size += f.stat().st_size + except OSError as e: + ret_err = e.errno + return ret_size, ret_err diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index d598e393..2fd00f01 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -330,6 +330,7 @@ class _GlancesCurses(object): 'PASSWORD': curses.A_PROTECT, 'SELECTED': self.selected_color, 'INFO': self.ifINFO_color, + 'ERROR': self.selected_color, } def set_cursor(self, value): diff --git a/glances/plugins/folders/model.py b/glances/plugins/folders/model.py index e1d8328b..7c5a098e 100644 --- a/glances/plugins/folders/model.py +++ b/glances/plugins/folders/model.py @@ -64,8 +64,8 @@ class PluginModel(GlancesPluginModel): def get_alert(self, stat, header=""): """Manage limits of the folder list.""" - if not isinstance(stat['size'], numbers.Number): - ret = 'DEFAULT' + if stat['errno'] != 0: + ret = 'ERROR' else: ret = 'OK' @@ -108,15 +108,15 @@ class PluginModel(GlancesPluginModel): ret.append(self.curse_new_line()) if len(i['path']) > name_max_width: # Cut path if it is too long - path = '_' + i['path'][-name_max_width + 1 :] + path = '_' + i['path'][-name_max_width + 1:] else: path = i['path'] msg = '{:{width}}'.format(nativestr(path), width=name_max_width) ret.append(self.curse_add_line(msg)) - try: + if i['errno'] != 0: + msg = '?{:>8}'.format(self.auto_unit(i['size'])) + else: msg = '{:>9}'.format(self.auto_unit(i['size'])) - except (TypeError, ValueError): - msg = '{:>9}'.format(i['size']) ret.append(self.curse_add_line(msg, self.get_alert(i, header='folder_' + i['indice']))) return ret diff --git a/setup.py b/setup.py index bb923f01..917e6cdc 100755 --- a/setup.py +++ b/setup.py @@ -9,8 +9,8 @@ from io import open from setuptools import setup, Command -if sys.version_info < (3, 4): - print('Glances requires at least Python 3.4 to run.') +if sys.version_info < (3, 8): + print('Glances requires at least Python 3.8 to run.') sys.exit(1) # Global functions @@ -60,7 +60,6 @@ def get_install_extras_require(): 'graphitesender', 'influxdb>=1.0.0', 'influxdb-client', 'pymongo', 'kafka-python', 'pika', 'paho-mqtt', 'potsdb', 'prometheus_client', 'pyzmq', 'statsd'], - 'folders': ['scandir'], 'gpu': ['py3nvml'], 'graph': ['pygal'], 'ip': ['netifaces'], From 990abccf9a3ccd7a090bbb01da40628bb5c6d639 Mon Sep 17 00:00:00 2001 From: nicolargo Date: Sat, 19 Aug 2023 17:51:28 +0200 Subject: [PATCH 2/2] Folders plugin always fails on special directories #2518 --- glances/outputs/static/css/style.scss | 4 ++++ .../static/js/components/plugin-folders.vue | 8 ++++++-- glances/outputs/static/public/glances.js | Bin 448339 -> 448470 bytes 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/glances/outputs/static/css/style.scss b/glances/outputs/static/css/style.scss index 2feafa6f..40c8c150 100644 --- a/glances/outputs/static/css/style.scss +++ b/glances/outputs/static/css/style.scss @@ -110,6 +110,10 @@ body { color: white; font-weight: bold; } +.error { + color: #EE6600; + font-weight: bold; +} /* Plugins */ #processlist-plugin .table-cell { diff --git a/glances/outputs/static/js/components/plugin-folders.vue b/glances/outputs/static/js/components/plugin-folders.vue index 7e6f704b..7422229e 100644 --- a/glances/outputs/static/js/components/plugin-folders.vue +++ b/glances/outputs/static/js/components/plugin-folders.vue @@ -9,6 +9,9 @@
{{ folder.path }}
+ + ? + {{ $filters.bytes(folder.size) }}
@@ -31,6 +34,7 @@ export default { return { path: folderData['path'], size: folderData['size'], + errno: folderData['errno'], careful: folderData['careful'], warning: folderData['warning'], critical: folderData['critical'] @@ -40,8 +44,8 @@ export default { }, methods: { getDecoration(folder) { - if (!Number.isInteger(folder.size)) { - return; + if (folder.errno > 0) { + return 'error'; } if (folder.critical !== null && folder.size > folder.critical * 1000000) { return 'critical'; diff --git a/glances/outputs/static/public/glances.js b/glances/outputs/static/public/glances.js index 891999b957350b543ee58ea93efd769cb59cacff..eebe9c505715845134791506d239ff8e210494db 100644 GIT binary patch delta 7395 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 delta 7550 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