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,181 @@
1
+ module Bluepill
2
+ class Application
3
+ PROCESS_COMMANDS = [:start, :stop, :restart, :unmonitor, :status]
4
+
5
+ attr_accessor :name, :logger, :base_dir, :socket, :pid_file
6
+ attr_accessor :groups, :work_queue
7
+ attr_accessor :pids_dir, :log_file
8
+
9
+ def initialize(name, options = {})
10
+ self.name = name
11
+
12
+ self.log_file = options[:log_file]
13
+ self.base_dir = options[:base_dir] || '/var/bluepill'
14
+ self.pid_file = File.join(self.base_dir, 'pids', self.name + ".pid")
15
+ self.pids_dir = File.join(self.base_dir, 'pids', self.name)
16
+
17
+ self.groups = {}
18
+
19
+ self.logger = Bluepill::Logger.new(:log_file => self.log_file).prefix_with(self.name)
20
+
21
+ self.setup_signal_traps
22
+ self.setup_pids_dir
23
+ end
24
+
25
+ def load
26
+ begin
27
+ self.start_server
28
+ rescue StandardError => e
29
+ $stderr.puts "Failed to start bluepill:"
30
+ $stderr.puts "%s `%s`" % [e.class.name, e.message]
31
+ $stderr.puts e.backtrace
32
+ exit(5)
33
+ end
34
+ end
35
+
36
+ PROCESS_COMMANDS.each do |command|
37
+ class_eval <<-END
38
+ def #{command}(group_name = nil, process_name = nil)
39
+ self.send_to_process_or_group(:#{command}, group_name, process_name)
40
+ end
41
+ END
42
+ end
43
+
44
+ def add_process(process, group_name = nil)
45
+ group_name = group_name.to_s if group_name
46
+
47
+ self.groups[group_name] ||= Group.new(group_name, :logger => self.logger.prefix_with(group_name))
48
+ self.groups[group_name].add_process(process)
49
+ end
50
+
51
+ def version
52
+ Bluepill::VERSION
53
+ end
54
+
55
+ protected
56
+ def send_to_process_or_group(method, group_name, process_name)
57
+ if group_name.nil? && process_name.nil?
58
+ self.groups.values.collect do |group|
59
+ group.send(method)
60
+ end.flatten
61
+ elsif self.groups.key?(group_name)
62
+ self.groups[group_name].send(method, process_name)
63
+ elsif process_name.nil?
64
+ # they must be targeting just by process name
65
+ process_name = group_name
66
+ self.groups.values.collect do |group|
67
+ group.send(method, process_name)
68
+ end.flatten
69
+ else
70
+ []
71
+ end
72
+ end
73
+
74
+ def start_listener
75
+ @listener_thread.kill if @listener_thread
76
+ @listener_thread = Thread.new do
77
+ loop do
78
+ begin
79
+ client = self.socket.accept
80
+ command, *args = client.readline.strip.split(":")
81
+ response = begin
82
+ self.send(command, *args)
83
+ rescue Exception => e
84
+ e
85
+ end
86
+ client.write(Marshal.dump(response))
87
+ rescue StandardError => e
88
+ logger.err("Got exception in cmd listener: %s `%s`" % [e.class.name, e.message])
89
+ e.backtrace.each {|l| logger.err(l)}
90
+ ensure
91
+ client.close
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def start_server
98
+ self.kill_previous_bluepill
99
+
100
+ Daemonize.daemonize
101
+
102
+ self.logger.reopen
103
+
104
+ $0 = "bluepilld: #{self.name}"
105
+
106
+ self.groups.each {|_, group| group.boot }
107
+
108
+
109
+ self.write_pid_file
110
+ self.socket = Bluepill::Socket.server(self.base_dir, self.name)
111
+ self.start_listener
112
+
113
+ self.run
114
+ end
115
+
116
+ def run
117
+ @running = true # set to false by signal trap
118
+ while @running
119
+ System.reset_data
120
+ self.groups.each { |_, group| group.tick }
121
+ sleep 1
122
+ end
123
+ cleanup
124
+ end
125
+
126
+ def cleanup
127
+ File.unlink(self.socket.path) if self.socket
128
+ File.unlink(self.pid_file) if File.exists?(self.pid_file)
129
+ end
130
+
131
+ def setup_signal_traps
132
+ terminator = lambda do
133
+ puts "Terminating..."
134
+ @running = false
135
+ end
136
+
137
+ Signal.trap("TERM", &terminator)
138
+ Signal.trap("INT", &terminator)
139
+
140
+ Signal.trap("HUP") do
141
+ self.logger.reopen if self.logger
142
+ end
143
+ end
144
+
145
+ def setup_pids_dir
146
+ FileUtils.mkdir_p(self.pids_dir) unless File.exists?(self.pids_dir)
147
+ # we need everybody to be able to write to the pids_dir as processes managed by
148
+ # bluepill will be writing to this dir after they've dropped privileges
149
+ FileUtils.chmod(0777, self.pids_dir)
150
+ end
151
+
152
+ def kill_previous_bluepill
153
+ if File.exists?(self.pid_file)
154
+ previous_pid = File.read(self.pid_file).to_i
155
+ begin
156
+ ::Process.kill(0, previous_pid)
157
+ puts "Killing previous bluepilld[#{previous_pid}]"
158
+ ::Process.kill(2, previous_pid)
159
+ rescue Exception => e
160
+ $stderr.puts "Encountered error trying to kill previous bluepill:"
161
+ $stderr.puts "#{e.class}: #{e.message}"
162
+ exit(4) unless e.is_a?(Errno::ESRCH)
163
+ else
164
+ 10.times do |i|
165
+ sleep 0.5
166
+ break unless System.pid_alive?(previous_pid)
167
+ end
168
+
169
+ if System.pid_alive?(previous_pid)
170
+ $stderr.puts "Previous bluepilld[#{previous_pid}] didn't die"
171
+ exit(4)
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def write_pid_file
178
+ File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) }
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,7 @@
1
+ module Bluepill
2
+ module Application
3
+ module Client
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module Bluepill
2
+ module Application
3
+ module ServerMethods
4
+
5
+ def status
6
+ buffer = ""
7
+ self.processes.each do | process |
8
+ buffer << "#{process.name} #{process.state}\n" +
9
+ end
10
+ buffer
11
+ end
12
+
13
+ def restart
14
+ self.socket = Bluepill::Socket.new(name, base_dir).client
15
+ socket.send("restart\n", 0)
16
+ end
17
+
18
+ def stop
19
+ self.socket = Bluepill::Socket.new(name, base_dir).client
20
+ socket.send("stop\n", 0)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,55 @@
1
+ module Bluepill
2
+ class ConditionWatch
3
+ attr_accessor :logger, :name
4
+ EMPTY_ARRAY = [].freeze # no need to recreate one every tick
5
+
6
+ def initialize(name, options = {})
7
+ @name = name
8
+
9
+ @logger = options.delete(:logger)
10
+ @fires = options.has_key?(:fires) ? Array(options.delete(:fires)) : [:restart]
11
+ @every = options.delete(:every)
12
+ @times = options.delete(:times) || [1,1]
13
+ @times = [@times, @times] unless @times.is_a?(Array) # handles :times => 5
14
+
15
+ self.clear_history!
16
+
17
+ @process_condition = ProcessConditions[@name].new(options)
18
+ end
19
+
20
+ def run(pid, tick_number = Time.now.to_i)
21
+ if @last_ran_at.nil? || (@last_ran_at + @every) <= tick_number
22
+ @last_ran_at = tick_number
23
+ self.record_value(@process_condition.run(pid))
24
+ return @fires if self.fired?
25
+ end
26
+ EMPTY_ARRAY
27
+ end
28
+
29
+ def record_value(value)
30
+ # TODO: record value in ProcessStatistics
31
+ @history[@history_index] = [value, @process_condition.check(value)]
32
+ @history_index = (@history_index + 1) % @history.size
33
+ self.logger.info(self.to_s)
34
+ end
35
+
36
+ def clear_history!
37
+ @last_ran_at = nil
38
+ @history = Array.new(@times[1])
39
+ @history_index = 0
40
+ end
41
+
42
+ def fired?
43
+ @history.select {|v| v && !v[1]}.size >= @times[0]
44
+ end
45
+
46
+ def to_s
47
+ # TODO: this will be out of order because of the way history values are assigned
48
+ # use (@history[(@history_index - 1)..1] + @history[0..(@history_index - 1)]).
49
+ # collect {|v| "#{v[0]}#{v[1] ? '' : '*'}"}.join(", ")
50
+ # but that's gross so... it's gonna be out of order till we figure out a better way to get it in order
51
+ data = @history.collect {|v| "#{@process_condition.format_value(v[0])}#{v[1] ? '' : '*'}" if v}.compact.join(", ")
52
+ "#{@name}: [#{data}]"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,136 @@
1
+ require 'fileutils'
2
+
3
+ module Bluepill
4
+ class Controller
5
+ attr_accessor :base_dir, :log_file, :sockets_dir, :pids_dir
6
+
7
+ def initialize(options = {})
8
+ self.log_file = options[:log_file]
9
+ self.base_dir = options[:base_dir]
10
+ self.sockets_dir = File.join(base_dir, 'socks')
11
+ self.pids_dir = File.join(base_dir, 'pids')
12
+
13
+ setup_dir_structure
14
+ cleanup_bluepill_directory
15
+ end
16
+
17
+ def running_applications
18
+ Dir[File.join(sockets_dir, "*.sock")].map{|x| File.basename(x, ".sock")}
19
+ end
20
+
21
+ def handle_command(application, command, *args)
22
+ case command.to_sym
23
+ when :status
24
+ puts self.send_to_daemon(application, :status, *args)
25
+ when *Application::PROCESS_COMMANDS
26
+ # these need to be sent to the daemon and the results printed out
27
+ affected = self.send_to_daemon(application, command, *args)
28
+ if affected.empty?
29
+ puts "No processes effected"
30
+ else
31
+ puts "Sent #{command} to:"
32
+ affected.each do |process|
33
+ puts " #{process}"
34
+ end
35
+ end
36
+ when :quit
37
+ pid = pid_for(application)
38
+ if System.pid_alive?(pid)
39
+ ::Process.kill("TERM", pid)
40
+ puts "Killing bluepilld[#{pid}]"
41
+ else
42
+ puts "bluepilld[#{pid}] not running"
43
+ end
44
+ when :log
45
+ log_file_location = self.send_to_daemon(application, :log_file)
46
+ log_file_location = self.log_file if log_file_location.to_s.strip.empty?
47
+
48
+ requested_pattern = args.first
49
+ grep_pattern = self.grep_pattern(application, requested_pattern)
50
+
51
+ tail = "tail -n 100 -f #{log_file_location} | grep -E '#{grep_pattern}'"
52
+ puts "Tailing log for #{requested_pattern}..."
53
+ Kernel.exec(tail)
54
+ else
55
+ $stderr.puts "Unknown command `%s` (or application `%s` has not been loaded yet)" % [command, command]
56
+ end
57
+ end
58
+
59
+ def send_to_daemon(application, command, *args)
60
+ begin
61
+ Timeout::timeout(Socket::TIMEOUT) do
62
+ verify_version!(application)
63
+ buffer = ""
64
+ socket = Socket.client(base_dir, application) # Something that should be interrupted if it takes too much time...
65
+ socket.puts(([command] + args).join(":"))
66
+ while line = socket.gets
67
+ buffer << line
68
+ end
69
+ if buffer.size > 0
70
+ response = Marshal.load(buffer)
71
+ if response.is_a?(Exception)
72
+ $stderr.puts "Received error from server:"
73
+ $stderr.puts response.inspect
74
+ $stderr.puts response.backtrace.join("\n")
75
+ exit(8)
76
+ else
77
+ response
78
+ end
79
+ else
80
+ abort("No response from server")
81
+ end
82
+ end
83
+ rescue Timeout::Error
84
+ abort("Socket Timeout: Server may not be responding")
85
+ rescue Errno::ECONNREFUSED
86
+ abort("Connection Refused: Server is not running")
87
+ end
88
+ end
89
+
90
+ def grep_pattern(application, query = nil)
91
+ pattern = [application, query].compact.join(':')
92
+ ['\[.*', Regexp.escape(pattern), '.*'].compact.join
93
+ end
94
+ private
95
+
96
+ def cleanup_bluepill_directory
97
+ self.running_applications.each do |app|
98
+ pid = pid_for(app)
99
+ if !pid || !System.pid_alive?(pid)
100
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
101
+ sock_file = File.join(self.sockets_dir, "#{app}.sock")
102
+ File.unlink(pid_file) if File.exists?(pid_file)
103
+ File.unlink(sock_file) if File.exists?(sock_file)
104
+ end
105
+ end
106
+ end
107
+
108
+ def pid_for(app)
109
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
110
+ File.exists?(pid_file) && File.read(pid_file).to_i
111
+ end
112
+
113
+ def setup_dir_structure
114
+ [@sockets_dir, @pids_dir].each do |dir|
115
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
116
+ end
117
+ end
118
+
119
+ def verify_version!(application)
120
+ begin
121
+ socket = Socket.client(self.base_dir, application)
122
+ socket.puts("version")
123
+ buffer = ""
124
+ while line = socket.gets
125
+ buffer << line
126
+ end
127
+ version = Marshal.load(buffer)
128
+ if version != Bluepill::VERSION
129
+ abort("The running version of your daemon seems to be out of date.\nDaemon Version: #{version}, CLI Version: #{Bluepill::VERSION}")
130
+ end
131
+ rescue ArgumentError
132
+ abort("The running version of your daemon seems to be out of date.")
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,145 @@
1
+ require 'ostruct'
2
+ module Bluepill
3
+ def self.define_process_condition(name, &block)
4
+ klass = Class.new(ProcessConditions::ProcessCondition, &block)
5
+ ProcessConditions.const_set("#{name.to_s.camelcase}", klass)
6
+ end
7
+
8
+ def self.application(app_name, options = {}, &block)
9
+ app = Application.new(app_name.to_s, options, &block)
10
+
11
+ process_proxy = Class.new do
12
+ attr_reader :attributes, :watches
13
+ def initialize(process_name = nil)
14
+ @name = process_name
15
+ @attributes = {}
16
+ @watches = {}
17
+ end
18
+
19
+ def method_missing(name, *args)
20
+ if args.size == 1 && name.to_s =~ /^(.*)=$/
21
+ @attributes[$1.to_sym] = args.first
22
+ elsif args.empty? && @attributes.key?(name.to_sym)
23
+ @attributes[name.to_sym]
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def checks(name, options = {})
30
+ @watches[name] = options
31
+ end
32
+
33
+ def validate_child_process(child)
34
+ unless child.attributes.has_key?(:stop_command)
35
+ $stderr.puts "Config Error: Invalid child process monitor for #{@name}"
36
+ $stderr.puts "You must specify a stop command to monitor child processes."
37
+ exit(6)
38
+ end
39
+ end
40
+
41
+ def monitor_children(&child_process_block)
42
+ child_proxy = self.class.new
43
+
44
+ # Children inherit some properties of the parent
45
+ child_proxy.start_grace_time = @attributes[:start_grace_time]
46
+ child_proxy.stop_grace_time = @attributes[:stop_grace_time]
47
+ child_proxy.restart_grace_time = @attributes[:restart_grace_time]
48
+
49
+ child_process_block.call(child_proxy)
50
+ validate_child_process(child_proxy)
51
+
52
+ @attributes[:child_process_template] = child_proxy.to_process(nil)
53
+ # @attributes[:child_process_template].freeze
54
+ @attributes[:monitor_children] = true
55
+ end
56
+
57
+ def to_process(process_name)
58
+ process = Bluepill::Process.new(process_name, @attributes)
59
+ @watches.each do |name, opts|
60
+ if Bluepill::Trigger[name]
61
+ process.add_trigger(name, opts)
62
+ else
63
+ process.add_watch(name, opts)
64
+ end
65
+ end
66
+
67
+ process
68
+ end
69
+ end
70
+
71
+ app_proxy = Class.new do
72
+ @@app = app
73
+ @@process_proxy = process_proxy
74
+ @@process_keys = Hash.new # because I don't want to require Set just for validations
75
+ @@pid_files = Hash.new
76
+ attr_accessor :working_dir, :uid, :gid
77
+
78
+ def validate_process(process, process_name)
79
+ # validate uniqueness of group:process
80
+ process_key = [process.attributes[:group], process_name].join(":")
81
+ if @@process_keys.key?(process_key)
82
+ $stderr.print "Config Error: You have two entries for the process name '#{process_name}'"
83
+ $stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
84
+ $stderr.puts
85
+ exit(6)
86
+ else
87
+ @@process_keys[process_key] = 0
88
+ end
89
+
90
+ # validate required attributes
91
+ [:start_command].each do |required_attr|
92
+ if !process.attributes.key?(required_attr)
93
+ $stderr.puts "Config Error: You must specify a #{required_attr} for '#{process_name}'"
94
+ exit(6)
95
+ end
96
+ end
97
+
98
+ # validate uniqueness of pid files
99
+ pid_key = process.pid_file.strip
100
+ if @@pid_files.key?(pid_key)
101
+ $stderr.puts "Config Error: You have two entries with the pid file: #{process.pid_file}"
102
+ exit(6)
103
+ else
104
+ @@pid_files[pid_key] = 0
105
+ end
106
+ end
107
+
108
+ def process(process_name, &process_block)
109
+ process_proxy = @@process_proxy.new(process_name)
110
+ process_block.call(process_proxy)
111
+ set_app_wide_attributes(process_proxy)
112
+
113
+ assign_default_pid_file(process_proxy, process_name)
114
+
115
+ validate_process(process_proxy, process_name)
116
+
117
+ group = process_proxy.attributes.delete(:group)
118
+ process = process_proxy.to_process(process_name)
119
+
120
+
121
+
122
+ @@app.add_process(process, group)
123
+ end
124
+
125
+ def set_app_wide_attributes(process_proxy)
126
+ [:working_dir, :uid, :gid].each do |attribute|
127
+ unless process_proxy.attributes.key?(attribute)
128
+ process_proxy.attributes[attribute] = self.send(attribute)
129
+ end
130
+ end
131
+ end
132
+
133
+ def assign_default_pid_file(process_proxy, process_name)
134
+ unless process_proxy.attributes.key?(:pid_file)
135
+ group_name = process_proxy.attributes["group"]
136
+ default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_\-]/, "_")
137
+ process_proxy.pid_file = File.join(@@app.pids_dir, default_pid_name + ".pid")
138
+ end
139
+ end
140
+ end
141
+
142
+ yield(app_proxy.new)
143
+ app.load
144
+ end
145
+ end