freud 0.0.1

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