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