class QuartzTorrent::TrackerClient
This class represents a connection to a tracker for a specific torrent. It can be used to get peers for that torrent.
Attributes
This member can be set to an Alarms
object. If it is, this tracker will raise alarms when it doesn’t get a response, and clear them when it does.
This should be set to a Proc that when called will return a TrackerDynamicRequestParams
object with up-to-date information.
Public Class Methods
Create a new TrackerClient
using the passed information. The announceUrl may be a string or a list.
# File lib/quartz_torrent/trackerclient.rb, line 218 def self.create(announceUrl, infoHash, dataLength = 0, start = true) result = TrackerClient.new(announceUrl, infoHash, dataLength) result.start if start result end
Create a TrackerDriver
for the specified URL. TrackerDriver
is a lower-level interface to the tracker.
# File lib/quartz_torrent/trackerclient.rb, line 233 def self.createDriver(announceUrl, infoHash) result = nil if announceUrl =~ /udp:\/\// result = UdpTrackerDriver.new(announceUrl, infoHash) else result = HttpTrackerDriver.new(announceUrl, infoHash) end result end
Create a TrackerDriver
using the passed Metainfo
object. TrackerDriver
is a lower-level interface to the tracker.
# File lib/quartz_torrent/trackerclient.rb, line 244 def self.createDriverFromMetainfo(metainfo) TrackerClient.createDriver(metainfo.announce, metainfo.infoHash) end
Create a new TrackerClient
using the passed Metainfo
object.
# File lib/quartz_torrent/trackerclient.rb, line 225 def self.createFromMetainfo(metainfo, start = true) announce = [] announce.push metainfo.announce if metainfo.announce announce = announce.concat(metainfo.announceList) if metainfo.announceList create(announce, metainfo.infoHash, metainfo.info.dataLength, start) end
Create a new TrackerClient
@param announceUrl The announce URL of the tracker @param infoHash The infoHash of the torrent we’re tracking
# File lib/quartz_torrent/trackerclient.rb, line 135 def initialize(announceUrl, infoHash, dataLength = 0, maxErrors = 20) @peerId = "-QR0001-" # Azureus style @peerId << Process.pid.to_s @peerId = @peerId + "x" * (20-@peerId.length) @stopped = false @started = false @peers = {} @port = 6881 @peersMutex = Mutex.new @errors = [] @maxErrors = @errors @sleeper = InterruptibleSleep.new # Event to send on the next update @event = :started @worker = nil @logger = LogManager.getLogger("tracker_client") @announceUrlList = announceUrl @infoHash = infoHash @peersChangedListeners = [] @dynamicRequestParamsBuilder = Proc.new do result = TrackerDynamicRequestParams.new(dataLength) result.port = @port result.peerId = @peerId result end @alarms = nil # Convert announceUrl to an array if @announceUrlList.nil? @announceUrlList = [] elsif @announceUrlList.is_a? String @announceUrlList = [@announceUrlList] @announceUrlList.compact! else @announceUrlList = @announceUrlList.flatten.sort.uniq end raise "AnnounceURL contained no valid trackers" if @announceUrlList.size == 0 @announceUrlIndex = 0 end
Public Instance Methods
Add an error to the error list
# File lib/quartz_torrent/trackerclient.rb, line 353 def addError(e) @errors.pop if @errors.length == @maxErrors @errors.push e end
Add a listener that gets notified when the peers list has changed. This listener is called from another thread so be sure to synchronize if necessary. The passed listener should be a proc that takes no arguments.
# File lib/quartz_torrent/trackerclient.rb, line 205 def addPeersChangedListener(listener) @peersChangedListeners.push listener end
Notify the tracker that we have finished downloading all pieces.
# File lib/quartz_torrent/trackerclient.rb, line 347 def completed @event = :completed @sleeper.wake end
Get the last N errors reported
# File lib/quartz_torrent/trackerclient.rb, line 213 def errors @errors end
Return the list of peers that the TrackerClient
knows about. This list grows over time as more peers are reported from the tracker.
# File lib/quartz_torrent/trackerclient.rb, line 194 def peers result = nil @peersMutex.synchronize do result = @peers.keys end result end
# File lib/quartz_torrent/trackerclient.rb, line 208 def removePeersChangedListener(listener) @peersChangedListeners.delete listener end
Start the worker thread
# File lib/quartz_torrent/trackerclient.rb, line 249 def start @stopped = false return if @started @started = true @worker = Thread.new do QuartzTorrent.initThread("trackerclient") @logger.info "Worker thread starting" @event = :started trackerInterval = nil while ! @stopped begin response = nil driver = currentDriver if driver begin @logger.debug "Sending request to tracker #{currentAnnounceUrl}" response = driver.request(@event) @event = nil trackerInterval = response.interval rescue addError $! @logger.info "Request failed due to exception: #{$!}" @logger.debug $!.backtrace.join("\n") changeToNextTracker next @alarms.raise Alarm.new(:tracker, "Tracker request failed: #{$!}") if @alarms end end if response && response.successful? @alarms.clear :tracker if @alarms # Replace the list of peers peersHash = {} @logger.info "Response contained #{response.peers.length} peers" if response.peers.length == 0 @alarms.raise Alarm.new(:tracker, "Response from tracker contained no peers") if @alarms end response.peers.each do |p| peersHash[p] = 1 end @peersMutex.synchronize do @peers = peersHash end if @peersChangedListeners.size > 0 @peersChangedListeners.each{ |l| l.call } end else @logger.info "Response was unsuccessful from tracker: #{response.error}" addError response.error if response @alarms.raise Alarm.new(:tracker, "Unsuccessful response from tracker: #{response.error}") if @alarms && response changeToNextTracker next end # If we have no interval from the tracker yet, and the last request didn't error out leaving us with no peers, # then set the interval to 20 seconds. interval = trackerInterval interval = 20 if ! interval interval = 2 if response && !response.successful? && @peers.length == 0 @logger.debug "Sleeping for #{interval} seconds" @sleeper.sleep interval rescue @logger.warn "Unhandled exception in worker thread: #{$!}" @logger.warn $!.backtrace.join("\n") end end @logger.info "Worker thread shutting down" @logger.info "Sending final update to tracker" begin driver = currentDriver driver.request(:stopped) if driver rescue addError $! @logger.debug "Request failed due to exception: #{$!}" @logger.debug $!.backtrace.join("\n") end @started = false end end
Return true if this TrackerClient
is started, false otherwise.
# File lib/quartz_torrent/trackerclient.rb, line 188 def started? @started end
Stop the worker thread
# File lib/quartz_torrent/trackerclient.rb, line 337 def stop @stopped = true @sleeper.wake if @worker @logger.info "Stop called. Waiting for worker" @logger.info "Worker wait timed out after 2 seconds. Shutting down anyway" if ! @worker.join(2) end end
Private Instance Methods
# File lib/quartz_torrent/trackerclient.rb, line 381 def changeToNextTracker @announceUrlIndex += 1 @logger.info "Changed to next tracker #{currentAnnounceUrl}" sleep 0.5 end
# File lib/quartz_torrent/trackerclient.rb, line 374 def currentAnnounceUrl return nil if @announceUrlList.size == 0 @announceUrlIndex = 0 if @announceUrlIndex > @announceUrlList.size-1 @announceUrlList[@announceUrlIndex] end
# File lib/quartz_torrent/trackerclient.rb, line 363 def currentDriver announceUrl = currentAnnounceUrl return nil if ! announceUrl driver = TrackerClient.createDriver announceUrl, @infoHash driver.dynamicRequestParamsBuilder = @dynamicRequestParamsBuilder if driver driver.port = @port driver.peerId = @peerId driver end
# File lib/quartz_torrent/trackerclient.rb, line 359 def eventValid?(event) event == :started || event == :stopped || event == :completed end