leecher 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/leecher +35 -24
- data/config/aria2.conf +15 -0
- data/config/client.yml +8 -5
- data/lib/leecher/client.rb +105 -26
- data/lib/leecher/log.rb +4 -2
- data/lib/leecher/shellout.rb +1 -1
- data/lib/leecher/version.rb +1 -1
- data/spec/client_spec.rb +24 -20
- data/spec/shellout_spec.rb +26 -0
- metadata +6 -4
data/bin/leecher
CHANGED
@@ -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
|
-
@
|
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=
|
33
|
-
"
|
34
|
-
"Default: #{@
|
35
|
-
@
|
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 #{@
|
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
|
66
|
-
config =
|
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:",
|
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(@
|
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
|
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(@
|
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(
|
147
|
-
|
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 #{
|
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
|
|
data/config/aria2.conf
ADDED
@@ -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") %>
|
data/config/client.yml
CHANGED
@@ -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: [-
|
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:
|
29
|
+
:chdir: <%= File.join(config_dirname) %>
|
26
30
|
:umask: 0000
|
27
|
-
:
|
28
|
-
:
|
29
|
-
:stderr: nil
|
31
|
+
:stdout: <%= File.join(config_dirname, "stdout") %>
|
32
|
+
:stderr: <%= File.join(config_dirname, "stderr") %>
|
data/lib/leecher/client.rb
CHANGED
@@ -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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
87
|
-
log.error("
|
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
|
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
|
-
|
121
|
-
uri.to_s(),
|
200
|
+
args.join(" "),
|
122
201
|
].join(" ")
|
123
202
|
end
|
124
203
|
end # class Client
|
data/lib/leecher/log.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/leecher/shellout.rb
CHANGED
data/lib/leecher/version.rb
CHANGED
data/spec/client_spec.rb
CHANGED
@@ -41,34 +41,38 @@ describe Leecher::Client do
|
|
41
41
|
end
|
42
42
|
|
43
43
|
describe "#drain_metalink_queue" do
|
44
|
-
|
45
|
-
|
46
|
-
@
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
52
|
+
|
53
|
+
it "should rpc to aria2" do
|
53
54
|
@c.stub(:next_queue_message).
|
54
55
|
and_yield("http://some/url")
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
- 0
|
9
8
|
- 1
|
10
|
-
|
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-
|
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: ""
|