Path: blob/develop/build/sage_bootstrap/download/mirror_list.py
4055 views
# -*- coding: utf-8 -*-1"""2Access the List of Sage Download Mirrors3"""45#*****************************************************************************6# Copyright (C) 2014-2016 Volker Braun <[email protected]>7# 2015 Jeroen Demeyer8# 2023 Matthias Koeppe9#10# This program is free software: you can redistribute it and/or modify11# it under the terms of the GNU General Public License as published by12# the Free Software Foundation, either version 2 of the License, or13# (at your option) any later version.14# http://www.gnu.org/licenses/15#*****************************************************************************1617import os18import contextlib19import logging20log = logging.getLogger()2122from sage_bootstrap.compat import urllib, urlparse23from sage_bootstrap.env import SAGE_DISTFILES, SAGE_ROOT2425from fcntl import flock, LOCK_SH, LOCK_EX26from errno import ENOLCK272829def try_lock(fd, operation):30"""31Try flock() but ignore ``ENOLCK`` errors, which could happen if the32file system does not support locking.33"""34try:35flock(fd, operation)36except IOError as e:37if e.errno != ENOLCK:38raise394041class MirrorListException(RuntimeError):42pass434445class MirrorList(object):4647def __init__(self):48self.sources = []49upstream_d = os.path.join(SAGE_ROOT, '.upstream.d')50for fname in sorted(os.listdir(upstream_d)):51if '~' in fname or '#' in fname:52# Ignore auto-save and backup files53continue54try:55with open(os.path.join(upstream_d, fname), 'r') as f:56for line in f:57line = line.strip()58if line.startswith('#'):59continue60if not line:61continue62line = line.replace('${SAGE_ROOT}', SAGE_ROOT)63line = line.replace('${SAGE_DISTFILES}', SAGE_DISTFILES)64if '${SAGE_SERVER}' in line:65SAGE_SERVER = os.environ.get("SAGE_SERVER", "")66if not SAGE_SERVER:67continue68line = line.replace('${SAGE_SERVER}', SAGE_SERVER)69if line.endswith('mirror_list'):70cache_filename = os.path.join(SAGE_DISTFILES, line.rpartition('/')[2])71self.sources.append(MirrorList_from_url(line, cache_filename))72else:73self.sources.append([line])74except IOError:75# Silently ignore files that do not exist76pass7778def __iter__(self):79"""80Iterate through the list of mirrors.8182This is the main entry point into the mirror list. Every83script should just use this function to try mirrors in order84of preference. This will not just yield the official mirrors,85but also urls for packages that are currently being tested.86"""87for source in self.sources:88for mirror in source:89yield mirror909192class MirrorList_from_url(object):9394MAXAGE = 24*60*60 # seconds9596def __init__(self, url, filename):97self.url = url98self.filename = filename99self._mirrors = None100101@property102def mirrors(self):103if self._mirrors is not None:104return self._mirrors105106try:107self.mirrorfile = open(self.filename, 'r+t')108except IOError:109self.mirrorfile = open(self.filename, 'w+t')110111with self.mirrorfile:112self.mirrorfd = self.mirrorfile.fileno()113try_lock(self.mirrorfd, LOCK_SH) # shared (read) lock114if self._must_refresh():115try_lock(self.mirrorfd, LOCK_EX) # exclusive (write) lock116# Maybe the mirror list file was updated by a different117# process while we waited for the lock? Check again.118if self._must_refresh():119self._refresh()120if self._mirrors is None:121self._mirrors = self._load()122123return self._mirrors124125def _load(self, mirror_list=None):126"""127Load and return `mirror_list` (defaults to the one on disk) as128a list of strings129"""130if mirror_list is None:131try:132self.mirrorfile.seek(0)133mirror_list = self.mirrorfile.read()134except IOError:135log.critical('Failed to load the cached mirror list')136return []137if mirror_list == '':138return []139import ast140try:141return ast.literal_eval(mirror_list)142except SyntaxError:143log.critical('Downloaded mirror list has syntax error: {0}'.format(mirror_list))144return []145146def _save(self):147"""148Save the mirror list for (short-term) future use.149"""150self.mirrorfile.seek(0)151self.mirrorfile.write(repr(self.mirrors))152self.mirrorfile.truncate()153self.mirrorfile.flush()154155def _port_of_mirror(self, mirror):156if mirror.startswith('http://'):157return 80158if mirror.startswith('https://'):159return 443160if mirror.startswith('ftp://'):161return 21162# Sensible default (invalid mirror?)163return 80164165def _rank_mirrors(self):166"""167Sort the mirrors by speed, fastest being first168169This method is used by the YUM fastestmirror plugin170"""171timed_mirrors = []172import time173import socket174log.info('Searching fastest mirror')175timeout = 1176for mirror in self.mirrors:177if not mirror.startswith('http'):178log.debug('we currently can only handle http, got %s', mirror)179continue180port = self._port_of_mirror(mirror)181mirror_hostname = urlparse.urlsplit(mirror).netloc182time_before = time.time()183try:184sock = socket.create_connection((mirror_hostname, port), timeout)185sock.close()186except (IOError, socket.error, socket.timeout) as err:187log.warning(str(err).strip() + ': ' + mirror)188continue189result = time.time() - time_before190result_ms = int(1000 * result)191log.info(str(result_ms).rjust(5) + 'ms: ' + mirror)192timed_mirrors.append((result, mirror))193timed_mirrors.sort()194if len(timed_mirrors) >= 5 and timed_mirrors[4][0] < 0.3:195# We don't need more than 5 decent mirrors196break197198if len(timed_mirrors) == 0:199# We cannot reach any mirror directly, most likely firewall issue200if 'http_proxy' not in os.environ:201log.error('Could not reach any mirror directly and no proxy set')202raise MirrorListException('Failed to connect to any mirror, probably no internet connection')203log.info('Cannot time mirrors via proxy, using default order')204else:205self._mirrors = [m[1] for m in timed_mirrors]206log.info('Fastest mirror: ' + self.fastest)207208def _age(self):209"""210Return the age of the cached mirror list in seconds211"""212import time213mtime = os.fstat(self.mirrorfd).st_mtime214now = time.mktime(time.localtime())215return now - mtime216217def _must_refresh(self):218"""219Return whether we must download the mirror list.220221If and only if this method returns ``False`` is it admissible222to use the cached mirror list.223"""224if os.fstat(self.mirrorfd).st_size == 0:225return True226return self._age() > self.MAXAGE227228def _refresh(self):229"""230Download and rank the mirror list.231"""232log.info('Downloading the Sage mirror list')233try:234with contextlib.closing(urllib.urlopen(self.url)) as f:235mirror_list = f.read().decode("ascii")236except IOError:237log.critical('Downloading the mirror list failed, using cached version')238else:239self._mirrors = self._load(mirror_list)240self._rank_mirrors()241self._save()242243def __iter__(self):244"""245Iterate through the list of mirrors.246247This is the main entry point into the mirror list. Every248script should just use this function to try mirrors in order249of preference. This will not just yield the official mirrors,250but also urls for packages that are currently being tested.251"""252try:253yield os.environ['SAGE_SERVER']254except KeyError:255pass256for mirror in self.mirrors:257if not mirror.endswith('/'):258mirror += '/'259yield mirror + '/'.join(['spkg', 'upstream', '${SPKG}'])260261@property262def fastest(self):263return next(iter(self))264265266