dylanvaughn-bluepill 0.0.39

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