1from __future__ import unicode_literals
2
3import sys
4import os
5import time
6import requests
7import colorama
8
9import dvc.logger as logger
10from dvc import VERSION_BASE
11from dvc.lock import Lock, LockError
12from dvc.utils import is_binary
13
14
15class Updater(object):  # pragma: no cover
16    URL = "https://updater.dvc.org"
17    UPDATER_FILE = "updater"
18    TIMEOUT = 24 * 60 * 60  # every day
19    TIMEOUT_GET = 10
20
21    def __init__(self, dvc_dir):
22        self.dvc_dir = dvc_dir
23        self.updater_file = os.path.join(dvc_dir, self.UPDATER_FILE)
24        self.lock = Lock(dvc_dir, self.updater_file + ".lock")
25        self.current = VERSION_BASE
26
27    def _is_outdated_file(self):
28        ctime = os.path.getmtime(self.updater_file)
29        outdated = time.time() - ctime >= self.TIMEOUT
30        if outdated:
31            logger.debug("'{}' is outdated(".format(self.updater_file))
32        return outdated
33
34    def _with_lock(self, func, action):
35        try:
36            with self.lock:
37                func()
38        except LockError:
39            msg = "Failed to acquire '{}' before {} updates"
40            logger.debug(msg.format(self.lock.lock_file, action))
41
42    def check(self):
43        if os.getenv("CI") or os.getenv("DVC_TEST"):
44            return
45
46        self._with_lock(self._check, "checking")
47
48    def _check(self):
49        if not os.path.exists(self.updater_file) or self._is_outdated_file():
50            self.fetch()
51            return
52
53        with open(self.updater_file, "r") as fobj:
54            import json
55
56            try:
57                info = json.load(fobj)
58                self.latest = info["version"]
59            except Exception as exc:
60                msg = "'{}' is not a valid json: {}"
61                logger.debug(msg.format(self.updater_file, exc))
62                self.fetch()
63                return
64
65        if self._is_outdated():
66            self._notify()
67
68    def fetch(self, detach=True):
69        from dvc.daemon import daemon
70
71        if detach:
72            daemon(["updater"])
73            return
74
75        self._with_lock(self._get_latest_version, "fetching")
76
77    def _get_latest_version(self):
78        import json
79
80        try:
81            r = requests.get(self.URL, timeout=self.TIMEOUT_GET)
82            info = r.json()
83        except requests.exceptions.RequestException as exc:
84            msg = "Failed to retrieve latest version: {}"
85            logger.debug(msg.format(exc))
86            return
87
88        with open(self.updater_file, "w+") as fobj:
89            json.dump(info, fobj)
90
91    def _is_outdated(self):
92        l_major, l_minor, l_patch = [int(x) for x in self.latest.split(".")]
93        c_major, c_minor, c_patch = [int(x) for x in self.current.split(".")]
94
95        if l_major != c_major:
96            return l_major > c_major
97
98        if l_minor != c_minor:
99            return l_minor > c_minor
100
101        return l_patch > c_patch
102
103    def _notify(self):
104        if not sys.stdout.isatty():
105            return
106
107        message = (
108            "Update available {red}{current}{reset} -> {green}{latest}{reset}"
109            + "\n"
110            + self._get_update_instructions()
111        ).format(
112            red=colorama.Fore.RED,
113            reset=colorama.Fore.RESET,
114            green=colorama.Fore.GREEN,
115            yellow=colorama.Fore.YELLOW,
116            blue=colorama.Fore.BLUE,
117            current=self.current,
118            latest=self.latest,
119        )
120
121        logger.box(message, border_color="yellow")
122
123    def _get_update_instructions(self):
124        instructions = {
125            "pip": "Run {yellow}pip{reset} install dvc {blue}--upgrade{reset}",
126            "yum": "Run {yellow}yum{reset} update dvc",
127            "yay": "Run {yellow}yay{reset} {blue}-S{reset} dvc",
128            "formula": "Run {yellow}brew{reset} upgrade dvc",
129            "cask": "Run {yellow}brew cask{reset} upgrade dvc",
130            "apt": (
131                "Run {yellow}apt-get{reset} install"
132                " {blue}--only-upgrade{reset} dvc"
133            ),
134            "binary": (
135                "To upgrade follow this steps:\n"
136                "1. Uninstall dvc binary\n"
137                "2. Go to {blue}https://dvc.org{reset}\n"
138                "3. Download and install new binary"
139            ),
140            None: (
141                "Find the latest release at\n{blue}"
142                "https://github.com/iterative/dvc/releases/latest"
143                "{reset}"
144            ),
145        }
146
147        package_manager = self._get_package_manager()
148
149        return instructions[package_manager]
150
151    def _get_linux(self):
152        import distro
153
154        if not is_binary():
155            return "pip"
156
157        package_managers = {
158            "rhel": "yum",
159            "centos": "yum",
160            "fedora": "yum",
161            "amazon": "yum",
162            "opensuse": "yum",
163            "ubuntu": "apt",
164            "debian": "apt",
165        }
166
167        return package_managers.get(distro.id())
168
169    def _get_darwin(self):
170        if not is_binary():
171            if __file__.startswith("/usr/local/Cellar"):
172                return "formula"
173            else:
174                return "pip"
175
176        # NOTE: both pkg and cask put dvc binary into /usr/local/bin,
177        # so in order to know which method of installation was used,
178        # we need to actually call `brew cask`
179        ret = os.system("brew cask ls dvc")
180        if ret == 0:
181            return "cask"
182
183        return None
184
185    def _get_windows(self):
186        return None if is_binary() else "pip"
187
188    def _get_package_manager(self):
189        import platform
190        from dvc.exceptions import DvcException
191
192        m = {
193            "Windows": self._get_windows,
194            "Darwin": self._get_darwin,
195            "Linux": self._get_linux,
196        }
197
198        system = platform.system()
199        func = m.get(system)
200        if func is None:
201            raise DvcException("not supported system '{}'".format(system))
202
203        return func()
204