summaryrefslogtreecommitdiffstats
path: root/glances/outdated.py
blob: b5816b39e0fa5d50cfd32e4e2f25a98a0bd1e4d9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2019 Nicolargo <nicolas@nicolargo.com>
#
# Glances is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Glances is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Manage Glances update."""

from datetime import datetime, timedelta
import threading
import json
import pickle
import os
from ssl import CertificateError

from glances import __version__
from glances.compat import nativestr, urlopen, HTTPError, URLError
from glances.config import user_cache_dir
from glances.globals import safe_makedirs
from glances.logger import logger

try:
    from packaging.version import Version
    PACKAGING_IMPORT = True
except Exception as e:
    logger.error("Unable to import 'packaging' module ({}). Glances cannot check for updates.".format(e))
    PACKAGING_IMPORT = False

PYPI_API_URL = 'https://pypi.python.org/pypi/Glances/json'


class Outdated(object):

    """
    This class aims at providing methods to warn the user when a new Glances
    version is available on the PyPI repository (https://pypi.python.org/pypi/Glances/).
    """

    def __init__(self, args, config):
        """Init the Outdated class"""
        self.args = args
        self.config = config
        self.cache_dir = user_cache_dir()
        self.cache_file = os.path.join(self.cache_dir, 'glances-version.db')

        # Set default value...
        self.data = {u'installed_version': __version__, u'latest_version': '0.0', u'refresh_date': datetime.now()}
        # Read the configuration file
        self.load_config(config)
        if not PACKAGING_IMPORT:
            self.args.disable_check_update = False
        logger.debug("Check Glances version up-to-date: {}".format(not self.args.disable_check_update))

        # And update !
        self.get_pypi_version()

    def load_config(self, config):
        """Load outdated parameter in the global section of the configuration file."""

        global_section = 'global'
        if hasattr(config, 'has_section') and config.has_section(global_section):
            self.args.disable_check_update = config.get_value(global_section, 'check_update').lower() == 'false'
        else:
            logger.debug("Cannot find section {} in the configuration file".format(global_section))
            return False

        return True

    def installed_version(self):
        return self.data['installed_version']

    def latest_version(self):
        return self.data['latest_version']

    def refresh_date(self):
        return self.data['refresh_date']

    def get_pypi_version(self):
        """Wrapper to get the latest PyPI version (async)

        The data are stored in a cached file
        Only update online once a week
        """
        if self.args.disable_check_update:
            return

        # If the cached file exist, read-it
        cached_data = self._load_cache()

        if cached_data == {}:
            # Update needed
            # Update and save the cache
            thread = threading.Thread(target=self._update_pypi_version)
            thread.start()
        else:
            # Update not needed
            self.data['latest_version'] = cached_data['latest_version']
            logger.debug("Get Glances version from cache file")

    def is_outdated(self):
        """Return True if a new version is available"""
        if self.args.disable_check_update:
            # Check is disabled by configuration
            return False

        logger.debug(
            "Check Glances version (installed: {} / latest: {})".format(self.installed_version(), self.latest_version())
        )
        return Version(self.latest_version()) > Version(self.installed_version())

    def _load_cache(self):
        """Load cache file and return cached data"""
        # If the cached file exist, read-it
        max_refresh_date = timedelta(days=7)
        cached_data = {}
        try:
            with open(self.cache_file, 'rb') as f:
                cached_data = pickle.load(f)
        except Exception as e:
            logger.debug("Cannot read version from cache file: {} ({})".format(self.cache_file, e))
        else:
            logger.debug("Read version from cache file")
            if (
                cached_data['installed_version'] != self.installed_version()
                or datetime.now() - cached_data['refresh_date'] > max_refresh_date
            ):
                # Reset the cache if:
                # - the installed version is different
                # - the refresh_date is > max_refresh_date
                cached_data = {}
        return cached_data

    def _save_cache(self):
        """Save data to the cache file."""
        # Create the cache directory
        safe_makedirs(self.cache_dir)

        # Create/overwrite the cache file
        try:
            with open(self.cache_file, 'wb') as f:
                pickle.dump(self.data, f)
        except Exception as e:
            logger.error("Cannot write version to cache file {} ({})".format(self.cache_file, e))

    def _update_pypi_version(self):
        """Get the latest PyPI version (as a string) via the RESTful JSON API"""
        logger.debug("Get latest Glances version from the PyPI RESTful API ({})".format(PYPI_API_URL))

        # Update the current time
        self.data[u'refresh_date'] = datetime.now()

        try:
            res = urlopen(PYPI_API_URL, timeout=3).read()
        except (HTTPError, URLError, CertificateError) as e:
            logger.debug("Cannot get Glances version from the PyPI RESTful API ({})".format(e))
        else:
            self.data[u'latest_version'] = json.loads(nativestr(res))['info']['version']
            logger.debug("Save Glances version to the cache file")

        # Save result to the cache file
        # Note: also saved if the Glances PyPI version cannot be grabbed
        self._save_cache()

        return self.data