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