rubytorrent 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,59 @@
1
+ RubyTorrent Design
2
+ ------------------
3
+
4
+ This is pretty sketchy at the moment but it might help if you want to
5
+ do some hacking.
6
+
7
+
8
+ +---------+ +----------------+
9
+ disk <::::::::>| Package | /-| PeerConnection |<=== network ===> peer
10
+ +---------+ | +----------------+
11
+ | |
12
+ +---------+ +------------+ / +----------------+
13
+ | Tracker |--| Controller |----| PeerConnection |<=== network ===> peer
14
+ +---------+ +------------+ \ +----------------+
15
+ / | .
16
+ +--------+ / | .
17
+ | Server |- | .
18
+ +--------+ \
19
+ \ +---------+
20
+ | | Package |<:::::::> disk
21
+ | +---------+
22
+ | |
23
+ +---------+ \+------------+ +----------------+
24
+ | Tracker |--| Controller |----| PeerConnection |<=== network ===> peer
25
+ +---------+ +------------+ \ +----------------+
26
+ |
27
+ | +----------------+
28
+ |--| PeerConnection |<=== network ===> peer
29
+ | +----------------+
30
+ .
31
+ .
32
+ .
33
+
34
+
35
+ Each .torrent download is associated with a Package. A Package is
36
+ composed of several Pieces, each corresponding to a BitTorrent
37
+ piece. A Package provides simple aggregate operations over all the
38
+ Pieces. Each Piece handles writing to and reading from disk (across
39
+ potentially multiple file pointers), as well as dividing its data into
40
+ one or more Blocks. Each Block is an in-memory section of a Piece and
41
+ corresponds to the BitTorrent piece, transferrable across the network.
42
+
43
+ One Server coordinates all BitTorrent downloads. It maintains several
44
+ Controllers, one per .torrent download. The server handles all
45
+ handshaking. It accepts incoming connections, shunting them to the
46
+ appropriate Controller, and creates outgoing ones at the Controllers'
47
+ behest. Each connection to a peer is maintained by a PeerConnection,
48
+ which keeps track of the peer's state and the connection state.
49
+ PeerConnections get empty Blocks from their Controller and send
50
+ requests for them across the wire, and, upon receiving requests from
51
+ the peer, get full Blocks from the Package and transmit them back.
52
+
53
+ The Controller also keeps a Tracker object, which it uses to
54
+ communicate with the tracker.
55
+
56
+ PeerConnections are completely reactive, and are tightly integrated
57
+ with their Controller. They rely on the Controller's heartbeat thread
58
+ to trigger any time-dependent events, and also for propagating any
59
+ messages to other peers.
@@ -0,0 +1,55 @@
1
+ ## dump-metainfo.rb -- command-line .torrent dumper
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require 'rubytorrent'
15
+
16
+ def dump_metainfoinfo(mii)
17
+ if mii.single?
18
+ <<EOS
19
+ length: #{mii.length / 1024}kb
20
+ filename: #{mii.name}
21
+ EOS
22
+ else
23
+ mii.files.map do |f|
24
+ <<EOS
25
+ - filename: #{File.join(mii.name, f.path)}
26
+ length: #{f.length}
27
+ EOS
28
+ end.join + "\n"
29
+ end + <<EOS
30
+ piece length: #{mii.piece_length / 1024}kb
31
+ pieces: #{mii.pieces.length / 20}
32
+ EOS
33
+ end
34
+
35
+ def dump_metainfo(mi)
36
+ <<EOS
37
+ #{dump_metainfoinfo(mi.info).chomp}
38
+ announce: #{mi.announce}
39
+ announce-list: #{(mi.announce_list.nil? ? "<not specified>" : mi.announce_list.map { |x| x.join(', ') }.join('; '))}
40
+ creation date: #{mi.creation_date || "<not specified>"}
41
+ created by: #{mi.created_by || "<not specified>"}
42
+ comment: #{mi.comment || "<not specified>"}
43
+ EOS
44
+ end
45
+
46
+ if ARGV.length == 1
47
+ fn = ARGV[0]
48
+ begin
49
+ puts dump_metainfo(RubyTorrent::MetaInfo.from_location(fn))
50
+ rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
51
+ puts "Can't parse #{fn}: maybe not a .torrent file?"
52
+ end
53
+ else
54
+ puts "Usage: dump-metainfo <filename>"
55
+ end
@@ -0,0 +1,45 @@
1
+ ## dump-peers.rb -- command-line peer lister
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require "rubytorrent"
15
+
16
+ def die(x); $stderr << "#{x}\n" && exit(-1); end
17
+ def dump_peer(p)
18
+ "#{(p.peer_id.nil? ? '<not specified>' : p.peer_id.inspect)} on #{p.ip}:#{p.port}"
19
+ end
20
+
21
+ fn = ARGV.shift or raise "first argument must be .torrent file"
22
+
23
+ mi = nil
24
+ begin
25
+ mi = RubyTorrent::MetaInfo.from_location(fn)
26
+ rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
27
+ die "error parsing metainfo file #{fn}---maybe not a .torrent?"
28
+ end
29
+
30
+ # complete abuse
31
+ mi.trackers.each do |track|
32
+ puts "#{track}:"
33
+
34
+ tc = RubyTorrent::TrackerConnection.new(track, mi.info.sha1, mi.info.total_length, 9999, "rubytorrent.dumppeer") # complete abuse, i know
35
+ begin
36
+ tc.force_refresh
37
+ puts "<no peers>" if tc.peers.length == 0
38
+ tc.peers.each do |p|
39
+ puts dump_peer(p)
40
+ end
41
+ rescue RubyTorrent::TrackerError => e
42
+ puts "error connecting to tracker: #{e.message}"
43
+ end
44
+ end
45
+
@@ -0,0 +1,94 @@
1
+ ## rubytorrent.rb -- top-level RubyTorrent file.
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require 'rubytorrent/util'
15
+ require 'rubytorrent/bencoding'
16
+ require 'rubytorrent/metainfo'
17
+ require 'rubytorrent/tracker'
18
+ require 'rubytorrent/package'
19
+ require 'rubytorrent/server'
20
+
21
+ require "socket"
22
+ Socket.do_not_reverse_lookup = true
23
+
24
+ module RubyTorrent
25
+ VERSION = 0.3
26
+
27
+ ## the top-level class for RubyTorrent.
28
+ class BitTorrent
29
+ include EventSource
30
+ event :trying_peer, :forgetting_peer, :added_peer, :removed_peer,
31
+ :received_block, :sent_block, :have_piece, :discarded_piece, :complete,
32
+ :tracker_connected, :tracker_lost, :requested_block
33
+
34
+ @@server = nil
35
+
36
+ ## hash arguments: host, port, dlratelim, ulratelim
37
+ def initialize(metainfo, *rest)
38
+ args, rest = RubyTorrent::get_args(rest, :host, :port, :dlratelim, :ulratelim, :http_proxy)
39
+ out = rest.shift
40
+ raise ArgumentError, "wrong number of arguments (expected 0/1, got #{rest.length})" unless rest.empty?
41
+
42
+ case metainfo
43
+ when MetaInfo
44
+ @metainfo = metainfo
45
+ when String
46
+ @metainfo = MetaInfo.from_location(metainfo)
47
+ when IO
48
+ @metainfo = MetaInfo.from_stream(metainfo)
49
+ else
50
+ raise ArgumentError, "'metainfo' should be a String, IO or RubyTorrent::MetaInfo object"
51
+ end
52
+
53
+ case out
54
+ when Package
55
+ @package = out
56
+ else
57
+ @package = Package.new(@metainfo, out)
58
+ end
59
+
60
+ unless @@server
61
+ @@server = RubyTorrent::Server.new(args[:host], args[:port], args[:http_proxy])
62
+ @@server.start
63
+ end
64
+
65
+ @cont = @@server.add_torrent(@metainfo, @package, args[:dlratelim], args[:ulratelim])
66
+
67
+ @cont.relay_event self, :trying_peer, :forgetting_peer, :added_peer,
68
+ :removed_peer, :received_block, :sent_block,
69
+ :have_piece, :discarded_piece, :tracker_connected,
70
+ :tracker_lost, :requested_block
71
+ @package.relay_event self, :complete
72
+ end
73
+
74
+ def ip; @@server.ip; end
75
+ def port; @@server.port; end
76
+ def peer_info; @cont.peer_info; end
77
+ def shutdown; @cont.shutdown; end
78
+ def shutdown_all; @@server.shutdown; end
79
+ def complete?; @package.complete?; end
80
+ def bytes_completed; @package.bytes_completed; end
81
+ def percent_completed; @package.percent_completed; end
82
+ def pieces_completed; @package.pieces_completed; end
83
+ def dlrate; @cont.dlrate; end
84
+ def ulrate; @cont.ulrate; end
85
+ def dlamt; @cont.dlamt; end
86
+ def ulamt; @cont.ulamt; end
87
+ def num_pieces; @package.num_pieces; end
88
+ def tracker; (@cont.tracker ? @cont.tracker.url : nil); end
89
+ def num_possible_peers; (@cont.tracker ? @cont.tracker.peers.length : 0); end
90
+ def num_active_peers; @cont.num_peers; end
91
+ def total_bytes; @package.size; end
92
+ end
93
+
94
+ end
@@ -0,0 +1,174 @@
1
+ ## bencoding.rb -- parse and generate bencoded values.
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require 'uri'
15
+ require 'digest/sha1'
16
+
17
+ module RubyTorrent
18
+
19
+ ## we mess in the users' namespaces in this file. there's no good way
20
+ ## around it. i don't think it's too egregious though.
21
+
22
+ class BEncodingError < StandardError; end
23
+
24
+ class BStream
25
+ include Enumerable
26
+
27
+ @@classes = []
28
+
29
+ def initialize(s)
30
+ @s = s
31
+ end
32
+
33
+ def self.register_bencoded_class(c)
34
+ @@classes.push c
35
+ end
36
+
37
+ def each
38
+ happy = true
39
+ begin
40
+ happy = false
41
+ c = @s.getc
42
+ @@classes.each do |klass|
43
+ if klass.bencoded? c
44
+ o = klass.parse_bencoding(c, @s)
45
+ happy = true
46
+ yield o
47
+ break
48
+ end
49
+ end unless c.nil?
50
+ unless happy
51
+ @s.ungetc c unless c.nil?
52
+ end
53
+ end while happy
54
+ self
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ class String
61
+ def to_bencoding
62
+ self.length.to_s + ":" + self.to_s
63
+ end
64
+
65
+ def self.bencoded?(c)
66
+ (?0 .. ?9).include? c
67
+ end
68
+
69
+ def self.parse_bencoding(c, s)
70
+ lens = c.chr
71
+ while ((x = s.getc) != ?:)
72
+ unless (?0 .. ?9).include? x
73
+ s.ungetc x
74
+ raise RubyTorrent::BEncodingError, "invalid bencoded string length #{lens} + #{x}"
75
+ end
76
+ lens += x.chr
77
+ end
78
+ raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless lens.length <= 20
79
+ len = lens.to_i
80
+ raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless len >= 0
81
+ (len > 0 ? s.read(len) : "")
82
+ end
83
+
84
+ RubyTorrent::BStream.register_bencoded_class self
85
+ end
86
+
87
+ class Integer
88
+ def to_bencoding
89
+ "i" + self.to_s + "e"
90
+ end
91
+
92
+ def self.bencoded?(c)
93
+ c == ?i
94
+ end
95
+
96
+ def self.parse_bencoding(c, s)
97
+ ints = ""
98
+ while ((x = s.getc.chr) != 'e')
99
+ raise RubyTorrent::BEncodingError, "invalid bencoded integer #{x.inspect}" unless x =~ /\d|-/
100
+ ints += x
101
+ end
102
+ raise RubyTorrent::BEncodingError, "invalid integer #{ints} (too long)" unless ints.length <= 20
103
+ int = ints.to_i
104
+ raise RubyTorrent::BEncodingError, %{can't parse bencoded integer "#{ints}"} if (int == 0) && (ints !~ /^0$/) #'
105
+ int
106
+ end
107
+
108
+ RubyTorrent::BStream.register_bencoded_class self
109
+ end
110
+
111
+ class Time
112
+ def to_bencoding
113
+ self.to_i.to_bencoding
114
+ end
115
+ end
116
+
117
+ module URI
118
+ def to_bencoding
119
+ self.to_s.to_bencoding
120
+ end
121
+ end
122
+
123
+ class Array
124
+ def to_bencoding
125
+ "l" + self.map { |e| e.to_bencoding }.join + "e"
126
+ end
127
+
128
+ def self.bencoded?(c)
129
+ c == ?l
130
+ end
131
+
132
+ def self.parse_bencoding(c, s)
133
+ ret = RubyTorrent::BStream.new(s).map { |x| x }
134
+ raise RubyTorrent::BEncodingError, "missing list terminator" unless s.getc == ?e
135
+ ret
136
+ end
137
+
138
+ RubyTorrent::BStream.register_bencoded_class self
139
+ end
140
+
141
+ class Hash
142
+ def to_bencoding
143
+ "d" + keys.sort.map do |k|
144
+ v = self[k]
145
+ if v.nil?
146
+ nil
147
+ else
148
+ [k.to_bencoding, v.to_bencoding].join
149
+ end
150
+ end.compact.join + "e"
151
+ end
152
+
153
+ def self.bencoded?(c)
154
+ c == ?d
155
+ end
156
+
157
+ def self.parse_bencoding(c, s)
158
+ ret = {}
159
+ key = nil
160
+ RubyTorrent::BStream.new(s).each do |x|
161
+ if key == nil
162
+ key = x
163
+ else
164
+ ret[key] = x
165
+ key = nil
166
+ end
167
+ end
168
+
169
+ raise RubyTorrent::BEncodingError, "no dictionary terminator" unless s.getc == ?e
170
+ ret
171
+ end
172
+
173
+ RubyTorrent::BStream.register_bencoded_class self
174
+ end
@@ -0,0 +1,610 @@
1
+ ## controller.rb -- cross-peer logic.
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require 'socket'
15
+ require 'thread'
16
+
17
+ module RubyTorrent
18
+
19
+ ## keeps pieces in order
20
+ class PieceOrder
21
+ POP_RECALC_THRESH = 20 # popularity of all pieces is recalculated
22
+ # (expensive sort) when this number of pieces
23
+ # have arrived at any of the peers, OR:
24
+ POP_RECALC_LIMIT = 30 # ... when this many seconds have passed when
25
+ # at least one piece has changed in
26
+ # popularity, or if we're in fuseki mode.
27
+ def initialize(package)
28
+ @package = package
29
+ @order = nil
30
+ @num_changed = 0
31
+ @pop = Array.new(@package.pieces.length, 0)
32
+ @jitter = Array.new(@package.pieces.length) { rand }
33
+ @m = Mutex.new
34
+ @last_recalc = nil
35
+ end
36
+
37
+ ## increment the popularity of a piece
38
+ def inc(i)
39
+ @m.synchronize do
40
+ @pop[i.to_i] += 1
41
+ @num_changed += 1
42
+ end
43
+ end
44
+
45
+ ## increment the popularity of multiple pieces
46
+ def inc_all(bitfield, inc=1)
47
+ @m.synchronize do
48
+ bitfield.each_index do |i|
49
+ if bitfield[i]
50
+ @pop[i] += inc
51
+ @num_changed += 1
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def dec_all(bitfield)
58
+ inc_all(bitfield, -1)
59
+ end
60
+
61
+ def each(in_fuseki, num_peers)
62
+ if (@num_changed > POP_RECALC_THRESH) || @last_recalc.nil? || (((@num_changed > 0) || in_fuseki) && ((Time.now - @last_recalc) > POP_RECALC_LIMIT))
63
+ rt_debug "* reordering pieces: (#@num_changed changed, last recalc #{(@last_recalc.nil? ? '(never)' : (Time.now - @last_recalc).round)}s ago)..."
64
+ recalc_order(in_fuseki, num_peers)
65
+ end
66
+
67
+ @order.each { |i| yield i }
68
+ end
69
+
70
+ private
71
+
72
+ def recalc_order(in_fuseki, num_peers)
73
+ @m.synchronize do
74
+ @num_changed = 0
75
+ @order = (0 ... @pop.length).sort_by do |i|
76
+ p = @package.pieces[i]
77
+ @jitter[i] +
78
+ if p.started? && !p.complete? # always try to complete a started piece
79
+ pri = -1 + p.unclaimed_bytes.to_f / p.length
80
+ rt_debug " piece #{i} is started but not completed => priority #{pri} (#{p.percent_claimed.round}% claimed, #{p.percent_done.round}% done)"
81
+ pri
82
+ elsif p.complete? # don't need these
83
+ # puts " piece #{i} is completed => #{@pop.length}"
84
+ @pop.length # a big number
85
+ elsif in_fuseki # distance from (# peers) / 2
86
+ # puts " piece #{i} has fuseki score #{(@pop[i] - (num_peers / 2)).abs}"
87
+ (@pop[i] - (num_peers / 2)).abs
88
+ else
89
+ # puts " piece #{i} has popularity #{@pop[i]}"
90
+ @pop[i]
91
+ end
92
+ end
93
+ end
94
+ @last_recalc = Time.now
95
+ rt_debug "* new piece priority: " + @order[0...15].map { |x| x.to_s }.join(', ') + " ..."
96
+ end
97
+ end
98
+
99
+ ## The Controller manages all PeerConnections for a single Package. It
100
+ ## instructs them to request blocks, and tells them whether to choke
101
+ ## their connections or not. It also reports progress to the tracker.
102
+ ##
103
+ ## Incoming, post-handshake peer connections are added by the Server
104
+ ## via calling add_connection; deciding to accept these is the
105
+ ## Controller's responsibility, as is connecting to any new peers.
106
+ class Controller
107
+ include EventSource
108
+ extend AttrReaderQ, MinIntervalMethods
109
+
110
+ ## general behavior parameters
111
+ HEARTBEAT = 5 # seconds between iterations of the heartbeat
112
+ MAX_PEERS = 15 # hard limit on the number of peers
113
+ ENDGAME_PIECE_THRESH = 5 # (wild guess) number of pieces remaining
114
+ # before we trigger end-game mode
115
+ FUSEKI_PIECE_THRESH = 2 # number of pieces we must have before
116
+ # getting out of fuseki mode. in fuseki
117
+ # ("opening", if you're not a weiqi/go fan)
118
+ # mode, rather than ranking pieces by
119
+ # rarity, we rank them by how distant their
120
+ # popularity is from (# peers) / 2, and we're
121
+ # also stingly in handing out requests.
122
+ SPAWN_NEW_PEER_THRESH = 0.75 # portion of the download rate above
123
+ # which we'll stop making new peer
124
+ # connections
125
+ RATE_WINDOW = 20 # window size (in seconds) of the rate calculation.
126
+ # presumably this should be the same as the window
127
+ # used in the RateMeter class.
128
+
129
+ ## tracker parameters. when we can't access a tracker, we retry at
130
+ ## DEAD_TRACKER_INITIAL_DELAY seconds and double that after every
131
+ ## failure, capping at DEAD_TRACKER_MAX_DELAY.
132
+ DEAD_TRACKER_INITIAL_INTERVAL = 5
133
+ DEAD_TRACKER_MAX_INTERVAL = 3600
134
+
135
+ ## single peer parameters
136
+ KEEPALIVE_INTERVAL = 120 # seconds of silence before sending a keepalive
137
+ SILENT_DEATH_INTERVAL = 240 # seconds of silence before we drop a peer
138
+ BOREDOM_DEATH_INTERVAL = 120 # seconds of existence with no downloaded data
139
+ # at which we drop a peer in favor of
140
+ # an incoming peer (unless the package
141
+ # is complete)
142
+
143
+ BLOCK_SIZE = 2**15 # send this size blocks. need to find out more
144
+ # about this parameter: how does it affect
145
+ # transfer rates?
146
+
147
+ ## antisnubbing
148
+ ANTISNUB_RATE_THRESH = 1024 # if the total bytes/second across all
149
+ # peers falls below this threshold, we
150
+ # trigger anti-snubbing mode
151
+ ANTISNUB_INTERVAL = 60 # seconds of no blocks from a peer before we
152
+ # add an optimistic unchoke slot when in
153
+ # anti-snubbing mode.
154
+
155
+ ## choking and optimistic unchoking parameters
156
+ NUM_FRIENDS = 4 # number of peers unchoked due to high download rates
157
+ CALC_FRIENDS_INTERVAL = 10 # seconds between recalculating choked
158
+ # status for each peer
159
+ CALC_OPTUNCHOKES_INTERVAL = 30 # seconds between reassigning
160
+ # optimistic unchoked status
161
+ NUM_OPTUNCHOKES = 1 # number of optimistic unchoke slots
162
+ # (not including any temporary ones
163
+ # generated in anti-snubbing mode.
164
+ NEW_OPTUNCHOKE_PROB = 0.5 # peers are ranked by the age of
165
+ # their connection, and optimistic
166
+ # unchoking slots are given with
167
+ # probability p*(1-p)^r, where r
168
+ # is the rank and p is this number.
169
+
170
+ attr_accessor :package, :info_hash, :tracker, :ulratelim, :dlratelim,
171
+ :http_proxy
172
+ attr_reader_q :running
173
+ event :trying_peer, :forgetting_peer, :added_peer, :removed_peer,
174
+ :received_block, :sent_block, :have_piece, :discarded_piece,
175
+ :tracker_connected, :tracker_lost, :requested_block
176
+
177
+ def initialize(server, package, info_hash, trackers, dlratelim=nil, ulratelim=nil, http_proxy=ENV["http_proxy"])
178
+ @server = server
179
+ @info_hash = info_hash
180
+ @package = package
181
+ @trackers = trackers
182
+ @http_proxy = http_proxy
183
+
184
+ @dlratelim = dlratelim
185
+ @ulratelim = ulratelim
186
+
187
+ @peers = [].extend(ArrayShuffle)
188
+ @peers_m = Mutex.new
189
+ @thread = nil
190
+
191
+ @tracker = nil
192
+ @last_tracker_attempt = nil
193
+ @tracker_delay = DEAD_TRACKER_INITIAL_INTERVAL
194
+
195
+ ## friends
196
+ @num_friends = 0
197
+ @num_optunchokes = 0
198
+ @num_snubbed = 0
199
+
200
+ ## keep track of the popularity of the pieces so as to assign
201
+ ## blocks optimally to peers.
202
+ @piece_order = PieceOrder.new @package
203
+
204
+ @running = false
205
+ end
206
+
207
+ def dlrate; @peers.inject(0) { |s, p| s + p.dlrate }; end
208
+ def ulrate; @peers.inject(0) { |s, p| s + p.ulrate }; end
209
+ def dlamt; @peers.inject(0) { |s, p| s + p.dlamt }; end
210
+ def ulamt; @peers.inject(0) { |s, p| s + p.ulamt }; end
211
+ def num_peers; @peers.length; end
212
+
213
+ def start
214
+ raise "already" if @running
215
+
216
+ find_tracker
217
+
218
+ @in_endgame = false
219
+ @in_antisnub = false
220
+ @in_fuseki = false
221
+ @running = true
222
+ @thread = Thread.new do
223
+ while @running
224
+ step
225
+ sleep HEARTBEAT
226
+ end
227
+ end
228
+
229
+ @peers.each { |p| p.start unless p.running? }
230
+
231
+ self
232
+ end
233
+
234
+ def shutdown
235
+ @running = false
236
+ @tracker.stopped unless @tracker.nil? rescue TrackerError
237
+ @thread.join(0.2)
238
+ @peers.each { |c| c.shutdown }
239
+ self
240
+ end
241
+
242
+ def to_s
243
+ "<#{self.class}: package #{@package}>"
244
+ end
245
+
246
+ ## this could be called at any point by the Server, if it receives
247
+ ## incoming peer connections.
248
+ def add_peer(p)
249
+ accept = true
250
+
251
+ if @peers.length >= MAX_PEERS && !@package.complete?
252
+ oldp = @peers.find { |x| !x.running? || ((x.dlamt == 0) && ((Time.now - x.start_time) > BOREDOM_DEATH_INTERVAL)) }
253
+
254
+ if oldp
255
+ rt_debug "killing peer for being boring: #{oldp}"
256
+ oldp.shutdown
257
+ else
258
+ rt_debug "too many peers, ignoring #{p}"
259
+ p.shutdown
260
+ accept = false
261
+ end
262
+ end
263
+
264
+ if accept
265
+ p.on_event(self, :received_block) { |peer, block| received_block(block, peer) }
266
+ p.on_event(self, :peer_has_piece) { |peer, piece| peer_has_piece(piece, peer) }
267
+ p.on_event(self, :peer_has_pieces) { |peer, bitfield| peer_has_pieces(bitfield, peer) }
268
+ p.on_event(self, :sent_block) { |peer, block| send_event(:sent_block, block, peer.name) }
269
+ p.on_event(self, :requested_block) { |peer, block| send_event(:requested_block, block, peer.name) }
270
+
271
+ @peers_m.synchronize do
272
+ @peers.push p
273
+ ## it's important not to call p.start (which triggers the
274
+ ## bitfield message) until it's been added to @peer, such that
275
+ ## any :have messages that might happen from other peers in
276
+ ## the mean time are propagated to it.
277
+ ##
278
+ ## of course that means we need to call p.start within the
279
+ ## mutex context so that the reaper section of the heartbeat
280
+ ## doesn't kill it between push and start.
281
+ ##
282
+ ## ah, the joys of threaded programming.
283
+ p.start if @running
284
+ end
285
+
286
+ send_event(:added_peer, p.name)
287
+ end
288
+ end
289
+
290
+ def received_block(block, peer)
291
+ if @in_endgame
292
+ @peers_m.synchronize { @peers.each { |p| p.cancel block if p.running? && (p != peer)} }
293
+ end
294
+ send_event(:received_block, block, peer.name)
295
+
296
+ piece = @package.pieces[block.pindex] # find corresponding piece
297
+ if piece.complete?
298
+ if piece.valid?
299
+ @peers_m.synchronize { @peers.each { |peer| peer.have_piece piece } }
300
+ send_event(:have_piece, piece)
301
+ else
302
+ rt_warning "#{self}: received data for #{piece} does not match SHA1 hash, discarding"
303
+ send_event(:discarded_piece, piece)
304
+ piece.discard
305
+ end
306
+ end
307
+ end
308
+
309
+ def peer_has_piece(piece, peer)
310
+ @piece_order.inc piece.index
311
+ end
312
+
313
+ def peer_has_pieces(bitfield, peer)
314
+ @piece_order.inc_all bitfield
315
+ end
316
+
317
+ ## yield all desired blocks, in order of desire. called by peers to
318
+ ## refill their queues.
319
+ def claim_blocks
320
+ @piece_order.each(@in_fuseki, @peers.length) do |i|
321
+ p = @package.pieces[i]
322
+ next if p.complete?
323
+ # rt_debug "+ considering piece #{p}"
324
+ if @in_endgame
325
+ p.each_empty_block(BLOCK_SIZE) { |b| yield b }
326
+ else
327
+ p.each_unclaimed_block(BLOCK_SIZE) do |b|
328
+ if yield b
329
+ p.claim_block b
330
+ return if @in_fuseki # fuseki shortcut
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
336
+
337
+ def forget_blocks(blocks)
338
+ # rt_debug "#{self}: forgetting blocks #{blocks.join(', ')}"
339
+ blocks.each { |b| @package.pieces[b.pindex].unclaim_block b }
340
+ end
341
+
342
+ def peer_info
343
+ @peers.map do |p|
344
+ next nil unless p.running?
345
+ {:name => p.name, :seed => p.peer_complete?,
346
+ :dlamt => p.dlamt, :ulamt => p.ulamt,
347
+ :dlrate => p.dlrate, :ulrate => p.ulrate,
348
+ :pending_send => p.pending_send, :pending_recv => p.pending_recv,
349
+ :interested => p.interested?, :peer_interested => p.peer_interested?,
350
+ :choking => p.choking?, :peer_choking => p.peer_choking?,
351
+ :snubbing => p.snubbing?,
352
+ :we_desire => @package.pieces.inject(0) do |s, piece|
353
+ s + (!piece.complete? && p.piece_available?(piece.index) ? 1 : 0)
354
+ end,
355
+ :they_desire => @package.pieces.inject(0) do |s, piece|
356
+ s + (piece.complete? && !p.piece_available?(piece.index) ? 1 : 0)
357
+ end,
358
+ :start_time => p.start_time
359
+ }
360
+ end.compact
361
+ end
362
+
363
+ private
364
+
365
+ def find_tracker
366
+ return if @tracker || (@last_tracker_attempt && (Time.now - @last_tracker_attempt) < @tracker_delay)
367
+
368
+ @last_tracker_attempt = Time.now
369
+ Thread.new do
370
+ @trackers.each do |tracker|
371
+ break if @tracker
372
+ rt_debug "trying tracker #{tracker}"
373
+ tc = TrackerConnection.new(tracker, @info_hash, @package.size, @server.port, @server.id, nil, 50, @http_proxy)
374
+ begin
375
+ @tracker = tc.started
376
+ tc.already_completed if @package.complete?
377
+ @tracker_delay = DEAD_TRACKER_INITIAL_INTERVAL
378
+ send_event(:tracker_connected, tc.url)
379
+ rescue TrackerError => e
380
+ rt_debug "couldn't connect: #{e.message}"
381
+ end
382
+ end
383
+ end
384
+
385
+ @tracker_delay = [@tracker_delay * 2, DEAD_TRACKER_MAX_INTERVAL].min if @tracker.nil?
386
+ rt_warning "couldn't connect to tracker, next try in #@tracker_delay seconds" if @tracker.nil?
387
+ end
388
+
389
+ def add_a_peer
390
+ return false if @tracker.nil? || (@peers.length >= MAX_PEERS) || @package.complete? || (@num_friends >= NUM_FRIENDS) || (@dlratelim && (dlrate > (@dlratelim * SPAWN_NEW_PEER_THRESH)))
391
+
392
+ @tracker.peers.shuffle.each do |peer|
393
+ # rt_debug "]] comparing: #{peer.ip} vs #{@server.ip} and #{peer.port} vs #{@server.port} (tried? #{peer.tried?})"
394
+ next if peer.tried? || ((peer.ip == @server.ip) && (peer.port == @server.port)) rescue next
395
+
396
+ peername = "#{peer.ip}:#{peer.port}"
397
+ send_event(:trying_peer, peername)
398
+
399
+ Thread.new do # this may ultimately result in a call to add_peer
400
+ sleep rand(10)
401
+ rt_debug "=> making outgoing connection to #{peername}"
402
+ begin
403
+ peer.tried = true
404
+ socket = TCPSocket.new(peer.ip, peer.port)
405
+ @server.add_connection(peername, self, socket)
406
+ rescue SocketError, SystemCallError, Timeout::Error => e
407
+ rt_debug "couldn't connect to #{peername}: #{e}"
408
+ send_event(:forgetting_peer, peername)
409
+ end
410
+ end
411
+ break
412
+ end
413
+ true
414
+ end
415
+
416
+ def refresh_tracker
417
+ return if @tracker.nil?
418
+
419
+ @tracker.downloaded = dlamt
420
+ @tracker.uploaded = ulamt
421
+ @tracker.left = @package.size - @package.bytes_completed
422
+ begin
423
+ @tracker.refresh
424
+ rescue TrackerError
425
+ send_event(:tracker_lost, @tracker.url)
426
+ @tracker = nil
427
+ find_tracker # find a new one
428
+ end
429
+ end
430
+
431
+ def calc_friends
432
+ @num_friends = 0
433
+
434
+ if @package.complete?
435
+ @peers.sort_by { |p| -p.ulrate }.each do |p|
436
+ next if p.snubbing? || !p.running?
437
+ p.choke = (@num_friends >= NUM_FRIENDS)
438
+ @num_friends += 1 if p.peer_interested?
439
+ end
440
+ else
441
+ @peers.sort_by { |p| -p.dlrate }.each do |p|
442
+ next if p.snubbing? || !p.running?
443
+ p.choke = (@num_friends >= NUM_FRIENDS)
444
+ @num_friends += 1 if p.peer_interested?
445
+ end
446
+ end
447
+ end
448
+ min_interval :calc_friends, CALC_FRIENDS_INTERVAL
449
+
450
+ def calc_optunchokes
451
+ rt_debug "* calculating optimistic unchokes..."
452
+ @num_optunchokes = 0
453
+
454
+ if @in_antisnub
455
+ ## count up the number of our fair weather friends: peers who
456
+ ## are interested and whom we're not choking, but who haven't
457
+ ## sent us a block for ANTISNUB_INTERVAL seconds. for each of
458
+ ## these, we add an extra optimistic unchoking slot to our usual
459
+ ## NUM_OPTUNCHOKES slots. in actuality that's the number of
460
+ ## friends PLUS the number of optimistic unchokes who are
461
+ ## snubbing us, but that's not a big deal, as long as we cap the
462
+ ## number of extra slots at NUM_FRIENDS.
463
+ @num_optunchokes -= @peers.inject(0) { |s, p| s + (p.running? && p.peer_interested? && !p.choking? && (Time.now - (p.last_recv_block_time || p.start_time) > ANTISNUB_INTERVAL) ? 1 : 0) }
464
+ @num_optunchokes = [-NUM_FRIENDS, @num_optunchokes].max
465
+ rt_debug "* anti-snubbing mode, #{-@num_optunchokes} extra optimistic unchoke slots"
466
+ end
467
+
468
+ ## i love ruby
469
+ @peers.find_all { |p| p.running? }.sort_by { |p| p.start_time }.reverse.each do |p|
470
+ break if @num_optunchokes >= NUM_OPTUNCHOKES
471
+ next if p.snubbing?
472
+ # rt_debug "* considering #{p}: #{p.peer_interested?} and #{@num_optunchokes < NUM_OPTUNCHOKES} and #{rand(0.999) < NEW_OPTUNCHOKE_PROB}"
473
+ if p.peer_interested? && (rand < NEW_OPTUNCHOKE_PROB)
474
+ rt_debug " #{p}: awarded optimistic unchoke"
475
+ p.choke = false
476
+ @num_optunchokes += 1
477
+ end
478
+ end
479
+ end
480
+ min_interval :calc_optunchokes, CALC_OPTUNCHOKES_INTERVAL
481
+
482
+ ## the "heartbeat". all time-based actions are triggered here.
483
+ def step
484
+ ## see if we should be in antisnubbing mode
485
+ if !@package.complete? && (dlrate < ANTISNUB_RATE_THRESH)
486
+ rt_debug "= dl rate #{dlrate} < #{ANTISNUB_RATE_THRESH}, in antisnub mode" if !@in_antisnub
487
+ @in_antisnub = true
488
+ else
489
+ rt_debug "= dl rate #{dlrate} >= #{ANTISNUB_RATE_THRESH}, out of antisnub mode" if @in_antisnub
490
+ @in_antisnub = false
491
+ end
492
+
493
+ ## see if we should be in fuseki mode
494
+ if !@package.complete? && (@package.pieces_completed < FUSEKI_PIECE_THRESH)
495
+ rt_debug "= num pieces #{@package.pieces_completed} < #{FUSEKI_PIECE_THRESH}, in fuseki mode" if !@in_fuseki
496
+ @in_fuseki = true
497
+ else
498
+ rt_debug "= num pieces #{@package.pieces_completed} >= #{FUSEKI_PIECE_THRESH}, out of fuseki mode" if @in_fuseki
499
+ @in_fuseki = false
500
+ end
501
+
502
+ ## see if we should be in endgame mode
503
+ if @package.complete?
504
+ rt_debug "= left endgame mode" if @in_endgame
505
+ @in_endgame = false
506
+ elsif (@package.pieces.length - @package.pieces_completed) <= ENDGAME_PIECE_THRESH
507
+ rt_debug "= have #{@package.pieces_completed} pieces, in endgame mode"
508
+ @in_endgame = true
509
+ end
510
+
511
+ # puts " heartbeat: dlrate #{(dlrate / 1024.0).round}kb/s (lim #{(@dlratelim ? (@dlratelim / 1024.0).round : 'none')}) ulrate #{(ulrate / 1024.0).round}kb/s (lim #{(@ulratelim ? (@ulratelim / 1024.0).round : 'none')}) endgame? #@in_endgame antisnubbing? #@in_antisnub fuseki? #@in_fuseki"
512
+ # @package.pieces.each do |p|
513
+ # next if p.complete? || !p.started?
514
+ # l1 = 0
515
+ # p.each_unclaimed_block(9999999) { |b| l1 += b.length }
516
+ # l2 = 0
517
+ # p.each_empty_block(9999999) { |b| l2 += b.length }
518
+ # puts " heartbeat: #{p.index}: #{l1} unclaimed bytes, #{l2} unfilled bytes"
519
+ # end
520
+
521
+ ## find a tracker if we aren't already connected to one
522
+ find_tracker if @tracker.nil?
523
+
524
+ if @package.complete? # if package is complete...
525
+ ## kill all peers who are complete as well, as per bram's client
526
+ @peers.each { |p| p.shutdown if p.peer_complete? }
527
+ @tracker.completed unless @tracker.nil? || @tracker.sent_completed?
528
+ ## reopen all files as readonly (dunno why, just seems like a
529
+ ## good idea)
530
+ @package.reopen_ro unless @package.ro?
531
+ end
532
+
533
+ ## kill any silent connections, and anyone who hasn't sent or
534
+ ## received data in a long time.
535
+ @peers_m.synchronize do
536
+ @peers.each do |p|
537
+ next unless p.running?
538
+ if ((Time.now - (p.last_send_time || p.start_time)) > SILENT_DEATH_INTERVAL)
539
+ rt_warning "shutting down peer #{p} for silence/boredom"
540
+ p.shutdown
541
+ end
542
+ end
543
+ end
544
+
545
+ ## discard any dead connections
546
+ @peers_m.synchronize do
547
+ @peers.delete_if do |p|
548
+ !p.running? && begin
549
+ p.unregister_events self
550
+ @piece_order.dec_all p.peer_pieces
551
+ rt_debug "burying corpse of #{p}"
552
+ send_event(:removed_peer, p)
553
+ true
554
+ end
555
+ end
556
+ end
557
+
558
+ ## get more peers from the tracker, if all of the following are true:
559
+ ## a) the package is incomplete (i.e. we're downloading, not uploading)
560
+ ## b) we're connected to a tracker
561
+ ## c) we've tried all the peers we've gotten so far
562
+ ## d) the tracker hasn't already reported the maximum number of peers
563
+ if !@package.complete? && @tracker && (@tracker.peers.inject(0) { |s, p| s + (p.tried? ? 0 : 1) } == 0) && (@tracker.numwant <= @tracker.peers.length)
564
+ rt_debug "* getting more peers from the tracker"
565
+ @tracker.numwant += 50
566
+ unless @tracker.in_force_refresh
567
+ Thread.new do
568
+ begin
569
+ @tracker.force_refresh
570
+ rescue TrackerError
571
+ end
572
+ end
573
+ end
574
+ end
575
+
576
+ ## add peer if necessary
577
+ 3.times { add_a_peer } # there's no place like home
578
+
579
+
580
+ ## iterate choking policy
581
+ calc_friends
582
+ calc_optunchokes
583
+
584
+ ## this is needed. sigh.
585
+ break unless @running
586
+
587
+ ## send keepalives
588
+ @peers_m.synchronize { @peers.each { |p| p.send_keepalive if p.running? && p.last_send_time && ((Time.now - p.last_send_time) > KEEPALIVE_INTERVAL) } }
589
+
590
+ ## now we apportion our bandwidth amongst all the peers. we'll go
591
+ ## through them at random, dump everything we can, and move on iff
592
+ ## we don't expect to hit our bandwidth cap.
593
+ dllim = @dlratelim.nil? ? nil : (@dlratelim.to_f * (RATE_WINDOW.to_f + HEARTBEAT)) - (dlrate.to_f * RATE_WINDOW)
594
+ ullim = @ulratelim.nil? ? nil : (@ulratelim.to_f * (RATE_WINDOW.to_f + HEARTBEAT)) - (ulrate.to_f * RATE_WINDOW)
595
+ dl = ul = 0
596
+ @peers.shuffle.each do |p|
597
+ break if (dllim && (dl >= dllim)) || (ullim && (ul >= ullim))
598
+ if p.running?
599
+ pdl, pul = p.send_blocks_and_reqs(dllim && (dllim - dl), ullim && (ullim - ul))
600
+ dl += pdl
601
+ ul += pul
602
+ end
603
+ end
604
+
605
+ ## refresh tracker stats
606
+ refresh_tracker if @tracker
607
+ end
608
+ end
609
+
610
+ end