glances.outdated.Outdated.refresh_date()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""Manage Glances update."""
10
11
import json
12
import os
13
import pickle
14
import threading
15
from datetime import datetime, timedelta
16
from ssl import CertificateError
17
18
from glances import __version__
19
from glances.config import user_cache_dir
20
from glances.globals import HTTPError, URLError, nativestr, safe_makedirs, urlopen
21
from glances.logger import logger
22
23
try:
24
    from packaging.version import Version
25
26
    PACKAGING_IMPORT = True
27
except Exception as e:
28
    logger.warning(f"Unable to import 'packaging' module ({e}). Glances cannot check for updates.")
29
    PACKAGING_IMPORT = False
30
31
PYPI_API_URL = 'https://pypi.python.org/pypi/Glances/json'
32
33
34
class Outdated:
35
    """
36
    This class aims at providing methods to warn the user when a new Glances
37
    version is available on the PyPI repository (https://pypi.python.org/pypi/Glances/).
38
    """
39
40
    def __init__(self, args, config):
41
        """Init the Outdated class"""
42
        self.args = args
43
        self.config = config
44
        self.cache_dir = user_cache_dir()[0]
45
        self.cache_file = os.path.join(self.cache_dir, 'glances-version.db')
46
47
        # Set default value...
48
        self.data = {'installed_version': __version__, 'latest_version': '0.0', 'refresh_date': datetime.now()}
49
50
        # Disable update check if `packaging` is not installed
51
        if not PACKAGING_IMPORT:
52
            self.args.disable_check_update = True
53
54
        # Read the configuration file only if update check is not explicitly disabled
55
        if not self.args.disable_check_update:
56
            self.load_config(config)
57
58
        logger.debug(f"Check Glances version up-to-date: {not self.args.disable_check_update}")
59
60
        # And update !
61
        self.get_pypi_version()
62
63
    def load_config(self, config):
64
        """Load outdated parameter in the global section of the configuration file."""
65
66
        global_section = 'global'
67
        if hasattr(config, 'has_section') and config.has_section(global_section):
68
            self.args.disable_check_update = config.get_value(global_section, 'check_update').lower() == 'false'
69
        else:
70
            logger.debug(f"Cannot find section {global_section} in the configuration file")
71
            return False
72
73
        return True
74
75
    def installed_version(self):
76
        return self.data['installed_version']
77
78
    def latest_version(self):
79
        return self.data['latest_version']
80
81
    def refresh_date(self):
82
        return self.data['refresh_date']
83
84
    def get_pypi_version(self):
85
        """Wrapper to get the latest PyPI version (async)
86
87
        The data are stored in a cached file
88
        Only update online once a week
89
        """
90
        if self.args.disable_check_update:
91
            return
92
93
        # If the cached file exist, read-it
94
        cached_data = self._load_cache()
95
96
        if cached_data == {}:
97
            # Update needed
98
            # Update and save the cache
99
            thread = threading.Thread(target=self._update_pypi_version)
100
            thread.start()
101
        else:
102
            # Update not needed
103
            self.data['latest_version'] = cached_data['latest_version']
104
            logger.debug("Get Glances version from cache file")
105
106
    def is_outdated(self):
107
        """Return True if a new version is available"""
108
        if self.args.disable_check_update:
109
            # Check is disabled by configuration
110
            return False
111
112
        logger.debug(f"Check Glances version (installed: {self.installed_version()} / latest: {self.latest_version()})")
113
        return Version(self.latest_version()) > Version(self.installed_version())
114
115
    def _load_cache(self):
116
        """Load cache file and return cached data"""
117
        # If the cached file exist, read-it
118
        max_refresh_date = timedelta(days=7)
119
        cached_data = {}
120
        try:
121
            with open(self.cache_file, 'rb') as f:
122
                cached_data = pickle.load(f)
123
        except Exception as e:
124
            logger.debug(f"Cannot read version from cache file: {self.cache_file} ({e})")
125
        else:
126
            logger.debug("Read version from cache file")
127
            if (
128
                cached_data['installed_version'] != self.installed_version()
129
                or datetime.now() - cached_data['refresh_date'] > max_refresh_date
130
            ):
131
                # Reset the cache if:
132
                # - the installed version is different
133
                # - the refresh_date is > max_refresh_date
134
                cached_data = {}
135
        return cached_data
136
137
    def _save_cache(self):
138
        """Save data to the cache file."""
139
        # Create the cache directory
140
        safe_makedirs(self.cache_dir)
141
142
        # Create/overwrite the cache file
143
        try:
144
            with open(self.cache_file, 'wb') as f:
145
                pickle.dump(self.data, f)
146
        except Exception as e:
147
            logger.error(f"Cannot write version to cache file {self.cache_file} ({e})")
148
149
    def _update_pypi_version(self):
150
        """Get the latest PyPI version (as a string) via the RESTful JSON API"""
151
        logger.debug(f"Get latest Glances version from the PyPI RESTful API ({PYPI_API_URL})")
152
153
        # Update the current time
154
        self.data['refresh_date'] = datetime.now()
155
156
        try:
157
            res = urlopen(PYPI_API_URL, timeout=3).read()
158
        except (HTTPError, URLError, CertificateError) as e:
159
            logger.debug(f"Cannot get Glances version from the PyPI RESTful API ({e})")
160
        else:
161
            self.data['latest_version'] = json.loads(nativestr(res))['info']['version']
162
            logger.debug("Save Glances version to the cache file")
163
164
        # Save result to the cache file
165
        # Note: also saved if the Glances PyPI version cannot be grabbed
166
        self._save_cache()
167
168
        return self.data
169