dosire-god 0.7.9
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.
- data/History.txt +261 -0
- data/Manifest.txt +107 -0
- data/README.txt +59 -0
- data/Rakefile +35 -0
- data/bin/god +127 -0
- data/examples/events.god +84 -0
- data/examples/gravatar.god +54 -0
- data/examples/single.god +66 -0
- data/ext/god/extconf.rb +55 -0
- data/ext/god/kqueue_handler.c +123 -0
- data/ext/god/netlink_handler.c +167 -0
- data/init/god +42 -0
- data/lib/god/behavior.rb +52 -0
- data/lib/god/behaviors/clean_pid_file.rb +21 -0
- data/lib/god/behaviors/clean_unix_socket.rb +21 -0
- data/lib/god/behaviors/notify_when_flapping.rb +51 -0
- data/lib/god/cli/command.rb +206 -0
- data/lib/god/cli/run.rb +177 -0
- data/lib/god/cli/version.rb +23 -0
- data/lib/god/condition.rb +96 -0
- data/lib/god/conditions/always.rb +23 -0
- data/lib/god/conditions/complex.rb +86 -0
- data/lib/god/conditions/cpu_usage.rb +80 -0
- data/lib/god/conditions/degrading_lambda.rb +52 -0
- data/lib/god/conditions/disk_usage.rb +27 -0
- data/lib/god/conditions/flapping.rb +128 -0
- data/lib/god/conditions/http_response_code.rb +168 -0
- data/lib/god/conditions/lambda.rb +25 -0
- data/lib/god/conditions/memory_usage.rb +82 -0
- data/lib/god/conditions/process_exits.rb +72 -0
- data/lib/god/conditions/process_running.rb +74 -0
- data/lib/god/conditions/tries.rb +44 -0
- data/lib/god/configurable.rb +57 -0
- data/lib/god/contact.rb +106 -0
- data/lib/god/contacts/email.rb +95 -0
- data/lib/god/dependency_graph.rb +41 -0
- data/lib/god/diagnostics.rb +37 -0
- data/lib/god/driver.rb +206 -0
- data/lib/god/errors.rb +24 -0
- data/lib/god/event_handler.rb +111 -0
- data/lib/god/event_handlers/dummy_handler.rb +13 -0
- data/lib/god/event_handlers/kqueue_handler.rb +17 -0
- data/lib/god/event_handlers/netlink_handler.rb +13 -0
- data/lib/god/logger.rb +120 -0
- data/lib/god/metric.rb +59 -0
- data/lib/god/process.rb +327 -0
- data/lib/god/registry.rb +32 -0
- data/lib/god/simple_logger.rb +53 -0
- data/lib/god/socket.rb +96 -0
- data/lib/god/sugar.rb +47 -0
- data/lib/god/system/portable_poller.rb +42 -0
- data/lib/god/system/process.rb +42 -0
- data/lib/god/system/slash_proc_poller.rb +82 -0
- data/lib/god/task.rb +487 -0
- data/lib/god/timeline.rb +25 -0
- data/lib/god/trigger.rb +43 -0
- data/lib/god/watch.rb +183 -0
- data/lib/god.rb +644 -0
- data/test/configs/child_events/child_events.god +44 -0
- data/test/configs/child_events/simple_server.rb +3 -0
- data/test/configs/child_polls/child_polls.god +37 -0
- data/test/configs/child_polls/simple_server.rb +12 -0
- data/test/configs/complex/complex.god +59 -0
- data/test/configs/complex/simple_server.rb +3 -0
- data/test/configs/contact/contact.god +74 -0
- data/test/configs/contact/simple_server.rb +3 -0
- data/test/configs/daemon_events/daemon_events.god +37 -0
- data/test/configs/daemon_events/simple_server.rb +8 -0
- data/test/configs/daemon_events/simple_server_stop.rb +11 -0
- data/test/configs/daemon_polls/daemon_polls.god +17 -0
- data/test/configs/daemon_polls/simple_server.rb +6 -0
- data/test/configs/degrading_lambda/degrading_lambda.god +31 -0
- data/test/configs/degrading_lambda/tcp_server.rb +15 -0
- data/test/configs/matias/matias.god +50 -0
- data/test/configs/real.rb +59 -0
- data/test/configs/running_load/running_load.god +16 -0
- data/test/configs/stress/simple_server.rb +3 -0
- data/test/configs/stress/stress.god +15 -0
- data/test/configs/task/logs/.placeholder +0 -0
- data/test/configs/task/task.god +26 -0
- data/test/configs/test.rb +61 -0
- data/test/helper.rb +151 -0
- data/test/suite.rb +6 -0
- data/test/test_behavior.rb +21 -0
- data/test/test_condition.rb +50 -0
- data/test/test_conditions_disk_usage.rb +56 -0
- data/test/test_conditions_http_response_code.rb +109 -0
- data/test/test_conditions_process_running.rb +44 -0
- data/test/test_conditions_tries.rb +67 -0
- data/test/test_contact.rb +109 -0
- data/test/test_dependency_graph.rb +62 -0
- data/test/test_driver.rb +11 -0
- data/test/test_event_handler.rb +80 -0
- data/test/test_god.rb +598 -0
- data/test/test_handlers_kqueue_handler.rb +16 -0
- data/test/test_logger.rb +63 -0
- data/test/test_metric.rb +72 -0
- data/test/test_process.rb +246 -0
- data/test/test_registry.rb +15 -0
- data/test/test_socket.rb +42 -0
- data/test/test_sugar.rb +42 -0
- data/test/test_system_portable_poller.rb +17 -0
- data/test/test_system_process.rb +30 -0
- data/test/test_task.rb +262 -0
- data/test/test_timeline.rb +37 -0
- data/test/test_trigger.rb +59 -0
- data/test/test_watch.rb +279 -0
- metadata +186 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'netlink_handler_ext'
|
2
|
+
|
3
|
+
module God
|
4
|
+
class NetlinkHandler
|
5
|
+
EVENT_SYSTEM = "netlink"
|
6
|
+
|
7
|
+
def self.register_process(pid, events)
|
8
|
+
# netlink doesn't need to do this
|
9
|
+
# it just reads from the eventhandler actions to see if the pid
|
10
|
+
# matches the list we're looking for -- Kev
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/god/logger.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
module God
|
2
|
+
|
3
|
+
class Logger < SimpleLogger
|
4
|
+
SYSLOG_EQUIVALENTS = {:fatal => :crit,
|
5
|
+
:error => :err,
|
6
|
+
:warn => :debug,
|
7
|
+
:info => :debug,
|
8
|
+
:debug => :debug}
|
9
|
+
|
10
|
+
attr_accessor :logs
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_accessor :syslog
|
14
|
+
end
|
15
|
+
|
16
|
+
self.syslog ||= true
|
17
|
+
|
18
|
+
# Instantiate a new Logger object
|
19
|
+
def initialize
|
20
|
+
super($stdout)
|
21
|
+
self.logs = {}
|
22
|
+
@mutex = Mutex.new
|
23
|
+
@capture = nil
|
24
|
+
@templogio = StringIO.new
|
25
|
+
@templog = SimpleLogger.new(@templogio)
|
26
|
+
@templog.level = Logger::INFO
|
27
|
+
load_syslog
|
28
|
+
end
|
29
|
+
|
30
|
+
# If Logger.syslog is true then attempt to load the syslog bindings. If syslog
|
31
|
+
# cannot be loaded, then set Logger.syslog to false and continue.
|
32
|
+
#
|
33
|
+
# Returns nothing
|
34
|
+
def load_syslog
|
35
|
+
return unless Logger.syslog
|
36
|
+
|
37
|
+
begin
|
38
|
+
require 'syslog'
|
39
|
+
|
40
|
+
# Ensure that Syslog is open
|
41
|
+
begin
|
42
|
+
Syslog.open('god')
|
43
|
+
rescue RuntimeError
|
44
|
+
Syslog.reopen('god')
|
45
|
+
end
|
46
|
+
rescue Exception
|
47
|
+
Logger.syslog = false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Log a message
|
52
|
+
# +watch+ is the String name of the Watch (may be nil if not Watch is applicable)
|
53
|
+
# +level+ is the log level [:debug|:info|:warn|:error|:fatal]
|
54
|
+
# +text+ is the String message
|
55
|
+
#
|
56
|
+
# Returns nothing
|
57
|
+
def log(watch, level, text)
|
58
|
+
# initialize watch log if necessary
|
59
|
+
self.logs[watch.name] ||= Timeline.new(God::LOG_BUFFER_SIZE_DEFAULT) if watch
|
60
|
+
|
61
|
+
# push onto capture and timeline for the given watch
|
62
|
+
@templogio.truncate(0)
|
63
|
+
@templogio.rewind
|
64
|
+
@templog.send(level, text % [])
|
65
|
+
@mutex.synchronize do
|
66
|
+
@capture.puts(@templogio.string.dup) if @capture
|
67
|
+
self.logs[watch.name] << [Time.now, @templogio.string.dup] if watch
|
68
|
+
end
|
69
|
+
|
70
|
+
# send to regular logger
|
71
|
+
self.send(level, text % [])
|
72
|
+
|
73
|
+
# send to syslog
|
74
|
+
Syslog.send(SYSLOG_EQUIVALENTS[level], text) if Logger.syslog
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get all log output for a given Watch since a certain Time.
|
78
|
+
# +watch_name+ is the String name of the Watch
|
79
|
+
# +since+ is the Time since which to fetch log lines
|
80
|
+
#
|
81
|
+
# Returns String
|
82
|
+
def watch_log_since(watch_name, since)
|
83
|
+
# initialize watch log if necessary
|
84
|
+
self.logs[watch_name] ||= Timeline.new(God::LOG_BUFFER_SIZE_DEFAULT)
|
85
|
+
|
86
|
+
# get and join lines since given time
|
87
|
+
@mutex.synchronize do
|
88
|
+
self.logs[watch_name].select do |x|
|
89
|
+
x.first > since
|
90
|
+
end.map do |x|
|
91
|
+
x[1]
|
92
|
+
end.join
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# private
|
97
|
+
|
98
|
+
# Enable capturing of log
|
99
|
+
#
|
100
|
+
# Returns nothing
|
101
|
+
def start_capture
|
102
|
+
@mutex.synchronize do
|
103
|
+
@capture = StringIO.new
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Disable capturing of log and return what was captured since
|
108
|
+
# capturing was enabled with Logger#start_capture
|
109
|
+
#
|
110
|
+
# Returns String
|
111
|
+
def finish_capture
|
112
|
+
@mutex.synchronize do
|
113
|
+
cap = @capture.string
|
114
|
+
@capture = nil
|
115
|
+
cap
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
data/lib/god/metric.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module God
|
2
|
+
|
3
|
+
class Metric
|
4
|
+
attr_accessor :watch, :destination, :conditions
|
5
|
+
|
6
|
+
def initialize(watch, destination = nil)
|
7
|
+
self.watch = watch
|
8
|
+
self.destination = destination
|
9
|
+
self.conditions = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Instantiate a Condition of type +kind+ and pass it into the optional
|
13
|
+
# block. Attributes of the condition must be set in the config file
|
14
|
+
def condition(kind)
|
15
|
+
# create the condition
|
16
|
+
begin
|
17
|
+
c = Condition.generate(kind, self.watch)
|
18
|
+
rescue NoSuchConditionError => e
|
19
|
+
abort e.message
|
20
|
+
end
|
21
|
+
|
22
|
+
# send to block so config can set attributes
|
23
|
+
yield(c) if block_given?
|
24
|
+
|
25
|
+
# call prepare on the condition
|
26
|
+
c.prepare
|
27
|
+
|
28
|
+
# test generic and specific validity
|
29
|
+
unless Condition.valid?(c) && c.valid?
|
30
|
+
abort "Exiting on invalid condition"
|
31
|
+
end
|
32
|
+
|
33
|
+
# inherit interval from watch if no poll condition specific interval was set
|
34
|
+
if c.kind_of?(PollCondition) && !c.interval
|
35
|
+
if self.watch.interval
|
36
|
+
c.interval = self.watch.interval
|
37
|
+
else
|
38
|
+
abort "No interval set for Condition '#{c.class.name}' in Watch '#{self.watch.name}', and no default Watch interval from which to inherit"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# remember
|
43
|
+
self.conditions << c
|
44
|
+
end
|
45
|
+
|
46
|
+
def enable
|
47
|
+
self.conditions.each do |c|
|
48
|
+
self.watch.attach(c)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def disable
|
53
|
+
self.conditions.each do |c|
|
54
|
+
self.watch.detach(c)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
data/lib/god/process.rb
ADDED
@@ -0,0 +1,327 @@
|
|
1
|
+
module God
|
2
|
+
class Process
|
3
|
+
WRITES_PID = [:start, :restart]
|
4
|
+
|
5
|
+
attr_accessor :name, :uid, :gid, :log, :start, :stop, :restart, :unix_socket, :chroot, :env
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.log = '/dev/null'
|
9
|
+
|
10
|
+
@pid_file = nil
|
11
|
+
@tracking_pid = true
|
12
|
+
@user_log = false
|
13
|
+
@pid = nil
|
14
|
+
@unix_socket = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def alive?
|
18
|
+
if self.pid
|
19
|
+
System::Process.new(self.pid).exists?
|
20
|
+
else
|
21
|
+
false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def file_writable?(file)
|
26
|
+
pid = fork do
|
27
|
+
uid_num = Etc.getpwnam(self.uid).uid if self.uid
|
28
|
+
gid_num = Etc.getgrnam(self.gid).gid if self.gid
|
29
|
+
|
30
|
+
::Dir.chroot(self.chroot) if self.chroot
|
31
|
+
::Process.groups = [gid_num] if self.gid
|
32
|
+
::Process::Sys.setgid(gid_num) if self.gid
|
33
|
+
::Process::Sys.setuid(uid_num) if self.uid
|
34
|
+
|
35
|
+
File.writable?(file_in_chroot(file)) ? exit(0) : exit(1)
|
36
|
+
end
|
37
|
+
|
38
|
+
wpid, status = ::Process.waitpid2(pid)
|
39
|
+
status.exitstatus == 0 ? true : false
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid?
|
43
|
+
# determine if we're tracking pid or not
|
44
|
+
self.pid_file
|
45
|
+
|
46
|
+
valid = true
|
47
|
+
|
48
|
+
# a start command must be specified
|
49
|
+
if self.start.nil?
|
50
|
+
valid = false
|
51
|
+
applog(self, :error, "No start command was specified")
|
52
|
+
end
|
53
|
+
|
54
|
+
# self-daemonizing processes must specify a stop command
|
55
|
+
if !@tracking_pid && self.stop.nil?
|
56
|
+
valid = false
|
57
|
+
applog(self, :error, "No stop command was specified")
|
58
|
+
end
|
59
|
+
|
60
|
+
# uid must exist if specified
|
61
|
+
if self.uid
|
62
|
+
begin
|
63
|
+
Etc.getpwnam(self.uid)
|
64
|
+
rescue ArgumentError
|
65
|
+
valid = false
|
66
|
+
applog(self, :error, "UID for '#{self.uid}' does not exist")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# gid must exist if specified
|
71
|
+
if self.gid
|
72
|
+
begin
|
73
|
+
Etc.getgrnam(self.gid)
|
74
|
+
rescue ArgumentError
|
75
|
+
valid = false
|
76
|
+
applog(self, :error, "GID for '#{self.gid}' does not exist")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# pid dir must exist if specified
|
81
|
+
if !@tracking_pid && !File.exist?(File.dirname(self.pid_file))
|
82
|
+
valid = false
|
83
|
+
applog(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' does not exist")
|
84
|
+
end
|
85
|
+
|
86
|
+
# pid dir must be writable if specified
|
87
|
+
if !@tracking_pid && File.exist?(File.dirname(self.pid_file)) && !file_writable?(File.dirname(self.pid_file))
|
88
|
+
valid = false
|
89
|
+
applog(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' is not writable by #{self.uid || Etc.getlogin}")
|
90
|
+
end
|
91
|
+
|
92
|
+
# log dir must exist
|
93
|
+
if !File.exist?(File.dirname(self.log))
|
94
|
+
valid = false
|
95
|
+
applog(self, :error, "Log directory '#{File.dirname(self.log)}' does not exist")
|
96
|
+
end
|
97
|
+
|
98
|
+
# log file or dir must be writable
|
99
|
+
if File.exist?(self.log)
|
100
|
+
unless file_writable?(self.log)
|
101
|
+
valid = false
|
102
|
+
applog(self, :error, "Log file '#{self.log}' exists but is not writable by #{self.uid || Etc.getlogin}")
|
103
|
+
end
|
104
|
+
else
|
105
|
+
unless file_writable?(File.dirname(self.log))
|
106
|
+
valid = false
|
107
|
+
applog(self, :error, "Log directory '#{File.dirname(self.log)}' is not writable by #{self.uid || Etc.getlogin}")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# chroot directory must exist and have /dev/null in it
|
112
|
+
if self.chroot
|
113
|
+
if !File.directory?(self.chroot)
|
114
|
+
valid = false
|
115
|
+
LOG.log(self, :error, "CHROOT directory '#{self.chroot}' does not exist")
|
116
|
+
end
|
117
|
+
|
118
|
+
if !File.exist?(File.join(self.chroot, '/dev/null'))
|
119
|
+
valid = false
|
120
|
+
LOG.log(self, :error, "CHROOT directory '#{self.chroot}' does not contain '/dev/null'")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
valid
|
125
|
+
end
|
126
|
+
|
127
|
+
# DON'T USE THIS INTERNALLY. Use the instance variable. -- Kev
|
128
|
+
# No really, trust me. Use the instance variable.
|
129
|
+
def pid_file=(value)
|
130
|
+
# if value is nil, do the right thing
|
131
|
+
if value
|
132
|
+
@tracking_pid = false
|
133
|
+
else
|
134
|
+
@tracking_pid = true
|
135
|
+
end
|
136
|
+
|
137
|
+
@pid_file = value
|
138
|
+
end
|
139
|
+
|
140
|
+
def pid_file
|
141
|
+
@pid_file ||= default_pid_file
|
142
|
+
end
|
143
|
+
|
144
|
+
# Fetch the PID from pid_file. If the pid_file does not
|
145
|
+
# exist, then use the PID from the last time it was read.
|
146
|
+
# If it has never been read, then return nil.
|
147
|
+
#
|
148
|
+
# Returns Integer(pid) or nil
|
149
|
+
def pid
|
150
|
+
contents = File.read(self.pid_file).strip rescue ''
|
151
|
+
real_pid = contents =~ /^\d+$/ ? contents.to_i : nil
|
152
|
+
|
153
|
+
if real_pid
|
154
|
+
@pid = real_pid
|
155
|
+
real_pid
|
156
|
+
else
|
157
|
+
@pid
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def start!
|
162
|
+
call_action(:start)
|
163
|
+
end
|
164
|
+
|
165
|
+
def stop!
|
166
|
+
call_action(:stop)
|
167
|
+
end
|
168
|
+
|
169
|
+
def restart!
|
170
|
+
call_action(:restart)
|
171
|
+
end
|
172
|
+
|
173
|
+
def default_pid_file
|
174
|
+
File.join(God.pid_file_directory, "#{self.name}.pid")
|
175
|
+
end
|
176
|
+
|
177
|
+
def call_action(action)
|
178
|
+
command = send(action)
|
179
|
+
|
180
|
+
if action == :stop && command.nil?
|
181
|
+
pid = self.pid
|
182
|
+
name = self.name
|
183
|
+
command = lambda do
|
184
|
+
applog(self, :info, "#{self.name} stop: default lambda killer")
|
185
|
+
|
186
|
+
::Process.kill('TERM', pid) rescue nil
|
187
|
+
applog(self, :info, "#{self.name} sent SIGTERM")
|
188
|
+
|
189
|
+
# Poll to see if it's dead
|
190
|
+
5.times do
|
191
|
+
begin
|
192
|
+
::Process.kill(0, pid)
|
193
|
+
rescue Errno::ESRCH
|
194
|
+
# It died. Good.
|
195
|
+
applog(self, :info, "#{self.name} process stopped")
|
196
|
+
return
|
197
|
+
end
|
198
|
+
|
199
|
+
sleep 1
|
200
|
+
end
|
201
|
+
|
202
|
+
::Process.kill('KILL', pid) rescue nil
|
203
|
+
applog(self, :info, "#{self.name} still alive; sent SIGKILL")
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
if command.kind_of?(String)
|
208
|
+
pid = nil
|
209
|
+
|
210
|
+
if @tracking_pid
|
211
|
+
# double fork god-daemonized processes
|
212
|
+
# we don't want to wait for them to finish
|
213
|
+
r, w = IO.pipe
|
214
|
+
begin
|
215
|
+
opid = fork do
|
216
|
+
STDOUT.reopen(w)
|
217
|
+
r.close
|
218
|
+
pid = self.spawn(command)
|
219
|
+
puts pid.to_s # send pid back to forker
|
220
|
+
end
|
221
|
+
|
222
|
+
::Process.waitpid(opid, 0)
|
223
|
+
w.close
|
224
|
+
pid = r.gets.chomp
|
225
|
+
ensure
|
226
|
+
# make sure the file descriptors get closed no matter what
|
227
|
+
r.close rescue nil
|
228
|
+
w.close rescue nil
|
229
|
+
end
|
230
|
+
else
|
231
|
+
# single fork self-daemonizing processes
|
232
|
+
# we want to wait for them to finish
|
233
|
+
pid = self.spawn(command)
|
234
|
+
status = ::Process.waitpid2(pid, 0)
|
235
|
+
exit_code = status[1] >> 8
|
236
|
+
|
237
|
+
if exit_code != 0
|
238
|
+
applog(self, :warn, "#{self.name} #{action} command exited with non-zero code = #{exit_code}")
|
239
|
+
end
|
240
|
+
|
241
|
+
ensure_stop if action == :stop
|
242
|
+
end
|
243
|
+
|
244
|
+
if @tracking_pid or (@pid_file.nil? and WRITES_PID.include?(action))
|
245
|
+
File.open(default_pid_file, 'w') do |f|
|
246
|
+
f.write pid
|
247
|
+
end
|
248
|
+
|
249
|
+
@tracking_pid = true
|
250
|
+
@pid_file = default_pid_file
|
251
|
+
end
|
252
|
+
elsif command.kind_of?(Proc)
|
253
|
+
# lambda command
|
254
|
+
command.call
|
255
|
+
else
|
256
|
+
raise NotImplementedError
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Fork/exec the given command, returns immediately
|
261
|
+
# +command+ is the String containing the shell command
|
262
|
+
#
|
263
|
+
# Returns nothing
|
264
|
+
def spawn(command)
|
265
|
+
fork do
|
266
|
+
uid_num = Etc.getpwnam(self.uid).uid if self.uid
|
267
|
+
gid_num = Etc.getgrnam(self.gid).gid if self.gid
|
268
|
+
|
269
|
+
::Dir.chroot(self.chroot) if self.chroot
|
270
|
+
::Process.setsid
|
271
|
+
::Process.groups = [gid_num] if self.gid
|
272
|
+
::Process::Sys.setgid(gid_num) if self.gid
|
273
|
+
::Process::Sys.setuid(uid_num) if self.uid
|
274
|
+
Dir.chdir "/"
|
275
|
+
$0 = command
|
276
|
+
STDIN.reopen "/dev/null"
|
277
|
+
STDOUT.reopen file_in_chroot(self.log), "a"
|
278
|
+
STDERR.reopen STDOUT
|
279
|
+
|
280
|
+
# close any other file descriptors
|
281
|
+
3.upto(256){|fd| IO::new(fd).close rescue nil}
|
282
|
+
|
283
|
+
if self.env && self.env.is_a?(Hash)
|
284
|
+
self.env.each do |(key, value)|
|
285
|
+
ENV[key] = value
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
exec command unless command.empty?
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Ensure that a stop command actually stops the process. Force kill
|
294
|
+
# if necessary.
|
295
|
+
#
|
296
|
+
# Returns nothing
|
297
|
+
def ensure_stop
|
298
|
+
unless self.pid
|
299
|
+
applog(self, :warn, "#{self.name} stop called but pid is uknown")
|
300
|
+
return
|
301
|
+
end
|
302
|
+
|
303
|
+
# Poll to see if it's dead
|
304
|
+
10.times do
|
305
|
+
begin
|
306
|
+
::Process.kill(0, self.pid)
|
307
|
+
rescue Errno::ESRCH
|
308
|
+
# It died. Good.
|
309
|
+
return
|
310
|
+
end
|
311
|
+
|
312
|
+
sleep 1
|
313
|
+
end
|
314
|
+
|
315
|
+
# last resort
|
316
|
+
::Process.kill('KILL', self.pid) rescue nil
|
317
|
+
applog(self, :warn, "#{self.name} process still running 10 seconds after stop command returned. Force killing.")
|
318
|
+
end
|
319
|
+
|
320
|
+
private
|
321
|
+
def file_in_chroot(file)
|
322
|
+
return file unless self.chroot
|
323
|
+
|
324
|
+
file.gsub(/^#{Regexp.escape(File.expand_path(self.chroot))}/, '')
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
data/lib/god/registry.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module God
|
2
|
+
def self.registry
|
3
|
+
@registry ||= Registry.new
|
4
|
+
end
|
5
|
+
|
6
|
+
class Registry
|
7
|
+
def initialize
|
8
|
+
@storage = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(item)
|
12
|
+
# raise TypeError unless item.is_a? God::Process
|
13
|
+
@storage[item.name] = item
|
14
|
+
end
|
15
|
+
|
16
|
+
def remove(item)
|
17
|
+
@storage.delete(item.name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def size
|
21
|
+
@storage.size
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](name)
|
25
|
+
@storage[name]
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset
|
29
|
+
@storage.clear
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module God
|
2
|
+
|
3
|
+
class SimpleLogger
|
4
|
+
DEBUG = 2
|
5
|
+
INFO = 4
|
6
|
+
WARN = 8
|
7
|
+
ERROR = 16
|
8
|
+
FATAL = 32
|
9
|
+
|
10
|
+
SEV_LABEL = {DEBUG => 'DEBUG',
|
11
|
+
INFO => 'INFO',
|
12
|
+
WARN => 'WARN',
|
13
|
+
ERROR => 'ERROR',
|
14
|
+
FATAL => 'FATAL'}
|
15
|
+
|
16
|
+
attr_accessor :datetime_format, :level
|
17
|
+
|
18
|
+
def initialize(io)
|
19
|
+
@io = io
|
20
|
+
@level = INFO
|
21
|
+
@datetime_format = "%Y-%m-%d %H:%M:%S"
|
22
|
+
end
|
23
|
+
|
24
|
+
def output(level, msg)
|
25
|
+
return if level < self.level
|
26
|
+
|
27
|
+
time = Time.now.strftime(self.datetime_format)
|
28
|
+
label = SEV_LABEL[level]
|
29
|
+
@io.print("#{label[0..0]} [#{time}] #{label.rjust(5)}: #{msg}\n")
|
30
|
+
end
|
31
|
+
|
32
|
+
def fatal(msg)
|
33
|
+
self.output(FATAL, msg)
|
34
|
+
end
|
35
|
+
|
36
|
+
def error(msg)
|
37
|
+
self.output(ERROR, msg)
|
38
|
+
end
|
39
|
+
|
40
|
+
def warn(msg)
|
41
|
+
self.output(WARN, msg)
|
42
|
+
end
|
43
|
+
|
44
|
+
def info(msg)
|
45
|
+
self.output(INFO, msg)
|
46
|
+
end
|
47
|
+
|
48
|
+
def debug(msg)
|
49
|
+
self.output(DEBUG, msg)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/lib/god/socket.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'drb'
|
2
|
+
|
3
|
+
module God
|
4
|
+
|
5
|
+
# The God::Server oversees the DRb server which dishes out info on this God daemon.
|
6
|
+
class Socket
|
7
|
+
attr_reader :port
|
8
|
+
|
9
|
+
# The location of the socket for a given port
|
10
|
+
# +port+ is the port number
|
11
|
+
#
|
12
|
+
# Returns String (file location)
|
13
|
+
def self.socket_file(port)
|
14
|
+
"/tmp/god.#{port}.sock"
|
15
|
+
end
|
16
|
+
|
17
|
+
# The address of the socket for a given port
|
18
|
+
# +port+ is the port number
|
19
|
+
#
|
20
|
+
# Returns String (drb address)
|
21
|
+
def self.socket(port)
|
22
|
+
"drbunix://#{self.socket_file(port)}"
|
23
|
+
end
|
24
|
+
|
25
|
+
# The location of the socket for this Server
|
26
|
+
#
|
27
|
+
# Returns String (file location)
|
28
|
+
def socket_file
|
29
|
+
self.class.socket_file(@port)
|
30
|
+
end
|
31
|
+
|
32
|
+
# The address of the socket for this Server
|
33
|
+
#
|
34
|
+
# Returns String (drb address)
|
35
|
+
def socket
|
36
|
+
self.class.socket(@port)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Create a new Server and star the DRb server
|
40
|
+
# +port+ is the port on which to start the DRb service (default nil)
|
41
|
+
def initialize(port = nil)
|
42
|
+
@port = port
|
43
|
+
start
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns true
|
47
|
+
def ping
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
# Forward API calls to God
|
52
|
+
#
|
53
|
+
# Returns whatever the forwarded call returns
|
54
|
+
def method_missing(*args, &block)
|
55
|
+
God.send(*args, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Stop the DRb server and delete the socket file
|
59
|
+
#
|
60
|
+
# Returns nothing
|
61
|
+
def stop
|
62
|
+
DRb.stop_service
|
63
|
+
FileUtils.rm_f(self.socket_file)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Start the DRb server. Abort if there is already a running god instance
|
69
|
+
# on the socket.
|
70
|
+
#
|
71
|
+
# Returns nothing
|
72
|
+
def start
|
73
|
+
begin
|
74
|
+
@drb ||= DRb.start_service(self.socket, self)
|
75
|
+
applog(nil, :info, "Started on #{DRb.uri}")
|
76
|
+
rescue Errno::EADDRINUSE
|
77
|
+
applog(nil, :info, "Socket already in use")
|
78
|
+
DRb.start_service
|
79
|
+
server = DRbObject.new(nil, self.socket)
|
80
|
+
|
81
|
+
begin
|
82
|
+
Timeout.timeout(5) do
|
83
|
+
server.ping
|
84
|
+
end
|
85
|
+
abort "Socket #{self.socket} already in use by another instance of god"
|
86
|
+
rescue StandardError, Timeout::Error
|
87
|
+
applog(nil, :info, "Socket is stale, reopening")
|
88
|
+
File.delete(self.socket_file) rescue nil
|
89
|
+
@drb ||= DRb.start_service(self.socket, self)
|
90
|
+
applog(nil, :info, "Started on #{DRb.uri}")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|