quartz_torrent 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/bin/quartztorrent_download +127 -0
  2. data/bin/quartztorrent_download_curses +841 -0
  3. data/bin/quartztorrent_magnet_from_torrent +32 -0
  4. data/bin/quartztorrent_show_info +62 -0
  5. data/lib/quartz_torrent.rb +2 -0
  6. data/lib/quartz_torrent/bitfield.rb +314 -0
  7. data/lib/quartz_torrent/blockstate.rb +354 -0
  8. data/lib/quartz_torrent/classifiedpeers.rb +95 -0
  9. data/lib/quartz_torrent/extension.rb +37 -0
  10. data/lib/quartz_torrent/filemanager.rb +543 -0
  11. data/lib/quartz_torrent/formatter.rb +92 -0
  12. data/lib/quartz_torrent/httptrackerclient.rb +121 -0
  13. data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
  14. data/lib/quartz_torrent/log.rb +132 -0
  15. data/lib/quartz_torrent/magnet.rb +92 -0
  16. data/lib/quartz_torrent/memprofiler.rb +27 -0
  17. data/lib/quartz_torrent/metainfo.rb +221 -0
  18. data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
  19. data/lib/quartz_torrent/peer.rb +145 -0
  20. data/lib/quartz_torrent/peerclient.rb +1627 -0
  21. data/lib/quartz_torrent/peerholder.rb +123 -0
  22. data/lib/quartz_torrent/peermanager.rb +170 -0
  23. data/lib/quartz_torrent/peermsg.rb +502 -0
  24. data/lib/quartz_torrent/peermsgserialization.rb +102 -0
  25. data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
  26. data/lib/quartz_torrent/rate.rb +58 -0
  27. data/lib/quartz_torrent/ratelimit.rb +48 -0
  28. data/lib/quartz_torrent/reactor.rb +949 -0
  29. data/lib/quartz_torrent/regionmap.rb +124 -0
  30. data/lib/quartz_torrent/semaphore.rb +43 -0
  31. data/lib/quartz_torrent/trackerclient.rb +271 -0
  32. data/lib/quartz_torrent/udptrackerclient.rb +70 -0
  33. data/lib/quartz_torrent/udptrackermsg.rb +250 -0
  34. data/lib/quartz_torrent/util.rb +100 -0
  35. 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
+