quartz_torrent 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/bin/quartztorrent_download +127 -0
  2. data/bin/quartztorrent_download_curses +841 -0
  3. data/bin/quartztorrent_magnet_from_torrent +32 -0
  4. data/bin/quartztorrent_show_info +62 -0
  5. data/lib/quartz_torrent.rb +2 -0
  6. data/lib/quartz_torrent/bitfield.rb +314 -0
  7. data/lib/quartz_torrent/blockstate.rb +354 -0
  8. data/lib/quartz_torrent/classifiedpeers.rb +95 -0
  9. data/lib/quartz_torrent/extension.rb +37 -0
  10. data/lib/quartz_torrent/filemanager.rb +543 -0
  11. data/lib/quartz_torrent/formatter.rb +92 -0
  12. data/lib/quartz_torrent/httptrackerclient.rb +121 -0
  13. data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
  14. data/lib/quartz_torrent/log.rb +132 -0
  15. data/lib/quartz_torrent/magnet.rb +92 -0
  16. data/lib/quartz_torrent/memprofiler.rb +27 -0
  17. data/lib/quartz_torrent/metainfo.rb +221 -0
  18. data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
  19. data/lib/quartz_torrent/peer.rb +145 -0
  20. data/lib/quartz_torrent/peerclient.rb +1627 -0
  21. data/lib/quartz_torrent/peerholder.rb +123 -0
  22. data/lib/quartz_torrent/peermanager.rb +170 -0
  23. data/lib/quartz_torrent/peermsg.rb +502 -0
  24. data/lib/quartz_torrent/peermsgserialization.rb +102 -0
  25. data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
  26. data/lib/quartz_torrent/rate.rb +58 -0
  27. data/lib/quartz_torrent/ratelimit.rb +48 -0
  28. data/lib/quartz_torrent/reactor.rb +949 -0
  29. data/lib/quartz_torrent/regionmap.rb +124 -0
  30. data/lib/quartz_torrent/semaphore.rb +43 -0
  31. data/lib/quartz_torrent/trackerclient.rb +271 -0
  32. data/lib/quartz_torrent/udptrackerclient.rb +70 -0
  33. data/lib/quartz_torrent/udptrackermsg.rb +250 -0
  34. data/lib/quartz_torrent/util.rb +100 -0
  35. metadata +195 -0
@@ -0,0 +1,145 @@
1
+ require 'quartz_torrent/rate'
2
+ require 'quartz_torrent/peermsgserialization'
3
+
4
+ module QuartzTorrent
5
+ class Peer
6
+ @@stateChangeListeners = []
7
+
8
+ def initialize(trackerPeer)
9
+ @trackerPeer = trackerPeer
10
+ @amChoked = true
11
+ @amInterested = false
12
+ @peerChoked = true
13
+ @peerInterested = false
14
+ @infoHash = nil
15
+ @state = :disconnected
16
+ @uploadRate = Rate.new
17
+ @downloadRate = Rate.new
18
+ @uploadRateDataOnly = Rate.new
19
+ @downloadRateDataOnly = Rate.new
20
+ @bitfield = nil
21
+ @firstEstablishTime = nil
22
+ @isUs = false
23
+ @requestedBlocks = {}
24
+ @requestedBlocksSizeLastPass = nil
25
+ @maxRequestedBlocks = 50
26
+ @peerMsgSerializer = PeerWireMessageSerializer.new
27
+ end
28
+
29
+ # A TrackerPeer class with the information about the peer retrieved from
30
+ # the tracker. When initially created the trackerPeer.id property may be null,
31
+ # but once the peer has connected it is set.
32
+ attr_accessor :trackerPeer
33
+
34
+ # Am I choked by this peer
35
+ attr_accessor :amChoked
36
+ # Am I interested in this peer
37
+ attr_accessor :amInterested
38
+
39
+ # This peer is choked by me
40
+ attr_accessor :peerChoked
41
+ # Is this peer interested
42
+ attr_accessor :peerInterested
43
+
44
+ # Info hash for the torrent of this peer
45
+ attr_accessor :infoHash
46
+
47
+ # Time when the peers connection was established the first time.
48
+ # This is nil when the peer has never had an established connection.
49
+ attr_accessor :firstEstablishTime
50
+
51
+ # Maximum number of outstanding block requests allowed for this peer.
52
+ attr_accessor :maxRequestedBlocks
53
+
54
+ # Peer connection state.
55
+ # All peers start of in :disconnected. When trying to handshake,
56
+ # they are in state :handshaking. Once handshaking is complete and we can
57
+ # send/accept requests, the state is :established.
58
+ attr_reader :state
59
+ def state=(state)
60
+ oldState = @state
61
+ @state = state
62
+ @@stateChangeListeners.each{ |l| l.call(self, oldState, @state) }
63
+ @firstEstablishTime = Time.new if @state == :established && ! @firstEstablishTime
64
+ end
65
+
66
+ # Is this peer ourself? Used to tell if we connected to ourself.
67
+ attr_accessor :isUs
68
+
69
+ # Upload rate of peer to us.
70
+ attr_accessor :uploadRate
71
+ # Download rate of us to peer.
72
+ attr_accessor :downloadRate
73
+
74
+ # Upload rate of peer to us, only counting actual torrent data
75
+ attr_accessor :uploadRateDataOnly
76
+ # Download rate of us to peer, only counting actual torrent data
77
+ attr_accessor :downloadRateDataOnly
78
+
79
+ # A Bitfield representing the pieces that the peer has.
80
+ attr_accessor :bitfield
81
+
82
+ # A hash of the block indexes of the outstanding blocks requested from this peer
83
+ attr_accessor :requestedBlocks
84
+ attr_accessor :requestedBlocksSizeLastPass
85
+
86
+ # A PeerWireMessageSerializer that can unserialize and serialize messages to and from this peer.
87
+ attr_accessor :peerMsgSerializer
88
+
89
+ def to_s
90
+ @trackerPeer.to_s
91
+ end
92
+
93
+ # Add a proc to the list of state change listeners.
94
+ def self.addStateChangeListener(l)
95
+ @@stateChangeListeners.push l
96
+ end
97
+
98
+ def eql?(o)
99
+ o.is_a?(Peer) && trackerPeer.eql?(o.trackerPeer)
100
+ end
101
+
102
+ # Update the upload rate of the peer from the passed PeerWireMessage.
103
+ def updateUploadRate(msg)
104
+ @uploadRate.update msg.length
105
+ if msg.is_a? Piece
106
+ @uploadRateDataOnly.update msg.data.length
107
+ end
108
+ end
109
+
110
+ # Update the download rate of the peer from the passed PeerWireMessage.
111
+ def updateDownloadRate(msg)
112
+ @downloadRate.update msg.length
113
+ if msg.is_a? Piece
114
+ @downloadRateDataOnly.update msg.data.length
115
+ end
116
+ end
117
+
118
+ # Create a clone of this peer. This method does not clone listeners.
119
+ def clone
120
+ peer = Peer.new(@trackerPeer)
121
+ peer.amChoked = amChoked
122
+ peer.amInterested = amInterested
123
+ peer.peerChoked = peerChoked
124
+ peer.peerInterested = peerInterested
125
+ peer.firstEstablishTime = firstEstablishTime
126
+ peer.state = state
127
+ peer.isUs = isUs
128
+ # Take the values of the rates. This is so that if the caller doesn't read the rates
129
+ # in a timely fashion, they don't decay.
130
+ peer.uploadRate = uploadRate.value
131
+ peer.downloadRate = downloadRate.value
132
+ peer.uploadRateDataOnly = uploadRateDataOnly.value
133
+ peer.downloadRateDataOnly = downloadRateDataOnly.value
134
+ if bitfield
135
+ peer.bitfield = Bitfield.new(bitfield.length)
136
+ peer.bitfield.copyFrom bitfield
137
+ end
138
+ peer.requestedBlocks = requestedBlocks.clone
139
+ peer.maxRequestedBlocks = maxRequestedBlocks
140
+ peer.peerMsgSerializer = peerMsgSerializer
141
+
142
+ peer
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,1627 @@
1
+ require "quartz_torrent/log.rb"
2
+ require "quartz_torrent/trackerclient.rb"
3
+ require "quartz_torrent/peermsg.rb"
4
+ require "quartz_torrent/reactor.rb"
5
+ require "quartz_torrent/util.rb"
6
+ require "quartz_torrent/classifiedpeers.rb"
7
+ require "quartz_torrent/classifiedpeers.rb"
8
+ require "quartz_torrent/peerholder.rb"
9
+ require "quartz_torrent/peermanager.rb"
10
+ require "quartz_torrent/blockstate.rb"
11
+ require "quartz_torrent/filemanager.rb"
12
+ require "quartz_torrent/semaphore.rb"
13
+ require "quartz_torrent/piecemanagerrequestmetadata.rb"
14
+ require "quartz_torrent/metainfopiecestate.rb"
15
+ require "quartz_torrent/extension.rb"
16
+ require "quartz_torrent/magnet.rb"
17
+
18
+
19
+ module QuartzTorrent
20
+
21
+ # Extra metadata stored in a PieceManagerRequestMetadata specific to read requests.
22
+ class ReadRequestMetadata
23
+ def initialize(peer, requestMsg)
24
+ @peer = peer
25
+ @requestMsg = requestMsg
26
+ end
27
+ attr_accessor :peer
28
+ attr_accessor :requestMsg
29
+ end
30
+
31
+ # Class used by PeerClientHandler to keep track of information associated with a single torrent
32
+ # being downloaded/uploaded.
33
+ class TorrentData
34
+ def initialize(infoHash, info, trackerClient)
35
+ @infoHash = infoHash
36
+ @info = info
37
+ @trackerClient = trackerClient
38
+ @peerManager = PeerManager.new
39
+ @pieceManagerRequestMetadata = {}
40
+ @pieceManagerMetainfoRequestMetadata = {}
41
+ @bytesDownloaded = 0
42
+ @bytesUploaded = 0
43
+ @magnet = nil
44
+ @peers = PeerHolder.new
45
+ @state = :initializing
46
+ @blockState = nil
47
+ @metainfoPieceState = nil
48
+ @metainfoRequestTimer = nil
49
+ @managePeersTimer = nil
50
+ @checkMetadataPieceManagerTimer = nil
51
+ @checkPieceManagerTimer = nil
52
+ @requestBlocksTimer = nil
53
+ @paused = false
54
+ @downRateLimit = nil
55
+ @upRateLimit = nil
56
+ @ratio = nil
57
+ end
58
+ # The torrents Metainfo.Info struct. This is nil if the torrent has no metadata and we need to download it
59
+ # (i.e. a magnet link)
60
+ attr_accessor :info
61
+ # The infoHash of the torrent
62
+ attr_accessor :infoHash
63
+ attr_accessor :trackerClient
64
+ attr_accessor :peers
65
+ # The MagnetURI object, if this torrent was created from a magnet link. Nil for torrents not created from magnets.
66
+ attr_accessor :magnet
67
+ attr_accessor :peerManager
68
+ attr_accessor :blockState
69
+ attr_accessor :pieceManager
70
+ # Metadata associated with outstanding requests to the PieceManager responsible for the pieces of the torrent data.
71
+ attr_accessor :pieceManagerRequestMetadata
72
+ # Metadata associated with outstanding requests to the PieceManager responsible for the pieces of the torrent metainfo.
73
+ attr_accessor :pieceManagerMetainfoRequestMetadata
74
+ attr_accessor :peerChangeListener
75
+ attr_accessor :bytesDownloaded
76
+ attr_accessor :bytesUploaded
77
+ attr_accessor :state
78
+ attr_accessor :metainfoPieceState
79
+ # The timer handle for the timer that requests metainfo pieces. This is used to cancel the
80
+ # timer when the metadata is completely downloaded.
81
+ attr_accessor :metainfoRequestTimer
82
+ # Timer handle for timer that manages peers.
83
+ attr_accessor :managePeersTimer
84
+ # Timer handle for timer that checks metadata piece manager results
85
+ attr_accessor :checkMetadataPieceManagerTimer
86
+ # Timer handle for timer that checks piece manager results
87
+ attr_accessor :checkPieceManagerTimer
88
+ # Timer handle for timer that requests blocks
89
+ attr_accessor :requestBlocksTimer
90
+
91
+ attr_accessor :paused
92
+ # The RateLimit for downloading this torrent.
93
+ attr_accessor :downRateLimit
94
+ # The RateLimit for uploading to peers for this torrent.
95
+ attr_accessor :upRateLimit
96
+ # After we have completed downloading a torrent, we will continue to upload until we have
97
+ # uploaded ratio * torrent_size bytes. If nil, no limit on upload.
98
+ attr_accessor :ratio
99
+ end
100
+
101
+ # Data about torrents for use by the end user.
102
+ class TorrentDataDelegate
103
+ # Create a new TorrentDataDelegate. This is meant to only be called internally.
104
+ def initialize(torrentData, peerClientHandler)
105
+ fillFrom(torrentData)
106
+ @torrentData = torrentData
107
+ @peerClientHandler = peerClientHandler
108
+ end
109
+
110
+ # Torrent Metainfo.info struct. This is nil if the torrent has no metadata and we haven't downloaded it yet
111
+ # (i.e. a magnet link).
112
+ attr_accessor :info
113
+ # Infohash of the torrent. This is binary data.
114
+ attr_accessor :infoHash
115
+ # Recommended display name for this torrent.
116
+ attr_accessor :recommendedName
117
+ # Download rate in bytes/second
118
+ attr_reader :downloadRate
119
+ # Upload rate in bytes/second
120
+ attr_reader :uploadRate
121
+ # Download rate limit in bytes/second if a limit is set, nil otherwise
122
+ attr_reader :downloadRateLimit
123
+ # Upload rate limit in bytes/second if a limit is set, nil otherwise
124
+ attr_reader :uploadRateLimit
125
+ # Download rate limit in bytes/second if a limit is set, nil otherwise
126
+ attr_reader :downloadRateDataOnly
127
+ attr_reader :uploadRateDataOnly
128
+ # Count of completed bytes of the torrent
129
+ attr_reader :completedBytes
130
+ # Array of peers for the torrent. These include connected, disconnected, and handshaking peers
131
+ attr_reader :peers
132
+ # State of the torrent. This may be one of :downloading_metainfo, :error, :checking_pieces, :running, :downloading_metainfo, or :deleted.
133
+ # The :deleted state indicates that the torrent that this TorrentDataDelegate refers to is no longer being managed by the peer client.
134
+ attr_reader :state
135
+ # Bitfield representing which pieces of the torrent are completed.
136
+ attr_reader :completePieceBitfield
137
+ # Length of metainfo info in bytes. This is only set when the state is :downloading_metainfo
138
+ attr_reader :metainfoLength
139
+ # How much of the metainfo info we have downloaded in bytes. This is only set when the state is :downloading_metainfo
140
+ attr_reader :metainfoCompletedLength
141
+ # Whether or not the torrent is paused.
142
+ attr_reader :paused
143
+ # After we have completed downloading a torrent, we will continue to upload until we have
144
+ # uploaded ratio * torrent_size bytes. If nil, no limit on upload.
145
+ attr_accessor :ratio
146
+
147
+ # Update the data in this TorrentDataDelegate from the torrentData
148
+ # object that it was created from. TODO: What if that torrentData is now gone?
149
+ def refresh
150
+ @peerClientHandler.updateDelegateTorrentData self
151
+ end
152
+
153
+ # Set the fields of this TorrentDataDelegate from the passed torrentData.
154
+ # This is meant to only be called internally.
155
+ def internalRefresh
156
+ fillFrom(@torrentData)
157
+ end
158
+
159
+ private
160
+ def fillFrom(torrentData)
161
+ @infoHash = torrentData.infoHash
162
+ @info = torrentData.info
163
+ @bytesUploaded = torrentData.bytesUploaded
164
+ @bytesDownloaded = torrentData.bytesDownloaded
165
+
166
+ if torrentData.state == :checking_pieces
167
+ # When checking pieces there is only one request pending with the piece manager.
168
+ checkExistingRequestId = torrentData.pieceManagerRequestMetadata.keys.first
169
+ progress = torrentData.pieceManager.progress checkExistingRequestId
170
+ @completedBytes = progress ? progress * torrentData.info.dataLength / 100 : 0
171
+ else
172
+ @completedBytes = torrentData.blockState.nil? ? 0 : torrentData.blockState.completedLength
173
+ end
174
+
175
+ # This should really be a copy:
176
+ @completePieceBitfield = torrentData.blockState.nil? ? nil : torrentData.blockState.completePieceBitfield
177
+ buildPeersList(torrentData)
178
+ @downloadRate = @peers.reduce(0){ |memo, peer| memo + peer.uploadRate }
179
+ @uploadRate = @peers.reduce(0){ |memo, peer| memo + peer.downloadRate }
180
+ @downloadRateDataOnly = @peers.reduce(0){ |memo, peer| memo + peer.uploadRateDataOnly }
181
+ @uploadRateDataOnly = @peers.reduce(0){ |memo, peer| memo + peer.downloadRateDataOnly }
182
+ @state = torrentData.state
183
+ @metainfoLength = nil
184
+ @paused = torrentData.paused
185
+ @metainfoCompletedLength = nil
186
+ if torrentData.metainfoPieceState && torrentData.state == :downloading_metainfo
187
+ @metainfoLength = torrentData.metainfoPieceState.metainfoLength
188
+ @metainfoCompletedLength = torrentData.metainfoPieceState.metainfoCompletedLength
189
+ end
190
+
191
+ if torrentData.info
192
+ @recommendedName = torrentData.info.name
193
+ else
194
+ if torrentData.magnet
195
+ @recommendedName = torrentData.magnet.displayName
196
+ else
197
+ @recommendedName = nil
198
+ end
199
+ end
200
+
201
+ @downloadRateLimit = torrentData.downRateLimit.unitsPerSecond if torrentData.downRateLimit
202
+ @uploadRateLimit = torrentData.upRateLimit.unitsPerSecond if torrentData.upRateLimit
203
+ @ratio = torrentData.ratio
204
+ end
205
+
206
+ def buildPeersList(torrentData)
207
+ @peers = []
208
+ torrentData.peers.all.each do |peer|
209
+ @peers.push peer.clone
210
+ end
211
+ end
212
+
213
+ end
214
+
215
+ # This class implements a Reactor Handler object. This Handler implements the PeerClient.
216
+ class PeerClientHandler < QuartzTorrent::Handler
217
+
218
+ def initialize(baseDirectory)
219
+ # Hash of TorrentData objects, keyed by torrent infoHash
220
+ @torrentData = {}
221
+
222
+ @baseDirectory = baseDirectory
223
+
224
+ @logger = LogManager.getLogger("peerclient")
225
+
226
+ # Overall maximum number of peers (connected + disconnected)
227
+ @maxPeerCount = 120
228
+ # Number of peers we ideally want to try and be downloading/uploading with
229
+ @targetActivePeerCount = 50
230
+ @targetUnchokedPeerCount = 4
231
+ @managePeersPeriod = 10 # Defined in bittorrent spec. Only unchoke peers every 10 seconds.
232
+ @requestBlocksPeriod = 1
233
+ @handshakeTimeout = 1
234
+ @requestTimeout = 60
235
+ end
236
+
237
+ attr_reader :torrentData
238
+
239
+ # Add a new tracker client. This effectively adds a new torrent to download. Returns the TorrentData object for the
240
+ # new torrent.
241
+ def addTrackerClient(infoHash, info, trackerclient)
242
+ raise "There is already a tracker registered for torrent #{QuartzTorrent.bytesToHex(infoHash)}" if @torrentData.has_key? infoHash
243
+ torrentData = TorrentData.new(infoHash, info, trackerclient)
244
+ @torrentData[infoHash] = torrentData
245
+
246
+ # If we already have the metainfo info for this torrent, we can begin checking the pieces.
247
+ # If we don't have the metainfo info then we need to get the metainfo first.
248
+ if ! info
249
+ info = MetainfoPieceState.downloaded(@baseDirectory, torrentData.infoHash)
250
+ torrentData.info = info
251
+ end
252
+
253
+ if info
254
+ startCheckingPieces torrentData
255
+ else
256
+ # Request the metainfo from peers.
257
+ torrentData.state = :downloading_metainfo
258
+
259
+ @logger.info "Downloading metainfo"
260
+ #torrentData.metainfoPieceState = MetainfoPieceState.new(@baseDirectory, infoHash, )
261
+
262
+ # Schedule peer connection management. Recurring and immediate
263
+ torrentData.managePeersTimer =
264
+ @reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], true, true)
265
+
266
+ # Schedule a timer for requesting metadata pieces from peers.
267
+ torrentData.metainfoRequestTimer =
268
+ @reactor.scheduleTimer(@requestBlocksPeriod, [:request_metadata_pieces, infoHash], true, false)
269
+
270
+ # Schedule checking for metainfo PieceManager results (including when piece reading completes)
271
+ torrentData.checkMetadataPieceManagerTimer =
272
+ @reactor.scheduleTimer(@requestBlocksPeriod, [:check_metadata_piece_manager, infoHash], true, false)
273
+ end
274
+
275
+ torrentData
276
+ end
277
+
278
+ # Remove a torrent.
279
+ def removeTorrent(infoHash, deleteFiles = false)
280
+ # Can't do this right now, since it could be in use by an event handler. Use an immediate, non-recurring timer instead.
281
+ @logger.info "Scheduling immediate timer to remove torrent #{QuartzTorrent.bytesToHex(infoHash)}. #{deleteFiles ? "Will" : "Wont"} delete downloaded files."
282
+ @reactor.scheduleTimer(0, [:removetorrent, infoHash, deleteFiles], false, true)
283
+ end
284
+
285
+ # Pause or unpause the specified torrent.
286
+ def setPaused(infoHash, value)
287
+ # Can't do this right now, since it could be in use by an event handler. Use an immediate, non-recurring timer instead.
288
+ @logger.info "Scheduling immediate timer to pause torrent #{QuartzTorrent.bytesToHex(infoHash)}."
289
+ @reactor.scheduleTimer(0, [:pausetorrent, infoHash, value], false, true)
290
+ end
291
+
292
+ # Set the download rate limit. Pass nil as the bytesPerSecond to disable the limit.
293
+ def setDownloadRateLimit(infoHash, bytesPerSecond)
294
+ torrentData = @torrentData[infoHash]
295
+ if ! torrentData
296
+ @logger.warn "Asked to set download rate limit for a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
297
+ return
298
+ end
299
+
300
+ if bytesPerSecond
301
+ if ! torrentData.downRateLimit
302
+ torrentData.downRateLimit = RateLimit.new(bytesPerSecond, 2*bytesPerSecond, 0)
303
+ else
304
+ torrentData.downRateLimit.unitsPerSecond = bytesPerSecond
305
+ end
306
+ else
307
+ torrentData.downRateLimit = nil
308
+ end
309
+ end
310
+
311
+ # Set the upload rate limit. Pass nil as the bytesPerSecond to disable the limit.
312
+ def setUploadRateLimit(infoHash, bytesPerSecond)
313
+ torrentData = @torrentData[infoHash]
314
+ if ! torrentData
315
+ @logger.warn "Asked to set upload rate limit for a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
316
+ return
317
+ end
318
+
319
+ if bytesPerSecond
320
+ if ! torrentData.upRateLimit
321
+ torrentData.upRateLimit = RateLimit.new(bytesPerSecond, 2*bytesPerSecond, 0)
322
+ else
323
+ torrentData.upRateLimit.unitsPerSecond = bytesPerSecond
324
+ end
325
+ else
326
+ torrentData.upRateLimit = nil
327
+ end
328
+ end
329
+
330
+ # Set the upload ratio. Pass nil to disable
331
+ def setUploadRatio(infoHash, ratio)
332
+ torrentData = @torrentData[infoHash]
333
+ if ! torrentData
334
+ @logger.warn "Asked to set upload ratio limit for a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
335
+ return
336
+ end
337
+
338
+ torrentData.ratio = ratio
339
+ end
340
+
341
+ # Reactor method called when a peer has connected to us.
342
+ def serverInit(metadata, addr, port)
343
+ # A peer connected to us
344
+ # Read handshake message
345
+ @logger.warn "Peer connection from #{addr}:#{port}"
346
+ begin
347
+ msg = PeerHandshake.unserializeExceptPeerIdFrom currentIo
348
+ rescue
349
+ @logger.warn "Peer failed handshake: #{$!}"
350
+ close
351
+ return
352
+ end
353
+
354
+ torrentData = torrentDataForHandshake(msg, "#{addr}:#{port}")
355
+ # Are we tracking this torrent?
356
+ if !torrentData
357
+ @logger.warn "Peer sent handshake for unknown torrent"
358
+ close
359
+ return
360
+ end
361
+ trackerclient = torrentData.trackerClient
362
+
363
+ # If we already have too many connections, don't allow this connection.
364
+ classifiedPeers = ClassifiedPeers.new torrentData.peers.all
365
+ if classifiedPeers.establishedPeers.length > @targetActivePeerCount
366
+ @logger.warn "Closing connection to peer from #{addr}:#{port} because we already have #{classifiedPeers.establishedPeers.length} active peers which is > the target count of #{@targetActivePeerCount} "
367
+ close
368
+ return
369
+ end
370
+
371
+ # Send handshake
372
+ outgoing = PeerHandshake.new
373
+ outgoing.peerId = trackerclient.peerId
374
+ outgoing.infoHash = torrentData.infoHash
375
+ outgoing.serializeTo currentIo
376
+
377
+ # Send extended handshake if the peer supports extensions
378
+ if (msg.reserved.unpack("C8")[5] & 0x10) != 0
379
+ @logger.warn "Peer supports extensions. Sending extended handshake"
380
+ extended = Extension.createExtendedHandshake torrentData.info
381
+ extended.serializeTo currentIo
382
+ end
383
+
384
+ # Read incoming handshake's peerid
385
+ msg.peerId = currentIo.read(PeerHandshake::PeerIdLen)
386
+
387
+ if msg.peerId == trackerclient.peerId
388
+ @logger.info "We got a connection from ourself. Closing connection."
389
+ close
390
+ return
391
+ end
392
+
393
+ peer = nil
394
+ peers = torrentData.peers.findById(msg.peerId)
395
+ if peers
396
+ peers.each do |existingPeer|
397
+ if existingPeer.state != :disconnected
398
+ @logger.warn "Peer with id #{msg.peerId} created a new connection when we already have a connection in state #{existingPeer.state}. Closing new connection."
399
+ close
400
+ return
401
+ else
402
+ if existingPeer.trackerPeer.ip == addr && existingPeer.trackerPeer.port == port
403
+ peer = existingPeer
404
+ end
405
+ end
406
+ end
407
+ end
408
+
409
+ if ! peer
410
+ peer = Peer.new(TrackerPeer.new(addr, port))
411
+ updatePeerWithHandshakeInfo(torrentData, msg, peer)
412
+ torrentData.peers.add peer
413
+ if ! peers
414
+ @logger.warn "Unknown peer with id #{msg.peerId} connected."
415
+ else
416
+ @logger.warn "Known peer with id #{msg.peerId} connected from new location."
417
+ end
418
+ else
419
+ @logger.warn "Known peer with id #{msg.peerId} connected from known location."
420
+ end
421
+
422
+ @logger.info "Peer #{peer} connected to us. "
423
+
424
+ peer.state = :established
425
+ peer.amChoked = true
426
+ peer.peerChoked = true
427
+ peer.amInterested = false
428
+ peer.peerInterested = false
429
+ if torrentData.info
430
+ peer.bitfield = Bitfield.new(torrentData.info.pieces.length)
431
+ else
432
+ peer.bitfield = EmptyBitfield.new
433
+ @logger.info "We have no metainfo yet, so setting peer #{peer} to have an EmptyBitfield"
434
+ end
435
+
436
+ # Send bitfield
437
+ sendBitfield(currentIo, torrentData.blockState.completePieceBitfield) if torrentData.blockState
438
+
439
+ setMetaInfo(peer)
440
+ setReadRateLimit(torrentData.downRateLimit) if torrentData.downRateLimit
441
+ setWriteRateLimit(torrentData.upRateLimit) if torrentData.upRateLimit
442
+ end
443
+
444
+ # Reactor method called when we have connected to a peer.
445
+ def clientInit(peer)
446
+ # We connected to a peer
447
+ # Send handshake
448
+ torrentData = @torrentData[peer.infoHash]
449
+ if ! torrentData
450
+ @logger.warn "No tracker client found for peer #{peer}. Closing connection."
451
+ close
452
+ return
453
+ end
454
+ trackerclient = torrentData.trackerClient
455
+
456
+ @logger.info "Connected to peer #{peer}. Sending handshake."
457
+ msg = PeerHandshake.new
458
+ msg.peerId = trackerclient.peerId
459
+ msg.infoHash = peer.infoHash
460
+ msg.serializeTo currentIo
461
+ peer.state = :handshaking
462
+ @reactor.scheduleTimer(@handshakeTimeout, [:handshake_timeout, peer], false)
463
+ @logger.debug "Done sending handshake."
464
+
465
+ # Send bitfield
466
+ sendBitfield(currentIo, torrentData.blockState.completePieceBitfield) if torrentData.blockState
467
+
468
+ setReadRateLimit(torrentData.downRateLimit) if torrentData.downRateLimit
469
+ setWriteRateLimit(torrentData.upRateLimit) if torrentData.upRateLimit
470
+ end
471
+
472
+ # Reactor method called when there is data ready to be read from a socket
473
+ def recvData(peer)
474
+ msg = nil
475
+
476
+ @logger.debug "Got data from peer #{peer}"
477
+
478
+ if peer.state == :handshaking
479
+ # Read handshake message
480
+ begin
481
+ @logger.debug "Reading handshake from #{peer}"
482
+ msg = PeerHandshake.unserializeFrom currentIo
483
+ rescue
484
+ @logger.warn "Peer #{peer} failed handshake: #{$!}"
485
+ setPeerDisconnected(peer)
486
+ close
487
+ return
488
+ end
489
+ else
490
+ begin
491
+ @logger.debug "Reading wire-message from #{peer}"
492
+ msg = peer.peerMsgSerializer.unserializeFrom currentIo
493
+ #msg = PeerWireMessage.unserializeFrom currentIo
494
+ rescue EOFError
495
+ @logger.info "Peer #{peer} disconnected."
496
+ setPeerDisconnected(peer)
497
+ close
498
+ return
499
+ rescue
500
+ @logger.warn "Unserializing message from peer #{peer} failed: #{$!}"
501
+ @logger.warn $!.backtrace.join "\n"
502
+ setPeerDisconnected(peer)
503
+ close
504
+ return
505
+ end
506
+ peer.updateUploadRate msg
507
+ @logger.debug "Peer #{peer} upload rate: #{peer.uploadRate.value} data only: #{peer.uploadRateDataOnly.value}"
508
+ end
509
+
510
+
511
+ if msg.is_a? PeerHandshake
512
+ # This is a remote peer that we connected to returning our handshake.
513
+ processHandshake(msg, peer)
514
+ peer.state = :established
515
+ peer.amChoked = true
516
+ peer.peerChoked = true
517
+ peer.amInterested = false
518
+ peer.peerInterested = false
519
+ elsif msg.is_a? BitfieldMessage
520
+ @logger.debug "Received bitfield message from peer."
521
+ handleBitfield(msg, peer)
522
+ elsif msg.is_a? Unchoke
523
+ @logger.debug "Received unchoke message from peer."
524
+ peer.amChoked = false
525
+ elsif msg.is_a? Choke
526
+ @logger.debug "Received choke message from peer."
527
+ peer.amChoked = true
528
+ elsif msg.is_a? Interested
529
+ @logger.debug "Received interested message from peer."
530
+ peer.peerInterested = true
531
+ elsif msg.is_a? Uninterested
532
+ @logger.debug "Received uninterested message from peer."
533
+ peer.peerInterested = false
534
+ elsif msg.is_a? Piece
535
+ @logger.debug "Received piece message from peer for torrent #{QuartzTorrent.bytesToHex(peer.infoHash)}: piece #{msg.pieceIndex} offset #{msg.blockOffset} length #{msg.data.length}."
536
+ handlePieceReceive(msg, peer)
537
+ elsif msg.is_a? Request
538
+ @logger.debug "Received request message from peer for torrent #{QuartzTorrent.bytesToHex(peer.infoHash)}: piece #{msg.pieceIndex} offset #{msg.blockOffset} length #{msg.blockLength}."
539
+ handleRequest(msg, peer)
540
+ elsif msg.is_a? Have
541
+ @logger.debug "Received have message from peer for torrent #{QuartzTorrent.bytesToHex(peer.infoHash)}: piece #{msg.pieceIndex}"
542
+ handleHave(msg, peer)
543
+ elsif msg.is_a? KeepAlive
544
+ @logger.debug "Received keep alive message from peer."
545
+ elsif msg.is_a? ExtendedHandshake
546
+ @logger.debug "Received extended handshake message from peer."
547
+ handleExtendedHandshake(msg, peer)
548
+ elsif msg.is_a? ExtendedMetaInfo
549
+ @logger.debug "Received extended metainfo message from peer."
550
+ handleExtendedMetainfo(msg, peer)
551
+ else
552
+ @logger.warn "Received a #{msg.class} message but handler is not implemented"
553
+ end
554
+ end
555
+
556
+ # Reactor method called when a scheduled timer expires.
557
+ def timerExpired(metadata)
558
+ if metadata.is_a?(Array) && metadata[0] == :manage_peers
559
+ managePeers(metadata[1])
560
+ elsif metadata.is_a?(Array) && metadata[0] == :request_blocks
561
+ requestBlocks(metadata[1])
562
+ elsif metadata.is_a?(Array) && metadata[0] == :check_piece_manager
563
+ checkPieceManagerResults(metadata[1])
564
+ elsif metadata.is_a?(Array) && metadata[0] == :handshake_timeout
565
+ handleHandshakeTimeout(metadata[1])
566
+ elsif metadata.is_a?(Array) && metadata[0] == :removetorrent
567
+ handleRemoveTorrent(metadata[1], metadata[2])
568
+ elsif metadata.is_a?(Array) && metadata[0] == :pausetorrent
569
+ handlePause(metadata[1], metadata[2])
570
+ elsif metadata.is_a?(Array) && metadata[0] == :get_torrent_data
571
+ @torrentData.each do |k,v|
572
+ begin
573
+ if metadata[3].nil? || k == metadata[3]
574
+ v = TorrentDataDelegate.new(v, self)
575
+ metadata[1][k] = v
576
+ end
577
+ rescue
578
+ @logger.error "Error building torrent data response for user: #{$!}"
579
+ @logger.error "#{$!.backtrace.join("\n")}"
580
+ end
581
+ end
582
+ metadata[2].signal
583
+ elsif metadata.is_a?(Array) && metadata[0] == :update_torrent_data
584
+ delegate = metadata[1]
585
+ if ! @torrentData.has_key?(infoHash)
586
+ delegate.state = :deleted
587
+ else
588
+ delegate.internalRefresh
589
+ end
590
+ metadata[2].signal
591
+ elsif metadata.is_a?(Array) && metadata[0] == :request_metadata_pieces
592
+ requestMetadataPieces(metadata[1])
593
+ elsif metadata.is_a?(Array) && metadata[0] == :check_metadata_piece_manager
594
+ checkMetadataPieceManagerResults(metadata[1])
595
+ else
596
+ @logger.info "Unknown timer #{metadata} expired."
597
+ end
598
+ end
599
+
600
+ # Reactor method called when an IO error occurs.
601
+ def error(peer, details)
602
+ # If a peer closes the connection during handshake before we determine their id, we don't have a completed
603
+ # Peer object yet. In this case the peer parameter is the symbol :listener_socket
604
+ if peer == :listener_socket
605
+ @logger.info "Error with handshaking peer: #{details}. Closing connection."
606
+ else
607
+ @logger.info "Error with peer #{peer}: #{details}. Closing connection."
608
+ setPeerDisconnected(peer)
609
+ end
610
+ # Close connection
611
+ close
612
+ end
613
+
614
+ # Get a hash of new TorrentDataDelegate objects keyed by torrent infohash.
615
+ # This method is meant to be called from a different thread than the one
616
+ # the reactor is running in. This method is not immediate but blocks until the
617
+ # data is prepared.
618
+ # If infoHash is passed, only that torrent data is returned (still in a hashtable; just one entry)
619
+ def getDelegateTorrentData(infoHash = nil)
620
+ # Use an immediate, non-recurring timer.
621
+ result = {}
622
+ return result if stopped?
623
+ semaphore = Semaphore.new
624
+ @reactor.scheduleTimer(0, [:get_torrent_data, result, semaphore, infoHash], false, true)
625
+ semaphore.wait
626
+ result
627
+ end
628
+
629
+ def updateDelegateTorrentData(delegate)
630
+ return if stopped?
631
+ # Use an immediate, non-recurring timer.
632
+ semaphore = Semaphore.new
633
+ @reactor.scheduleTimer(0, [:update_torrent_data, delegate, semaphore], false, true)
634
+ semaphore.wait
635
+ result
636
+ end
637
+
638
+ private
639
+ def setPeerDisconnected(peer)
640
+ peer.state = :disconnected
641
+ peer.uploadRate.reset
642
+ peer.downloadRate.reset
643
+ peer.uploadRateDataOnly.reset
644
+ peer.downloadRateDataOnly.reset
645
+
646
+ torrentData = @torrentData[peer.infoHash]
647
+ # Are we tracking this torrent?
648
+ if torrentData && torrentData.blockState
649
+ # For any outstanding requests, mark that we no longer have requested them
650
+ peer.requestedBlocks.each do |blockIndex, b|
651
+ blockInfo = torrentData.blockState.createBlockinfoByBlockIndex(blockIndex)
652
+ torrentData.blockState.setBlockRequested blockInfo, false
653
+ end
654
+ peer.requestedBlocks.clear
655
+ end
656
+
657
+ end
658
+
659
+ def processHandshake(msg, peer)
660
+ torrentData = torrentDataForHandshake(msg, peer)
661
+ # Are we tracking this torrent?
662
+ return false if !torrentData
663
+
664
+ if msg.peerId == torrentData.trackerClient.peerId
665
+ @logger.info "We connected to ourself. Closing connection."
666
+ peer.isUs = true
667
+ close
668
+ return
669
+ end
670
+
671
+ peers = torrentData.peers.findById(msg.peerId)
672
+ if peers
673
+ peers.each do |existingPeer|
674
+ if existingPeer.state == :connected
675
+ @logger.warn "Peer with id #{msg.peerId} created a new connection when we already have a connection in state #{existingPeer.state}. Closing new connection."
676
+ torrentData.peers.delete existingPeer
677
+ setPeerDisconnected(peer)
678
+ close
679
+ return
680
+ end
681
+ end
682
+ end
683
+
684
+ trackerclient = torrentData.trackerClient
685
+
686
+ updatePeerWithHandshakeInfo(torrentData, msg, peer)
687
+ if torrentData.info
688
+ peer.bitfield = Bitfield.new(torrentData.info.pieces.length)
689
+ else
690
+ peer.bitfield = EmptyBitfield.new
691
+ @logger.info "We have no metainfo yet, so setting peer #{peer} to have an EmptyBitfield"
692
+ end
693
+
694
+ # Send extended handshake if the peer supports extensions
695
+ if (msg.reserved.unpack("C8")[5] & 0x10) != 0
696
+ @logger.warn "Peer supports extensions. Sending extended handshake"
697
+ extended = Extension.createExtendedHandshake torrentData.info
698
+ extended.serializeTo currentIo
699
+ end
700
+
701
+ true
702
+ end
703
+
704
+ def torrentDataForHandshake(msg, peer)
705
+ torrentData = @torrentData[msg.infoHash]
706
+ # Are we tracking this torrent?
707
+ if !torrentData
708
+ if peer.is_a?(Peer)
709
+ @logger.info "Peer #{peer} failed handshake: we are not managing torrent #{QuartzTorrent.bytesToHex(msg.infoHash)}"
710
+ setPeerDisconnected(peer)
711
+ else
712
+ @logger.info "Incoming peer #{peer} failed handshake: we are not managing torrent #{QuartzTorrent.bytesToHex(msg.infoHash)}"
713
+ end
714
+ close
715
+ return nil
716
+ end
717
+ torrentData
718
+ end
719
+
720
+ def updatePeerWithHandshakeInfo(torrentData, msg, peer)
721
+ @logger.info "peer #{peer} sent valid handshake for torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)}"
722
+ peer.infoHash = msg.infoHash
723
+ # If this was a peer we got from a tracker that had no id then we only learn the id on handshake.
724
+ peer.trackerPeer.id = msg.peerId
725
+ torrentData.peers.idSet peer
726
+ end
727
+
728
+ def handleHandshakeTimeout(peer)
729
+ if peer.state == :handshaking
730
+ @logger.warn "Peer #{peer} failed handshake: handshake timed out after #{@handshakeTimeout} seconds."
731
+ withPeersIo(peer, "handling handshake timeout") do |io|
732
+ setPeerDisconnected(peer)
733
+ close(io)
734
+ end
735
+ end
736
+ end
737
+
738
+ def managePeers(infoHash)
739
+ torrentData = @torrentData[infoHash]
740
+ if ! torrentData
741
+ @logger.error "Manage peers: tracker client for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
742
+ return
743
+ end
744
+
745
+ return if torrentData.paused
746
+
747
+ trackerclient = torrentData.trackerClient
748
+
749
+ # Update our internal peer list for this torrent from the tracker client
750
+ getPeersFromTracker(torrentData, infoHash)
751
+
752
+ classifiedPeers = ClassifiedPeers.new torrentData.peers.all
753
+
754
+ manager = torrentData.peerManager
755
+ if ! manager
756
+ @logger.error "Manage peers: peer manager client for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
757
+ return
758
+ end
759
+
760
+ toConnect = manager.manageConnections(classifiedPeers)
761
+ toConnect.each do |peer|
762
+ @logger.debug "Connecting to peer #{peer}"
763
+ connect peer.trackerPeer.ip, peer.trackerPeer.port, peer
764
+ end
765
+
766
+ manageResult = manager.managePeers(classifiedPeers)
767
+ manageResult.unchoke.each do |peer|
768
+ @logger.debug "Unchoking peer #{peer}"
769
+ withPeersIo(peer, "unchoking peer") do |io|
770
+ msg = Unchoke.new
771
+ sendMessageToPeer msg, io, peer
772
+ peer.peerChoked = false
773
+ end
774
+ end
775
+
776
+ manageResult.choke.each do |peer|
777
+ @logger.debug "Choking peer #{peer}"
778
+ withPeersIo(peer, "choking peer") do |io|
779
+ msg = Choke.new
780
+ sendMessageToPeer msg, io, peer
781
+ peer.peerChoked = true
782
+ end
783
+ end
784
+
785
+ end
786
+
787
+ def requestBlocks(infoHash)
788
+ torrentData = @torrentData[infoHash]
789
+ if ! torrentData
790
+ @logger.error "Request blocks peers: tracker client for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
791
+ return
792
+ end
793
+
794
+ return if torrentData.paused
795
+
796
+ classifiedPeers = ClassifiedPeers.new torrentData.peers.all
797
+
798
+ if ! torrentData.blockState
799
+ @logger.error "Request blocks peers: no blockstate yet."
800
+ return
801
+ end
802
+
803
+ if torrentData.state == :uploading && (torrentData.state != :paused) && torrentData.ratio
804
+ if torrentData.bytesUploaded >= torrentData.ratio*torrentData.blockState.totalLength
805
+ @logger.info "Pausing torrent due to upload ratio limit." if torrentData.metainfoPieceState.complete?
806
+ setPaused(infoHash, true)
807
+ return
808
+ end
809
+ end
810
+
811
+ # Delete any timed-out requests.
812
+ classifiedPeers.establishedPeers.each do |peer|
813
+ toDelete = []
814
+ peer.requestedBlocks.each do |blockIndex, requestTime|
815
+ toDelete.push blockIndex if (Time.new - requestTime) > @requestTimeout
816
+ end
817
+ toDelete.each do |blockIndex|
818
+ @logger.debug "Block #{blockIndex} request timed out."
819
+ blockInfo = torrentData.blockState.createBlockinfoByBlockIndex(blockIndex)
820
+ torrentData.blockState.setBlockRequested blockInfo, false
821
+ peer.requestedBlocks.delete blockIndex
822
+ end
823
+ end
824
+
825
+ # Update the allowed pending requests based on how well the peer did since last time.
826
+ classifiedPeers.establishedPeers.each do |peer|
827
+ if peer.requestedBlocksSizeLastPass
828
+ if peer.requestedBlocksSizeLastPass == peer.maxRequestedBlocks
829
+ downloaded = peer.requestedBlocksSizeLastPass - peer.requestedBlocks.size
830
+ if downloaded > peer.maxRequestedBlocks*8/10
831
+ peer.maxRequestedBlocks = peer.maxRequestedBlocks * 12 / 10
832
+ elsif downloaded == 0
833
+ peer.maxRequestedBlocks = peer.maxRequestedBlocks * 8 / 10
834
+ end
835
+ peer.maxRequestedBlocks = 10 if peer.maxRequestedBlocks < 10
836
+ end
837
+ end
838
+ end
839
+
840
+ # Request blocks
841
+ blockInfos = torrentData.blockState.findRequestableBlocks(classifiedPeers, 100)
842
+ blockInfos.each do |blockInfo|
843
+ # Pick one of the peers that has the piece to download it from. Pick one of the
844
+ # peers with the top 3 upload rates.
845
+ elegiblePeers = blockInfo.peers.find_all{ |p| p.requestedBlocks.length < p.maxRequestedBlocks }.sort{ |a,b| b.uploadRate.value <=> a.uploadRate.value}
846
+ random = elegiblePeers[rand(blockInfo.peers.size)]
847
+ peer = elegiblePeers.first(3).push(random).shuffle.first
848
+ next if ! peer
849
+ withPeersIo(peer, "requesting block") do |io|
850
+ if ! peer.amInterested
851
+ # Let this peer know that I'm interested if I haven't yet.
852
+ msg = Interested.new
853
+ sendMessageToPeer msg, io, peer
854
+ peer.amInterested = true
855
+ end
856
+ @logger.debug "Requesting block from #{peer}: piece #{blockInfo.pieceIndex} offset #{blockInfo.offset} length #{blockInfo.length}"
857
+ msg = blockInfo.getRequest
858
+ sendMessageToPeer msg, io, peer
859
+ torrentData.blockState.setBlockRequested blockInfo, true
860
+ peer.requestedBlocks[blockInfo.blockIndex] = Time.new
861
+ end
862
+ end
863
+
864
+ if blockInfos.size == 0
865
+ if torrentData.blockState.completePieceBitfield.allSet?
866
+ @logger.info "Download of #{QuartzTorrent.bytesToHex(infoHash)} complete."
867
+ torrentData.state = :uploading
868
+ end
869
+ end
870
+
871
+ classifiedPeers.establishedPeers.each { |peer| peer.requestedBlocksSizeLastPass = peer.requestedBlocks.length }
872
+ end
873
+
874
+ # For a torrent where we don't have the metainfo, request metainfo pieces from peers.
875
+ def requestMetadataPieces(infoHash)
876
+ torrentData = @torrentData[infoHash]
877
+ if ! torrentData
878
+ @logger.error "Request metadata pices: torrent data for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
879
+ return
880
+ end
881
+
882
+ return if torrentData.paused
883
+
884
+ # We may not have completed the extended handshake with the peer which specifies the torrent size.
885
+ # In this case torrentData.metainfoPieceState is not yet set.
886
+ return if ! torrentData.metainfoPieceState
887
+
888
+ @logger.info "Obtained all pieces of metainfo." if torrentData.metainfoPieceState.complete?
889
+
890
+ pieces = torrentData.metainfoPieceState.findRequestablePieces
891
+ classifiedPeers = ClassifiedPeers.new torrentData.peers.all
892
+ peers = torrentData.metainfoPieceState.findRequestablePeers(classifiedPeers)
893
+
894
+ if peers.size > 0
895
+ # For now, just request all pieces from the first peer.
896
+ pieces.each do |pieceIndex|
897
+ msg = ExtendedMetaInfo.new
898
+ msg.msgType = :request
899
+ msg.piece = pieceIndex
900
+ withPeersIo(peers.first, "requesting metadata piece") do |io|
901
+ sendMessageToPeer msg, io, peers.first
902
+ torrentData.metainfoPieceState.setPieceRequested(pieceIndex, true)
903
+ @logger.debug "Requesting metainfo piece from #{peers.first}: piece #{pieceIndex}"
904
+ end
905
+ end
906
+ else
907
+ @logger.error "No peers found that have metadata."
908
+ end
909
+
910
+ end
911
+
912
+ def checkMetadataPieceManagerResults(infoHash)
913
+ torrentData = @torrentData[infoHash]
914
+ if ! torrentData
915
+ @logger.error "Check metadata piece manager results: data for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
916
+ return
917
+ end
918
+
919
+ # We may not have completed the extended handshake with the peer which specifies the torrent size.
920
+ # In this case torrentData.metainfoPieceState is not yet set.
921
+ return if ! torrentData.metainfoPieceState
922
+
923
+ results = torrentData.metainfoPieceState.checkResults
924
+ results.each do |result|
925
+ metaData = torrentData.pieceManagerMetainfoRequestMetadata.delete(result.requestId)
926
+ if ! metaData
927
+ @logger.error "Can't find metadata for PieceManager request #{result.requestId}"
928
+ next
929
+ end
930
+
931
+ if metaData.type == :read && result.successful?
932
+ # Send the piece to the peer.
933
+ msg = ExtendedMetaInfo.new
934
+ msg.msgType = :piece
935
+ msg.piece = metaData.data.requestMsg.piece
936
+ msg.data = result.data
937
+ withPeersIo(metaData.data.peer, "sending extended metainfo piece message") do |io|
938
+ @logger.debug "Sending metainfo piece to #{metaData.data.peer}: piece #{msg.piece} with data length #{msg.data.length}"
939
+ sendMessageToPeer msg, io, metaData.data.peer
940
+ end
941
+ result.data
942
+ end
943
+ end
944
+
945
+ if torrentData.metainfoPieceState.complete? && torrentData.state == :downloading_metainfo
946
+ @logger.info "Obtained all pieces of metainfo. Will begin checking existing pieces."
947
+ torrentData.metainfoPieceState.flush
948
+ # We don't need to download metainfo anymore.
949
+ cancelTimer torrentData.metainfoRequestTimer if torrentData.metainfoRequestTimer
950
+ info = MetainfoPieceState.downloaded(@baseDirectory, torrentData.infoHash)
951
+ if info
952
+ torrentData.info = info
953
+ startCheckingPieces torrentData
954
+ else
955
+ @logger.error "Metadata download is complete but reading the metadata failed"
956
+ torrentData.state = :error
957
+ end
958
+ end
959
+ end
960
+
961
+ def handlePieceReceive(msg, peer)
962
+ torrentData = @torrentData[peer.infoHash]
963
+ if ! torrentData
964
+ @logger.error "Receive piece: torrent data for torrent #{QuartzTorrent.bytesToHex(peer.infoHash)} not found."
965
+ return
966
+ end
967
+
968
+ if ! torrentData.blockState
969
+ @logger.error "Receive piece: no blockstate yet."
970
+ return
971
+ end
972
+
973
+ blockInfo = torrentData.blockState.createBlockinfoByPieceResponse(msg.pieceIndex, msg.blockOffset, msg.data.length)
974
+ if torrentData.blockState.blockCompleted?(blockInfo)
975
+ @logger.debug "Receive piece: we already have this block. Ignoring this message."
976
+ return
977
+ end
978
+ peer.requestedBlocks.delete blockInfo.blockIndex
979
+ # Block is marked as not requested when hash is confirmed
980
+
981
+ torrentData.bytesDownloaded += msg.data.length
982
+ id = torrentData.pieceManager.writeBlock(msg.pieceIndex, msg.blockOffset, msg.data)
983
+ torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:write, msg)
984
+ end
985
+
986
+ def handleRequest(msg, peer)
987
+ if peer.peerChoked
988
+ @logger.warn "Request piece: peer #{peer} requested a block when they are choked."
989
+ return
990
+ end
991
+
992
+ torrentData = @torrentData[peer.infoHash]
993
+ if ! torrentData
994
+ @logger.error "Request piece: torrent data for torrent #{QuartzTorrent.bytesToHex(peer.infoHash)} not found."
995
+ return
996
+ end
997
+ if msg.blockLength <= 0
998
+ @logger.error "Request piece: peer requested block of length #{msg.blockLength} which is invalid."
999
+ return
1000
+ end
1001
+
1002
+ id = torrentData.pieceManager.readBlock(msg.pieceIndex, msg.blockOffset, msg.blockLength)
1003
+ torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:read, ReadRequestMetadata.new(peer,msg))
1004
+ end
1005
+
1006
+ def handleBitfield(msg, peer)
1007
+ torrentData = @torrentData[peer.infoHash]
1008
+ if ! torrentData
1009
+ @logger.error "Bitfield: torrent data for torrent #{QuartzTorrent.bytesToHex(peer.infoHash)} not found."
1010
+ return
1011
+ end
1012
+
1013
+ peer.bitfield = msg.bitfield
1014
+
1015
+ if ! torrentData.blockState
1016
+ @logger.warn "Bitfield: no blockstate yet."
1017
+ return
1018
+ end
1019
+
1020
+ # If we are interested in something from this peer, let them know.
1021
+ needed = torrentData.blockState.completePieceBitfield.compliment
1022
+ needed.intersection!(peer.bitfield)
1023
+ if ! needed.allClear?
1024
+ if ! peer.amInterested
1025
+ @logger.debug "Need some pieces from peer #{peer} so sending Interested message"
1026
+ msg = Interested.new
1027
+ sendMessageToPeer msg, currentIo, peer
1028
+ peer.amInterested = true
1029
+ end
1030
+ end
1031
+ end
1032
+
1033
+ def handleHave(msg, peer)
1034
+ torrentData = @torrentData[peer.infoHash]
1035
+ if ! torrentData
1036
+ @logger.error "Have: torrent data for torrent #{QuartzTorrent.bytesToHex(peer.infoHash)} not found."
1037
+ return
1038
+ end
1039
+
1040
+ if msg.pieceIndex >= peer.bitfield.length
1041
+ @logger.warn "Peer #{peer} sent Have message with invalid piece index"
1042
+ return
1043
+ end
1044
+
1045
+ # Update peer's bitfield
1046
+ peer.bitfield.set msg.pieceIndex
1047
+
1048
+ if ! torrentData.blockState
1049
+ @logger.warn "Have: no blockstate yet."
1050
+ return
1051
+ end
1052
+
1053
+ # If we are interested in something from this peer, let them know.
1054
+ if ! torrentData.blockState.completePieceBitfield.set?(msg.pieceIndex)
1055
+ @logger.debug "Peer #{peer} just got a piece we need so sending Interested message"
1056
+ msg = Interested.new
1057
+ sendMessageToPeer msg, currentIo, peer
1058
+ peer.amInterested = true
1059
+ end
1060
+ end
1061
+
1062
+ def checkPieceManagerResults(infoHash)
1063
+ torrentData = @torrentData[infoHash]
1064
+ if ! torrentData
1065
+ @logger.error "Request blocks peers: tracker client for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
1066
+ return
1067
+ end
1068
+
1069
+ while true
1070
+ result = torrentData.pieceManager.nextResult
1071
+ break if ! result
1072
+
1073
+ metaData = torrentData.pieceManagerRequestMetadata.delete(result.requestId)
1074
+ if ! metaData
1075
+ @logger.error "Can't find metadata for PieceManager request #{result.requestId}"
1076
+ next
1077
+ end
1078
+
1079
+ if metaData.type == :write
1080
+ if result.successful?
1081
+ @logger.debug "Block written to disk. "
1082
+ # Block successfully written!
1083
+ torrentData.blockState.setBlockCompleted metaData.data.pieceIndex, metaData.data.blockOffset, true do |pieceIndex|
1084
+ # The peice is completed! Check hash.
1085
+ @logger.debug "Piece #{pieceIndex} is complete. Checking hash. "
1086
+ id = torrentData.pieceManager.checkPieceHash(metaData.data.pieceIndex)
1087
+ torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:hash, metaData.data.pieceIndex)
1088
+ end
1089
+ else
1090
+ # Block failed! Clear completed and requested state.
1091
+ torrentData.blockState.setBlockCompleted metaData.data.pieceIndex, metaData.data.blockOffset, false
1092
+ @logger.error "Writing block failed: #{result.error}"
1093
+ end
1094
+ elsif metaData.type == :read
1095
+ if result.successful?
1096
+ readRequestMetadata = metaData.data
1097
+ peer = readRequestMetadata.peer
1098
+ withPeersIo(peer, "sending piece message") do |io|
1099
+ msg = Piece.new
1100
+ msg.pieceIndex = readRequestMetadata.requestMsg.pieceIndex
1101
+ msg.blockOffset = readRequestMetadata.requestMsg.blockOffset
1102
+ msg.data = result.data
1103
+ sendMessageToPeer msg, io, peer
1104
+ torrentData.bytesUploaded += msg.data.length
1105
+ @logger.debug "Sending piece to peer"
1106
+ end
1107
+ else
1108
+ @logger.error "Reading block failed: #{result.error}"
1109
+ end
1110
+ elsif metaData.type == :hash
1111
+ if result.successful?
1112
+ @logger.debug "Hash of piece #{metaData.data} is correct"
1113
+ sendHaves(torrentData, metaData.data)
1114
+ sendUninterested(torrentData)
1115
+ else
1116
+ @logger.info "Hash of piece #{metaData.data} is incorrect. Marking piece as not complete."
1117
+ torrentData.blockState.setPieceCompleted metaData.data, false
1118
+ end
1119
+ elsif metaData.type == :check_existing
1120
+ handleCheckExistingResult(torrentData, result)
1121
+ end
1122
+ end
1123
+ end
1124
+
1125
+ # Handle the result of the PieceManager's checkExisting (check which pieces we already have) operation.
1126
+ # If the resukt is successful, this begins the actual download.
1127
+ def handleCheckExistingResult(torrentData, pieceManagerResult)
1128
+ if pieceManagerResult.successful?
1129
+ existingBitfield = pieceManagerResult.data
1130
+ @logger.info "We already have #{existingBitfield.countSet}/#{existingBitfield.length} pieces."
1131
+
1132
+ info = torrentData.info
1133
+
1134
+ torrentData.blockState = BlockState.new(info, existingBitfield)
1135
+
1136
+ @logger.info "Starting torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)}. Information:"
1137
+ @logger.info " piece length: #{info.pieceLen}"
1138
+ @logger.info " number of pieces: #{info.pieces.size}"
1139
+ @logger.info " total length #{info.dataLength}"
1140
+
1141
+ startDownload torrentData
1142
+ else
1143
+ @logger.info "Checking existing pieces of torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)} failed: #{pieceManagerResult.error}"
1144
+ torrentData.state = :error
1145
+ end
1146
+ end
1147
+
1148
+ # Start checking which pieces we already have downloaded. This method schedules the necessary timers
1149
+ # and changes the state to :checking_pieces. When the pieces are finished being checked the actual download will
1150
+ # begin.
1151
+ # Preconditions: The torrentData object already has it's info member set.
1152
+ def startCheckingPieces(torrentData)
1153
+ torrentData.pieceManager = QuartzTorrent::PieceManager.new(@baseDirectory, torrentData.info)
1154
+
1155
+ torrentData.state = :checking_pieces
1156
+ @logger.info "Checking pieces of torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)} asynchronously."
1157
+ id = torrentData.pieceManager.findExistingPieces
1158
+ torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:check_existing, nil)
1159
+
1160
+ if ! torrentData.metainfoPieceState
1161
+ torrentData.metainfoPieceState = MetainfoPieceState.new(@baseDirectory, torrentData.infoHash, nil, torrentData.info)
1162
+ end
1163
+
1164
+ # Schedule checking for PieceManager results
1165
+ torrentData.checkPieceManagerTimer =
1166
+ @reactor.scheduleTimer(@requestBlocksPeriod, [:check_piece_manager, torrentData.infoHash], true, false)
1167
+
1168
+ # Schedule checking for metainfo PieceManager results (including when piece reading completes)
1169
+ if ! torrentData.checkMetadataPieceManagerTimer
1170
+ torrentData.checkMetadataPieceManagerTimer =
1171
+ @reactor.scheduleTimer(@requestBlocksPeriod, [:check_metadata_piece_manager, torrentData.infoHash], true, false)
1172
+ end
1173
+ end
1174
+
1175
+ # Start the actual torrent download. This method schedules the necessary timers and registers the necessary listeners
1176
+ # and changes the state to :running. It is meant to be called after checking for existing pieces or downloading the
1177
+ # torrent metadata (if this is a magnet link torrent)
1178
+ def startDownload(torrentData)
1179
+ # Add a listener for when the tracker's peers change.
1180
+ torrentData.peerChangeListener = Proc.new do
1181
+ @logger.debug "Managing peers for torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)} on peer change event"
1182
+
1183
+ # Non-recurring and immediate timer
1184
+ torrentData.managePeersTimer =
1185
+ @reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], false, true)
1186
+ end
1187
+ torrentData.trackerClient.addPeersChangedListener torrentData.peerChangeListener
1188
+
1189
+ # Schedule peer connection management. Recurring and immediate
1190
+ if ! torrentData.managePeersTimer
1191
+ torrentData.managePeersTimer =
1192
+ @reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], true, true)
1193
+ end
1194
+
1195
+ # Schedule requesting blocks from peers. Recurring and not immediate
1196
+ torrentData.requestBlocksTimer =
1197
+ @reactor.scheduleTimer(@requestBlocksPeriod, [:request_blocks, torrentData.infoHash], true, false)
1198
+ torrentData.state = :running
1199
+ end
1200
+
1201
+ def handleExtendedHandshake(msg, peer)
1202
+ torrentData = @torrentData[peer.infoHash]
1203
+ if ! torrentData
1204
+ @logger.error "Extended Handshake: torrent data for torrent #{QuartzTorrent.bytesToHex(peer.infoHash)} not found."
1205
+ return
1206
+ end
1207
+
1208
+ metadataSize = msg.dict['metadata_size']
1209
+ if metadataSize
1210
+ # This peer knows the size of the metadata. If we haven't created our MetainfoPieceState yet, create it now.
1211
+ if ! torrentData.metainfoPieceState
1212
+ @logger.info "Extended Handshake: Learned that metadata size is #{metadataSize}. Creating MetainfoPieceState"
1213
+ torrentData.metainfoPieceState = MetainfoPieceState.new(@baseDirectory, torrentData.infoHash, metadataSize)
1214
+ end
1215
+ end
1216
+
1217
+ end
1218
+
1219
+ def handleExtendedMetainfo(msg, peer)
1220
+ torrentData = @torrentData[peer.infoHash]
1221
+ if ! torrentData
1222
+ @logger.error "Extended Handshake: torrent data for torrent #{QuartzTorrent.bytesToHex(peer.infoHash)} not found."
1223
+ return
1224
+ end
1225
+
1226
+ if msg.msgType == :request
1227
+ @logger.debug "Got extended metainfo request for piece #{msg.piece}"
1228
+ # Build a response for this piece.
1229
+ if torrentData.metainfoPieceState.pieceCompleted? msg.piece
1230
+ @logger.debug "Requesting extended metainfo piece #{msg.piece} from metainfoPieceState."
1231
+ id = torrentData.metainfoPieceState.readPiece msg.piece
1232
+ torrentData.pieceManagerMetainfoRequestMetadata[id] =
1233
+ PieceManagerRequestMetadata.new(:read, ReadRequestMetadata.new(peer,msg))
1234
+ else
1235
+ reject = ExtendedMetaInfo.new
1236
+ reject.msgType = :reject
1237
+ reject.piece = msg.piece
1238
+ withPeersIo(peer, "sending extended metainfo reject message") do |io|
1239
+ @logger.debug "Sending metainfo reject to #{peer}: piece #{msg.piece}"
1240
+ sendMessageToPeer reject, io, peer
1241
+ end
1242
+ end
1243
+ elsif msg.msgType == :piece
1244
+ @logger.debug "Got extended metainfo piece response for piece #{msg.piece} with data length #{msg.data.length}"
1245
+ if ! torrentData.metainfoPieceState.pieceCompleted? msg.piece
1246
+ id = torrentData.metainfoPieceState.savePiece msg.piece, msg.data
1247
+ torrentData.pieceManagerMetainfoRequestMetadata[id] =
1248
+ PieceManagerRequestMetadata.new(:write, msg)
1249
+ end
1250
+ elsif msg.msgType == :reject
1251
+ @logger.debug "Got extended metainfo reject response for piece #{msg.piece}"
1252
+ # Mark this peer as bad.
1253
+ torrentData.metainfoPieceState.markPeerBad peer
1254
+ torrentData.metainfoPieceState.setPieceRequested(msg.piece, false)
1255
+ end
1256
+ end
1257
+
1258
+ # Find the io associated with the peer and yield it to the passed block.
1259
+ # If no io is found an error is logged.
1260
+ #
1261
+ def withPeersIo(peer, what = nil)
1262
+ io = findIoByMetainfo(peer)
1263
+ if io
1264
+ yield io
1265
+ else
1266
+ s = ""
1267
+ s = "when #{what}" if what
1268
+ @logger.warn "Couldn't find the io for peer #{peer} #{what}"
1269
+ end
1270
+ end
1271
+
1272
+ def sendBitfield(io, bitfield)
1273
+ if ! bitfield.allClear?
1274
+ @logger.debug "Sending bitfield of size #{bitfield.length}."
1275
+ msg = BitfieldMessage.new
1276
+ msg.bitfield = bitfield
1277
+ msg.serializeTo io
1278
+ end
1279
+ end
1280
+
1281
+ def sendHaves(torrentData, pieceIndex)
1282
+ @logger.debug "Sending Have messages to all connected peers for piece #{pieceIndex}"
1283
+ torrentData.peers.all.each do |peer|
1284
+ next if peer.state != :established || peer.isUs
1285
+ withPeersIo(peer, "when sending Have message") do |io|
1286
+ msg = Have.new
1287
+ msg.pieceIndex = pieceIndex
1288
+ sendMessageToPeer msg, io, peer
1289
+ end
1290
+ end
1291
+ end
1292
+
1293
+ def sendUninterested(torrentData)
1294
+ # If we are no longer interested in peers once this piece has been completed, let them know
1295
+ return if ! torrentData.blockState
1296
+ needed = torrentData.blockState.completePieceBitfield.compliment
1297
+
1298
+ classifiedPeers = ClassifiedPeers.new torrentData.peers.all
1299
+ classifiedPeers.establishedPeers.each do |peer|
1300
+ # Don't bother sending uninterested message if we are already uninterested.
1301
+ next if ! peer.amInterested || peer.isUs
1302
+ needFromPeer = needed.intersection(peer.bitfield)
1303
+ if needFromPeer.allClear?
1304
+ withPeersIo(peer, "when sending Uninterested message") do |io|
1305
+ msg = Uninterested.new
1306
+ sendMessageToPeer msg, io, peer
1307
+ peer.amInterested = false
1308
+ @logger.debug "Sending Uninterested message to peer #{peer}"
1309
+ end
1310
+ end
1311
+ end
1312
+ end
1313
+
1314
+ def sendMessageToPeer(msg, io, peer)
1315
+ peer.updateDownloadRate(msg)
1316
+ begin
1317
+ peer.peerMsgSerializer.serializeTo(msg, io)
1318
+ rescue
1319
+ @logger.warn "Sending message to peer #{peer} failed: #{$!.message}"
1320
+ end
1321
+ msg.serializeTo io
1322
+ end
1323
+
1324
+ # Update our internal peer list for this torrent from the tracker client
1325
+ def getPeersFromTracker(torrentData, infoHash)
1326
+ addPeer = Proc.new do |trackerPeer|
1327
+ peer = Peer.new(trackerPeer)
1328
+ peer.infoHash = infoHash
1329
+ torrentData.peers.add peer
1330
+ true
1331
+ end
1332
+
1333
+ classifiedPeers = nil
1334
+ replaceDisconnectedPeer = Proc.new do |trackerPeer|
1335
+ classifiedPeers = ClassifiedPeers.new(torrentData.peers.all) if ! classifiedPeers
1336
+
1337
+ if classifiedPeers.disconnectedPeers.size > 0
1338
+ torrentData.peers.delete classifiedPeers.disconnectedPeers.pop
1339
+ addPeer.call trackerPeer
1340
+ true
1341
+ else
1342
+ false
1343
+ end
1344
+ end
1345
+
1346
+ trackerclient = torrentData.trackerClient
1347
+
1348
+ addProc = addPeer
1349
+ flipped = false
1350
+ trackerclient.peers.each do |p|
1351
+ if ! flipped && torrentData.peers.size >= @maxPeerCount
1352
+ addProc = replaceDisconnectedPeer
1353
+ flipped = true
1354
+ end
1355
+
1356
+ # Don't treat ourself as a peer.
1357
+ next if p.id && p.id == trackerclient.peerId
1358
+
1359
+ if ! torrentData.peers.findByAddr(p.ip, p.port)
1360
+ @logger.debug "Adding tracker peer #{p} to peers list"
1361
+ break if ! addProc.call(p)
1362
+ end
1363
+ end
1364
+ end
1365
+
1366
+ # Remove a torrent that we are downloading.
1367
+ def handleRemoveTorrent(infoHash, deleteFiles)
1368
+ torrentData = @torrentData.delete infoHash
1369
+ if ! torrentData
1370
+ @logger.warn "Asked to remove a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
1371
+ return
1372
+ end
1373
+ @logger.info "Removing torrent #{QuartzTorrent.bytesToHex(infoHash)} and #{deleteFiles ? "will" : "wont"} delete downloaded files."
1374
+
1375
+ @logger.info "Removing torrent: no torrentData.metainfoRequestTimer" if ! torrentData.metainfoRequestTimer
1376
+ @logger.info "Removing torrent: no torrentData.managePeersTimer" if ! torrentData.managePeersTimer
1377
+ @logger.info "Removing torrent: no torrentData.checkMetadataPieceManagerTimer" if ! torrentData.checkMetadataPieceManagerTimer
1378
+ @logger.info "Removing torrent: no torrentData.checkPieceManagerTimer" if ! torrentData.checkPieceManagerTimer
1379
+ @logger.info "Removing torrent: no torrentData.requestBlocksTimer" if ! torrentData.requestBlocksTimer
1380
+
1381
+
1382
+ # Stop all timers
1383
+ cancelTimer torrentData.metainfoRequestTimer if torrentData.metainfoRequestTimer
1384
+ cancelTimer torrentData.managePeersTimer if torrentData.managePeersTimer
1385
+ cancelTimer torrentData.checkMetadataPieceManagerTimer if torrentData.checkMetadataPieceManagerTimer
1386
+ cancelTimer torrentData.checkPieceManagerTimer if torrentData.checkPieceManagerTimer
1387
+ cancelTimer torrentData.requestBlocksTimer if torrentData.requestBlocksTimer
1388
+
1389
+ torrentData.trackerClient.removePeersChangedListener(torrentData.peerChangeListener)
1390
+
1391
+ # Remove all the peers for this torrent.
1392
+ torrentData.peers.all.each do |peer|
1393
+ if peer.state != :disconnected
1394
+ # Close socket
1395
+ withPeersIo(peer, "when removing torrent") do |io|
1396
+ setPeerDisconnected(peer)
1397
+ close(io)
1398
+ @logger.debug "Closing connection to peer #{peer}"
1399
+ end
1400
+ end
1401
+ torrentData.peers.delete peer
1402
+ end
1403
+
1404
+ # Stop tracker client
1405
+ torrentData.trackerClient.stop if torrentData.trackerClient
1406
+
1407
+ # Stop PieceManagers
1408
+ torrentData.pieceManager.stop if torrentData.pieceManager
1409
+ torrentData.metainfoPieceState.stop if torrentData.metainfoPieceState
1410
+
1411
+ # Remove metainfo file if it exists
1412
+ begin
1413
+ torrentData.metainfoPieceState.remove if torrentData.metainfoPieceState
1414
+ rescue
1415
+ @logger.warn "Deleting metainfo file for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
1416
+ end
1417
+
1418
+ if deleteFiles
1419
+ if torrentData.info
1420
+ begin
1421
+ path = @baseDirectory + File::SEPARATOR + torrentData.info.name
1422
+ if File.exists? path
1423
+ FileUtils.rm_r path
1424
+ @logger.info "Deleted #{path}"
1425
+ else
1426
+ @logger.warn "Deleting '#{path}' for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
1427
+ end
1428
+ rescue
1429
+ @logger.warn "When removing torrent #{QuartzTorrent.bytesToHex(infoHash)} deleting '#{path}' failed because it doesn't exist"
1430
+ end
1431
+ end
1432
+ end
1433
+ end
1434
+
1435
+ # Pause or unpause a torrent that we are downloading.
1436
+ def handlePause(infoHash, value)
1437
+ torrentData = @torrentData[infoHash]
1438
+ if ! torrentData
1439
+ @logger.warn "Asked to pause a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
1440
+ return
1441
+ end
1442
+
1443
+ return if torrentData.paused == value
1444
+
1445
+ if value
1446
+ torrentData.paused = true
1447
+
1448
+ # Disconnect from all peers so we won't reply to any messages.
1449
+ torrentData.peers.all.each do |peer|
1450
+ if peer.state != :disconnected
1451
+ # Close socket
1452
+ withPeersIo(peer, "when removing torrent") do |io|
1453
+ setPeerDisconnected(peer)
1454
+ close(io)
1455
+ end
1456
+ end
1457
+ torrentData.peers.delete peer
1458
+ end
1459
+ else
1460
+ torrentData.paused = false
1461
+
1462
+ # Get our list of peers and start connecting right away
1463
+ # Non-recurring and immediate timer
1464
+ torrentData.managePeersTimer =
1465
+ @reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], false, true)
1466
+ end
1467
+ end
1468
+
1469
+ end
1470
+
1471
+ # Represents a client that talks to bittorrent peers. This is the main class used to download and upload
1472
+ # bittorrents.
1473
+ class PeerClient
1474
+
1475
+ # Create a new PeerClient that will save and load torrent data under the specified baseDirectory.
1476
+ def initialize(baseDirectory)
1477
+ @port = 9998
1478
+ @handler = nil
1479
+ @stopped = true
1480
+ @reactor = nil
1481
+ @logger = LogManager.getLogger("peerclient")
1482
+ @worker = nil
1483
+ @handler = PeerClientHandler.new baseDirectory
1484
+ @reactor = QuartzTorrent::Reactor.new(@handler, LogManager.getLogger("peerclient.reactor"))
1485
+ @toStart = []
1486
+ end
1487
+
1488
+ # Set the port used by the torrent peer client. This only has an effect if start has not yet been called.
1489
+ attr_accessor :port
1490
+
1491
+ # Start the PeerClient: open the listening port, and start a new thread to begin downloading/uploading pieces.
1492
+ def start
1493
+ return if ! @stopped
1494
+
1495
+ @reactor.listen("0.0.0.0",@port,:listener_socket)
1496
+
1497
+ @stopped = false
1498
+ @worker = Thread.new do
1499
+ QuartzTorrent.initThread("peerclient")
1500
+ begin
1501
+ @toStart.each{ |trackerclient| trackerclient.start }
1502
+ @reactor.start
1503
+ @logger.info "Reactor stopped."
1504
+ @handler.torrentData.each do |k,v|
1505
+ v.trackerClient.stop
1506
+ end
1507
+ rescue
1508
+ @logger.error "Unexpected exception in worker thread: #{$!}"
1509
+ @logger.error $!.backtrace.join("\n")
1510
+ end
1511
+ end
1512
+ end
1513
+
1514
+ # Stop the PeerClient. This method may take some time to complete.
1515
+ def stop
1516
+ return if @stopped
1517
+
1518
+ @logger.info "Stop called. Stopping reactor"
1519
+ @reactor.stop
1520
+ if @worker
1521
+ @logger.info "Worker wait timed out after 10 seconds. Shutting down anyway" if ! @worker.join(10)
1522
+ end
1523
+ @stopped = true
1524
+ end
1525
+
1526
+ # Add a new torrent to manage described by a Metainfo object. This is generally the
1527
+ # method to call if you have a .torrent file.
1528
+ # Returns the infoHash of the newly added torrent.
1529
+ def addTorrentByMetainfo(metainfo)
1530
+ raise "addTorrentByMetainfo should be called with a Metainfo object, not #{metainfo.class}" if ! metainfo.is_a?(Metainfo)
1531
+ trackerclient = TrackerClient.createFromMetainfo(metainfo, false)
1532
+ addTorrent(trackerclient, metainfo.infoHash, metainfo.info)
1533
+ end
1534
+
1535
+ # Add a new torrent to manage given an announceUrl and an infoHash.
1536
+ # Returns the infoHash of the newly added torrent.
1537
+ def addTorrentWithoutMetainfo(announceUrl, infoHash, magnet = nil)
1538
+ raise "addTorrentWithoutMetainfo should be called with a Magnet object, not a #{magnet.class}" if magnet && ! magnet.is_a?(MagnetURI)
1539
+ trackerclient = TrackerClient.create(announceUrl, infoHash, 0, false)
1540
+ addTorrent(trackerclient, infoHash, nil, magnet)
1541
+ end
1542
+
1543
+ # Add a new torrent to manage given a MagnetURI object. This is generally the
1544
+ # method to call if you have a magnet link.
1545
+ # Returns the infoHash of the newly added torrent.
1546
+ def addTorrentByMagnetURI(magnet)
1547
+ raise "addTorrentByMagnetURI should be called with a MagnetURI object, not a #{magnet.class}" if ! magnet.is_a?(MagnetURI)
1548
+
1549
+ trackerUrl = magnet.tracker
1550
+ raise "addTorrentByMagnetURI can't handle magnet links that don't have a tracker URL." if !trackerUrl
1551
+
1552
+ addTorrentWithoutMetainfo(trackerUrl, magnet.btInfoHash, magnet)
1553
+ end
1554
+
1555
+ # Get a hash of new TorrentDataDelegate objects keyed by torrent infohash. This is the method to
1556
+ # call to get information about the state of torrents being downloaded.
1557
+ def torrentData(infoHash = nil)
1558
+ # This will have to work by putting an event in the handler's queue, and blocking for a response.
1559
+ # The handler will build a response and return it.
1560
+ @handler.getDelegateTorrentData(infoHash)
1561
+ end
1562
+
1563
+ # Pause or unpause the specified torrent.
1564
+ def setPaused(infoHash, value)
1565
+ @handler.setPaused(infoHash, value)
1566
+ end
1567
+
1568
+ # Set the download rate limit in bytes/second.
1569
+ def setDownloadRateLimit(infoHash, bytesPerSecond)
1570
+ raise "download rate limit must be an Integer, not a #{bytesPerSecond.class}" if bytesPerSecond && ! bytesPerSecond.is_a?(Integer)
1571
+ @handler.setDownloadRateLimit(infoHash, bytesPerSecond)
1572
+ end
1573
+
1574
+ # Set the upload rate limit in bytes/second.
1575
+ def setUploadRateLimit(infoHash, bytesPerSecond)
1576
+ raise "upload rate limit must be an Integer, not a #{bytesPerSecond.class}" if bytesPerSecond && ! bytesPerSecond.is_a?(Integer)
1577
+ @handler.setUploadRateLimit(infoHash, bytesPerSecond)
1578
+ end
1579
+
1580
+ # Set the upload ratio. Pass nil to disable
1581
+ def setUploadRatio(infoHash, ratio)
1582
+ raise "upload ratio must be Numeric, not a #{ratio.class}" if ratio && ! ratio.is_a?(Numeric)
1583
+ @handler.setUploadRatio(infoHash, ratio)
1584
+ end
1585
+
1586
+ # Remove a currently running torrent
1587
+ def removeTorrent(infoHash, deleteFiles = false)
1588
+ @handler.removeTorrent(infoHash, deleteFiles)
1589
+ end
1590
+
1591
+ private
1592
+ # Helper method for adding a torrent.
1593
+ def addTorrent(trackerclient, infoHash, info, magnet = nil)
1594
+ trackerclient.port = @port
1595
+
1596
+ torrentData = @handler.addTrackerClient(infoHash, info, trackerclient)
1597
+ torrentData.magnet = magnet
1598
+
1599
+ trackerclient.dynamicRequestParamsBuilder = Proc.new do
1600
+ torrentData = @handler.torrentData[infoHash]
1601
+ dataLength = (info ? info.dataLength : nil)
1602
+ result = TrackerDynamicRequestParams.new(dataLength)
1603
+ if torrentData && torrentData.blockState
1604
+ result.left = torrentData.blockState.totalLength - torrentData.blockState.completedLength
1605
+ result.downloaded = torrentData.bytesDownloaded
1606
+ result.uploaded = torrentData.bytesUploaded
1607
+ end
1608
+ result
1609
+ end
1610
+
1611
+ # If we haven't started yet then add this trackerclient to a queue of
1612
+ # trackerclients to start once we are started. If we start too soon we
1613
+ # will connect to the tracker, and it will try to connect back to us before we are listening.
1614
+ if ! trackerclient.started?
1615
+ if @stopped
1616
+ @toStart.push trackerclient
1617
+ else
1618
+ trackerclient.start
1619
+ end
1620
+ end
1621
+
1622
+ torrentData.infoHash
1623
+ end
1624
+
1625
+ end
1626
+ end
1627
+