quartz_torrent 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -16,6 +16,7 @@ Features:
16
16
  - Upload and download rate limiting
17
17
  - Upload ratio enforcement
18
18
  - Upload duration limit
19
+ - Torrent Queueing
19
20
 
20
21
  Requirements
21
22
  ------------
@@ -99,14 +100,10 @@ And a specific test case in a test using:
99
100
 
100
101
  To-Do
101
102
  -----
102
- - Start a peerclient with the full torrent. Connect with another peerclient and start downloading, then pause.
103
+ - Bug: Start a peerclient with the full torrent. Connect with another peerclient and start downloading, then pause.
103
104
  Finally unpause. The "server" peerclient will refuse the connections to re-establish because it believes the
104
105
  peer is already connected.
105
- - Improve CPU usage.
106
- - Implement endgame strategy and support Cancel messages.
107
- - Refactor Metadata.Info into it's own independent class.
108
106
  - Improve Documentation
109
- - In peerclient, prefix log messages with torrent infohash, or (truncated) torrent name
110
107
  - Implement uTP
111
108
  - <http://www.bittorrent.org/beps/bep_0029.html#packet-sizes>
112
109
  - <https://forum.utorrent.com/viewtopic.php?id=76640>
@@ -128,19 +128,16 @@ class SummaryScreen < Screen
128
128
  end
129
129
 
130
130
  private
131
- def summaryLine(state, paused, size, uploadRate, downloadRate, complete, total, progress)
131
+ def summaryLine(state, paused, queued, size, uploadRate, downloadRate, complete, total, progress)
132
132
  if state == :downloading_metainfo
133
133
  " %12s Rate: %6s | %6s Bytes: %4d/%4d Progress: %5s\n" % [state, uploadRate, downloadRate, complete, total, progress]
134
134
  else
135
135
  primaryState = state.to_s
136
136
  secondaryState = ""
137
- if state == :running && complete == total
138
- secondaryState = "(completed)"
139
- elsif (state == :running || state == :uploading) && paused
140
- secondaryState = "(paused)"
141
- end
137
+ secondaryState += "(paused)" if paused
138
+ secondaryState += "(queued)" if queued
142
139
  state = "#{primaryState} #{secondaryState}"
143
- " %12s %9s Rate: %6s | %6s Pieces: %4d/%4d Progress: %5s\n" % [state, size, uploadRate, downloadRate, complete, total, progress]
140
+ " %14s %9s Rate: %6s | %6s Pieces: %4d/%4d Progress: %5s\n" % [state, size, uploadRate, downloadRate, complete, total, progress]
144
141
  end
145
142
  end
146
143
 
@@ -189,6 +186,7 @@ class SummaryScreen < Screen
189
186
  display.push summaryLine(
190
187
  state,
191
188
  torrent.paused,
189
+ torrent.queued,
192
190
  Formatter.formatSize(dataLength),
193
191
  Formatter.formatSpeed(torrent.uploadRate),
194
192
  Formatter.formatSpeed(torrent.downloadRate),
@@ -348,7 +346,7 @@ class DebugScreen < Screen
348
346
  @profiler.trackClass QuartzTorrent::PieceManager::Result
349
347
  @profiler.trackClass QuartzTorrent::Formatter
350
348
  @profiler.trackClass QuartzTorrent::TrackerClient
351
- @profiler.trackClass QuartzTorrent::HttpTrackerClient
349
+ @profiler.trackClass QuartzTorrent::HttpTrackerDriver
352
350
  @profiler.trackClass QuartzTorrent::InterruptibleSleep
353
351
  @profiler.trackClass QuartzTorrent::LogManager
354
352
  @profiler.trackClass QuartzTorrent::Metainfo
@@ -392,7 +390,7 @@ class DebugScreen < Screen
392
390
  @profiler.trackClass QuartzTorrent::TrackerDynamicRequestParams
393
391
  @profiler.trackClass QuartzTorrent::TrackerResponse
394
392
  @profiler.trackClass QuartzTorrent::TrackerClient
395
- @profiler.trackClass QuartzTorrent::UdpTrackerClient
393
+ @profiler.trackClass QuartzTorrent::UdpTrackerDriver
396
394
  @profiler.trackClass QuartzTorrent::UdpTrackerMessage
397
395
  @profiler.trackClass QuartzTorrent::UdpTrackerRequest
398
396
  @profiler.trackClass QuartzTorrent::UdpTrackerResponse
@@ -605,17 +603,6 @@ def initializeLogging(file)
605
603
  setLogfile file
606
604
  setDefaultLevel :info
607
605
  end
608
- LogManager.setLevel "peer_manager", :debug
609
- LogManager.setLevel "tracker_client", :debug
610
- LogManager.setLevel "http_tracker_client", :debug
611
- LogManager.setLevel "udp_tracker_client", :debug
612
- LogManager.setLevel "peerclient", :debug
613
- LogManager.setLevel "peerclient.reactor", :info
614
- #LogManager.setLevel "peerclient.reactor", :debug
615
- LogManager.setLevel "blockstate", :debug
616
- LogManager.setLevel "piecemanager", :info
617
- LogManager.setLevel "peerholder", :debug
618
- #LogManager.setLevel "peermsg_serializer", :debug
619
606
  end
620
607
 
621
608
  def help
@@ -657,7 +644,9 @@ class Settings
657
644
  @uploadLimit = nil
658
645
  @downloadLimit = nil
659
646
  @uploadRatio = nil
660
- @logfile = "/tmp/download_torrent_curses.log"
647
+ @logfile = "/tmp/download_torrent_curses.pid#{Process.pid}_.log"
648
+ @maxIncomplete = 5
649
+ @maxActive = 10
661
650
  end
662
651
 
663
652
  attr_accessor :baseDirectory
@@ -667,6 +656,20 @@ class Settings
667
656
  attr_accessor :uploadRatio
668
657
  attr_accessor :logfile
669
658
  attr_accessor :debugTTY
659
+ attr_accessor :maxIncomplete
660
+ attr_accessor :maxActive
661
+
662
+ def validate
663
+ if @maxIncomplete > @maxActive
664
+ puts "Max number of incomplete torrents must be <= Max number of active torrents"
665
+ return false
666
+ end
667
+ if @maxActive <= 0 || @maxIncomplete <= 0
668
+ puts "Max number of incomplete torrents and max number of active torrents must both be > 0"
669
+ return false
670
+ end
671
+ true
672
+ end
670
673
  end
671
674
 
672
675
  class PeerClient
@@ -702,6 +705,8 @@ begin
702
705
  [ '--help', '-h', GetoptLong::NO_ARGUMENT],
703
706
  [ '--ratio', '-r', GetoptLong::REQUIRED_ARGUMENT],
704
707
  [ '--debug-tty', '-t', GetoptLong::REQUIRED_ARGUMENT],
708
+ [ '--queue-max-incomplete', '-i', GetoptLong::REQUIRED_ARGUMENT],
709
+ [ '--queue-max-active', '-a', GetoptLong::REQUIRED_ARGUMENT],
705
710
  )
706
711
 
707
712
  opts.each do |opt, arg|
@@ -720,14 +725,18 @@ begin
720
725
  $settings.uploadRatio = arg.to_f
721
726
  elsif opt == '--debug-tty'
722
727
  $log = File.open arg, "w"
728
+ elsif opt == '--queue-max-incomplete'
729
+ $settings.maxIncomplete = arg.to_i
730
+ elsif opt == '--queue-max-active'
731
+ $settings.maxActive = arg.to_i
723
732
  end
724
733
  end
734
+ exit 1 if ! $settings.validate
725
735
 
726
736
  torrents = ARGV
727
737
 
728
738
  $log = File.open("/dev/null","w") if $log == $stdout
729
739
 
730
-
731
740
  initializeCurses
732
741
  cursesInitialized = true
733
742
  initializeLogging($settings.logfile)
@@ -743,7 +752,7 @@ begin
743
752
  scrManager.add :add, AddScreen.new(Ncurses::stdscr)
744
753
  scrManager.set :summary
745
754
 
746
- peerclient = QuartzTorrent::PeerClient.new($settings.baseDirectory)
755
+ peerclient = QuartzTorrent::PeerClient.new($settings.baseDirectory, $settings.maxIncomplete, $settings.maxActive)
747
756
  peerclient.port = $settings.port
748
757
 
749
758
  torrents.each do |torrent|
@@ -769,7 +778,6 @@ begin
769
778
 
770
779
  RubyProf.start if $doProfiling
771
780
 
772
- #puts "Starting peer client"
773
781
  peerclient.start
774
782
 
775
783
  while running
@@ -29,7 +29,7 @@ end
29
29
  info = nil
30
30
  begin
31
31
  metainfo = Metainfo.createFromFile(torrent)
32
- puts "Info Hash: #{bytesToHex(metainfo.infoHash)}"
32
+ puts "Info Hash: #{QuartzTorrent.bytesToHex(metainfo.infoHash)}"
33
33
  puts "Announce: #{metainfo.announce}" if metainfo.announce
34
34
  if metainfo.announceList
35
35
  puts "Announce list: "
@@ -43,10 +43,7 @@ begin
43
43
  info = metainfo.info
44
44
  rescue
45
45
  # Doesn't seem to be a complete .torrent file. Maybe it is just the info part.
46
- File.open(torrent, "r") do |file|
47
- bencoded = file.read
48
- info = Metainfo::Info.createFromBdecode(bencoded.bdecode)
49
- end
46
+ info = info = Metainfo::Info.createFromBdecode(BEncode.load_file(torrent, {:ignore_trailing_junk => 1} ))
50
47
  end
51
48
 
52
49
  if info
@@ -0,0 +1,36 @@
1
+ module QuartzTorrent
2
+ class Alarm
3
+ def initialize(id, details)
4
+ @details = details
5
+ @time = Time.new
6
+ end
7
+
8
+ attr_accessor :id
9
+ attr_accessor :details
10
+ attr_accessor :time
11
+ end
12
+
13
+ class Alarms
14
+ def initialize
15
+ @alarms = {}
16
+ end
17
+
18
+ # Raise a new alarm, or overwrite the existing alarm with the same id if one exists.
19
+ def raise(alarm)
20
+ @alarms[alarm.id] = alarm
21
+ end
22
+
23
+ def clear(alarm)
24
+ if alarm.is_a?(Alarm)
25
+ @alarms.delete alarm.id
26
+ else
27
+ # Assume variable `alarm` is an id.
28
+ @alarms.delete alarm
29
+ end
30
+ end
31
+
32
+ def all
33
+ @alarms.values
34
+ end
35
+ end
36
+ end
@@ -222,6 +222,13 @@ module QuartzTorrent
222
222
  result
223
223
  end
224
224
 
225
+ # Get a bitfield representing the completed pieces.
226
+ def completeBlockBitfield
227
+ result = Bitfield.new(@numBlocks)
228
+ result.copyFrom(@completeBlocks)
229
+ result
230
+ end
231
+
225
232
  # Number of bytes we have downloaded and verified.
226
233
  def completedLength
227
234
  num = @completeBlocks.countSet
@@ -11,6 +11,7 @@ module QuartzTorrent
11
11
 
12
12
  # Format a size in bytes.
13
13
  def self.formatSize(size)
14
+ return nil if !size
14
15
  s = size.to_f
15
16
  if s >= Gig
16
17
  s = "%.2fGB" % (s / Gig)
@@ -26,17 +27,24 @@ module QuartzTorrent
26
27
 
27
28
  # Format a floating point number as a percentage with one decimal place.
28
29
  def self.formatPercent(frac)
30
+ return nil if ! frac
29
31
  s = "%.1f" % (frac.to_f*100)
30
32
  s + "%"
31
33
  end
32
34
 
33
35
  # Format a speed in bytes per second.
34
36
  def self.formatSpeed(s)
35
- Formatter.formatSize(s) + "/s"
37
+ size = Formatter.formatSize(s)
38
+ if size
39
+ size + "/s"
40
+ else
41
+ nil
42
+ end
36
43
  end
37
44
 
38
45
  # Format a duration of time in seconds.
39
46
  def self.formatTime(secs)
47
+ return nil if ! secs
40
48
  s = ""
41
49
  time = secs.to_i
42
50
  arr = []
@@ -68,6 +76,7 @@ module QuartzTorrent
68
76
 
69
77
  # Parse a size in the format '50 KB'
70
78
  def self.parseSize(size)
79
+ return nil if ! size
71
80
  if size =~ /(\d+(?:\.\d+)?)\s*([^\d]+)*/
72
81
  value = $1.to_f
73
82
  suffix = ($2 ? $2.downcase : nil)
@@ -1,13 +1,17 @@
1
1
  module QuartzTorrent
2
2
  class TrackerClient
3
3
  end
4
+ class TrackerDriver
5
+ end
4
6
 
5
- # A tracker client that uses the HTTP protocol. This is the classic BitTorrent tracker protocol.
6
- class HttpTrackerClient < TrackerClient
7
- def initialize(announceUrl, infoHash, dataLength)
8
- super(announceUrl, infoHash, dataLength)
7
+ # A tracker driver that uses the HTTP protocol. This is the classic BitTorrent tracker protocol.
8
+ class HttpTrackerDriver < TrackerDriver
9
+ def initialize(announceUrl, infoHash)
10
+ super()
9
11
  @startSent = false
10
12
  @logger = LogManager.getLogger("http_tracker_client")
13
+ @announceUrl = announceUrl
14
+ @infoHash = infoHash
11
15
  end
12
16
 
13
17
  # Request a list of peers from the tracker and return it as a TrackerResponse.
@@ -24,8 +28,8 @@ module QuartzTorrent
24
28
 
25
29
  params = {}
26
30
  params['info_hash'] = CGI.escape(@infoHash)
27
- params['peer_id'] = @peerId
28
- params['port'] = @port
31
+ params['peer_id'] = dynamicParams.peerId
32
+ params['port'] = dynamicParams.port
29
33
  params['uploaded'] = dynamicParams.uploaded.to_s
30
34
  params['downloaded'] = dynamicParams.downloaded.to_s
31
35
  params['left'] = dynamicParams.left.to_s
@@ -50,7 +54,11 @@ module QuartzTorrent
50
54
  end
51
55
  uri.query = query
52
56
 
53
- res = Net::HTTP.get_response(uri)
57
+ begin
58
+ res = Net::HTTP.get_response(uri)
59
+ rescue Timeout::Error
60
+ return TrackerResponse.new(false, "Tracker request timed out", [])
61
+ end
54
62
  @logger.debug "Tracker response code: #{res.code}"
55
63
  @logger.debug "Tracker response body: #{res.body}"
56
64
  result = buildTrackerResponse(res)
@@ -60,6 +60,16 @@ module QuartzTorrent
60
60
  end
61
61
  end
62
62
 
63
+ # Return an array of all tracker URLS in the magnet link.
64
+ def trackers
65
+ tr = @params['tr']
66
+ if tr
67
+ tr
68
+ else
69
+ []
70
+ end
71
+ end
72
+
63
73
  # Return the first display name found in the magnet link. Returns nil if the magnet has no display name.
64
74
  def displayName
65
75
  dn = @params['dn']
@@ -58,6 +58,7 @@ module QuartzTorrent
58
58
  # multi-file download. For a single-file download the
59
59
  @files = []
60
60
  @logger = LogManager.getLogger("metainfo")
61
+ @source = nil
61
62
  end
62
63
 
63
64
  # Array of FileInfo objects
@@ -70,6 +71,8 @@ module QuartzTorrent
70
71
  attr_accessor :pieces
71
72
  # True if no external peer source is allowed.
72
73
  attr_accessor :private
74
+ # Optional source
75
+ attr_accessor :source
73
76
 
74
77
  # Total length of the torrent data in bytes.
75
78
  def dataLength
@@ -81,6 +84,7 @@ module QuartzTorrent
81
84
  result = Info.new
82
85
  result.pieceLen = infoDict['piece length']
83
86
  result.private = infoDict['private']
87
+ result.source = infoDict['source']
84
88
  result.pieces = parsePieces(Metainfo.valueOrException(infoDict['pieces'], "Torrent metainfo is missing the pieces property."))
85
89
  result.name = Metainfo.valueOrException(infoDict['name'], "Torrent metainfo is missing the name property.")
86
90
 
@@ -110,6 +114,7 @@ module QuartzTorrent
110
114
 
111
115
  hash['piece length'] = @pieceLen
112
116
  hash['private'] = @private if @private
117
+ hash['source'] = @source if @source
113
118
  hash['name'] = @name
114
119
  hash['pieces'] = @pieces.join
115
120
 
@@ -177,7 +182,7 @@ module QuartzTorrent
177
182
  def self.createFromString(data)
178
183
  logger = LogManager.getLogger("metainfo")
179
184
 
180
- decoded = data.bdecode
185
+ decoded = BEncode.load(data, {:ignore_trailing_junk => 1})
181
186
  logger.debug "Decoded torrent metainfo: #{decoded.inspect}"
182
187
  result = Metainfo.new
183
188
  result.createdBy = decoded['created by']
@@ -92,7 +92,7 @@ module QuartzTorrent
92
92
  # Sanity check
93
93
  testInfoHash = Digest::SHA1.digest( bencoded )
94
94
  if testInfoHash == infoHash
95
- result = Metainfo::Info.createFromBdecode(bencoded.bdecode)
95
+ result = Metainfo::Info.createFromBdecode(BEncode.load(bencoded, {:ignore_trailing_junk => 1}))
96
96
  else
97
97
  logger.info "the computed SHA1 hash doesn't match the specified infoHash in #{path}"
98
98
  end
@@ -120,7 +120,11 @@ module QuartzTorrent
120
120
 
121
121
  # Return true if the specified piece is completed. The piece is specified by index.
122
122
  def pieceCompleted?(pieceIndex)
123
- @completePieces.set? pieceIndex
123
+ if pieceIndex >= 0 && pieceIndex < @completePieces.length
124
+ @completePieces.set? pieceIndex
125
+ else
126
+ false
127
+ end
124
128
  end
125
129
 
126
130
  # Do we have all the pieces of the metadata?
@@ -13,6 +13,8 @@ require "quartz_torrent/piecemanagerrequestmetadata.rb"
13
13
  require "quartz_torrent/metainfopiecestate.rb"
14
14
  require "quartz_torrent/extension.rb"
15
15
  require "quartz_torrent/magnet.rb"
16
+ require "quartz_torrent/torrentqueue.rb"
17
+ require "quartz_torrent/alarm.rb"
16
18
 
17
19
 
18
20
  module QuartzTorrent
@@ -52,11 +54,14 @@ module QuartzTorrent
52
54
  @checkPieceManagerTimer = nil
53
55
  @requestBlocksTimer = nil
54
56
  @paused = false
57
+ @queued = false
55
58
  @downRateLimit = nil
56
59
  @upRateLimit = nil
57
60
  @ratio = nil
58
61
  @uploadDuration = nil
59
62
  @downloadCompletedTime = nil
63
+ @isEndgame = false
64
+ @alarms = Alarms.new
60
65
  end
61
66
  # The torrents Metainfo.Info struct. This is nil if the torrent has no metadata and we need to download it
62
67
  # (i.e. a magnet link)
@@ -79,7 +84,15 @@ module QuartzTorrent
79
84
  attr_accessor :bytesUploadedDataOnly
80
85
  attr_accessor :bytesDownloaded
81
86
  attr_accessor :bytesUploaded
87
+ # State of the torrent. Is one of the following states:
88
+ # :initializing Datastructures have been created, but no work started.
89
+ # :checking_pieces Checking piece hashes on startup
90
+ # :downloading_metainfo Downloading the torrent metainfo
91
+ # :uploading The torrent is complete and we are only uploading
92
+ # :running The torrent is incomplete and we are downloading and uploading
93
+ # :error There was an unrecoverable error with the torrent.
82
94
  attr_accessor :state
95
+ attr_accessor :isEndgame
83
96
  attr_accessor :metainfoPieceState
84
97
  # The timer handle for the timer that requests metainfo pieces. This is used to cancel the
85
98
  # timer when the metadata is completely downloaded.
@@ -92,8 +105,10 @@ module QuartzTorrent
92
105
  attr_accessor :checkPieceManagerTimer
93
106
  # Timer handle for timer that requests blocks
94
107
  attr_accessor :requestBlocksTimer
95
-
108
+ # Is the torrent paused
96
109
  attr_accessor :paused
110
+ # Is the torrent queued
111
+ attr_accessor :queued
97
112
  # The RateLimit for downloading this torrent.
98
113
  attr_accessor :downRateLimit
99
114
  # The RateLimit for uploading to peers for this torrent.
@@ -105,7 +120,8 @@ module QuartzTorrent
105
120
  attr_accessor :uploadDuration
106
121
  # Time at which we completely downloaded all bytes of the torrent.
107
122
  attr_accessor :downloadCompletedTime
108
-
123
+ # Alarms object for this torrent
124
+ attr_reader :alarms
109
125
  end
110
126
 
111
127
  # Data about torrents for use by the end user.
@@ -150,6 +166,8 @@ module QuartzTorrent
150
166
  attr_reader :metainfoCompletedLength
151
167
  # Whether or not the torrent is paused.
152
168
  attr_reader :paused
169
+ # Whether or not the torrent is queued.
170
+ attr_reader :queued
153
171
  # After we have completed downloading a torrent, we will continue to upload until we have
154
172
  # uploaded ratio * torrent_size bytes. If nil, no limit on upload.
155
173
  attr_accessor :ratio
@@ -158,6 +176,8 @@ module QuartzTorrent
158
176
  attr_accessor :bytesDownloadedDataOnly
159
177
  attr_accessor :bytesUploaded
160
178
  attr_accessor :bytesDownloaded
179
+ # Array of currently raised alarms
180
+ attr_accessor :alarms
161
181
 
162
182
  # Update the data in this TorrentDataDelegate from the torrentData
163
183
  # object that it was created from. TODO: What if that torrentData is now gone?
@@ -199,6 +219,7 @@ module QuartzTorrent
199
219
  @state = torrentData.state
200
220
  @metainfoLength = nil
201
221
  @paused = torrentData.paused
222
+ @queued = torrentData.queued
202
223
  @metainfoCompletedLength = nil
203
224
  if torrentData.metainfoPieceState && torrentData.state == :downloading_metainfo
204
225
  @metainfoLength = torrentData.metainfoPieceState.metainfoLength
@@ -219,6 +240,7 @@ module QuartzTorrent
219
240
  @uploadRateLimit = torrentData.upRateLimit.unitsPerSecond if torrentData.upRateLimit
220
241
  @ratio = torrentData.ratio
221
242
  @uploadDuration = torrentData.uploadDuration
243
+ @alarms = torrentData.alarms.all
222
244
  end
223
245
 
224
246
  def buildPeersList(torrentData)
@@ -233,9 +255,10 @@ module QuartzTorrent
233
255
  # This class implements a Reactor Handler object. This Handler implements the PeerClient.
234
256
  class PeerClientHandler < QuartzTorrent::Handler
235
257
 
236
- def initialize(baseDirectory)
258
+ def initialize(baseDirectory, maxIncomplete = 5, maxActive = 10)
237
259
  # Hash of TorrentData objects, keyed by torrent infoHash
238
260
  @torrentData = {}
261
+ @torrentQueue = TorrentQueue.new(maxIncomplete, maxActive)
239
262
 
240
263
  @baseDirectory = baseDirectory
241
264
 
@@ -250,8 +273,11 @@ module QuartzTorrent
250
273
  @requestBlocksPeriod = 1
251
274
  @handshakeTimeout = 1
252
275
  @requestTimeout = 60
276
+ @endgameBlockThreshold = 20
253
277
  end
254
278
 
279
+ ################################################ PUBLIC API METHODS ################################################
280
+
255
281
  attr_reader :torrentData
256
282
 
257
283
  # Add a new tracker client. This effectively adds a new torrent to download. Returns the TorrentData object for the
@@ -259,51 +285,28 @@ module QuartzTorrent
259
285
  def addTrackerClient(infoHash, info, trackerclient)
260
286
  raise "There is already a tracker registered for torrent #{QuartzTorrent.bytesToHex(infoHash)}" if @torrentData.has_key? infoHash
261
287
  torrentData = TorrentData.new(infoHash, info, trackerclient)
288
+ trackerclient.alarms = torrentData.alarms
262
289
  @torrentData[infoHash] = torrentData
290
+ torrentData.info = info
291
+ torrentData.state = :initializing
263
292
 
264
- # If we already have the metainfo info for this torrent, we can begin checking the pieces.
265
- # If we don't have the metainfo info then we need to get the metainfo first.
266
- if ! info
267
- info = MetainfoPieceState.downloaded(@baseDirectory, torrentData.infoHash)
268
- torrentData.info = info
269
- end
270
-
271
- if info
272
- startCheckingPieces torrentData
273
- else
274
- # Request the metainfo from peers.
275
- torrentData.state = :downloading_metainfo
276
-
277
- @logger.info "Downloading metainfo"
278
- #torrentData.metainfoPieceState = MetainfoPieceState.new(@baseDirectory, infoHash, )
279
-
280
- # Schedule peer connection management. Recurring and immediate
281
- torrentData.managePeersTimer =
282
- @reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], true, true)
293
+ queue(torrentData)
294
+ dequeue
283
295
 
284
- # Schedule a timer for requesting metadata pieces from peers.
285
- torrentData.metainfoRequestTimer =
286
- @reactor.scheduleTimer(@requestBlocksPeriod, [:request_metadata_pieces, infoHash], true, false)
287
-
288
- # Schedule checking for metainfo PieceManager results (including when piece reading completes)
289
- torrentData.checkMetadataPieceManagerTimer =
290
- @reactor.scheduleTimer(@requestBlocksPeriod, [:check_metadata_piece_manager, infoHash], true, false)
291
- end
292
-
293
296
  torrentData
294
297
  end
295
298
 
296
299
  # Remove a torrent.
297
300
  def removeTorrent(infoHash, deleteFiles = false)
298
301
  # Can't do this right now, since it could be in use by an event handler. Use an immediate, non-recurring timer instead.
299
- @logger.info "Scheduling immediate timer to remove torrent #{QuartzTorrent.bytesToHex(infoHash)}. #{deleteFiles ? "Will" : "Wont"} delete downloaded files."
302
+ @logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Scheduling immediate timer to remove torrent. #{deleteFiles ? "Will" : "Wont"} delete downloaded files."
300
303
  @reactor.scheduleTimer(0, [:removetorrent, infoHash, deleteFiles], false, true)
301
304
  end
302
305
 
303
306
  # Pause or unpause the specified torrent.
304
307
  def setPaused(infoHash, value)
305
308
  # Can't do this right now, since it could be in use by an event handler. Use an immediate, non-recurring timer instead.
306
- @logger.info "Scheduling immediate timer to pause torrent #{QuartzTorrent.bytesToHex(infoHash)}."
309
+ @logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Scheduling immediate timer to #{value ? "pause" : "unpause"} torrent."
307
310
  @reactor.scheduleTimer(0, [:pausetorrent, infoHash, value], false, true)
308
311
  end
309
312
 
@@ -381,6 +384,68 @@ module QuartzTorrent
381
384
  torrentData.uploadDuration = seconds
382
385
  end
383
386
 
387
+ # Adjust the bytesUploaded property of the specified torrent by the passed amount.
388
+ # Adjustment should be an integer. It is added to the current bytesUploaded amount.
389
+ def adjustBytesUploaded(infoHash, adjustment)
390
+ torrentData = @torrentData[infoHash]
391
+ if ! torrentData
392
+ @logger.warn "Asked to adjust uploaded bytes for a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
393
+ return
394
+ end
395
+
396
+ runInReactorThread do
397
+ torrentData.bytesUploaded += adjustment
398
+ torrentData.bytesUploadedDataOnly += adjustment
399
+ end
400
+ end
401
+
402
+ # Adjust the bytesDownloaded property of the specified torrent by the passed amount.
403
+ # Adjustment should be an integer. It is added to the current bytesDownloaded amount.
404
+ def adjustBytesDownloaded(infoHash, adjustment)
405
+ torrentData = @torrentData[infoHash]
406
+ if ! torrentData
407
+ @logger.warn "Asked to adjust uploaded bytes for a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
408
+ return
409
+ end
410
+
411
+ runInReactorThread do
412
+ torrentData.bytesDownloaded += adjustment
413
+ torrentData.bytesDownloadedDataOnly += adjustment
414
+ end
415
+ end
416
+
417
+ # Get a hash of new TorrentDataDelegate objects keyed by torrent infohash.
418
+ # This method is meant to be called from a different thread than the one
419
+ # the reactor is running in. This method is not immediate but blocks until the
420
+ # data is prepared.
421
+ # If infoHash is passed, only that torrent data is returned (still in a hashtable; just one entry)
422
+ def getDelegateTorrentData(infoHash = nil)
423
+ # Use an immediate, non-recurring timer.
424
+ result = {}
425
+ return result if stopped?
426
+ semaphore = Semaphore.new
427
+ timer = @reactor.scheduleTimer(0, [:get_torrent_data, result, semaphore, infoHash], false, true)
428
+ if semaphore.wait(3)
429
+ result
430
+ else
431
+ @logger.warn "getDelegateTorrentData: Waiting on semaphore timed out"
432
+ throw "Waiting on semaphore for timer #{timer.object_id} timed out"
433
+ end
434
+ end
435
+
436
+ # Update the data stored in a TorrentDataDelegate to the latest information.
437
+ def updateDelegateTorrentData(delegate)
438
+ return if stopped?
439
+ # Use an immediate, non-recurring timer.
440
+ semaphore = Semaphore.new
441
+ @reactor.scheduleTimer(0, [:update_torrent_data, delegate, semaphore], false, true)
442
+ semaphore.wait
443
+ result
444
+ end
445
+
446
+
447
+ ################################################ REACTOR METHODS ################################################
448
+
384
449
  # Reactor method called when a peer has connected to us.
385
450
  def serverInit(metadata, addr, port)
386
451
  # A peer connected to us
@@ -638,6 +703,8 @@ module QuartzTorrent
638
703
  requestMetadataPieces(metadata[1])
639
704
  elsif metadata.is_a?(Array) && metadata[0] == :check_metadata_piece_manager
640
705
  checkMetadataPieceManagerResults(metadata[1])
706
+ elsif metadata.is_a?(Array) && metadata[0] == :runproc
707
+ metadata[1].call
641
708
  else
642
709
  @logger.info "Unknown timer #{metadata} expired."
643
710
  end
@@ -657,30 +724,7 @@ module QuartzTorrent
657
724
  close
658
725
  end
659
726
 
660
- # Get a hash of new TorrentDataDelegate objects keyed by torrent infohash.
661
- # This method is meant to be called from a different thread than the one
662
- # the reactor is running in. This method is not immediate but blocks until the
663
- # data is prepared.
664
- # If infoHash is passed, only that torrent data is returned (still in a hashtable; just one entry)
665
- def getDelegateTorrentData(infoHash = nil)
666
- # Use an immediate, non-recurring timer.
667
- result = {}
668
- return result if stopped?
669
- semaphore = Semaphore.new
670
- @reactor.scheduleTimer(0, [:get_torrent_data, result, semaphore, infoHash], false, true)
671
- semaphore.wait
672
- result
673
- end
674
-
675
- def updateDelegateTorrentData(delegate)
676
- return if stopped?
677
- # Use an immediate, non-recurring timer.
678
- semaphore = Semaphore.new
679
- @reactor.scheduleTimer(0, [:update_torrent_data, delegate, semaphore], false, true)
680
- semaphore.wait
681
- result
682
- end
683
-
727
+ ################################################ PRIVATE METHODS ################################################
684
728
  private
685
729
  def setPeerDisconnected(peer)
686
730
  peer.state = :disconnected
@@ -708,7 +752,7 @@ module QuartzTorrent
708
752
  return false if !torrentData
709
753
 
710
754
  if msg.peerId == torrentData.trackerClient.peerId
711
- @logger.info "We connected to ourself. Closing connection."
755
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: We connected to ourself. Closing connection."
712
756
  peer.isUs = true
713
757
  close
714
758
  return
@@ -718,7 +762,7 @@ module QuartzTorrent
718
762
  if peers
719
763
  peers.each do |existingPeer|
720
764
  if existingPeer.state == :connected
721
- @logger.warn "Peer with id #{msg.peerId} created a new connection when we already have a connection in state #{existingPeer.state}. Closing new connection."
765
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Peer with id #{msg.peerId} created a new connection when we already have a connection in state #{existingPeer.state}. Closing new connection."
722
766
  torrentData.peers.delete existingPeer
723
767
  setPeerDisconnected(peer)
724
768
  close
@@ -734,12 +778,12 @@ module QuartzTorrent
734
778
  peer.bitfield = Bitfield.new(torrentData.info.pieces.length)
735
779
  else
736
780
  peer.bitfield = EmptyBitfield.new
737
- @logger.info "We have no metainfo yet, so setting peer #{peer} to have an EmptyBitfield"
781
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: We have no metainfo yet, so setting peer #{peer} to have an EmptyBitfield"
738
782
  end
739
783
 
740
784
  # Send extended handshake if the peer supports extensions
741
785
  if (msg.reserved.unpack("C8")[5] & 0x10) != 0
742
- @logger.warn "Peer supports extensions. Sending extended handshake"
786
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Peer supports extensions. Sending extended handshake"
743
787
  extended = Extension.createExtendedHandshake torrentData.info
744
788
  extended.serializeTo currentIo
745
789
  end
@@ -764,7 +808,7 @@ module QuartzTorrent
764
808
  end
765
809
 
766
810
  def updatePeerWithHandshakeInfo(torrentData, msg, peer)
767
- @logger.info "peer #{peer} sent valid handshake for torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)}"
811
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: peer #{peer} sent valid handshake for torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)}"
768
812
  peer.infoHash = msg.infoHash
769
813
  # If this was a peer we got from a tracker that had no id then we only learn the id on handshake.
770
814
  peer.trackerPeer.id = msg.peerId
@@ -788,7 +832,7 @@ module QuartzTorrent
788
832
  return
789
833
  end
790
834
 
791
- return if torrentData.paused
835
+ return if torrentData.paused || torrentData.queued
792
836
 
793
837
  trackerclient = torrentData.trackerClient
794
838
 
@@ -799,19 +843,19 @@ module QuartzTorrent
799
843
 
800
844
  manager = torrentData.peerManager
801
845
  if ! manager
802
- @logger.error "Manage peers: peer manager client for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
846
+ @logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: Manage peers: peer manager client for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
803
847
  return
804
848
  end
805
849
 
806
850
  toConnect = manager.manageConnections(classifiedPeers)
807
851
  toConnect.each do |peer|
808
- @logger.debug "Connecting to peer #{peer}"
852
+ @logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Connecting to peer #{peer}"
809
853
  connect peer.trackerPeer.ip, peer.trackerPeer.port, peer
810
854
  end
811
855
 
812
856
  manageResult = manager.managePeers(classifiedPeers)
813
857
  manageResult.unchoke.each do |peer|
814
- @logger.debug "Unchoking peer #{peer}"
858
+ @logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Unchoking peer #{peer}"
815
859
  withPeersIo(peer, "unchoking peer") do |io|
816
860
  msg = Unchoke.new
817
861
  sendMessageToPeer msg, io, peer
@@ -820,7 +864,7 @@ module QuartzTorrent
820
864
  end
821
865
 
822
866
  manageResult.choke.each do |peer|
823
- @logger.debug "Choking peer #{peer}"
867
+ @logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Choking peer #{peer}"
824
868
  withPeersIo(peer, "choking peer") do |io|
825
869
  msg = Choke.new
826
870
  sendMessageToPeer msg, io, peer
@@ -837,32 +881,44 @@ module QuartzTorrent
837
881
  return
838
882
  end
839
883
 
840
- return if torrentData.paused
884
+ return if torrentData.paused || torrentData.queued
841
885
 
842
886
  classifiedPeers = ClassifiedPeers.new torrentData.peers.all
843
887
 
844
888
  if ! torrentData.blockState
845
- @logger.error "Request blocks peers: no blockstate yet."
889
+ @logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: Request blocks peers: no blockstate yet."
846
890
  return
847
891
  end
848
892
 
849
- if torrentData.state == :uploading && (torrentData.state != :paused)
893
+ if torrentData.state == :uploading && !torrentData.paused
850
894
  if torrentData.ratio
851
895
  if torrentData.bytesUploadedDataOnly >= torrentData.ratio*torrentData.blockState.totalLength
852
- @logger.info "Pausing torrent due to upload ratio limit." if torrentData.metainfoPieceState.complete?
896
+ @logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Pausing torrent due to upload ratio limit." if torrentData.metainfoPieceState.complete?
853
897
  setPaused(infoHash, true)
854
898
  return
855
899
  end
856
900
  end
857
901
  if torrentData.uploadDuration && torrentData.downloadCompletedTime
858
902
  if Time.new > torrentData.downloadCompletedTime + torrentData.uploadDuration
859
- @logger.info "Pausing torrent due to upload duration being reached." if torrentData.metainfoPieceState.complete?
903
+ @logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Pausing torrent due to upload duration being reached." if torrentData.metainfoPieceState.complete?
860
904
  setPaused(infoHash, true)
861
905
  return
862
906
  end
863
907
  end
864
908
  end
865
909
 
910
+ # Should we switch to endgame mode?
911
+ if torrentData.state == :running && !torrentData.isEndgame
912
+ blocks = torrentData.blockState.completeBlockBitfield
913
+ set = blocks.countSet
914
+ if set >= blocks.length - @endgameBlockThreshold && set < blocks.length
915
+ @logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Entering endgame mode: blocks #{set}/#{blocks.length} complete."
916
+ torrentData.isEndgame = true
917
+ end
918
+ elsif torrentData.isEndgame && torrentData.state != :running
919
+ torrentData.isEndgame = false
920
+ end
921
+
866
922
  # Delete any timed-out requests.
867
923
  classifiedPeers.establishedPeers.each do |peer|
868
924
  toDelete = []
@@ -870,7 +926,7 @@ module QuartzTorrent
870
926
  toDelete.push blockIndex if (Time.new - requestTime) > @requestTimeout
871
927
  end
872
928
  toDelete.each do |blockIndex|
873
- @logger.debug "Block #{blockIndex} request timed out."
929
+ @logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Block #{blockIndex} request timed out."
874
930
  blockInfo = torrentData.blockState.createBlockinfoByBlockIndex(blockIndex)
875
931
  torrentData.blockState.setBlockRequested blockInfo, false
876
932
  peer.requestedBlocks.delete blockIndex
@@ -895,32 +951,47 @@ module QuartzTorrent
895
951
  # Request blocks
896
952
  blockInfos = torrentData.blockState.findRequestableBlocks(classifiedPeers, 100)
897
953
  blockInfos.each do |blockInfo|
898
- # Pick one of the peers that has the piece to download it from. Pick one of the
899
- # peers with the top 3 upload rates.
900
- elegiblePeers = blockInfo.peers.find_all{ |p| p.requestedBlocks.length < p.maxRequestedBlocks }.sort{ |a,b| b.uploadRate.value <=> a.uploadRate.value}
901
- random = elegiblePeers[rand(blockInfo.peers.size)]
902
- peer = elegiblePeers.first(3).push(random).shuffle.first
903
- next if ! peer
904
- withPeersIo(peer, "requesting block") do |io|
905
- if ! peer.amInterested
906
- # Let this peer know that I'm interested if I haven't yet.
907
- msg = Interested.new
954
+
955
+ peersToRequest = []
956
+ if torrentData.isEndgame
957
+ # Since we are in endgame mode, request blocks from all elegible peers
958
+ elegiblePeers = blockInfo.peers.find_all{ |p| p.requestedBlocks.length < p.maxRequestedBlocks }
959
+
960
+ peersToRequest.concat elegiblePeers
961
+ else
962
+ # Pick one of the peers that has the piece to download it from. Pick one of the
963
+ # peers with the top 3 upload rates.
964
+ elegiblePeers = blockInfo.peers.find_all{ |p| p.requestedBlocks.length < p.maxRequestedBlocks }.sort{ |a,b| b.uploadRate.value <=> a.uploadRate.value}
965
+ random = elegiblePeers[rand(blockInfo.peers.size)]
966
+ peer = elegiblePeers.first(3).push(random).shuffle.first
967
+ next if ! peer
968
+ peersToRequest.push peer
969
+ end
970
+
971
+ peersToRequest.each do |peer|
972
+ withPeersIo(peer, "requesting block") do |io|
973
+ if ! peer.amInterested
974
+ # Let this peer know that I'm interested if I haven't yet.
975
+ msg = Interested.new
976
+ sendMessageToPeer msg, io, peer
977
+ peer.amInterested = true
978
+ end
979
+ @logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Requesting block from #{peer}: piece #{blockInfo.pieceIndex} offset #{blockInfo.offset} length #{blockInfo.length}"
980
+ msg = blockInfo.getRequest
908
981
  sendMessageToPeer msg, io, peer
909
- peer.amInterested = true
982
+ torrentData.blockState.setBlockRequested blockInfo, true
983
+ peer.requestedBlocks[blockInfo.blockIndex] = Time.new
910
984
  end
911
- @logger.debug "Requesting block from #{peer}: piece #{blockInfo.pieceIndex} offset #{blockInfo.offset} length #{blockInfo.length}"
912
- msg = blockInfo.getRequest
913
- sendMessageToPeer msg, io, peer
914
- torrentData.blockState.setBlockRequested blockInfo, true
915
- peer.requestedBlocks[blockInfo.blockIndex] = Time.new
916
985
  end
917
986
  end
918
987
 
919
988
  if blockInfos.size == 0
920
989
  if torrentData.state != :uploading && torrentData.blockState.completePieceBitfield.allSet?
921
- @logger.info "Download of #{QuartzTorrent.bytesToHex(infoHash)} complete."
990
+ @logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Download complete."
922
991
  torrentData.state = :uploading
923
992
  torrentData.downloadCompletedTime = Time.new
993
+
994
+ dequeue
924
995
  end
925
996
  end
926
997
 
@@ -935,13 +1006,13 @@ module QuartzTorrent
935
1006
  return
936
1007
  end
937
1008
 
938
- return if torrentData.paused
1009
+ return if torrentData.paused || torrentData.queued
939
1010
 
940
1011
  # We may not have completed the extended handshake with the peer which specifies the torrent size.
941
1012
  # In this case torrentData.metainfoPieceState is not yet set.
942
1013
  return if ! torrentData.metainfoPieceState
943
1014
 
944
- @logger.info "Obtained all pieces of metainfo." if torrentData.metainfoPieceState.complete?
1015
+ @logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Obtained all pieces of metainfo." if torrentData.metainfoPieceState.complete?
945
1016
 
946
1017
  pieces = torrentData.metainfoPieceState.findRequestablePieces
947
1018
  classifiedPeers = ClassifiedPeers.new torrentData.peers.all
@@ -956,11 +1027,11 @@ module QuartzTorrent
956
1027
  withPeersIo(peers.first, "requesting metadata piece") do |io|
957
1028
  sendMessageToPeer msg, io, peers.first
958
1029
  torrentData.metainfoPieceState.setPieceRequested(pieceIndex, true)
959
- @logger.debug "Requesting metainfo piece from #{peers.first}: piece #{pieceIndex}"
1030
+ @logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Requesting metainfo piece from #{peers.first}: piece #{pieceIndex}"
960
1031
  end
961
1032
  end
962
1033
  else
963
- @logger.error "No peers found that have metadata."
1034
+ @logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: No peers found that have metadata."
964
1035
  end
965
1036
 
966
1037
  end
@@ -971,7 +1042,7 @@ module QuartzTorrent
971
1042
  @logger.error "Check metadata piece manager results: data for torrent #{QuartzTorrent.bytesToHex(infoHash)} not found."
972
1043
  return
973
1044
  end
974
-
1045
+
975
1046
  # We may not have completed the extended handshake with the peer which specifies the torrent size.
976
1047
  # In this case torrentData.metainfoPieceState is not yet set.
977
1048
  return if ! torrentData.metainfoPieceState
@@ -980,7 +1051,7 @@ module QuartzTorrent
980
1051
  results.each do |result|
981
1052
  metaData = torrentData.pieceManagerMetainfoRequestMetadata.delete(result.requestId)
982
1053
  if ! metaData
983
- @logger.error "Can't find metadata for PieceManager request #{result.requestId}"
1054
+ @logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: Can't find metadata for PieceManager request #{result.requestId}"
984
1055
  next
985
1056
  end
986
1057
 
@@ -991,7 +1062,7 @@ module QuartzTorrent
991
1062
  msg.piece = metaData.data.requestMsg.piece
992
1063
  msg.data = result.data
993
1064
  withPeersIo(metaData.data.peer, "sending extended metainfo piece message") do |io|
994
- @logger.debug "Sending metainfo piece to #{metaData.data.peer}: piece #{msg.piece} with data length #{msg.data.length}"
1065
+ @logger.debug "#{QuartzTorrent.bytesToHex(infoHash)}: Sending metainfo piece to #{metaData.data.peer}: piece #{msg.piece} with data length #{msg.data.length}"
995
1066
  sendMessageToPeer msg, io, metaData.data.peer
996
1067
  end
997
1068
  result.data
@@ -999,7 +1070,7 @@ module QuartzTorrent
999
1070
  end
1000
1071
 
1001
1072
  if torrentData.metainfoPieceState.complete? && torrentData.state == :downloading_metainfo
1002
- @logger.info "Obtained all pieces of metainfo. Will begin checking existing pieces."
1073
+ @logger.info "#{QuartzTorrent.bytesToHex(infoHash)}: Obtained all pieces of metainfo. Will begin checking existing pieces."
1003
1074
  torrentData.metainfoPieceState.flush
1004
1075
  # We don't need to download metainfo anymore.
1005
1076
  cancelTimer torrentData.metainfoRequestTimer if torrentData.metainfoRequestTimer
@@ -1008,7 +1079,7 @@ module QuartzTorrent
1008
1079
  torrentData.info = info
1009
1080
  startCheckingPieces torrentData
1010
1081
  else
1011
- @logger.error "Metadata download is complete but reading the metadata failed"
1082
+ @logger.error "#{QuartzTorrent.bytesToHex(infoHash)}: Metadata download is complete but reading the metadata failed"
1012
1083
  torrentData.state = :error
1013
1084
  end
1014
1085
  end
@@ -1022,27 +1093,47 @@ module QuartzTorrent
1022
1093
  end
1023
1094
 
1024
1095
  if ! torrentData.blockState
1025
- @logger.error "Receive piece: no blockstate yet."
1096
+ @logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Receive piece: no blockstate yet."
1026
1097
  return
1027
1098
  end
1028
1099
 
1029
1100
  blockInfo = torrentData.blockState.createBlockinfoByPieceResponse(msg.pieceIndex, msg.blockOffset, msg.data.length)
1030
1101
 
1031
1102
  if ! peer.requestedBlocks.has_key?(blockInfo.blockIndex)
1032
- @logger.debug "Receive piece: we either didn't request this piece, or it was already received due to endgame strategy. Ignoring this message."
1103
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Receive piece: we either didn't request this piece, or it was already received due to endgame strategy. Ignoring this message."
1033
1104
  return
1034
1105
  end
1035
1106
 
1036
1107
  if torrentData.blockState.blockCompleted?(blockInfo)
1037
- @logger.debug "Receive piece: we already have this block. Ignoring this message."
1108
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Receive piece: we already have this block. Ignoring this message."
1038
1109
  return
1039
1110
  end
1040
1111
  peer.requestedBlocks.delete blockInfo.blockIndex
1041
- # Block is marked as not requested when hash is confirmed
1042
1112
 
1113
+ # Block is marked as not requested when hash is confirmed
1043
1114
  torrentData.bytesDownloadedDataOnly += msg.data.length
1044
1115
  id = torrentData.pieceManager.writeBlock(msg.pieceIndex, msg.blockOffset, msg.data)
1045
1116
  torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:write, msg)
1117
+
1118
+ if torrentData.isEndgame
1119
+ # Assume this block is correct. Send a Cancel message to all other peers from whom we requested
1120
+ # this piece.
1121
+ classifiedPeers = ClassifiedPeers.new torrentData.peers.all
1122
+ classifiedPeers.requestablePeers.each do |otherPeer|
1123
+ if otherPeer.requestedBlocks.has_key?(blockInfo.blockIndex)
1124
+ withPeersIo(otherPeer, "when sending Cancel message") do |io|
1125
+
1126
+ cancel = Cancel.new
1127
+ cancel.pieceIndex = msg.pieceIndex
1128
+ cancel.blockOffset = msg.blockOffset
1129
+ cancel.blockLength = msg.data.length
1130
+ sendMessageToPeer cancel, io, otherPeer
1131
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending Cancel message to peer #{peer}"
1132
+ end
1133
+ end
1134
+ end
1135
+ end
1136
+
1046
1137
  end
1047
1138
 
1048
1139
  def handleRequest(msg, peer)
@@ -1057,7 +1148,7 @@ module QuartzTorrent
1057
1148
  return
1058
1149
  end
1059
1150
  if msg.blockLength <= 0
1060
- @logger.error "Request piece: peer requested block of length #{msg.blockLength} which is invalid."
1151
+ @logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Request piece: peer requested block of length #{msg.blockLength} which is invalid."
1061
1152
  return
1062
1153
  end
1063
1154
 
@@ -1076,11 +1167,11 @@ module QuartzTorrent
1076
1167
  if torrentData.info
1077
1168
  peer.bitfield.length = torrentData.info.pieces.length
1078
1169
  else
1079
- @logger.warn "A peer connected and sent a bitfield but we don't know the length of the torrent yet. Assuming number of pieces is divisible by 8"
1170
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: A peer connected and sent a bitfield but we don't know the length of the torrent yet. Assuming number of pieces is divisible by 8"
1080
1171
  end
1081
1172
 
1082
1173
  if ! torrentData.blockState
1083
- @logger.warn "Bitfield: no blockstate yet."
1174
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Bitfield: no blockstate yet."
1084
1175
  return
1085
1176
  end
1086
1177
 
@@ -1089,7 +1180,7 @@ module QuartzTorrent
1089
1180
  needed.intersection!(peer.bitfield)
1090
1181
  if ! needed.allClear?
1091
1182
  if ! peer.amInterested
1092
- @logger.debug "Need some pieces from peer #{peer} so sending Interested message"
1183
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Need some pieces from peer #{peer} so sending Interested message"
1093
1184
  msg = Interested.new
1094
1185
  sendMessageToPeer msg, currentIo, peer
1095
1186
  peer.amInterested = true
@@ -1105,7 +1196,7 @@ module QuartzTorrent
1105
1196
  end
1106
1197
 
1107
1198
  if msg.pieceIndex >= peer.bitfield.length
1108
- @logger.warn "Peer #{peer} sent Have message with invalid piece index"
1199
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Peer #{peer} sent Have message with invalid piece index"
1109
1200
  return
1110
1201
  end
1111
1202
 
@@ -1113,13 +1204,13 @@ module QuartzTorrent
1113
1204
  peer.bitfield.set msg.pieceIndex
1114
1205
 
1115
1206
  if ! torrentData.blockState
1116
- @logger.warn "Have: no blockstate yet."
1207
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Have: no blockstate yet."
1117
1208
  return
1118
1209
  end
1119
1210
 
1120
1211
  # If we are interested in something from this peer, let them know.
1121
1212
  if ! torrentData.blockState.completePieceBitfield.set?(msg.pieceIndex)
1122
- @logger.debug "Peer #{peer} just got a piece we need so sending Interested message"
1213
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Peer #{peer} just got a piece we need so sending Interested message"
1123
1214
  msg = Interested.new
1124
1215
  sendMessageToPeer msg, currentIo, peer
1125
1216
  peer.amInterested = true
@@ -1139,24 +1230,24 @@ module QuartzTorrent
1139
1230
 
1140
1231
  metaData = torrentData.pieceManagerRequestMetadata.delete(result.requestId)
1141
1232
  if ! metaData
1142
- @logger.error "Can't find metadata for PieceManager request #{result.requestId}"
1233
+ @logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Can't find metadata for PieceManager request #{result.requestId}"
1143
1234
  next
1144
1235
  end
1145
1236
 
1146
1237
  if metaData.type == :write
1147
1238
  if result.successful?
1148
- @logger.debug "Block written to disk. "
1239
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Block written to disk. "
1149
1240
  # Block successfully written!
1150
1241
  torrentData.blockState.setBlockCompleted metaData.data.pieceIndex, metaData.data.blockOffset, true do |pieceIndex|
1151
1242
  # The peice is completed! Check hash.
1152
- @logger.debug "Piece #{pieceIndex} is complete. Checking hash. "
1243
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Piece #{pieceIndex} is complete. Checking hash. "
1153
1244
  id = torrentData.pieceManager.checkPieceHash(metaData.data.pieceIndex)
1154
1245
  torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:hash, metaData.data.pieceIndex)
1155
1246
  end
1156
1247
  else
1157
1248
  # Block failed! Clear completed and requested state.
1158
1249
  torrentData.blockState.setBlockCompleted metaData.data.pieceIndex, metaData.data.blockOffset, false
1159
- @logger.error "Writing block failed: #{result.error}"
1250
+ @logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Writing block failed: #{result.error}"
1160
1251
  end
1161
1252
  elsif metaData.type == :read
1162
1253
  if result.successful?
@@ -1167,21 +1258,21 @@ module QuartzTorrent
1167
1258
  msg.pieceIndex = readRequestMetadata.requestMsg.pieceIndex
1168
1259
  msg.blockOffset = readRequestMetadata.requestMsg.blockOffset
1169
1260
  msg.data = result.data
1170
- @logger.debug "Sending block to #{peer}: piece #{msg.pieceIndex} offset #{msg.blockOffset} length #{msg.data.length}"
1261
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending block to #{peer}: piece #{msg.pieceIndex} offset #{msg.blockOffset} length #{msg.data.length}"
1171
1262
  sendMessageToPeer msg, io, peer
1172
1263
  torrentData.bytesUploadedDataOnly += msg.data.length
1173
- @logger.debug "Sending piece to peer"
1264
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending piece to peer"
1174
1265
  end
1175
1266
  else
1176
- @logger.error "Reading block failed: #{result.error}"
1267
+ @logger.error "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Reading block failed: #{result.error}"
1177
1268
  end
1178
1269
  elsif metaData.type == :hash
1179
1270
  if result.successful?
1180
- @logger.debug "Hash of piece #{metaData.data} is correct"
1271
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Hash of piece #{metaData.data} is correct"
1181
1272
  sendHaves(torrentData, metaData.data)
1182
1273
  sendUninterested(torrentData)
1183
1274
  else
1184
- @logger.info "Hash of piece #{metaData.data} is incorrect. Marking piece as not complete."
1275
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Hash of piece #{metaData.data} is incorrect. Marking piece as not complete."
1185
1276
  torrentData.blockState.setPieceCompleted metaData.data, false
1186
1277
  end
1187
1278
  elsif metaData.type == :check_existing
@@ -1195,20 +1286,20 @@ module QuartzTorrent
1195
1286
  def handleCheckExistingResult(torrentData, pieceManagerResult)
1196
1287
  if pieceManagerResult.successful?
1197
1288
  existingBitfield = pieceManagerResult.data
1198
- @logger.info "We already have #{existingBitfield.countSet}/#{existingBitfield.length} pieces."
1289
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: We already have #{existingBitfield.countSet}/#{existingBitfield.length} pieces."
1199
1290
 
1200
1291
  info = torrentData.info
1201
1292
 
1202
1293
  torrentData.blockState = BlockState.new(info, existingBitfield)
1203
1294
 
1204
- @logger.info "Starting torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)}. Information:"
1205
- @logger.info " piece length: #{info.pieceLen}"
1206
- @logger.info " number of pieces: #{info.pieces.size}"
1207
- @logger.info " total length #{info.dataLength}"
1295
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Starting torrent. Information:"
1296
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: piece length: #{info.pieceLen}"
1297
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: number of pieces: #{info.pieces.size}"
1298
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: total length #{info.dataLength}"
1208
1299
 
1209
1300
  startDownload torrentData
1210
1301
  else
1211
- @logger.info "Checking existing pieces of torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)} failed: #{pieceManagerResult.error}"
1302
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Checking existing pieces of torrent failed: #{pieceManagerResult.error}"
1212
1303
  torrentData.state = :error
1213
1304
  end
1214
1305
  end
@@ -1221,7 +1312,7 @@ module QuartzTorrent
1221
1312
  torrentData.pieceManager = QuartzTorrent::PieceManager.new(@baseDirectory, torrentData.info)
1222
1313
 
1223
1314
  torrentData.state = :checking_pieces
1224
- @logger.info "Checking pieces of torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)} asynchronously."
1315
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Checking pieces of torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)} asynchronously."
1225
1316
  id = torrentData.pieceManager.findExistingPieces
1226
1317
  torrentData.pieceManagerRequestMetadata[id] = PieceManagerRequestMetadata.new(:check_existing, nil)
1227
1318
 
@@ -1240,13 +1331,43 @@ module QuartzTorrent
1240
1331
  end
1241
1332
  end
1242
1333
 
1334
+ # Take a torrent that is in the :initializing state and make it go.
1335
+ def initTorrent(torrentData)
1336
+ # If we already have the metainfo info for this torrent, we can begin checking the pieces.
1337
+ # If we don't have the metainfo info then we need to get the metainfo first.
1338
+ if ! torrentData.info
1339
+ torrentData.info = MetainfoPieceState.downloaded(@baseDirectory, torrentData.infoHash)
1340
+ end
1341
+
1342
+ if torrentData.info
1343
+ startCheckingPieces torrentData
1344
+ else
1345
+ # Request the metainfo from peers.
1346
+ torrentData.state = :downloading_metainfo
1347
+
1348
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Downloading metainfo"
1349
+
1350
+ # Schedule peer connection management. Recurring and immediate
1351
+ torrentData.managePeersTimer =
1352
+ @reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], true, true)
1353
+
1354
+ # Schedule a timer for requesting metadata pieces from peers.
1355
+ torrentData.metainfoRequestTimer =
1356
+ @reactor.scheduleTimer(@requestBlocksPeriod, [:request_metadata_pieces, torrentData.infoHash], true, false)
1357
+
1358
+ # Schedule checking for metainfo PieceManager results (including when piece reading completes)
1359
+ torrentData.checkMetadataPieceManagerTimer =
1360
+ @reactor.scheduleTimer(@requestBlocksPeriod, [:check_metadata_piece_manager, torrentData.infoHash], true, false)
1361
+ end
1362
+ end
1363
+
1243
1364
  # Start the actual torrent download. This method schedules the necessary timers and registers the necessary listeners
1244
1365
  # and changes the state to :running. It is meant to be called after checking for existing pieces or downloading the
1245
1366
  # torrent metadata (if this is a magnet link torrent)
1246
1367
  def startDownload(torrentData)
1247
1368
  # Add a listener for when the tracker's peers change.
1248
1369
  torrentData.peerChangeListener = Proc.new do
1249
- @logger.debug "Managing peers for torrent #{QuartzTorrent.bytesToHex(torrentData.infoHash)} on peer change event"
1370
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Managing peers on peer change event"
1250
1371
 
1251
1372
  # Non-recurring and immediate timer
1252
1373
  torrentData.managePeersTimer =
@@ -1277,7 +1398,7 @@ module QuartzTorrent
1277
1398
  if metadataSize
1278
1399
  # This peer knows the size of the metadata. If we haven't created our MetainfoPieceState yet, create it now.
1279
1400
  if ! torrentData.metainfoPieceState
1280
- @logger.info "Extended Handshake: Learned that metadata size is #{metadataSize}. Creating MetainfoPieceState"
1401
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Extended Handshake: Learned that metadata size is #{metadataSize}. Creating MetainfoPieceState"
1281
1402
  torrentData.metainfoPieceState = MetainfoPieceState.new(@baseDirectory, torrentData.infoHash, metadataSize)
1282
1403
  end
1283
1404
  end
@@ -1292,10 +1413,10 @@ module QuartzTorrent
1292
1413
  end
1293
1414
 
1294
1415
  if msg.msgType == :request
1295
- @logger.debug "Got extended metainfo request for piece #{msg.piece}"
1416
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Got extended metainfo request for piece #{msg.piece}"
1296
1417
  # Build a response for this piece.
1297
1418
  if torrentData.metainfoPieceState.pieceCompleted? msg.piece
1298
- @logger.debug "Requesting extended metainfo piece #{msg.piece} from metainfoPieceState."
1419
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Requesting extended metainfo piece #{msg.piece} from metainfoPieceState."
1299
1420
  id = torrentData.metainfoPieceState.readPiece msg.piece
1300
1421
  torrentData.pieceManagerMetainfoRequestMetadata[id] =
1301
1422
  PieceManagerRequestMetadata.new(:read, ReadRequestMetadata.new(peer,msg))
@@ -1304,19 +1425,19 @@ module QuartzTorrent
1304
1425
  reject.msgType = :reject
1305
1426
  reject.piece = msg.piece
1306
1427
  withPeersIo(peer, "sending extended metainfo reject message") do |io|
1307
- @logger.debug "Sending metainfo reject to #{peer}: piece #{msg.piece}"
1428
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending metainfo reject to #{peer}: piece #{msg.piece}"
1308
1429
  sendMessageToPeer reject, io, peer
1309
1430
  end
1310
1431
  end
1311
1432
  elsif msg.msgType == :piece
1312
- @logger.debug "Got extended metainfo piece response for piece #{msg.piece} with data length #{msg.data.length}"
1433
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Got extended metainfo piece response for piece #{msg.piece} with data length #{msg.data.length}"
1313
1434
  if ! torrentData.metainfoPieceState.pieceCompleted? msg.piece
1314
1435
  id = torrentData.metainfoPieceState.savePiece msg.piece, msg.data
1315
1436
  torrentData.pieceManagerMetainfoRequestMetadata[id] =
1316
1437
  PieceManagerRequestMetadata.new(:write, msg)
1317
1438
  end
1318
1439
  elsif msg.msgType == :reject
1319
- @logger.debug "Got extended metainfo reject response for piece #{msg.piece}"
1440
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Got extended metainfo reject response for piece #{msg.piece}"
1320
1441
  # Mark this peer as bad.
1321
1442
  torrentData.metainfoPieceState.markPeerBad peer
1322
1443
  torrentData.metainfoPieceState.setPieceRequested(msg.piece, false)
@@ -1347,7 +1468,7 @@ module QuartzTorrent
1347
1468
  end
1348
1469
 
1349
1470
  def sendHaves(torrentData, pieceIndex)
1350
- @logger.debug "Sending Have messages to all connected peers for piece #{pieceIndex}"
1471
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending Have messages to all connected peers for piece #{pieceIndex}"
1351
1472
  torrentData.peers.all.each do |peer|
1352
1473
  next if peer.state != :established || peer.isUs
1353
1474
  withPeersIo(peer, "when sending Have message") do |io|
@@ -1373,7 +1494,7 @@ module QuartzTorrent
1373
1494
  msg = Uninterested.new
1374
1495
  sendMessageToPeer msg, io, peer
1375
1496
  peer.amInterested = false
1376
- @logger.debug "Sending Uninterested message to peer #{peer}"
1497
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending Uninterested message to peer #{peer}"
1377
1498
  end
1378
1499
  end
1379
1500
  end
@@ -1387,7 +1508,7 @@ module QuartzTorrent
1387
1508
  begin
1388
1509
  peer.peerMsgSerializer.serializeTo(msg, io)
1389
1510
  rescue
1390
- @logger.warn "Sending message to peer #{peer} failed: #{$!.message}"
1511
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Sending message to peer #{peer} failed: #{$!.message}"
1391
1512
  end
1392
1513
  end
1393
1514
 
@@ -1427,7 +1548,7 @@ module QuartzTorrent
1427
1548
  next if p.id && p.id == trackerclient.peerId
1428
1549
 
1429
1550
  if ! torrentData.peers.findByAddr(p.ip, p.port)
1430
- @logger.debug "Adding tracker peer #{p} to peers list"
1551
+ @logger.debug "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Adding tracker peer #{p} to peers list"
1431
1552
  break if ! addProc.call(p)
1432
1553
  end
1433
1554
  end
@@ -1440,13 +1561,13 @@ module QuartzTorrent
1440
1561
  @logger.warn "Asked to remove a non-existent torrent #{QuartzTorrent.bytesToHex(infoHash)}"
1441
1562
  return
1442
1563
  end
1443
- @logger.info "Removing torrent #{QuartzTorrent.bytesToHex(infoHash)} and #{deleteFiles ? "will" : "wont"} delete downloaded files."
1564
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent. #{deleteFiles ? "Will" : "Wont"} delete downloaded files."
1444
1565
 
1445
- @logger.info "Removing torrent: no torrentData.metainfoRequestTimer" if ! torrentData.metainfoRequestTimer
1446
- @logger.info "Removing torrent: no torrentData.managePeersTimer" if ! torrentData.managePeersTimer
1447
- @logger.info "Removing torrent: no torrentData.checkMetadataPieceManagerTimer" if ! torrentData.checkMetadataPieceManagerTimer
1448
- @logger.info "Removing torrent: no torrentData.checkPieceManagerTimer" if ! torrentData.checkPieceManagerTimer
1449
- @logger.info "Removing torrent: no torrentData.requestBlocksTimer" if ! torrentData.requestBlocksTimer
1566
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.metainfoRequestTimer" if ! torrentData.metainfoRequestTimer
1567
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.managePeersTimer" if ! torrentData.managePeersTimer
1568
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.checkMetadataPieceManagerTimer" if ! torrentData.checkMetadataPieceManagerTimer
1569
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.checkPieceManagerTimer" if ! torrentData.checkPieceManagerTimer
1570
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Removing torrent: no torrentData.requestBlocksTimer" if ! torrentData.requestBlocksTimer
1450
1571
 
1451
1572
 
1452
1573
  # Stop all timers
@@ -1482,7 +1603,7 @@ module QuartzTorrent
1482
1603
  begin
1483
1604
  torrentData.metainfoPieceState.remove if torrentData.metainfoPieceState
1484
1605
  rescue
1485
- @logger.warn "Deleting metainfo file for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
1606
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Deleting metainfo file for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
1486
1607
  end
1487
1608
 
1488
1609
  if deleteFiles
@@ -1491,15 +1612,17 @@ module QuartzTorrent
1491
1612
  path = @baseDirectory + File::SEPARATOR + torrentData.info.name
1492
1613
  if File.exists? path
1493
1614
  FileUtils.rm_r path
1494
- @logger.info "Deleted #{path}"
1615
+ @logger.info "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Deleted #{path}"
1495
1616
  else
1496
- @logger.warn "Deleting '#{path}' for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
1617
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: Deleting '#{path}' for torrent #{QuartzTorrent.bytesToHex(infoHash)} failed: #{$!}"
1497
1618
  end
1498
1619
  rescue
1499
- @logger.warn "When removing torrent #{QuartzTorrent.bytesToHex(infoHash)} deleting '#{path}' failed because it doesn't exist"
1620
+ @logger.warn "#{QuartzTorrent.bytesToHex(torrentData.infoHash)}: When removing torrent, deleting '#{path}' failed because it doesn't exist"
1500
1621
  end
1501
1622
  end
1502
1623
  end
1624
+
1625
+ dequeue
1503
1626
  end
1504
1627
 
1505
1628
  # Pause or unpause a torrent that we are downloading.
@@ -1512,9 +1635,59 @@ module QuartzTorrent
1512
1635
 
1513
1636
  return if torrentData.paused == value
1514
1637
 
1515
- if value
1516
- torrentData.paused = true
1638
+ torrentData.paused = value
1639
+
1640
+ if !value
1641
+ # On unpause, queue the torrent since there might not be room for it to run.
1642
+ # Make sure it goes to the head of the queue.
1643
+ queue(torrentData, :unshift)
1644
+ end
1645
+
1646
+ setFrozen infoHash, value if ! torrentData.queued
1647
+
1648
+ dequeue
1649
+ end
1650
+
1651
+ # Queue a torrent
1652
+ def queue(torrentData, mode = :queue)
1653
+ return if torrentData.queued
1654
+
1655
+ # Queue the torrent
1656
+ if mode == :unshift
1657
+ @torrentQueue.unshift torrentData
1658
+ else
1659
+ @torrentQueue.push torrentData
1660
+ end
1661
+
1662
+ setFrozen torrentData, true if ! torrentData.paused
1663
+ end
1664
+
1665
+ # Dequeue any torrents that can now run based on available space
1666
+ def dequeue
1667
+ torrents = @torrentQueue.dequeue(@torrentData.values)
1668
+ torrents.each do |torrentData|
1669
+ if torrentData.state == :initializing
1670
+ initTorrent torrentData
1671
+ else
1672
+ setFrozen torrentData, false if ! torrentData.paused
1673
+ end
1674
+ end
1675
+ end
1517
1676
 
1677
+ # Freeze or unfreeze a torrent. If value is true, then we disconnect from all peers for this torrent and forget
1678
+ # the peers. If value is false, we start reconnecting to peers.
1679
+ # Parameter torrent can be an infoHash or TorrentData
1680
+ def setFrozen(torrent, value)
1681
+ torrentData = torrent
1682
+ if ! torrent.is_a?(TorrentData)
1683
+ torrentData = @torrentData[torrent]
1684
+ if ! torrentData
1685
+ @logger.warn "Asked to freeze a non-existent torrent #{QuartzTorrent.bytesToHex(torrent)}"
1686
+ return
1687
+ end
1688
+ end
1689
+
1690
+ if value
1518
1691
  # Disconnect from all peers so we won't reply to any messages.
1519
1692
  torrentData.peers.all.each do |peer|
1520
1693
  if peer.state != :disconnected
@@ -1525,16 +1698,21 @@ module QuartzTorrent
1525
1698
  end
1526
1699
  end
1527
1700
  torrentData.peers.delete peer
1528
- end
1701
+ end
1529
1702
  else
1530
- torrentData.paused = false
1531
-
1532
1703
  # Get our list of peers and start connecting right away
1533
1704
  # Non-recurring and immediate timer
1534
- torrentData.managePeersTimer =
1705
+ torrentData.managePeersTimer =
1535
1706
  @reactor.scheduleTimer(@managePeersPeriod, [:manage_peers, torrentData.infoHash], false, true)
1536
1707
  end
1537
1708
  end
1709
+
1710
+ # Run the passed block in the Reactor's thread. This allows manipulation of torrent data without race
1711
+ # conditions. This method works by scheduling a non-recurring, immediate timer in the reactor that
1712
+ # on expiry runs the passed block.
1713
+ def runInReactorThread(&block)
1714
+ @reactor.scheduleTimer(0, [:runproc, block], false, true)
1715
+ end
1538
1716
 
1539
1717
  end
1540
1718
 
@@ -1543,14 +1721,14 @@ module QuartzTorrent
1543
1721
  class PeerClient
1544
1722
 
1545
1723
  # Create a new PeerClient that will save and load torrent data under the specified baseDirectory.
1546
- def initialize(baseDirectory)
1724
+ def initialize(baseDirectory, maxIncomplete = 5, maxActive = 10)
1547
1725
  @port = 9998
1548
1726
  @handler = nil
1549
1727
  @stopped = true
1550
1728
  @reactor = nil
1551
1729
  @logger = LogManager.getLogger("peerclient")
1552
1730
  @worker = nil
1553
- @handler = PeerClientHandler.new baseDirectory
1731
+ @handler = PeerClientHandler.new baseDirectory, maxIncomplete, maxActive
1554
1732
  @reactor = QuartzTorrent::Reactor.new(@handler, LogManager.getLogger("peerclient.reactor"))
1555
1733
  @toStart = []
1556
1734
  end
@@ -1559,8 +1737,10 @@ module QuartzTorrent
1559
1737
  attr_accessor :port
1560
1738
 
1561
1739
  # Start the PeerClient: open the listening port, and start a new thread to begin downloading/uploading pieces.
1740
+ # If listening fails, an exception of class Errno::EADDRINUSE is thrown.
1562
1741
  def start
1563
1742
  return if ! @stopped
1743
+ @logger.info "Starting"
1564
1744
 
1565
1745
  @reactor.listen("0.0.0.0",@port,:listener_socket)
1566
1746
 
@@ -1602,7 +1782,7 @@ module QuartzTorrent
1602
1782
  addTorrent(trackerclient, metainfo.infoHash, metainfo.info)
1603
1783
  end
1604
1784
 
1605
- # Add a new torrent to manage given an announceUrl and an infoHash.
1785
+ # Add a new torrent to manage given an announceUrl and an infoHash. The announceUrl may be a list.
1606
1786
  # Returns the infoHash of the newly added torrent.
1607
1787
  def addTorrentWithoutMetainfo(announceUrl, infoHash, magnet = nil)
1608
1788
  raise "addTorrentWithoutMetainfo should be called with a Magnet object, not a #{magnet.class}" if magnet && ! magnet.is_a?(MagnetURI)
@@ -1616,7 +1796,7 @@ module QuartzTorrent
1616
1796
  def addTorrentByMagnetURI(magnet)
1617
1797
  raise "addTorrentByMagnetURI should be called with a MagnetURI object, not a #{magnet.class}" if ! magnet.is_a?(MagnetURI)
1618
1798
 
1619
- trackerUrl = magnet.tracker
1799
+ trackerUrl = magnet.trackers
1620
1800
  raise "addTorrentByMagnetURI can't handle magnet links that don't have a tracker URL." if !trackerUrl
1621
1801
 
1622
1802
  addTorrentWithoutMetainfo(trackerUrl, magnet.btInfoHash, magnet)
@@ -1660,6 +1840,22 @@ module QuartzTorrent
1660
1840
  @handler.setUploadDuration(infoHash, seconds)
1661
1841
  end
1662
1842
 
1843
+ # Adjust the bytesUploaded property of the specified torrent by the passed amount.
1844
+ # Adjustment should be an integer. It is added to the current bytesUploaded amount.
1845
+ def adjustBytesUploaded(infoHash, adjustment)
1846
+ return if ! adjustment
1847
+ raise "Bytes uploaded adjustment must be an Integer, not a #{adjustment.class}" if !adjustment.is_a?(Integer)
1848
+ @handler.adjustBytesUploaded(infoHash, adjustment)
1849
+ end
1850
+
1851
+ # Adjust the bytesDownloaded property of the specified torrent by the passed amount.
1852
+ # Adjustment should be an integer. It is added to the current bytesUploaded amount.
1853
+ def adjustBytesDownloaded(infoHash, adjustment)
1854
+ return if ! adjustment
1855
+ raise "Bytes downloaded adjustment must be an Integer, not a #{adjustment.class}" if !adjustment.is_a?(Integer)
1856
+ @handler.adjustBytesDownloaded(infoHash, adjustment)
1857
+ end
1858
+
1663
1859
  # Remove a currently running torrent
1664
1860
  def removeTorrent(infoHash, deleteFiles = false)
1665
1861
  @handler.removeTorrent(infoHash, deleteFiles)