1 # Written by John Hoffman
2 # see LICENSE.txt for license information
4 # Patched by Diego Andres Rabaioli.
5 # This is the HTTPDownloader class that implements the GetRight
6 # style WebSeeding technique. Compared to the John Hoffman's style it
7 # doesn't require any web server support.However the biggest gap (see
8 # http://www.bittorrent.org/beps/bep_0019.html) is not taken into
9 # account when requesting pieces.
12 from random import randint
13 from urlparse import urlparse
14 from httplib import HTTPConnection
16 from threading import Thread,currentThread,Lock
17 from traceback import print_exc, print_stack
19 from BaseLib.Core.BitTornado.__init__ import product_name,version_short
20 from BaseLib.Core.BitTornado.CurrentRateMeasure import Measure
21 from BaseLib.Core.Utilities.timeouturlopen import find_proxy
26 from BaseLib.Core.ProxyService.Helper import SingleDownloadHelperInterface
28 class SingleDownloadHelperInterface:
39 VERSION = product_name+'/'+version_short
44 def __getitem__(self, x):
46 haveall = haveComplete()
48 class SingleDownload(SingleDownloadHelperInterface):
50 def __init__(self, downloader, url):
51 SingleDownloadHelperInterface.__init__(self)
52 self.downloader = downloader
56 (self.scheme, self.netloc, path, pars, query, fragment) = urlparse(url)
58 self.downloader.errorfunc('cannot parse http seed address: '+url)
60 if self.scheme != 'http':
61 self.downloader.errorfunc('http seed url not http: '+url)
64 # Arno, 2010-03-08: Make proxy aware
65 self.proxyhost = find_proxy(url)
67 if self.proxyhost is None:
68 self.connection = HTTPConnection(self.netloc)
70 self.connection = HTTPConnection(self.proxyhost)
72 self.downloader.errorfunc('cannot connect to http seed: '+url)
76 self.measure = Measure(downloader.max_rate_period)
78 self.piece_size = self.downloader.storage._piecelen( 0 )
79 self.total_len = self.downloader.storage.total_length
85 self.retry_period = 30
86 self._retry_period = None
90 self.cancelled = False
92 self.request_lock = Lock()
93 self.video_support_policy = True # TODO : get from constructor parameters
94 self.video_support_enabled = False # Don't start immediately with support
95 self.video_support_speed = 0.0 # Start with the faster rescheduling speed
96 self.video_support_slow_start = False # If enabled delay the first request (give chance to peers to give bandwidth)
97 # Arno, 2010-04-07: Wait 1 second before using HTTP seed. TODO good policy
98 # If Video Support policy is not eneabled then use Http seed normaly
99 if not self.video_support_policy:
103 def resched(self, len = None):
104 if self.video_support_policy:
105 if ( not self.video_support_enabled ) or self.video_support_slow_start:
108 len = self.retry_period
109 if self.errorcount > 3:
110 len = min(1.0,len) * (self.errorcount - 2)
112 # Arno, 2010-04-07: If immediately, don't go via queue. Actual work is
113 # done by other thread, so no worries of hogging NetworkThread.
115 self.downloader.rawserver.add_task(self.download, len)
119 def _want(self, index):
121 return self.downloader.storage.do_I_have_requests(index)
123 return self.downloader.storage.is_unstarted(index)
126 from BaseLib.Core.Session import Session
127 session = Session.get_instance()
128 session.uch.perform_usercallback(self._download)
132 self.request_lock.acquire()
134 print "http-sdownload: download()"
135 if self.is_frozen_by_helper():
137 print "http-sdownload: blocked, rescheduling"
141 self.cancelled = False
142 if self.downloader.picker.am_I_complete():
143 self.downloader.downloads.remove(self)
145 self.index = self.downloader.picker.next(haveall, self._want, self)
147 if self.index is None and self.frozen_by_helper:
151 if ( self.index is None and not self.endflag
152 and not self.downloader.peerdownloader.has_downloaders() ):
154 self.index = self.downloader.picker.next(haveall, self._want, self)
155 if self.index is None:
159 self.url = self.seedurl
160 start = self.piece_size * self.index
161 end = start + self.downloader.storage._piecelen( self.index ) - 1
162 self.request_range = '%d-%d' % ( start, end )
164 # Just overwrite other blocks and don't ask for ranges.
166 # Diego : 2010-05-19 : Moving thread creation on _download and not on
167 # _request anymore. One Lock handles sync problems between threads performing
168 # new requests before the previous response is read.
170 # Arno, 2010-04-07: Use threads from pool to Download, more efficient
171 # than creating a new one for every piece.
172 from BaseLib.Core.Session import Session
173 session = Session.get_instance()
174 session.uch.perform_usercallback(self._request)
176 rq = Thread(target = self._request)
177 rq.setName( "GetRightHTTPDownloader"+rq.getName() )
184 import encodings.ascii
185 import encodings.punycode
186 import encodings.idna
189 self.received_data = None
191 #print >>sys.stderr, 'HTTP piece ', self.index
192 if self.proxyhost is None:
195 realurl = self.scheme+'://'+self.netloc+self.url
197 self.connection.request( 'GET', realurl, None,
198 {'Host': self.netloc, 'User-Agent': VERSION, 'Range' : 'bytes=%s' % self.request_range } )
200 r = self.connection.getresponse()
201 self.connection_status = r.status
202 self.received_data = r.read()
207 self.error = 'error accessing http seed: '+str(e)
209 self.connection.close()
213 self.connection = HTTPConnection(self.netloc)
215 self.connection = None # will cause an exception and retry next cycle
216 self.downloader.rawserver.add_task(self.request_finished)
218 def request_finished(self):
220 if self.error is not None:
222 self.downloader.errorfunc(self.error)
224 if self.received_data:
226 if not self._got_data():
227 self.received_data = None
228 if not self.received_data:
229 self._release_requests()
230 self.downloader.peerdownloader.piece_flunked(self.index)
231 self.request_lock.release()
232 if self._retry_period is not None:
233 self.resched(self._retry_period)
234 self._retry_period = None
239 if self.connection_status == 503: # seed is busy
241 self.retry_period = max(int(self.received_data), 5)
246 if self.connection_status != 200 and self.connection_status != 206: # 206 = partial download OK
249 # Arno, 2010-04-07: retry_period set to 0 for faster DL speeds
250 # Diego, 2010-04-16: retry_period set depending on the level of support asked by the MovieOnDemandTransporter
251 self._retry_period = self.video_support_speed
253 if len(self.received_data) != self.request_size:
255 self.downloader.errorfunc('corrupt data from http seed - redownloading')
257 self.measure.update_rate(len(self.received_data))
258 self.downloader.measurefunc(len(self.received_data))
261 if not self._fulfill_requests():
263 if not self.goodseed:
265 self.downloader.seedsfound += 1
266 if self.downloader.storage.do_I_have(self.index):
267 self.downloader.picker.complete(self.index)
268 self.downloader.peerdownloader.check_complete(self.index)
269 self.downloader.gotpiecefunc(self.index)
272 def _get_requests(self):
274 self.request_size = 0L
275 while self.downloader.storage.do_I_have_requests(self.index):
276 r = self.downloader.storage.new_request(self.index)
277 self.requests.append(r)
278 self.request_size += r[1]
281 def _fulfill_requests(self):
285 begin, length = self.requests.pop(0)
287 if not self.downloader.storage.piece_came_in(self.index, begin, [],
288 self.received_data[start:start+length], length):
295 def _release_requests(self):
296 for begin, length in self.requests:
297 self.downloader.storage.request_lost(self.index, begin, length)
300 def _request_ranges(self):
302 begin, length = self.requests[0]
303 for begin1, length1 in self.requests[1:]:
304 if begin + length == begin1:
310 s += str(begin)+'-'+str(begin+length-1)
311 begin, length = begin1, length1
314 s += str(begin)+'-'+str(begin+length-1)
318 def helper_forces_unchoke(self):
321 def helper_set_freezing(self,val):
322 self.frozen_by_helper = val
325 def slow_start_wake_up( self ):
326 self.video_support_slow_start = False
329 def is_slow_start( self ):
330 return self.video_support_slow_start
332 def start_video_support( self, level = 0.0, sleep_time = None ):
334 Level indicates how fast a new request is scheduled and therefore the level of support required.
335 0 = maximum support. (immediate rescheduling)
336 1 ~= 0.01 seconds between each request
337 2 ~= 0.1 seconds between each request
338 and so on... at the moment just level 0 is asked. To be noted that level is a float!
342 print >>sys.stderr,"GetRightHTTPDownloader: START"
343 self.video_support_speed = 0.001 * ( ( 10 ** level ) - 1 )
344 if not self.video_support_enabled:
345 self.video_support_enabled = True
347 if not self.video_support_slow_start:
348 self.video_support_slow_start = True
349 self.downloader.rawserver.add_task( self.slow_start_wake_up, sleep_time )
351 self.resched( self.video_support_speed )
353 def stop_video_support( self ):
355 print >>sys.stderr,"GetRightHTTPDownloader: STOP"
356 if not self.video_support_enabled:
358 self.video_support_enabled = False
360 def is_video_support_enabled( self ):
361 return self.video_support_enabled
364 class GetRightHTTPDownloader:
365 def __init__(self, storage, picker, rawserver,
366 finflag, errorfunc, peerdownloader,
367 max_rate_period, infohash, measurefunc, gotpiecefunc):
368 self.storage = storage
370 self.rawserver = rawserver
371 self.finflag = finflag
372 self.errorfunc = errorfunc
373 self.peerdownloader = peerdownloader
374 self.infohash = infohash
375 self.max_rate_period = max_rate_period
376 self.gotpiecefunc = gotpiecefunc
377 self.measurefunc = measurefunc
380 self.video_support_enabled = False
382 def make_download(self, url):
383 self.downloads.append(SingleDownload(self, url))
384 return self.downloads[-1]
386 def get_downloads(self):
387 if self.finflag.isSet():
389 return self.downloads
391 def cancel_piece_download(self, pieces):
392 for d in self.downloads:
393 if d.active and d.index in pieces:
396 # Diego : wrap each single http download
397 def start_video_support( self, level = 0.0, sleep_time = None ):
398 for d in self.downloads:
399 d.start_video_support( level, sleep_time )
400 self.video_support_enabled = True
402 def stop_video_support( self ):
403 for d in self.downloads:
404 d.stop_video_support()
405 self.video_support_enabled = False
407 def is_video_support_enabled( self ):
408 return self.video_support_enabled
410 def is_slow_start( self ):
411 for d in self.downloads:
412 if d.is_slow_start():