quartz_torrent 0.0.1

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