Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/build/sage_bootstrap/download/mirror_list.py
4055 views
1
# -*- coding: utf-8 -*-
2
"""
3
Access the List of Sage Download Mirrors
4
"""
5
6
#*****************************************************************************
7
# Copyright (C) 2014-2016 Volker Braun <[email protected]>
8
# 2015 Jeroen Demeyer
9
# 2023 Matthias Koeppe
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 2 of the License, or
14
# (at your option) any later version.
15
# http://www.gnu.org/licenses/
16
#*****************************************************************************
17
18
import os
19
import contextlib
20
import logging
21
log = logging.getLogger()
22
23
from sage_bootstrap.compat import urllib, urlparse
24
from sage_bootstrap.env import SAGE_DISTFILES, SAGE_ROOT
25
26
from fcntl import flock, LOCK_SH, LOCK_EX
27
from errno import ENOLCK
28
29
30
def try_lock(fd, operation):
31
"""
32
Try flock() but ignore ``ENOLCK`` errors, which could happen if the
33
file system does not support locking.
34
"""
35
try:
36
flock(fd, operation)
37
except IOError as e:
38
if e.errno != ENOLCK:
39
raise
40
41
42
class MirrorListException(RuntimeError):
43
pass
44
45
46
class MirrorList(object):
47
48
def __init__(self):
49
self.sources = []
50
upstream_d = os.path.join(SAGE_ROOT, '.upstream.d')
51
for fname in sorted(os.listdir(upstream_d)):
52
if '~' in fname or '#' in fname:
53
# Ignore auto-save and backup files
54
continue
55
try:
56
with open(os.path.join(upstream_d, fname), 'r') as f:
57
for line in f:
58
line = line.strip()
59
if line.startswith('#'):
60
continue
61
if not line:
62
continue
63
line = line.replace('${SAGE_ROOT}', SAGE_ROOT)
64
line = line.replace('${SAGE_DISTFILES}', SAGE_DISTFILES)
65
if '${SAGE_SERVER}' in line:
66
SAGE_SERVER = os.environ.get("SAGE_SERVER", "")
67
if not SAGE_SERVER:
68
continue
69
line = line.replace('${SAGE_SERVER}', SAGE_SERVER)
70
if line.endswith('mirror_list'):
71
cache_filename = os.path.join(SAGE_DISTFILES, line.rpartition('/')[2])
72
self.sources.append(MirrorList_from_url(line, cache_filename))
73
else:
74
self.sources.append([line])
75
except IOError:
76
# Silently ignore files that do not exist
77
pass
78
79
def __iter__(self):
80
"""
81
Iterate through the list of mirrors.
82
83
This is the main entry point into the mirror list. Every
84
script should just use this function to try mirrors in order
85
of preference. This will not just yield the official mirrors,
86
but also urls for packages that are currently being tested.
87
"""
88
for source in self.sources:
89
for mirror in source:
90
yield mirror
91
92
93
class MirrorList_from_url(object):
94
95
MAXAGE = 24*60*60 # seconds
96
97
def __init__(self, url, filename):
98
self.url = url
99
self.filename = filename
100
self._mirrors = None
101
102
@property
103
def mirrors(self):
104
if self._mirrors is not None:
105
return self._mirrors
106
107
try:
108
self.mirrorfile = open(self.filename, 'r+t')
109
except IOError:
110
self.mirrorfile = open(self.filename, 'w+t')
111
112
with self.mirrorfile:
113
self.mirrorfd = self.mirrorfile.fileno()
114
try_lock(self.mirrorfd, LOCK_SH) # shared (read) lock
115
if self._must_refresh():
116
try_lock(self.mirrorfd, LOCK_EX) # exclusive (write) lock
117
# Maybe the mirror list file was updated by a different
118
# process while we waited for the lock? Check again.
119
if self._must_refresh():
120
self._refresh()
121
if self._mirrors is None:
122
self._mirrors = self._load()
123
124
return self._mirrors
125
126
def _load(self, mirror_list=None):
127
"""
128
Load and return `mirror_list` (defaults to the one on disk) as
129
a list of strings
130
"""
131
if mirror_list is None:
132
try:
133
self.mirrorfile.seek(0)
134
mirror_list = self.mirrorfile.read()
135
except IOError:
136
log.critical('Failed to load the cached mirror list')
137
return []
138
if mirror_list == '':
139
return []
140
import ast
141
try:
142
return ast.literal_eval(mirror_list)
143
except SyntaxError:
144
log.critical('Downloaded mirror list has syntax error: {0}'.format(mirror_list))
145
return []
146
147
def _save(self):
148
"""
149
Save the mirror list for (short-term) future use.
150
"""
151
self.mirrorfile.seek(0)
152
self.mirrorfile.write(repr(self.mirrors))
153
self.mirrorfile.truncate()
154
self.mirrorfile.flush()
155
156
def _port_of_mirror(self, mirror):
157
if mirror.startswith('http://'):
158
return 80
159
if mirror.startswith('https://'):
160
return 443
161
if mirror.startswith('ftp://'):
162
return 21
163
# Sensible default (invalid mirror?)
164
return 80
165
166
def _rank_mirrors(self):
167
"""
168
Sort the mirrors by speed, fastest being first
169
170
This method is used by the YUM fastestmirror plugin
171
"""
172
timed_mirrors = []
173
import time
174
import socket
175
log.info('Searching fastest mirror')
176
timeout = 1
177
for mirror in self.mirrors:
178
if not mirror.startswith('http'):
179
log.debug('we currently can only handle http, got %s', mirror)
180
continue
181
port = self._port_of_mirror(mirror)
182
mirror_hostname = urlparse.urlsplit(mirror).netloc
183
time_before = time.time()
184
try:
185
sock = socket.create_connection((mirror_hostname, port), timeout)
186
sock.close()
187
except (IOError, socket.error, socket.timeout) as err:
188
log.warning(str(err).strip() + ': ' + mirror)
189
continue
190
result = time.time() - time_before
191
result_ms = int(1000 * result)
192
log.info(str(result_ms).rjust(5) + 'ms: ' + mirror)
193
timed_mirrors.append((result, mirror))
194
timed_mirrors.sort()
195
if len(timed_mirrors) >= 5 and timed_mirrors[4][0] < 0.3:
196
# We don't need more than 5 decent mirrors
197
break
198
199
if len(timed_mirrors) == 0:
200
# We cannot reach any mirror directly, most likely firewall issue
201
if 'http_proxy' not in os.environ:
202
log.error('Could not reach any mirror directly and no proxy set')
203
raise MirrorListException('Failed to connect to any mirror, probably no internet connection')
204
log.info('Cannot time mirrors via proxy, using default order')
205
else:
206
self._mirrors = [m[1] for m in timed_mirrors]
207
log.info('Fastest mirror: ' + self.fastest)
208
209
def _age(self):
210
"""
211
Return the age of the cached mirror list in seconds
212
"""
213
import time
214
mtime = os.fstat(self.mirrorfd).st_mtime
215
now = time.mktime(time.localtime())
216
return now - mtime
217
218
def _must_refresh(self):
219
"""
220
Return whether we must download the mirror list.
221
222
If and only if this method returns ``False`` is it admissible
223
to use the cached mirror list.
224
"""
225
if os.fstat(self.mirrorfd).st_size == 0:
226
return True
227
return self._age() > self.MAXAGE
228
229
def _refresh(self):
230
"""
231
Download and rank the mirror list.
232
"""
233
log.info('Downloading the Sage mirror list')
234
try:
235
with contextlib.closing(urllib.urlopen(self.url)) as f:
236
mirror_list = f.read().decode("ascii")
237
except IOError:
238
log.critical('Downloading the mirror list failed, using cached version')
239
else:
240
self._mirrors = self._load(mirror_list)
241
self._rank_mirrors()
242
self._save()
243
244
def __iter__(self):
245
"""
246
Iterate through the list of mirrors.
247
248
This is the main entry point into the mirror list. Every
249
script should just use this function to try mirrors in order
250
of preference. This will not just yield the official mirrors,
251
but also urls for packages that are currently being tested.
252
"""
253
try:
254
yield os.environ['SAGE_SERVER']
255
except KeyError:
256
pass
257
for mirror in self.mirrors:
258
if not mirror.endswith('/'):
259
mirror += '/'
260
yield mirror + '/'.join(['spkg', 'upstream', '${SPKG}'])
261
262
@property
263
def fastest(self):
264
return next(iter(self))
265
266