leecher 0.2.3 → 1.0.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.
- data/.gitignore +5 -4
- data/.travis.yml +2 -0
- data/Gemfile +1 -2
- data/Gemfile.lock +48 -0
- data/README +58 -0
- data/Rakefile +5 -7
- data/bin/leecher +2 -265
- data/bin/leecher-aria2-event-hook +12 -0
- data/config/amqp.example.yml +28 -0
- data/config/arguments.rb +12 -0
- data/config/aria2.example.yml +6 -0
- data/config/boot.rb +64 -0
- data/config/environment.rb +22 -0
- data/config/environments/development.rb +2 -0
- data/config/environments/production.rb +5 -0
- data/config/environments/test.rb +2 -0
- data/config/gheed.example.yml +2 -0
- data/config/leecher.example.yml +1 -0
- data/config/post-daemonize/readme +5 -0
- data/config/pre-daemonize/amqp.rb +6 -0
- data/config/pre-daemonize/json.rb +3 -0
- data/config/pre-daemonize/readme +12 -0
- data/config/pre-daemonize/safely.rb +13 -0
- data/leecher.gemspec +13 -4
- data/lib/leecher.rb +24 -2
- data/lib/leecher/aria2/client.rb +65 -0
- data/lib/leecher/client.rb +31 -211
- data/lib/leecher/gheed/client.rb +35 -0
- data/lib/leecher/metalink_queue.rb +68 -0
- data/lib/leecher/version.rb +1 -1
- data/libexec/leecher-daemon.rb +34 -0
- data/script/console +4 -0
- data/script/destroy +4 -0
- data/script/generate +4 -0
- data/spec/aria2/client_spec.rb +26 -0
- data/spec/client_spec.rb +42 -83
- data/spec/metalink_queue_spec.rb +25 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +0 -2
- data/tasks/rspec.rake +6 -0
- metadata +134 -90
- data/config/aria2.conf +0 -15
- data/config/client.yml +0 -32
- data/lib/leecher/log.rb +0 -91
- data/lib/leecher/shellout.rb +0 -27
- data/spec/shellout_spec.rb +0 -26
data/config/arguments.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Argument handling for your daemon is configured here.
|
2
|
+
#
|
3
|
+
# You have access to two variables when this file is
|
4
|
+
# parsed. The first is +opts+, which is the object yielded from
|
5
|
+
# +OptionParser.new+, the second is +@options+ which is a standard
|
6
|
+
# Ruby hash that is later accessible through
|
7
|
+
# DaemonKit.arguments.options and can be used in your daemon process.
|
8
|
+
|
9
|
+
# Here is an example:
|
10
|
+
# opts.on('-f', '--foo FOO', 'Set foo') do |foo|
|
11
|
+
# @options[:foo] = foo
|
12
|
+
# end
|
data/config/boot.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Don't change this file!
|
2
|
+
# Configure your daemon in config/environment.rb
|
3
|
+
|
4
|
+
DAEMON_ROOT = "#{File.expand_path(File.dirname(__FILE__))}/.." unless defined?( DAEMON_ROOT )
|
5
|
+
|
6
|
+
require "rubygems"
|
7
|
+
require "bundler/setup"
|
8
|
+
|
9
|
+
module DaemonKit
|
10
|
+
class << self
|
11
|
+
def boot!
|
12
|
+
unless booted?
|
13
|
+
pick_boot.run
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def booted?
|
18
|
+
defined? DaemonKit::Initializer
|
19
|
+
end
|
20
|
+
|
21
|
+
def pick_boot
|
22
|
+
(vendor_kit? ? VendorBoot : GemBoot).new
|
23
|
+
end
|
24
|
+
|
25
|
+
def vendor_kit?
|
26
|
+
File.exists?( "#{DAEMON_ROOT}/vendor/daemon-kit" )
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Boot
|
31
|
+
def run
|
32
|
+
load_initializer
|
33
|
+
DaemonKit::Initializer.run
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class VendorBoot < Boot
|
38
|
+
def load_initializer
|
39
|
+
require "#{DAEMON_ROOT}/vendor/daemon-kit/lib/daemon_kit/initializer"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class GemBoot < Boot
|
44
|
+
def load_initializer
|
45
|
+
begin
|
46
|
+
require 'rubygems' unless defined?( ::Gem )
|
47
|
+
gem 'daemon-kit'
|
48
|
+
require 'daemon_kit/initializer'
|
49
|
+
rescue ::Gem::LoadError => e
|
50
|
+
msg = <<EOF
|
51
|
+
|
52
|
+
You are missing the daemon-kit gem. Please install the following gem:
|
53
|
+
|
54
|
+
sudo gem install daemon-kit
|
55
|
+
|
56
|
+
EOF
|
57
|
+
$stderr.puts msg
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
DaemonKit.boot!
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Be sure to restart your daemon when you modify this file
|
2
|
+
|
3
|
+
# Uncomment below to force your daemon into production mode
|
4
|
+
ENV['DAEMON_ENV'] ||= "production"
|
5
|
+
|
6
|
+
# Boot up
|
7
|
+
require File.join(File.dirname(__FILE__), 'boot')
|
8
|
+
|
9
|
+
# Auto-require default libraries and those for the current Rails environment.
|
10
|
+
Bundler.require :default, DaemonKit.env
|
11
|
+
|
12
|
+
DaemonKit::Initializer.run do |config|
|
13
|
+
|
14
|
+
# The name of the daemon as reported by process monitoring tools
|
15
|
+
config.daemon_name = 'leecher'
|
16
|
+
|
17
|
+
# Force the daemon to be killed after X seconds from asking it to
|
18
|
+
# config.force_kill_wait = 30
|
19
|
+
|
20
|
+
# Log backraces when a thread/daemon dies (Recommended)
|
21
|
+
# config.backtraces = true
|
22
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
work_dir: /home/$USER/.leecher
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# You can place files in here to be loaded before the code is daemonized.
|
2
|
+
#
|
3
|
+
# DaemonKit looks for a file named '<config.daemon_name>.rb' and loads
|
4
|
+
# that file first, and inside a DaemonKit::Initializer block. The
|
5
|
+
# remaning files then simply required into the running process.
|
6
|
+
#
|
7
|
+
# These files are mostly useful for operations that should fail blatantly
|
8
|
+
# before daemonizing, like loading gems.
|
9
|
+
#
|
10
|
+
# Be careful not to open any form of IO in here and expecting it to be
|
11
|
+
# open inside the running daemon since all IO instances are closed when
|
12
|
+
# daemonizing (including STDIN, STDOUT & STDERR).
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Safely is responsible for providing exception reporting and the
|
2
|
+
# logging of backtraces when your daemon dies unexpectedly. The full
|
3
|
+
# documentation for safely can be found at
|
4
|
+
# http://github.com/kennethkalmer/safely/wiki
|
5
|
+
|
6
|
+
# By default Safely will use the daemon-kit's logger to log exceptions,
|
7
|
+
# and will store backtraces in the "log" directory.
|
8
|
+
|
9
|
+
# Comment out to enable Hoptoad support
|
10
|
+
# Safely::Strategy::Hoptoad.hoptoad_key = ""
|
11
|
+
|
12
|
+
# Comment out to use email exceptions
|
13
|
+
# Safely::Strategy::Mail.recipient = "your.name@gmail.com"
|
data/leecher.gemspec
CHANGED
@@ -19,8 +19,17 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = ["lib"]
|
21
21
|
|
22
|
-
s.
|
23
|
-
s.add_runtime_dependency
|
24
|
-
s.add_runtime_dependency
|
25
|
-
s.add_runtime_dependency
|
22
|
+
s.add_runtime_dependency "daemon-kit"
|
23
|
+
s.add_runtime_dependency "safely"
|
24
|
+
s.add_runtime_dependency "multi_json"
|
25
|
+
s.add_runtime_dependency "yajl-ruby"
|
26
|
+
s.add_runtime_dependency "amqp"
|
27
|
+
s.add_runtime_dependency "erubis"
|
28
|
+
|
29
|
+
s.add_development_dependency "rake"
|
30
|
+
s.add_development_dependency "rspec"
|
31
|
+
|
32
|
+
if ENV["LEECHER_DEV"] && RUBY_VERSION == "1.9.3"
|
33
|
+
s.add_development_dependency "debugger"
|
34
|
+
end
|
26
35
|
end
|
data/lib/leecher.rb
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
module Leecher
|
2
|
-
|
3
|
-
require "leecher/version"
|
2
|
+
|
4
3
|
require "leecher/client"
|
4
|
+
require "leecher/gheed/client"
|
5
|
+
require "leecher/aria2/client"
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def config
|
10
|
+
DaemonKit::Config.load("leecher")
|
11
|
+
end
|
12
|
+
|
13
|
+
def gheed_client
|
14
|
+
config = DaemonKit::Config.load("gheed")
|
15
|
+
Leecher::Gheed::Client.new(config)
|
16
|
+
end
|
17
|
+
|
18
|
+
def aria2_client
|
19
|
+
config = DaemonKit::Config.load("aria2")
|
20
|
+
Leecher::Aria2::Client.new(config)
|
21
|
+
end
|
22
|
+
|
23
|
+
def new
|
24
|
+
Client.new(config, aria2_client, gheed_client)
|
25
|
+
end
|
26
|
+
end
|
5
27
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Leecher
|
2
|
+
module Aria2
|
3
|
+
|
4
|
+
require "xmlrpc/client"
|
5
|
+
|
6
|
+
class UnableToStartAriaError < RuntimeError; end
|
7
|
+
|
8
|
+
class Client < XMLRPC::Client
|
9
|
+
|
10
|
+
attr_reader :config
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
@config = config
|
14
|
+
super("127.0.0.1", "/rpc", config.port, nil, nil, config.user, config.password)
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(method, *args)
|
18
|
+
retried = false
|
19
|
+
super("aria2.#{method}", *args)
|
20
|
+
rescue Errno::ECONNREFUSED
|
21
|
+
start_aria2
|
22
|
+
Kernel.sleep(3) # Give it a chance to start.
|
23
|
+
retry unless retried
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_uri(uris, options)
|
27
|
+
call("addUri", uris, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def tell_status(gid, keys = nil)
|
31
|
+
if keys.nil? or keys.empty?
|
32
|
+
call("tellStatus", gid.to_s)
|
33
|
+
else
|
34
|
+
call("tellStatus", gid.to_s, keys)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def make_aria_exec
|
39
|
+
args = config.args.dup
|
40
|
+
[
|
41
|
+
["--conf-path", config.config],
|
42
|
+
["--on-download-complete", callback_command],
|
43
|
+
["--on-download-error", callback_command],
|
44
|
+
["--on-download-pause", callback_command],
|
45
|
+
["--on-download-start", callback_command],
|
46
|
+
["--on-download-stop", callback_command],
|
47
|
+
].each do |option , value|
|
48
|
+
args.push(option + "=" + value)
|
49
|
+
end
|
50
|
+
|
51
|
+
[config.bin, args.join(" ")].join(" ")
|
52
|
+
end
|
53
|
+
|
54
|
+
def start_aria2
|
55
|
+
command = make_aria_exec
|
56
|
+
IO.popen(command) {}
|
57
|
+
raise UnableToStartAriaError unless $?.success?
|
58
|
+
end
|
59
|
+
|
60
|
+
def callback_command
|
61
|
+
File.expand_path("../../../bin/leecher-aria2-event-hook", File.dirname(__FILE__))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/leecher/client.rb
CHANGED
@@ -1,226 +1,46 @@
|
|
1
1
|
module Leecher
|
2
|
-
|
3
2
|
class Client
|
4
3
|
|
5
|
-
require "
|
6
|
-
require "
|
7
|
-
require "
|
8
|
-
require "yajl"
|
9
|
-
|
10
|
-
include Leecher::Shellout
|
11
|
-
|
12
|
-
class ShutdownError < RuntimeError; end
|
13
|
-
|
14
|
-
# This is annoying: Bunny's errors don't inherit from a
|
15
|
-
# Bunny::Error parent, so we have to keep track of em :-(
|
16
|
-
BunnyErrors =
|
17
|
-
[
|
18
|
-
Bunny::ConnectionError,
|
19
|
-
Bunny::ForcedChannelCloseError,
|
20
|
-
Bunny::ForcedConnectionCloseError,
|
21
|
-
Bunny::MessageError,
|
22
|
-
Bunny::ProtocolError,
|
23
|
-
Bunny::ServerDownError,
|
24
|
-
Bunny::UnsubscribeError,
|
25
|
-
Bunny::AcknowledgementError,
|
26
|
-
]
|
27
|
-
|
28
|
-
attr_accessor :username
|
29
|
-
attr_accessor :password
|
30
|
-
attr_accessor :metalink_queue_name
|
31
|
-
attr_accessor :bunny_opts
|
32
|
-
attr_accessor :aria2_opts
|
33
|
-
attr_accessor :log
|
34
|
-
attr_accessor :rpc_server
|
35
|
-
|
36
|
-
def initialize(username,
|
37
|
-
password,
|
38
|
-
metalink_queue_name,
|
39
|
-
bunny_opts,
|
40
|
-
aria2_opts,
|
41
|
-
log)
|
42
|
-
self.username = username
|
43
|
-
self.password = password
|
44
|
-
self.metalink_queue_name = metalink_queue_name
|
45
|
-
self.bunny_opts = bunny_opts
|
46
|
-
self.aria2_opts = aria2_opts
|
47
|
-
self.log = log
|
48
|
-
self.rpc_server = make_rpc_client(aria2_opts)
|
49
|
-
end
|
50
|
-
|
51
|
-
|
52
|
-
def run()
|
53
|
-
@graceful_shutdown = false
|
54
|
-
@dont_kill_me = false
|
55
|
-
[:INT, :TERM].each do |sig|
|
56
|
-
trap(sig) do
|
57
|
-
log.info("Caught SIG#{sig}")
|
58
|
-
|
59
|
-
# Already asked to go away - impatient users :-(
|
60
|
-
if @graceful_shutdown
|
61
|
-
log.info("I'm tired of catching this so I'm going to (unsafely) go away.")
|
62
|
-
raise ShutdownError.new("SIG#{sig}")
|
63
|
-
end
|
64
|
-
|
65
|
-
@graceful_shutdown = true
|
66
|
-
raise ShutdownError.new("SIG#{sig}") unless @dont_kill_me
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
until @graceful_shutdown
|
71
|
-
begin
|
72
|
-
b = make_bunny(self.bunny_opts)
|
73
|
-
q = get_queue(b, username, metalink_queue_name)
|
74
|
-
drain_metalink_queue(q)
|
75
|
-
rescue ShutdownError => e
|
76
|
-
log.info(["Going away because of", e.message].join(": "))
|
77
|
-
rescue *BunnyErrors => e
|
78
|
-
log.warn("Something has gone wrong with our "\
|
79
|
-
"connection. We're going to try start again.")
|
80
|
-
e.backtrace.each do |line|
|
81
|
-
log.debug(line)
|
82
|
-
end
|
83
|
-
|
84
|
-
# Silently try throw away our existing bunny.
|
85
|
-
if b && b.status == :connected
|
86
|
-
b.stop() rescue Exception
|
87
|
-
end
|
88
|
-
|
89
|
-
log.debug("Will reconnect after 3s")
|
90
|
-
rescue Exception => e
|
91
|
-
log.error([e.class.name, e.message].join(": "))
|
92
|
-
e.backtrace.each do |line|
|
93
|
-
log.debug(line)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def make_bunny(bunny_opts)
|
100
|
-
b = Bunny.new(bunny_opts)
|
101
|
-
b.start()
|
102
|
-
b.qos()
|
103
|
-
b
|
104
|
-
end
|
4
|
+
require "fileutils"
|
5
|
+
require "multi_json"
|
6
|
+
require "leecher/metalink_queue"
|
105
7
|
|
106
|
-
|
107
|
-
bunny.queue(File.join(*queue_joinable_name))
|
108
|
-
end
|
109
|
-
|
110
|
-
# Continuously get messages (metalinks) off the rabbitmq queue and
|
111
|
-
# invoke aria2c to download things.
|
112
|
-
def drain_metalink_queue(q)
|
113
|
-
log.info("Subscribing to #{q.name.inspect()}")
|
114
|
-
|
115
|
-
next_queue_message(q) do |msg|
|
116
|
-
@dont_kill_me = true
|
8
|
+
attr_reader :config, :aria2_client, :gheed_client
|
117
9
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
true
|
123
|
-
rescue Exception => e
|
124
|
-
log.error("Failed to add uri to aria2c: #{e.message}")
|
125
|
-
false
|
126
|
-
ensure
|
127
|
-
@dont_kill_me = false
|
128
|
-
end
|
129
|
-
end
|
10
|
+
def initialize(config, aria2_client, gheed_client)
|
11
|
+
@config = config
|
12
|
+
@aria2_client = aria2_client
|
13
|
+
@gheed_client = gheed_client
|
130
14
|
end
|
131
15
|
|
132
|
-
def enqueue_with_aria(uris, options)
|
133
|
-
rewrite_aria_dir_opt(options)
|
134
|
-
log.info("Will call aria2.addUri(#{uris.inspect()}, #{options.inspect()})")
|
135
|
-
status = call_rpc("aria2.addUri", uris, options)
|
136
|
-
log.info("Added uri to aria with gid: #{status}")
|
137
|
-
end
|
138
16
|
|
139
|
-
def
|
140
|
-
|
141
|
-
if (dir = options.delete("dir"))
|
142
|
-
begin
|
143
|
-
global_opts = call_rpc("aria2.getGlobalOption")
|
144
|
-
global_dir = global_opts.delete("dir")
|
145
|
-
options["dir"] = File.join(global_dir, dir)
|
146
|
-
rescue Exception => e
|
147
|
-
log.error("Couldn't get aria2 global options")
|
148
|
-
end
|
149
|
-
end
|
17
|
+
def queue
|
18
|
+
@queue ||= MetalinkQueue.new(filename("queue.yml"))
|
150
19
|
end
|
151
|
-
|
152
|
-
def
|
153
|
-
|
154
|
-
# drops and comes back) but the connection never drops. Rabbitmq
|
155
|
-
# kicks us off after 15 min. Thus, use a timeout of 15 minutes
|
156
|
-
# so that, if lightning strikes, worst case is we're 15 minutes
|
157
|
-
# behind.
|
158
|
-
# +:ack+: Acks are sent by +queue.ack+, which is sent via the
|
159
|
-
# subscribe block automatically if the option is turned on.
|
160
|
-
q.subscribe(:timeout => 900,
|
161
|
-
:ack => true) do |msg|
|
162
|
-
payload = msg[:payload]
|
163
|
-
case payload
|
164
|
-
when :queue_empty
|
165
|
-
next
|
166
|
-
else
|
167
|
-
if block_given?
|
168
|
-
yield(payload)
|
169
|
-
else
|
170
|
-
payload
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
def make_rpc_client(aria2_opts)
|
177
|
-
XMLRPC::Client.new2("http://#{aria2_opts[:user]}:#{aria2_opts[:password]}@127.0.0.1:#{aria2_opts[:port]}/rpc")
|
20
|
+
|
21
|
+
def filename(relative)
|
22
|
+
File.join(@config.work_dir, relative)
|
178
23
|
end
|
179
24
|
|
180
|
-
def
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
rpc_server.call(method, *args)
|
187
|
-
end
|
188
|
-
rescue Errno::ECONNREFUSED => e
|
189
|
-
if retried == true
|
190
|
-
raise e
|
191
|
-
else
|
192
|
-
retried = true
|
193
|
-
log.debug("It appears aria isn't running. Attempting to start it up by calling: #{make_aria_exec}")
|
194
|
-
shellout(make_aria_exec, nil, nil, nil)
|
195
|
-
sleep 10
|
196
|
-
retry
|
197
|
-
end
|
198
|
-
rescue Errno::EPIPE => e
|
199
|
-
# The RPC client doesn't realise if the server closes the
|
200
|
-
# connection. So, we retry (which seems to remake the connection?).
|
201
|
-
if retried == true
|
202
|
-
raise e
|
203
|
-
else
|
204
|
-
retried = true
|
205
|
-
retry
|
206
|
-
end
|
207
|
-
end
|
25
|
+
def process_message_on_metalink_queue(msg)
|
26
|
+
# URIs will only ever be one (contract with gheed)
|
27
|
+
uris, options = MultiJson.decode(msg)
|
28
|
+
queue.add_uris(uris)
|
29
|
+
gid = aria2_client.add_uri(uris, options)
|
30
|
+
queue.set_store_entry(uris.first, :gid => gid.to_i, :state => :added_to_aria2)
|
208
31
|
end
|
209
32
|
|
210
|
-
def
|
211
|
-
|
33
|
+
def state_change(gid)
|
34
|
+
response = aria2_client.tell_status(gid)
|
35
|
+
uri = response["files"].first["uris"].first["uri"]
|
36
|
+
state = response["status"].to_sym
|
37
|
+
queue.set_store_entry(uri, :gid => gid, :state => state)
|
38
|
+
notify_gheed(uri, state) if [:complete, :error].include?(state)
|
39
|
+
end
|
212
40
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
aria2_opts[:config],
|
217
|
-
].join("="))
|
218
|
-
end
|
219
|
-
|
220
|
-
[
|
221
|
-
aria2_opts[:bin],
|
222
|
-
args.join(" "),
|
223
|
-
].join(" ")
|
41
|
+
def notify_gheed(uri, state)
|
42
|
+
metalink, deliveries = gheed_client.state_change(uri, state)
|
43
|
+
queue.set_store_entry(metalink["metalink_url"], :state => :completed)
|
224
44
|
end
|
225
|
-
end
|
226
|
-
end
|
45
|
+
end
|
46
|
+
end
|