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
@@ -1,8 +1,8 @@
|
|
1
1
|
require "quartz_torrent/log"
|
2
2
|
require "quartz_torrent/metainfo"
|
3
3
|
require "quartz_torrent/udptrackermsg"
|
4
|
-
require "quartz_torrent/
|
5
|
-
require "quartz_torrent/
|
4
|
+
require "quartz_torrent/httptrackerdriver"
|
5
|
+
require "quartz_torrent/udptrackerdriver"
|
6
6
|
require "quartz_torrent/interruptiblesleep"
|
7
7
|
require "quartz_torrent/util"
|
8
8
|
require "net/http"
|
@@ -67,6 +67,10 @@ module QuartzTorrent
|
|
67
67
|
else
|
68
68
|
@left = 0
|
69
69
|
end
|
70
|
+
@port = 6881
|
71
|
+
@peerId = "-QR0001-" # Azureus style
|
72
|
+
@peerId << Process.pid.to_s
|
73
|
+
@peerId = @peerId + "x" * (20-@peerId.length)
|
70
74
|
end
|
71
75
|
# Number of bytes uploaded
|
72
76
|
attr_accessor :uploaded
|
@@ -74,6 +78,8 @@ module QuartzTorrent
|
|
74
78
|
attr_accessor :downloaded
|
75
79
|
# Number of bytes left to download before torrent is completed
|
76
80
|
attr_accessor :left
|
81
|
+
attr_accessor :port
|
82
|
+
attr_accessor :peerId
|
77
83
|
end
|
78
84
|
|
79
85
|
# Represents the response from a tracker request
|
@@ -100,6 +106,24 @@ module QuartzTorrent
|
|
100
106
|
end
|
101
107
|
end
|
102
108
|
|
109
|
+
# Low-level interface to trackers. TrackerClient uses an instance of a subclass of this to talk to
|
110
|
+
# trackers using different protocols.
|
111
|
+
class TrackerDriver
|
112
|
+
def initialize(dataLength = 0)
|
113
|
+
@dynamicRequestParamsBuilder = Proc.new{ TrackerDynamicRequestParams.new(dataLength) }
|
114
|
+
end
|
115
|
+
|
116
|
+
# This should be set to a Proc that when called will return a TrackerDynamicRequestParams object
|
117
|
+
# with up-to-date information.
|
118
|
+
attr_accessor :dynamicRequestParamsBuilder
|
119
|
+
attr_accessor :port
|
120
|
+
attr_accessor :peerId
|
121
|
+
|
122
|
+
def request(event = nil)
|
123
|
+
raise "Implement me"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
103
127
|
# This class represents a connection to a tracker for a specific torrent. It can be used to get
|
104
128
|
# peers for that torrent.
|
105
129
|
class TrackerClient
|
@@ -124,10 +148,30 @@ module QuartzTorrent
|
|
124
148
|
@event = :started
|
125
149
|
@worker = nil
|
126
150
|
@logger = LogManager.getLogger("tracker_client")
|
127
|
-
@
|
151
|
+
@announceUrlList = announceUrl
|
128
152
|
@infoHash = infoHash
|
129
153
|
@peersChangedListeners = []
|
130
|
-
@dynamicRequestParamsBuilder = Proc.new
|
154
|
+
@dynamicRequestParamsBuilder = Proc.new do
|
155
|
+
result = TrackerDynamicRequestParams.new(dataLength)
|
156
|
+
result.port = @port
|
157
|
+
result.peerId = @peerId
|
158
|
+
result
|
159
|
+
end
|
160
|
+
@alarms = nil
|
161
|
+
|
162
|
+
# Convert announceUrl to an array
|
163
|
+
if @announceUrlList.nil?
|
164
|
+
@announceUrlList = []
|
165
|
+
elsif @announceUrlList.is_a? String
|
166
|
+
@announceUrlList = [@announceUrlList]
|
167
|
+
@announceUrlList.compact!
|
168
|
+
else
|
169
|
+
@announceUrlList = @announceUrlList.flatten.sort.uniq
|
170
|
+
end
|
171
|
+
|
172
|
+
raise "AnnounceURL contained no valid trackers" if @announceUrlList.size == 0
|
173
|
+
|
174
|
+
@announceUrlIndex = 0
|
131
175
|
end
|
132
176
|
|
133
177
|
attr_reader :peerId
|
@@ -136,6 +180,10 @@ module QuartzTorrent
|
|
136
180
|
# with up-to-date information.
|
137
181
|
attr_accessor :dynamicRequestParamsBuilder
|
138
182
|
|
183
|
+
# This member can be set to an Alarms object. If it is, this tracker will raise alarms
|
184
|
+
# when it doesn't get a response, and clear them when it does.
|
185
|
+
attr_accessor :alarms
|
186
|
+
|
139
187
|
# Return true if this TrackerClient is started, false otherwise.
|
140
188
|
def started?
|
141
189
|
@started
|
@@ -166,23 +214,35 @@ module QuartzTorrent
|
|
166
214
|
@errors
|
167
215
|
end
|
168
216
|
|
169
|
-
# Create a new TrackerClient using the passed information.
|
170
|
-
# a tracker that talks the protocol specified in the URL.
|
217
|
+
# Create a new TrackerClient using the passed information. The announceUrl may be a string or a list.
|
171
218
|
def self.create(announceUrl, infoHash, dataLength = 0, start = true)
|
219
|
+
result = TrackerClient.new(announceUrl, infoHash, dataLength)
|
220
|
+
result.start if start
|
221
|
+
result
|
222
|
+
end
|
223
|
+
|
224
|
+
# Create a new TrackerClient using the passed Metainfo object.
|
225
|
+
def self.createFromMetainfo(metainfo, start = true)
|
226
|
+
announce = []
|
227
|
+
announce.push metainfo.announce if metainfo.announce
|
228
|
+
announce = announce.concat(metainfo.announceList) if metainfo.announceList
|
229
|
+
create(announce, metainfo.infoHash, metainfo.info.dataLength, start)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Create a TrackerDriver for the specified URL. TrackerDriver is a lower-level interface to the tracker.
|
233
|
+
def self.createDriver(announceUrl, infoHash)
|
172
234
|
result = nil
|
173
235
|
if announceUrl =~ /udp:\/\//
|
174
|
-
result =
|
236
|
+
result = UdpTrackerDriver.new(announceUrl, infoHash)
|
175
237
|
else
|
176
|
-
result =
|
238
|
+
result = HttpTrackerDriver.new(announceUrl, infoHash)
|
177
239
|
end
|
178
|
-
result.start if start
|
179
|
-
|
180
240
|
result
|
181
241
|
end
|
182
242
|
|
183
|
-
# Create a
|
184
|
-
def self.
|
185
|
-
|
243
|
+
# Create a TrackerDriver using the passed Metainfo object. TrackerDriver is a lower-level interface to the tracker.
|
244
|
+
def self.createDriverFromMetainfo(metainfo)
|
245
|
+
TrackerClient.createDriver(metainfo.announce, metainfo.infoHash)
|
186
246
|
end
|
187
247
|
|
188
248
|
# Start the worker thread
|
@@ -198,21 +258,36 @@ module QuartzTorrent
|
|
198
258
|
while ! @stopped
|
199
259
|
begin
|
200
260
|
response = nil
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
261
|
+
|
262
|
+
driver = currentDriver
|
263
|
+
|
264
|
+
if driver
|
265
|
+
begin
|
266
|
+
@logger.debug "Sending request to tracker #{currentAnnounceUrl}"
|
267
|
+
response = driver.request(@event)
|
268
|
+
@event = nil
|
269
|
+
trackerInterval = response.interval
|
270
|
+
rescue
|
271
|
+
addError $!
|
272
|
+
@logger.info "Request failed due to exception: #{$!}"
|
273
|
+
@logger.debug $!.backtrace.join("\n")
|
274
|
+
changeToNextTracker
|
275
|
+
next
|
276
|
+
|
277
|
+
@alarms.raise Alarm.new(:tracker, "Tracker request failed: #{$!}") if @alarms
|
278
|
+
end
|
210
279
|
end
|
211
280
|
|
212
281
|
if response && response.successful?
|
282
|
+
@alarms.clear :tracker if @alarms
|
213
283
|
# Replace the list of peers
|
214
284
|
peersHash = {}
|
215
285
|
@logger.info "Response contained #{response.peers.length} peers"
|
286
|
+
|
287
|
+
if response.peers.length == 0
|
288
|
+
@alarms.raise Alarm.new(:tracker, "Response from tracker contained no peers") if @alarms
|
289
|
+
end
|
290
|
+
|
216
291
|
response.peers.each do |p|
|
217
292
|
peersHash[p] = 1
|
218
293
|
end
|
@@ -223,8 +298,11 @@ module QuartzTorrent
|
|
223
298
|
@peersChangedListeners.each{ |l| l.call }
|
224
299
|
end
|
225
300
|
else
|
226
|
-
@logger.
|
301
|
+
@logger.info "Response was unsuccessful from tracker: #{response.error}"
|
227
302
|
addError response.error if response
|
303
|
+
@alarms.raise Alarm.new(:tracker, "Unsuccessful response from tracker: #{response.error}") if @alarms && response
|
304
|
+
changeToNextTracker
|
305
|
+
next
|
228
306
|
end
|
229
307
|
|
230
308
|
# If we have no interval from the tracker yet, and the last request didn't error out leaving us with no peers,
|
@@ -244,7 +322,8 @@ module QuartzTorrent
|
|
244
322
|
@logger.info "Worker thread shutting down"
|
245
323
|
@logger.info "Sending final update to tracker"
|
246
324
|
begin
|
247
|
-
|
325
|
+
driver = currentDriver
|
326
|
+
driver.request(:stopped) if driver
|
248
327
|
rescue
|
249
328
|
addError $!
|
250
329
|
@logger.debug "Request failed due to exception: #{$!}"
|
@@ -280,6 +359,31 @@ module QuartzTorrent
|
|
280
359
|
def eventValid?(event)
|
281
360
|
event == :started || event == :stopped || event == :completed
|
282
361
|
end
|
362
|
+
|
363
|
+
def currentDriver
|
364
|
+
announceUrl = currentAnnounceUrl
|
365
|
+
return nil if ! announceUrl
|
366
|
+
|
367
|
+
driver = TrackerClient.createDriver announceUrl, @infoHash
|
368
|
+
driver.dynamicRequestParamsBuilder = @dynamicRequestParamsBuilder if driver
|
369
|
+
driver.port = @port
|
370
|
+
driver.peerId = @peerId
|
371
|
+
driver
|
372
|
+
end
|
373
|
+
|
374
|
+
def currentAnnounceUrl
|
375
|
+
return nil if @announceUrlList.size == 0
|
376
|
+
@announceUrlIndex = 0 if @announceUrlIndex > @announceUrlList.size-1
|
377
|
+
|
378
|
+
@announceUrlList[@announceUrlIndex]
|
379
|
+
end
|
380
|
+
|
381
|
+
def changeToNextTracker
|
382
|
+
@announceUrlIndex += 1
|
383
|
+
@logger.info "Changed to next tracker #{currentAnnounceUrl}"
|
384
|
+
sleep 0.5
|
385
|
+
end
|
386
|
+
|
283
387
|
end
|
284
388
|
|
285
389
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module QuartzTorrent
|
2
|
+
# A tracker driver that uses the UDP protocol as defined by http://xbtt.sourceforge.net/udp_tracker_protocol.html
|
3
|
+
class UdpTrackerDriver < TrackerDriver
|
4
|
+
# Set UDP receive length to a value that allows up to 100 peers to be returned in an announce.
|
5
|
+
ReceiveLength = 620
|
6
|
+
def initialize(announceUrl, infoHash, timeout = 2)
|
7
|
+
super()
|
8
|
+
@announceUrl = announceUrl
|
9
|
+
@infoHash = infoHash
|
10
|
+
@timeout = timeout
|
11
|
+
@logger = LogManager.getLogger("udp_tracker_client")
|
12
|
+
if @announceUrl =~ /udp:\/\/([^:]+):(\d+)/
|
13
|
+
@host = $1
|
14
|
+
@trackerPort = $2
|
15
|
+
else
|
16
|
+
throw "UDP Tracker announce URL is invalid: #{announceUrl}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def request(event = nil)
|
21
|
+
if event == :started
|
22
|
+
event = UdpTrackerMessage::EventStarted
|
23
|
+
elsif event == :stopped
|
24
|
+
event = UdpTrackerMessage::EventStopped
|
25
|
+
elsif event == :completed
|
26
|
+
event = UdpTrackerMessage::EventCompleted
|
27
|
+
else
|
28
|
+
event = UdpTrackerMessage::EventNone
|
29
|
+
end
|
30
|
+
|
31
|
+
socket = UDPSocket.new
|
32
|
+
result = nil
|
33
|
+
begin
|
34
|
+
socket.connect @host, @trackerPort
|
35
|
+
|
36
|
+
@logger.debug "Sending UDP tracker request to #{@host}:#{@trackerPort}"
|
37
|
+
|
38
|
+
# Send connect request
|
39
|
+
req = UdpTrackerConnectRequest.new
|
40
|
+
socket.send req.serialize, 0
|
41
|
+
resp = UdpTrackerConnectResponse.unserialize(readWithTimeout(socket,ReceiveLength,@timeout))
|
42
|
+
@logger.debug "Connect response: #{resp.inspect}"
|
43
|
+
raise "Invalid connect response: response transaction id is different from the request transaction id" if resp.transactionId != req.transactionId
|
44
|
+
connectionId = resp.connectionId
|
45
|
+
|
46
|
+
dynamicParams = @dynamicRequestParamsBuilder.call
|
47
|
+
|
48
|
+
# Send announce request
|
49
|
+
req = UdpTrackerAnnounceRequest.new(connectionId)
|
50
|
+
req.peerId = dynamicParams.peerId
|
51
|
+
req.infoHash = @infoHash
|
52
|
+
req.downloaded = dynamicParams.downloaded
|
53
|
+
req.left = dynamicParams.left
|
54
|
+
req.uploaded = dynamicParams.uploaded
|
55
|
+
req.event = event
|
56
|
+
#req.port = socket.addr[1]
|
57
|
+
req.port = dynamicParams.port
|
58
|
+
socket.send req.serialize, 0
|
59
|
+
resp = UdpTrackerAnnounceResponse.unserialize(readWithTimeout(socket,ReceiveLength,@timeout))
|
60
|
+
@logger.debug "Announce response: #{resp.inspect}"
|
61
|
+
|
62
|
+
peers = []
|
63
|
+
resp.ips.length.times do |i|
|
64
|
+
ip = resp.ips[i].unpack("CCCC").join('.')
|
65
|
+
port = resp.ports[i].unpack("n").first
|
66
|
+
peers.push TrackerPeer.new ip, port
|
67
|
+
end
|
68
|
+
peers
|
69
|
+
|
70
|
+
result = TrackerResponse.new(true, nil, peers)
|
71
|
+
result.interval = resp.interval if resp.interval
|
72
|
+
rescue
|
73
|
+
result = TrackerResponse.new(false, $!, [])
|
74
|
+
ensure
|
75
|
+
socket.close if ! socket.closed?
|
76
|
+
end
|
77
|
+
result
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
# Throws exception if timeout occurs
|
82
|
+
def readWithTimeout(socket, length, timeout)
|
83
|
+
rc = IO.select([socket], nil, nil, timeout)
|
84
|
+
if ! rc
|
85
|
+
raise "Waiting for response from UDP tracker #{@host}:#{@trackerPort} timed out after #{@timeout} seconds"
|
86
|
+
elsif rc[0].size > 0
|
87
|
+
socket.recvfrom(length)[0]
|
88
|
+
else
|
89
|
+
raise "Error receiving response from UDP tracker #{@host}:#{@trackerPort}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
end
|
data/lib/quartz_torrent/util.rb
CHANGED
@@ -71,7 +71,7 @@ module QuartzTorrent
|
|
71
71
|
|
72
72
|
# Log backtraces of all threads currently running. The threads are logged to the
|
73
73
|
# passed io, or if that's nil they are written to the logger named 'util' at error level.
|
74
|
-
def self.logBacktraces(io)
|
74
|
+
def self.logBacktraces(io = nil)
|
75
75
|
logger = nil
|
76
76
|
logger = LogManager.getLogger("util") if ! io
|
77
77
|
isLinux = RUBY_PLATFORM.downcase.include?("linux")
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quartz_torrent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2014-02-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bencode
|
@@ -135,15 +135,19 @@ extra_rdoc_files: []
|
|
135
135
|
files:
|
136
136
|
- lib/quartz_torrent.rb
|
137
137
|
- lib/quartz_torrent/blockstate.rb
|
138
|
+
- lib/quartz_torrent/httptrackerdriver.rb
|
138
139
|
- lib/quartz_torrent/memprofiler.rb
|
140
|
+
- lib/quartz_torrent/alarm.rb
|
139
141
|
- lib/quartz_torrent/regionmap.rb
|
140
|
-
- lib/quartz_torrent/
|
142
|
+
- lib/quartz_torrent/udptrackerdriver.rb
|
143
|
+
- lib/quartz_torrent/torrentqueue.rb
|
141
144
|
- lib/quartz_torrent/interruptiblesleep.rb
|
142
145
|
- lib/quartz_torrent/extension.rb
|
143
146
|
- lib/quartz_torrent/util.rb
|
144
147
|
- lib/quartz_torrent/log.rb
|
145
148
|
- lib/quartz_torrent/metainfo.rb
|
146
149
|
- lib/quartz_torrent/trackerclient.rb
|
150
|
+
- lib/quartz_torrent/timermanager.rb
|
147
151
|
- lib/quartz_torrent/peermsgserialization.rb
|
148
152
|
- lib/quartz_torrent/semaphore.rb
|
149
153
|
- lib/quartz_torrent/reactor.rb
|
@@ -162,7 +166,6 @@ files:
|
|
162
166
|
- lib/quartz_torrent/peermsg.rb
|
163
167
|
- lib/quartz_torrent/filemanager.rb
|
164
168
|
- lib/quartz_torrent/magnet.rb
|
165
|
-
- lib/quartz_torrent/udptrackerclient.rb
|
166
169
|
- README.md
|
167
170
|
- LICENSE
|
168
171
|
- .yardopts
|
@@ -1,70 +0,0 @@
|
|
1
|
-
module QuartzTorrent
|
2
|
-
# A tracker client that uses the UDP protocol as defined by http://xbtt.sourceforge.net/udp_tracker_protocol.html
|
3
|
-
class UdpTrackerClient < TrackerClient
|
4
|
-
# Set UDP receive length to a value that allows up to 100 peers to be returned in an announce.
|
5
|
-
ReceiveLength = 620
|
6
|
-
def initialize(announceUrl, infoHash, dataLength)
|
7
|
-
super(announceUrl, infoHash, dataLength)
|
8
|
-
@logger = LogManager.getLogger("udp_tracker_client")
|
9
|
-
if @announceUrl =~ /udp:\/\/([^:]+):(\d+)/
|
10
|
-
@host = $1
|
11
|
-
@trackerPort = $2
|
12
|
-
else
|
13
|
-
throw "UDP Tracker announce URL is invalid: #{announceUrl}"
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def request(event = nil)
|
18
|
-
if event == :started
|
19
|
-
event = UdpTrackerMessage::EventStarted
|
20
|
-
elsif event == :stopped
|
21
|
-
event = UdpTrackerMessage::EventStopped
|
22
|
-
elsif event == :completed
|
23
|
-
event = UdpTrackerMessage::EventCompleted
|
24
|
-
else
|
25
|
-
event = UdpTrackerMessage::EventNone
|
26
|
-
end
|
27
|
-
|
28
|
-
socket = UDPSocket.new
|
29
|
-
socket.connect @host, @trackerPort
|
30
|
-
|
31
|
-
@logger.debug "Sending UDP tracker request to #{@host}:#{@trackerPort}"
|
32
|
-
|
33
|
-
# Send connect request
|
34
|
-
req = UdpTrackerConnectRequest.new
|
35
|
-
socket.send req.serialize, 0
|
36
|
-
resp = UdpTrackerConnectResponse.unserialize(socket.recvfrom(ReceiveLength)[0])
|
37
|
-
raise "Invalid connect response: response transaction id is different from the request transaction id" if resp.transactionId != req.transactionId
|
38
|
-
connectionId = resp.connectionId
|
39
|
-
|
40
|
-
dynamicParams = @dynamicRequestParamsBuilder.call
|
41
|
-
|
42
|
-
# Send announce request
|
43
|
-
req = UdpTrackerAnnounceRequest.new(connectionId)
|
44
|
-
req.peerId = @peerId
|
45
|
-
req.infoHash = @infoHash
|
46
|
-
req.downloaded = dynamicParams.downloaded
|
47
|
-
req.left = dynamicParams.left
|
48
|
-
req.uploaded = dynamicParams.uploaded
|
49
|
-
req.event = event
|
50
|
-
#req.port = socket.addr[1]
|
51
|
-
req.port = @port
|
52
|
-
socket.send req.serialize, 0
|
53
|
-
resp = UdpTrackerAnnounceResponse.unserialize(socket.recvfrom(ReceiveLength)[0])
|
54
|
-
socket.close
|
55
|
-
|
56
|
-
peers = []
|
57
|
-
resp.ips.length.times do |i|
|
58
|
-
ip = resp.ips[i].unpack("CCCC").join('.')
|
59
|
-
port = resp.ports[i].unpack("n").first
|
60
|
-
peers.push TrackerPeer.new ip, port
|
61
|
-
end
|
62
|
-
peers
|
63
|
-
|
64
|
-
result = TrackerResponse.new(true, nil, peers)
|
65
|
-
result.interval = resp.interval if resp.interval
|
66
|
-
result
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
end
|