wijet-bluepill 0.0.33

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