sumskyi-bluepill 0.0.42.0

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