quartz_torrent 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|