freud 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +3 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +33 -0
- data/Rakefile +13 -0
- data/bin/freud +5 -0
- data/freud.gemspec +27 -0
- data/lib/freud.rb +5 -0
- data/lib/freud/config.rb +227 -0
- data/lib/freud/launcher.rb +121 -0
- data/lib/freud/logging.rb +44 -0
- data/lib/freud/pidfile.rb +49 -0
- data/lib/freud/runner.rb +197 -0
- data/lib/freud/variables.rb +64 -0
- data/lib/freud/version.rb +3 -0
- data/spec/config_spec.rb +141 -0
- data/spec/fixtures/true.json +6 -0
- data/spec/launcher_spec.rb +114 -0
- data/spec/pidfile_spec.rb +41 -0
- data/spec/runner_spec.rb +133 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/variables_spec.rb +74 -0
- metadata +194 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
require "agrippa/mutable"
|
2
|
+
require "freud/logging"
|
3
|
+
|
4
|
+
module Freud
|
5
|
+
class Launcher
|
6
|
+
include Logging
|
7
|
+
|
8
|
+
include Agrippa::Mutable
|
9
|
+
|
10
|
+
state_reader %w(name root pidfile logfile background create_pidfile
|
11
|
+
reset_env env commands sudo_user args)
|
12
|
+
|
13
|
+
state_accessor :process
|
14
|
+
|
15
|
+
def default_state
|
16
|
+
{ process: Process }
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(command, args = [])
|
20
|
+
@args = args
|
21
|
+
case(command)
|
22
|
+
when "help" then show_help
|
23
|
+
when "start" then daemonize(fetch_executable(command, true))
|
24
|
+
else execute(fetch_executable(command))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def fetch_executable(command, background = false)
|
31
|
+
apply_sudo(background) do
|
32
|
+
commands.fetch(command) do
|
33
|
+
show_help(false)
|
34
|
+
logger.fatal("Unknown command: #{command}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def apply_sudo(background)
|
40
|
+
command = yield
|
41
|
+
return(command) unless (sudo_user.to_s != "")
|
42
|
+
bash = sprintf('bash -c "%s"', command.gsub(/"/, '\\"'))
|
43
|
+
sudo_env = env.map { |key, value| sprintf('%s="%s"', key,
|
44
|
+
value.gsub(/"/, '\\"')) }.join(" ")
|
45
|
+
maybe_background = background ? "-b" : ""
|
46
|
+
sudo_options = "-n #{maybe_background} -u #{sudo_user}"
|
47
|
+
"sudo #{sudo_options} #{sudo_env} -- #{bash}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def show_help(terminate = true)
|
51
|
+
logger.info("Valid commands: #{commands.keys.join(", ")}")
|
52
|
+
exit(0) if terminate
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def execute(command, options = nil)
|
57
|
+
log_runtime_environment(command)
|
58
|
+
$PROGRAM_NAME = command
|
59
|
+
process.exec(env, command, options || spawn_default_options)
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def daemonize(command)
|
64
|
+
return(self) if running?
|
65
|
+
options = spawn_default_options
|
66
|
+
options[:err] = [ logfile, "a" ] if logfile
|
67
|
+
create_logfile
|
68
|
+
if background
|
69
|
+
options.merge!(pgroup: true)
|
70
|
+
log_runtime_environment(command, options)
|
71
|
+
pid = process.spawn(env, command, options)
|
72
|
+
maybe_create_pidfile(pid)
|
73
|
+
else
|
74
|
+
$PROGRAM_NAME = command
|
75
|
+
maybe_create_pidfile(process.pid)
|
76
|
+
execute(command, options)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def log_runtime_environment(command, options = nil)
|
81
|
+
options ||= spawn_default_options
|
82
|
+
logger.debug("running #{command}")
|
83
|
+
logger.debug("env #{ENV.inspect}")
|
84
|
+
logger.debug("env #{env.inspect}")
|
85
|
+
logger.debug("spawn_default_options #{options.inspect}")
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def create_logfile
|
90
|
+
return unless logfile
|
91
|
+
begin
|
92
|
+
file = File.open(logfile, "a")
|
93
|
+
file.close
|
94
|
+
rescue
|
95
|
+
logger.fatal("Unable to open logfile: #{logfile}")
|
96
|
+
end
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
def spawn_default_options
|
101
|
+
output = {}
|
102
|
+
output[:unsetenv_others] = (reset_env == true)
|
103
|
+
output[:chdir] = root
|
104
|
+
output[:close_others] = true
|
105
|
+
output[:in] = "/dev/null"
|
106
|
+
output[:out] = :err
|
107
|
+
output
|
108
|
+
end
|
109
|
+
|
110
|
+
def maybe_create_pidfile(pid)
|
111
|
+
return(self) unless (create_pidfile == true)
|
112
|
+
pidfile.write(pid)
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
# FIXME Kill stale pidfile?
|
117
|
+
def running?
|
118
|
+
pidfile.running?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Freud
|
4
|
+
class RunnerExit < StandardError
|
5
|
+
attr_reader :message, :value
|
6
|
+
|
7
|
+
def initialize(message, value = 1)
|
8
|
+
@message, @value = message, value
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class FreudLogger < Logger
|
13
|
+
def initialize(*args)
|
14
|
+
super
|
15
|
+
debug_on = ENV.has_key?("DEBUG")
|
16
|
+
self.level = debug_on ? Logger::DEBUG : Logger::INFO
|
17
|
+
self.formatter = proc { |s, t, p, m| "#{m.strip}\n" }
|
18
|
+
end
|
19
|
+
|
20
|
+
def fatal(message)
|
21
|
+
super
|
22
|
+
raise(RunnerExit.new(message, 1))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Logging
|
27
|
+
def self.log_to(stream)
|
28
|
+
@logger = FreudLogger.new(stream)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.logger
|
33
|
+
@logger ||= FreudLogger.new($stderr)
|
34
|
+
end
|
35
|
+
|
36
|
+
def logger
|
37
|
+
Freud::Logging.logger
|
38
|
+
end
|
39
|
+
|
40
|
+
def exit(value = 0)
|
41
|
+
raise(RunnerExit.new(nil, value))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "freud/logging"
|
2
|
+
|
3
|
+
module Freud
|
4
|
+
class Pidfile
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
def initialize(path)
|
8
|
+
@path = path
|
9
|
+
end
|
10
|
+
|
11
|
+
def write(pid)
|
12
|
+
File.open(@path, "w") { |f| f.write(pid.to_s) }
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def read
|
17
|
+
return unless @path
|
18
|
+
return unless (File.exists?(@path) and File.readable?(@path))
|
19
|
+
output = File.read(@path)
|
20
|
+
output ? output.to_i : nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
@path
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
File.expand_path(to_s) == File.expand_path(other.to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
def kill(signal)
|
32
|
+
pid = read
|
33
|
+
return(self) unless pid
|
34
|
+
Process.kill(signal, pid)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def running?
|
39
|
+
begin
|
40
|
+
kill(0)
|
41
|
+
true
|
42
|
+
rescue Errno::ESRCH
|
43
|
+
false
|
44
|
+
rescue Errno::EPERM
|
45
|
+
true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/freud/runner.rb
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
require "freud/version"
|
2
|
+
require "freud/logging"
|
3
|
+
require "freud/config"
|
4
|
+
require "freud/launcher"
|
5
|
+
|
6
|
+
module Freud
|
7
|
+
class Runner
|
8
|
+
include Logging
|
9
|
+
|
10
|
+
def self.run(args = ARGV)
|
11
|
+
begin
|
12
|
+
new.run(args)
|
13
|
+
rescue RunnerExit => exception
|
14
|
+
exit(exception.value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def run(args)
|
19
|
+
command = extract_command(args)
|
20
|
+
case(command)
|
21
|
+
when "version" then run_version
|
22
|
+
when "generate", "g" then run_generate(args)
|
23
|
+
when "@check" then run_check(args)
|
24
|
+
when "@wait-up" then run_wait_up(args)
|
25
|
+
when "@wait-down" then run_wait_down(args)
|
26
|
+
when "@signal-term" then run_signal(args, "TERM")
|
27
|
+
when "@signal-kill" then run_signal(args, "KILL")
|
28
|
+
when "@signal-hup" then run_signal(args, "HUP")
|
29
|
+
when "@dump" then run_dump(args)
|
30
|
+
else Launcher.new(fetch_config(args).to_hash).run(command, args)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def fetch_config(args)
|
37
|
+
file = extract_file(args)
|
38
|
+
stage = extract_stage(args)
|
39
|
+
Config.new.load(file, stage)
|
40
|
+
end
|
41
|
+
|
42
|
+
def run_version
|
43
|
+
logger.info Freud::VERSION
|
44
|
+
exit(0)
|
45
|
+
end
|
46
|
+
|
47
|
+
def run_generate(args)
|
48
|
+
logger.fatal("Usage: freud new [file]") unless args.first
|
49
|
+
path = args.first.sub(/(\.json)?$/, ".json")
|
50
|
+
name = File.basename(path).sub(/(\.json)?$/, "")
|
51
|
+
logger.fatal("File exists: #{path}") if File.exists?(path)
|
52
|
+
scaffold = <<-END
|
53
|
+
{
|
54
|
+
"name": "#{name}",
|
55
|
+
"root": "#{File.expand_path(Dir.pwd)}",
|
56
|
+
"background": false,
|
57
|
+
"create_pidfile": false,
|
58
|
+
"reset_env": false,
|
59
|
+
"pidfile": "tmp/#{name}.pid",
|
60
|
+
"logfile": "log/#{name}.log",
|
61
|
+
"vars": {},
|
62
|
+
"env": {},
|
63
|
+
"stages": {
|
64
|
+
"development": {},
|
65
|
+
"production": {}
|
66
|
+
},
|
67
|
+
"commands": {
|
68
|
+
"start": "/bin/false",
|
69
|
+
"stop": "%freud @signal-term; %freud @wait-down",
|
70
|
+
"restart": "%freud stop && %freud start",
|
71
|
+
"reload": "%freud @signal-hup; %freud @wait-up",
|
72
|
+
"kill": "%freud @signal-kill; %freud @wait-down",
|
73
|
+
"status": "%freud @check"
|
74
|
+
}
|
75
|
+
}
|
76
|
+
END
|
77
|
+
lines = scaffold.lines.map { |l| l.rstrip.sub(/^\s{16}/, "") }
|
78
|
+
File.open(path, "w") { |f| f.write(lines.join("\n")) }
|
79
|
+
exit(0)
|
80
|
+
end
|
81
|
+
|
82
|
+
def run_check(args)
|
83
|
+
config = fetch_config(args)
|
84
|
+
print_status(config)
|
85
|
+
end
|
86
|
+
|
87
|
+
def print_status(config)
|
88
|
+
pidfile = config.fetch("pidfile")
|
89
|
+
name = config.fetch("name")
|
90
|
+
if pidfile.running?
|
91
|
+
pid = pidfile.read
|
92
|
+
logger.info("#{name} up with PID #{pid}.")
|
93
|
+
else
|
94
|
+
logger.info("#{name} down.")
|
95
|
+
end
|
96
|
+
exit(0)
|
97
|
+
end
|
98
|
+
|
99
|
+
def run_wait_down(args)
|
100
|
+
timeout = (extract_option(args, "-t", "--timeout") || 30).to_i
|
101
|
+
config = fetch_config(args)
|
102
|
+
pidfile = config.fetch("pidfile")
|
103
|
+
name = config.fetch("name")
|
104
|
+
started_at = Time.now.to_i
|
105
|
+
logger.info("Waiting #{timeout} seconds for #{name} to stop.") \
|
106
|
+
if pidfile.running?
|
107
|
+
while(pidfile.running?)
|
108
|
+
sleep(0.25)
|
109
|
+
next if ((Time.now.to_i - started_at) < timeout)
|
110
|
+
logger.info("#{name} not down within #{timeout} seconds.")
|
111
|
+
exit(1)
|
112
|
+
end
|
113
|
+
print_status(config)
|
114
|
+
end
|
115
|
+
|
116
|
+
def run_wait_up(args)
|
117
|
+
timeout = (extract_option(args, "-t", "--timeout") || 30).to_i
|
118
|
+
config = fetch_config(args)
|
119
|
+
pidfile = config.fetch("pidfile")
|
120
|
+
name = config.fetch("name")
|
121
|
+
started_at = Time.now.to_i
|
122
|
+
while(not pidfile.running?)
|
123
|
+
sleep(0.25)
|
124
|
+
next if ((Time.now.to_i - started_at) < timeout)
|
125
|
+
logger.info("#{name} not up within #{timeout} seconds.")
|
126
|
+
exit(1)
|
127
|
+
end
|
128
|
+
print_status(config)
|
129
|
+
end
|
130
|
+
|
131
|
+
def run_signal(args, signal)
|
132
|
+
config = fetch_config(args)
|
133
|
+
pidfile = config.fetch("pidfile")
|
134
|
+
exit(1) unless pidfile.running?
|
135
|
+
pidfile.kill(signal)
|
136
|
+
exit(0)
|
137
|
+
end
|
138
|
+
|
139
|
+
def run_dump(args)
|
140
|
+
fetch_config(args).dump
|
141
|
+
exit(0)
|
142
|
+
end
|
143
|
+
|
144
|
+
def extract_flag(args, *flags)
|
145
|
+
flags.inject(false) { |out, flag| args.delete(flag) ? true : out }
|
146
|
+
end
|
147
|
+
|
148
|
+
def extract_option(args, *options)
|
149
|
+
output_args, index, value = [], 0, nil
|
150
|
+
while(index < args.length)
|
151
|
+
head = args[index]
|
152
|
+
tail = args[index + 1]
|
153
|
+
if options.include?(head)
|
154
|
+
index += 2
|
155
|
+
value = tail
|
156
|
+
else
|
157
|
+
index += 1
|
158
|
+
output_args.push(head)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
args.replace(output_args)
|
162
|
+
value
|
163
|
+
end
|
164
|
+
|
165
|
+
def extract_command(args)
|
166
|
+
return(args.shift) unless args.empty?
|
167
|
+
usage
|
168
|
+
end
|
169
|
+
|
170
|
+
def extract_file(args)
|
171
|
+
service_path = ENV["FREUD_SERVICE_PATH"] || "services"
|
172
|
+
path = args.shift
|
173
|
+
filename = first_file_in(path, "#{service_path}/#{path}.json",
|
174
|
+
ENV["FREUD_CONFIG"])
|
175
|
+
usage unless filename
|
176
|
+
logger.fatal("Can't open: #{filename}") \
|
177
|
+
unless (File.file?(filename) and File.readable?(filename))
|
178
|
+
File.open(filename, "r")
|
179
|
+
end
|
180
|
+
|
181
|
+
def extract_stage(args)
|
182
|
+
args.shift || ENV["FREUD_STAGE"] || "development"
|
183
|
+
end
|
184
|
+
|
185
|
+
def first_file_in(*paths)
|
186
|
+
paths.each do |path|
|
187
|
+
next if path.nil?
|
188
|
+
return(path) if File.exists?(path)
|
189
|
+
end
|
190
|
+
nil
|
191
|
+
end
|
192
|
+
|
193
|
+
def usage
|
194
|
+
logger.fatal("Usage: freud [command] [file] <stage>")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "agrippa/mutable_hash"
|
2
|
+
|
3
|
+
module Freud
|
4
|
+
class Variables
|
5
|
+
VARIABLES = /(?<!\\)%((\w+)|{(\w+)}|{(\w+)\|(.*)})/i
|
6
|
+
|
7
|
+
ESCAPED_SIGILS = /\\%/
|
8
|
+
|
9
|
+
UNDEFINED = Object.new
|
10
|
+
|
11
|
+
include Agrippa::MutableHash
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
super
|
15
|
+
@stack = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def each_pair(&block)
|
19
|
+
@state.each_pair(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test(input)
|
23
|
+
(input =~ VARIABLES) ? true : false
|
24
|
+
end
|
25
|
+
|
26
|
+
def merge(updates)
|
27
|
+
chain(updates)
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch(key, default = UNDEFINED)
|
31
|
+
key = key.to_sym
|
32
|
+
return(@state.fetch(key, default)) unless (default == UNDEFINED)
|
33
|
+
@state.fetch(key) { raise(KeyError, "Unknown variable: #{key}") }
|
34
|
+
end
|
35
|
+
|
36
|
+
def apply(input)
|
37
|
+
return(nil) if input.nil?
|
38
|
+
interpolated = input.gsub(VARIABLES) do
|
39
|
+
key = $~[2] || $~[3] || $~[4]
|
40
|
+
default = $~[5] || UNDEFINED
|
41
|
+
push_stack(key, input)
|
42
|
+
output = apply(fetch(key, default).to_s)
|
43
|
+
pop_stack(key)
|
44
|
+
output
|
45
|
+
end
|
46
|
+
interpolated.gsub(ESCAPED_SIGILS, "%")
|
47
|
+
end
|
48
|
+
|
49
|
+
def push_stack(key, input)
|
50
|
+
if @stack[key]
|
51
|
+
message = "Infinite loop evaluating '%#{key}' in '#{input}'"
|
52
|
+
raise(RuntimeError, message)
|
53
|
+
else
|
54
|
+
@stack[key] = true
|
55
|
+
self
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def pop_stack(key)
|
60
|
+
@stack.delete(key)
|
61
|
+
self
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|