qs 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.
- data/.gitignore +1 -0
- data/Gemfile +6 -1
- data/LICENSE.txt +1 -1
- data/bench/config.qs +46 -0
- data/bench/queue.rb +8 -0
- data/bench/report.rb +114 -0
- data/bench/report.txt +11 -0
- data/bin/qs +7 -0
- data/lib/qs/cli.rb +124 -0
- data/lib/qs/client.rb +121 -0
- data/lib/qs/config_file.rb +79 -0
- data/lib/qs/daemon.rb +350 -0
- data/lib/qs/daemon_data.rb +46 -0
- data/lib/qs/error_handler.rb +58 -0
- data/lib/qs/job.rb +70 -0
- data/lib/qs/job_handler.rb +90 -0
- data/lib/qs/logger.rb +23 -0
- data/lib/qs/payload_handler.rb +136 -0
- data/lib/qs/pid_file.rb +42 -0
- data/lib/qs/process.rb +136 -0
- data/lib/qs/process_signal.rb +20 -0
- data/lib/qs/qs_runner.rb +49 -0
- data/lib/qs/queue.rb +69 -0
- data/lib/qs/redis_item.rb +33 -0
- data/lib/qs/route.rb +52 -0
- data/lib/qs/runner.rb +26 -0
- data/lib/qs/test_helpers.rb +17 -0
- data/lib/qs/test_runner.rb +43 -0
- data/lib/qs/version.rb +1 -1
- data/lib/qs.rb +92 -2
- data/qs.gemspec +7 -2
- data/test/helper.rb +8 -1
- data/test/support/app_daemon.rb +74 -0
- data/test/support/config.qs +7 -0
- data/test/support/config_files/empty.qs +0 -0
- data/test/support/config_files/invalid.qs +1 -0
- data/test/support/config_files/valid.qs +7 -0
- data/test/support/config_invalid_run.qs +3 -0
- data/test/support/config_no_run.qs +0 -0
- data/test/support/factory.rb +14 -0
- data/test/support/pid_file_spy.rb +19 -0
- data/test/support/runner_spy.rb +17 -0
- data/test/system/daemon_tests.rb +226 -0
- data/test/unit/cli_tests.rb +188 -0
- data/test/unit/client_tests.rb +269 -0
- data/test/unit/config_file_tests.rb +59 -0
- data/test/unit/daemon_data_tests.rb +96 -0
- data/test/unit/daemon_tests.rb +702 -0
- data/test/unit/error_handler_tests.rb +163 -0
- data/test/unit/job_handler_tests.rb +253 -0
- data/test/unit/job_tests.rb +132 -0
- data/test/unit/logger_tests.rb +38 -0
- data/test/unit/payload_handler_tests.rb +276 -0
- data/test/unit/pid_file_tests.rb +70 -0
- data/test/unit/process_signal_tests.rb +61 -0
- data/test/unit/process_tests.rb +371 -0
- data/test/unit/qs_runner_tests.rb +166 -0
- data/test/unit/qs_tests.rb +217 -0
- data/test/unit/queue_tests.rb +132 -0
- data/test/unit/redis_item_tests.rb +49 -0
- data/test/unit/route_tests.rb +81 -0
- data/test/unit/runner_tests.rb +63 -0
- data/test/unit/test_helper_tests.rb +61 -0
- data/test/unit/test_runner_tests.rb +128 -0
- metadata +180 -15
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
data/bench/config.qs
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'qs'
|
2
|
+
require 'bench/queue'
|
3
|
+
|
4
|
+
ROOT_PATH = Pathname.new(File.expand_path('../..', __FILE__))
|
5
|
+
|
6
|
+
LOGGER = if ENV['BENCH_REPORT']
|
7
|
+
Logger.new(ROOT_PATH.join('log/bench_daemon.log').to_s)
|
8
|
+
else
|
9
|
+
Logger.new(STDOUT)
|
10
|
+
end
|
11
|
+
|
12
|
+
PROGRESS_IO = if ENV['BENCH_PROGRESS_IO']
|
13
|
+
::IO.for_fd(ENV['BENCH_PROGRESS_IO'].to_i)
|
14
|
+
else
|
15
|
+
File.open('/dev/null', 'w')
|
16
|
+
end
|
17
|
+
|
18
|
+
class BenchDaemon
|
19
|
+
include Qs::Daemon
|
20
|
+
|
21
|
+
name 'bench'
|
22
|
+
pid_file ROOT_PATH.join('tmp/bench_daemon.pid').to_s
|
23
|
+
|
24
|
+
logger LOGGER
|
25
|
+
verbose_logging false
|
26
|
+
|
27
|
+
queue BenchQueue
|
28
|
+
|
29
|
+
# if jobs fail notify the bench report so it doesn't hang forever on IO.select
|
30
|
+
error do |exception, daemon_data, job|
|
31
|
+
PROGRESS_IO.write_nonblock('F')
|
32
|
+
end
|
33
|
+
|
34
|
+
class Multiply
|
35
|
+
include Qs::JobHandler
|
36
|
+
|
37
|
+
after{ PROGRESS_IO.write_nonblock('.') }
|
38
|
+
|
39
|
+
def run!
|
40
|
+
'a' * params['size']
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
run BenchDaemon.new
|
data/bench/queue.rb
ADDED
data/bench/report.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'scmd'
|
3
|
+
require 'bench/queue'
|
4
|
+
|
5
|
+
class BenchRunner
|
6
|
+
|
7
|
+
TIME_MODIFIER = 10 ** 4 # 4 decimal places
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
output_file_path = if ENV['OUTPUT_FILE']
|
11
|
+
File.expand_path(ENV['OUTPUT_FILE'])
|
12
|
+
else
|
13
|
+
File.expand_path('../report.txt', __FILE__)
|
14
|
+
end
|
15
|
+
@output_file = File.open(output_file_path, 'w')
|
16
|
+
|
17
|
+
@number_of_jobs = ENV['NUM_JOBS'] || 10_000
|
18
|
+
@job_name = 'multiply'
|
19
|
+
@job_params = { 'size' => 100_000 }
|
20
|
+
|
21
|
+
@progress_reader, @progress_writer = IO.pipe
|
22
|
+
|
23
|
+
@results = {}
|
24
|
+
|
25
|
+
Qs.init
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
output "Running benchmark report..."
|
30
|
+
output("\n", false)
|
31
|
+
|
32
|
+
benchmark_adding_jobs
|
33
|
+
benchmark_running_jobs
|
34
|
+
|
35
|
+
size = @results.values.map(&:size).max
|
36
|
+
output "Adding #{@number_of_jobs} Jobs Time: #{@results[:adding_jobs].rjust(size)}s"
|
37
|
+
output "Running #{@number_of_jobs} Jobs Time: #{@results[:running_jobs].rjust(size)}s"
|
38
|
+
|
39
|
+
output "\n"
|
40
|
+
output "Done running benchmark report"
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def benchmark_adding_jobs
|
46
|
+
output "Adding jobs"
|
47
|
+
benchmark = Benchmark.measure do
|
48
|
+
(1..@number_of_jobs).each do |n|
|
49
|
+
BenchQueue.add(@job_name, @job_params)
|
50
|
+
output('.', false) if ((n - 1) % 100 == 0)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
@results[:adding_jobs] = round_and_display(benchmark.real)
|
54
|
+
output("\n", false)
|
55
|
+
end
|
56
|
+
|
57
|
+
def benchmark_running_jobs
|
58
|
+
cmd_str = "bundle exec ./bin/qs bench/config.qs"
|
59
|
+
cmd = Scmd.new(cmd_str, {
|
60
|
+
'BENCH_REPORT' => 'yes',
|
61
|
+
'BENCH_PROGRESS_IO' => @progress_writer.fileno
|
62
|
+
})
|
63
|
+
|
64
|
+
output "Running jobs"
|
65
|
+
begin
|
66
|
+
benchmark = Benchmark.measure do
|
67
|
+
cmd.start
|
68
|
+
if !cmd.running?
|
69
|
+
raise "failed to start qs process: #{cmd_str.inspect}"
|
70
|
+
end
|
71
|
+
|
72
|
+
progress = 0
|
73
|
+
while progress < @number_of_jobs
|
74
|
+
::IO.select([@progress_reader])
|
75
|
+
result = @progress_reader.read_nonblock(1)
|
76
|
+
progress += 1
|
77
|
+
output(result, false) if ((progress - 1) % 100 == 0)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
@results[:running_jobs] = round_and_display(benchmark.real)
|
81
|
+
output("\n", false)
|
82
|
+
ensure
|
83
|
+
cmd.kill('TERM')
|
84
|
+
cmd.wait(5)
|
85
|
+
end
|
86
|
+
|
87
|
+
output("\n", false)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def output(message, puts = true)
|
93
|
+
method = puts ? :puts : :print
|
94
|
+
self.send(method, message)
|
95
|
+
@output_file.send(method, message)
|
96
|
+
STDOUT.flush if method == :print
|
97
|
+
end
|
98
|
+
|
99
|
+
def round_and_display(time_in_ms)
|
100
|
+
display_time(round_time(time_in_ms))
|
101
|
+
end
|
102
|
+
|
103
|
+
def round_time(time_in_ms)
|
104
|
+
(time_in_ms * TIME_MODIFIER).to_i / TIME_MODIFIER.to_f
|
105
|
+
end
|
106
|
+
|
107
|
+
def display_time(time)
|
108
|
+
integer, fractional = time.to_s.split('.')
|
109
|
+
[ integer, fractional.ljust(4, '0') ].join('.')
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
BenchRunner.new.run
|
data/bench/report.txt
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Running benchmark report...
|
2
|
+
|
3
|
+
Adding jobs
|
4
|
+
....................................................................................................
|
5
|
+
Running jobs
|
6
|
+
....................................................................................................
|
7
|
+
|
8
|
+
Adding 10000 Jobs Time: 1.6041s
|
9
|
+
Running 10000 Jobs Time: 12.3159s
|
10
|
+
|
11
|
+
Done running benchmark report
|
data/bin/qs
ADDED
data/lib/qs/cli.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'qs'
|
2
|
+
require 'qs/config_file'
|
3
|
+
require 'qs/process'
|
4
|
+
require 'qs/process_signal'
|
5
|
+
require 'qs/version'
|
6
|
+
|
7
|
+
module Qs
|
8
|
+
|
9
|
+
class CLI
|
10
|
+
|
11
|
+
def self.run(args)
|
12
|
+
self.new.run(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(kernel = nil)
|
16
|
+
@kernel = kernel || Kernel
|
17
|
+
@cli = CLIRB.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def run(*args)
|
21
|
+
begin
|
22
|
+
run!(*args)
|
23
|
+
rescue CLIRB::HelpExit
|
24
|
+
@kernel.puts help
|
25
|
+
rescue CLIRB::VersionExit
|
26
|
+
@kernel.puts Qs::VERSION
|
27
|
+
rescue CLIRB::Error, Qs::ConfigFile::InvalidError => exception
|
28
|
+
@kernel.puts "#{exception.message}\n\n"
|
29
|
+
@kernel.puts help
|
30
|
+
@kernel.exit 1
|
31
|
+
rescue StandardError => exception
|
32
|
+
@kernel.puts "#{exception.class}: #{exception.message}"
|
33
|
+
@kernel.puts exception.backtrace.join("\n")
|
34
|
+
@kernel.exit 1
|
35
|
+
end
|
36
|
+
@kernel.exit 0
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def run!(*args)
|
42
|
+
@cli.parse!(args)
|
43
|
+
config_file_path, command = @cli.args
|
44
|
+
config_file_path ||= 'config.qs'
|
45
|
+
command ||= 'run'
|
46
|
+
daemon = Qs::ConfigFile.new(config_file_path).daemon
|
47
|
+
case(command)
|
48
|
+
when 'run'
|
49
|
+
Qs::Process.new(daemon, :daemonize => false).run
|
50
|
+
when 'start'
|
51
|
+
Qs::Process.new(daemon, :daemonize => true).run
|
52
|
+
when 'stop'
|
53
|
+
Qs::ProcessSignal.new(daemon, 'TERM').send
|
54
|
+
when 'restart'
|
55
|
+
Qs::ProcessSignal.new(daemon, 'USR2').send
|
56
|
+
else
|
57
|
+
raise CLIRB::Error, "#{command.inspect} is not a valid command"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def help
|
62
|
+
"Usage: qs [CONFIG_FILE] [COMMAND]\n\n" \
|
63
|
+
"Commands: run, start, stop, restart\n" \
|
64
|
+
"Options: #{@cli}"
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
class CLIRB # Version 1.0.0, https://github.com/redding/cli.rb
|
70
|
+
Error = Class.new(RuntimeError);
|
71
|
+
HelpExit = Class.new(RuntimeError); VersionExit = Class.new(RuntimeError)
|
72
|
+
attr_reader :argv, :args, :opts, :data
|
73
|
+
|
74
|
+
def initialize(&block)
|
75
|
+
@options = []; instance_eval(&block) if block
|
76
|
+
require 'optparse'
|
77
|
+
@data, @args, @opts = [], [], {}; @parser = OptionParser.new do |p|
|
78
|
+
p.banner = ''; @options.each do |o|
|
79
|
+
@opts[o.name] = o.value; p.on(*o.parser_args){ |v| @opts[o.name] = v }
|
80
|
+
end
|
81
|
+
p.on_tail('--version', ''){ |v| raise VersionExit, v.to_s }
|
82
|
+
p.on_tail('--help', ''){ |v| raise HelpExit, v.to_s }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def option(*args); @options << Option.new(*args); end
|
87
|
+
def parse!(argv)
|
88
|
+
@args = (argv || []).dup.tap do |args_list|
|
89
|
+
begin; @parser.parse!(args_list)
|
90
|
+
rescue OptionParser::ParseError => err; raise Error, err.message; end
|
91
|
+
end; @data = @args + [@opts]
|
92
|
+
end
|
93
|
+
def to_s; @parser.to_s; end
|
94
|
+
def inspect
|
95
|
+
"#<#{self.class}:#{'0x0%x' % (object_id << 1)} @data=#{@data.inspect}>"
|
96
|
+
end
|
97
|
+
|
98
|
+
class Option
|
99
|
+
attr_reader :name, :opt_name, :desc, :abbrev, :value, :klass, :parser_args
|
100
|
+
|
101
|
+
def initialize(name, *args)
|
102
|
+
settings, @desc = args.last.kind_of?(::Hash) ? args.pop : {}, args.pop || ''
|
103
|
+
@name, @opt_name, @abbrev = parse_name_values(name, settings[:abbrev])
|
104
|
+
@value, @klass = gvalinfo(settings[:value])
|
105
|
+
@parser_args = if [TrueClass, FalseClass, NilClass].include?(@klass)
|
106
|
+
["-#{@abbrev}", "--[no-]#{@opt_name}", @desc]
|
107
|
+
else
|
108
|
+
["-#{@abbrev}", "--#{@opt_name} #{@opt_name.upcase}", @klass, @desc]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def parse_name_values(name, custom_abbrev)
|
115
|
+
[ (processed_name = name.to_s.strip.downcase), processed_name.gsub('_', '-'),
|
116
|
+
custom_abbrev || processed_name.gsub(/[^a-z]/, '').chars.first || 'a'
|
117
|
+
]
|
118
|
+
end
|
119
|
+
def gvalinfo(v); v.kind_of?(Class) ? [nil,gklass(v)] : [v,gklass(v.class)]; end
|
120
|
+
def gklass(k); k == Fixnum ? Integer : k; end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
data/lib/qs/client.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'hella-redis'
|
2
|
+
require 'qs'
|
3
|
+
require 'qs/job'
|
4
|
+
require 'qs/queue'
|
5
|
+
|
6
|
+
module Qs
|
7
|
+
|
8
|
+
module Client
|
9
|
+
|
10
|
+
def self.new(*args)
|
11
|
+
if !ENV['QS_TEST_MODE']
|
12
|
+
QsClient.new(*args)
|
13
|
+
else
|
14
|
+
TestClient.new(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(klass)
|
19
|
+
klass.class_eval do
|
20
|
+
include InstanceMethods
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module InstanceMethods
|
25
|
+
|
26
|
+
attr_reader :redis_config, :redis
|
27
|
+
|
28
|
+
def initialize(redis_config)
|
29
|
+
@redis_config = redis_config
|
30
|
+
end
|
31
|
+
|
32
|
+
def enqueue(queue, job_name, params = nil)
|
33
|
+
job = Qs::Job.new(job_name, params || {})
|
34
|
+
enqueue!(queue, job)
|
35
|
+
job
|
36
|
+
end
|
37
|
+
|
38
|
+
def push(queue_name, payload)
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
def block_dequeue(*args)
|
43
|
+
self.redis.with{ |c| c.brpop(*args) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def append(queue_redis_key, serialized_payload)
|
47
|
+
self.redis.with{ |c| c.lpush(queue_redis_key, serialized_payload) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def prepend(queue_redis_key, serialized_payload)
|
51
|
+
self.redis.with{ |c| c.rpush(queue_redis_key, serialized_payload) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def clear(redis_key)
|
55
|
+
self.redis.with{ |c| c.del(redis_key) }
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
class QsClient
|
63
|
+
include Client
|
64
|
+
|
65
|
+
def initialize(*args)
|
66
|
+
super
|
67
|
+
@redis = HellaRedis::Connection.new(self.redis_config)
|
68
|
+
end
|
69
|
+
|
70
|
+
def push(queue_name, payload)
|
71
|
+
queue_redis_key = Queue::RedisKey.new(queue_name)
|
72
|
+
serialized_payload = Qs.serialize(payload)
|
73
|
+
self.append(queue_redis_key, serialized_payload)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def enqueue!(queue, job)
|
79
|
+
serialized_payload = Qs.serialize(job.to_payload)
|
80
|
+
self.append(queue.redis_key, serialized_payload)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
class TestClient
|
86
|
+
include Client
|
87
|
+
|
88
|
+
attr_reader :pushed_items
|
89
|
+
|
90
|
+
def initialize(*args)
|
91
|
+
super
|
92
|
+
require 'hella-redis/connection_spy'
|
93
|
+
@redis = HellaRedis::ConnectionSpy.new(self.redis_config)
|
94
|
+
@pushed_items = []
|
95
|
+
end
|
96
|
+
|
97
|
+
def push(queue_name, payload)
|
98
|
+
# attempt to serialize (and then throw away) the payload, this will error
|
99
|
+
# on the developer if it can't be serialized
|
100
|
+
Qs.serialize(payload)
|
101
|
+
@pushed_items << PushedItem.new(queue_name, payload)
|
102
|
+
end
|
103
|
+
|
104
|
+
def reset!
|
105
|
+
@pushed_items.clear
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def enqueue!(queue, job)
|
111
|
+
# attempt to serialize (and then throw away) the job payload, this will
|
112
|
+
# error on the developer if it can't serialize the job
|
113
|
+
Qs.serialize(job.to_payload)
|
114
|
+
queue.enqueued_jobs << job
|
115
|
+
end
|
116
|
+
|
117
|
+
PushedItem = Struct.new(:queue_name, :payload)
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'qs/daemon'
|
2
|
+
|
3
|
+
module Qs
|
4
|
+
|
5
|
+
class ConfigFile
|
6
|
+
|
7
|
+
attr_reader :daemon
|
8
|
+
|
9
|
+
def initialize(file_path)
|
10
|
+
@file_path = build_file_path(file_path)
|
11
|
+
@daemon = nil
|
12
|
+
evaluate_file(@file_path)
|
13
|
+
validate!
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(daemon)
|
17
|
+
@daemon = daemon
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def validate!
|
23
|
+
if !@daemon.kind_of?(Qs::Daemon)
|
24
|
+
raise NoDaemonError.new(@daemon, @file_path)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_file_path(path)
|
29
|
+
full_path = File.expand_path(path)
|
30
|
+
raise NoConfigFileError.new(full_path) unless File.exists?(full_path)
|
31
|
+
full_path
|
32
|
+
rescue NoConfigFileError
|
33
|
+
full_path_with_sanford = "#{full_path}.qs"
|
34
|
+
raise unless File.exists?(full_path_with_sanford)
|
35
|
+
full_path_with_sanford
|
36
|
+
end
|
37
|
+
|
38
|
+
# This evaluates the file and creates a proc using it's contents. This is
|
39
|
+
# a trick borrowed from Rack. It is essentially converting a file into a
|
40
|
+
# proc and then instance eval'ing it. This has a couple benefits:
|
41
|
+
# * The obvious benefit is the file is evaluated in the context of this
|
42
|
+
# class. This allows the file to call `run`, setting the daemon that
|
43
|
+
# will be used.
|
44
|
+
# * The other benefit is that the file's contents behave like they were a
|
45
|
+
# proc defined by the user. Instance eval'ing the file directly, makes
|
46
|
+
# any constants (modules/classes) defined in it namespaced by the
|
47
|
+
# instance of the config (not namespaced by `Qs::ConfigFile`,
|
48
|
+
# they are actually namespaced by an instance of this class, its like
|
49
|
+
# accessing it via `ConfigFile.new::MyDaemon`), which is very confusing.
|
50
|
+
# Thus, the proc is created and eval'd using the `TOPLEVEL_BINDING`,
|
51
|
+
# which defines the constants at the top-level, as would be expected.
|
52
|
+
def evaluate_file(file_path)
|
53
|
+
config_file_code = "proc{ #{File.read(file_path)} }"
|
54
|
+
config_file_proc = eval(config_file_code, TOPLEVEL_BINDING, file_path, 0)
|
55
|
+
self.instance_eval(&config_file_proc)
|
56
|
+
end
|
57
|
+
|
58
|
+
InvalidError = Class.new(StandardError)
|
59
|
+
|
60
|
+
class NoConfigFileError < InvalidError
|
61
|
+
def initialize(path)
|
62
|
+
super "A configuration file couldn't be found at: #{path.to_s.inspect}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class NoDaemonError < InvalidError
|
67
|
+
def initialize(daemon, path)
|
68
|
+
prefix = "Configuration file #{path.to_s.inspect}"
|
69
|
+
if daemon
|
70
|
+
super "#{prefix} called `run` without a Qs::Daemon"
|
71
|
+
else
|
72
|
+
super "#{prefix} didn't call `run` with a Qs::Daemon"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|