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.
- data/bin/quartztorrent_download +127 -0
- data/bin/quartztorrent_download_curses +841 -0
- data/bin/quartztorrent_magnet_from_torrent +32 -0
- data/bin/quartztorrent_show_info +62 -0
- data/lib/quartz_torrent.rb +2 -0
- data/lib/quartz_torrent/bitfield.rb +314 -0
- data/lib/quartz_torrent/blockstate.rb +354 -0
- data/lib/quartz_torrent/classifiedpeers.rb +95 -0
- data/lib/quartz_torrent/extension.rb +37 -0
- data/lib/quartz_torrent/filemanager.rb +543 -0
- data/lib/quartz_torrent/formatter.rb +92 -0
- data/lib/quartz_torrent/httptrackerclient.rb +121 -0
- data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
- data/lib/quartz_torrent/log.rb +132 -0
- data/lib/quartz_torrent/magnet.rb +92 -0
- data/lib/quartz_torrent/memprofiler.rb +27 -0
- data/lib/quartz_torrent/metainfo.rb +221 -0
- data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
- data/lib/quartz_torrent/peer.rb +145 -0
- data/lib/quartz_torrent/peerclient.rb +1627 -0
- data/lib/quartz_torrent/peerholder.rb +123 -0
- data/lib/quartz_torrent/peermanager.rb +170 -0
- data/lib/quartz_torrent/peermsg.rb +502 -0
- data/lib/quartz_torrent/peermsgserialization.rb +102 -0
- data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
- data/lib/quartz_torrent/rate.rb +58 -0
- data/lib/quartz_torrent/ratelimit.rb +48 -0
- data/lib/quartz_torrent/reactor.rb +949 -0
- data/lib/quartz_torrent/regionmap.rb +124 -0
- data/lib/quartz_torrent/semaphore.rb +43 -0
- data/lib/quartz_torrent/trackerclient.rb +271 -0
- data/lib/quartz_torrent/udptrackerclient.rb +70 -0
- data/lib/quartz_torrent/udptrackermsg.rb +250 -0
- data/lib/quartz_torrent/util.rb +100 -0
- 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
|
+
|