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