dylanvaughn-bluepill 0.0.39

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,71 @@
1
+ module Bluepill
2
+ class Group
3
+ attr_accessor :name, :processes, :logger
4
+ attr_accessor :process_logger
5
+
6
+ def initialize(name, options = {})
7
+ self.name = name
8
+ self.processes = []
9
+ self.logger = options[:logger]
10
+ end
11
+
12
+ def add_process(process)
13
+ process.logger = self.logger.prefix_with(process.name)
14
+ self.processes << process
15
+ end
16
+
17
+ def tick
18
+ self.processes.each do |process|
19
+ process.tick
20
+ end
21
+ end
22
+
23
+ def determine_initial_state
24
+ self.processes.each do |process|
25
+ process.determine_initial_state
26
+ end
27
+ end
28
+
29
+ # proxied events
30
+ [:start, :unmonitor, :stop, :restart].each do |event|
31
+ class_eval <<-END
32
+ def #{event}(process_name = nil)
33
+ threads = []
34
+ affected = []
35
+ self.processes.each do |process|
36
+ next if process_name && process_name != process.name
37
+ affected << [self.name, process.name].join(":")
38
+ threads << Thread.new { process.handle_user_command("#{event}") }
39
+ end
40
+ threads.each { |t| t.join }
41
+ affected
42
+ end
43
+ END
44
+ end
45
+
46
+ def status(process_name = nil)
47
+ lines = []
48
+ if process_name.nil?
49
+ prefix = self.name ? " " : ""
50
+ lines << "#{self.name}:" if self.name
51
+
52
+ self.processes.each do |process|
53
+ lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
54
+ if process.monitor_children?
55
+ process.children.each do |child|
56
+ lines << " %s%s: %s" % [prefix, child.name, child.state]
57
+ end
58
+ end
59
+ end
60
+ else
61
+ self.processes.each do |process|
62
+ next if process_name != process.name
63
+ lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
64
+ lines << process.statistics.to_s
65
+ end
66
+ end
67
+ lines << ""
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,62 @@
1
+ module Bluepill
2
+ class Logger
3
+ LOG_METHODS = [:emerg, :alert, :crit, :err, :warning, :notice, :info, :debug]
4
+
5
+ def initialize(options = {})
6
+ @options = options
7
+ @logger = options[:logger] || self.create_logger
8
+ @prefix = options[:prefix]
9
+ @stdout = options[:stdout]
10
+ @prefixes = {}
11
+ end
12
+
13
+ LOG_METHODS.each do |method|
14
+ eval <<-END
15
+ def #{method}(msg, prefix = [])
16
+ if @logger.is_a?(self.class)
17
+ @logger.#{method}(msg, [@prefix] + prefix)
18
+ else
19
+ s_prefix = prefix.size > 0 ? "[\#{prefix.compact.join(':')}] " : ""
20
+ if @stdout
21
+ $stdout.puts("[#{method}]: \#{s_prefix}\#{msg}")
22
+ $stdout.flush
23
+ end
24
+ @logger.#{method}("\#{s_prefix}\#{msg}")
25
+ end
26
+ end
27
+ END
28
+ end
29
+
30
+ def prefix_with(prefix)
31
+ @prefixes[prefix] ||= self.class.new(:logger => self, :prefix => prefix)
32
+ end
33
+
34
+ def reopen
35
+ if @logger.is_a?(self.class)
36
+ @logger.reopen
37
+ else
38
+ @logger = create_logger
39
+ end
40
+ end
41
+
42
+ protected
43
+ def create_logger
44
+ if @options[:log_file]
45
+ LoggerAdapter.new(@options[:log_file])
46
+ else
47
+ Syslog.close if Syslog.opened? # need to explictly close it before reopening it
48
+ Syslog.open(@options[:identity] || 'bluepilld', Syslog::LOG_PID, Syslog::LOG_LOCAL6)
49
+ end
50
+ end
51
+
52
+ class LoggerAdapter < ::Logger
53
+ LOGGER_EQUIVALENTS =
54
+ {:debug => :debug, :err => :error, :warning => :warn, :info => :info, :emerg => :fatal, :alert => :warn, :crit => :fatal, :notice => :info}
55
+
56
+ LOG_METHODS.each do |method|
57
+ next if method == LOGGER_EQUIVALENTS[method]
58
+ alias_method method, LOGGER_EQUIVALENTS[method]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,419 @@
1
+ require "state_machine"
2
+ require "daemons"
3
+
4
+ module Bluepill
5
+ class Process
6
+ CONFIGURABLE_ATTRIBUTES = [
7
+ :start_command,
8
+ :stop_command,
9
+ :restart_command,
10
+
11
+ :stdout,
12
+ :stderr,
13
+ :stdin,
14
+
15
+ :daemonize,
16
+ :pid_file,
17
+ :working_dir,
18
+ :environment,
19
+
20
+ :start_grace_time,
21
+ :stop_grace_time,
22
+ :restart_grace_time,
23
+
24
+ :start_wait_time,
25
+ :stop_wait_time,
26
+ :restart_wait_time,
27
+
28
+ :uid,
29
+ :gid,
30
+
31
+ :monitor_children,
32
+ :child_process_template
33
+ ]
34
+
35
+ attr_accessor :name, :watches, :triggers, :logger, :skip_ticks_until
36
+ attr_accessor *CONFIGURABLE_ATTRIBUTES
37
+ attr_reader :children, :statistics
38
+
39
+ state_machine :initial => :unmonitored do
40
+ # These are the idle states, i.e. only an event (either external or internal) will trigger a transition.
41
+ # The distinction between down and unmonitored is that down
42
+ # means we know it is not running and unmonitored is that we don't care if it's running.
43
+ state :unmonitored, :up, :down
44
+
45
+ # These are transitionary states, we expect the process to change state after a certain period of time.
46
+ state :starting, :stopping, :restarting
47
+
48
+ event :tick do
49
+ transition :starting => :up, :if => :process_running?
50
+ transition :starting => :down, :unless => :process_running?
51
+
52
+ transition :up => :up, :if => :process_running?
53
+ transition :up => :down, :unless => :process_running?
54
+
55
+ # The process failed to die after entering the stopping state. Change the state to reflect
56
+ # reality.
57
+ transition :stopping => :up, :if => :process_running?
58
+ transition :stopping => :down, :unless => :process_running?
59
+
60
+ transition :down => :up, :if => :process_running?
61
+ transition :down => :starting, :unless => :process_running?
62
+
63
+ transition :restarting => :up, :if => :process_running?
64
+ transition :restarting => :down, :unless => :process_running?
65
+ end
66
+
67
+ event :start do
68
+ transition [:unmonitored, :down] => :starting
69
+ end
70
+
71
+ event :stop do
72
+ transition :up => :stopping
73
+ end
74
+
75
+ event :unmonitor do
76
+ transition any => :unmonitored
77
+ end
78
+
79
+ event :restart do
80
+ transition [:up, :down] => :restarting
81
+ end
82
+
83
+ before_transition any => any, :do => :notify_triggers
84
+
85
+ after_transition any => :starting, :do => :start_process
86
+ after_transition any => :stopping, :do => :stop_process
87
+ after_transition any => :restarting, :do => :restart_process
88
+
89
+ after_transition any => any, :do => :record_transition
90
+ end
91
+
92
+ def initialize(process_name, options = {})
93
+ @name = process_name
94
+ @event_mutex = Monitor.new
95
+ @transition_history = Util::RotationalArray.new(10)
96
+ @watches = []
97
+ @triggers = []
98
+ @children = []
99
+ @statistics = ProcessStatistics.new
100
+
101
+ # These defaults are overriden below if it's configured to be something else.
102
+ @monitor_children = false
103
+ @start_grace_time = @stop_grace_time = @restart_grace_time = 3
104
+ @start_wait_time = @stop_wait_time = @restart_wait_time = 3
105
+ @environment = {}
106
+
107
+ CONFIGURABLE_ATTRIBUTES.each do |attribute_name|
108
+ self.send("#{attribute_name}=", options[attribute_name]) if options.has_key?(attribute_name)
109
+ end
110
+
111
+ # Let state_machine do its initialization stuff
112
+ super() # no arguments intentional
113
+ end
114
+
115
+ def tick
116
+ return if self.skipping_ticks?
117
+ self.skip_ticks_until = nil
118
+
119
+ # clear the memoization per tick
120
+ @process_running = nil
121
+
122
+ # run state machine transitions
123
+ super
124
+
125
+ if self.up?
126
+ self.run_watches
127
+
128
+ if self.monitor_children?
129
+ refresh_children!
130
+ children.each {|child| child.tick}
131
+ end
132
+ end
133
+ end
134
+
135
+ def logger=(logger)
136
+ @logger = logger
137
+ self.watches.each {|w| w.logger = logger }
138
+ self.triggers.each {|t| t.logger = logger }
139
+ end
140
+
141
+ # State machine methods
142
+ def dispatch!(event, reason = nil)
143
+ @event_mutex.synchronize do
144
+ @statistics.record_event(event, reason)
145
+ self.send("#{event}")
146
+ end
147
+ end
148
+
149
+ def record_transition(transition)
150
+ unless transition.loopback?
151
+ @transitioned = true
152
+
153
+ # When a process changes state, we should clear the memory of all the watches
154
+ self.watches.each { |w| w.clear_history! }
155
+
156
+ # Also, when a process changes state, we should re-populate its child list
157
+ if self.monitor_children?
158
+ self.logger.warning "Clearing child list"
159
+ self.children.clear
160
+ end
161
+ logger.info "Going from #{transition.from_name} => #{transition.to_name}"
162
+ end
163
+ end
164
+
165
+ def notify_triggers(transition)
166
+ self.triggers.each {|trigger| trigger.notify(transition)}
167
+ end
168
+
169
+ # Watch related methods
170
+ def add_watch(name, options = {})
171
+ self.watches << ConditionWatch.new(name, options.merge(:logger => self.logger))
172
+ end
173
+
174
+ def add_trigger(name, options = {})
175
+ self.triggers << Trigger[name].new(self, options.merge(:logger => self.logger))
176
+ end
177
+
178
+ def run_watches
179
+ now = Time.now.to_i
180
+
181
+ threads = self.watches.collect do |watch|
182
+ [watch, Thread.new { Thread.current[:events] = watch.run(self.actual_pid, now) }]
183
+ end
184
+
185
+ @transitioned = false
186
+
187
+ threads.inject([]) do |events, (watch, thread)|
188
+ thread.join
189
+ if thread[:events].size > 0
190
+ logger.info "#{watch.name} dispatched: #{thread[:events].join(',')}"
191
+ thread[:events].each do |event|
192
+ events << [event, watch.to_s]
193
+ end
194
+ end
195
+ events
196
+ end.each do |(event, reason)|
197
+ break if @transitioned
198
+ self.dispatch!(event, reason)
199
+ end
200
+ end
201
+
202
+ def determine_initial_state
203
+ if self.process_running?(true)
204
+ self.state = 'up'
205
+ else
206
+ # TODO: or "unmonitored" if bluepill was started in no auto-start mode.
207
+ self.state = 'down'
208
+ end
209
+ end
210
+
211
+ def handle_user_command(cmd)
212
+ case cmd
213
+ when "start"
214
+ if self.process_running?(true)
215
+ logger.warning("Refusing to re-run start command on an already running process.")
216
+ else
217
+ dispatch!(:start, "user initiated")
218
+ end
219
+ when "stop"
220
+ stop_process
221
+ dispatch!(:unmonitor, "user initiated")
222
+ when "restart"
223
+ restart_process
224
+ when "unmonitor"
225
+ # When the user issues an unmonitor cmd, reset any triggers so that
226
+ # scheduled events gets cleared
227
+ triggers.each {|t| t.reset! }
228
+ dispatch!(:unmonitor, "user initiated")
229
+ end
230
+ end
231
+
232
+ # System Process Methods
233
+ def process_running?(force = false)
234
+ @process_running = nil if force # clear existing state if forced
235
+
236
+ @process_running ||= signal_process(0)
237
+ # the process isn't running, so we should clear the PID
238
+ self.clear_pid unless @process_running
239
+ @process_running
240
+ end
241
+
242
+ def start_process
243
+ logger.warning "Executing start command: #{start_command}"
244
+
245
+ if self.daemonize?
246
+ System.daemonize(start_command, self.system_command_options)
247
+
248
+ else
249
+ # This is a self-daemonizing process
250
+ with_timeout(start_wait_time) do
251
+ result = System.execute_blocking(start_command, self.system_command_options)
252
+
253
+ unless result[:exit_code].zero?
254
+ logger.warning "Start command execution returned non-zero exit code:"
255
+ logger.warning result.inspect
256
+ end
257
+ end
258
+ end
259
+
260
+ self.skip_ticks_for(start_grace_time)
261
+ end
262
+
263
+ def stop_process
264
+ if stop_command
265
+ cmd = self.prepare_command(stop_command)
266
+ logger.warning "Executing stop command: #{cmd}"
267
+
268
+ with_timeout(stop_wait_time) do
269
+ result = System.execute_blocking(cmd, self.system_command_options)
270
+
271
+ unless result[:exit_code].zero?
272
+ logger.warning "Stop command execution returned non-zero exit code:"
273
+ logger.warning result.inspect
274
+ end
275
+ end
276
+
277
+ else
278
+ logger.warning "Executing default stop command. Sending TERM signal to #{actual_pid}"
279
+ signal_process("TERM")
280
+ end
281
+ self.unlink_pid # TODO: we only write the pid file if we daemonize, should we only unlink it if we daemonize?
282
+
283
+ self.skip_ticks_for(stop_grace_time)
284
+ end
285
+
286
+ def restart_process
287
+ if restart_command
288
+ cmd = self.prepare_command(restart_command)
289
+
290
+ logger.warning "Executing restart command: #{cmd}"
291
+
292
+ with_timeout(restart_wait_time) do
293
+ result = System.execute_blocking(cmd, self.system_command_options)
294
+
295
+ unless result[:exit_code].zero?
296
+ logger.warning "Restart command execution returned non-zero exit code:"
297
+ logger.warning result.inspect
298
+ end
299
+ end
300
+
301
+ self.skip_ticks_for(restart_grace_time)
302
+ else
303
+ logger.warning "No restart_command specified. Must stop and start to restart"
304
+ self.stop_process
305
+ # the tick will bring it back.
306
+ end
307
+ end
308
+
309
+ def daemonize?
310
+ !!self.daemonize
311
+ end
312
+
313
+ def monitor_children?
314
+ !!self.monitor_children
315
+ end
316
+
317
+ def signal_process(code)
318
+ ::Process.kill(code, actual_pid)
319
+ true
320
+ rescue
321
+ false
322
+ end
323
+
324
+ def actual_pid
325
+ @actual_pid ||= begin
326
+ if pid_file
327
+ if File.exists?(pid_file)
328
+ str = File.read(pid_file)
329
+ str.to_i if str.size > 0
330
+ else
331
+ logger.warning("pid_file #{pid_file} does not exist or cannot be read")
332
+ nil
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ def actual_pid=(pid)
339
+ @actual_pid = pid
340
+ end
341
+
342
+ def clear_pid
343
+ @actual_pid = nil
344
+ end
345
+
346
+ def unlink_pid
347
+ File.unlink(pid_file) if pid_file && File.exists?(pid_file)
348
+ end
349
+
350
+ # Internal State Methods
351
+ def skip_ticks_for(seconds)
352
+ # TODO: should this be addative or longest wins?
353
+ # i.e. if two calls for skip_ticks_for come in for 5 and 10, should it skip for 10 or 15?
354
+ self.skip_ticks_until = (self.skip_ticks_until || Time.now.to_i) + seconds.to_i
355
+ end
356
+
357
+ def skipping_ticks?
358
+ self.skip_ticks_until && self.skip_ticks_until > Time.now.to_i
359
+ end
360
+
361
+ def refresh_children!
362
+ # First prune the list of dead children
363
+ @children.delete_if {|child| !child.process_running?(true) }
364
+
365
+ # Add new found children to the list
366
+ new_children_pids = System.get_children(self.actual_pid) - @children.map {|child| child.actual_pid}
367
+
368
+ unless new_children_pids.empty?
369
+ logger.info "Existing children: #{@children.collect{|c| c.actual_pid}.join(",")}. Got new children: #{new_children_pids.inspect} for #{actual_pid}"
370
+ end
371
+
372
+ # Construct a new process wrapper for each new found children
373
+ new_children_pids.each do |child_pid|
374
+ child = self.child_process_template.deep_copy
375
+
376
+ child.name = "<child(pid:#{child_pid})>"
377
+ child.actual_pid = child_pid
378
+ child.logger = self.logger.prefix_with(child.name)
379
+
380
+ child.initialize_state_machines
381
+ child.state = "up"
382
+
383
+ @children << child
384
+ end
385
+ end
386
+
387
+ def deep_copy
388
+ Marshal.load(Marshal.dump(self))
389
+ end
390
+
391
+ def prepare_command(command)
392
+ command.to_s.gsub("{{PID}}", actual_pid.to_s)
393
+ end
394
+
395
+ def system_command_options
396
+ {
397
+ :uid => self.uid,
398
+ :gid => self.gid,
399
+ :working_dir => self.working_dir,
400
+ :environment => self.environment,
401
+ :pid_file => self.pid_file,
402
+ :logger => self.logger,
403
+ :stdin => self.stdin,
404
+ :stdout => self.stdout,
405
+ :stderr => self.stderr
406
+ }
407
+ end
408
+
409
+ def with_timeout(secs, &blk)
410
+ Timeout.timeout(secs.to_f, &blk)
411
+
412
+ rescue Timeout::Error
413
+ logger.err "Execution is taking longer than expected. Unmonitoring."
414
+ logger.err "Did you forget to tell bluepill to daemonize this process?"
415
+ self.dispatch!("unmonitor")
416
+ end
417
+ end
418
+ end
419
+