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
data/README.md
CHANGED
@@ -16,6 +16,7 @@ Features:
|
|
16
16
|
- Upload and download rate limiting
|
17
17
|
- Upload ratio enforcement
|
18
18
|
- Upload duration limit
|
19
|
+
- Torrent Queueing
|
19
20
|
|
20
21
|
Requirements
|
21
22
|
------------
|
@@ -99,14 +100,10 @@ And a specific test case in a test using:
|
|
99
100
|
|
100
101
|
To-Do
|
101
102
|
-----
|
102
|
-
- Start a peerclient with the full torrent. Connect with another peerclient and start downloading, then pause.
|
103
|
+
- Bug: Start a peerclient with the full torrent. Connect with another peerclient and start downloading, then pause.
|
103
104
|
Finally unpause. The "server" peerclient will refuse the connections to re-establish because it believes the
|
104
105
|
peer is already connected.
|
105
|
-
- Improve CPU usage.
|
106
|
-
- Implement endgame strategy and support Cancel messages.
|
107
|
-
- Refactor Metadata.Info into it's own independent class.
|
108
106
|
- Improve Documentation
|
109
|
-
- In peerclient, prefix log messages with torrent infohash, or (truncated) torrent name
|
110
107
|
- Implement uTP
|
111
108
|
- <http://www.bittorrent.org/beps/bep_0029.html#packet-sizes>
|
112
109
|
- <https://forum.utorrent.com/viewtopic.php?id=76640>
|
@@ -128,19 +128,16 @@ class SummaryScreen < Screen
|
|
128
128
|
end
|
129
129
|
|
130
130
|
private
|
131
|
-
def summaryLine(state, paused, size, uploadRate, downloadRate, complete, total, progress)
|
131
|
+
def summaryLine(state, paused, queued, size, uploadRate, downloadRate, complete, total, progress)
|
132
132
|
if state == :downloading_metainfo
|
133
133
|
" %12s Rate: %6s | %6s Bytes: %4d/%4d Progress: %5s\n" % [state, uploadRate, downloadRate, complete, total, progress]
|
134
134
|
else
|
135
135
|
primaryState = state.to_s
|
136
136
|
secondaryState = ""
|
137
|
-
|
138
|
-
|
139
|
-
elsif (state == :running || state == :uploading) && paused
|
140
|
-
secondaryState = "(paused)"
|
141
|
-
end
|
137
|
+
secondaryState += "(paused)" if paused
|
138
|
+
secondaryState += "(queued)" if queued
|
142
139
|
state = "#{primaryState} #{secondaryState}"
|
143
|
-
" %
|
140
|
+
" %14s %9s Rate: %6s | %6s Pieces: %4d/%4d Progress: %5s\n" % [state, size, uploadRate, downloadRate, complete, total, progress]
|
144
141
|
end
|
145
142
|
end
|
146
143
|
|
@@ -189,6 +186,7 @@ class SummaryScreen < Screen
|
|
189
186
|
display.push summaryLine(
|
190
187
|
state,
|
191
188
|
torrent.paused,
|
189
|
+
torrent.queued,
|
192
190
|
Formatter.formatSize(dataLength),
|
193
191
|
Formatter.formatSpeed(torrent.uploadRate),
|
194
192
|
Formatter.formatSpeed(torrent.downloadRate),
|
@@ -348,7 +346,7 @@ class DebugScreen < Screen
|
|
348
346
|
@profiler.trackClass QuartzTorrent::PieceManager::Result
|
349
347
|
@profiler.trackClass QuartzTorrent::Formatter
|
350
348
|
@profiler.trackClass QuartzTorrent::TrackerClient
|
351
|
-
@profiler.trackClass QuartzTorrent::
|
349
|
+
@profiler.trackClass QuartzTorrent::HttpTrackerDriver
|
352
350
|
@profiler.trackClass QuartzTorrent::InterruptibleSleep
|
353
351
|
@profiler.trackClass QuartzTorrent::LogManager
|
354
352
|
@profiler.trackClass QuartzTorrent::Metainfo
|
@@ -392,7 +390,7 @@ class DebugScreen < Screen
|
|
392
390
|
@profiler.trackClass QuartzTorrent::TrackerDynamicRequestParams
|
393
391
|
@profiler.trackClass QuartzTorrent::TrackerResponse
|
394
392
|
@profiler.trackClass QuartzTorrent::TrackerClient
|
395
|
-
@profiler.trackClass QuartzTorrent::
|
393
|
+
@profiler.trackClass QuartzTorrent::UdpTrackerDriver
|
396
394
|
@profiler.trackClass QuartzTorrent::UdpTrackerMessage
|
397
395
|
@profiler.trackClass QuartzTorrent::UdpTrackerRequest
|
398
396
|
@profiler.trackClass QuartzTorrent::UdpTrackerResponse
|
@@ -605,17 +603,6 @@ def initializeLogging(file)
|
|
605
603
|
setLogfile file
|
606
604
|
setDefaultLevel :info
|
607
605
|
end
|
608
|
-
LogManager.setLevel "peer_manager", :debug
|
609
|
-
LogManager.setLevel "tracker_client", :debug
|
610
|
-
LogManager.setLevel "http_tracker_client", :debug
|
611
|
-
LogManager.setLevel "udp_tracker_client", :debug
|
612
|
-
LogManager.setLevel "peerclient", :debug
|
613
|
-
LogManager.setLevel "peerclient.reactor", :info
|
614
|
-
#LogManager.setLevel "peerclient.reactor", :debug
|
615
|
-
LogManager.setLevel "blockstate", :debug
|
616
|
-
LogManager.setLevel "piecemanager", :info
|
617
|
-
LogManager.setLevel "peerholder", :debug
|
618
|
-
#LogManager.setLevel "peermsg_serializer", :debug
|
619
606
|
end
|
620
607
|
|
621
608
|
def help
|
@@ -657,7 +644,9 @@ class Settings
|
|
657
644
|
@uploadLimit = nil
|
658
645
|
@downloadLimit = nil
|
659
646
|
@uploadRatio = nil
|
660
|
-
@logfile = "/tmp/download_torrent_curses.log"
|
647
|
+
@logfile = "/tmp/download_torrent_curses.pid#{Process.pid}_.log"
|
648
|
+
@maxIncomplete = 5
|
649
|
+
@maxActive = 10
|
661
650
|
end
|
662
651
|
|
663
652
|
attr_accessor :baseDirectory
|
@@ -667,6 +656,20 @@ class Settings
|
|
667
656
|
attr_accessor :uploadRatio
|
668
657
|
attr_accessor :logfile
|
669
658
|
attr_accessor :debugTTY
|
659
|
+
attr_accessor :maxIncomplete
|
660
|
+
attr_accessor :maxActive
|
661
|
+
|
662
|
+
def validate
|
663
|
+
if @maxIncomplete > @maxActive
|
664
|
+
puts "Max number of incomplete torrents must be <= Max number of active torrents"
|
665
|
+
return false
|
666
|
+
end
|
667
|
+
if @maxActive <= 0 || @maxIncomplete <= 0
|
668
|
+
puts "Max number of incomplete torrents and max number of active torrents must both be > 0"
|
669
|
+
return false
|
670
|
+
end
|
671
|
+
true
|
672
|
+
end
|
670
673
|
end
|
671
674
|
|
672
675
|
class PeerClient
|
@@ -702,6 +705,8 @@ begin
|
|
702
705
|
[ '--help', '-h', GetoptLong::NO_ARGUMENT],
|
703
706
|
[ '--ratio', '-r', GetoptLong::REQUIRED_ARGUMENT],
|
704
707
|
[ '--debug-tty', '-t', GetoptLong::REQUIRED_ARGUMENT],
|
708
|
+
[ '--queue-max-incomplete', '-i', GetoptLong::REQUIRED_ARGUMENT],
|
709
|
+
[ '--queue-max-active', '-a', GetoptLong::REQUIRED_ARGUMENT],
|
705
710
|
)
|
706
711
|
|
707
712
|
opts.each do |opt, arg|
|
@@ -720,14 +725,18 @@ begin
|
|
720
725
|
$settings.uploadRatio = arg.to_f
|
721
726
|
elsif opt == '--debug-tty'
|
722
727
|
$log = File.open arg, "w"
|
728
|
+
elsif opt == '--queue-max-incomplete'
|
729
|
+
$settings.maxIncomplete = arg.to_i
|
730
|
+
elsif opt == '--queue-max-active'
|
731
|
+
$settings.maxActive = arg.to_i
|
723
732
|
end
|
724
733
|
end
|
734
|
+
exit 1 if ! $settings.validate
|
725
735
|
|
726
736
|
torrents = ARGV
|
727
737
|
|
728
738
|
$log = File.open("/dev/null","w") if $log == $stdout
|
729
739
|
|
730
|
-
|
731
740
|
initializeCurses
|
732
741
|
cursesInitialized = true
|
733
742
|
initializeLogging($settings.logfile)
|
@@ -743,7 +752,7 @@ begin
|
|
743
752
|
scrManager.add :add, AddScreen.new(Ncurses::stdscr)
|
744
753
|
scrManager.set :summary
|
745
754
|
|
746
|
-
peerclient = QuartzTorrent::PeerClient.new($settings.baseDirectory)
|
755
|
+
peerclient = QuartzTorrent::PeerClient.new($settings.baseDirectory, $settings.maxIncomplete, $settings.maxActive)
|
747
756
|
peerclient.port = $settings.port
|
748
757
|
|
749
758
|
torrents.each do |torrent|
|
@@ -769,7 +778,6 @@ begin
|
|
769
778
|
|
770
779
|
RubyProf.start if $doProfiling
|
771
780
|
|
772
|
-
#puts "Starting peer client"
|
773
781
|
peerclient.start
|
774
782
|
|
775
783
|
while running
|
data/bin/quartztorrent_show_info
CHANGED
@@ -29,7 +29,7 @@ end
|
|
29
29
|
info = nil
|
30
30
|
begin
|
31
31
|
metainfo = Metainfo.createFromFile(torrent)
|
32
|
-
puts "Info Hash: #{bytesToHex(metainfo.infoHash)}"
|
32
|
+
puts "Info Hash: #{QuartzTorrent.bytesToHex(metainfo.infoHash)}"
|
33
33
|
puts "Announce: #{metainfo.announce}" if metainfo.announce
|
34
34
|
if metainfo.announceList
|
35
35
|
puts "Announce list: "
|
@@ -43,10 +43,7 @@ begin
|
|
43
43
|
info = metainfo.info
|
44
44
|
rescue
|
45
45
|
# Doesn't seem to be a complete .torrent file. Maybe it is just the info part.
|
46
|
-
|
47
|
-
bencoded = file.read
|
48
|
-
info = Metainfo::Info.createFromBdecode(bencoded.bdecode)
|
49
|
-
end
|
46
|
+
info = info = Metainfo::Info.createFromBdecode(BEncode.load_file(torrent, {:ignore_trailing_junk => 1} ))
|
50
47
|
end
|
51
48
|
|
52
49
|
if info
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module QuartzTorrent
|
2
|
+
class Alarm
|
3
|
+
def initialize(id, details)
|
4
|
+
@details = details
|
5
|
+
@time = Time.new
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_accessor :id
|
9
|
+
attr_accessor :details
|
10
|
+
attr_accessor :time
|
11
|
+
end
|
12
|
+
|
13
|
+
class Alarms
|
14
|
+
def initialize
|
15
|
+
@alarms = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Raise a new alarm, or overwrite the existing alarm with the same id if one exists.
|
19
|
+
def raise(alarm)
|
20
|
+
@alarms[alarm.id] = alarm
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear(alarm)
|
24
|
+
if alarm.is_a?(Alarm)
|
25
|
+
@alarms.delete alarm.id
|
26
|
+
else
|
27
|
+
# Assume variable `alarm` is an id.
|
28
|
+
@alarms.delete alarm
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def all
|
33
|
+
@alarms.values
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -222,6 +222,13 @@ module QuartzTorrent
|
|
222
222
|
result
|
223
223
|
end
|
224
224
|
|
225
|
+
# Get a bitfield representing the completed pieces.
|
226
|
+
def completeBlockBitfield
|
227
|
+
result = Bitfield.new(@numBlocks)
|
228
|
+
result.copyFrom(@completeBlocks)
|
229
|
+
result
|
230
|
+
end
|
231
|
+
|
225
232
|
# Number of bytes we have downloaded and verified.
|
226
233
|
def completedLength
|
227
234
|
num = @completeBlocks.countSet
|
@@ -11,6 +11,7 @@ module QuartzTorrent
|
|
11
11
|
|
12
12
|
# Format a size in bytes.
|
13
13
|
def self.formatSize(size)
|
14
|
+
return nil if !size
|
14
15
|
s = size.to_f
|
15
16
|
if s >= Gig
|
16
17
|
s = "%.2fGB" % (s / Gig)
|
@@ -26,17 +27,24 @@ module QuartzTorrent
|
|
26
27
|
|
27
28
|
# Format a floating point number as a percentage with one decimal place.
|
28
29
|
def self.formatPercent(frac)
|
30
|
+
return nil if ! frac
|
29
31
|
s = "%.1f" % (frac.to_f*100)
|
30
32
|
s + "%"
|
31
33
|
end
|
32
34
|
|
33
35
|
# Format a speed in bytes per second.
|
34
36
|
def self.formatSpeed(s)
|
35
|
-
Formatter.formatSize(s)
|
37
|
+
size = Formatter.formatSize(s)
|
38
|
+
if size
|
39
|
+
size + "/s"
|
40
|
+
else
|
41
|
+
nil
|
42
|
+
end
|
36
43
|
end
|
37
44
|
|
38
45
|
# Format a duration of time in seconds.
|
39
46
|
def self.formatTime(secs)
|
47
|
+
return nil if ! secs
|
40
48
|
s = ""
|
41
49
|
time = secs.to_i
|
42
50
|
arr = []
|
@@ -68,6 +76,7 @@ module QuartzTorrent
|
|
68
76
|
|
69
77
|
# Parse a size in the format '50 KB'
|
70
78
|
def self.parseSize(size)
|
79
|
+
return nil if ! size
|
71
80
|
if size =~ /(\d+(?:\.\d+)?)\s*([^\d]+)*/
|
72
81
|
value = $1.to_f
|
73
82
|
suffix = ($2 ? $2.downcase : nil)
|
@@ -1,13 +1,17 @@
|
|
1
1
|
module QuartzTorrent
|
2
2
|
class TrackerClient
|
3
3
|
end
|
4
|
+
class TrackerDriver
|
5
|
+
end
|
4
6
|
|
5
|
-
# A tracker
|
6
|
-
class
|
7
|
-
def initialize(announceUrl, infoHash
|
8
|
-
super(
|
7
|
+
# A tracker driver that uses the HTTP protocol. This is the classic BitTorrent tracker protocol.
|
8
|
+
class HttpTrackerDriver < TrackerDriver
|
9
|
+
def initialize(announceUrl, infoHash)
|
10
|
+
super()
|
9
11
|
@startSent = false
|
10
12
|
@logger = LogManager.getLogger("http_tracker_client")
|
13
|
+
@announceUrl = announceUrl
|
14
|
+
@infoHash = infoHash
|
11
15
|
end
|
12
16
|
|
13
17
|
# Request a list of peers from the tracker and return it as a TrackerResponse.
|
@@ -24,8 +28,8 @@ module QuartzTorrent
|
|
24
28
|
|
25
29
|
params = {}
|
26
30
|
params['info_hash'] = CGI.escape(@infoHash)
|
27
|
-
params['peer_id'] =
|
28
|
-
params['port'] =
|
31
|
+
params['peer_id'] = dynamicParams.peerId
|
32
|
+
params['port'] = dynamicParams.port
|
29
33
|
params['uploaded'] = dynamicParams.uploaded.to_s
|
30
34
|
params['downloaded'] = dynamicParams.downloaded.to_s
|
31
35
|
params['left'] = dynamicParams.left.to_s
|
@@ -50,7 +54,11 @@ module QuartzTorrent
|
|
50
54
|
end
|
51
55
|
uri.query = query
|
52
56
|
|
53
|
-
|
57
|
+
begin
|
58
|
+
res = Net::HTTP.get_response(uri)
|
59
|
+
rescue Timeout::Error
|
60
|
+
return TrackerResponse.new(false, "Tracker request timed out", [])
|
61
|
+
end
|
54
62
|
@logger.debug "Tracker response code: #{res.code}"
|
55
63
|
@logger.debug "Tracker response body: #{res.body}"
|
56
64
|
result = buildTrackerResponse(res)
|
@@ -60,6 +60,16 @@ module QuartzTorrent
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
+
# Return an array of all tracker URLS in the magnet link.
|
64
|
+
def trackers
|
65
|
+
tr = @params['tr']
|
66
|
+
if tr
|
67
|
+
tr
|
68
|
+
else
|
69
|
+
[]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
63
73
|
# Return the first display name found in the magnet link. Returns nil if the magnet has no display name.
|
64
74
|
def displayName
|
65
75
|
dn = @params['dn']
|
@@ -58,6 +58,7 @@ module QuartzTorrent
|
|
58
58
|
# multi-file download. For a single-file download the
|
59
59
|
@files = []
|
60
60
|
@logger = LogManager.getLogger("metainfo")
|
61
|
+
@source = nil
|
61
62
|
end
|
62
63
|
|
63
64
|
# Array of FileInfo objects
|
@@ -70,6 +71,8 @@ module QuartzTorrent
|
|
70
71
|
attr_accessor :pieces
|
71
72
|
# True if no external peer source is allowed.
|
72
73
|
attr_accessor :private
|
74
|
+
# Optional source
|
75
|
+
attr_accessor :source
|
73
76
|
|
74
77
|
# Total length of the torrent data in bytes.
|
75
78
|
def dataLength
|
@@ -81,6 +84,7 @@ module QuartzTorrent
|
|
81
84
|
result = Info.new
|
82
85
|
result.pieceLen = infoDict['piece length']
|
83
86
|
result.private = infoDict['private']
|
87
|
+
result.source = infoDict['source']
|
84
88
|
result.pieces = parsePieces(Metainfo.valueOrException(infoDict['pieces'], "Torrent metainfo is missing the pieces property."))
|
85
89
|
result.name = Metainfo.valueOrException(infoDict['name'], "Torrent metainfo is missing the name property.")
|
86
90
|
|
@@ -110,6 +114,7 @@ module QuartzTorrent
|
|
110
114
|
|
111
115
|
hash['piece length'] = @pieceLen
|
112
116
|
hash['private'] = @private if @private
|
117
|
+
hash['source'] = @source if @source
|
113
118
|
hash['name'] = @name
|
114
119
|
hash['pieces'] = @pieces.join
|
115
120
|
|
@@ -177,7 +182,7 @@ module QuartzTorrent
|
|
177
182
|
def self.createFromString(data)
|
178
183
|
logger = LogManager.getLogger("metainfo")
|
179
184
|
|
180
|
-
decoded = data
|
185
|
+
decoded = BEncode.load(data, {:ignore_trailing_junk => 1})
|
181
186
|
logger.debug "Decoded torrent metainfo: #{decoded.inspect}"
|
182
187
|
result = Metainfo.new
|
183
188
|
result.createdBy = decoded['created by']
|
@@ -92,7 +92,7 @@ module QuartzTorrent
|
|
92
92
|
# Sanity check
|
93
93
|
testInfoHash = Digest::SHA1.digest( bencoded )
|
94
94
|
if testInfoHash == infoHash
|
95
|
-
result = Metainfo::Info.createFromBdecode(bencoded
|
95
|
+
result = Metainfo::Info.createFromBdecode(BEncode.load(bencoded, {:ignore_trailing_junk => 1}))
|
96
96
|
else
|
97
97
|
logger.info "the computed SHA1 hash doesn't match the specified infoHash in #{path}"
|
98
98
|
end
|
@@ -120,7 +120,11 @@ module QuartzTorrent
|
|
120
120
|
|
121
121
|
# Return true if the specified piece is completed. The piece is specified by index.
|
122
122
|
def pieceCompleted?(pieceIndex)
|
123
|
-
@completePieces.
|
123
|
+
if pieceIndex >= 0 && pieceIndex < @completePieces.length
|
124
|
+
@completePieces.set? pieceIndex
|
125
|
+
else
|
126
|
+
false
|
127
|
+
end
|
124
128
|
end
|
125
129
|
|
126
130
|
# Do we have all the pieces of the metadata?
|
@@ -13,6 +13,8 @@ require "quartz_torrent/piecemanagerrequestmetadata.rb"
|
|
13
13
|
require "quartz_torrent/metainfopiecestate.rb"
|
14
14
|
require "quartz_torrent/extension.rb"
|
15
15
|
require "quartz_torrent/magnet.rb"
|
16
|
+
require "quartz_torrent/torrentqueue.rb"
|
17
|
+
require "quartz_torrent/alarm.rb"
|
16
18
|
|
17
19
|
|
18
20
|
module QuartzTorrent
|
@@ -52,11 +54,14 @@ module QuartzTorrent
|
|
52
54
|
@checkPieceManagerTimer = nil
|
53
55
|
@requestBlocksTimer = nil
|
54
56
|
@paused = false
|
57
|
+
@queued = false
|
55
58
|
@downRateLimit = nil
|
56
59
|
@upRateLimit = nil
|
57
60
|
@ratio = nil
|
58
61
|
@uploadDuration = nil
|
59
62
|
@downloadCompletedTime = nil
|
63
|
+
@isEndgame = false
|
64
|
+
@alarms = Alarms.new
|
60
65
|
end
|
61
66
|
# The torrents Metainfo.Info struct. This is nil if the torrent has no metadata and we need to download it
|
62
67
|
# (i.e. a magnet link)
|
@@ -79,7 +84,15 @@ module QuartzTorrent
|
|
79
84
|
attr_accessor :bytesUploadedDataOnly
|
80
85
|
attr_accessor :bytesDownloaded
|
81
86
|
attr_accessor :bytesUploaded
|
87
|
+
# State of the torrent. Is one of the following states:
|
88
|
+
# :initializing Datastructures have been created, but no work started.
|
89
|
+
# :checking_pieces Checking piece hashes on startup
|
90
|
+
# :downloading_metainfo Downloading the torrent metainfo
|
91
|
+
# :uploading The torrent is complete and we are only uploading
|
92
|
+
# :running The torrent is incomplete and we are downloading and uploading
|
93
|
+
# :error There was an unrecoverable error with the torrent.
|
82
94
|
attr_accessor :state
|
95
|
+
attr_accessor :isEndgame
|
83
96
|
attr_accessor :metainfoPieceState
|
84
97
|
# The timer handle for the timer that requests metainfo pieces. This is used to cancel the
|
85
98
|
# timer when the metadata is completely downloaded.
|
@@ -92,8 +105,10 @@ module QuartzTorrent
|
|
92
105
|
attr_accessor :checkPieceManagerTimer
|
93
106
|
# Timer handle for timer that requests blocks
|
94
107
|
attr_accessor :requestBlocksTimer
|
95
|
-
|
108
|
+
# Is the torrent paused
|
96
109
|
attr_accessor :paused
|
110
|
+
# Is the torrent queued
|
111
|
+
attr_accessor :queued
|
97
112
|
# The RateLimit for downloading this torrent.
|
98
113
|
attr_accessor :downRateLimit
|
99
114
|
# The RateLimit for uploading to peers for this torrent.
|
@@ -105,7 +120,8 @@ module QuartzTorrent
|
|
105
120
|
attr_accessor :uploadDuration
|
106
121
|
# Time at which we completely downloaded all bytes of the torrent.
|
107
122
|
attr_accessor :downloadCompletedTime
|
108
|
-
|
123
|
+
# Alarms object for this torrent
|
124
|
+
attr_reader :alarms
|
109
125
|
end
|
110
126
|
|
111
127
|
# Data about torrents for use by the end user.
|
@@ -150,6 +166,8 @@ module QuartzTorrent
|
|
150
166
|
attr_reader :metainfoCompletedLength
|
151
167
|
# Whether or not the torrent is paused.
|
152
168
|
attr_reader :paused
|
169
|
+
# Whether or not the torrent is queued.
|
170
|
+
attr_reader :queued
|
153
171
|
# After we have completed downloading a torrent, we will continue to upload until we have
|
154
172
|
# uploaded ratio * torrent_size bytes. If nil, no limit on upload.
|
155
173
|
attr_accessor :ratio
|
@@ -158,6 +176,8 @@ module QuartzTorrent
|
|
158
176
|
attr_accessor :bytesDownloadedDataOnly
|
159
177
|
attr_accessor :bytesUploaded
|
160
178
|
attr_accessor :bytesDownloaded
|
179
|
+
# Array of currently raised alarms
|
180
|
+
attr_accessor :alarms
|
161
181
|
|
162
182
|
# Update the data in this TorrentDataDelegate from the torrentData
|
163
183
|
# object that it was created from. TODO: What if that torrentData is now gone?
|
@@ -199,6 +219,7 @@ module QuartzTorrent
|
|
199
219
|
@state = torrentData.state
|
200
220
|
@metainfoLength = nil
|
201
221
|
@paused = torrentData.paused
|
222
|
+
@queued = torrentData.queued
|
202
223
|
@metainfoCompletedLength = nil
|
203
224
|
if torrentData.metainfoPieceState && torrentData.state == :downloading_metainfo
|
204
225
|
@metainfoLength = torrentData.metainfoPieceState.metainfoLength
|
@@ -219,6 +240,7 @@ module QuartzTorrent
|
|
219
240
|
@uploadRateLimit = torrentData.upRateLimit.unitsPerSecond if torrentData.upRateLimit
|
220
241
|
@ratio = torrentData.ratio
|
221
242
|
@uploadDuration = torrentData.uploadDuration
|
243
|
+
@alarms = torrentData.alarms.all
|
222
244
|
end
|
223
245
|
|
224
246
|
def buildPeersList(torrentData)
|
@@ -233,9 +255,10 @@ module QuartzTorrent
|
|
233
255
|
# This class implements a Reactor Handler object. This Handler implements the PeerClient.
|
234
256
|
class PeerClientHandler < QuartzTorrent::Handler
|
235
257
|
|
236
|
-
def initialize(baseDirectory)
|
258
|
+
def initialize(baseDirectory, maxIncomplete = 5, maxActive = 10)
|
237
259
|
# Hash of TorrentData objects, keyed by torrent infoHash
|
238
260
|
@torrentData = {}
|
261
|
+
@torrentQueue = TorrentQueue.new(maxIncomplete, maxActive)
|
239
262
|
|
240
263
|
@baseDirectory = baseDirectory
|
241
264
|
|
@@ -250,8 +273,11 @@ module QuartzTorrent
|
|
250
273
|
@requestBlocksPeriod = 1
|
251
274
|
@handshakeTimeout = 1
|
252
275
|
@requestTimeout = 60
|
276
|
+
@endgameBlockThreshold = 20
|
253
277
|
end
|
254
278
|
|
279
|
+
################################################ PUBLIC API METHODS ################################################
|
280
|
+
|
255
281
|
attr_reader :torrentData
|
256
282
|
|
257
283
|
# Add a new tracker client. This effectively adds a new torrent to download. Returns the TorrentData object for the
|
@@ -259,51 +285,28 @@ module QuartzTorrent
|
|
259
285
|
def addTrackerClient(infoHash, info, trackerclient)
|
260
286
|
raise "There is already a tracker registered for torrent #{QuartzTorrent.bytesToHex(infoHash)}" if @torrentData.has_key? infoHash
|
261
287
|
torrentData = TorrentData.new(infoHash, info, trackerclient)
|
288
|
+
trackerclient.alarms = torrentData.alarms
|
262
289
|
@torrentData[infoHash] = torrentData
|
290
|
+
torrentData.info = info
|
291
|
+
torrentData.state = :initializing
|
263
292
|
|
264
|
-
|
265
|
-
|
266
|
-
if ! info
|
267
|
-
info = MetainfoPieceState.downloaded(@baseDirectory, torrentData.infoHash)
|
268
|
-
torrentData.info = info
|
269
|
-
end
|
270
|
-
|
271
|
-
if info
|
272
|
-
startCheckingPieces torrentData
|
273
|
-
else
|
274
|
-
# Request the metainfo from peers.
|
275
|
-
torrentData.state = :downloading_metainfo
|
276
|
-
|
277
|
-
@logger.info "Downloading metainfo"
|
278
|
-
#torrentData.metainfoPieceState = MetainfoPieceState.new(@baseDirectory, infoHash, )
|
279
|
-
|
280
|
-
# Schedule peer connection management. Recurring and immediate
|
281
|
-
torrentData.managePeersTimer =
|
282
|
-
@reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], true, true)
|
293
|
+
queue(torrentData)
|
294
|
+
dequeue
|
283
295
|
|
284
|
-
# Schedule a timer for requesting metadata pieces from peers.
|
285
|
-
torrentData.metainfoRequestTimer =
|
286
|
-
@reactor.scheduleTimer(@requestBlocksPeriod, [:request_metadata_pieces, infoHash], true, false)
|
287
|
-
|
288
|
-
# Schedule checking for metainfo PieceManager results (including when piece reading completes)
|
289
|
-
torrentData.checkMetadataPieceManagerTimer =
|
290
|
-
@reactor.scheduleTimer(@requestBlocksPeriod, [:check_metadata_piece_manager, infoHash], true, false)
|
291
|
-
end
|
292
|
-
|
293
296
|
torrentData
|
294
297
|
end
|
295
298
|
|
296
299
|
# Remove a torrent.
|
297
300
|
def removeTorrent(infoHash, deleteFiles = false)
|
298
301
|
# Can't do this right now, since it could be in use by an event handler. Use an immediate, non-recurring timer instead.
|
299
|
-
@logger.info "Scheduling immediate timer to remove torrent
|
302
|
+
@logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Scheduling immediate timer to remove torrent. #{deleteFiles ? "Will" : "Wont"} delete downloaded files."
|
300
303
|
@reactor.scheduleTimer(0, [:removetorrent, infoHash, deleteFiles], false, true)
|
301
304
|
end
|
302
305
|
|
303
306
|
# Pause or unpause the specified torrent.
|
304
307
|
def setPaused(infoHash, value)
|
305
308
|
# Can't do this right now, since it could be in use by an event handler. Use an immediate, non-recurring timer instead.
|
306
|
-
@logger.info "Scheduling immediate timer to pause
|
309
|
+
@logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Scheduling immediate timer to #{value ? "pause" : "unpause"} torrent."
|
307
310
|
@reactor.scheduleTimer(0, [:pausetorrent, infoHash, value], false, true)
|
308
311
|
end
|
309
312
|
|
@@ -381,6 +384,68 @@ module QuartzTorrent
|
|
381
384
|
torrentData.uploadDuration = seconds
|
382
385
|
end
|
383
386
|
|
387
|
+
# Adjust the bytesUploaded property of the specified torrent by the passed amount.
|
388
|
+
# Adjustment should be an integer. It is added to the current bytesUploaded amount.
|
389
|
+
def adjustBytesUploaded(infoHash, adjustment)
|
390
|
+
torrentData = @torrentData[infoHash]
|
391
|
+
if ! torrentData
|
392
|
+
@logger.warn "Asked to adjust uploaded bytes for a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
|
393
|
+
return
|
394
|
+
end
|
395
|
+
|
396
|
+
runInReactorThread do
|
397
|
+
torrentData.bytesUploaded += adjustment
|
398
|
+
torrentData.bytesUploadedDataOnly += adjustment
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Adjust the bytesDownloaded property of the specified torrent by the passed amount.
|
403
|
+
# Adjustment should be an integer. It is added to the current bytesDownloaded amount.
|
404
|
+
def adjustBytesDownloaded(infoHash, adjustment)
|
405
|
+
torrentData = @torrentData[infoHash]
|
406
|
+
if ! torrentData
|
407
|
+
@logger.warn "Asked to adjust uploaded bytes for a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
|
408
|
+
return
|
409
|
+
end
|
410
|
+
|
411
|
+
runInReactorThread do
|
412
|
+
torrentData.bytesDownloaded += adjustment
|
413
|
+
torrentData.bytesDownloadedDataOnly += adjustment
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# Get a hash of new TorrentDataDelegate objects keyed by torrent infohash.
|
418
|
+
# This method is meant to be called from a different thread than the one
|
419
|
+
# the reactor is running in. This method is not immediate but blocks until the
|
420
|
+
# data is prepared.
|
421
|
+
# If infoHash is passed, only that torrent data is returned (still in a hashtable; just one entry)
|
422
|
+
def getDelegateTorrentData(infoHash = nil)
|
423
|
+
# Use an immediate, non-recurring timer.
|
424
|
+
result = {}
|
425
|
+
return result if stopped?
|
426
|
+
semaphore = Semaphore.new
|
427
|
+
timer = @reactor.scheduleTimer(0, [:get_torrent_data, result, semaphore, infoHash], false, true)
|
428
|
+
if semaphore.wait(3)
|
429
|
+
result
|
430
|
+
else
|
431
|
+
@logger.warn "getDelegateTorrentData: Waiting on semaphore timed out"
|
432
|
+
throw "Waiting on semaphore for timer #{timer.object_id} timed out"
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
# Update the data stored in a TorrentDataDelegate to the latest information.
|
437
|
+
def updateDelegateTorrentData(delegate)
|
438
|
+
return if stopped?
|
439
|
+
# Use an immediate, non-recurring timer.
|
440
|
+
semaphore = Semaphore.new
|
441
|
+
@reactor.scheduleTimer(0, [:update_torrent_data, delegate, semaphore], false, true)
|
442
|
+
semaphore.wait
|
443
|
+
result
|
444
|
+
end
|
445
|
+
|
446
|
+
|
447
|
+
################################################ REACTOR METHODS ################################################
|
448
|
+
|
384
449
|
# Reactor method called when a peer has connected to us.
|
385
450
|
def serverInit(metadata, addr, port)
|
386
451
|
# A peer connected to us
|
@@ -638,6 +703,8 @@ module QuartzTorrent
|
|
638
703
|
requestMetadataPieces(metadata[1])
|
639
704
|
elsif metadata.is_a?(Array) && metadata[0] == :check_metadata_piece_manager
|
640
705
|
checkMetadataPieceManagerResults(metadata[1])
|
706
|
+
elsif metadata.is_a?(Array) && metadata[0] == :runproc
|
707
|
+
metadata[1].call
|
641
708
|
else
|
642
709
|
@logger.info "Unknown timer #{metadata} expired."
|
643
710
|
end
|
@@ -657,30 +724,7 @@ module QuartzTorrent
|
|
657
724
|
close
|
658
725
|
end
|
659
726
|
|
660
|
-
|
661
|
-
# This method is meant to be called from a different thread than the one
|
662
|
-
# the reactor is running in. This method is not immediate but blocks until the
|
663
|
-
# data is prepared.
|
664
|
-
# If infoHash is passed, only that torrent data is returned (still in a hashtable; just one entry)
|
665
|
-
def getDelegateTorrentData(infoHash = nil)
|
666
|
-
# Use an immediate, non-recurring timer.
|
667
|
-
result = {}
|
668
|
-
return result if stopped?
|
669
|
-
semaphore = Semaphore.new
|
670
|
-
@reactor.scheduleTimer(0, [:get_torrent_data, result, semaphore, infoHash], false, true)
|
671
|
-
semaphore.wait
|
672
|
-
result
|
673
|
-
end
|
674
|
-
|
675
|
-
def updateDelegateTorrentData(delegate)
|
676
|
-
return if stopped?
|
677
|
-
# Use an immediate, non-recurring timer.
|
678
|
-
semaphore = Semaphore.new
|
679
|
-
@reactor.scheduleTimer(0, [:update_torrent_data, delegate, semaphore], false, true)
|
680
|
-
semaphore.wait
|
681
|
-
result
|
682
|
-
end
|
683
|
-
|
727
|
+
################################################ PRIVATE METHODS ################################################
|
684
728
|
private
|
685
729
|
def setPeerDisconnected(peer)
|
686
730
|
peer.state = :disconnected
|
@@ -708,7 +752,7 @@ module QuartzTorrent
|
|
708
752
|
return false if !torrentData
|
709
753
|
|
710
754
|
if msg.peerId == torrentData.trackerClient.peerId
|
711
|
-
@logger.info "We connected to ourself. Closing connection."
|
755
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: We connected to ourself. Closing connection."
|
712
756
|
peer.isUs = true
|
713
757
|
close
|
714
758
|
return
|
@@ -718,7 +762,7 @@ module QuartzTorrent
|
|
718
762
|
if peers
|
719
763
|
peers.each do |existingPeer|
|
720
764
|
if existingPeer.state == :connected
|
721
|
-
@logger.warn "Peer with id #{msg.peerId} created a new connection when we already have a connection in state #{existingPeer.state}. Closing new connection."
|
765
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Peer with id #{msg.peerId} created a new connection when we already have a connection in state #{existingPeer.state}. Closing new connection."
|
722
766
|
torrentData.peers.delete existingPeer
|
723
767
|
setPeerDisconnected(peer)
|
724
768
|
close
|
@@ -734,12 +778,12 @@ module QuartzTorrent
|
|
734
778
|
peer.bitfield = Bitfield.new(torrentData.info.pieces.length)
|
735
779
|
else
|
736
780
|
peer.bitfield = EmptyBitfield.new
|
737
|
-
@logger.info "We have no metainfo yet, so setting peer #{peer} to have an EmptyBitfield"
|
781
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: We have no metainfo yet, so setting peer #{peer} to have an EmptyBitfield"
|
738
782
|
end
|
739
783
|
|
740
784
|
# Send extended handshake if the peer supports extensions
|
741
785
|
if (msg.reserved.unpack("C8")[5] & 0x10) != 0
|
742
|
-
@logger.warn "Peer supports extensions. Sending extended handshake"
|
786
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Peer supports extensions. Sending extended handshake"
|
743
787
|
extended = Extension.createExtendedHandshake torrentData.info
|
744
788
|
extended.serializeTo currentIo
|
745
789
|
end
|
@@ -764,7 +808,7 @@ module QuartzTorrent
|
|
764
808
|
end
|
765
809
|
|
766
810
|
def updatePeerWithHandshakeInfo(torrentData, msg, peer)
|
767
|
-
@logger.info "peer #{peer} sent valid handshake for torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)}"
|
811
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: peer #{peer} sent valid handshake for torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)}"
|
768
812
|
peer.infoHash = msg.infoHash
|
769
813
|
# If this was a peer we got from a tracker that had no id then we only learn the id on handshake.
|
770
814
|
peer.trackerPeer.id = msg.peerId
|
@@ -788,7 +832,7 @@ module QuartzTorrent
|
|
788
832
|
return
|
789
833
|
end
|
790
834
|
|
791
|
-
return if torrentData.paused
|
835
|
+
return if torrentData.paused || torrentData.queued
|
792
836
|
|
793
837
|
trackerclient = torrentData.trackerClient
|
794
838
|
|
@@ -799,19 +843,19 @@ module QuartzTorrent
|
|
799
843
|
|
800
844
|
manager = torrentData.peerManager
|
801
845
|
if ! manager
|
802
|
-
@logger.error "Manage peers: peer manager client for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
|
846
|
+
@logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: Manage peers: peer manager client for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
|
803
847
|
return
|
804
848
|
end
|
805
849
|
|
806
850
|
toConnect = manager.manageConnections(classifiedPeers)
|
807
851
|
toConnect.each do |peer|
|
808
|
-
@logger.debug "Connecting to peer #{peer}"
|
852
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Connecting to peer #{peer}"
|
809
853
|
connect peer.trackerPeer.ip, peer.trackerPeer.port, peer
|
810
854
|
end
|
811
855
|
|
812
856
|
manageResult = manager.managePeers(classifiedPeers)
|
813
857
|
manageResult.unchoke.each do |peer|
|
814
|
-
@logger.debug "Unchoking peer #{peer}"
|
858
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Unchoking peer #{peer}"
|
815
859
|
withPeersIo(peer, "unchoking peer") do |io|
|
816
860
|
msg = Unchoke.new
|
817
861
|
sendMessageToPeer msg, io, peer
|
@@ -820,7 +864,7 @@ module QuartzTorrent
|
|
820
864
|
end
|
821
865
|
|
822
866
|
manageResult.choke.each do |peer|
|
823
|
-
@logger.debug "Choking peer #{peer}"
|
867
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Choking peer #{peer}"
|
824
868
|
withPeersIo(peer, "choking peer") do |io|
|
825
869
|
msg = Choke.new
|
826
870
|
sendMessageToPeer msg, io, peer
|
@@ -837,32 +881,44 @@ module QuartzTorrent
|
|
837
881
|
return
|
838
882
|
end
|
839
883
|
|
840
|
-
return if torrentData.paused
|
884
|
+
return if torrentData.paused || torrentData.queued
|
841
885
|
|
842
886
|
classifiedPeers = ClassifiedPeers.new torrentData.peers.all
|
843
887
|
|
844
888
|
if ! torrentData.blockState
|
845
|
-
@logger.error "Request blocks peers: no blockstate yet."
|
889
|
+
@logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: Request blocks peers: no blockstate yet."
|
846
890
|
return
|
847
891
|
end
|
848
892
|
|
849
|
-
if torrentData.state == :uploading &&
|
893
|
+
if torrentData.state == :uploading && !torrentData.paused
|
850
894
|
if torrentData.ratio
|
851
895
|
if torrentData.bytesUploadedDataOnly >= torrentData.ratio*torrentData.blockState.totalLength
|
852
|
-
@logger.info "Pausing torrent due to upload ratio limit." if torrentData.metainfoPieceState.complete?
|
896
|
+
@logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Pausing torrent due to upload ratio limit." if torrentData.metainfoPieceState.complete?
|
853
897
|
setPaused(infoHash, true)
|
854
898
|
return
|
855
899
|
end
|
856
900
|
end
|
857
901
|
if torrentData.uploadDuration && torrentData.downloadCompletedTime
|
858
902
|
if Time.new > torrentData.downloadCompletedTime + torrentData.uploadDuration
|
859
|
-
@logger.info "Pausing torrent due to upload duration being reached." if torrentData.metainfoPieceState.complete?
|
903
|
+
@logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Pausing torrent due to upload duration being reached." if torrentData.metainfoPieceState.complete?
|
860
904
|
setPaused(infoHash, true)
|
861
905
|
return
|
862
906
|
end
|
863
907
|
end
|
864
908
|
end
|
865
909
|
|
910
|
+
# Should we switch to endgame mode?
|
911
|
+
if torrentData.state == :running && !torrentData.isEndgame
|
912
|
+
blocks = torrentData.blockState.completeBlockBitfield
|
913
|
+
set = blocks.countSet
|
914
|
+
if set >= blocks.length - @endgameBlockThreshold && set < blocks.length
|
915
|
+
@logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Entering endgame mode: blocks #{set}/#{blocks.length} complete."
|
916
|
+
torrentData.isEndgame = true
|
917
|
+
end
|
918
|
+
elsif torrentData.isEndgame && torrentData.state != :running
|
919
|
+
torrentData.isEndgame = false
|
920
|
+
end
|
921
|
+
|
866
922
|
# Delete any timed-out requests.
|
867
923
|
classifiedPeers.establishedPeers.each do |peer|
|
868
924
|
toDelete = []
|
@@ -870,7 +926,7 @@ module QuartzTorrent
|
|
870
926
|
toDelete.push blockIndex if (Time.new - requestTime) > @requestTimeout
|
871
927
|
end
|
872
928
|
toDelete.each do |blockIndex|
|
873
|
-
@logger.debug "Block #{blockIndex} request timed out."
|
929
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Block #{blockIndex} request timed out."
|
874
930
|
blockInfo = torrentData.blockState.createBlockinfoByBlockIndex(blockIndex)
|
875
931
|
torrentData.blockState.setBlockRequested blockInfo, false
|
876
932
|
peer.requestedBlocks.delete blockIndex
|
@@ -895,32 +951,47 @@ module QuartzTorrent
|
|
895
951
|
# Request blocks
|
896
952
|
blockInfos = torrentData.blockState.findRequestableBlocks(classifiedPeers, 100)
|
897
953
|
blockInfos.each do |blockInfo|
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
954
|
+
|
955
|
+
peersToRequest = []
|
956
|
+
if torrentData.isEndgame
|
957
|
+
# Since we are in endgame mode, request blocks from all elegible peers
|
958
|
+
elegiblePeers = blockInfo.peers.find_all{ |p| p.requestedBlocks.length < p.maxRequestedBlocks }
|
959
|
+
|
960
|
+
peersToRequest.concat elegiblePeers
|
961
|
+
else
|
962
|
+
# Pick one of the peers that has the piece to download it from. Pick one of the
|
963
|
+
# peers with the top 3 upload rates.
|
964
|
+
elegiblePeers = blockInfo.peers.find_all{ |p| p.requestedBlocks.length < p.maxRequestedBlocks }.sort{ |a,b| b.uploadRate.value <=> a.uploadRate.value}
|
965
|
+
random = elegiblePeers[rand(blockInfo.peers.size)]
|
966
|
+
peer = elegiblePeers.first(3).push(random).shuffle.first
|
967
|
+
next if ! peer
|
968
|
+
peersToRequest.push peer
|
969
|
+
end
|
970
|
+
|
971
|
+
peersToRequest.each do |peer|
|
972
|
+
withPeersIo(peer, "requesting block") do |io|
|
973
|
+
if ! peer.amInterested
|
974
|
+
# Let this peer know that I'm interested if I haven't yet.
|
975
|
+
msg = Interested.new
|
976
|
+
sendMessageToPeer msg, io, peer
|
977
|
+
peer.amInterested = true
|
978
|
+
end
|
979
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Requesting block from #{peer}: piece #{blockInfo.pieceIndex} offset #{blockInfo.offset} length #{blockInfo.length}"
|
980
|
+
msg = blockInfo.getRequest
|
908
981
|
sendMessageToPeer msg, io, peer
|
909
|
-
|
982
|
+
torrentData.blockState.setBlockRequested blockInfo, true
|
983
|
+
peer.requestedBlocks[blockInfo.blockIndex] = Time.new
|
910
984
|
end
|
911
|
-
@logger.debug "Requesting block from #{peer}: piece #{blockInfo.pieceIndex} offset #{blockInfo.offset} length #{blockInfo.length}"
|
912
|
-
msg = blockInfo.getRequest
|
913
|
-
sendMessageToPeer msg, io, peer
|
914
|
-
torrentData.blockState.setBlockRequested blockInfo, true
|
915
|
-
peer.requestedBlocks[blockInfo.blockIndex] = Time.new
|
916
985
|
end
|
917
986
|
end
|
918
987
|
|
919
988
|
if blockInfos.size == 0
|
920
989
|
if torrentData.state != :uploading && torrentData.blockState.completePieceBitfield.allSet?
|
921
|
-
@logger.info "
|
990
|
+
@logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Download complete."
|
922
991
|
torrentData.state = :uploading
|
923
992
|
torrentData.downloadCompletedTime = Time.new
|
993
|
+
|
994
|
+
dequeue
|
924
995
|
end
|
925
996
|
end
|
926
997
|
|
@@ -935,13 +1006,13 @@ module QuartzTorrent
|
|
935
1006
|
return
|
936
1007
|
end
|
937
1008
|
|
938
|
-
return if torrentData.paused
|
1009
|
+
return if torrentData.paused || torrentData.queued
|
939
1010
|
|
940
1011
|
# We may not have completed the extended handshake with the peer which specifies the torrent size.
|
941
1012
|
# In this case torrentData.metainfoPieceState is not yet set.
|
942
1013
|
return if ! torrentData.metainfoPieceState
|
943
1014
|
|
944
|
-
@logger.info "Obtained all pieces of metainfo." if torrentData.metainfoPieceState.complete?
|
1015
|
+
@logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Obtained all pieces of metainfo." if torrentData.metainfoPieceState.complete?
|
945
1016
|
|
946
1017
|
pieces = torrentData.metainfoPieceState.findRequestablePieces
|
947
1018
|
classifiedPeers = ClassifiedPeers.new torrentData.peers.all
|
@@ -956,11 +1027,11 @@ module QuartzTorrent
|
|
956
1027
|
withPeersIo(peers.first, "requesting metadata piece") do |io|
|
957
1028
|
sendMessageToPeer msg, io, peers.first
|
958
1029
|
torrentData.metainfoPieceState.setPieceRequested(pieceIndex, true)
|
959
|
-
@logger.debug "Requesting metainfo piece from #{peers.first}: piece #{pieceIndex}"
|
1030
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Requesting metainfo piece from #{peers.first}: piece #{pieceIndex}"
|
960
1031
|
end
|
961
1032
|
end
|
962
1033
|
else
|
963
|
-
@logger.error "No peers found that have metadata."
|
1034
|
+
@logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: No peers found that have metadata."
|
964
1035
|
end
|
965
1036
|
|
966
1037
|
end
|
@@ -971,7 +1042,7 @@ module QuartzTorrent
|
|
971
1042
|
@logger.error "Check metadata piece manager results: data for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
|
972
1043
|
return
|
973
1044
|
end
|
974
|
-
|
1045
|
+
|
975
1046
|
# We may not have completed the extended handshake with the peer which specifies the torrent size.
|
976
1047
|
# In this case torrentData.metainfoPieceState is not yet set.
|
977
1048
|
return if ! torrentData.metainfoPieceState
|
@@ -980,7 +1051,7 @@ module QuartzTorrent
|
|
980
1051
|
results.each do |result|
|
981
1052
|
metaData = torrentData.pieceManagerMetainfoRequestMetadata.delete(result.requestId)
|
982
1053
|
if ! metaData
|
983
|
-
@logger.error "Can't find metadata for PieceManager request #{result.requestId}"
|
1054
|
+
@logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: Can't find metadata for PieceManager request #{result.requestId}"
|
984
1055
|
next
|
985
1056
|
end
|
986
1057
|
|
@@ -991,7 +1062,7 @@ module QuartzTorrent
|
|
991
1062
|
msg.piece = metaData.data.requestMsg.piece
|
992
1063
|
msg.data = result.data
|
993
1064
|
withPeersIo(metaData.data.peer, "sending extended metainfo piece message") do |io|
|
994
|
-
@logger.debug "Sending metainfo piece to #{metaData.data.peer}: piece #{msg.piece} with data length #{msg.data.length}"
|
1065
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Sending metainfo piece to #{metaData.data.peer}: piece #{msg.piece} with data length #{msg.data.length}"
|
995
1066
|
sendMessageToPeer msg, io, metaData.data.peer
|
996
1067
|
end
|
997
1068
|
result.data
|
@@ -999,7 +1070,7 @@ module QuartzTorrent
|
|
999
1070
|
end
|
1000
1071
|
|
1001
1072
|
if torrentData.metainfoPieceState.complete? && torrentData.state == :downloading_metainfo
|
1002
|
-
@logger.info "Obtained all pieces of metainfo. Will begin checking existing pieces."
|
1073
|
+
@logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Obtained all pieces of metainfo. Will begin checking existing pieces."
|
1003
1074
|
torrentData.metainfoPieceState.flush
|
1004
1075
|
# We don't need to download metainfo anymore.
|
1005
1076
|
cancelTimer torrentData.metainfoRequestTimer if torrentData.metainfoRequestTimer
|
@@ -1008,7 +1079,7 @@ module QuartzTorrent
|
|
1008
1079
|
torrentData.info = info
|
1009
1080
|
startCheckingPieces torrentData
|
1010
1081
|
else
|
1011
|
-
@logger.error "Metadata download is complete but reading the metadata failed"
|
1082
|
+
@logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: Metadata download is complete but reading the metadata failed"
|
1012
1083
|
torrentData.state = :error
|
1013
1084
|
end
|
1014
1085
|
end
|
@@ -1022,27 +1093,47 @@ module QuartzTorrent
|
|
1022
1093
|
end
|
1023
1094
|
|
1024
1095
|
if ! torrentData.blockState
|
1025
|
-
@logger.error "Receive piece: no blockstate yet."
|
1096
|
+
@logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Receive piece: no blockstate yet."
|
1026
1097
|
return
|
1027
1098
|
end
|
1028
1099
|
|
1029
1100
|
blockInfo = torrentData.blockState.createBlockinfoByPieceResponse(msg.pieceIndex, msg.blockOffset, msg.data.length)
|
1030
1101
|
|
1031
1102
|
if ! peer.requestedBlocks.has_key?(blockInfo.blockIndex)
|
1032
|
-
@logger.debug "Receive piece: we either didn't request this piece, or it was already received due to endgame strategy. Ignoring this message."
|
1103
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Receive piece: we either didn't request this piece, or it was already received due to endgame strategy. Ignoring this message."
|
1033
1104
|
return
|
1034
1105
|
end
|
1035
1106
|
|
1036
1107
|
if torrentData.blockState.blockCompleted?(blockInfo)
|
1037
|
-
@logger.debug "Receive piece: we already have this block. Ignoring this message."
|
1108
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Receive piece: we already have this block. Ignoring this message."
|
1038
1109
|
return
|
1039
1110
|
end
|
1040
1111
|
peer.requestedBlocks.delete blockInfo.blockIndex
|
1041
|
-
# Block is marked as not requested when hash is confirmed
|
1042
1112
|
|
1113
|
+
# Block is marked as not requested when hash is confirmed
|
1043
1114
|
torrentData.bytesDownloadedDataOnly += msg.data.length
|
1044
1115
|
id = torrentData.pieceManager.writeBlock(msg.pieceIndex, msg.blockOffset, msg.data)
|
1045
1116
|
torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:write, msg)
|
1117
|
+
|
1118
|
+
if torrentData.isEndgame
|
1119
|
+
# Assume this block is correct. Send a Cancel message to all other peers from whom we requested
|
1120
|
+
# this piece.
|
1121
|
+
classifiedPeers = ClassifiedPeers.new torrentData.peers.all
|
1122
|
+
classifiedPeers.requestablePeers.each do |otherPeer|
|
1123
|
+
if otherPeer.requestedBlocks.has_key?(blockInfo.blockIndex)
|
1124
|
+
withPeersIo(otherPeer, "when sending Cancel message") do |io|
|
1125
|
+
|
1126
|
+
cancel = Cancel.new
|
1127
|
+
cancel.pieceIndex = msg.pieceIndex
|
1128
|
+
cancel.blockOffset = msg.blockOffset
|
1129
|
+
cancel.blockLength = msg.data.length
|
1130
|
+
sendMessageToPeer cancel, io, otherPeer
|
1131
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending Cancel message to peer #{peer}"
|
1132
|
+
end
|
1133
|
+
end
|
1134
|
+
end
|
1135
|
+
end
|
1136
|
+
|
1046
1137
|
end
|
1047
1138
|
|
1048
1139
|
def handleRequest(msg, peer)
|
@@ -1057,7 +1148,7 @@ module QuartzTorrent
|
|
1057
1148
|
return
|
1058
1149
|
end
|
1059
1150
|
if msg.blockLength <= 0
|
1060
|
-
@logger.error "Request piece: peer requested block of length #{msg.blockLength} which is invalid."
|
1151
|
+
@logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Request piece: peer requested block of length #{msg.blockLength} which is invalid."
|
1061
1152
|
return
|
1062
1153
|
end
|
1063
1154
|
|
@@ -1076,11 +1167,11 @@ module QuartzTorrent
|
|
1076
1167
|
if torrentData.info
|
1077
1168
|
peer.bitfield.length = torrentData.info.pieces.length
|
1078
1169
|
else
|
1079
|
-
@logger.warn "A peer connected and sent a bitfield but we don't know the length of the torrent yet. Assuming number of pieces is divisible by 8"
|
1170
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: A peer connected and sent a bitfield but we don't know the length of the torrent yet. Assuming number of pieces is divisible by 8"
|
1080
1171
|
end
|
1081
1172
|
|
1082
1173
|
if ! torrentData.blockState
|
1083
|
-
@logger.warn "Bitfield: no blockstate yet."
|
1174
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Bitfield: no blockstate yet."
|
1084
1175
|
return
|
1085
1176
|
end
|
1086
1177
|
|
@@ -1089,7 +1180,7 @@ module QuartzTorrent
|
|
1089
1180
|
needed.intersection!(peer.bitfield)
|
1090
1181
|
if ! needed.allClear?
|
1091
1182
|
if ! peer.amInterested
|
1092
|
-
@logger.debug "Need some pieces from peer #{peer} so sending Interested message"
|
1183
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Need some pieces from peer #{peer} so sending Interested message"
|
1093
1184
|
msg = Interested.new
|
1094
1185
|
sendMessageToPeer msg, currentIo, peer
|
1095
1186
|
peer.amInterested = true
|
@@ -1105,7 +1196,7 @@ module QuartzTorrent
|
|
1105
1196
|
end
|
1106
1197
|
|
1107
1198
|
if msg.pieceIndex >= peer.bitfield.length
|
1108
|
-
@logger.warn "Peer #{peer} sent Have message with invalid piece index"
|
1199
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Peer #{peer} sent Have message with invalid piece index"
|
1109
1200
|
return
|
1110
1201
|
end
|
1111
1202
|
|
@@ -1113,13 +1204,13 @@ module QuartzTorrent
|
|
1113
1204
|
peer.bitfield.set msg.pieceIndex
|
1114
1205
|
|
1115
1206
|
if ! torrentData.blockState
|
1116
|
-
@logger.warn "Have: no blockstate yet."
|
1207
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Have: no blockstate yet."
|
1117
1208
|
return
|
1118
1209
|
end
|
1119
1210
|
|
1120
1211
|
# If we are interested in something from this peer, let them know.
|
1121
1212
|
if ! torrentData.blockState.completePieceBitfield.set?(msg.pieceIndex)
|
1122
|
-
@logger.debug "Peer #{peer} just got a piece we need so sending Interested message"
|
1213
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Peer #{peer} just got a piece we need so sending Interested message"
|
1123
1214
|
msg = Interested.new
|
1124
1215
|
sendMessageToPeer msg, currentIo, peer
|
1125
1216
|
peer.amInterested = true
|
@@ -1139,24 +1230,24 @@ module QuartzTorrent
|
|
1139
1230
|
|
1140
1231
|
metaData = torrentData.pieceManagerRequestMetadata.delete(result.requestId)
|
1141
1232
|
if ! metaData
|
1142
|
-
@logger.error "Can't find metadata for PieceManager request #{result.requestId}"
|
1233
|
+
@logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Can't find metadata for PieceManager request #{result.requestId}"
|
1143
1234
|
next
|
1144
1235
|
end
|
1145
1236
|
|
1146
1237
|
if metaData.type == :write
|
1147
1238
|
if result.successful?
|
1148
|
-
@logger.debug "Block written to disk. "
|
1239
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Block written to disk. "
|
1149
1240
|
# Block successfully written!
|
1150
1241
|
torrentData.blockState.setBlockCompleted metaData.data.pieceIndex, metaData.data.blockOffset, true do |pieceIndex|
|
1151
1242
|
# The peice is completed! Check hash.
|
1152
|
-
@logger.debug "Piece #{pieceIndex} is complete. Checking hash. "
|
1243
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Piece #{pieceIndex} is complete. Checking hash. "
|
1153
1244
|
id = torrentData.pieceManager.checkPieceHash(metaData.data.pieceIndex)
|
1154
1245
|
torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:hash, metaData.data.pieceIndex)
|
1155
1246
|
end
|
1156
1247
|
else
|
1157
1248
|
# Block failed! Clear completed and requested state.
|
1158
1249
|
torrentData.blockState.setBlockCompleted metaData.data.pieceIndex, metaData.data.blockOffset, false
|
1159
|
-
@logger.error "Writing block failed: #{result.error}"
|
1250
|
+
@logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Writing block failed: #{result.error}"
|
1160
1251
|
end
|
1161
1252
|
elsif metaData.type == :read
|
1162
1253
|
if result.successful?
|
@@ -1167,21 +1258,21 @@ module QuartzTorrent
|
|
1167
1258
|
msg.pieceIndex = readRequestMetadata.requestMsg.pieceIndex
|
1168
1259
|
msg.blockOffset = readRequestMetadata.requestMsg.blockOffset
|
1169
1260
|
msg.data = result.data
|
1170
|
-
@logger.debug "Sending block to #{peer}: piece #{msg.pieceIndex} offset #{msg.blockOffset} length #{msg.data.length}"
|
1261
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending block to #{peer}: piece #{msg.pieceIndex} offset #{msg.blockOffset} length #{msg.data.length}"
|
1171
1262
|
sendMessageToPeer msg, io, peer
|
1172
1263
|
torrentData.bytesUploadedDataOnly += msg.data.length
|
1173
|
-
@logger.debug "Sending piece to peer"
|
1264
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending piece to peer"
|
1174
1265
|
end
|
1175
1266
|
else
|
1176
|
-
@logger.error "Reading block failed: #{result.error}"
|
1267
|
+
@logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Reading block failed: #{result.error}"
|
1177
1268
|
end
|
1178
1269
|
elsif metaData.type == :hash
|
1179
1270
|
if result.successful?
|
1180
|
-
@logger.debug "Hash of piece #{metaData.data} is correct"
|
1271
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Hash of piece #{metaData.data} is correct"
|
1181
1272
|
sendHaves(torrentData, metaData.data)
|
1182
1273
|
sendUninterested(torrentData)
|
1183
1274
|
else
|
1184
|
-
@logger.info "Hash of piece #{metaData.data} is incorrect. Marking piece as not complete."
|
1275
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Hash of piece #{metaData.data} is incorrect. Marking piece as not complete."
|
1185
1276
|
torrentData.blockState.setPieceCompleted metaData.data, false
|
1186
1277
|
end
|
1187
1278
|
elsif metaData.type == :check_existing
|
@@ -1195,20 +1286,20 @@ module QuartzTorrent
|
|
1195
1286
|
def handleCheckExistingResult(torrentData, pieceManagerResult)
|
1196
1287
|
if pieceManagerResult.successful?
|
1197
1288
|
existingBitfield = pieceManagerResult.data
|
1198
|
-
@logger.info "We already have #{existingBitfield.countSet}/#{existingBitfield.length} pieces."
|
1289
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: We already have #{existingBitfield.countSet}/#{existingBitfield.length} pieces."
|
1199
1290
|
|
1200
1291
|
info = torrentData.info
|
1201
1292
|
|
1202
1293
|
torrentData.blockState = BlockState.new(info, existingBitfield)
|
1203
1294
|
|
1204
|
-
@logger.info "
|
1205
|
-
@logger.info "
|
1206
|
-
@logger.info "
|
1207
|
-
@logger.info "
|
1295
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Starting torrent. Information:"
|
1296
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: piece length: #{info.pieceLen}"
|
1297
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: number of pieces: #{info.pieces.size}"
|
1298
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: total length #{info.dataLength}"
|
1208
1299
|
|
1209
1300
|
startDownload torrentData
|
1210
1301
|
else
|
1211
|
-
@logger.info "
|
1302
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Checking existing pieces of torrent failed: #{pieceManagerResult.error}"
|
1212
1303
|
torrentData.state = :error
|
1213
1304
|
end
|
1214
1305
|
end
|
@@ -1221,7 +1312,7 @@ module QuartzTorrent
|
|
1221
1312
|
torrentData.pieceManager = QuartzTorrent::PieceManager.new(@baseDirectory, torrentData.info)
|
1222
1313
|
|
1223
1314
|
torrentData.state = :checking_pieces
|
1224
|
-
@logger.info "Checking pieces of torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)} asynchronously."
|
1315
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Checking pieces of torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)} asynchronously."
|
1225
1316
|
id = torrentData.pieceManager.findExistingPieces
|
1226
1317
|
torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:check_existing, nil)
|
1227
1318
|
|
@@ -1240,13 +1331,43 @@ module QuartzTorrent
|
|
1240
1331
|
end
|
1241
1332
|
end
|
1242
1333
|
|
1334
|
+
# Take a torrent that is in the :initializing state and make it go.
|
1335
|
+
def initTorrent(torrentData)
|
1336
|
+
# If we already have the metainfo info for this torrent, we can begin checking the pieces.
|
1337
|
+
# If we don't have the metainfo info then we need to get the metainfo first.
|
1338
|
+
if ! torrentData.info
|
1339
|
+
torrentData.info = MetainfoPieceState.downloaded(@baseDirectory, torrentData.infoHash)
|
1340
|
+
end
|
1341
|
+
|
1342
|
+
if torrentData.info
|
1343
|
+
startCheckingPieces torrentData
|
1344
|
+
else
|
1345
|
+
# Request the metainfo from peers.
|
1346
|
+
torrentData.state = :downloading_metainfo
|
1347
|
+
|
1348
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Downloading metainfo"
|
1349
|
+
|
1350
|
+
# Schedule peer connection management. Recurring and immediate
|
1351
|
+
torrentData.managePeersTimer =
|
1352
|
+
@reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], true, true)
|
1353
|
+
|
1354
|
+
# Schedule a timer for requesting metadata pieces from peers.
|
1355
|
+
torrentData.metainfoRequestTimer =
|
1356
|
+
@reactor.scheduleTimer(@requestBlocksPeriod, [:request_metadata_pieces, torrentData.infoHash], true, false)
|
1357
|
+
|
1358
|
+
# Schedule checking for metainfo PieceManager results (including when piece reading completes)
|
1359
|
+
torrentData.checkMetadataPieceManagerTimer =
|
1360
|
+
@reactor.scheduleTimer(@requestBlocksPeriod, [:check_metadata_piece_manager, torrentData.infoHash], true, false)
|
1361
|
+
end
|
1362
|
+
end
|
1363
|
+
|
1243
1364
|
# Start the actual torrent download. This method schedules the necessary timers and registers the necessary listeners
|
1244
1365
|
# and changes the state to :running. It is meant to be called after checking for existing pieces or downloading the
|
1245
1366
|
# torrent metadata (if this is a magnet link torrent)
|
1246
1367
|
def startDownload(torrentData)
|
1247
1368
|
# Add a listener for when the tracker's peers change.
|
1248
1369
|
torrentData.peerChangeListener = Proc.new do
|
1249
|
-
@logger.debug "
|
1370
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Managing peers on peer change event"
|
1250
1371
|
|
1251
1372
|
# Non-recurring and immediate timer
|
1252
1373
|
torrentData.managePeersTimer =
|
@@ -1277,7 +1398,7 @@ module QuartzTorrent
|
|
1277
1398
|
if metadataSize
|
1278
1399
|
# This peer knows the size of the metadata. If we haven't created our MetainfoPieceState yet, create it now.
|
1279
1400
|
if ! torrentData.metainfoPieceState
|
1280
|
-
@logger.info "Extended Handshake: Learned that metadata size is #{metadataSize}. Creating MetainfoPieceState"
|
1401
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Extended Handshake: Learned that metadata size is #{metadataSize}. Creating MetainfoPieceState"
|
1281
1402
|
torrentData.metainfoPieceState = MetainfoPieceState.new(@baseDirectory, torrentData.infoHash, metadataSize)
|
1282
1403
|
end
|
1283
1404
|
end
|
@@ -1292,10 +1413,10 @@ module QuartzTorrent
|
|
1292
1413
|
end
|
1293
1414
|
|
1294
1415
|
if msg.msgType == :request
|
1295
|
-
@logger.debug "Got extended metainfo request for piece #{msg.piece}"
|
1416
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Got extended metainfo request for piece #{msg.piece}"
|
1296
1417
|
# Build a response for this piece.
|
1297
1418
|
if torrentData.metainfoPieceState.pieceCompleted? msg.piece
|
1298
|
-
@logger.debug "Requesting extended metainfo piece #{msg.piece} from metainfoPieceState."
|
1419
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Requesting extended metainfo piece #{msg.piece} from metainfoPieceState."
|
1299
1420
|
id = torrentData.metainfoPieceState.readPiece msg.piece
|
1300
1421
|
torrentData.pieceManagerMetainfoRequestMetadata[id] =
|
1301
1422
|
PieceManagerRequestMetadata.new(:read, ReadRequestMetadata.new(peer,msg))
|
@@ -1304,19 +1425,19 @@ module QuartzTorrent
|
|
1304
1425
|
reject.msgType = :reject
|
1305
1426
|
reject.piece = msg.piece
|
1306
1427
|
withPeersIo(peer, "sending extended metainfo reject message") do |io|
|
1307
|
-
@logger.debug "Sending metainfo reject to #{peer}: piece #{msg.piece}"
|
1428
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending metainfo reject to #{peer}: piece #{msg.piece}"
|
1308
1429
|
sendMessageToPeer reject, io, peer
|
1309
1430
|
end
|
1310
1431
|
end
|
1311
1432
|
elsif msg.msgType == :piece
|
1312
|
-
@logger.debug "Got extended metainfo piece response for piece #{msg.piece} with data length #{msg.data.length}"
|
1433
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Got extended metainfo piece response for piece #{msg.piece} with data length #{msg.data.length}"
|
1313
1434
|
if ! torrentData.metainfoPieceState.pieceCompleted? msg.piece
|
1314
1435
|
id = torrentData.metainfoPieceState.savePiece msg.piece, msg.data
|
1315
1436
|
torrentData.pieceManagerMetainfoRequestMetadata[id] =
|
1316
1437
|
PieceManagerRequestMetadata.new(:write, msg)
|
1317
1438
|
end
|
1318
1439
|
elsif msg.msgType == :reject
|
1319
|
-
@logger.debug "Got extended metainfo reject response for piece #{msg.piece}"
|
1440
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Got extended metainfo reject response for piece #{msg.piece}"
|
1320
1441
|
# Mark this peer as bad.
|
1321
1442
|
torrentData.metainfoPieceState.markPeerBad peer
|
1322
1443
|
torrentData.metainfoPieceState.setPieceRequested(msg.piece, false)
|
@@ -1347,7 +1468,7 @@ module QuartzTorrent
|
|
1347
1468
|
end
|
1348
1469
|
|
1349
1470
|
def sendHaves(torrentData, pieceIndex)
|
1350
|
-
@logger.debug "Sending Have messages to all connected peers for piece #{pieceIndex}"
|
1471
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending Have messages to all connected peers for piece #{pieceIndex}"
|
1351
1472
|
torrentData.peers.all.each do |peer|
|
1352
1473
|
next if peer.state != :established || peer.isUs
|
1353
1474
|
withPeersIo(peer, "when sending Have message") do |io|
|
@@ -1373,7 +1494,7 @@ module QuartzTorrent
|
|
1373
1494
|
msg = Uninterested.new
|
1374
1495
|
sendMessageToPeer msg, io, peer
|
1375
1496
|
peer.amInterested = false
|
1376
|
-
@logger.debug "Sending Uninterested message to peer #{peer}"
|
1497
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending Uninterested message to peer #{peer}"
|
1377
1498
|
end
|
1378
1499
|
end
|
1379
1500
|
end
|
@@ -1387,7 +1508,7 @@ module QuartzTorrent
|
|
1387
1508
|
begin
|
1388
1509
|
peer.peerMsgSerializer.serializeTo(msg, io)
|
1389
1510
|
rescue
|
1390
|
-
@logger.warn "Sending message to peer #{peer} failed: #{$!.message}"
|
1511
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending message to peer #{peer} failed: #{$!.message}"
|
1391
1512
|
end
|
1392
1513
|
end
|
1393
1514
|
|
@@ -1427,7 +1548,7 @@ module QuartzTorrent
|
|
1427
1548
|
next if p.id && p.id == trackerclient.peerId
|
1428
1549
|
|
1429
1550
|
if ! torrentData.peers.findByAddr(p.ip, p.port)
|
1430
|
-
@logger.debug "Adding tracker peer #{p} to peers list"
|
1551
|
+
@logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Adding tracker peer #{p} to peers list"
|
1431
1552
|
break if ! addProc.call(p)
|
1432
1553
|
end
|
1433
1554
|
end
|
@@ -1440,13 +1561,13 @@ module QuartzTorrent
|
|
1440
1561
|
@logger.warn "Asked to remove a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
|
1441
1562
|
return
|
1442
1563
|
end
|
1443
|
-
@logger.info "
|
1564
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent. #{deleteFiles ? "Will" : "Wont"} delete downloaded files."
|
1444
1565
|
|
1445
|
-
@logger.info "Removing torrent: no torrentData.metainfoRequestTimer" if ! torrentData.metainfoRequestTimer
|
1446
|
-
@logger.info "Removing torrent: no torrentData.managePeersTimer" if ! torrentData.managePeersTimer
|
1447
|
-
@logger.info "Removing torrent: no torrentData.checkMetadataPieceManagerTimer" if ! torrentData.checkMetadataPieceManagerTimer
|
1448
|
-
@logger.info "Removing torrent: no torrentData.checkPieceManagerTimer" if ! torrentData.checkPieceManagerTimer
|
1449
|
-
@logger.info "Removing torrent: no torrentData.requestBlocksTimer" if ! torrentData.requestBlocksTimer
|
1566
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.metainfoRequestTimer" if ! torrentData.metainfoRequestTimer
|
1567
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.managePeersTimer" if ! torrentData.managePeersTimer
|
1568
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.checkMetadataPieceManagerTimer" if ! torrentData.checkMetadataPieceManagerTimer
|
1569
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.checkPieceManagerTimer" if ! torrentData.checkPieceManagerTimer
|
1570
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.requestBlocksTimer" if ! torrentData.requestBlocksTimer
|
1450
1571
|
|
1451
1572
|
|
1452
1573
|
# Stop all timers
|
@@ -1482,7 +1603,7 @@ module QuartzTorrent
|
|
1482
1603
|
begin
|
1483
1604
|
torrentData.metainfoPieceState.remove if torrentData.metainfoPieceState
|
1484
1605
|
rescue
|
1485
|
-
@logger.warn "Deleting metainfo file for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
|
1606
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Deleting metainfo file for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
|
1486
1607
|
end
|
1487
1608
|
|
1488
1609
|
if deleteFiles
|
@@ -1491,15 +1612,17 @@ module QuartzTorrent
|
|
1491
1612
|
path = @baseDirectory + File::SEPARATOR + torrentData.info.name
|
1492
1613
|
if File.exists? path
|
1493
1614
|
FileUtils.rm_r path
|
1494
|
-
@logger.info "Deleted #{path}"
|
1615
|
+
@logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Deleted #{path}"
|
1495
1616
|
else
|
1496
|
-
@logger.warn "Deleting '#{path}' for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
|
1617
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Deleting '#{path}' for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
|
1497
1618
|
end
|
1498
1619
|
rescue
|
1499
|
-
@logger.warn "
|
1620
|
+
@logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: When removing torrent, deleting '#{path}' failed because it doesn't exist"
|
1500
1621
|
end
|
1501
1622
|
end
|
1502
1623
|
end
|
1624
|
+
|
1625
|
+
dequeue
|
1503
1626
|
end
|
1504
1627
|
|
1505
1628
|
# Pause or unpause a torrent that we are downloading.
|
@@ -1512,9 +1635,59 @@ module QuartzTorrent
|
|
1512
1635
|
|
1513
1636
|
return if torrentData.paused == value
|
1514
1637
|
|
1515
|
-
|
1516
|
-
|
1638
|
+
torrentData.paused = value
|
1639
|
+
|
1640
|
+
if !value
|
1641
|
+
# On unpause, queue the torrent since there might not be room for it to run.
|
1642
|
+
# Make sure it goes to the head of the queue.
|
1643
|
+
queue(torrentData, :unshift)
|
1644
|
+
end
|
1645
|
+
|
1646
|
+
setFrozen infoHash, value if ! torrentData.queued
|
1647
|
+
|
1648
|
+
dequeue
|
1649
|
+
end
|
1650
|
+
|
1651
|
+
# Queue a torrent
|
1652
|
+
def queue(torrentData, mode = :queue)
|
1653
|
+
return if torrentData.queued
|
1654
|
+
|
1655
|
+
# Queue the torrent
|
1656
|
+
if mode == :unshift
|
1657
|
+
@torrentQueue.unshift torrentData
|
1658
|
+
else
|
1659
|
+
@torrentQueue.push torrentData
|
1660
|
+
end
|
1661
|
+
|
1662
|
+
setFrozen torrentData, true if ! torrentData.paused
|
1663
|
+
end
|
1664
|
+
|
1665
|
+
# Dequeue any torrents that can now run based on available space
|
1666
|
+
def dequeue
|
1667
|
+
torrents = @torrentQueue.dequeue(@torrentData.values)
|
1668
|
+
torrents.each do |torrentData|
|
1669
|
+
if torrentData.state == :initializing
|
1670
|
+
initTorrent torrentData
|
1671
|
+
else
|
1672
|
+
setFrozen torrentData, false if ! torrentData.paused
|
1673
|
+
end
|
1674
|
+
end
|
1675
|
+
end
|
1517
1676
|
|
1677
|
+
# Freeze or unfreeze a torrent. If value is true, then we disconnect from all peers for this torrent and forget
|
1678
|
+
# the peers. If value is false, we start reconnecting to peers.
|
1679
|
+
# Parameter torrent can be an infoHash or TorrentData
|
1680
|
+
def setFrozen(torrent, value)
|
1681
|
+
torrentData = torrent
|
1682
|
+
if ! torrent.is_a?(TorrentData)
|
1683
|
+
torrentData = @torrentData[torrent]
|
1684
|
+
if ! torrentData
|
1685
|
+
@logger.warn "Asked to freeze a non-existent torrent #{QuartzTorrent.bytesToHex(torrent)}"
|
1686
|
+
return
|
1687
|
+
end
|
1688
|
+
end
|
1689
|
+
|
1690
|
+
if value
|
1518
1691
|
# Disconnect from all peers so we won't reply to any messages.
|
1519
1692
|
torrentData.peers.all.each do |peer|
|
1520
1693
|
if peer.state != :disconnected
|
@@ -1525,16 +1698,21 @@ module QuartzTorrent
|
|
1525
1698
|
end
|
1526
1699
|
end
|
1527
1700
|
torrentData.peers.delete peer
|
1528
|
-
end
|
1701
|
+
end
|
1529
1702
|
else
|
1530
|
-
torrentData.paused = false
|
1531
|
-
|
1532
1703
|
# Get our list of peers and start connecting right away
|
1533
1704
|
# Non-recurring and immediate timer
|
1534
|
-
torrentData.managePeersTimer =
|
1705
|
+
torrentData.managePeersTimer =
|
1535
1706
|
@reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], false, true)
|
1536
1707
|
end
|
1537
1708
|
end
|
1709
|
+
|
1710
|
+
# Run the passed block in the Reactor's thread. This allows manipulation of torrent data without race
|
1711
|
+
# conditions. This method works by scheduling a non-recurring, immediate timer in the reactor that
|
1712
|
+
# on expiry runs the passed block.
|
1713
|
+
def runInReactorThread(&block)
|
1714
|
+
@reactor.scheduleTimer(0, [:runproc, block], false, true)
|
1715
|
+
end
|
1538
1716
|
|
1539
1717
|
end
|
1540
1718
|
|
@@ -1543,14 +1721,14 @@ module QuartzTorrent
|
|
1543
1721
|
class PeerClient
|
1544
1722
|
|
1545
1723
|
# Create a new PeerClient that will save and load torrent data under the specified baseDirectory.
|
1546
|
-
def initialize(baseDirectory)
|
1724
|
+
def initialize(baseDirectory, maxIncomplete = 5, maxActive = 10)
|
1547
1725
|
@port = 9998
|
1548
1726
|
@handler = nil
|
1549
1727
|
@stopped = true
|
1550
1728
|
@reactor = nil
|
1551
1729
|
@logger = LogManager.getLogger("peerclient")
|
1552
1730
|
@worker = nil
|
1553
|
-
@handler = PeerClientHandler.new baseDirectory
|
1731
|
+
@handler = PeerClientHandler.new baseDirectory, maxIncomplete, maxActive
|
1554
1732
|
@reactor = QuartzTorrent::Reactor.new(@handler, LogManager.getLogger("peerclient.reactor"))
|
1555
1733
|
@toStart = []
|
1556
1734
|
end
|
@@ -1559,8 +1737,10 @@ module QuartzTorrent
|
|
1559
1737
|
attr_accessor :port
|
1560
1738
|
|
1561
1739
|
# Start the PeerClient: open the listening port, and start a new thread to begin downloading/uploading pieces.
|
1740
|
+
# If listening fails, an exception of class Errno::EADDRINUSE is thrown.
|
1562
1741
|
def start
|
1563
1742
|
return if ! @stopped
|
1743
|
+
@logger.info "Starting"
|
1564
1744
|
|
1565
1745
|
@reactor.listen("0.0.0.0",@port,:listener_socket)
|
1566
1746
|
|
@@ -1602,7 +1782,7 @@ module QuartzTorrent
|
|
1602
1782
|
addTorrent(trackerclient, metainfo.infoHash, metainfo.info)
|
1603
1783
|
end
|
1604
1784
|
|
1605
|
-
# Add a new torrent to manage given an announceUrl and an infoHash.
|
1785
|
+
# Add a new torrent to manage given an announceUrl and an infoHash. The announceUrl may be a list.
|
1606
1786
|
# Returns the infoHash of the newly added torrent.
|
1607
1787
|
def addTorrentWithoutMetainfo(announceUrl, infoHash, magnet = nil)
|
1608
1788
|
raise "addTorrentWithoutMetainfo should be called with a Magnet object, not a #{magnet.class}" if magnet && ! magnet.is_a?(MagnetURI)
|
@@ -1616,7 +1796,7 @@ module QuartzTorrent
|
|
1616
1796
|
def addTorrentByMagnetURI(magnet)
|
1617
1797
|
raise "addTorrentByMagnetURI should be called with a MagnetURI object, not a #{magnet.class}" if ! magnet.is_a?(MagnetURI)
|
1618
1798
|
|
1619
|
-
trackerUrl = magnet.
|
1799
|
+
trackerUrl = magnet.trackers
|
1620
1800
|
raise "addTorrentByMagnetURI can't handle magnet links that don't have a tracker URL." if !trackerUrl
|
1621
1801
|
|
1622
1802
|
addTorrentWithoutMetainfo(trackerUrl, magnet.btInfoHash, magnet)
|
@@ -1660,6 +1840,22 @@ module QuartzTorrent
|
|
1660
1840
|
@handler.setUploadDuration(infoHash, seconds)
|
1661
1841
|
end
|
1662
1842
|
|
1843
|
+
# Adjust the bytesUploaded property of the specified torrent by the passed amount.
|
1844
|
+
# Adjustment should be an integer. It is added to the current bytesUploaded amount.
|
1845
|
+
def adjustBytesUploaded(infoHash, adjustment)
|
1846
|
+
return if ! adjustment
|
1847
|
+
raise "Bytes uploaded adjustment must be an Integer, not a #{adjustment.class}" if !adjustment.is_a?(Integer)
|
1848
|
+
@handler.adjustBytesUploaded(infoHash, adjustment)
|
1849
|
+
end
|
1850
|
+
|
1851
|
+
# Adjust the bytesDownloaded property of the specified torrent by the passed amount.
|
1852
|
+
# Adjustment should be an integer. It is added to the current bytesUploaded amount.
|
1853
|
+
def adjustBytesDownloaded(infoHash, adjustment)
|
1854
|
+
return if ! adjustment
|
1855
|
+
raise "Bytes downloaded adjustment must be an Integer, not a #{adjustment.class}" if !adjustment.is_a?(Integer)
|
1856
|
+
@handler.adjustBytesDownloaded(infoHash, adjustment)
|
1857
|
+
end
|
1858
|
+
|
1663
1859
|
# Remove a currently running torrent
|
1664
1860
|
def removeTorrent(infoHash, deleteFiles = false)
|
1665
1861
|
@handler.removeTorrent(infoHash, deleteFiles)
|