quartz_torrent 0.0.2 → 0.0.3
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/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
|
+
[](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
|