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.
@@ -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/httptrackerclient"
5
- require "quartz_torrent/udptrackerclient"
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
- @announceUrl = announceUrl
151
+ @announceUrlList = announceUrl
128
152
  @infoHash = infoHash
129
153
  @peersChangedListeners = []
130
- @dynamicRequestParamsBuilder = Proc.new{ TrackerDynamicRequestParams.new(dataLength) }
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. This is a factory method that will return
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 = UdpTrackerClient.new(announceUrl, infoHash, dataLength)
236
+ result = UdpTrackerDriver.new(announceUrl, infoHash)
175
237
  else
176
- result = HttpTrackerClient.new(announceUrl, infoHash, dataLength)
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 new TrackerClient using the passed Metainfo object.
184
- def self.createFromMetainfo(metainfo, start = true)
185
- create(metainfo.announce, metainfo.infoHash, metainfo.info.dataLength, start)
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
- begin
202
- @logger.debug "Sending request"
203
- response = request(@event)
204
- @event = nil
205
- trackerInterval = response.interval
206
- rescue
207
- addError $!
208
- @logger.debug "Request failed due to exception: #{$!}"
209
- @logger.debug $!.backtrace.join("\n")
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.debug "Response was unsuccessful from tracker"
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
- request(:stopped)
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
@@ -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.1.1
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: 2013-11-19 00:00:00.000000000 Z
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/httptrackerclient.rb
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