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.
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
+