mojombo-god 0.7.7

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.
Files changed (108) hide show
  1. data/History.txt +255 -0
  2. data/Manifest.txt +107 -0
  3. data/README.txt +59 -0
  4. data/Rakefile +35 -0
  5. data/bin/god +127 -0
  6. data/examples/events.god +84 -0
  7. data/examples/gravatar.god +54 -0
  8. data/examples/single.god +66 -0
  9. data/ext/god/extconf.rb +55 -0
  10. data/ext/god/kqueue_handler.c +123 -0
  11. data/ext/god/netlink_handler.c +167 -0
  12. data/init/god +42 -0
  13. data/lib/god.rb +644 -0
  14. data/lib/god/behavior.rb +52 -0
  15. data/lib/god/behaviors/clean_pid_file.rb +21 -0
  16. data/lib/god/behaviors/clean_unix_socket.rb +21 -0
  17. data/lib/god/behaviors/notify_when_flapping.rb +51 -0
  18. data/lib/god/cli/command.rb +206 -0
  19. data/lib/god/cli/run.rb +177 -0
  20. data/lib/god/cli/version.rb +23 -0
  21. data/lib/god/condition.rb +96 -0
  22. data/lib/god/conditions/always.rb +23 -0
  23. data/lib/god/conditions/complex.rb +86 -0
  24. data/lib/god/conditions/cpu_usage.rb +80 -0
  25. data/lib/god/conditions/degrading_lambda.rb +52 -0
  26. data/lib/god/conditions/disk_usage.rb +27 -0
  27. data/lib/god/conditions/flapping.rb +128 -0
  28. data/lib/god/conditions/http_response_code.rb +168 -0
  29. data/lib/god/conditions/lambda.rb +25 -0
  30. data/lib/god/conditions/memory_usage.rb +82 -0
  31. data/lib/god/conditions/process_exits.rb +72 -0
  32. data/lib/god/conditions/process_running.rb +74 -0
  33. data/lib/god/conditions/tries.rb +44 -0
  34. data/lib/god/configurable.rb +57 -0
  35. data/lib/god/contact.rb +106 -0
  36. data/lib/god/contacts/email.rb +95 -0
  37. data/lib/god/dependency_graph.rb +41 -0
  38. data/lib/god/diagnostics.rb +37 -0
  39. data/lib/god/driver.rb +108 -0
  40. data/lib/god/errors.rb +24 -0
  41. data/lib/god/event_handler.rb +111 -0
  42. data/lib/god/event_handlers/dummy_handler.rb +13 -0
  43. data/lib/god/event_handlers/kqueue_handler.rb +17 -0
  44. data/lib/god/event_handlers/netlink_handler.rb +13 -0
  45. data/lib/god/logger.rb +120 -0
  46. data/lib/god/metric.rb +59 -0
  47. data/lib/god/process.rb +325 -0
  48. data/lib/god/registry.rb +32 -0
  49. data/lib/god/simple_logger.rb +53 -0
  50. data/lib/god/socket.rb +96 -0
  51. data/lib/god/sugar.rb +47 -0
  52. data/lib/god/system/portable_poller.rb +42 -0
  53. data/lib/god/system/process.rb +42 -0
  54. data/lib/god/system/slash_proc_poller.rb +82 -0
  55. data/lib/god/task.rb +487 -0
  56. data/lib/god/timeline.rb +25 -0
  57. data/lib/god/trigger.rb +43 -0
  58. data/lib/god/watch.rb +183 -0
  59. data/test/configs/child_events/child_events.god +44 -0
  60. data/test/configs/child_events/simple_server.rb +3 -0
  61. data/test/configs/child_polls/child_polls.god +37 -0
  62. data/test/configs/child_polls/simple_server.rb +12 -0
  63. data/test/configs/complex/complex.god +59 -0
  64. data/test/configs/complex/simple_server.rb +3 -0
  65. data/test/configs/contact/contact.god +74 -0
  66. data/test/configs/contact/simple_server.rb +3 -0
  67. data/test/configs/daemon_events/daemon_events.god +37 -0
  68. data/test/configs/daemon_events/simple_server.rb +8 -0
  69. data/test/configs/daemon_events/simple_server_stop.rb +11 -0
  70. data/test/configs/daemon_polls/daemon_polls.god +17 -0
  71. data/test/configs/daemon_polls/simple_server.rb +6 -0
  72. data/test/configs/degrading_lambda/degrading_lambda.god +31 -0
  73. data/test/configs/degrading_lambda/tcp_server.rb +15 -0
  74. data/test/configs/matias/matias.god +50 -0
  75. data/test/configs/real.rb +59 -0
  76. data/test/configs/running_load/running_load.god +16 -0
  77. data/test/configs/stress/simple_server.rb +3 -0
  78. data/test/configs/stress/stress.god +15 -0
  79. data/test/configs/task/logs/.placeholder +0 -0
  80. data/test/configs/task/task.god +26 -0
  81. data/test/configs/test.rb +61 -0
  82. data/test/helper.rb +151 -0
  83. data/test/suite.rb +6 -0
  84. data/test/test_behavior.rb +21 -0
  85. data/test/test_condition.rb +50 -0
  86. data/test/test_conditions_disk_usage.rb +56 -0
  87. data/test/test_conditions_http_response_code.rb +109 -0
  88. data/test/test_conditions_process_running.rb +44 -0
  89. data/test/test_conditions_tries.rb +67 -0
  90. data/test/test_contact.rb +109 -0
  91. data/test/test_dependency_graph.rb +62 -0
  92. data/test/test_driver.rb +11 -0
  93. data/test/test_event_handler.rb +80 -0
  94. data/test/test_god.rb +598 -0
  95. data/test/test_handlers_kqueue_handler.rb +16 -0
  96. data/test/test_logger.rb +63 -0
  97. data/test/test_metric.rb +72 -0
  98. data/test/test_process.rb +246 -0
  99. data/test/test_registry.rb +15 -0
  100. data/test/test_socket.rb +42 -0
  101. data/test/test_sugar.rb +42 -0
  102. data/test/test_system_portable_poller.rb +17 -0
  103. data/test/test_system_process.rb +30 -0
  104. data/test/test_task.rb +262 -0
  105. data/test/test_timeline.rb +37 -0
  106. data/test/test_trigger.rb +59 -0
  107. data/test/test_watch.rb +279 -0
  108. metadata +186 -0
data/lib/god/errors.rb ADDED
@@ -0,0 +1,24 @@
1
+ module God
2
+
3
+ class AbstractMethodNotOverriddenError < StandardError
4
+ end
5
+
6
+ class NoSuchWatchError < StandardError
7
+ end
8
+
9
+ class NoSuchConditionError < StandardError
10
+ end
11
+
12
+ class NoSuchBehaviorError < StandardError
13
+ end
14
+
15
+ class NoSuchContactError < StandardError
16
+ end
17
+
18
+ class InvalidCommandError < StandardError
19
+ end
20
+
21
+ class EventRegistrationFailedError < StandardError
22
+ end
23
+
24
+ end
@@ -0,0 +1,111 @@
1
+ module God
2
+ class EventHandler
3
+ @@actions = {}
4
+ @@handler = nil
5
+ @@loaded = false
6
+
7
+ def self.loaded?
8
+ @@loaded
9
+ end
10
+
11
+ def self.event_system
12
+ @@handler::EVENT_SYSTEM
13
+ end
14
+
15
+ def self.load
16
+ begin
17
+ case RUBY_PLATFORM
18
+ when /darwin/i, /bsd/i
19
+ require 'god/event_handlers/kqueue_handler'
20
+ @@handler = KQueueHandler
21
+ when /linux/i
22
+ require 'god/event_handlers/netlink_handler'
23
+ @@handler = NetlinkHandler
24
+ else
25
+ raise NotImplementedError, "Platform not supported for EventHandler"
26
+ end
27
+ @@loaded = true
28
+ rescue Exception
29
+ require 'god/event_handlers/dummy_handler'
30
+ @@handler = DummyHandler
31
+ @@loaded = false
32
+ end
33
+ end
34
+
35
+ def self.register(pid, event, &block)
36
+ @@actions[pid] ||= {}
37
+ @@actions[pid][event] = block
38
+ @@handler.register_process(pid, @@actions[pid].keys)
39
+ end
40
+
41
+ def self.deregister(pid, event=nil)
42
+ if watching_pid? pid
43
+ running = ::Process.kill(0, pid.to_i) rescue false
44
+ if event.nil?
45
+ @@actions.delete(pid)
46
+ @@handler.register_process(pid, []) if running
47
+ else
48
+ @@actions[pid].delete(event)
49
+ @@handler.register_process(pid, @@actions[pid].keys) if running
50
+ end
51
+ end
52
+ end
53
+
54
+ def self.call(pid, event, extra_data = {})
55
+ @@actions[pid][event].call(extra_data) if watching_pid?(pid) && @@actions[pid][event]
56
+ end
57
+
58
+ def self.watching_pid?(pid)
59
+ @@actions[pid]
60
+ end
61
+
62
+ def self.start
63
+ Thread.new do
64
+ loop do
65
+ begin
66
+ @@handler.handle_events
67
+ rescue Exception => e
68
+ message = format("Unhandled exception (%s): %s\n%s",
69
+ e.class, e.message, e.backtrace.join("\n"))
70
+ applog(nil, :fatal, message)
71
+ end
72
+ end
73
+ end
74
+
75
+ # do a real test to make sure events are working properly
76
+ @@loaded = self.operational?
77
+ end
78
+
79
+ def self.operational?
80
+ com = [false]
81
+
82
+ Thread.new do
83
+ begin
84
+ event_system = God::EventHandler.event_system
85
+
86
+ pid = fork do
87
+ loop { sleep(1) }
88
+ end
89
+
90
+ self.register(pid, :proc_exit) do
91
+ com[0] = true
92
+ end
93
+
94
+ ::Process.kill('KILL', pid)
95
+
96
+ sleep(0.1)
97
+
98
+ self.deregister(pid, :proc_exit) rescue nil
99
+ rescue => e
100
+ puts e.message
101
+ puts e.backtrace.join("\n")
102
+ end
103
+ end.join
104
+
105
+ sleep(0.1)
106
+
107
+ com.first
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,13 @@
1
+ module God
2
+ class DummyHandler
3
+ EVENT_SYSTEM = "none"
4
+
5
+ def self.register_process
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def self.handle_events
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ require 'kqueue_handler_ext'
2
+
3
+ module God
4
+ class KQueueHandler
5
+ EVENT_SYSTEM = "kqueue"
6
+
7
+ def self.register_process(pid, events)
8
+ monitor_process(pid, events_mask(events))
9
+ end
10
+
11
+ def self.events_mask(events)
12
+ events.inject(0) do |mask, event|
13
+ mask |= event_mask(event)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -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
@@ -0,0 +1,325 @@
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::Sys.setgid(gid_num) if self.gid
32
+ ::Process::Sys.setuid(uid_num) if self.uid
33
+
34
+ File.writable?(file) ? exit(0) : exit(1)
35
+ end
36
+
37
+ wpid, status = ::Process.waitpid2(pid)
38
+ status.exitstatus == 0 ? true : false
39
+ end
40
+
41
+ def valid?
42
+ # determine if we're tracking pid or not
43
+ self.pid_file
44
+
45
+ valid = true
46
+
47
+ # a start command must be specified
48
+ if self.start.nil?
49
+ valid = false
50
+ applog(self, :error, "No start command was specified")
51
+ end
52
+
53
+ # self-daemonizing processes must specify a stop command
54
+ if !@tracking_pid && self.stop.nil?
55
+ valid = false
56
+ applog(self, :error, "No stop command was specified")
57
+ end
58
+
59
+ # uid must exist if specified
60
+ if self.uid
61
+ begin
62
+ Etc.getpwnam(self.uid)
63
+ rescue ArgumentError
64
+ valid = false
65
+ applog(self, :error, "UID for '#{self.uid}' does not exist")
66
+ end
67
+ end
68
+
69
+ # gid must exist if specified
70
+ if self.gid
71
+ begin
72
+ Etc.getgrnam(self.gid)
73
+ rescue ArgumentError
74
+ valid = false
75
+ applog(self, :error, "GID for '#{self.gid}' does not exist")
76
+ end
77
+ end
78
+
79
+ # pid dir must exist if specified
80
+ if !@tracking_pid && !File.exist?(File.dirname(self.pid_file))
81
+ valid = false
82
+ applog(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' does not exist")
83
+ end
84
+
85
+ # pid dir must be writable if specified
86
+ if !@tracking_pid && File.exist?(File.dirname(self.pid_file)) && !file_writable?(File.dirname(self.pid_file))
87
+ valid = false
88
+ applog(self, :error, "PID file directory '#{File.dirname(self.pid_file)}' is not writable by #{self.uid || Etc.getlogin}")
89
+ end
90
+
91
+ # log dir must exist
92
+ if !File.exist?(File.dirname(self.log))
93
+ valid = false
94
+ applog(self, :error, "Log directory '#{File.dirname(self.log)}' does not exist")
95
+ end
96
+
97
+ # log file or dir must be writable
98
+ if File.exist?(self.log)
99
+ unless file_writable?(self.log)
100
+ valid = false
101
+ applog(self, :error, "Log file '#{self.log}' exists but is not writable by #{self.uid || Etc.getlogin}")
102
+ end
103
+ else
104
+ unless file_writable?(File.dirname(self.log))
105
+ valid = false
106
+ applog(self, :error, "Log directory '#{File.dirname(self.log)}' is not writable by #{self.uid || Etc.getlogin}")
107
+ end
108
+ end
109
+
110
+ # chroot directory must exist and have /dev/null in it
111
+ if self.chroot
112
+ if !File.directory?(self.chroot)
113
+ valid = false
114
+ LOG.log(self, :error, "CHROOT directory '#{self.chroot}' does not exist")
115
+ end
116
+
117
+ if !File.exist?(File.join(self.chroot, '/dev/null'))
118
+ valid = false
119
+ LOG.log(self, :error, "CHROOT directory '#{self.chroot}' does not contain '/dev/null'")
120
+ end
121
+ end
122
+
123
+ valid
124
+ end
125
+
126
+ # DON'T USE THIS INTERNALLY. Use the instance variable. -- Kev
127
+ # No really, trust me. Use the instance variable.
128
+ def pid_file=(value)
129
+ # if value is nil, do the right thing
130
+ if value
131
+ @tracking_pid = false
132
+ else
133
+ @tracking_pid = true
134
+ end
135
+
136
+ @pid_file = value
137
+ end
138
+
139
+ def pid_file
140
+ @pid_file ||= default_pid_file
141
+ end
142
+
143
+ # Fetch the PID from pid_file. If the pid_file does not
144
+ # exist, then use the PID from the last time it was read.
145
+ # If it has never been read, then return nil.
146
+ #
147
+ # Returns Integer(pid) or nil
148
+ def pid
149
+ contents = File.read(self.pid_file).strip rescue ''
150
+ real_pid = contents =~ /^\d+$/ ? contents.to_i : nil
151
+
152
+ if real_pid
153
+ @pid = real_pid
154
+ real_pid
155
+ else
156
+ @pid
157
+ end
158
+ end
159
+
160
+ def start!
161
+ call_action(:start)
162
+ end
163
+
164
+ def stop!
165
+ call_action(:stop)
166
+ end
167
+
168
+ def restart!
169
+ call_action(:restart)
170
+ end
171
+
172
+ def default_pid_file
173
+ File.join(God.pid_file_directory, "#{self.name}.pid")
174
+ end
175
+
176
+ def call_action(action)
177
+ command = send(action)
178
+
179
+ if action == :stop && command.nil?
180
+ pid = self.pid
181
+ name = self.name
182
+ command = lambda do
183
+ applog(self, :info, "#{self.name} stop: default lambda killer")
184
+
185
+ ::Process.kill('TERM', pid) rescue nil
186
+ applog(self, :info, "#{self.name} sent SIGTERM")
187
+
188
+ # Poll to see if it's dead
189
+ 5.times do
190
+ begin
191
+ ::Process.kill(0, pid)
192
+ rescue Errno::ESRCH
193
+ # It died. Good.
194
+ applog(self, :info, "#{self.name} process stopped")
195
+ return
196
+ end
197
+
198
+ sleep 1
199
+ end
200
+
201
+ ::Process.kill('KILL', pid) rescue nil
202
+ applog(self, :info, "#{self.name} still alive; sent SIGKILL")
203
+ end
204
+ end
205
+
206
+ if command.kind_of?(String)
207
+ pid = nil
208
+
209
+ if @tracking_pid
210
+ # double fork god-daemonized processes
211
+ # we don't want to wait for them to finish
212
+ r, w = IO.pipe
213
+ begin
214
+ opid = fork do
215
+ STDOUT.reopen(w)
216
+ r.close
217
+ pid = self.spawn(command)
218
+ puts pid.to_s # send pid back to forker
219
+ end
220
+
221
+ ::Process.waitpid(opid, 0)
222
+ w.close
223
+ pid = r.gets.chomp
224
+ ensure
225
+ # make sure the file descriptors get closed no matter what
226
+ r.close rescue nil
227
+ w.close rescue nil
228
+ end
229
+ else
230
+ # single fork self-daemonizing processes
231
+ # we want to wait for them to finish
232
+ pid = self.spawn(command)
233
+ status = ::Process.waitpid2(pid, 0)
234
+ exit_code = status[1] >> 8
235
+
236
+ if exit_code != 0
237
+ applog(self, :warn, "#{self.name} #{action} command exited with non-zero code = #{exit_code}")
238
+ end
239
+
240
+ ensure_stop if action == :stop
241
+ end
242
+
243
+ if @tracking_pid or (@pid_file.nil? and WRITES_PID.include?(action))
244
+ File.open(default_pid_file, 'w') do |f|
245
+ f.write pid
246
+ end
247
+
248
+ @tracking_pid = true
249
+ @pid_file = default_pid_file
250
+ end
251
+ elsif command.kind_of?(Proc)
252
+ # lambda command
253
+ command.call
254
+ else
255
+ raise NotImplementedError
256
+ end
257
+ end
258
+
259
+ # Fork/exec the given command, returns immediately
260
+ # +command+ is the String containing the shell command
261
+ #
262
+ # Returns nothing
263
+ def spawn(command)
264
+ fork do
265
+ uid_num = Etc.getpwnam(self.uid).uid if self.uid
266
+ gid_num = Etc.getgrnam(self.gid).gid if self.gid
267
+
268
+ ::Dir.chroot(self.chroot) if self.chroot
269
+ ::Process.setsid
270
+ ::Process::Sys.setgid(gid_num) if self.gid
271
+ ::Process::Sys.setuid(uid_num) if self.uid
272
+ Dir.chdir "/"
273
+ $0 = command
274
+ STDIN.reopen "/dev/null"
275
+ STDOUT.reopen file_in_chroot(self.log), "a"
276
+ STDERR.reopen STDOUT
277
+
278
+ # close any other file descriptors
279
+ 3.upto(256){|fd| IO::new(fd).close rescue nil}
280
+
281
+ if self.env && self.env.is_a?(Hash)
282
+ self.env.each do |(key, value)|
283
+ ENV[key] = value
284
+ end
285
+ end
286
+
287
+ exec command unless command.empty?
288
+ end
289
+ end
290
+
291
+ # Ensure that a stop command actually stops the process. Force kill
292
+ # if necessary.
293
+ #
294
+ # Returns nothing
295
+ def ensure_stop
296
+ unless self.pid
297
+ applog(self, :warn, "#{self.name} stop called but pid is uknown")
298
+ return
299
+ end
300
+
301
+ # Poll to see if it's dead
302
+ 10.times do
303
+ begin
304
+ ::Process.kill(0, self.pid)
305
+ rescue Errno::ESRCH
306
+ # It died. Good.
307
+ return
308
+ end
309
+
310
+ sleep 1
311
+ end
312
+
313
+ # last resort
314
+ ::Process.kill('KILL', self.pid) rescue nil
315
+ applog(self, :warn, "#{self.name} process still running 10 seconds after stop command returned. Force killing.")
316
+ end
317
+
318
+ private
319
+ def file_in_chroot(file)
320
+ return file unless self.chroot
321
+
322
+ file.gsub(/^#{Regexp.escape(File.expand_path(self.chroot))}/, '')
323
+ end
324
+ end
325
+ end