bluepill 0.1.3 → 0.1.4
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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +820 -0
- data/CONTRIBUTING.md +31 -0
- data/LICENSE +1 -1
- data/README.md +64 -29
- data/bin/bluepill +1 -1
- data/bluepill.gemspec +17 -6
- data/lib/bluepill/application.rb +309 -41
- data/lib/bluepill/condition_watch.rb +103 -8
- data/lib/bluepill/controller.rb +124 -13
- data/lib/bluepill/dsl/app_proxy.rb +102 -3
- data/lib/bluepill/dsl/process_factory.rb +183 -60
- data/lib/bluepill/dsl/process_proxy.rb +132 -14
- data/lib/bluepill/dsl.rb +27 -5
- data/lib/bluepill/group.rb +142 -9
- data/lib/bluepill/logger.rb +120 -5
- data/lib/bluepill/process.rb +571 -200
- data/lib/bluepill/process_commands.rb +262 -0
- data/lib/bluepill/process_conditions/always_true.rb +18 -4
- data/lib/bluepill/process_conditions/cpu_usage.rb +34 -0
- data/lib/bluepill/process_conditions/file_time.rb +40 -4
- data/lib/bluepill/process_conditions/http.rb +81 -23
- data/lib/bluepill/process_conditions/mem_usage.rb +61 -2
- data/lib/bluepill/process_conditions/process_condition.rb +43 -0
- data/lib/bluepill/process_conditions/running_time.rb +36 -1
- data/lib/bluepill/process_conditions/zombie_process.rb +26 -5
- data/lib/bluepill/process_conditions.rb +15 -1
- data/lib/bluepill/process_journal.rb +256 -104
- data/lib/bluepill/process_statistics.rb +33 -3
- data/lib/bluepill/socket.rb +63 -16
- data/lib/bluepill/system/commands.rb +286 -0
- data/lib/bluepill/system.rb +192 -228
- data/lib/bluepill/trigger.rb +110 -14
- data/lib/bluepill/triggers/flapping.rb +84 -3
- data/lib/bluepill/util/rotational_array.rb +24 -1
- data/lib/bluepill/version.rb +71 -19
- data/lib/bluepill.rb +25 -2
- data/sig/bluepill.rbs +926 -0
- data/sig/manifest.yaml +7 -0
- metadata +16 -34
- data/bin/sample_forking_server +0 -66
- data/lib/bluepill/application/client.rb +0 -6
- data/lib/bluepill/application/server.rb +0 -21
data/lib/bluepill/system.rb
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
require 'etc'
|
|
2
|
-
require 'shellwords'
|
|
3
|
-
|
|
4
1
|
module Bluepill
|
|
5
|
-
#
|
|
6
|
-
#
|
|
2
|
+
# Read what the system says about its processes, and run commands
|
|
3
|
+
#
|
|
4
|
+
# This represents the system that bluepill is running on. It memoizes
|
|
5
|
+
# the results of running ps each tick, so that every watch of every
|
|
6
|
+
# process doesn't fork a ps of its own.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
7
9
|
module System
|
|
10
|
+
# The mode that a daemon's stdout and stderr files are opened in
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
8
13
|
APPEND_MODE = 'a'.freeze
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
# Raised when ps could not be run, so there is no data for this tick
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
class PsError < StandardError; end
|
|
11
19
|
|
|
12
20
|
# The position of each field in ps output
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
13
23
|
IDX_MAP = {
|
|
14
24
|
pid: 0,
|
|
15
25
|
ppid: 1,
|
|
@@ -17,108 +27,142 @@ module Bluepill
|
|
|
17
27
|
rss: 3,
|
|
18
28
|
etime: 4,
|
|
19
29
|
command: 5,
|
|
20
|
-
}.freeze
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
}.freeze #: { pid: Integer, ppid: Integer, pcpu: Integer, rss: Integer, etime: Integer, command: Integer }
|
|
31
|
+
|
|
32
|
+
# Return whether a process is running
|
|
33
|
+
#
|
|
34
|
+
# @param [Integer] pid the process's pid
|
|
35
|
+
#
|
|
36
|
+
# @return [Boolean] true if the process exists, even if bluepill may
|
|
37
|
+
# not signal it
|
|
38
|
+
#
|
|
39
|
+
# @api private
|
|
40
|
+
def self.pid_alive?(pid)
|
|
23
41
|
::Process.kill(0, pid)
|
|
24
42
|
true
|
|
25
43
|
rescue Errno::EPERM # no permission, but it is definitely alive
|
|
26
44
|
true
|
|
27
|
-
rescue
|
|
28
|
-
false
|
|
29
|
-
rescue
|
|
30
|
-
false # other falsy (no pid)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
false # no such process (ESRCH), or no pid
|
|
31
47
|
end
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
# Return a process's CPU usage, from this tick's ps table
|
|
50
|
+
#
|
|
51
|
+
# @param [Integer, nil] pid the process's pid
|
|
52
|
+
# @param [Boolean] include_children whether to add its descendants'
|
|
53
|
+
#
|
|
54
|
+
# @return [Float, nil] the percent of a CPU it uses, or nil if ps doesn't
|
|
55
|
+
# list it
|
|
56
|
+
#
|
|
57
|
+
# @raise [PsError] if ps could not be run this tick
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
def self.cpu_usage(pid, include_children)
|
|
34
61
|
ps = ps_axu
|
|
35
|
-
return unless ps
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
return unless ps.key?(pid)
|
|
63
|
+
|
|
64
|
+
cpu_used = ps.fetch(pid).fetch(IDX_MAP.fetch(:pcpu)).to_f
|
|
65
|
+
if include_children
|
|
66
|
+
get_children(pid).each do |child_pid|
|
|
67
|
+
cpu_used += ps.fetch(child_pid).fetch(IDX_MAP.fetch(:pcpu)).to_f if ps.key?(child_pid)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
40
70
|
cpu_used
|
|
41
71
|
end
|
|
42
72
|
|
|
43
|
-
|
|
73
|
+
# Return a process's resident memory, from this tick's ps table
|
|
74
|
+
#
|
|
75
|
+
# @param [Integer, nil] pid the process's pid
|
|
76
|
+
# @param [Boolean] include_children whether to add its descendants'
|
|
77
|
+
#
|
|
78
|
+
# @return [Float, nil] its resident memory in kilobytes, or nil if ps
|
|
79
|
+
# doesn't list it
|
|
80
|
+
#
|
|
81
|
+
# @raise [PsError] if ps could not be run this tick
|
|
82
|
+
#
|
|
83
|
+
# @api private
|
|
84
|
+
def self.memory_usage(pid, include_children)
|
|
44
85
|
ps = ps_axu
|
|
45
|
-
return unless ps
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
86
|
+
return unless ps.key?(pid)
|
|
87
|
+
|
|
88
|
+
mem_used = ps.fetch(pid).fetch(IDX_MAP.fetch(:rss)).to_f
|
|
89
|
+
if include_children
|
|
90
|
+
get_children(pid).each do |child_pid|
|
|
91
|
+
mem_used += ps.fetch(child_pid).fetch(IDX_MAP.fetch(:rss)).to_f if ps.key?(child_pid)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
50
94
|
mem_used
|
|
51
95
|
end
|
|
52
96
|
|
|
53
|
-
|
|
97
|
+
# Return how long a process has been running, from this tick's ps table
|
|
98
|
+
#
|
|
99
|
+
# @param [Integer, nil] pid the process's pid
|
|
100
|
+
#
|
|
101
|
+
# @return [Integer, nil] its running time in seconds, or nil if ps
|
|
102
|
+
# doesn't list it
|
|
103
|
+
#
|
|
104
|
+
# @raise [PsError] if ps could not be run this tick
|
|
105
|
+
#
|
|
106
|
+
# @api private
|
|
107
|
+
def self.running_time(pid)
|
|
54
108
|
ps = ps_axu
|
|
55
|
-
return unless ps
|
|
56
|
-
|
|
109
|
+
return unless ps.key?(pid)
|
|
110
|
+
|
|
111
|
+
parse_elapsed_time(ps.fetch(pid).fetch(IDX_MAP.fetch(:etime)))
|
|
57
112
|
end
|
|
58
113
|
|
|
59
|
-
|
|
114
|
+
# Return a process's command line, from this tick's ps table
|
|
115
|
+
#
|
|
116
|
+
# @param [Integer, nil] pid the process's pid
|
|
117
|
+
#
|
|
118
|
+
# @return [String, nil] its command and arguments, or nil if ps doesn't
|
|
119
|
+
# list it
|
|
120
|
+
#
|
|
121
|
+
# @raise [PsError] if ps could not be run this tick
|
|
122
|
+
#
|
|
123
|
+
# @api private
|
|
124
|
+
def self.command(pid)
|
|
60
125
|
ps = ps_axu
|
|
61
|
-
return unless ps
|
|
62
|
-
|
|
126
|
+
return unless ps.key?(pid)
|
|
127
|
+
|
|
128
|
+
ps.fetch(pid).fetch(IDX_MAP.fetch(:command))
|
|
63
129
|
end
|
|
64
130
|
|
|
65
|
-
|
|
66
|
-
|
|
131
|
+
# Return the pids of a process's descendants, from this tick's ps table
|
|
132
|
+
#
|
|
133
|
+
# @param [Integer, nil] parent_pid the process's pid
|
|
134
|
+
#
|
|
135
|
+
# @return [Array<Integer>] its children's pids, followed by theirs, or
|
|
136
|
+
# none if ps could not be run
|
|
137
|
+
#
|
|
138
|
+
# @api private
|
|
139
|
+
def self.get_children(parent_pid)
|
|
140
|
+
# A process that is still starting has no pid yet, and nil.to_i is 0.
|
|
141
|
+
# The ps header row parses as pid 0 with ppid 0, so it would be its own
|
|
142
|
+
# child and the recursion would never end.
|
|
143
|
+
return [] unless parent_pid.to_i > 0
|
|
144
|
+
|
|
145
|
+
child_pids = [] #: Array[Integer]
|
|
67
146
|
ps_axu.each_pair do |_pid, chunks|
|
|
68
|
-
child_pids << chunks
|
|
147
|
+
child_pids << Integer(chunks.fetch(IDX_MAP.fetch(:pid))) if chunks.fetch(IDX_MAP.fetch(:ppid)).to_i.eql?(parent_pid)
|
|
69
148
|
end
|
|
70
149
|
grand_children = child_pids.collect { |pid| get_children(pid) }.flatten
|
|
71
150
|
child_pids.concat grand_children
|
|
151
|
+
rescue PsError
|
|
152
|
+
[]
|
|
72
153
|
end
|
|
73
154
|
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
daemon_id = rd.read.to_i
|
|
87
|
-
rd.close
|
|
88
|
-
|
|
89
|
-
return daemon_id if daemon_id > 0
|
|
90
|
-
else
|
|
91
|
-
# child
|
|
92
|
-
rd.close
|
|
93
|
-
|
|
94
|
-
drop_privileges(options[:uid], options[:gid], options[:supplementary_groups])
|
|
95
|
-
|
|
96
|
-
# if we cannot write the pid file as the provided user, err out
|
|
97
|
-
exit unless can_write_pid_file(options[:pid_file], options[:logger])
|
|
98
|
-
|
|
99
|
-
to_daemonize = lambda do
|
|
100
|
-
# Setting end PWD env emulates bash behavior when dealing with symlinks
|
|
101
|
-
Dir.chdir(ENV['PWD'] = options[:working_dir].to_s) if options[:working_dir]
|
|
102
|
-
options[:environment].each { |key, value| ENV[key.to_s] = value.to_s } if options[:environment]
|
|
103
|
-
|
|
104
|
-
redirect_io(*options.values_at(:stdin, :stdout, :stderr))
|
|
105
|
-
|
|
106
|
-
::Kernel.exec(*Shellwords.shellwords(cmd))
|
|
107
|
-
exit
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
daemon_id = Daemonize.call_as_daemon(to_daemonize, nil, cmd)
|
|
111
|
-
|
|
112
|
-
File.open(options[:pid_file], 'w') { |f| f.write(daemon_id) }
|
|
113
|
-
|
|
114
|
-
wr.write daemon_id
|
|
115
|
-
wr.close
|
|
116
|
-
|
|
117
|
-
::Process.exit!(true)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def delete_if_exists(filename)
|
|
155
|
+
# Delete a file if it exists
|
|
156
|
+
#
|
|
157
|
+
# It tries three times when permission is denied, and then prints a
|
|
158
|
+
# warning instead of raising.
|
|
159
|
+
#
|
|
160
|
+
# @param [String, nil] filename the file's path
|
|
161
|
+
#
|
|
162
|
+
# @return [void]
|
|
163
|
+
#
|
|
164
|
+
# @api private
|
|
165
|
+
def self.delete_if_exists(filename)
|
|
122
166
|
tries = 0
|
|
123
167
|
|
|
124
168
|
begin
|
|
@@ -130,166 +174,86 @@ module Bluepill
|
|
|
130
174
|
end
|
|
131
175
|
end
|
|
132
176
|
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
wr.close
|
|
141
|
-
|
|
142
|
-
cmd_status = rd.read
|
|
143
|
-
rd.close
|
|
144
|
-
|
|
145
|
-
::Process.waitpid(child)
|
|
146
|
-
|
|
147
|
-
cmd_status.strip != '' ? Marshal.load(cmd_status) : {exit_code: 0, stdout: '', stderr: ''}
|
|
148
|
-
else
|
|
149
|
-
# child
|
|
150
|
-
rd.close
|
|
151
|
-
|
|
152
|
-
# create a child in which we can override the stdin, stdout and stderr
|
|
153
|
-
cmd_out_read, cmd_out_write = IO.pipe
|
|
154
|
-
cmd_err_read, cmd_err_write = IO.pipe
|
|
155
|
-
|
|
156
|
-
pid = fork do
|
|
157
|
-
begin
|
|
158
|
-
# grandchild
|
|
159
|
-
drop_privileges(options[:uid], options[:gid], options[:supplementary_groups])
|
|
160
|
-
|
|
161
|
-
Dir.chdir(ENV['PWD'] = options[:working_dir].to_s) if options[:working_dir]
|
|
162
|
-
options[:environment].each { |key, value| ENV[key.to_s] = value.to_s } if options[:environment]
|
|
163
|
-
|
|
164
|
-
# close unused fds so ancestors wont hang. This line is the only reason we are not
|
|
165
|
-
# using something like popen3. If this fd is not closed, the .read call on the parent
|
|
166
|
-
# will never return because "wr" would still be open in the "exec"-ed cmd
|
|
167
|
-
wr.close
|
|
168
|
-
|
|
169
|
-
# we do not care about stdin of cmd
|
|
170
|
-
STDIN.reopen('/dev/null')
|
|
171
|
-
|
|
172
|
-
# point stdout of cmd to somewhere we can read
|
|
173
|
-
cmd_out_read.close
|
|
174
|
-
STDOUT.reopen(cmd_out_write)
|
|
175
|
-
cmd_out_write.close
|
|
176
|
-
|
|
177
|
-
# same thing for stderr
|
|
178
|
-
cmd_err_read.close
|
|
179
|
-
STDERR.reopen(cmd_err_write)
|
|
180
|
-
cmd_err_write.close
|
|
181
|
-
|
|
182
|
-
# finally, replace grandchild with cmd
|
|
183
|
-
::Kernel.exec(*Shellwords.shellwords(cmd))
|
|
184
|
-
rescue => e
|
|
185
|
-
(cmd_err_write.closed? ? STDERR : cmd_err_write).puts "Exception in grandchild: #{e}."
|
|
186
|
-
(cmd_err_write.closed? ? STDERR : cmd_err_write).puts e.backtrace
|
|
187
|
-
exit 1
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# we do not use these ends of the pipes in the child
|
|
192
|
-
cmd_out_write.close
|
|
193
|
-
cmd_err_write.close
|
|
194
|
-
|
|
195
|
-
# wait for the cmd to finish executing and acknowledge it's death
|
|
196
|
-
::Process.waitpid(pid)
|
|
197
|
-
|
|
198
|
-
# collect stdout, stderr and exitcode
|
|
199
|
-
result = {
|
|
200
|
-
stdout: cmd_out_read.read,
|
|
201
|
-
stderr: cmd_err_read.read,
|
|
202
|
-
exit_code: $?.exitstatus,
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
# We're done with these ends of the pipes as well
|
|
206
|
-
cmd_out_read.close
|
|
207
|
-
cmd_err_read.close
|
|
208
|
-
|
|
209
|
-
# Time to tell the parent about what went down
|
|
210
|
-
wr.write Marshal.dump(result)
|
|
211
|
-
wr.close
|
|
212
|
-
|
|
213
|
-
::Process.exit!
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def store
|
|
177
|
+
# Return what this tick has memoized
|
|
178
|
+
#
|
|
179
|
+
# @return [Hash{Symbol => Hash{Integer => Array<String>}, String}] the
|
|
180
|
+
# ps table, or ps's error, by name
|
|
181
|
+
#
|
|
182
|
+
# @api private
|
|
183
|
+
def self.store
|
|
218
184
|
@store ||= {}
|
|
219
185
|
end
|
|
220
186
|
|
|
221
|
-
|
|
222
|
-
|
|
187
|
+
# Forget what the last tick memoized, so ps runs again
|
|
188
|
+
#
|
|
189
|
+
# @return [void]
|
|
190
|
+
#
|
|
191
|
+
# @api private
|
|
192
|
+
def self.reset_data
|
|
193
|
+
store.clear
|
|
223
194
|
end
|
|
224
195
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
196
|
+
# Watches run in their own threads, so only let one of them run ps per tick
|
|
197
|
+
#
|
|
198
|
+
# @api private
|
|
199
|
+
PS_MUTEX = Mutex.new
|
|
200
|
+
|
|
201
|
+
# Return this tick's ps table, running ps the first time it's asked for
|
|
202
|
+
#
|
|
203
|
+
# @return [Hash{Integer => Array<String>}] each process's ps fields, by
|
|
204
|
+
# pid, in the order of IDX_MAP
|
|
205
|
+
#
|
|
206
|
+
# @raise [PsError] if ps could not be run this tick
|
|
207
|
+
#
|
|
208
|
+
# @api private
|
|
209
|
+
def self.ps_axu
|
|
210
|
+
ps = PS_MUTEX.synchronize do
|
|
211
|
+
store[:ps_axu] ||= begin
|
|
212
|
+
# POSIX options, so this also works with System V ps on Solaris and SmartOS.
|
|
213
|
+
# Use -A because -e shows the environment on FreeBSD and OpenBSD.
|
|
214
|
+
lines = `ps -Ao pid,ppid,pcpu,rss,etime,args`.split("\n")
|
|
215
|
+
|
|
216
|
+
table = {} #: ps_table
|
|
217
|
+
lines.each do |line|
|
|
218
|
+
chunks = line.split
|
|
219
|
+
pid = chunks.fetch(IDX_MAP.fetch(:pid)).to_i
|
|
220
|
+
command = chunks.slice!(IDX_MAP.fetch(:command), chunks.length) #: Array[String]
|
|
221
|
+
chunks[IDX_MAP.fetch(:command)] = command.join ' '
|
|
222
|
+
table[pid] = chunks
|
|
223
|
+
end
|
|
224
|
+
table
|
|
225
|
+
rescue SystemCallError => e
|
|
226
|
+
# Forking ps can fail, for example with ENOMEM or EAGAIN under memory pressure.
|
|
227
|
+
# Cache the message so ps is not run again until the next tick.
|
|
228
|
+
"Could not run ps: #{e.class}: #{e}"
|
|
238
229
|
end
|
|
239
230
|
end
|
|
231
|
+
raise PsError, ps if ps.instance_of?(String)
|
|
232
|
+
|
|
233
|
+
ps
|
|
240
234
|
end
|
|
241
235
|
|
|
242
|
-
|
|
236
|
+
# Convert ps's elapsed time to seconds
|
|
237
|
+
#
|
|
238
|
+
# @param [String, nil] str an elapsed time, [[dd-]hh:]mm:ss
|
|
239
|
+
#
|
|
240
|
+
# @return [Integer] the seconds, or 0 if str isn't an elapsed time
|
|
241
|
+
#
|
|
242
|
+
# @api private
|
|
243
|
+
def self.parse_elapsed_time(str)
|
|
243
244
|
# [[dd-]hh:]mm:ss
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
match = /(?:(?:(\d+)-)?(\d\d):)?(\d\d):(\d\d)/.match(str)
|
|
246
|
+
if match
|
|
247
|
+
days = match[1].to_i
|
|
248
|
+
hours = match[2].to_i
|
|
249
|
+
mins = match[3].to_i
|
|
250
|
+
secs = match[4].to_i
|
|
249
251
|
((days * 24 + hours) * 60 + mins) * 60 + secs
|
|
250
252
|
else
|
|
251
253
|
0
|
|
252
254
|
end
|
|
253
255
|
end
|
|
254
|
-
|
|
255
|
-
# be sure to call this from a fork otherwise it will modify the attributes
|
|
256
|
-
# of the bluepill daemon
|
|
257
|
-
def drop_privileges(uid, gid, supplementary_groups)
|
|
258
|
-
return unless ::Process::Sys.geteuid.zero?
|
|
259
|
-
uid_num = Etc.getpwnam(uid).uid if uid
|
|
260
|
-
gid_num = Etc.getgrnam(gid).gid if gid
|
|
261
|
-
supplementary_groups ||= []
|
|
262
|
-
group_nums = supplementary_groups.collect do |group|
|
|
263
|
-
Etc.getgrnam(group).gid
|
|
264
|
-
end
|
|
265
|
-
::Process.groups = [gid_num] if gid
|
|
266
|
-
::Process.groups |= group_nums unless group_nums.empty?
|
|
267
|
-
::Process::Sys.setgid(gid_num) if gid
|
|
268
|
-
::Process::Sys.setuid(uid_num) if uid
|
|
269
|
-
ENV['HOME'] = Etc.getpwuid(uid_num).try(:dir) || ENV['HOME'] if uid
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def can_write_pid_file(pid_file, logger)
|
|
273
|
-
FileUtils.touch(pid_file)
|
|
274
|
-
File.unlink(pid_file)
|
|
275
|
-
true
|
|
276
|
-
rescue => e
|
|
277
|
-
logger.warning(format('%s - %s', e.class.name, e.message))
|
|
278
|
-
e.backtrace.each { |l| logger.warning l }
|
|
279
|
-
false
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def redirect_io(io_in, io_out, io_err)
|
|
283
|
-
$stdin.reopen(io_in) if io_in
|
|
284
|
-
|
|
285
|
-
if !io_out.nil? && !io_err.nil? && io_out == io_err
|
|
286
|
-
$stdout.reopen(io_out, APPEND_MODE)
|
|
287
|
-
$stderr.reopen($stdout)
|
|
288
|
-
|
|
289
|
-
else
|
|
290
|
-
$stdout.reopen(io_out, APPEND_MODE) if io_out
|
|
291
|
-
$stderr.reopen(io_err, APPEND_MODE) if io_err
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
256
|
end
|
|
295
257
|
end
|
|
258
|
+
|
|
259
|
+
require 'bluepill/system/commands'
|
data/lib/bluepill/trigger.rb
CHANGED
|
@@ -1,16 +1,76 @@
|
|
|
1
1
|
module Bluepill
|
|
2
|
+
# Reacts to a process's state transitions, as one of a process's checks
|
|
3
|
+
#
|
|
4
|
+
# A config names a trigger the way it names a check, by its underscored
|
|
5
|
+
# class name: `checks :flapping` makes a Triggers::Flapping. Subclasses
|
|
6
|
+
# register themselves by that name, and implement notify.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
2
9
|
class Trigger
|
|
3
10
|
@implementations = {}
|
|
11
|
+
|
|
12
|
+
# Register a subclass by its underscored name, such as :flapping
|
|
13
|
+
#
|
|
14
|
+
# @param [Class] klass the subclass
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
4
19
|
def self.inherited(klass)
|
|
20
|
+
super
|
|
5
21
|
@implementations[klass.name.split('::').last.underscore.to_sym] = klass
|
|
6
22
|
end
|
|
7
23
|
|
|
24
|
+
# Return the trigger class a config names
|
|
25
|
+
#
|
|
26
|
+
# @param [Symbol] name the trigger's underscored name, such as :flapping
|
|
27
|
+
#
|
|
28
|
+
# @return [Class, nil] the Trigger subclass, or nil when no trigger has
|
|
29
|
+
# that name (so the name is a check's)
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
8
32
|
def self.[](name)
|
|
9
33
|
@implementations[name]
|
|
10
34
|
end
|
|
11
35
|
|
|
12
|
-
|
|
36
|
+
# Return the process the trigger watches
|
|
37
|
+
#
|
|
38
|
+
# @return [Process] the process
|
|
39
|
+
#
|
|
40
|
+
# @api private
|
|
41
|
+
attr_accessor :process
|
|
42
|
+
|
|
43
|
+
# Return the logger the trigger writes to
|
|
44
|
+
#
|
|
45
|
+
# @return [Logger] the process's logger
|
|
46
|
+
#
|
|
47
|
+
# @api private
|
|
48
|
+
attr_accessor :logger
|
|
49
|
+
|
|
50
|
+
# Return the lock that guards the scheduled events
|
|
51
|
+
#
|
|
52
|
+
# @return [Thread::Mutex] the lock
|
|
53
|
+
#
|
|
54
|
+
# @api private
|
|
55
|
+
attr_accessor :mutex
|
|
13
56
|
|
|
57
|
+
# Return the events the trigger scheduled, each with its thread
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<Array(Symbol, Thread)>] each event and the thread that
|
|
60
|
+
# dispatches it
|
|
61
|
+
#
|
|
62
|
+
# @api private
|
|
63
|
+
attr_accessor :scheduled_events
|
|
64
|
+
|
|
65
|
+
# Create a trigger for a process
|
|
66
|
+
#
|
|
67
|
+
# @param [Process] process the process to watch
|
|
68
|
+
# @param [Hash{Symbol => Object}] options the trigger's options, with
|
|
69
|
+
# :logger
|
|
70
|
+
#
|
|
71
|
+
# @return [void]
|
|
72
|
+
#
|
|
73
|
+
# @api private
|
|
14
74
|
def initialize(process, options = {})
|
|
15
75
|
self.process = process
|
|
16
76
|
self.logger = options[:logger]
|
|
@@ -18,40 +78,76 @@ module Bluepill
|
|
|
18
78
|
self.scheduled_events = []
|
|
19
79
|
end
|
|
20
80
|
|
|
81
|
+
# Cancel the events the trigger scheduled, as when the user unmonitors
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
#
|
|
85
|
+
# @api private
|
|
21
86
|
def reset!
|
|
22
87
|
cancel_all_events
|
|
23
88
|
end
|
|
24
89
|
|
|
90
|
+
# React to a state transition, before it happens
|
|
91
|
+
#
|
|
92
|
+
# @param [StateMachine::Transition] _transition the transition
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
95
|
+
#
|
|
96
|
+
# @raise [RuntimeError] unless a subclass implements it
|
|
97
|
+
#
|
|
98
|
+
# @api private
|
|
25
99
|
def notify(_transition)
|
|
26
100
|
raise 'Implement in subclass'
|
|
27
101
|
end
|
|
28
102
|
|
|
103
|
+
# Send an event to the process, with the trigger's name as the reason
|
|
104
|
+
#
|
|
105
|
+
# @param [Symbol] event the event, such as :start
|
|
106
|
+
#
|
|
107
|
+
# @return [Boolean] whether the process made the transition
|
|
108
|
+
#
|
|
109
|
+
# @api private
|
|
29
110
|
def dispatch!(event)
|
|
30
111
|
process.dispatch!(event, self.class.name.split('::').last)
|
|
31
112
|
end
|
|
32
113
|
|
|
114
|
+
# Dispatch an event after a delay, in a thread of its own
|
|
115
|
+
#
|
|
116
|
+
# @param [Symbol] event the event, such as :start
|
|
117
|
+
# @param [Numeric] delay the seconds to wait first
|
|
118
|
+
#
|
|
119
|
+
# @return [Thread] the thread that waits and dispatches
|
|
120
|
+
#
|
|
121
|
+
# @api private
|
|
33
122
|
def schedule_event(event, delay)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
123
|
+
mutex.synchronize do
|
|
124
|
+
thread = Thread.new do
|
|
125
|
+
begin
|
|
126
|
+
sleep delay.to_f
|
|
127
|
+
dispatch!(event)
|
|
128
|
+
mutex.synchronize do
|
|
129
|
+
scheduled_events.delete_if { |_, t| t.equal?(Thread.current) }
|
|
130
|
+
end
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
logger.err(e)
|
|
133
|
+
logger.err(e.backtrace.to_a.join("\n"))
|
|
41
134
|
end
|
|
42
|
-
rescue StandardError => e
|
|
43
|
-
trigger.logger.err(e)
|
|
44
|
-
trigger.logger.err(e.backtrace.join("\n"))
|
|
45
135
|
end
|
|
136
|
+
scheduled_events.push([event, thread])
|
|
137
|
+
thread
|
|
46
138
|
end
|
|
47
|
-
|
|
48
|
-
scheduled_events.push([event, thread])
|
|
49
139
|
end
|
|
50
140
|
|
|
141
|
+
# Cancel every scheduled event, killing its thread
|
|
142
|
+
#
|
|
143
|
+
# @return [void]
|
|
144
|
+
#
|
|
145
|
+
# @api private
|
|
51
146
|
def cancel_all_events
|
|
52
147
|
logger.info 'Canceling all scheduled events'
|
|
53
148
|
mutex.synchronize do
|
|
54
|
-
scheduled_events.each
|
|
149
|
+
scheduled_events.collect(&:last).each(&:kill)
|
|
150
|
+
scheduled_events.clear
|
|
55
151
|
end
|
|
56
152
|
end
|
|
57
153
|
end
|