leecher 0.0.1 → 0.1.0

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.
@@ -6,6 +6,7 @@ module Leecher
6
6
  require "optparse"
7
7
  require "yaml"
8
8
  require "erubis"
9
+ require "fileutils"
9
10
  require "leecher/log"
10
11
  require "leecher/client"
11
12
 
@@ -17,7 +18,7 @@ module Leecher
17
18
 
18
19
 
19
20
  def initialize()
20
- @config_fn = File.join(ENV["HOME"], ".leecher.yml")
21
+ @config_dirname = File.join(ENV["HOME"], ".leecher")
21
22
  @foreground = false
22
23
  end
23
24
 
@@ -29,10 +30,10 @@ module Leecher
29
30
  o.separator("")
30
31
  o.separator("options:")
31
32
 
32
- o.on("--config=FILE",
33
- "File containing settings",
34
- "Default: #{@config_fn}") do |v|
35
- @config_fn = v
33
+ o.on("--config=DIR",
34
+ "Dir containing settings",
35
+ "Default: #{@config_dirname}") do |v|
36
+ @config_dirname = v
36
37
  end
37
38
 
38
39
  o.on("-f", "--[no-]foreground",
@@ -41,12 +42,6 @@ module Leecher
41
42
  @foreground = v
42
43
  end
43
44
 
44
- o.on("--getting-started",
45
- "Run a tutorial which will give you a config") do
46
- do_getting_started()
47
- Kernel.exit!(RC_OK)
48
- end
49
-
50
45
  o.separator("")
51
46
  o.on("-h", "--help", "You're reading it :-)") do
52
47
  puts o
@@ -59,28 +54,41 @@ module Leecher
59
54
  end
60
55
 
61
56
  def do_getting_started()
62
- log.info("We'll ask you a few questions then write you a config in #{@config_fn}")
57
+ log.info("We'll ask you a few questions then write you a config in #{@config_dirname}")
63
58
  log.info(".. you can change where your config lives with -c/--config")
64
59
 
65
- STDOUT.puts("\n\n")
66
- config = Hash.new()
60
+ STDOUT.puts("\n")
61
+ config = { :config_dirname => @config_dirname }
67
62
  [
68
63
  [:username, "your username on the website?", :required],
69
64
  [:password, ".. and the password for that user?", :password],
70
- [:log_fn, "filename to log to:", :disable],
65
+ [:log_fn, "filename to log to:", File.join(@config_dirname, "log")],
71
66
  [:host, "where does the amqp queue live?", "nzb.trim.za.net"],
72
67
  [:aria2_bin, "which aria2 must I use?", %x{which aria2c}.strip()],
68
+ [:aria2_dir, "what whould you like your default download directory to be?", Dir.pwd],
69
+ [:aria2_connections, "how many connections would you like to download with?", 5],
70
+ [:aria2_rpc_user, "what is your aria2 rpc username?", :required],
71
+ [:aria2_rpc_password, "what is your aria2 rpc password?", :password],
72
+ [:aria2_rpc_port, "what is your aria2 rpc port?", 6800],
73
73
  ].each do |setting, question, default|
74
74
  config[setting] = get_user_input(question, default)
75
75
  end
76
-
76
+
77
+ FileUtils.mkdir_p(@config_dirname)
77
78
  erb = Erubis::Eruby.new(File.read(File.join(File.dirname(__FILE__),
78
79
  "../config/client.yml")))
79
- File.open(@config_fn, "w") do |f|
80
+ File.open(File.join(@config_dirname, "config.yml"), "w") do |f|
81
+ f.puts(erb.result(config))
82
+ end
83
+
84
+ erb = Erubis::Eruby.new(File.read(File.join(File.dirname(__FILE__),
85
+ "../config/aria2.conf")))
86
+
87
+ File.open(File.join(@config_dirname, "aria2.conf"), "w") do |f|
80
88
  f.puts(erb.result(config))
81
89
  end
82
90
 
83
- STDOUT.puts("\n\n")
91
+ STDOUT.puts("\n")
84
92
  log.info("Looking good, now take us for a real spin!")
85
93
  end
86
94
 
@@ -124,7 +132,7 @@ module Leecher
124
132
  end
125
133
 
126
134
  def run()
127
- config = load_config(@config_fn)
135
+ config = load_config(@config_dirname)
128
136
  unless @foreground
129
137
  log.info("Fading into the ether ..")
130
138
  make_daemon(config[:daemon])
@@ -143,17 +151,20 @@ module Leecher
143
151
  @log ||= Leecher::Log.new()
144
152
  end
145
153
 
146
- def load_config(config_fn)
147
- config = YAML.load_file(config_fn)
154
+ def load_config(config_dirname)
155
+ unless File.exist?(config_dirname)
156
+ log.info("You don't have a leecher config setup")
157
+ do_getting_started()
158
+ end
159
+
160
+ config = YAML.load_file(File.join(config_dirname, "config.yml"))
148
161
  if (log_conf = config[:log])
149
162
  @log = Leecher::Log.new(config[:log][:level],
150
163
  config[:log][:filename])
151
164
  end
152
165
  config
153
166
  rescue Errno::ENOENT
154
- log.error("No such config file #{config_fn.inspect()}")
155
- log.info("New user? Try #{Leecher::Log::ANSI_GREEN}"\
156
- "--getting-started#{Leecher::Log::ANSI_NORMAL}")
167
+ log.error("No such config file #{config_dirname.inspect()}")
157
168
  Kernel.exit!(RC_CONF_MISSING)
158
169
  end
159
170
 
@@ -0,0 +1,15 @@
1
+ dir=<%= aria2_dir %>
2
+ log=/tmp/aria.log
3
+ check-integrity=true
4
+ max-connection-per-server=<%= aria2_connections %>
5
+ min-split-size=1M
6
+ follow-torrent=mem
7
+ max-overall-upload-limit=30K
8
+ follow-metalink=mem
9
+ metalink-servers=<%= aria2_connections %>
10
+ rpc-listen-all=false
11
+ rpc-listen-port=<%= aria2_rpc_port %>
12
+ rpc-passwd=<%= aria2_rpc_password %>
13
+ rpc-user=<%= aria2_rpc_user %>
14
+ allow-piece-length-change=true
15
+ save-session=<%= File.join(config_dirname, "session.aria2") %>
@@ -19,11 +19,14 @@
19
19
  :heartbeat: 300
20
20
  :connect_timeout: 5
21
21
  :aria2_opts:
22
+ :config: <%= File.join(config_dirname, "aria2.conf") %>
22
23
  :bin: <%= aria2_bin %>
23
- :args: [-x5, -V, --follow-metalink=mem]
24
+ :args: [-D, --enable-rpc]
25
+ :user: <%= aria2_rpc_user %>
26
+ :password: <%= aria2_rpc_password %>
27
+ :port: <%= aria2_rpc_port %>
24
28
  :daemon:
25
- :chdir: nil
29
+ :chdir: <%= File.join(config_dirname) %>
26
30
  :umask: 0000
27
- :stdin: nil
28
- :stdout: nil
29
- :stderr: nil
31
+ :stdout: <%= File.join(config_dirname, "stdout") %>
32
+ :stderr: <%= File.join(config_dirname, "stderr") %>
@@ -4,10 +4,25 @@ module Leecher
4
4
 
5
5
  require "bunny"
6
6
  require "leecher/shellout"
7
+ require 'xmlrpc/client'
7
8
 
8
9
  include Leecher::Shellout
9
10
 
10
11
  class ShutdownError < RuntimeError; end
12
+
13
+ # This is annoying: Bunny's errors don't inherit from a
14
+ # Bunny::Error parent, so we have to keep track of em :-(
15
+ BunnyErrors =
16
+ [
17
+ Bunny::ConnectionError,
18
+ Bunny::ForcedChannelCloseError,
19
+ Bunny::ForcedConnectionCloseError,
20
+ Bunny::MessageError,
21
+ Bunny::ProtocolError,
22
+ Bunny::ServerDownError,
23
+ Bunny::UnsubscribeError,
24
+ Bunny::AcknowledgementError,
25
+ ]
11
26
 
12
27
  attr_accessor :username
13
28
  attr_accessor :password
@@ -15,6 +30,7 @@ module Leecher
15
30
  attr_accessor :bunny_opts
16
31
  attr_accessor :aria2_opts
17
32
  attr_accessor :log
33
+ attr_accessor :rpc_server
18
34
 
19
35
  def initialize(username,
20
36
  password,
@@ -28,6 +44,7 @@ module Leecher
28
44
  self.bunny_opts = bunny_opts
29
45
  self.aria2_opts = aria2_opts
30
46
  self.log = log
47
+ self.rpc_server = make_rpc_client(aria2_opts)
31
48
  end
32
49
 
33
50
 
@@ -39,18 +56,45 @@ module Leecher
39
56
  @dont_kill_me = false
40
57
  [:INT, :TERM].each do |sig|
41
58
  trap(sig) do
59
+ log.info("Caught SIG#{sig}")
60
+
61
+ # Already asked to go away - impatient users :-(
62
+ if @graceful_shutdown
63
+ log.info("I'm tired of catching this so I'm going to (unsafely) go away.")
64
+ raise ShutdownError.new("SIG#{sig}")
65
+ end
66
+
42
67
  @graceful_shutdown = true
43
- log.info("Caught SIG{sig}")
44
68
  raise ShutdownError.new("SIG#{sig}") unless @dont_kill_me
45
69
  end
46
70
  end
47
71
 
48
- q = get_queue(@bunny, username, metalink_queue_name)
49
72
  until @graceful_shutdown
50
73
  begin
74
+ q = get_queue(@bunny, username, metalink_queue_name)
51
75
  drain_metalink_queue(q)
52
76
  rescue ShutdownError => e
53
77
  log.info(["Going away because of", e.message].join(": "))
78
+ rescue *BunnyErrors => e
79
+ log.warn("Something has gone wrong with our "\
80
+ "connection. We're going to try start again.")
81
+ e.backtrace.each do |line|
82
+ log.debug(line)
83
+ end
84
+
85
+ # Silently try throw away our existing bunny.
86
+ if @bunny.status == :connected
87
+ @bunny.stop() rescue Exception
88
+ end
89
+
90
+ log.debug("We've shot the old bunny - we'll make a new one in "\
91
+ "a few seconds.")
92
+ Kernel.sleep(3)
93
+
94
+ # Start again.
95
+ log.debug("Reconnecting to rabbitmq")
96
+ @bunny = make_bunny(self.bunny_opts)
97
+ @bunny.start()
54
98
  rescue Exception => e
55
99
  log.error([e.class.name, e.message].join(": "))
56
100
  e.backtrace.each do |line|
@@ -67,35 +111,29 @@ module Leecher
67
111
  def get_queue(bunny, *queue_joinable_name)
68
112
  bunny.queue(File.join(*queue_joinable_name))
69
113
  end
70
-
114
+
115
+ # Continuously get messages (metalinks) off the rabbitmq queue and
116
+ # invoke aria2c to download things.
71
117
  def drain_metalink_queue(q)
118
+ log.info("Subscribing to #{q.name.inspect()}")
119
+
72
120
  next_queue_message(q) do |msg|
73
121
  @dont_kill_me = true
74
122
 
75
- aria_exec = make_aria_exec(URI.parse(msg))
76
- log.info("Will exec #{aria_exec.inspect()}")
77
- stdout_rd, stdout_wr = IO.pipe()
78
- stderr_rd, stderr_wr = IO.pipe()
79
- rc = shellout(aria_exec, nil, stdout_wr, stderr_wr)
80
- stdout_wr.close()
81
- stderr_wr.close()
82
-
83
- if rc == 0
84
- log.info("aria2c exited happily")
123
+ log.info("Will call aria2.addUri #{msg.inspect()}")
124
+
125
+ #FIXME is it possible something can go wrong but not throw an exception?
126
+ begin
127
+ status = call_rpc("aria2.addUri", [msg])
128
+ log.info("Added uri to aria with gid: #{status}")
85
129
  return true
86
- else
87
- log.error("aria2c exited unhappily (rc = #{rc})")
88
- stdout_rd.each_line do |line|
89
- log.error(line)
90
- end
91
- stderr_rd.each_line do |line|
92
- log.debug(line)
93
- end
130
+ rescue Exception => e
131
+ log.error("Failed to add uri to aria2c: #{e.message}")
94
132
  return false
95
133
  end
96
-
97
- @dont_kill_me = false
98
134
  end
135
+ ensure
136
+ @dont_kill_me = false
99
137
  end
100
138
 
101
139
  def next_queue_message(q, &block)
@@ -114,11 +152,52 @@ module Leecher
114
152
  end
115
153
  end
116
154
 
117
- def make_aria_exec(uri)
155
+ def make_rpc_client(aria2_opts)
156
+ XMLRPC::Client.new2("http://#{aria2_opts[:user]}:#{aria2_opts[:password]}@127.0.0.1:#{aria2_opts[:port]}/rpc")
157
+ end
158
+
159
+ def call_rpc(method, *args)
160
+ retried = false
161
+ begin
162
+ if args.nil? || args.empty?
163
+ rpc_server.call(method)
164
+ else
165
+ rpc_server.call(method, *args)
166
+ end
167
+ rescue Errno::ECONNREFUSED => e
168
+ if retried == true
169
+ raise e
170
+ else
171
+ retried = true
172
+ shellout(make_aria_exec, nil, nil, nil)
173
+ sleep 10
174
+ retry
175
+ end
176
+ rescue Errno::EPIPE => e
177
+ # The RPC client doesn't realise if the server closes the
178
+ # connection. So, we retry (which seems to remake the connection?).
179
+ if retried == true
180
+ raise e
181
+ else
182
+ retried = true
183
+ retry
184
+ end
185
+ end
186
+ end
187
+
188
+ def make_aria_exec
189
+ args = aria2_opts[:args]
190
+
191
+ if aria2_opts.has_key?(:config)
192
+ args.push([
193
+ "--conf-path",
194
+ aria2_opts[:config],
195
+ ].join("="))
196
+ end
197
+
118
198
  [
119
199
  aria2_opts[:bin],
120
- aria2_opts[:args].join(" "),
121
- uri.to_s(),
200
+ args.join(" "),
122
201
  ].join(" ")
123
202
  end
124
203
  end # class Client
@@ -61,6 +61,7 @@ module Leecher
61
61
 
62
62
  if @log_fd and not @log_fd.closed?
63
63
  @log_fd.puts("%s %s %s" % [iso8601_timestr, level.to_s.upcase, message])
64
+ @log_fd.flush()
64
65
  end
65
66
  end
66
67
 
@@ -82,8 +83,9 @@ module Leecher
82
83
  }
83
84
  def log_for_humans(iso8601_timestr, log_level, log_msg)
84
85
  level_str, level_colour = HUMAN_LOG_LEVELS[log_level]
85
- puts("#{ANSI_BOLD_WHITE}#{iso8601_timestr}#{ANSI_NORMAL} "\
86
- "#{level_colour}#{"%5.5s" % level_str}#{ANSI_NORMAL} #{log_msg}")
86
+ STDOUT.puts("#{ANSI_BOLD_WHITE}#{iso8601_timestr}#{ANSI_NORMAL} "\
87
+ "#{level_colour}#{"%5.5s" % level_str}#{ANSI_NORMAL} #{log_msg}")
88
+ STDOUT.flush()
87
89
  end
88
90
  end
89
91
  end
@@ -8,7 +8,7 @@ module Leecher
8
8
 
9
9
  if child_pid
10
10
  # Parent process executes this
11
- child_status = (Process.waitpid2(child_pid)[1]).to_i >> 8
11
+ child_pid, child_status = Process.waitpid2(child_pid)
12
12
  else
13
13
  Process.setsid()
14
14
  STDIN.reopen(stdin || "/dev/null")
@@ -1,3 +1,3 @@
1
1
  module Leecher
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -41,34 +41,38 @@ describe Leecher::Client do
41
41
  end
42
42
 
43
43
  describe "#drain_metalink_queue" do
44
-
45
- it "should reject URIs that are not URIs" do
46
- @c.stub(:next_queue_message).
47
- and_yield(nil)
48
- lambda { @c.drain_metalink_queue(double("q")) }.
49
- should raise_error(URI::InvalidURIError)
44
+
45
+ before do
46
+ @q = double("q")
47
+ @q.should_receive(:name).
48
+ any_number_of_times.
49
+ and_return("qq")
50
50
  end
51
51
 
52
- it "should shellout to aria2" do
52
+
53
+ it "should rpc to aria2" do
53
54
  @c.stub(:next_queue_message).
54
55
  and_yield("http://some/url")
55
- @c.should_receive(:shellout).
56
- with("/usr/bin/aria2c -V http://some/url",
57
- anything(), anything(), anything()).
58
- and_return(0)
59
- @c.drain_metalink_queue(double("q")).should be_true
56
+ rpc_server = double("rpc_server")
57
+ rpc_server.stub(:call).
58
+ and_return(true)
59
+ @c.stub(:rpc_server).
60
+ and_return(rpc_server)
61
+ @c.drain_metalink_queue(@q).should be_true
60
62
  end
61
63
 
62
- it "should return according to the rc" do
64
+ it "should return according to the success of the rpc" do
63
65
  @c.stub(:next_queue_message).
64
66
  and_yield("http://some/url")
65
- @c.stub(:shellout).
66
- and_return(0)
67
- q = double("q")
68
- @c.drain_metalink_queue(q).should be_true
69
- @c.stub(:shellout).
70
- and_return(1)
71
- @c.drain_metalink_queue(q).should be_false
67
+ rpc_server = double("rpc_server")
68
+ @c.stub(:rpc_server).
69
+ and_return(rpc_server)
70
+ rpc_server.stub(:call).
71
+ and_return(true)
72
+ @c.drain_metalink_queue(@q).should be_true
73
+ rpc_server.stub(:call).
74
+ and_raise(RuntimeError)
75
+ @c.drain_metalink_queue(@q).should be_false
72
76
  end
73
77
  end
74
78
  end
@@ -0,0 +1,26 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ require 'leecher/shellout'
4
+
5
+ include Leecher::Shellout
6
+
7
+ describe Leecher::Shellout do
8
+ it "should return 0 for true" do
9
+ shellout("/bin/true").success?.should be_true
10
+ end
11
+
12
+ it "should return 1 for false" do
13
+ shellout("/bin/false").success?.should be_false
14
+ end
15
+
16
+ it "should work with pipes" do
17
+ stdin_rd, stdin_wr = IO.pipe()
18
+ stdout_rd, stdout_wr = IO.pipe()
19
+ stdin_wr.puts("foo")
20
+ stdin_wr.close()
21
+ child_status = shellout("cat", stdin_rd, stdout_wr)
22
+ stdout_wr.close()
23
+ stdout_rd.gets().should == "foo\n"
24
+ stdout_rd.close()
25
+ end
26
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leecher
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 0
9
8
  - 1
10
- version: 0.0.1
9
+ - 0
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Marc Bowes
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-04-22 00:00:00 +02:00
18
+ date: 2011-05-09 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -74,6 +74,7 @@ files:
74
74
  - Gemfile
75
75
  - Rakefile
76
76
  - bin/leecher
77
+ - config/aria2.conf
77
78
  - config/client.yml
78
79
  - leecher.gemspec
79
80
  - lib/leecher.rb
@@ -82,6 +83,7 @@ files:
82
83
  - lib/leecher/shellout.rb
83
84
  - lib/leecher/version.rb
84
85
  - spec/client_spec.rb
86
+ - spec/shellout_spec.rb
85
87
  - spec/spec_helper.rb
86
88
  has_rdoc: true
87
89
  homepage: ""