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

alarms[RW]

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.

dynamicRequestParamsBuilder[RW]

This should be set to a Proc that when called will return a TrackerDynamicRequestParams object with up-to-date information.

peerId[R]
port[RW]

Public Class Methods

create(announceUrl, infoHash, dataLength = 0, start = true) click to toggle source

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
createDriver(announceUrl, infoHash) click to toggle source

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
createDriverFromMetainfo(metainfo) click to toggle source

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
createFromMetainfo(metainfo, start = true) click to toggle source

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
new(announceUrl, infoHash, dataLength = 0, maxErrors = 20) click to toggle source

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

addError(e) click to toggle source

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
addPeersChangedListener(listener) click to toggle source

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
completed() click to toggle source

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
errors() click to toggle source

Get the last N errors reported

# File lib/quartz_torrent/trackerclient.rb, line 213
def errors
  @errors
end
peers() click to toggle source

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
removePeersChangedListener(listener) click to toggle source
# File lib/quartz_torrent/trackerclient.rb, line 208
def removePeersChangedListener(listener)
  @peersChangedListeners.delete listener
end
start() click to toggle source

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
started?() click to toggle source

Return true if this TrackerClient is started, false otherwise.

# File lib/quartz_torrent/trackerclient.rb, line 188
def started?
  @started
end
stop() click to toggle source

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

changeToNextTracker() click to toggle source
# File lib/quartz_torrent/trackerclient.rb, line 381
def changeToNextTracker
  @announceUrlIndex += 1
  @logger.info "Changed to next tracker #{currentAnnounceUrl}"
  sleep 0.5
end
currentAnnounceUrl() click to toggle source
# 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
currentDriver() click to toggle source
# 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
eventValid?(event) click to toggle source
# File lib/quartz_torrent/trackerclient.rb, line 359
def eventValid?(event)
  event == :started || event == :stopped || event == :completed
end