]>
Commit | Line | Data |
---|---|---|
879aa787 MT |
1 | # Written by Bram Cohen |
2 | # see LICENSE.txt for license information | |
3 | ||
4 | from zurllib import urlopen | |
5 | from urlparse import urlparse | |
6 | from BT1.btformats import check_message | |
7 | from BT1.Choker import Choker | |
8 | from BT1.Storage import Storage | |
9 | from BT1.StorageWrapper import StorageWrapper | |
10 | from BT1.FileSelector import FileSelector | |
11 | from BT1.Uploader import Upload | |
12 | from BT1.Downloader import Downloader | |
13 | from BT1.HTTPDownloader import HTTPDownloader | |
14 | from BT1.Connecter import Connecter | |
15 | from RateLimiter import RateLimiter | |
16 | from BT1.Encrypter import Encoder | |
17 | from RawServer import RawServer, autodetect_ipv6, autodetect_socket_style | |
18 | from BT1.Rerequester import Rerequester | |
19 | from BT1.DownloaderFeedback import DownloaderFeedback | |
20 | from RateMeasure import RateMeasure | |
21 | from CurrentRateMeasure import Measure | |
22 | from BT1.PiecePicker import PiecePicker | |
23 | from BT1.Statistics import Statistics | |
24 | from ConfigDir import ConfigDir | |
25 | from bencode import bencode, bdecode | |
26 | from natpunch import UPnP_test | |
27 | from sha import sha | |
28 | from os import path, makedirs, listdir | |
29 | from parseargs import parseargs, formatDefinitions, defaultargs | |
30 | from socket import error as socketerror | |
31 | from random import seed | |
32 | from threading import Thread, Event | |
33 | from clock import clock | |
34 | from __init__ import createPeerID | |
35 | ||
36 | try: | |
37 | True | |
38 | except: | |
39 | True = 1 | |
40 | False = 0 | |
41 | ||
42 | defaults = [ | |
43 | ('max_uploads', 7, | |
44 | "the maximum number of uploads to allow at once."), | |
45 | ('keepalive_interval', 120.0, | |
46 | 'number of seconds to pause between sending keepalives'), | |
47 | ('download_slice_size', 2 ** 14, | |
48 | "How many bytes to query for per request."), | |
49 | ('upload_unit_size', 1460, | |
50 | "when limiting upload rate, how many bytes to send at a time"), | |
51 | ('request_backlog', 10, | |
52 | "maximum number of requests to keep in a single pipe at once."), | |
53 | ('max_message_length', 2 ** 23, | |
54 | "maximum length prefix encoding you'll accept over the wire - larger values get the connection dropped."), | |
55 | ('ip', '', | |
56 | "ip to report you have to the tracker."), | |
57 | ('minport', 10000, 'minimum port to listen on, counts up if unavailable'), | |
58 | ('maxport', 60000, 'maximum port to listen on'), | |
59 | ('random_port', 1, 'whether to choose randomly inside the port range ' + | |
60 | 'instead of counting up linearly'), | |
61 | ('responsefile', '', | |
62 | 'file the server response was stored in, alternative to url'), | |
63 | ('url', '', | |
64 | 'url to get file from, alternative to responsefile'), | |
65 | ('selector_enabled', 1, | |
66 | 'whether to enable the file selector and fast resume function'), | |
67 | ('expire_cache_data', 10, | |
68 | 'the number of days after which you wish to expire old cache data ' + | |
69 | '(0 = disabled)'), | |
70 | ('priority', '', | |
71 | 'a list of file priorities separated by commas, must be one per file, ' + | |
72 | '0 = highest, 1 = normal, 2 = lowest, -1 = download disabled'), | |
73 | ('saveas', '', | |
74 | 'local file name to save the file as, null indicates query user'), | |
75 | ('timeout', 300.0, | |
76 | 'time to wait between closing sockets which nothing has been received on'), | |
77 | ('timeout_check_interval', 60.0, | |
78 | 'time to wait between checking if any connections have timed out'), | |
79 | ('max_slice_length', 2 ** 17, | |
80 | "maximum length slice to send to peers, larger requests are ignored"), | |
81 | ('max_rate_period', 20.0, | |
82 | "maximum amount of time to guess the current rate estimate represents"), | |
83 | ('bind', '', | |
84 | 'comma-separated list of ips/hostnames to bind to locally'), | |
85 | # ('ipv6_enabled', autodetect_ipv6(), | |
86 | ('ipv6_enabled', 0, | |
87 | 'allow the client to connect to peers via IPv6'), | |
88 | ('ipv6_binds_v4', autodetect_socket_style(), | |
89 | "set if an IPv6 server socket won't also field IPv4 connections"), | |
90 | ('upnp_nat_access', 1, | |
91 | 'attempt to autoconfigure a UPnP router to forward a server port ' + | |
92 | '(0 = disabled, 1 = mode 1 [fast], 2 = mode 2 [slow])'), | |
93 | ('upload_rate_fudge', 5.0, | |
94 | 'time equivalent of writing to kernel-level TCP buffer, for rate adjustment'), | |
95 | ('tcp_ack_fudge', 0.03, | |
96 | 'how much TCP ACK download overhead to add to upload rate calculations ' + | |
97 | '(0 = disabled)'), | |
98 | ('display_interval', .5, | |
99 | 'time between updates of displayed information'), | |
100 | ('rerequest_interval', 5 * 60, | |
101 | 'time to wait between requesting more peers'), | |
102 | ('min_peers', 20, | |
103 | 'minimum number of peers to not do rerequesting'), | |
104 | ('http_timeout', 60, | |
105 | 'number of seconds to wait before assuming that an http connection has timed out'), | |
106 | ('max_initiate', 40, | |
107 | 'number of peers at which to stop initiating new connections'), | |
108 | ('check_hashes', 1, | |
109 | 'whether to check hashes on disk'), | |
110 | ('max_upload_rate', 0, | |
111 | 'maximum kB/s to upload at (0 = no limit, -1 = automatic)'), | |
112 | ('max_download_rate', 0, | |
113 | 'maximum kB/s to download at (0 = no limit)'), | |
114 | ('alloc_type', 'normal', | |
115 | 'allocation type (may be normal, background, pre-allocate or sparse)'), | |
116 | ('alloc_rate', 2.0, | |
117 | 'rate (in MiB/s) to allocate space at using background allocation'), | |
118 | ('buffer_reads', 1, | |
119 | 'whether to buffer disk reads'), | |
120 | ('write_buffer_size', 4, | |
121 | 'the maximum amount of space to use for buffering disk writes ' + | |
122 | '(in megabytes, 0 = disabled)'), | |
123 | ('breakup_seed_bitfield', 1, | |
124 | 'sends an incomplete bitfield and then fills with have messages, ' | |
125 | 'in order to get around stupid ISP manipulation'), | |
126 | ('snub_time', 30.0, | |
127 | "seconds to wait for data to come in over a connection before assuming it's semi-permanently choked"), | |
128 | ('spew', 0, | |
129 | "whether to display diagnostic info to stdout"), | |
130 | ('rarest_first_cutoff', 2, | |
131 | "number of downloads at which to switch from random to rarest first"), | |
132 | ('rarest_first_priority_cutoff', 5, | |
133 | 'the number of peers which need to have a piece before other partials take priority over rarest first'), | |
134 | ('min_uploads', 4, | |
135 | "the number of uploads to fill out to with extra optimistic unchokes"), | |
136 | ('max_files_open', 50, | |
137 | 'the maximum number of files to keep open at a time, 0 means no limit'), | |
138 | ('round_robin_period', 30, | |
139 | "the number of seconds between the client's switching upload targets"), | |
140 | ('super_seeder', 0, | |
141 | "whether to use special upload-efficiency-maximizing routines (only for dedicated seeds)"), | |
142 | ('security', 1, | |
143 | "whether to enable extra security features intended to prevent abuse"), | |
144 | ('max_connections', 0, | |
145 | "the absolute maximum number of peers to connect with (0 = no limit)"), | |
146 | ('auto_kick', 1, | |
147 | "whether to allow the client to automatically kick/ban peers that send bad data"), | |
148 | ('double_check', 1, | |
149 | "whether to double-check data being written to the disk for errors (may increase CPU load)"), | |
150 | ('triple_check', 0, | |
151 | "whether to thoroughly check data being written to the disk (may slow disk access)"), | |
152 | ('lock_files', 1, | |
153 | "whether to lock files the client is working with"), | |
154 | ('lock_while_reading', 0, | |
155 | "whether to lock access to files being read"), | |
156 | ('auto_flush', 0, | |
157 | "minutes between automatic flushes to disk (0 = disabled)"), | |
158 | ('dedicated_seed_id', '', | |
159 | "code to send to tracker identifying as a dedicated seed"), | |
160 | ] | |
161 | ||
162 | argslistheader = 'Arguments are:\n\n' | |
163 | ||
164 | ||
165 | def _failfunc(x): | |
166 | print x | |
167 | ||
168 | # old-style downloader | |
169 | def download(params, filefunc, statusfunc, finfunc, errorfunc, doneflag, cols, | |
170 | pathFunc = None, presets = {}, exchandler = None, | |
171 | failed = _failfunc, paramfunc = None): | |
172 | ||
173 | try: | |
174 | config = parse_params(params, presets) | |
175 | except ValueError, e: | |
176 | failed('error: ' + str(e) + '\nrun with no args for parameter explanations') | |
177 | return | |
178 | if not config: | |
179 | errorfunc(get_usage()) | |
180 | return | |
181 | ||
182 | myid = createPeerID() | |
183 | seed(myid) | |
184 | ||
185 | rawserver = RawServer(doneflag, config['timeout_check_interval'], | |
186 | config['timeout'], ipv6_enable = config['ipv6_enabled'], | |
187 | failfunc = failed, errorfunc = exchandler) | |
188 | ||
189 | upnp_type = UPnP_test(config['upnp_nat_access']) | |
190 | try: | |
191 | listen_port = rawserver.find_and_bind(config['minport'], config['maxport'], | |
192 | config['bind'], ipv6_socket_style = config['ipv6_binds_v4'], | |
193 | upnp = upnp_type, randomizer = config['random_port']) | |
194 | except socketerror, e: | |
195 | failed("Couldn't listen - " + str(e)) | |
196 | return | |
197 | ||
198 | response = get_response(config['responsefile'], config['url'], failed) | |
199 | if not response: | |
200 | return | |
201 | ||
202 | infohash = sha(bencode(response['info'])).digest() | |
203 | ||
204 | d = BT1Download(statusfunc, finfunc, errorfunc, exchandler, doneflag, | |
205 | config, response, infohash, myid, rawserver, listen_port) | |
206 | ||
207 | if not d.saveAs(filefunc): | |
208 | return | |
209 | ||
210 | if pathFunc: | |
211 | pathFunc(d.getFilename()) | |
212 | ||
213 | hashcheck = d.initFiles(old_style = True) | |
214 | if not hashcheck: | |
215 | return | |
216 | if not hashcheck(): | |
217 | return | |
218 | if not d.startEngine(): | |
219 | return | |
220 | d.startRerequester() | |
221 | d.autoStats() | |
222 | ||
223 | statusfunc(activity = 'connecting to peers') | |
224 | ||
225 | if paramfunc: | |
226 | paramfunc({ 'max_upload_rate' : d.setUploadRate, # change_max_upload_rate(<int KiB/sec>) | |
227 | 'max_uploads': d.setConns, # change_max_uploads(<int max uploads>) | |
228 | 'listen_port' : listen_port, # int | |
229 | 'peer_id' : myid, # string | |
230 | 'info_hash' : infohash, # string | |
231 | 'start_connection' : d._startConnection, # start_connection((<string ip>, <int port>), <peer id>) | |
232 | }) | |
233 | ||
234 | rawserver.listen_forever(d.getPortHandler()) | |
235 | ||
236 | d.shutdown() | |
237 | ||
238 | ||
239 | def parse_params(params, presets = {}): | |
240 | if len(params) == 0: | |
241 | return None | |
242 | config, args = parseargs(params, defaults, 0, 1, presets = presets) | |
243 | if args: | |
244 | if config['responsefile'] or config['url']: | |
245 | raise ValueError,'must have responsefile or url as arg or parameter, not both' | |
246 | if path.isfile(args[0]): | |
247 | config['responsefile'] = args[0] | |
248 | else: | |
249 | try: | |
250 | urlparse(args[0]) | |
251 | except: | |
252 | raise ValueError, 'bad filename or url' | |
253 | config['url'] = args[0] | |
254 | elif (config['responsefile'] == '') == (config['url'] == ''): | |
255 | raise ValueError, 'need responsefile or url, must have one, cannot have both' | |
256 | return config | |
257 | ||
258 | ||
259 | def get_usage(defaults = defaults, cols = 100, presets = {}): | |
260 | return (argslistheader + formatDefinitions(defaults, cols, presets)) | |
261 | ||
262 | ||
263 | def get_response(file, url, errorfunc): | |
264 | try: | |
265 | if file: | |
266 | h = open(file, 'rb') | |
267 | try: | |
268 | line = h.read(10) # quick test to see if responsefile contains a dict | |
269 | front,garbage = line.split(':',1) | |
270 | assert front[0] == 'd' | |
271 | int(front[1:]) | |
272 | except: | |
273 | errorfunc(file+' is not a valid responsefile') | |
274 | return None | |
275 | try: | |
276 | h.seek(0) | |
277 | except: | |
278 | try: | |
279 | h.close() | |
280 | except: | |
281 | pass | |
282 | h = open(file, 'rb') | |
283 | else: | |
284 | try: | |
285 | h = urlopen(url) | |
286 | except: | |
287 | errorfunc(url+' bad url') | |
288 | return None | |
289 | response = h.read() | |
290 | ||
291 | except IOError, e: | |
292 | errorfunc('problem getting response info - ' + str(e)) | |
293 | return None | |
294 | try: | |
295 | h.close() | |
296 | except: | |
297 | pass | |
298 | try: | |
299 | try: | |
300 | response = bdecode(response) | |
301 | except: | |
302 | errorfunc("warning: bad data in responsefile") | |
303 | response = bdecode(response, sloppy=1) | |
304 | check_message(response) | |
305 | except ValueError, e: | |
306 | errorfunc("got bad file info - " + str(e)) | |
307 | return None | |
308 | ||
309 | return response | |
310 | ||
311 | ||
312 | class BT1Download: | |
313 | def __init__(self, statusfunc, finfunc, errorfunc, excfunc, doneflag, | |
314 | config, response, infohash, id, rawserver, port, | |
315 | appdataobj = None): | |
316 | self.statusfunc = statusfunc | |
317 | self.finfunc = finfunc | |
318 | self.errorfunc = errorfunc | |
319 | self.excfunc = excfunc | |
320 | self.doneflag = doneflag | |
321 | self.config = config | |
322 | self.response = response | |
323 | self.infohash = infohash | |
324 | self.myid = id | |
325 | self.rawserver = rawserver | |
326 | self.port = port | |
327 | ||
328 | self.info = self.response['info'] | |
329 | self.pieces = [self.info['pieces'][x:x+20] | |
330 | for x in xrange(0, len(self.info['pieces']), 20)] | |
331 | self.len_pieces = len(self.pieces) | |
332 | self.argslistheader = argslistheader | |
333 | self.unpauseflag = Event() | |
334 | self.unpauseflag.set() | |
335 | self.downloader = None | |
336 | self.storagewrapper = None | |
337 | self.fileselector = None | |
338 | self.super_seeding_active = False | |
339 | self.filedatflag = Event() | |
340 | self.spewflag = Event() | |
341 | self.superseedflag = Event() | |
342 | self.whenpaused = None | |
343 | self.finflag = Event() | |
344 | self.rerequest = None | |
345 | self.tcp_ack_fudge = config['tcp_ack_fudge'] | |
346 | ||
347 | self.selector_enabled = config['selector_enabled'] | |
348 | if appdataobj: | |
349 | self.appdataobj = appdataobj | |
350 | elif self.selector_enabled: | |
351 | self.appdataobj = ConfigDir() | |
352 | self.appdataobj.deleteOldCacheData( config['expire_cache_data'], | |
353 | [self.infohash] ) | |
354 | ||
355 | self.excflag = self.rawserver.get_exception_flag() | |
356 | self.failed = False | |
357 | self.checking = False | |
358 | self.started = False | |
359 | ||
360 | self.picker = PiecePicker(self.len_pieces, config['rarest_first_cutoff'], | |
361 | config['rarest_first_priority_cutoff']) | |
362 | self.choker = Choker(config, rawserver.add_task, | |
363 | self.picker, self.finflag.isSet) | |
364 | ||
365 | ||
366 | def checkSaveLocation(self, loc): | |
367 | if self.info.has_key('length'): | |
368 | return path.exists(loc) | |
369 | for x in self.info['files']: | |
370 | if path.exists(path.join(loc, x['path'][0])): | |
371 | return True | |
372 | return False | |
373 | ||
374 | ||
375 | def saveAs(self, filefunc, pathfunc = None): | |
376 | try: | |
377 | def make(f, forcedir = False): | |
378 | if not forcedir: | |
379 | f = path.split(f)[0] | |
380 | if f != '' and not path.exists(f): | |
381 | makedirs(f) | |
382 | ||
383 | if self.info.has_key('length'): | |
384 | file_length = self.info['length'] | |
385 | file = filefunc(self.info['name'], file_length, | |
386 | self.config['saveas'], False) | |
387 | if file is None: | |
388 | return None | |
389 | make(file) | |
390 | files = [(file, file_length)] | |
391 | else: | |
392 | file_length = 0L | |
393 | for x in self.info['files']: | |
394 | file_length += x['length'] | |
395 | file = filefunc(self.info['name'], file_length, | |
396 | self.config['saveas'], True) | |
397 | if file is None: | |
398 | return None | |
399 | ||
400 | # if this path exists, and no files from the info dict exist, we assume it's a new download and | |
401 | # the user wants to create a new directory with the default name | |
402 | existing = 0 | |
403 | if path.exists(file): | |
404 | if not path.isdir(file): | |
405 | self.errorfunc(file + 'is not a dir') | |
406 | return None | |
407 | if len(listdir(file)) > 0: # if it's not empty | |
408 | for x in self.info['files']: | |
409 | if path.exists(path.join(file, x['path'][0])): | |
410 | existing = 1 | |
411 | if not existing: | |
412 | file = path.join(file, self.info['name']) | |
413 | if path.exists(file) and not path.isdir(file): | |
414 | if file[-8:] == '.torrent': | |
415 | file = file[:-8] | |
416 | if path.exists(file) and not path.isdir(file): | |
417 | self.errorfunc("Can't create dir - " + self.info['name']) | |
418 | return None | |
419 | make(file, True) | |
420 | ||
421 | # alert the UI to any possible change in path | |
422 | if pathfunc != None: | |
423 | pathfunc(file) | |
424 | ||
425 | files = [] | |
426 | for x in self.info['files']: | |
427 | n = file | |
428 | for i in x['path']: | |
429 | n = path.join(n, i) | |
430 | files.append((n, x['length'])) | |
431 | make(n) | |
432 | except OSError, e: | |
433 | self.errorfunc("Couldn't allocate dir - " + str(e)) | |
434 | return None | |
435 | ||
436 | self.filename = file | |
437 | self.files = files | |
438 | self.datalength = file_length | |
439 | ||
440 | return file | |
441 | ||
442 | ||
443 | def getFilename(self): | |
444 | return self.filename | |
445 | ||
446 | ||
447 | def _finished(self): | |
448 | self.finflag.set() | |
449 | try: | |
450 | self.storage.set_readonly() | |
451 | except (IOError, OSError), e: | |
452 | self.errorfunc('trouble setting readonly at end - ' + str(e)) | |
453 | if self.superseedflag.isSet(): | |
454 | self._set_super_seed() | |
455 | self.choker.set_round_robin_period( | |
456 | max( self.config['round_robin_period'], | |
457 | self.config['round_robin_period'] * | |
458 | self.info['piece length'] / 200000 ) ) | |
459 | self.rerequest_complete() | |
460 | self.finfunc() | |
461 | ||
462 | def _data_flunked(self, amount, index): | |
463 | self.ratemeasure_datarejected(amount) | |
464 | if not self.doneflag.isSet(): | |
465 | self.errorfunc('piece %d failed hash check, re-downloading it' % index) | |
466 | ||
467 | def _failed(self, reason): | |
468 | self.failed = True | |
469 | self.doneflag.set() | |
470 | if reason is not None: | |
471 | self.errorfunc(reason) | |
472 | ||
473 | ||
474 | def initFiles(self, old_style = False, statusfunc = None): | |
475 | if self.doneflag.isSet(): | |
476 | return None | |
477 | if not statusfunc: | |
478 | statusfunc = self.statusfunc | |
479 | ||
480 | disabled_files = None | |
481 | if self.selector_enabled: | |
482 | self.priority = self.config['priority'] | |
483 | if self.priority: | |
484 | try: | |
485 | self.priority = self.priority.split(',') | |
486 | assert len(self.priority) == len(self.files) | |
487 | self.priority = [int(p) for p in self.priority] | |
488 | for p in self.priority: | |
489 | assert p >= -1 | |
490 | assert p <= 2 | |
491 | except: | |
492 | self.errorfunc('bad priority list given, ignored') | |
493 | self.priority = None | |
494 | ||
495 | data = self.appdataobj.getTorrentData(self.infohash) | |
496 | try: | |
497 | d = data['resume data']['priority'] | |
498 | assert len(d) == len(self.files) | |
499 | disabled_files = [x == -1 for x in d] | |
500 | except: | |
501 | try: | |
502 | disabled_files = [x == -1 for x in self.priority] | |
503 | except: | |
504 | pass | |
505 | ||
506 | try: | |
507 | try: | |
508 | self.storage = Storage(self.files, self.info['piece length'], | |
509 | self.doneflag, self.config, disabled_files) | |
510 | except IOError, e: | |
511 | self.errorfunc('trouble accessing files - ' + str(e)) | |
512 | return None | |
513 | if self.doneflag.isSet(): | |
514 | return None | |
515 | ||
516 | self.storagewrapper = StorageWrapper(self.storage, self.config['download_slice_size'], | |
517 | self.pieces, self.info['piece length'], self._finished, self._failed, | |
518 | statusfunc, self.doneflag, self.config['check_hashes'], | |
519 | self._data_flunked, self.rawserver.add_task, | |
520 | self.config, self.unpauseflag) | |
521 | ||
522 | except ValueError, e: | |
523 | self._failed('bad data - ' + str(e)) | |
524 | except IOError, e: | |
525 | self._failed('IOError - ' + str(e)) | |
526 | if self.doneflag.isSet(): | |
527 | return None | |
528 | ||
529 | if self.selector_enabled: | |
530 | self.fileselector = FileSelector(self.files, self.info['piece length'], | |
531 | self.appdataobj.getPieceDir(self.infohash), | |
532 | self.storage, self.storagewrapper, | |
533 | self.rawserver.add_task, | |
534 | self._failed) | |
535 | if data: | |
536 | data = data.get('resume data') | |
537 | if data: | |
538 | self.fileselector.unpickle(data) | |
539 | ||
540 | self.checking = True | |
541 | if old_style: | |
542 | return self.storagewrapper.old_style_init() | |
543 | return self.storagewrapper.initialize | |
544 | ||
545 | ||
546 | def getCachedTorrentData(self): | |
547 | return self.appdataobj.getTorrentData(self.infohash) | |
548 | ||
549 | ||
550 | def _make_upload(self, connection, ratelimiter, totalup): | |
551 | return Upload(connection, ratelimiter, totalup, | |
552 | self.choker, self.storagewrapper, self.picker, | |
553 | self.config) | |
554 | ||
555 | def _kick_peer(self, connection): | |
556 | def k(connection = connection): | |
557 | connection.close() | |
558 | self.rawserver.add_task(k,0) | |
559 | ||
560 | def _ban_peer(self, ip): | |
561 | self.encoder_ban(ip) | |
562 | ||
563 | def _received_raw_data(self, x): | |
564 | if self.tcp_ack_fudge: | |
565 | x = int(x*self.tcp_ack_fudge) | |
566 | self.ratelimiter.adjust_sent(x) | |
567 | # self.upmeasure.update_rate(x) | |
568 | ||
569 | def _received_data(self, x): | |
570 | self.downmeasure.update_rate(x) | |
571 | self.ratemeasure.data_came_in(x) | |
572 | ||
573 | def _received_http_data(self, x): | |
574 | self.downmeasure.update_rate(x) | |
575 | self.ratemeasure.data_came_in(x) | |
576 | self.downloader.external_data_received(x) | |
577 | ||
578 | def _cancelfunc(self, pieces): | |
579 | self.downloader.cancel_piece_download(pieces) | |
580 | self.httpdownloader.cancel_piece_download(pieces) | |
581 | def _reqmorefunc(self, pieces): | |
582 | self.downloader.requeue_piece_download(pieces) | |
583 | ||
584 | def startEngine(self, ratelimiter = None, statusfunc = None): | |
585 | if self.doneflag.isSet(): | |
586 | return False | |
587 | if not statusfunc: | |
588 | statusfunc = self.statusfunc | |
589 | ||
590 | self.checking = False | |
591 | ||
592 | for i in xrange(self.len_pieces): | |
593 | if self.storagewrapper.do_I_have(i): | |
594 | self.picker.complete(i) | |
595 | self.upmeasure = Measure(self.config['max_rate_period'], | |
596 | self.config['upload_rate_fudge']) | |
597 | self.downmeasure = Measure(self.config['max_rate_period']) | |
598 | ||
599 | if ratelimiter: | |
600 | self.ratelimiter = ratelimiter | |
601 | else: | |
602 | self.ratelimiter = RateLimiter(self.rawserver.add_task, | |
603 | self.config['upload_unit_size'], | |
604 | self.setConns) | |
605 | self.ratelimiter.set_upload_rate(self.config['max_upload_rate']) | |
606 | ||
607 | self.ratemeasure = RateMeasure() | |
608 | self.ratemeasure_datarejected = self.ratemeasure.data_rejected | |
609 | ||
610 | self.downloader = Downloader(self.storagewrapper, self.picker, | |
611 | self.config['request_backlog'], self.config['max_rate_period'], | |
612 | self.len_pieces, self.config['download_slice_size'], | |
613 | self._received_data, self.config['snub_time'], self.config['auto_kick'], | |
614 | self._kick_peer, self._ban_peer) | |
615 | self.downloader.set_download_rate(self.config['max_download_rate']) | |
616 | self.connecter = Connecter(self._make_upload, self.downloader, self.choker, | |
617 | self.len_pieces, self.upmeasure, self.config, | |
618 | self.ratelimiter, self.rawserver.add_task) | |
619 | self.encoder = Encoder(self.connecter, self.rawserver, | |
620 | self.myid, self.config['max_message_length'], self.rawserver.add_task, | |
621 | self.config['keepalive_interval'], self.infohash, | |
622 | self._received_raw_data, self.config) | |
623 | self.encoder_ban = self.encoder.ban | |
624 | ||
625 | self.httpdownloader = HTTPDownloader(self.storagewrapper, self.picker, | |
626 | self.rawserver, self.finflag, self.errorfunc, self.downloader, | |
627 | self.config['max_rate_period'], self.infohash, self._received_http_data, | |
628 | self.connecter.got_piece) | |
629 | if self.response.has_key('httpseeds') and not self.finflag.isSet(): | |
630 | for u in self.response['httpseeds']: | |
631 | self.httpdownloader.make_download(u) | |
632 | ||
633 | if self.selector_enabled: | |
634 | self.fileselector.tie_in(self.picker, self._cancelfunc, | |
635 | self._reqmorefunc, self.rerequest_ondownloadmore) | |
636 | if self.priority: | |
637 | self.fileselector.set_priorities_now(self.priority) | |
638 | self.appdataobj.deleteTorrentData(self.infohash) | |
639 | # erase old data once you've started modifying it | |
640 | ||
641 | if self.config['super_seeder']: | |
642 | self.set_super_seed() | |
643 | ||
644 | self.started = True | |
645 | return True | |
646 | ||
647 | ||
648 | def rerequest_complete(self): | |
649 | if self.rerequest: | |
650 | self.rerequest.announce(1) | |
651 | ||
652 | def rerequest_stopped(self): | |
653 | if self.rerequest: | |
654 | self.rerequest.announce(2) | |
655 | ||
656 | def rerequest_lastfailed(self): | |
657 | if self.rerequest: | |
658 | return self.rerequest.last_failed | |
659 | return False | |
660 | ||
661 | def rerequest_ondownloadmore(self): | |
662 | if self.rerequest: | |
663 | self.rerequest.hit() | |
664 | ||
665 | def startRerequester(self, seededfunc = None, force_rapid_update = False): | |
666 | if self.response.has_key('announce-list'): | |
667 | trackerlist = self.response['announce-list'] | |
668 | else: | |
669 | trackerlist = [[self.response['announce']]] | |
670 | ||
671 | self.rerequest = Rerequester(trackerlist, self.config['rerequest_interval'], | |
672 | self.rawserver.add_task, self.connecter.how_many_connections, | |
673 | self.config['min_peers'], self.encoder.start_connections, | |
674 | self.rawserver.add_task, self.storagewrapper.get_amount_left, | |
675 | self.upmeasure.get_total, self.downmeasure.get_total, self.port, self.config['ip'], | |
676 | self.myid, self.infohash, self.config['http_timeout'], | |
677 | self.errorfunc, self.excfunc, self.config['max_initiate'], | |
678 | self.doneflag, self.upmeasure.get_rate, self.downmeasure.get_rate, | |
679 | self.unpauseflag, self.config['dedicated_seed_id'], | |
680 | seededfunc, force_rapid_update ) | |
681 | ||
682 | self.rerequest.start() | |
683 | ||
684 | ||
685 | def _init_stats(self): | |
686 | self.statistics = Statistics(self.upmeasure, self.downmeasure, | |
687 | self.connecter, self.httpdownloader, self.ratelimiter, | |
688 | self.rerequest_lastfailed, self.filedatflag) | |
689 | if self.info.has_key('files'): | |
690 | self.statistics.set_dirstats(self.files, self.info['piece length']) | |
691 | if self.config['spew']: | |
692 | self.spewflag.set() | |
693 | ||
694 | def autoStats(self, displayfunc = None): | |
695 | if not displayfunc: | |
696 | displayfunc = self.statusfunc | |
697 | ||
698 | self._init_stats() | |
699 | DownloaderFeedback(self.choker, self.httpdownloader, self.rawserver.add_task, | |
700 | self.upmeasure.get_rate, self.downmeasure.get_rate, | |
701 | self.ratemeasure, self.storagewrapper.get_stats, | |
702 | self.datalength, self.finflag, self.spewflag, self.statistics, | |
703 | displayfunc, self.config['display_interval']) | |
704 | ||
705 | def startStats(self): | |
706 | self._init_stats() | |
707 | d = DownloaderFeedback(self.choker, self.httpdownloader, self.rawserver.add_task, | |
708 | self.upmeasure.get_rate, self.downmeasure.get_rate, | |
709 | self.ratemeasure, self.storagewrapper.get_stats, | |
710 | self.datalength, self.finflag, self.spewflag, self.statistics) | |
711 | return d.gather | |
712 | ||
713 | ||
714 | def getPortHandler(self): | |
715 | return self.encoder | |
716 | ||
717 | ||
718 | def shutdown(self, torrentdata = {}): | |
719 | if self.checking or self.started: | |
720 | self.storagewrapper.sync() | |
721 | self.storage.close() | |
722 | self.rerequest_stopped() | |
723 | if self.fileselector and self.started: | |
724 | if not self.failed: | |
725 | self.fileselector.finish() | |
726 | torrentdata['resume data'] = self.fileselector.pickle() | |
727 | try: | |
728 | self.appdataobj.writeTorrentData(self.infohash,torrentdata) | |
729 | except: | |
730 | self.appdataobj.deleteTorrentData(self.infohash) # clear it | |
731 | return not self.failed and not self.excflag.isSet() | |
732 | # if returns false, you may wish to auto-restart the torrent | |
733 | ||
734 | ||
735 | def setUploadRate(self, rate): | |
736 | try: | |
737 | def s(self = self, rate = rate): | |
738 | self.config['max_upload_rate'] = rate | |
739 | self.ratelimiter.set_upload_rate(rate) | |
740 | self.rawserver.add_task(s) | |
741 | except AttributeError: | |
742 | pass | |
743 | ||
744 | def setConns(self, conns, conns2 = None): | |
745 | if not conns2: | |
746 | conns2 = conns | |
747 | try: | |
748 | def s(self = self, conns = conns, conns2 = conns2): | |
749 | self.config['min_uploads'] = conns | |
750 | self.config['max_uploads'] = conns2 | |
751 | if (conns > 30): | |
752 | self.config['max_initiate'] = conns + 10 | |
753 | self.rawserver.add_task(s) | |
754 | except AttributeError: | |
755 | pass | |
756 | ||
757 | def setDownloadRate(self, rate): | |
758 | try: | |
759 | def s(self = self, rate = rate): | |
760 | self.config['max_download_rate'] = rate | |
761 | self.downloader.set_download_rate(rate) | |
762 | self.rawserver.add_task(s) | |
763 | except AttributeError: | |
764 | pass | |
765 | ||
766 | def startConnection(self, ip, port, id): | |
767 | self.encoder._start_connection((ip, port), id) | |
768 | ||
769 | def _startConnection(self, ipandport, id): | |
770 | self.encoder._start_connection(ipandport, id) | |
771 | ||
772 | def setInitiate(self, initiate): | |
773 | try: | |
774 | def s(self = self, initiate = initiate): | |
775 | self.config['max_initiate'] = initiate | |
776 | self.rawserver.add_task(s) | |
777 | except AttributeError: | |
778 | pass | |
779 | ||
780 | def getConfig(self): | |
781 | return self.config | |
782 | ||
783 | def getDefaults(self): | |
784 | return defaultargs(defaults) | |
785 | ||
786 | def getUsageText(self): | |
787 | return self.argslistheader | |
788 | ||
789 | def reannounce(self, special = None): | |
790 | try: | |
791 | def r(self = self, special = special): | |
792 | if special is None: | |
793 | self.rerequest.announce() | |
794 | else: | |
795 | self.rerequest.announce(specialurl = special) | |
796 | self.rawserver.add_task(r) | |
797 | except AttributeError: | |
798 | pass | |
799 | ||
800 | def getResponse(self): | |
801 | try: | |
802 | return self.response | |
803 | except: | |
804 | return None | |
805 | ||
806 | # def Pause(self): | |
807 | # try: | |
808 | # if self.storagewrapper: | |
809 | # self.rawserver.add_task(self._pausemaker, 0) | |
810 | # except: | |
811 | # return False | |
812 | # self.unpauseflag.clear() | |
813 | # return True | |
814 | # | |
815 | # def _pausemaker(self): | |
816 | # self.whenpaused = clock() | |
817 | # self.unpauseflag.wait() # sticks a monkey wrench in the main thread | |
818 | # | |
819 | # def Unpause(self): | |
820 | # self.unpauseflag.set() | |
821 | # if self.whenpaused and clock()-self.whenpaused > 60: | |
822 | # def r(self = self): | |
823 | # self.rerequest.announce(3) # rerequest automatically if paused for >60 seconds | |
824 | # self.rawserver.add_task(r) | |
825 | ||
826 | def Pause(self): | |
827 | if not self.storagewrapper: | |
828 | return False | |
829 | self.unpauseflag.clear() | |
830 | self.rawserver.add_task(self.onPause) | |
831 | return True | |
832 | ||
833 | def onPause(self): | |
834 | self.whenpaused = clock() | |
835 | if not self.downloader: | |
836 | return | |
837 | self.downloader.pause(True) | |
838 | self.encoder.pause(True) | |
839 | self.choker.pause(True) | |
840 | ||
841 | def Unpause(self): | |
842 | self.unpauseflag.set() | |
843 | self.rawserver.add_task(self.onUnpause) | |
844 | ||
845 | def onUnpause(self): | |
846 | if not self.downloader: | |
847 | return | |
848 | self.downloader.pause(False) | |
849 | self.encoder.pause(False) | |
850 | self.choker.pause(False) | |
851 | if self.rerequest and self.whenpaused and clock()-self.whenpaused > 60: | |
852 | self.rerequest.announce(3) # rerequest automatically if paused for >60 seconds | |
853 | ||
854 | def set_super_seed(self): | |
855 | try: | |
856 | self.superseedflag.set() | |
857 | def s(self = self): | |
858 | if self.finflag.isSet(): | |
859 | self._set_super_seed() | |
860 | self.rawserver.add_task(s) | |
861 | except AttributeError: | |
862 | pass | |
863 | ||
864 | def _set_super_seed(self): | |
865 | if not self.super_seeding_active: | |
866 | self.super_seeding_active = True | |
867 | self.errorfunc(' ** SUPER-SEED OPERATION ACTIVE **\n' + | |
868 | ' please set Max uploads so each peer gets 6-8 kB/s') | |
869 | def s(self = self): | |
870 | self.downloader.set_super_seed() | |
871 | self.choker.set_super_seed() | |
872 | self.rawserver.add_task(s) | |
873 | if self.finflag.isSet(): # mode started when already finished | |
874 | def r(self = self): | |
875 | self.rerequest.announce(3) # so after kicking everyone off, reannounce | |
876 | self.rawserver.add_task(r) | |
877 | ||
878 | def am_I_finished(self): | |
879 | return self.finflag.isSet() | |
880 | ||
881 | def get_transfer_stats(self): | |
882 | return self.upmeasure.get_total(), self.downmeasure.get_total() |