leecher 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: ""