quartz_torrent 0.0.1
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/bin/quartztorrent_download +127 -0
- data/bin/quartztorrent_download_curses +841 -0
- data/bin/quartztorrent_magnet_from_torrent +32 -0
- data/bin/quartztorrent_show_info +62 -0
- data/lib/quartz_torrent.rb +2 -0
- data/lib/quartz_torrent/bitfield.rb +314 -0
- data/lib/quartz_torrent/blockstate.rb +354 -0
- data/lib/quartz_torrent/classifiedpeers.rb +95 -0
- data/lib/quartz_torrent/extension.rb +37 -0
- data/lib/quartz_torrent/filemanager.rb +543 -0
- data/lib/quartz_torrent/formatter.rb +92 -0
- data/lib/quartz_torrent/httptrackerclient.rb +121 -0
- data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
- data/lib/quartz_torrent/log.rb +132 -0
- data/lib/quartz_torrent/magnet.rb +92 -0
- data/lib/quartz_torrent/memprofiler.rb +27 -0
- data/lib/quartz_torrent/metainfo.rb +221 -0
- data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
- data/lib/quartz_torrent/peer.rb +145 -0
- data/lib/quartz_torrent/peerclient.rb +1627 -0
- data/lib/quartz_torrent/peerholder.rb +123 -0
- data/lib/quartz_torrent/peermanager.rb +170 -0
- data/lib/quartz_torrent/peermsg.rb +502 -0
- data/lib/quartz_torrent/peermsgserialization.rb +102 -0
- data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
- data/lib/quartz_torrent/rate.rb +58 -0
- data/lib/quartz_torrent/ratelimit.rb +48 -0
- data/lib/quartz_torrent/reactor.rb +949 -0
- data/lib/quartz_torrent/regionmap.rb +124 -0
- data/lib/quartz_torrent/semaphore.rb +43 -0
- data/lib/quartz_torrent/trackerclient.rb +271 -0
- data/lib/quartz_torrent/udptrackerclient.rb +70 -0
- data/lib/quartz_torrent/udptrackermsg.rb +250 -0
- data/lib/quartz_torrent/util.rb +100 -0
- metadata +195 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$: << "."
|
3
|
+
require 'fileutils'
|
4
|
+
require 'getoptlong'
|
5
|
+
require 'quartz_torrent'
|
6
|
+
|
7
|
+
include QuartzTorrent
|
8
|
+
|
9
|
+
def help
|
10
|
+
puts "Usage: #{$0} [options] <torrent file> [torrent file...]"
|
11
|
+
puts
|
12
|
+
puts "Download torrents and print logs to stdout. One or more torrent files to download should "
|
13
|
+
puts "be passed as arguments."
|
14
|
+
puts
|
15
|
+
puts "Options:"
|
16
|
+
puts " --basedir DIR, -d DIR:"
|
17
|
+
puts " Set the base directory where torrents will be written to. The default is"
|
18
|
+
puts " the current directory."
|
19
|
+
puts
|
20
|
+
puts " --port PORT, -p PORT:"
|
21
|
+
puts" Port to listen on for incoming peer connections. Default is 9997"
|
22
|
+
puts
|
23
|
+
puts " --upload-limit N, -u N:"
|
24
|
+
puts " Limit upload speed for each torrent to the specified rate in bytes per second. "
|
25
|
+
puts " The default is no limit."
|
26
|
+
puts
|
27
|
+
puts " --download-limit N, -d N:"
|
28
|
+
puts " Limit upload speed for each torrent to the specified rate in bytes per second. "
|
29
|
+
puts " The default is no limit."
|
30
|
+
end
|
31
|
+
|
32
|
+
baseDirectory = "."
|
33
|
+
port = 9998
|
34
|
+
uploadLimit = nil
|
35
|
+
downloadLimit = nil
|
36
|
+
|
37
|
+
opts = GetoptLong.new(
|
38
|
+
[ '--basedir', '-d', GetoptLong::REQUIRED_ARGUMENT],
|
39
|
+
[ '--port', '-p', GetoptLong::REQUIRED_ARGUMENT],
|
40
|
+
[ '--upload-limit', '-u', GetoptLong::REQUIRED_ARGUMENT],
|
41
|
+
[ '--download-limit', '-n', GetoptLong::REQUIRED_ARGUMENT],
|
42
|
+
[ '--help', '-h', GetoptLong::NO_ARGUMENT],
|
43
|
+
)
|
44
|
+
|
45
|
+
opts.each do |opt, arg|
|
46
|
+
if opt == '--basedir'
|
47
|
+
baseDirectory = arg
|
48
|
+
elsif opt == '--port'
|
49
|
+
port = arg.to_i
|
50
|
+
elsif opt == '--download-limit'
|
51
|
+
downloadLimit = arg.to_i
|
52
|
+
elsif opt == '--upload-limit'
|
53
|
+
uploadLimit = arg.to_i
|
54
|
+
elsif opt == '--help'
|
55
|
+
help
|
56
|
+
exit 0
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
QuartzTorrent::LogManager.initializeFromEnv
|
61
|
+
LogManager.setup do
|
62
|
+
setLogfile "stdout"
|
63
|
+
setDefaultLevel :info
|
64
|
+
end
|
65
|
+
LogManager.setLevel "peer_manager", :info
|
66
|
+
LogManager.setLevel "tracker_client", :debug
|
67
|
+
LogManager.setLevel "http_tracker_client", :debug
|
68
|
+
LogManager.setLevel "peerclient", :info
|
69
|
+
LogManager.setLevel "peerclient.reactor", :info
|
70
|
+
#LogManager.setLevel "peerclient.reactor", :debug
|
71
|
+
LogManager.setLevel "blockstate", :info
|
72
|
+
LogManager.setLevel "piecemanager", :info
|
73
|
+
LogManager.setLevel "peerholder", :debug
|
74
|
+
LogManager.setLevel "util", :debug
|
75
|
+
LogManager.setLevel "peermsg_serializer", :info
|
76
|
+
|
77
|
+
|
78
|
+
FileUtils.mkdir baseDirectory if ! File.exists?(baseDirectory)
|
79
|
+
|
80
|
+
torrents = ARGV
|
81
|
+
if torrents.size == 0
|
82
|
+
puts "You need to specify a torrent to download."
|
83
|
+
exit 1
|
84
|
+
end
|
85
|
+
|
86
|
+
peerclient = PeerClient.new(baseDirectory)
|
87
|
+
peerclient.port = port
|
88
|
+
|
89
|
+
torrents.each do |torrent|
|
90
|
+
puts "Loading torrent #{torrent}"
|
91
|
+
infoHash = nil
|
92
|
+
# Check if the torrent is a torrent file or a magnet URI
|
93
|
+
if MagnetURI.magnetURI?(torrent)
|
94
|
+
infoHash = peerclient.addTorrentByMagnetURI MagnetURI.new(torrent)
|
95
|
+
else
|
96
|
+
metainfo = Metainfo.createFromFile(torrent)
|
97
|
+
infoHash = peerclient.addTorrentByMetainfo(metainfo)
|
98
|
+
end
|
99
|
+
peerclient.setDownloadRateLimit infoHash, downloadLimit
|
100
|
+
peerclient.setUploadRateLimit infoHash, uploadLimit
|
101
|
+
end
|
102
|
+
|
103
|
+
running = true
|
104
|
+
|
105
|
+
puts "Creating signal handler"
|
106
|
+
Signal.trap('SIGINT') do
|
107
|
+
puts "Got SIGINT. Shutting down."
|
108
|
+
running = false
|
109
|
+
end
|
110
|
+
|
111
|
+
QuartzTorrent.initThread("main")
|
112
|
+
if Signal.list.has_key?('USR1')
|
113
|
+
Signal.trap('SIGUSR1') do
|
114
|
+
QuartzTorrent.logBacktraces
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
puts "Starting peer client"
|
119
|
+
peerclient.start
|
120
|
+
|
121
|
+
while running do
|
122
|
+
sleep 2
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
peerclient.stop
|
127
|
+
|
@@ -0,0 +1,841 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'fileutils'
|
3
|
+
require 'getoptlong'
|
4
|
+
require "ncurses"
|
5
|
+
require 'quartz_torrent'
|
6
|
+
require 'quartz_torrent/memprofiler'
|
7
|
+
require 'quartz_torrent/formatter'
|
8
|
+
require 'quartz_torrent/magnet'
|
9
|
+
|
10
|
+
# Set this to true to profile using ruby-prof.
|
11
|
+
$doProfiling = false
|
12
|
+
|
13
|
+
require 'ruby-prof' if $doProfiling
|
14
|
+
|
15
|
+
include QuartzTorrent
|
16
|
+
|
17
|
+
def getmaxyx(win)
|
18
|
+
y = []
|
19
|
+
x = []
|
20
|
+
Ncurses::getmaxyx win, y, x
|
21
|
+
[y.first,x.first]
|
22
|
+
end
|
23
|
+
|
24
|
+
def getyx(win)
|
25
|
+
y = []
|
26
|
+
x = []
|
27
|
+
Ncurses.getyx win, y, x
|
28
|
+
[y.first,x.first]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Write string to window without allowing wrapping if the string is longer than available space.
|
32
|
+
def waddstrnw(win, str)
|
33
|
+
maxy, maxx = getmaxyx(win)
|
34
|
+
y,x = getyx(win)
|
35
|
+
|
36
|
+
trunc = str[0,maxx-x]
|
37
|
+
|
38
|
+
# If the string ended in a newline, make the truncated string also end in a newline
|
39
|
+
trunc[trunc.length-1,1] = "\n" if str[str.length-1,1] == "\n"
|
40
|
+
Ncurses::waddstr win, trunc
|
41
|
+
end
|
42
|
+
|
43
|
+
def torrentDisplayName(torrent)
|
44
|
+
return "Unknown" if ! torrent
|
45
|
+
name = torrent.recommendedName
|
46
|
+
name = QuartzTorrent::bytesToHex(torrent.infoHash) if ! name || name.length == 0
|
47
|
+
name
|
48
|
+
end
|
49
|
+
|
50
|
+
class WindowSizeChangeDetector
|
51
|
+
def initialize
|
52
|
+
@screenCols = Ncurses.COLS
|
53
|
+
@screenLines = Ncurses.LINES
|
54
|
+
end
|
55
|
+
|
56
|
+
def ifChanged
|
57
|
+
if @screenCols != Ncurses.COLS || @screenLines != Ncurses.LINES
|
58
|
+
yield Ncurses.LINES, Ncurses.COLS
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_accessor :screenCols
|
63
|
+
attr_accessor :screenLines
|
64
|
+
end
|
65
|
+
|
66
|
+
class KeyProcessor
|
67
|
+
def key(key)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Screen
|
72
|
+
def initialize
|
73
|
+
@peerClient = nil
|
74
|
+
@screenManager = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def onKey(k)
|
78
|
+
end
|
79
|
+
|
80
|
+
def screenManager=(m)
|
81
|
+
@screenManager = m
|
82
|
+
end
|
83
|
+
|
84
|
+
attr_accessor :peerClient
|
85
|
+
|
86
|
+
protected
|
87
|
+
def drawHeadline
|
88
|
+
ColorScheme.apply(ColorScheme::HeadingColorPair)
|
89
|
+
Ncurses.attron(Ncurses::A_BOLD)
|
90
|
+
waddstrnw @window, "=== QuartzTorrent Downloader [#{Time.new}] #{$doProfiling ? "PROFILING":""} ===\n\n"
|
91
|
+
Ncurses.attroff(Ncurses::A_BOLD)
|
92
|
+
ColorScheme.apply(ColorScheme::NormalColorPair)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class SummaryScreen < Screen
|
97
|
+
def initialize(window)
|
98
|
+
@window = window
|
99
|
+
@selectedIndex = -1
|
100
|
+
@torrents = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
def draw
|
104
|
+
Ncurses::werase @window
|
105
|
+
Ncurses::wmove(@window, 0,0)
|
106
|
+
drawHeadline
|
107
|
+
|
108
|
+
drawTorrents
|
109
|
+
end
|
110
|
+
|
111
|
+
def onKey(k)
|
112
|
+
if k == Ncurses::KEY_UP
|
113
|
+
@selectedIndex -= 1
|
114
|
+
elsif k == Ncurses::KEY_DOWN
|
115
|
+
@selectedIndex += 1
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def currentTorrent
|
120
|
+
return nil if ! @torrents
|
121
|
+
@selectedIndex = -1 if @selectedIndex < -1 || @selectedIndex >= @torrents.length
|
122
|
+
i = 0
|
123
|
+
@torrents.each do |infohash, torrent|
|
124
|
+
return torrent if i == @selectedIndex
|
125
|
+
i += 1
|
126
|
+
end
|
127
|
+
return nil
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
def summaryLine(state, paused, size, uploadRate, downloadRate, complete, total, progress)
|
132
|
+
if state == :downloading_metainfo
|
133
|
+
" %12s Rate: %6s | %6s Bytes: %4d/%4d Progress: %5s\n" % [state, uploadRate, downloadRate, complete, total, progress]
|
134
|
+
else
|
135
|
+
primaryState = state.to_s
|
136
|
+
secondaryState = ""
|
137
|
+
if state == :running && complete == total
|
138
|
+
secondaryState = "(completed)"
|
139
|
+
elsif (state == :running || state == :uploading) && paused
|
140
|
+
secondaryState = "(paused)"
|
141
|
+
end
|
142
|
+
state = "#{primaryState} #{secondaryState}"
|
143
|
+
" %12s %9s Rate: %6s | %6s Pieces: %4d/%4d Progress: %5s\n" % [state, size, uploadRate, downloadRate, complete, total, progress]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def drawTorrents
|
148
|
+
entries = []
|
149
|
+
|
150
|
+
if ! @peerClient
|
151
|
+
waddstrnw @window, "Loading..."
|
152
|
+
return
|
153
|
+
end
|
154
|
+
|
155
|
+
@torrents = @peerClient.torrentData
|
156
|
+
@torrents.each do |infohash, torrent|
|
157
|
+
name = torrentDisplayName(torrent)
|
158
|
+
#name = torrent.info.name
|
159
|
+
#name = bytesToHex(infohash) if ! name || name.length == 0
|
160
|
+
|
161
|
+
pct = "0%"
|
162
|
+
if torrent.info
|
163
|
+
pct = torrent.completedBytes.to_f / torrent.info.dataLength.to_f * 100.0
|
164
|
+
pct = "%.1f%%" % pct
|
165
|
+
elsif torrent.state == :downloading_metainfo && torrent.metainfoCompletedLength
|
166
|
+
if torrent.metainfoLength
|
167
|
+
pct = torrent.metainfoCompletedLength.to_f / torrent.metainfoLength.to_f * 100.0
|
168
|
+
pct = "%.1f%%" % pct
|
169
|
+
else
|
170
|
+
pct = "?%%"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
state = torrent.state
|
175
|
+
|
176
|
+
completePieces = 0
|
177
|
+
totalPieces = 0
|
178
|
+
dataLength = 0
|
179
|
+
if torrent.state == :downloading_metainfo
|
180
|
+
completePieces = torrent.metainfoCompletedLength if torrent.metainfoCompletedLength
|
181
|
+
totalPieces = torrent.metainfoLength if torrent.metainfoLength
|
182
|
+
else
|
183
|
+
completePieces = torrent.completePieceBitfield.countSet if torrent.completePieceBitfield
|
184
|
+
totalPieces = torrent.info.pieces.length if torrent.info
|
185
|
+
dataLength = torrent.info.dataLength if torrent.info
|
186
|
+
end
|
187
|
+
|
188
|
+
display = [name + "\n"]
|
189
|
+
display.push summaryLine(
|
190
|
+
state,
|
191
|
+
torrent.paused,
|
192
|
+
Formatter.formatSize(dataLength),
|
193
|
+
Formatter.formatSpeed(torrent.uploadRate),
|
194
|
+
Formatter.formatSpeed(torrent.downloadRate),
|
195
|
+
completePieces,
|
196
|
+
totalPieces,
|
197
|
+
pct)
|
198
|
+
entries.push display
|
199
|
+
end
|
200
|
+
@selectedIndex = -1 if @selectedIndex < -1 || @selectedIndex >= entries.length
|
201
|
+
|
202
|
+
index = 0
|
203
|
+
entries.each do |entry|
|
204
|
+
entry.each do |line|
|
205
|
+
Ncurses.attron(Ncurses::A_REVERSE) if index == @selectedIndex
|
206
|
+
waddstrnw @window, line
|
207
|
+
Ncurses.attroff(Ncurses::A_REVERSE) if index == @selectedIndex
|
208
|
+
end
|
209
|
+
index += 1
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class DetailsScreen < Screen
|
215
|
+
def initialize(window)
|
216
|
+
@window = window
|
217
|
+
@infoHash = nil
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
def infoHash=(infoHash)
|
222
|
+
@infoHash = infoHash
|
223
|
+
end
|
224
|
+
|
225
|
+
def draw
|
226
|
+
Ncurses::werase @window
|
227
|
+
Ncurses::wmove(@window, 0,0)
|
228
|
+
str = "nil"
|
229
|
+
if @infoHash
|
230
|
+
str = QuartzTorrent::bytesToHex(@infoHash)
|
231
|
+
end
|
232
|
+
|
233
|
+
drawHeadline
|
234
|
+
|
235
|
+
if ! @peerClient
|
236
|
+
waddstrnw @window, "Loading..."
|
237
|
+
return
|
238
|
+
end
|
239
|
+
|
240
|
+
@torrents = @peerClient.torrentData(@infoHash)
|
241
|
+
torrent = nil
|
242
|
+
@torrents.each do |infohash, t|
|
243
|
+
torrent = t
|
244
|
+
break
|
245
|
+
end
|
246
|
+
if ! torrent
|
247
|
+
waddstrnw @window, "No such torrent."
|
248
|
+
return
|
249
|
+
end
|
250
|
+
|
251
|
+
name = torrentDisplayName(torrent)
|
252
|
+
|
253
|
+
waddstrnw @window, "Details for #{name}\n"
|
254
|
+
|
255
|
+
classified = ClassifiedPeers.new(torrent.peers)
|
256
|
+
unchoked = classified.unchokedInterestedPeers.size + classified.unchokedUninterestedPeers.size
|
257
|
+
choked = classified.chokedInterestedPeers.size + classified.chokedUninterestedPeers.size
|
258
|
+
interested = classified.interestedPeers.size
|
259
|
+
uninterested = classified.uninterestedPeers.size
|
260
|
+
established = classified.establishedPeers.size
|
261
|
+
total = torrent.peers.size
|
262
|
+
|
263
|
+
waddstrnw @window, ("Peers: %3d/%3d choked %3d:%3d interested %3d:%3d\n" % [established, total, choked, unchoked, interested, uninterested] )
|
264
|
+
waddstrnw @window, "\n"
|
265
|
+
|
266
|
+
waddstrnw @window, "Peer details:\n"
|
267
|
+
|
268
|
+
# Order peers by usefulness.
|
269
|
+
torrent.peers.sort! do |a,b|
|
270
|
+
rc = stateSortValue(a.state) <=> stateSortValue(b.state)
|
271
|
+
rc = b.uploadRate <=> a.uploadRate if rc == 0
|
272
|
+
rc = b.downloadRate <=> a.downloadRate if rc == 0
|
273
|
+
rc = chokedSortValue(a.amChoked) <=> chokedSortValue(b.amChoked) if rc == 0
|
274
|
+
rc
|
275
|
+
end
|
276
|
+
|
277
|
+
maxy, maxx = getmaxyx(@window)
|
278
|
+
cury, curx = getyx(@window)
|
279
|
+
torrent.peers.each do |peer|
|
280
|
+
break if cury >= maxy
|
281
|
+
showPeer(peer)
|
282
|
+
cury += 1
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
private
|
287
|
+
def stateSortValue(state)
|
288
|
+
if state == :established
|
289
|
+
0
|
290
|
+
elsif state == :handshaking
|
291
|
+
1
|
292
|
+
else
|
293
|
+
2
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def chokedSortValue(choked)
|
298
|
+
if ! choked
|
299
|
+
0
|
300
|
+
else
|
301
|
+
1
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def showPeer(peer)
|
306
|
+
|
307
|
+
flags = ""
|
308
|
+
flags << (peer.peerChoked ? "chked" : "!chked" )
|
309
|
+
flags << ","
|
310
|
+
flags << (peer.amChoked ? "chking" : "!chking" )
|
311
|
+
flags << ","
|
312
|
+
flags << (peer.peerInterested ? "intsted" : "!intsted" )
|
313
|
+
flags << ","
|
314
|
+
flags << (peer.amInterested ? "intsting" : "!intsting" )
|
315
|
+
|
316
|
+
# host:port, upload, download, state, requestedblocks/maxblocks flags "
|
317
|
+
str = " %-21s Rate: %11s|%-11s %-12s Pending: %4d/%4d %s\n" %
|
318
|
+
[
|
319
|
+
"#{peer.trackerPeer.ip}:#{peer.trackerPeer.port}",
|
320
|
+
Formatter.formatSpeed(peer.uploadRate),
|
321
|
+
Formatter.formatSpeed(peer.downloadRate),
|
322
|
+
peer.state.to_s,
|
323
|
+
peer.requestedBlocks.length,
|
324
|
+
peer.maxRequestedBlocks,
|
325
|
+
flags
|
326
|
+
]
|
327
|
+
|
328
|
+
waddstrnw @window, str
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
class DebugScreen < Screen
|
333
|
+
def initialize(window)
|
334
|
+
@window = window
|
335
|
+
|
336
|
+
@profiler = QuartzTorrent::MemProfiler.new
|
337
|
+
@profiler.trackClass QuartzTorrent::Bitfield
|
338
|
+
@profiler.trackClass QuartzTorrent::BlockInfo
|
339
|
+
@profiler.trackClass QuartzTorrent::BlockState
|
340
|
+
@profiler.trackClass QuartzTorrent::ClassifiedPeers
|
341
|
+
@profiler.trackClass QuartzTorrent::RequestedBlock
|
342
|
+
@profiler.trackClass QuartzTorrent::IncompletePiece
|
343
|
+
@profiler.trackClass QuartzTorrent::FileRegion
|
344
|
+
@profiler.trackClass QuartzTorrent::PieceMapper
|
345
|
+
@profiler.trackClass QuartzTorrent::IOManager
|
346
|
+
@profiler.trackClass QuartzTorrent::PieceIO
|
347
|
+
@profiler.trackClass QuartzTorrent::PieceManager
|
348
|
+
@profiler.trackClass QuartzTorrent::PieceManager::Result
|
349
|
+
@profiler.trackClass QuartzTorrent::Formatter
|
350
|
+
@profiler.trackClass QuartzTorrent::TrackerClient
|
351
|
+
@profiler.trackClass QuartzTorrent::HttpTrackerClient
|
352
|
+
@profiler.trackClass QuartzTorrent::InterruptibleSleep
|
353
|
+
@profiler.trackClass QuartzTorrent::LogManager
|
354
|
+
@profiler.trackClass QuartzTorrent::Metainfo
|
355
|
+
@profiler.trackClass QuartzTorrent::Metainfo::FileInfo
|
356
|
+
@profiler.trackClass QuartzTorrent::Metainfo::Info
|
357
|
+
@profiler.trackClass QuartzTorrent::PieceManagerRequestMetadata
|
358
|
+
@profiler.trackClass QuartzTorrent::ReadRequestMetadata
|
359
|
+
@profiler.trackClass QuartzTorrent::TorrentData
|
360
|
+
@profiler.trackClass QuartzTorrent::TorrentDataDelegate
|
361
|
+
@profiler.trackClass QuartzTorrent::PeerClientHandler
|
362
|
+
@profiler.trackClass QuartzTorrent::PeerClient
|
363
|
+
@profiler.trackClass QuartzTorrent::PeerHolder
|
364
|
+
@profiler.trackClass QuartzTorrent::ManagePeersResult
|
365
|
+
@profiler.trackClass QuartzTorrent::PeerManager
|
366
|
+
@profiler.trackClass QuartzTorrent::PeerRequest
|
367
|
+
@profiler.trackClass QuartzTorrent::PeerHandshake
|
368
|
+
@profiler.trackClass QuartzTorrent::PeerWireMessage
|
369
|
+
@profiler.trackClass QuartzTorrent::KeepAlive
|
370
|
+
@profiler.trackClass QuartzTorrent::Choke
|
371
|
+
@profiler.trackClass QuartzTorrent::Unchoke
|
372
|
+
@profiler.trackClass QuartzTorrent::Interested
|
373
|
+
@profiler.trackClass QuartzTorrent::Uninterested
|
374
|
+
@profiler.trackClass QuartzTorrent::Have
|
375
|
+
@profiler.trackClass QuartzTorrent::BitfieldMessage
|
376
|
+
@profiler.trackClass QuartzTorrent::Request
|
377
|
+
@profiler.trackClass QuartzTorrent::Piece
|
378
|
+
@profiler.trackClass QuartzTorrent::Cancel
|
379
|
+
@profiler.trackClass QuartzTorrent::Peer
|
380
|
+
@profiler.trackClass QuartzTorrent::Rate
|
381
|
+
@profiler.trackClass QuartzTorrent::Handler
|
382
|
+
@profiler.trackClass QuartzTorrent::OutputBuffer
|
383
|
+
@profiler.trackClass QuartzTorrent::IoFacade
|
384
|
+
@profiler.trackClass QuartzTorrent::WriteOnlyIoFacade
|
385
|
+
@profiler.trackClass QuartzTorrent::IOInfo
|
386
|
+
@profiler.trackClass QuartzTorrent::TimerManager
|
387
|
+
@profiler.trackClass QuartzTorrent::TimerManager::TimerInfo
|
388
|
+
@profiler.trackClass QuartzTorrent::Reactor
|
389
|
+
@profiler.trackClass QuartzTorrent::Reactor::InternalTimerInfo
|
390
|
+
@profiler.trackClass QuartzTorrent::RegionMap
|
391
|
+
@profiler.trackClass QuartzTorrent::TrackerPeer
|
392
|
+
@profiler.trackClass QuartzTorrent::TrackerDynamicRequestParams
|
393
|
+
@profiler.trackClass QuartzTorrent::TrackerResponse
|
394
|
+
@profiler.trackClass QuartzTorrent::TrackerClient
|
395
|
+
@profiler.trackClass QuartzTorrent::UdpTrackerClient
|
396
|
+
@profiler.trackClass QuartzTorrent::UdpTrackerMessage
|
397
|
+
@profiler.trackClass QuartzTorrent::UdpTrackerRequest
|
398
|
+
@profiler.trackClass QuartzTorrent::UdpTrackerResponse
|
399
|
+
@profiler.trackClass QuartzTorrent::UdpTrackerConnectRequest
|
400
|
+
@profiler.trackClass QuartzTorrent::UdpTrackerConnectResponse
|
401
|
+
@profiler.trackClass QuartzTorrent::UdpTrackerAnnounceRequest
|
402
|
+
@profiler.trackClass QuartzTorrent::UdpTrackerAnnounceResponse
|
403
|
+
|
404
|
+
@lastRefreshTime = nil
|
405
|
+
@profilerInfo = nil
|
406
|
+
|
407
|
+
end
|
408
|
+
|
409
|
+
def draw
|
410
|
+
Ncurses::werase @window
|
411
|
+
Ncurses::wmove(@window, 0,0)
|
412
|
+
|
413
|
+
drawHeadline
|
414
|
+
|
415
|
+
if @lastRefreshTime.nil? || (Time.new - @lastRefreshTime > 4)
|
416
|
+
@profilerInfo = @profiler.getCounts
|
417
|
+
@lastRefreshTime = Time.new
|
418
|
+
end
|
419
|
+
|
420
|
+
waddstrnw @window, "Memory usage (count of instances of each class):\n"
|
421
|
+
@profilerInfo.each do |clazz, count|
|
422
|
+
waddstrnw @window, "#{clazz}: #{count}\n"
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
end
|
427
|
+
|
428
|
+
class LogScreen < Screen
|
429
|
+
def initialize(window)
|
430
|
+
@window = window
|
431
|
+
end
|
432
|
+
|
433
|
+
def draw
|
434
|
+
Ncurses::werase @window
|
435
|
+
Ncurses::wmove(@window, 0,0)
|
436
|
+
waddstrnw @window, "LOG:\n"
|
437
|
+
waddstrnw @window, "Blah blah blah"
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
class HelpScreen < Screen
|
442
|
+
def initialize(window)
|
443
|
+
@window = window
|
444
|
+
end
|
445
|
+
|
446
|
+
def draw
|
447
|
+
Ncurses::werase @window
|
448
|
+
Ncurses::wmove(@window, 0,0)
|
449
|
+
drawHeadline
|
450
|
+
waddstrnw @window, "\n\n"
|
451
|
+
waddstrnw @window, "Global Keys:\n\n"
|
452
|
+
waddstrnw @window, " '?': Show this help screen\n"
|
453
|
+
waddstrnw @window, " 's': Show the summary screen\n"
|
454
|
+
waddstrnw @window, " 'd': Show the memory debug screen\n"
|
455
|
+
waddstrnw @window, " <CTRL-C>: Exit\n"
|
456
|
+
waddstrnw @window, "\nSummary Screen Keys:\n\n"
|
457
|
+
waddstrnw @window, " <UP>,<DOWN>: Change which torrent is currently selected\n"
|
458
|
+
waddstrnw @window, " <ENTER>: Show the torrent details screen for the currently selected torrent\n"
|
459
|
+
waddstrnw @window, " 'p': Pause/Unpause the currently selected torrent\n"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
class AddScreen < Screen
|
464
|
+
def initialize(window)
|
465
|
+
@window = window
|
466
|
+
@error = nil
|
467
|
+
end
|
468
|
+
|
469
|
+
def draw
|
470
|
+
Ncurses::werase @window
|
471
|
+
Ncurses::wmove(@window, 0,0)
|
472
|
+
|
473
|
+
drawHeadline
|
474
|
+
waddstrnw @window, "\n\n"
|
475
|
+
|
476
|
+
drawError
|
477
|
+
waddstrnw @window, "Enter the torrent to add: "
|
478
|
+
setTerminalKeysMode :normal
|
479
|
+
str = ''
|
480
|
+
Ncurses::getstr str
|
481
|
+
setTerminalKeysMode :event
|
482
|
+
|
483
|
+
if str.length == 0
|
484
|
+
@screenManager.set :summary
|
485
|
+
else
|
486
|
+
$log.puts "Adding torrent '#{str}'"
|
487
|
+
|
488
|
+
begin
|
489
|
+
@peerClient.addTorrentFromClient $settings, str if @peerClient
|
490
|
+
@screenManager.set :summary
|
491
|
+
rescue
|
492
|
+
@error = "Adding torrent failed: #{$!}\n\n"
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
private
|
498
|
+
def drawError
|
499
|
+
waddstrnw @window, @error if @error
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
class ScreenManager
|
504
|
+
def initialize
|
505
|
+
@screens = {}
|
506
|
+
@current = nil
|
507
|
+
@currentId = nil
|
508
|
+
end
|
509
|
+
|
510
|
+
def add(id, screen)
|
511
|
+
@screens[id] = screen
|
512
|
+
screen.screenManager = self
|
513
|
+
end
|
514
|
+
|
515
|
+
def set(id)
|
516
|
+
@current = @screens[id]
|
517
|
+
@currentId = id
|
518
|
+
draw
|
519
|
+
end
|
520
|
+
|
521
|
+
def get(id)
|
522
|
+
@screens[id]
|
523
|
+
end
|
524
|
+
|
525
|
+
def draw
|
526
|
+
@current.draw if @current
|
527
|
+
end
|
528
|
+
|
529
|
+
def onKey(k)
|
530
|
+
@current.onKey(k) if @current
|
531
|
+
end
|
532
|
+
|
533
|
+
def peerClient=(peerClient)
|
534
|
+
@screens.each do |k,v|
|
535
|
+
v.peerClient=peerClient
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
def currentId
|
540
|
+
@currentId
|
541
|
+
end
|
542
|
+
|
543
|
+
attr_reader :current
|
544
|
+
end
|
545
|
+
|
546
|
+
class ColorScheme
|
547
|
+
NormalColorPair = 1
|
548
|
+
HeadingColorPair = 2
|
549
|
+
|
550
|
+
def self.init
|
551
|
+
Ncurses.init_pair(NormalColorPair, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLUE)
|
552
|
+
Ncurses.init_pair(HeadingColorPair, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLUE)
|
553
|
+
end
|
554
|
+
|
555
|
+
def self.apply(colorPair)
|
556
|
+
Ncurses.attron(Ncurses::COLOR_PAIR(colorPair));
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
def setTerminalKeysMode(mode)
|
561
|
+
if mode == :normal
|
562
|
+
# Turn on line-buffering
|
563
|
+
Ncurses::nocbreak
|
564
|
+
# Do display characters back
|
565
|
+
Ncurses::echo
|
566
|
+
elsif mode == :event
|
567
|
+
# Turn off line-buffering
|
568
|
+
Ncurses::cbreak
|
569
|
+
# Don't display characters back
|
570
|
+
Ncurses::noecho
|
571
|
+
# Don't block on reading characters (block 1 tenths of seconds)
|
572
|
+
Ncurses.halfdelay(1)
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
def initializeCurses
|
577
|
+
# Initialize Ncurses
|
578
|
+
Ncurses.initscr
|
579
|
+
|
580
|
+
# Initialize colors
|
581
|
+
Ncurses.start_color
|
582
|
+
$log.puts "Terminal supports #{Ncurses.COLORS} colors"
|
583
|
+
|
584
|
+
ColorScheme.init
|
585
|
+
|
586
|
+
#Ncurses.init_pair(ColorScheme::NormalColorPair, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLUE)
|
587
|
+
|
588
|
+
ColorScheme.apply(ColorScheme::NormalColorPair)
|
589
|
+
#Ncurses.attron(Ncurses::COLOR_PAIR(1));
|
590
|
+
|
591
|
+
setTerminalKeysMode :event
|
592
|
+
|
593
|
+
# Interpret arrow keys as one character
|
594
|
+
Ncurses.keypad Ncurses::stdscr, true
|
595
|
+
|
596
|
+
|
597
|
+
# Set the window background (used when clearing)
|
598
|
+
Ncurses::wbkgdset(Ncurses::stdscr, Ncurses::COLOR_PAIR(ColorScheme::NormalColorPair))
|
599
|
+
end
|
600
|
+
|
601
|
+
def initializeLogging(file)
|
602
|
+
QuartzTorrent::LogManager.initializeFromEnv
|
603
|
+
FileUtils.rm file if File.exists?(file)
|
604
|
+
LogManager.setup do
|
605
|
+
setLogfile file
|
606
|
+
setDefaultLevel :info
|
607
|
+
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
|
+
end
|
620
|
+
|
621
|
+
def help
|
622
|
+
puts "Usage: #{$0} [options] [torrent file...]"
|
623
|
+
puts
|
624
|
+
puts "Download torrents using a simple curses UI. One or more torrent files to download should "
|
625
|
+
puts "be passed as arguments."
|
626
|
+
puts
|
627
|
+
puts "Options:"
|
628
|
+
puts " --basedir DIR, -d DIR:"
|
629
|
+
puts " Set the base directory where torrents will be written to. The default is"
|
630
|
+
puts " the current directory."
|
631
|
+
puts
|
632
|
+
puts " --port PORT, -p PORT:"
|
633
|
+
puts" Port to listen on for incoming peer connections. Default is 9997"
|
634
|
+
puts
|
635
|
+
puts " --upload-limit N, -u N:"
|
636
|
+
puts " Limit upload speed for each torrent to the specified rate in bytes per second. "
|
637
|
+
puts " The default is no limit."
|
638
|
+
puts
|
639
|
+
puts " --download-limit N, -d N:"
|
640
|
+
puts " Limit upload speed for each torrent to the specified rate in bytes per second. "
|
641
|
+
puts " The default is no limit."
|
642
|
+
puts
|
643
|
+
puts " --ratio N, -r N:"
|
644
|
+
puts " Upload ratio. If we have completed downloading the torrent, when we have uploaded "
|
645
|
+
puts " N times the size of the torrent, stop uploading."
|
646
|
+
puts " The default is to never stop uploading."
|
647
|
+
puts
|
648
|
+
puts " --debug-tty T, -t T:"
|
649
|
+
puts " Use the specified TTY device file for printing debug info. This should be something"
|
650
|
+
puts " like '/dev/pts/3'"
|
651
|
+
end
|
652
|
+
|
653
|
+
class Settings
|
654
|
+
def initialize
|
655
|
+
@baseDirectory = "."
|
656
|
+
@port = 9997
|
657
|
+
@uploadLimit = nil
|
658
|
+
@downloadLimit = nil
|
659
|
+
@uploadRatio = nil
|
660
|
+
@logfile = "/tmp/download_torrent_curses.log"
|
661
|
+
end
|
662
|
+
|
663
|
+
attr_accessor :baseDirectory
|
664
|
+
attr_accessor :port
|
665
|
+
attr_accessor :uploadLimit
|
666
|
+
attr_accessor :downloadLimit
|
667
|
+
attr_accessor :uploadRatio
|
668
|
+
attr_accessor :logfile
|
669
|
+
attr_accessor :debugTTY
|
670
|
+
end
|
671
|
+
|
672
|
+
class PeerClient
|
673
|
+
def addTorrentFromClient(settings, torrent)
|
674
|
+
# Check if the torrent is a torrent file or a magnet URI
|
675
|
+
infoHash = nil
|
676
|
+
if MagnetURI.magnetURI?(torrent)
|
677
|
+
infoHash = addTorrentByMagnetURI MagnetURI.new(torrent)
|
678
|
+
else
|
679
|
+
metainfo = Metainfo.createFromFile(torrent)
|
680
|
+
infoHash = addTorrentByMetainfo(metainfo)
|
681
|
+
end
|
682
|
+
setDownloadRateLimit infoHash, settings.downloadLimit
|
683
|
+
setUploadRateLimit infoHash, settings.uploadLimit
|
684
|
+
setUploadRatio infoHash, settings.uploadRatio
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
#### MAIN
|
689
|
+
|
690
|
+
$log = $stdout
|
691
|
+
|
692
|
+
exception = nil
|
693
|
+
cursesInitialized = false
|
694
|
+
$settings = Settings.new
|
695
|
+
begin
|
696
|
+
|
697
|
+
opts = GetoptLong.new(
|
698
|
+
[ '--basedir', '-d', GetoptLong::REQUIRED_ARGUMENT],
|
699
|
+
[ '--port', '-p', GetoptLong::REQUIRED_ARGUMENT],
|
700
|
+
[ '--upload-limit', '-u', GetoptLong::REQUIRED_ARGUMENT],
|
701
|
+
[ '--download-limit', '-n', GetoptLong::REQUIRED_ARGUMENT],
|
702
|
+
[ '--help', '-h', GetoptLong::NO_ARGUMENT],
|
703
|
+
[ '--ratio', '-r', GetoptLong::REQUIRED_ARGUMENT],
|
704
|
+
[ '--debug-tty', '-t', GetoptLong::REQUIRED_ARGUMENT],
|
705
|
+
)
|
706
|
+
|
707
|
+
opts.each do |opt, arg|
|
708
|
+
if opt == '--basedir'
|
709
|
+
$settings.baseDirectory = arg
|
710
|
+
elsif opt == '--port'
|
711
|
+
$settings.port = arg.to_i
|
712
|
+
elsif opt == '--download-limit'
|
713
|
+
$settings.downloadLimit = arg.to_i
|
714
|
+
elsif opt == '--upload-limit'
|
715
|
+
$settings.uploadLimit = arg.to_i
|
716
|
+
elsif opt == '--help'
|
717
|
+
help
|
718
|
+
exit 0
|
719
|
+
elsif opt == '--ratio'
|
720
|
+
$settings.uploadRatio = arg.to_f
|
721
|
+
elsif opt == '--debug-tty'
|
722
|
+
$log = File.open arg, "w"
|
723
|
+
end
|
724
|
+
end
|
725
|
+
|
726
|
+
torrents = ARGV
|
727
|
+
|
728
|
+
$log = File.open("/dev/null","w") if $log == $stdout
|
729
|
+
|
730
|
+
|
731
|
+
initializeCurses
|
732
|
+
cursesInitialized = true
|
733
|
+
initializeLogging($settings.logfile)
|
734
|
+
|
735
|
+
sumScr = SummaryScreen.new(Ncurses::stdscr)
|
736
|
+
|
737
|
+
scrManager = ScreenManager.new
|
738
|
+
scrManager.add :summary, SummaryScreen.new(Ncurses::stdscr)
|
739
|
+
scrManager.add :details, DetailsScreen.new(Ncurses::stdscr)
|
740
|
+
scrManager.add :log, LogScreen.new(Ncurses::stdscr)
|
741
|
+
scrManager.add :debug, DebugScreen.new(Ncurses::stdscr)
|
742
|
+
scrManager.add :help, HelpScreen.new(Ncurses::stdscr)
|
743
|
+
scrManager.add :add, AddScreen.new(Ncurses::stdscr)
|
744
|
+
scrManager.set :summary
|
745
|
+
|
746
|
+
peerclient = QuartzTorrent::PeerClient.new($settings.baseDirectory)
|
747
|
+
peerclient.port = $settings.port
|
748
|
+
|
749
|
+
torrents.each do |torrent|
|
750
|
+
peerclient.addTorrentFromClient($settings, torrent)
|
751
|
+
end
|
752
|
+
|
753
|
+
scrManager.peerClient = peerclient
|
754
|
+
|
755
|
+
running = true
|
756
|
+
|
757
|
+
#puts "Creating signal handler"
|
758
|
+
Signal.trap('SIGINT') do
|
759
|
+
puts "Got SIGINT. Shutting down."
|
760
|
+
running = false
|
761
|
+
end
|
762
|
+
|
763
|
+
QuartzTorrent.initThread("main")
|
764
|
+
if Signal.list.has_key?('USR1')
|
765
|
+
Signal.trap('SIGUSR1') do
|
766
|
+
QuartzTorrent.logBacktraces
|
767
|
+
end
|
768
|
+
end
|
769
|
+
|
770
|
+
RubyProf.start if $doProfiling
|
771
|
+
|
772
|
+
#puts "Starting peer client"
|
773
|
+
peerclient.start
|
774
|
+
|
775
|
+
while running
|
776
|
+
scrManager.draw
|
777
|
+
Ncurses::refresh
|
778
|
+
key = Ncurses.getch
|
779
|
+
# Since halfdelay actually sleeps up to 1/10 second we can loop back without sleeping and still not burn too much CPU.
|
780
|
+
if key != Ncurses::ERR
|
781
|
+
if key < 256
|
782
|
+
if key.chr == 'l'
|
783
|
+
scrManager.set :log
|
784
|
+
elsif key.chr == 's'
|
785
|
+
scrManager.set :summary
|
786
|
+
elsif key.chr == 'd'
|
787
|
+
scrManager.set :debug
|
788
|
+
elsif key.chr == 'h' || key.chr == '?'
|
789
|
+
scrManager.set :help
|
790
|
+
elsif key.chr == 'a'
|
791
|
+
scrManager.set :add
|
792
|
+
elsif key.chr == "\n"
|
793
|
+
# Details
|
794
|
+
if scrManager.currentId == :summary
|
795
|
+
torrent = scrManager.current.currentTorrent
|
796
|
+
if torrent
|
797
|
+
detailsScreen = scrManager.get :details
|
798
|
+
detailsScreen.infoHash = torrent.infoHash
|
799
|
+
scrManager.set :details
|
800
|
+
end
|
801
|
+
end
|
802
|
+
elsif key.chr == "p"
|
803
|
+
# Pause/unpause
|
804
|
+
if scrManager.currentId == :summary
|
805
|
+
torrent = scrManager.current.currentTorrent
|
806
|
+
peerclient.setPaused(torrent.infoHash, !torrent.paused) if torrent
|
807
|
+
end
|
808
|
+
else
|
809
|
+
scrManager.onKey key
|
810
|
+
end
|
811
|
+
else
|
812
|
+
scrManager.onKey key
|
813
|
+
end
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
817
|
+
peerclient.stop
|
818
|
+
|
819
|
+
if $doProfiling
|
820
|
+
result = RubyProf.stop
|
821
|
+
File.open("/tmp/quartz_reactor.prof","w") do |file|
|
822
|
+
file.puts "FLAT PROFILE"
|
823
|
+
printer = RubyProf::FlatPrinter.new(result)
|
824
|
+
printer.print(file)
|
825
|
+
file.puts "GRAPH PROFILE"
|
826
|
+
printer = RubyProf::GraphPrinter.new(result)
|
827
|
+
printer.print(file, {})
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
rescue LoadError
|
832
|
+
exception = $!
|
833
|
+
rescue
|
834
|
+
exception = $!
|
835
|
+
end
|
836
|
+
|
837
|
+
# Restore previous screen
|
838
|
+
Ncurses.endwin if cursesInitialized
|
839
|
+
|
840
|
+
raise exception if exception
|
841
|
+
|