quartz_torrent 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +2 -5
- data/bin/quartztorrent_download_curses +32 -24
- data/bin/quartztorrent_show_info +2 -5
- data/lib/quartz_torrent/alarm.rb +36 -0
- data/lib/quartz_torrent/blockstate.rb +7 -0
- data/lib/quartz_torrent/formatter.rb +10 -1
- data/lib/quartz_torrent/{httptrackerclient.rb → httptrackerdriver.rb} +15 -7
- data/lib/quartz_torrent/magnet.rb +10 -0
- data/lib/quartz_torrent/metainfo.rb +6 -1
- data/lib/quartz_torrent/metainfopiecestate.rb +6 -2
- data/lib/quartz_torrent/peerclient.rb +353 -157
- data/lib/quartz_torrent/peermsgserialization.rb +1 -1
- data/lib/quartz_torrent/ratelimit.rb +6 -0
- data/lib/quartz_torrent/reactor.rb +18 -89
- data/lib/quartz_torrent/semaphore.rb +25 -11
- data/lib/quartz_torrent/timermanager.rb +120 -0
- data/lib/quartz_torrent/torrentqueue.rb +91 -0
- data/lib/quartz_torrent/trackerclient.rb +128 -24
- data/lib/quartz_torrent/udptrackerdriver.rb +95 -0
- data/lib/quartz_torrent/util.rb +1 -1
- metadata +7 -4
- data/lib/quartz_torrent/udptrackerclient.rb +0 -70
@@ -89,7 +89,7 @@ module QuartzTorrent
|
|
89
89
|
@logger.debug "Peer supports extension #{extName} using id '#{extId}'."
|
90
90
|
@extendedMessageIdToClass[extId] = clazz
|
91
91
|
else
|
92
|
-
@logger.
|
92
|
+
@logger.debug "Peer supports extension #{extName} using id '#{extId}', but I don't know what class to use for that extension."
|
93
93
|
end
|
94
94
|
end
|
95
95
|
else
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'socket'
|
2
|
-
require 'pqueue'
|
3
2
|
require 'fiber'
|
4
3
|
require 'thread'
|
5
4
|
require 'quartz_torrent/ratelimit'
|
5
|
+
require 'quartz_torrent/timermanager'
|
6
6
|
include Socket::Constants
|
7
7
|
|
8
8
|
module QuartzTorrent
|
@@ -311,7 +311,6 @@ module QuartzTorrent
|
|
311
311
|
@ioInfo.writeRateLimit = rate
|
312
312
|
end
|
313
313
|
|
314
|
-
attr_accessor :writeRateLimit
|
315
314
|
end
|
316
315
|
|
317
316
|
# An IoFacade that doesn't allow reading. This is not part of the public API.
|
@@ -346,6 +345,7 @@ module QuartzTorrent
|
|
346
345
|
@useErrorhandler = true
|
347
346
|
@readRateLimit = nil
|
348
347
|
@writeRateLimit = nil
|
348
|
+
raise "IO passed to IOInfo initialize may not be nil" if io.nil?
|
349
349
|
end
|
350
350
|
attr_accessor :io
|
351
351
|
attr_accessor :metainfo
|
@@ -366,84 +366,6 @@ module QuartzTorrent
|
|
366
366
|
end
|
367
367
|
end
|
368
368
|
|
369
|
-
# Class used to manage timers.
|
370
|
-
class TimerManager
|
371
|
-
class TimerInfo
|
372
|
-
def initialize(duration, recurring, metainfo)
|
373
|
-
@duration = duration
|
374
|
-
@recurring = recurring
|
375
|
-
@metainfo = metainfo
|
376
|
-
@cancelled = false
|
377
|
-
refresh
|
378
|
-
end
|
379
|
-
attr_accessor :recurring
|
380
|
-
attr_accessor :duration
|
381
|
-
attr_accessor :expiry
|
382
|
-
attr_accessor :metainfo
|
383
|
-
# Since pqueue doesn't allow removal of anything but the head
|
384
|
-
# we flag deleted items so they are deleted when they are pulled
|
385
|
-
attr_accessor :cancelled
|
386
|
-
|
387
|
-
def secondsUntilExpiry
|
388
|
-
@expiry - Time.new
|
389
|
-
end
|
390
|
-
|
391
|
-
def refresh
|
392
|
-
@expiry = Time.new + @duration
|
393
|
-
end
|
394
|
-
end
|
395
|
-
|
396
|
-
def initialize(logger = nil)
|
397
|
-
@queue = PQueue.new { |a,b| b.expiry <=> a.expiry }
|
398
|
-
@mutex = Mutex.new
|
399
|
-
@logger = logger
|
400
|
-
end
|
401
|
-
|
402
|
-
# Add a timer. Parameter 'duration' specifies the timer duration in seconds,
|
403
|
-
# 'metainfo' is caller information passed to the handler when the timer expires,
|
404
|
-
# 'recurring' should be true if the timer will repeat, or false if it will only
|
405
|
-
# expire once, and 'immed' when true specifies that the timer should expire immediately
|
406
|
-
# (and again each duration if recurring) while false specifies that the timer will only
|
407
|
-
# expire the first time after it's duration elapses.
|
408
|
-
def add(duration, metainfo = nil, recurring = true, immed = false)
|
409
|
-
raise "TimerManager.add: Timer duration may not be nil" if duration.nil?
|
410
|
-
info = TimerInfo.new(duration, recurring, metainfo)
|
411
|
-
info.expiry = Time.new if immed
|
412
|
-
@mutex.synchronize{ @queue.push info }
|
413
|
-
info
|
414
|
-
end
|
415
|
-
|
416
|
-
# Cancel a timer.
|
417
|
-
def cancel(timerInfo)
|
418
|
-
timerInfo.cancelled = true
|
419
|
-
end
|
420
|
-
|
421
|
-
# Return the next timer event from the queue, but don't remove it from the queue.
|
422
|
-
def peek
|
423
|
-
clearCancelled
|
424
|
-
@queue.top
|
425
|
-
end
|
426
|
-
|
427
|
-
# Remove the next timer event from the queue and return it as a TimerHandler::TimerInfo object.
|
428
|
-
def next
|
429
|
-
clearCancelled
|
430
|
-
result = nil
|
431
|
-
@mutex.synchronize{ result = @queue.pop }
|
432
|
-
if result && result.recurring
|
433
|
-
result.refresh
|
434
|
-
@mutex.synchronize{ @queue.push result }
|
435
|
-
end
|
436
|
-
result
|
437
|
-
end
|
438
|
-
|
439
|
-
private
|
440
|
-
|
441
|
-
def clearCancelled
|
442
|
-
while @queue.top && @queue.top.cancelled
|
443
|
-
info = @queue.pop
|
444
|
-
end
|
445
|
-
end
|
446
|
-
end
|
447
369
|
|
448
370
|
# This class implements the Reactor pattern. The Reactor listens for activity on IO objects and calls methods on
|
449
371
|
# an associated Handler object when activity is detected. Callers can use listen, connect or open to register IO
|
@@ -494,7 +416,9 @@ module QuartzTorrent
|
|
494
416
|
@stopped
|
495
417
|
end
|
496
418
|
|
497
|
-
# Create a TCP connection to the specified host
|
419
|
+
# Create a TCP connection to the specified host.
|
420
|
+
# Note that this method may raise exceptions. For example 'Too many open files' might be raised if
|
421
|
+
# the process is using too many file descriptors
|
498
422
|
def connect(addr, port, metainfo, timeout = nil)
|
499
423
|
ioInfo = startConnection(port, addr, metainfo)
|
500
424
|
@ioInfo[ioInfo.io] = ioInfo
|
@@ -704,14 +628,16 @@ module QuartzTorrent
|
|
704
628
|
end
|
705
629
|
end
|
706
630
|
|
707
|
-
if
|
708
|
-
# Process timer
|
709
|
-
@logger.debug "eventloop: processing timer" if @logger
|
631
|
+
if timer
|
710
632
|
# Calling processTimer in withReadFiber here is not correct. What if at this point the fiber was already paused in a read, and we
|
711
633
|
# want to process a timer? In that case we will resume the read and it will possibly finish, but we'll never
|
712
634
|
# call the timer handler. For this reason we must prevent read calls in timerHandlers.
|
713
|
-
|
714
|
-
|
635
|
+
# Process timer
|
636
|
+
@logger.debug "eventloop: processing timer" if @logger
|
637
|
+
processTimer(timer)
|
638
|
+
end
|
639
|
+
|
640
|
+
if ! selectResult.nil?
|
715
641
|
readable, writeable = selectResult
|
716
642
|
|
717
643
|
# If we are stopped, then ignore reads; we only care about completing our writes that were pending when we were stopped.
|
@@ -727,6 +653,10 @@ module QuartzTorrent
|
|
727
653
|
end
|
728
654
|
|
729
655
|
@currentIoInfo = @ioInfo[io]
|
656
|
+
|
657
|
+
# The IOInfo associated with this io could have been closed by the timer handler processed above.
|
658
|
+
next if @currentIoInfo.nil?
|
659
|
+
|
730
660
|
if @currentIoInfo.state == :listening
|
731
661
|
@logger.debug "eventloop: calling handleAccept for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
|
732
662
|
# Replace the currentIoInfo with the accepted socket
|
@@ -773,9 +703,9 @@ module QuartzTorrent
|
|
773
703
|
selectTimeout = nil
|
774
704
|
timer = nil
|
775
705
|
while true && ! @stopped
|
776
|
-
|
706
|
+
secondsUntilExpiry = nil
|
707
|
+
timer = @timerManager.next{ |s| secondsUntilExpiry = s }
|
777
708
|
break if ! timer
|
778
|
-
secondsUntilExpiry = timer.secondsUntilExpiry
|
779
709
|
if secondsUntilExpiry > 0
|
780
710
|
selectTimeout = secondsUntilExpiry
|
781
711
|
break
|
@@ -844,7 +774,6 @@ module QuartzTorrent
|
|
844
774
|
@logger.error "Exception in timer event handler: #{$!}" if @logger
|
845
775
|
@logger.error $!.backtrace.join "\n" if @logger
|
846
776
|
end
|
847
|
-
@timerManager.next
|
848
777
|
end
|
849
778
|
|
850
779
|
def disposeIo(io)
|
@@ -9,19 +9,31 @@ class Semaphore
|
|
9
9
|
end
|
10
10
|
|
11
11
|
# Wait on the semaphore. If the count zero or below, the calling thread blocks.
|
12
|
-
|
12
|
+
# Optionally a timeout in seconds can be specified. This method returns true if the wait
|
13
|
+
# ended because of a signal, and false if it ended because of a timeout.
|
14
|
+
def wait(timeout = nil)
|
15
|
+
result = true
|
13
16
|
c = nil
|
14
17
|
@mutex.synchronize do
|
15
18
|
@count -= 1
|
16
|
-
|
19
|
+
if @count < 0
|
20
|
+
@sleeping.push Thread.current
|
21
|
+
@mutex.sleep(timeout)
|
22
|
+
end
|
17
23
|
end
|
18
|
-
|
19
|
-
|
24
|
+
if timeout
|
25
|
+
# If we had a timeout we may have woken due to it expiring rather than
|
26
|
+
# due to signal being called. In that case we need to remove ourself from the sleepers.
|
20
27
|
@mutex.synchronize do
|
21
|
-
@sleeping.
|
28
|
+
i = @sleeping.index(Thread.current)
|
29
|
+
if i
|
30
|
+
@count += 1
|
31
|
+
@sleeping.delete_at(i)
|
32
|
+
result = false
|
33
|
+
end
|
22
34
|
end
|
23
|
-
Thread.stop
|
24
35
|
end
|
36
|
+
result
|
25
37
|
end
|
26
38
|
|
27
39
|
# Signal the semaphore. If the count is below zero the waiting threads are woken.
|
@@ -30,14 +42,16 @@ class Semaphore
|
|
30
42
|
@mutex.synchronize do
|
31
43
|
c = @count
|
32
44
|
@count += 1
|
33
|
-
|
34
|
-
if c < 0
|
35
|
-
t = nil
|
36
|
-
@mutex.synchronize do
|
45
|
+
if c < 0
|
37
46
|
t = @sleeping.shift
|
47
|
+
t.wakeup if t
|
38
48
|
end
|
39
|
-
t.wakeup if t
|
40
49
|
end
|
41
50
|
end
|
51
|
+
|
52
|
+
# Testing method.
|
53
|
+
def count
|
54
|
+
@count
|
55
|
+
end
|
42
56
|
end
|
43
57
|
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'pqueue'
|
2
|
+
|
3
|
+
module QuartzTorrent
|
4
|
+
# Class used to manage timers.
|
5
|
+
class TimerManager
|
6
|
+
class TimerInfo
|
7
|
+
def initialize(duration, recurring, metainfo)
|
8
|
+
@duration = duration
|
9
|
+
@recurring = recurring
|
10
|
+
@metainfo = metainfo
|
11
|
+
@cancelled = false
|
12
|
+
refresh
|
13
|
+
end
|
14
|
+
attr_accessor :recurring
|
15
|
+
attr_accessor :duration
|
16
|
+
attr_accessor :expiry
|
17
|
+
attr_accessor :metainfo
|
18
|
+
# Since pqueue doesn't allow removal of anything but the head
|
19
|
+
# we flag deleted items so they are deleted when they are pulled
|
20
|
+
attr_accessor :cancelled
|
21
|
+
|
22
|
+
def secondsUntilExpiry
|
23
|
+
@expiry - Time.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def refresh
|
27
|
+
@expiry = Time.new + @duration
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(logger = nil)
|
32
|
+
@queue = PQueue.new { |a,b| b.expiry <=> a.expiry }
|
33
|
+
@mutex = Mutex.new
|
34
|
+
@logger = logger
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add a timer. Parameter 'duration' specifies the timer duration in seconds,
|
38
|
+
# 'metainfo' is caller information passed to the handler when the timer expires,
|
39
|
+
# 'recurring' should be true if the timer will repeat, or false if it will only
|
40
|
+
# expire once, and 'immed' when true specifies that the timer should expire immediately
|
41
|
+
# (and again each duration if recurring) while false specifies that the timer will only
|
42
|
+
# expire the first time after it's duration elapses.
|
43
|
+
def add(duration, metainfo = nil, recurring = true, immed = false)
|
44
|
+
raise "TimerManager.add: Timer duration may not be nil" if duration.nil?
|
45
|
+
info = TimerInfo.new(duration, recurring, metainfo)
|
46
|
+
info.expiry = Time.new if immed
|
47
|
+
@mutex.synchronize{ @queue.push info }
|
48
|
+
info
|
49
|
+
end
|
50
|
+
|
51
|
+
# For testing. Add a cancelled timer.
|
52
|
+
def add_cancelled(duration, metainfo = nil, recurring = true, immed = false)
|
53
|
+
raise "TimerManager.add: Timer duration may not be nil" if duration.nil?
|
54
|
+
info = TimerInfo.new(duration, recurring, metainfo)
|
55
|
+
info.expiry = Time.new if immed
|
56
|
+
info.cancelled = true
|
57
|
+
@mutex.synchronize{ @queue.push info }
|
58
|
+
info
|
59
|
+
end
|
60
|
+
|
61
|
+
# Cancel a timer.
|
62
|
+
def cancel(timerInfo)
|
63
|
+
timerInfo.cancelled = true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return the next timer event from the queue, but don't remove it from the queue.
|
67
|
+
def peek
|
68
|
+
result = nil
|
69
|
+
@mutex.synchronize do
|
70
|
+
clearCancelled
|
71
|
+
result = @queue.top
|
72
|
+
end
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
# Remove the next timer event from the queue and return it as a TimerHandler::TimerInfo object.
|
77
|
+
# Warning: if the timer is a recurring timer, the secondsUntilExpiry will be set to the NEXT time
|
78
|
+
# the timer would expire, instead of this time. If the original secondsUntilExpiry is needed,
|
79
|
+
# pass a block to this method, and the block will be called with the original secondsUntilExpiry.
|
80
|
+
def next
|
81
|
+
result = nil
|
82
|
+
@mutex.synchronize do
|
83
|
+
clearCancelled
|
84
|
+
result = @queue.pop
|
85
|
+
end
|
86
|
+
if result
|
87
|
+
yield result.secondsUntilExpiry if block_given?
|
88
|
+
if result.recurring
|
89
|
+
result.refresh
|
90
|
+
@mutex.synchronize{ @queue.push result }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_s
|
97
|
+
arr = nil
|
98
|
+
@mutex.synchronize do
|
99
|
+
arr = @queue.to_a
|
100
|
+
end
|
101
|
+
s = "now = #{Time.new}. Queue = ["
|
102
|
+
arr.each do |e|
|
103
|
+
s << "(#{e.object_id};#{e.expiry};#{e.metainfo[0]};#{e.secondsUntilExpiry}),"
|
104
|
+
end
|
105
|
+
s << "]"
|
106
|
+
end
|
107
|
+
|
108
|
+
def empty?
|
109
|
+
@queue.empty?
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def clearCancelled
|
115
|
+
while @queue.top && @queue.top.cancelled
|
116
|
+
@queue.pop
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "quartz_torrent/log.rb"
|
2
|
+
|
3
|
+
module QuartzTorrent
|
4
|
+
class TorrentQueue
|
5
|
+
# The maxIncomplete and maxActive parameters specify how many torrents may be unpaused and unqueued at once.
|
6
|
+
# Parameter maxActive is the total maximum number of active torrents (unpaused, unqueued), and maxIncomplete is a subset of
|
7
|
+
# maxActive that are incomplete and thus downloading (as opposed to only uploading). An exception is thrown if
|
8
|
+
# maxIncomplete > maxActive.
|
9
|
+
#
|
10
|
+
def initialize(maxIncomplete, maxActive)
|
11
|
+
raise "The maximum number of running torrents may not be larger than the maximum number of unqueued torrents" if maxIncomplete > maxActive
|
12
|
+
@maxIncomplete = maxIncomplete
|
13
|
+
@maxActive = maxActive
|
14
|
+
@queue = []
|
15
|
+
@logger = LogManager.getLogger("queue")
|
16
|
+
end
|
17
|
+
|
18
|
+
# Compute which torrents can now be unqueued based on the state of running torrents.
|
19
|
+
# Parameter torrentDatas should be an array of TorrentData that the decision will be based off. At a minimum
|
20
|
+
# these items should respond to 'paused', 'queued', 'state', 'queued='
|
21
|
+
def dequeue(torrentDatas)
|
22
|
+
numActive = 0
|
23
|
+
numIncomplete = 0
|
24
|
+
torrentDatas.each do |torrentData|
|
25
|
+
next if torrentData.paused || torrentData.queued
|
26
|
+
numIncomplete += 1 if incomplete?(torrentData)
|
27
|
+
numActive += 1
|
28
|
+
end
|
29
|
+
@logger.debug "incomplete: #{numIncomplete}/#{@maxIncomplete} active: #{numActive}/#{@maxActive}"
|
30
|
+
@logger.debug "Queue contains #{size} torrents"
|
31
|
+
|
32
|
+
torrents = []
|
33
|
+
|
34
|
+
while numActive < @maxActive
|
35
|
+
torrentData = nil
|
36
|
+
if numIncomplete < @maxIncomplete
|
37
|
+
# Unqueue first incomplete torrent from queue
|
38
|
+
torrentData = dequeueFirstMatching{ |torrentData| incomplete?(torrentData)}
|
39
|
+
@logger.debug "#{torrentData ? "dequeued" : "failed to dequeue"} an incomplete torrent"
|
40
|
+
numIncomplete += 1 if torrentData
|
41
|
+
end
|
42
|
+
if ! torrentData
|
43
|
+
# Unqueue first complete (uploading) torrent from queue
|
44
|
+
torrentData = dequeueFirstMatching{ |torrentData| !incomplete?(torrentData)}
|
45
|
+
@logger.debug "#{torrentData ? "dequeued" : "failed to dequeue"} a complete torrent"
|
46
|
+
end
|
47
|
+
numActive += 1 if torrentData
|
48
|
+
|
49
|
+
if torrentData
|
50
|
+
torrentData.queued = false
|
51
|
+
torrents.push torrentData
|
52
|
+
else
|
53
|
+
break
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@logger.debug "Will dequeue #{torrents.size}"
|
58
|
+
|
59
|
+
torrents
|
60
|
+
end
|
61
|
+
|
62
|
+
def push(torrentData)
|
63
|
+
torrentData.queued = true
|
64
|
+
@queue.push torrentData
|
65
|
+
end
|
66
|
+
|
67
|
+
def unshift(torrentData)
|
68
|
+
torrentData.queued = true
|
69
|
+
@queue.unshift torrentData
|
70
|
+
end
|
71
|
+
|
72
|
+
def size
|
73
|
+
@queue.size
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def incomplete?(torrentData)
|
79
|
+
torrentData.state != :uploading
|
80
|
+
end
|
81
|
+
|
82
|
+
def dequeueFirstMatching
|
83
|
+
index = @queue.index{ |torrentData| yield(torrentData) }
|
84
|
+
if index
|
85
|
+
@queue.delete_at index
|
86
|
+
else
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|