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.
@@ -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