quartz_torrent 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +2 -0
- data/lib/quartz_torrent/bitfield.rb +11 -0
- data/lib/quartz_torrent/extension.rb +7 -3
- data/lib/quartz_torrent/formatter.rb +3 -0
- data/lib/quartz_torrent/magnet.rb +1 -0
- data/lib/quartz_torrent/metainfopiecestate.rb +10 -0
- data/lib/quartz_torrent/peer.rb +4 -0
- data/lib/quartz_torrent/peerclient.rb +37 -8
- data/lib/quartz_torrent/peerholder.rb +10 -1
- data/lib/quartz_torrent/peermsg.rb +4 -0
- data/lib/quartz_torrent/reactor.rb +66 -30
- data/lib/quartz_torrent/regionmap.rb +4 -0
- data/lib/quartz_torrent/trackerclient.rb +17 -2
- metadata +2 -1
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Jeffrey A. Williams
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
QuartzTorrent -- A Ruby Bittorrent Library
|
2
2
|
==========================================
|
3
3
|
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/quartz_torrent.png)](http://badge.fury.io/rb/quartz\_torrent)
|
5
|
+
|
4
6
|
Like the title says, a bittorrent library implemented in pure ruby. Currently
|
5
7
|
the library works, but is still alpha.
|
6
8
|
|
@@ -52,6 +52,17 @@ puts "]"
|
|
52
52
|
# Length of the Bitfield in bits.
|
53
53
|
attr_reader :length
|
54
54
|
|
55
|
+
# Adjust the length of the bitfield. This might be necessary if we had to unserialize the bitfield from
|
56
|
+
# a serialized array of bytes.
|
57
|
+
def length=(l)
|
58
|
+
byteLen = 0
|
59
|
+
byteLen = (l-1)/8+1 if l > 0
|
60
|
+
|
61
|
+
raise "Length adjustment would change size of underlying array" if byteLen != byteLength
|
62
|
+
|
63
|
+
@length = l
|
64
|
+
end
|
65
|
+
|
55
66
|
|
56
67
|
# Length of the Bitfield in bytes.
|
57
68
|
def byteLength
|
@@ -1,13 +1,15 @@
|
|
1
1
|
require 'quartz_torrent/peermsg'
|
2
2
|
module QuartzTorrent
|
3
|
-
# This class contains constants that represent our numbering of the Bittorrent extensions we support.
|
3
|
+
# This class contains constants that represent our numbering of the Bittorrent peer-protocol extensions we support.
|
4
4
|
# It also has some utility methods related to extensions.
|
5
5
|
class Extension
|
6
6
|
|
7
|
+
# The metadata extension (BEP 9)
|
7
8
|
MetadataExtensionId = 1
|
8
9
|
|
9
|
-
#
|
10
|
-
#
|
10
|
+
# Create an ExtendedHandshake object based on the passed Torrent metadata info struct.
|
11
|
+
# @param info The torrent metadata info struct. It is used to determine the size to send
|
12
|
+
# when negotiating the metadata extension.
|
11
13
|
def self.createExtendedHandshake(info)
|
12
14
|
msg = ExtendedHandshake.new
|
13
15
|
|
@@ -26,6 +28,8 @@ module QuartzTorrent
|
|
26
28
|
msg
|
27
29
|
end
|
28
30
|
|
31
|
+
# Get the class to use to serialize and unserialize the specified Bittorent extension. Returns nil if we don't support that extension.
|
32
|
+
# @param info The name of a bittorrent extension as specified in the BEP, for example 'ut_metadata'.
|
29
33
|
def self.peerMsgClassForExtensionName(info)
|
30
34
|
if info == 'ut_metadata'
|
31
35
|
ExtendedMetaInfo
|
@@ -2,8 +2,11 @@ module QuartzTorrent
|
|
2
2
|
# Class that can be used to format different quantities into
|
3
3
|
# human readable strings.
|
4
4
|
class Formatter
|
5
|
+
# Number of bytes in a Kilobyte.
|
5
6
|
Kb = 1024
|
7
|
+
# Number of bytes in a Megabyte.
|
6
8
|
Meg = 1024*Kb
|
9
|
+
# Number of bytes in a Gigabyte.
|
7
10
|
Gig = 1024*Meg
|
8
11
|
|
9
12
|
# Format a size in bytes.
|
@@ -118,6 +118,7 @@ module QuartzTorrent
|
|
118
118
|
num*BlockSize + extra
|
119
119
|
end
|
120
120
|
|
121
|
+
# Return true if the specified piece is completed. The piece is specified by index.
|
121
122
|
def pieceCompleted?(pieceIndex)
|
122
123
|
@completePieces.set? pieceIndex
|
123
124
|
end
|
@@ -132,12 +133,15 @@ module QuartzTorrent
|
|
132
133
|
raise "Metadata is not yet complete" if ! complete?
|
133
134
|
end
|
134
135
|
|
136
|
+
# Save the specified piece to disk asynchronously.
|
135
137
|
def savePiece(pieceIndex, data)
|
136
138
|
id = @pieceManager.writeBlock pieceIndex, 0, data
|
137
139
|
@pieceManagerRequests[id] = PieceManagerRequestMetadata.new(:write, pieceIndex)
|
138
140
|
id
|
139
141
|
end
|
140
142
|
|
143
|
+
# Read a piece from disk. This method is asynchronous; it returns a handle that can be later
|
144
|
+
# used to retreive the result.
|
141
145
|
def readPiece(pieceIndex)
|
142
146
|
length = BlockSize
|
143
147
|
length = @lastPieceLength if pieceIndex == @numPieces - 1
|
@@ -180,6 +184,7 @@ module QuartzTorrent
|
|
180
184
|
results
|
181
185
|
end
|
182
186
|
|
187
|
+
# Return a list of torrent pieces that can still be requested. These are pieces that are not completed and are not requested.
|
183
188
|
def findRequestablePieces
|
184
189
|
piecesRequired = []
|
185
190
|
|
@@ -192,6 +197,8 @@ module QuartzTorrent
|
|
192
197
|
piecesRequired
|
193
198
|
end
|
194
199
|
|
200
|
+
# Return a list of peers from whom we can request pieces. These are peers for whom we have an established connection, and
|
201
|
+
# are not marked as bad. See markPeerBad.
|
195
202
|
def findRequestablePeers(classifiedPeers)
|
196
203
|
result = []
|
197
204
|
|
@@ -213,6 +220,8 @@ module QuartzTorrent
|
|
213
220
|
end
|
214
221
|
end
|
215
222
|
|
223
|
+
# Mark the specified peer as 'bad'. We won't try requesting pieces from this peer. Used, for example, when
|
224
|
+
# a peer rejects our request for a metadata piece.
|
216
225
|
def markPeerBad(peer)
|
217
226
|
@badPeers.add peer
|
218
227
|
end
|
@@ -229,6 +238,7 @@ module QuartzTorrent
|
|
229
238
|
@pieceManager.wait
|
230
239
|
end
|
231
240
|
|
241
|
+
# Return the name of the file where this class will store the Torrent Info struct.
|
232
242
|
def self.generateInfoFileName(infoHash)
|
233
243
|
"#{QuartzTorrent.bytesToHex(infoHash)}.info"
|
234
244
|
end
|
data/lib/quartz_torrent/peer.rb
CHANGED
@@ -2,9 +2,11 @@ require 'quartz_torrent/rate'
|
|
2
2
|
require 'quartz_torrent/peermsgserialization'
|
3
3
|
|
4
4
|
module QuartzTorrent
|
5
|
+
# This class represents a torrent peer.
|
5
6
|
class Peer
|
6
7
|
@@stateChangeListeners = []
|
7
8
|
|
9
|
+
# Create a new Peer using the information from the passed TrackerPeer object.
|
8
10
|
def initialize(trackerPeer)
|
9
11
|
@trackerPeer = trackerPeer
|
10
12
|
@amChoked = true
|
@@ -86,6 +88,7 @@ module QuartzTorrent
|
|
86
88
|
# A PeerWireMessageSerializer that can unserialize and serialize messages to and from this peer.
|
87
89
|
attr_accessor :peerMsgSerializer
|
88
90
|
|
91
|
+
# Return a string representation of the peer.
|
89
92
|
def to_s
|
90
93
|
@trackerPeer.to_s
|
91
94
|
end
|
@@ -95,6 +98,7 @@ module QuartzTorrent
|
|
95
98
|
@@stateChangeListeners.push l
|
96
99
|
end
|
97
100
|
|
101
|
+
# Equate peers.
|
98
102
|
def eql?(o)
|
99
103
|
o.is_a?(Peer) && trackerPeer.eql?(o.trackerPeer)
|
100
104
|
end
|
@@ -4,7 +4,6 @@ require "quartz_torrent/peermsg.rb"
|
|
4
4
|
require "quartz_torrent/reactor.rb"
|
5
5
|
require "quartz_torrent/util.rb"
|
6
6
|
require "quartz_torrent/classifiedpeers.rb"
|
7
|
-
require "quartz_torrent/classifiedpeers.rb"
|
8
7
|
require "quartz_torrent/peerholder.rb"
|
9
8
|
require "quartz_torrent/peermanager.rb"
|
10
9
|
require "quartz_torrent/blockstate.rb"
|
@@ -38,6 +37,8 @@ module QuartzTorrent
|
|
38
37
|
@peerManager = PeerManager.new
|
39
38
|
@pieceManagerRequestMetadata = {}
|
40
39
|
@pieceManagerMetainfoRequestMetadata = {}
|
40
|
+
@bytesDownloadedDataOnly = 0
|
41
|
+
@bytesUploadedDataOnly = 0
|
41
42
|
@bytesDownloaded = 0
|
42
43
|
@bytesUploaded = 0
|
43
44
|
@magnet = nil
|
@@ -72,6 +73,8 @@ module QuartzTorrent
|
|
72
73
|
# Metadata associated with outstanding requests to the PieceManager responsible for the pieces of the torrent metainfo.
|
73
74
|
attr_accessor :pieceManagerMetainfoRequestMetadata
|
74
75
|
attr_accessor :peerChangeListener
|
76
|
+
attr_accessor :bytesDownloadedDataOnly
|
77
|
+
attr_accessor :bytesUploadedDataOnly
|
75
78
|
attr_accessor :bytesDownloaded
|
76
79
|
attr_accessor :bytesUploaded
|
77
80
|
attr_accessor :state
|
@@ -143,6 +146,10 @@ module QuartzTorrent
|
|
143
146
|
# After we have completed downloading a torrent, we will continue to upload until we have
|
144
147
|
# uploaded ratio * torrent_size bytes. If nil, no limit on upload.
|
145
148
|
attr_accessor :ratio
|
149
|
+
attr_accessor :bytesUploadedDataOnly
|
150
|
+
attr_accessor :bytesDownloadedDataOnly
|
151
|
+
attr_accessor :bytesUploaded
|
152
|
+
attr_accessor :bytesDownloaded
|
146
153
|
|
147
154
|
# Update the data in this TorrentDataDelegate from the torrentData
|
148
155
|
# object that it was created from. TODO: What if that torrentData is now gone?
|
@@ -160,6 +167,8 @@ module QuartzTorrent
|
|
160
167
|
def fillFrom(torrentData)
|
161
168
|
@infoHash = torrentData.infoHash
|
162
169
|
@info = torrentData.info
|
170
|
+
@bytesUploadedDataOnly = torrentData.bytesUploadedDataOnly
|
171
|
+
@bytesDownloadedDataOnly = torrentData.bytesDownloadedDataOnly
|
163
172
|
@bytesUploaded = torrentData.bytesUploaded
|
164
173
|
@bytesDownloaded = torrentData.bytesDownloaded
|
165
174
|
|
@@ -296,7 +305,7 @@ module QuartzTorrent
|
|
296
305
|
@logger.warn "Asked to set download rate limit for a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
|
297
306
|
return
|
298
307
|
end
|
299
|
-
|
308
|
+
|
300
309
|
if bytesPerSecond
|
301
310
|
if ! torrentData.downRateLimit
|
302
311
|
torrentData.downRateLimit = RateLimit.new(bytesPerSecond, 2*bytesPerSecond, 0)
|
@@ -306,6 +315,13 @@ module QuartzTorrent
|
|
306
315
|
else
|
307
316
|
torrentData.downRateLimit = nil
|
308
317
|
end
|
318
|
+
|
319
|
+
torrentData.peers.all.each do |peer|
|
320
|
+
withPeersIo(peer, "setting download rate limit") do |io|
|
321
|
+
io.readRateLimit = torrentData.downRateLimit
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
309
325
|
end
|
310
326
|
|
311
327
|
# Set the upload rate limit. Pass nil as the bytesPerSecond to disable the limit.
|
@@ -325,6 +341,12 @@ module QuartzTorrent
|
|
325
341
|
else
|
326
342
|
torrentData.upRateLimit = nil
|
327
343
|
end
|
344
|
+
|
345
|
+
torrentData.peers.all.each do |peer|
|
346
|
+
withPeersIo(peer, "setting upload rate limit") do |io|
|
347
|
+
io.writeRateLimit = torrentData.upRateLimit
|
348
|
+
end
|
349
|
+
end
|
328
350
|
end
|
329
351
|
|
330
352
|
# Set the upload ratio. Pass nil to disable
|
@@ -503,7 +525,10 @@ module QuartzTorrent
|
|
503
525
|
close
|
504
526
|
return
|
505
527
|
end
|
528
|
+
|
506
529
|
peer.updateUploadRate msg
|
530
|
+
torrentData = @torrentData[peer.infoHash]
|
531
|
+
torrentData.bytesDownloaded += msg.length if torrentData
|
507
532
|
@logger.debug "Peer #{peer} upload rate: #{peer.uploadRate.value} data only: #{peer.uploadRateDataOnly.value}"
|
508
533
|
end
|
509
534
|
|
@@ -801,7 +826,7 @@ module QuartzTorrent
|
|
801
826
|
end
|
802
827
|
|
803
828
|
if torrentData.state == :uploading && (torrentData.state != :paused) && torrentData.ratio
|
804
|
-
if torrentData.
|
829
|
+
if torrentData.bytesUploadedDataOnly >= torrentData.ratio*torrentData.blockState.totalLength
|
805
830
|
@logger.info "Pausing torrent due to upload ratio limit." if torrentData.metainfoPieceState.complete?
|
806
831
|
setPaused(infoHash, true)
|
807
832
|
return
|
@@ -978,7 +1003,7 @@ module QuartzTorrent
|
|
978
1003
|
peer.requestedBlocks.delete blockInfo.blockIndex
|
979
1004
|
# Block is marked as not requested when hash is confirmed
|
980
1005
|
|
981
|
-
torrentData.
|
1006
|
+
torrentData.bytesDownloadedDataOnly += msg.data.length
|
982
1007
|
id = torrentData.pieceManager.writeBlock(msg.pieceIndex, msg.blockOffset, msg.data)
|
983
1008
|
torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:write, msg)
|
984
1009
|
end
|
@@ -1011,6 +1036,7 @@ module QuartzTorrent
|
|
1011
1036
|
end
|
1012
1037
|
|
1013
1038
|
peer.bitfield = msg.bitfield
|
1039
|
+
peer.bitfield.length = torrentData.info.pieces.length
|
1014
1040
|
|
1015
1041
|
if ! torrentData.blockState
|
1016
1042
|
@logger.warn "Bitfield: no blockstate yet."
|
@@ -1100,8 +1126,9 @@ module QuartzTorrent
|
|
1100
1126
|
msg.pieceIndex = readRequestMetadata.requestMsg.pieceIndex
|
1101
1127
|
msg.blockOffset = readRequestMetadata.requestMsg.blockOffset
|
1102
1128
|
msg.data = result.data
|
1129
|
+
@logger.debug "Sending block to #{peer}: piece #{msg.pieceIndex} offset #{msg.blockOffset} length #{msg.data.length}"
|
1103
1130
|
sendMessageToPeer msg, io, peer
|
1104
|
-
torrentData.
|
1131
|
+
torrentData.bytesUploadedDataOnly += msg.data.length
|
1105
1132
|
@logger.debug "Sending piece to peer"
|
1106
1133
|
end
|
1107
1134
|
else
|
@@ -1313,12 +1340,14 @@ module QuartzTorrent
|
|
1313
1340
|
|
1314
1341
|
def sendMessageToPeer(msg, io, peer)
|
1315
1342
|
peer.updateDownloadRate(msg)
|
1343
|
+
torrentData = @torrentData[peer.infoHash]
|
1344
|
+
torrentData.bytesUploaded += msg.length if torrentData
|
1345
|
+
|
1316
1346
|
begin
|
1317
1347
|
peer.peerMsgSerializer.serializeTo(msg, io)
|
1318
1348
|
rescue
|
1319
1349
|
@logger.warn "Sending message to peer #{peer} failed: #{$!.message}"
|
1320
1350
|
end
|
1321
|
-
msg.serializeTo io
|
1322
1351
|
end
|
1323
1352
|
|
1324
1353
|
# Update our internal peer list for this torrent from the tracker client
|
@@ -1602,8 +1631,8 @@ module QuartzTorrent
|
|
1602
1631
|
result = TrackerDynamicRequestParams.new(dataLength)
|
1603
1632
|
if torrentData && torrentData.blockState
|
1604
1633
|
result.left = torrentData.blockState.totalLength - torrentData.blockState.completedLength
|
1605
|
-
result.downloaded = torrentData.
|
1606
|
-
result.uploaded = torrentData.
|
1634
|
+
result.downloaded = torrentData.bytesDownloadedDataOnly
|
1635
|
+
result.uploaded = torrentData.bytesUploadedDataOnly
|
1607
1636
|
end
|
1608
1637
|
result
|
1609
1638
|
end
|
@@ -2,6 +2,7 @@ require 'quartz_torrent/peer.rb'
|
|
2
2
|
require 'quartz_torrent/util'
|
3
3
|
|
4
4
|
module QuartzTorrent
|
5
|
+
# A container class for holding torrent peers. Allows lookup by different properties.
|
5
6
|
class PeerHolder
|
6
7
|
def initialize
|
7
8
|
@peersById = {}
|
@@ -10,20 +11,24 @@ module QuartzTorrent
|
|
10
11
|
@log = LogManager.getLogger("peerholder")
|
11
12
|
end
|
12
13
|
|
14
|
+
# Find a peer by its trackerpeer's peerid. This is the id returned by the tracker, and may be nil.
|
13
15
|
def findById(peerId)
|
14
16
|
@peersById[peerId]
|
15
17
|
end
|
16
18
|
|
19
|
+
# Find a peer by its IP address and port.
|
17
20
|
def findByAddr(ip, port)
|
18
21
|
@peersByAddr[ip + port.to_s]
|
19
22
|
end
|
20
23
|
|
24
|
+
# Find all peers related to the torrent with the passed infoHash.
|
21
25
|
def findByInfoHash(infoHash)
|
22
26
|
l = @peersByInfoHash[infoHash]
|
23
27
|
l = [] if ! l
|
24
28
|
l
|
25
29
|
end
|
26
30
|
|
31
|
+
# Add a peer to the PeerHolder.
|
27
32
|
def add(peer)
|
28
33
|
raise "Peer must have it's infoHash set." if ! peer.infoHash
|
29
34
|
|
@@ -44,7 +49,7 @@ module QuartzTorrent
|
|
44
49
|
@peersByInfoHash.pushToList(peer.infoHash, peer)
|
45
50
|
end
|
46
51
|
|
47
|
-
# This peer, which previously had no id, has finished handshaking and now has an ID.
|
52
|
+
# Set the id for a peer. This peer, which previously had no id, has finished handshaking and now has an ID.
|
48
53
|
def idSet(peer)
|
49
54
|
@peersById.each do |e|
|
50
55
|
return if e.eql?(peer)
|
@@ -52,6 +57,7 @@ module QuartzTorrent
|
|
52
57
|
@peersById.pushToList(peer.trackerPeer.id, peer)
|
53
58
|
end
|
54
59
|
|
60
|
+
# Delete the specified peer from the PeerHolder.
|
55
61
|
def delete(peer)
|
56
62
|
@peersByAddr.delete byAddrKey(peer)
|
57
63
|
|
@@ -82,14 +88,17 @@ module QuartzTorrent
|
|
82
88
|
end
|
83
89
|
end
|
84
90
|
|
91
|
+
# Return the list of all peers.
|
85
92
|
def all
|
86
93
|
@peersByAddr.values
|
87
94
|
end
|
88
95
|
|
96
|
+
# Return the number of peers in the holder.
|
89
97
|
def size
|
90
98
|
@peersByAddr.size
|
91
99
|
end
|
92
100
|
|
101
|
+
# Output a string representation of the PeerHolder, for debugging purposes.
|
93
102
|
def to_s(infoHash = nil)
|
94
103
|
def makeFlags(peer)
|
95
104
|
s = "["
|
@@ -87,12 +87,15 @@ module QuartzTorrent
|
|
87
87
|
MessageCancel = 8
|
88
88
|
MessageExtended = 20
|
89
89
|
|
90
|
+
# Create a new PeerWireMessage with the specified message id.
|
90
91
|
def initialize(messageId)
|
91
92
|
@messageId = messageId
|
92
93
|
end
|
93
94
|
|
95
|
+
# Get the message id of this message (the message type from the peer wire protocol).
|
94
96
|
attr_reader :messageId
|
95
97
|
|
98
|
+
# Serialize this message to the passed io
|
96
99
|
def serializeTo(io)
|
97
100
|
io.write [payloadLength+1].pack("N")
|
98
101
|
io.write [@messageId].pack("C")
|
@@ -108,6 +111,7 @@ module QuartzTorrent
|
|
108
111
|
payloadLength + 5
|
109
112
|
end
|
110
113
|
|
114
|
+
# Unserialize the message from the passed string.
|
111
115
|
def unserialize(payload)
|
112
116
|
raise "Subclasses of PeerWireMessage must implement unserialize but #{self.class} didn't"
|
113
117
|
end
|
@@ -21,7 +21,7 @@ module QuartzTorrent
|
|
21
21
|
def serverInit(metainfo, addr, port)
|
22
22
|
end
|
23
23
|
|
24
|
-
# Event handler
|
24
|
+
# Event handler: The current io is ready for reading.
|
25
25
|
# If you will write to the same io from both this handler and the timerExpired handler,
|
26
26
|
# you must make sure to perform all writing at once in this handler. If not then
|
27
27
|
# the writes from the timer handler may be interleaved.
|
@@ -43,19 +43,22 @@ module QuartzTorrent
|
|
43
43
|
def recvData(metainfo)
|
44
44
|
end
|
45
45
|
|
46
|
-
# Event handler
|
46
|
+
# Event handler: a timer has expired.
|
47
|
+
# @param metainfo The metainfo associated with the timer, that was passed to scheduleTimer.
|
47
48
|
def timerExpired(metainfo)
|
48
49
|
end
|
49
50
|
|
50
|
-
# Event handler
|
51
|
+
# Event handler: an error occurred during read or write. Connection errors are reported separately in connectError
|
52
|
+
# @param metainfo The metainfo associated with the io.
|
51
53
|
def error(metainfo, details)
|
52
54
|
end
|
53
55
|
|
54
|
-
# Event handler
|
56
|
+
# Event handler: an error occurred during connection, or connection timed out.
|
57
|
+
# @param metainfo The metainfo associated with the io, as passed to the connect method.
|
55
58
|
def connectError(metainfo, details)
|
56
59
|
end
|
57
60
|
|
58
|
-
# Event handler
|
61
|
+
# Event handler: this is called for events added using addUserEvent to the reactor.
|
59
62
|
def userEvent(event)
|
60
63
|
end
|
61
64
|
|
@@ -63,6 +66,12 @@ module QuartzTorrent
|
|
63
66
|
attr_accessor :reactor
|
64
67
|
|
65
68
|
# Schedule a timer.
|
69
|
+
# @param duration The duration of the timer in seconds
|
70
|
+
# @param metainfo The metainfo to associate with the timer
|
71
|
+
# @param recurring If true when the timer duration expires, the timer will be rescheduled. If false the timer
|
72
|
+
# will not be rescheduled.
|
73
|
+
# @param immed If true then the timer will expire immediately (the next pass through the event loop). If the timer
|
74
|
+
# is also recurring it will then be rescheduled according to it's duratoin.
|
66
75
|
def scheduleTimer(duration, metainfo = nil, recurring = true, immed = false)
|
67
76
|
@reactor.scheduleTimer(duration, metainfo, recurring, immed) if @reactor
|
68
77
|
end
|
@@ -73,48 +82,52 @@ module QuartzTorrent
|
|
73
82
|
@reactor.cancelTimer(timerInfo) if @reactor
|
74
83
|
end
|
75
84
|
|
85
|
+
# Create a TCP connection to the specified host and port. Associate the passed metainfo with the IO representing the connection.
|
76
86
|
def connect(addr, port, metainfo, timeout = nil)
|
77
87
|
@reactor.connect(addr, port, metainfo, timeout) if @reactor
|
78
88
|
end
|
79
89
|
|
80
|
-
# Write data to the current io
|
90
|
+
# Write data to the current io.
|
81
91
|
def write(data)
|
82
92
|
@reactor.write(data) if @reactor
|
83
93
|
end
|
84
94
|
|
85
|
-
# Read len bytes from the current io
|
95
|
+
# Read len bytes from the current io. This is meant to be called from one of the event handler methods.
|
86
96
|
def read(len)
|
87
97
|
result = ''
|
88
98
|
result = @reactor.read(len) if @reactor
|
89
99
|
result
|
90
100
|
end
|
91
101
|
|
92
|
-
# Shutdown the reactor
|
102
|
+
# Shutdown the reactor.
|
93
103
|
def stopReactor
|
94
104
|
@reactor.stop if @reactor
|
95
105
|
end
|
96
106
|
|
97
|
-
# Check if stop has been called on the reactor
|
107
|
+
# Check if stop has been called on the reactor.
|
98
108
|
def stopped?
|
99
109
|
@stopped
|
100
110
|
end
|
101
111
|
|
102
|
-
# Close the current io
|
112
|
+
# Close the current io. This is meant to be called from one of the event handler methods.
|
103
113
|
def close(io = nil)
|
104
114
|
@reactor.close(io) if @reactor
|
105
115
|
end
|
106
116
|
|
117
|
+
# Return the current IO object. This is meant to be called from one of the event handler methods.
|
118
|
+
# The returned object is actually an IoFacade, a wrapper around the IO object.
|
107
119
|
def currentIo
|
108
120
|
result = nil
|
109
121
|
result = @reactor.currentIo if @reactor
|
110
122
|
result
|
111
123
|
end
|
112
124
|
|
113
|
-
# Find an io by metainfo
|
125
|
+
# Find an io by metainfo.
|
114
126
|
def findIoByMetainfo(metainfo)
|
115
127
|
@reactor.findIoByMetainfo metainfo if metainfo && @reactor
|
116
128
|
end
|
117
|
-
|
129
|
+
|
130
|
+
# Set the metainfo for the current io. This is meant to be called from one of the event handler methods.
|
118
131
|
def setMetaInfo(metainfo)
|
119
132
|
@reactor.setMetaInfo metainfo if @reactor
|
120
133
|
end
|
@@ -130,6 +143,8 @@ module QuartzTorrent
|
|
130
143
|
end
|
131
144
|
end
|
132
145
|
|
146
|
+
# Simple class used to buffer output for an IO until it's ready for writing. This is not part of the
|
147
|
+
# public API; it's used internally by the IOInfo class.
|
133
148
|
class OutputBuffer
|
134
149
|
# Create a new OutputBuffer for the specified IO. The parameter seekable should be
|
135
150
|
# true or false. If true, then this output buffer will support seek
|
@@ -144,14 +159,17 @@ module QuartzTorrent
|
|
144
159
|
end
|
145
160
|
end
|
146
161
|
|
162
|
+
# Is the buffer empty?
|
147
163
|
def empty?
|
148
164
|
@buffer.length == 0
|
149
165
|
end
|
150
166
|
|
167
|
+
# Number of bytes in the buffer.
|
151
168
|
def size
|
152
169
|
@buffer.length
|
153
170
|
end
|
154
171
|
|
172
|
+
# Append data to the buffer.
|
155
173
|
def append(data)
|
156
174
|
if ! @seekable
|
157
175
|
@buffer << data
|
@@ -208,6 +226,7 @@ module QuartzTorrent
|
|
208
226
|
hash.delete @io
|
209
227
|
end
|
210
228
|
|
229
|
+
# Read `length` bytes.
|
211
230
|
def read(length)
|
212
231
|
data = ''
|
213
232
|
while data.length < length
|
@@ -249,6 +268,7 @@ module QuartzTorrent
|
|
249
268
|
data
|
250
269
|
end
|
251
270
|
|
271
|
+
# Write data to the IO.
|
252
272
|
def write(data)
|
253
273
|
# Issue: what about write, read, read on files opened for read/write? Write should happen at offset X, but reads moved to offset N. Since writes
|
254
274
|
# are buffered, write may happen after read which means write will happen at the wrong offset N. Can fix by always seeking (if needed) before writes to the
|
@@ -259,37 +279,61 @@ module QuartzTorrent
|
|
259
279
|
data.length
|
260
280
|
end
|
261
281
|
|
282
|
+
# Seek on the io.
|
283
|
+
# @param amount amount to seek.
|
284
|
+
# @param whence one of the whence constants from IO::seek.
|
262
285
|
def seek(amount, whence)
|
263
286
|
@io.seek amount, whence if @ioInfo.seekable?
|
264
287
|
end
|
265
288
|
|
289
|
+
# Flush data.
|
266
290
|
def flush
|
267
291
|
@io.flush
|
268
292
|
end
|
269
293
|
|
294
|
+
# Close the io.
|
270
295
|
def close
|
271
296
|
@io.close
|
272
297
|
end
|
273
298
|
|
299
|
+
# Check if the io is closed.
|
274
300
|
def closed?
|
275
301
|
@io.closed?
|
276
302
|
end
|
303
|
+
|
304
|
+
def readRateLimit=(rate)
|
305
|
+
raise "The argument must be a RateLimit" if ! rate.nil? && ! rate.is_a?(RateLimit)
|
306
|
+
@ioInfo.readRateLimit = rate
|
307
|
+
end
|
308
|
+
|
309
|
+
def writeRateLimit=(rate)
|
310
|
+
raise "The argument must be a RateLimit" if ! rate.nil? && !rate.is_a?(RateLimit)
|
311
|
+
@ioInfo.writeRateLimit = rate
|
312
|
+
end
|
313
|
+
|
314
|
+
attr_accessor :writeRateLimit
|
277
315
|
end
|
278
316
|
|
279
|
-
# An IoFacade that doesn't allow reading.
|
317
|
+
# An IoFacade that doesn't allow reading. This is not part of the public API.
|
280
318
|
class WriteOnlyIoFacade < IoFacade
|
319
|
+
# Create a new WriteOnlyIoFacade that delegates to the passed IOInfo object.
|
281
320
|
def initialize(ioInfo, logger = nil, readError = "Reading is not allowed for this IO")
|
282
321
|
super(ioInfo, logger)
|
283
322
|
@readError = readError
|
284
323
|
end
|
285
324
|
|
325
|
+
# Raise an exception.
|
286
326
|
def read(length)
|
287
327
|
raise @readError
|
288
328
|
end
|
289
329
|
end
|
290
330
|
|
291
|
-
# An IO and associated meta-information used by the Reactor.
|
331
|
+
# An IO and associated meta-information used by the Reactor. This is not part of the public API.
|
292
332
|
class IOInfo
|
333
|
+
# Create a new IOInfo object that operates on the passed IO object.
|
334
|
+
# @param io An IO object
|
335
|
+
# @param metainfo The metainfo to associate with the IO.
|
336
|
+
# @param seekable Whether the IO is seekable or not.
|
293
337
|
def initialize(io, metainfo, seekable = false)
|
294
338
|
@io = io
|
295
339
|
@metainfo = metainfo
|
@@ -315,6 +359,8 @@ module QuartzTorrent
|
|
315
359
|
attr_accessor :useErrorhandler
|
316
360
|
attr_accessor :readRateLimit
|
317
361
|
attr_accessor :writeRateLimit
|
362
|
+
|
363
|
+
# Is the IO seekable.
|
318
364
|
def seekable?
|
319
365
|
@seekable
|
320
366
|
end
|
@@ -583,13 +629,15 @@ module QuartzTorrent
|
|
583
629
|
end
|
584
630
|
|
585
631
|
# Meant to be called from the handler. Sets the max rate at which the current io can read.
|
586
|
-
def setReadRateLimit(
|
587
|
-
|
632
|
+
def setReadRateLimit(rate)
|
633
|
+
raise "The argument must be a RateLimit" if ! rate.nil? && !rate.is_a?(RateLimit)
|
634
|
+
@currentIoInfo.readRateLimit = rate
|
588
635
|
end
|
589
636
|
|
590
637
|
# Meant to be called from the handler. Sets the max rate at which the current io can be written to.
|
591
|
-
def setWriteRateLimit(
|
592
|
-
|
638
|
+
def setWriteRateLimit(rate)
|
639
|
+
raise "The argument must be a RateLimit" if ! rate.nil? && !rate.is_a?(RateLimit)
|
640
|
+
@currentIoInfo.writeRateLimit = rate
|
593
641
|
end
|
594
642
|
|
595
643
|
# Meant to be called from the handler. Find an IO by metainfo. The == operator is used to
|
@@ -653,18 +701,6 @@ module QuartzTorrent
|
|
653
701
|
rescue
|
654
702
|
# Exception occurred. Probably EINTR.
|
655
703
|
@logger.warn "Select raised exception; will retry. Reason: #{$!}" if @logger
|
656
|
-
@logger.warn "IOs at time of failure:"
|
657
|
-
@logger.warn "Readset:"
|
658
|
-
readset.each do |io|
|
659
|
-
ioInfo = @ioInfo[io]
|
660
|
-
puts " #{ioInfo.metainfo}: #{(io.closed?) ? "closed" : "not closed"}"
|
661
|
-
end
|
662
|
-
@logger.warn "Writeset:"
|
663
|
-
writeset.each do |io|
|
664
|
-
ioInfo = @ioInfo[io]
|
665
|
-
puts " #{ioInfo.metainfo}: #{(io.closed?) ? "closed" : "not closed"}"
|
666
|
-
end
|
667
|
-
|
668
704
|
end
|
669
705
|
end
|
670
706
|
|
@@ -1,4 +1,6 @@
|
|
1
1
|
class Array
|
2
|
+
# Perform a binary search for a value in the array between the index low and high. This method expects a block. The
|
3
|
+
# block is passed a value v, and should return true if the target value is >= v, and false otherwise.
|
2
4
|
def binsearch(low = nil, high = nil)
|
3
5
|
return nil if length == 0
|
4
6
|
result = binsearch_index(low, high){ |x| yield x if !x.nil?}
|
@@ -6,6 +8,8 @@ class Array
|
|
6
8
|
result
|
7
9
|
end
|
8
10
|
|
11
|
+
# Perform a binary search for an index in the array between the index low and high. This method expects a block. The
|
12
|
+
# block is passed a value v, and should return true if the target value is >= v, and false otherwise.
|
9
13
|
def binsearch_index(low = nil, high = nil)
|
10
14
|
return nil if length == 0
|
11
15
|
low = 0 if !low
|
@@ -35,17 +35,21 @@ module QuartzTorrent
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
+
# Hash code of this TrackerPeer.
|
38
39
|
def hash
|
39
40
|
@hash
|
40
41
|
end
|
41
42
|
|
43
|
+
# Equate to another TrackerPeer.
|
42
44
|
def eql?(o)
|
43
45
|
o.ip == @ip && o.port == @port
|
44
46
|
end
|
45
47
|
|
46
|
-
#
|
48
|
+
# IP address, a string in dotted-quad notation
|
47
49
|
attr_accessor :ip
|
50
|
+
# TCP port
|
48
51
|
attr_accessor :port
|
52
|
+
# Peer Id. This may be nil.
|
49
53
|
attr_accessor :id
|
50
54
|
|
51
55
|
def to_s
|
@@ -64,8 +68,11 @@ module QuartzTorrent
|
|
64
68
|
@left = 0
|
65
69
|
end
|
66
70
|
end
|
71
|
+
# Number of bytes uploaded
|
67
72
|
attr_accessor :uploaded
|
73
|
+
# Number of bytes downloaded
|
68
74
|
attr_accessor :downloaded
|
75
|
+
# Number of bytes left to download before torrent is completed
|
69
76
|
attr_accessor :left
|
70
77
|
end
|
71
78
|
|
@@ -98,6 +105,9 @@ module QuartzTorrent
|
|
98
105
|
class TrackerClient
|
99
106
|
include QuartzTorrent
|
100
107
|
|
108
|
+
# Create a new TrackerClient
|
109
|
+
# @param announceUrl The announce URL of the tracker
|
110
|
+
# @param infoHash The infoHash of the torrent we're tracking
|
101
111
|
def initialize(announceUrl, infoHash, dataLength = 0, maxErrors = 20)
|
102
112
|
@peerId = "-QR0001-" # Azureus style
|
103
113
|
@peerId << Process.pid.to_s
|
@@ -126,10 +136,13 @@ module QuartzTorrent
|
|
126
136
|
# with up-to-date information.
|
127
137
|
attr_accessor :dynamicRequestParamsBuilder
|
128
138
|
|
139
|
+
# Return true if this TrackerClient is started, false otherwise.
|
129
140
|
def started?
|
130
141
|
@started
|
131
142
|
end
|
132
143
|
|
144
|
+
# Return the list of peers that the TrackerClient knows about. This list grows over time
|
145
|
+
# as more peers are reported from the tracker.
|
133
146
|
def peers
|
134
147
|
result = nil
|
135
148
|
@peersMutex.synchronize do
|
@@ -153,7 +166,8 @@ module QuartzTorrent
|
|
153
166
|
@errors
|
154
167
|
end
|
155
168
|
|
156
|
-
# Create a new TrackerClient using the passed information.
|
169
|
+
# Create a new TrackerClient using the passed information. This is a factory method that will return
|
170
|
+
# a tracker that talks the protocol specified in the URL.
|
157
171
|
def self.create(announceUrl, infoHash, dataLength = 0, start = true)
|
158
172
|
result = nil
|
159
173
|
if announceUrl =~ /udp:\/\//
|
@@ -166,6 +180,7 @@ module QuartzTorrent
|
|
166
180
|
result
|
167
181
|
end
|
168
182
|
|
183
|
+
# Create a new TrackerClient using the passed Metainfo object.
|
169
184
|
def self.createFromMetainfo(metainfo, start = true)
|
170
185
|
create(metainfo.announce, metainfo.infoHash, metainfo.info.dataLength, start)
|
171
186
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quartz_torrent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -164,6 +164,7 @@ files:
|
|
164
164
|
- lib/quartz_torrent/magnet.rb
|
165
165
|
- lib/quartz_torrent/udptrackerclient.rb
|
166
166
|
- README.md
|
167
|
+
- LICENSE
|
167
168
|
- .yardopts
|
168
169
|
- bin/quartztorrent_download
|
169
170
|
- bin/quartztorrent_download_curses
|