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.
@@ -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