quartz_torrent 0.1.1 → 0.2.0

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/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)