foreman-systemd 0.78.0
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 +7 -0
- data/README.md +64 -0
- data/bin/foreman +7 -0
- data/bin/foreman-runner +41 -0
- data/data/example/Procfile +4 -0
- data/data/example/Procfile.without_colon +2 -0
- data/data/example/error +7 -0
- data/data/example/log/neverdie.log +4 -0
- data/data/example/spawnee +14 -0
- data/data/example/spawner +7 -0
- data/data/example/ticker +14 -0
- data/data/example/utf8 +11 -0
- data/data/export/bluepill/master.pill.erb +28 -0
- data/data/export/daemon/master.conf.erb +14 -0
- data/data/export/daemon/process.conf.erb +8 -0
- data/data/export/daemon/process_master.conf.erb +2 -0
- data/data/export/launchd/launchd.plist.erb +33 -0
- data/data/export/runit/log/run.erb +7 -0
- data/data/export/runit/run.erb +4 -0
- data/data/export/supervisord/app.conf.erb +28 -0
- data/data/export/systemd/master.target.erb +6 -0
- data/data/export/systemd/process.service.erb +16 -0
- data/data/export/systemd/process_master.target.erb +4 -0
- data/data/export/upstart/master.conf.erb +2 -0
- data/data/export/upstart/process.conf.erb +14 -0
- data/data/export/upstart/process_master.conf.erb +2 -0
- data/lib/foreman.rb +17 -0
- data/lib/foreman/cli.rb +161 -0
- data/lib/foreman/distribution.rb +9 -0
- data/lib/foreman/engine.rb +441 -0
- data/lib/foreman/engine/cli.rb +104 -0
- data/lib/foreman/env.rb +29 -0
- data/lib/foreman/export.rb +36 -0
- data/lib/foreman/export/base.rb +156 -0
- data/lib/foreman/export/bluepill.rb +12 -0
- data/lib/foreman/export/daemon.rb +28 -0
- data/lib/foreman/export/inittab.rb +42 -0
- data/lib/foreman/export/launchd.rb +22 -0
- data/lib/foreman/export/runit.rb +34 -0
- data/lib/foreman/export/supervisord.rb +16 -0
- data/lib/foreman/export/systemd.rb +32 -0
- data/lib/foreman/export/upstart.rb +43 -0
- data/lib/foreman/helpers.rb +45 -0
- data/lib/foreman/process.rb +80 -0
- data/lib/foreman/procfile.rb +92 -0
- data/lib/foreman/version.rb +5 -0
- data/man/foreman.1 +278 -0
- data/spec/foreman/cli_spec.rb +107 -0
- data/spec/foreman/engine_spec.rb +112 -0
- data/spec/foreman/export/base_spec.rb +19 -0
- data/spec/foreman/export/bluepill_spec.rb +37 -0
- data/spec/foreman/export/daemon_spec.rb +97 -0
- data/spec/foreman/export/inittab_spec.rb +40 -0
- data/spec/foreman/export/launchd_spec.rb +31 -0
- data/spec/foreman/export/runit_spec.rb +36 -0
- data/spec/foreman/export/supervisord_spec.rb +36 -0
- data/spec/foreman/export/systemd_spec.rb +91 -0
- data/spec/foreman/export/upstart_spec.rb +118 -0
- data/spec/foreman/export_spec.rb +24 -0
- data/spec/foreman/helpers_spec.rb +26 -0
- data/spec/foreman/process_spec.rb +71 -0
- data/spec/foreman/procfile_spec.rb +43 -0
- data/spec/foreman_spec.rb +16 -0
- data/spec/helper_spec.rb +19 -0
- data/spec/resources/Procfile +5 -0
- data/spec/resources/bin/echo +2 -0
- data/spec/resources/bin/env +2 -0
- data/spec/resources/bin/test +2 -0
- data/spec/resources/bin/utf8 +2 -0
- data/spec/resources/export/bluepill/app-concurrency.pill +49 -0
- data/spec/resources/export/bluepill/app.pill +81 -0
- data/spec/resources/export/daemon/app-alpha-1.conf +7 -0
- data/spec/resources/export/daemon/app-alpha-2.conf +7 -0
- data/spec/resources/export/daemon/app-alpha.conf +2 -0
- data/spec/resources/export/daemon/app-bravo-1.conf +7 -0
- data/spec/resources/export/daemon/app-bravo.conf +2 -0
- data/spec/resources/export/daemon/app.conf +14 -0
- data/spec/resources/export/inittab/inittab.concurrency +4 -0
- data/spec/resources/export/inittab/inittab.default +6 -0
- data/spec/resources/export/launchd/launchd-a.default +29 -0
- data/spec/resources/export/launchd/launchd-b.default +29 -0
- data/spec/resources/export/launchd/launchd-c.default +30 -0
- data/spec/resources/export/runit/app-alpha-1/log/run +7 -0
- data/spec/resources/export/runit/app-alpha-1/run +4 -0
- data/spec/resources/export/runit/app-alpha-2/log/run +7 -0
- data/spec/resources/export/runit/app-alpha-2/run +4 -0
- data/spec/resources/export/runit/app-bravo-1/log/run +7 -0
- data/spec/resources/export/runit/app-bravo-1/run +4 -0
- data/spec/resources/export/supervisord/app-alpha-1.conf +46 -0
- data/spec/resources/export/supervisord/app-alpha-2.conf +24 -0
- data/spec/resources/export/systemd/concurrency/app-alpha-1.service +14 -0
- data/spec/resources/export/systemd/concurrency/app-alpha-2.service +14 -0
- data/spec/resources/export/systemd/concurrency/app-alpha.target +3 -0
- data/spec/resources/export/systemd/concurrency/app.target +6 -0
- data/spec/resources/export/systemd/standard/app-alpha-1.service +14 -0
- data/spec/resources/export/systemd/standard/app-alpha.target +3 -0
- data/spec/resources/export/systemd/standard/app-bravo-1.service +14 -0
- data/spec/resources/export/systemd/standard/app-bravo.target +3 -0
- data/spec/resources/export/systemd/standard/app.target +6 -0
- data/spec/resources/export/upstart/app-alpha-1.conf +11 -0
- data/spec/resources/export/upstart/app-alpha-2.conf +11 -0
- data/spec/resources/export/upstart/app-alpha.conf +2 -0
- data/spec/resources/export/upstart/app-bravo-1.conf +11 -0
- data/spec/resources/export/upstart/app-bravo.conf +2 -0
- data/spec/resources/export/upstart/app.conf +2 -0
- data/spec/spec_helper.rb +166 -0
- metadata +164 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
require "foreman"
|
|
2
|
+
require "foreman/env"
|
|
3
|
+
require "foreman/process"
|
|
4
|
+
require "foreman/procfile"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require "timeout"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require "thread"
|
|
9
|
+
|
|
10
|
+
class Foreman::Engine
|
|
11
|
+
|
|
12
|
+
# The signals that the engine cares about.
|
|
13
|
+
#
|
|
14
|
+
HANDLED_SIGNALS = [ :TERM, :INT, :HUP ]
|
|
15
|
+
|
|
16
|
+
attr_reader :env
|
|
17
|
+
attr_reader :options
|
|
18
|
+
attr_reader :processes
|
|
19
|
+
|
|
20
|
+
# Create an +Engine+ for running processes
|
|
21
|
+
#
|
|
22
|
+
# @param [Hash] options
|
|
23
|
+
#
|
|
24
|
+
# @option options [String] :formation (all=1) The process formation to use
|
|
25
|
+
# @option options [Fixnum] :port (5000) The base port to assign to processes
|
|
26
|
+
# @option options [String] :root (Dir.pwd) The root directory from which to run processes
|
|
27
|
+
#
|
|
28
|
+
def initialize(options={})
|
|
29
|
+
@options = options.dup
|
|
30
|
+
|
|
31
|
+
@options[:formation] ||= (options[:concurrency] || "all=1")
|
|
32
|
+
@options[:timeout] ||= 5
|
|
33
|
+
|
|
34
|
+
@env = {}
|
|
35
|
+
@mutex = Mutex.new
|
|
36
|
+
@names = {}
|
|
37
|
+
@processes = []
|
|
38
|
+
@running = {}
|
|
39
|
+
@readers = {}
|
|
40
|
+
|
|
41
|
+
# Self-pipe for deferred signal-handling (ala djb: http://cr.yp.to/docs/selfpipe.html)
|
|
42
|
+
reader, writer = create_pipe
|
|
43
|
+
reader.close_on_exec = true if reader.respond_to?(:close_on_exec)
|
|
44
|
+
writer.close_on_exec = true if writer.respond_to?(:close_on_exec)
|
|
45
|
+
@selfpipe = { :reader => reader, :writer => writer }
|
|
46
|
+
|
|
47
|
+
# Set up a global signal queue
|
|
48
|
+
# http://blog.rubybestpractices.com/posts/ewong/016-Implementing-Signal-Handlers.html
|
|
49
|
+
Thread.main[:signal_queue] = []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Start the processes registered to this +Engine+
|
|
53
|
+
#
|
|
54
|
+
def start
|
|
55
|
+
register_signal_handlers
|
|
56
|
+
startup
|
|
57
|
+
spawn_processes
|
|
58
|
+
watch_for_output
|
|
59
|
+
sleep 0.1
|
|
60
|
+
watch_for_termination { terminate_gracefully }
|
|
61
|
+
shutdown
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Set up deferred signal handlers
|
|
65
|
+
#
|
|
66
|
+
def register_signal_handlers
|
|
67
|
+
HANDLED_SIGNALS.each do |sig|
|
|
68
|
+
if ::Signal.list.include? sig.to_s
|
|
69
|
+
trap(sig) { Thread.main[:signal_queue] << sig ; notice_signal }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Unregister deferred signal handlers
|
|
75
|
+
#
|
|
76
|
+
def restore_default_signal_handlers
|
|
77
|
+
HANDLED_SIGNALS.each do |sig|
|
|
78
|
+
trap(sig, :DEFAULT) if ::Signal.list.include? sig.to_s
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Wake the main thread up via the selfpipe when there's a signal
|
|
83
|
+
#
|
|
84
|
+
def notice_signal
|
|
85
|
+
@selfpipe[:writer].write_nonblock( '.' )
|
|
86
|
+
rescue Errno::EAGAIN
|
|
87
|
+
# Ignore writes that would block
|
|
88
|
+
rescue Errno::EINT
|
|
89
|
+
# Retry if another signal arrived while writing
|
|
90
|
+
retry
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Invoke the real handler for signal +sig+. This shouldn't be called directly
|
|
94
|
+
# by signal handlers, as it might invoke code which isn't re-entrant.
|
|
95
|
+
#
|
|
96
|
+
# @param [Symbol] sig the name of the signal to be handled
|
|
97
|
+
#
|
|
98
|
+
def handle_signal(sig)
|
|
99
|
+
case sig
|
|
100
|
+
when :TERM
|
|
101
|
+
handle_term_signal
|
|
102
|
+
when :INT
|
|
103
|
+
handle_interrupt
|
|
104
|
+
when :HUP
|
|
105
|
+
handle_hangup
|
|
106
|
+
else
|
|
107
|
+
system "unhandled signal #{sig}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Handle a TERM signal
|
|
112
|
+
#
|
|
113
|
+
def handle_term_signal
|
|
114
|
+
puts "SIGTERM received"
|
|
115
|
+
terminate_gracefully
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Handle an INT signal
|
|
119
|
+
#
|
|
120
|
+
def handle_interrupt
|
|
121
|
+
puts "SIGINT received"
|
|
122
|
+
terminate_gracefully
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Handle a HUP signal
|
|
126
|
+
#
|
|
127
|
+
def handle_hangup
|
|
128
|
+
puts "SIGHUP received"
|
|
129
|
+
terminate_gracefully
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Register a process to be run by this +Engine+
|
|
133
|
+
#
|
|
134
|
+
# @param [String] name A name for this process
|
|
135
|
+
# @param [String] command The command to run
|
|
136
|
+
# @param [Hash] options
|
|
137
|
+
#
|
|
138
|
+
# @option options [Hash] :env A custom environment for this process
|
|
139
|
+
#
|
|
140
|
+
def register(name, command, options={})
|
|
141
|
+
options[:env] ||= env
|
|
142
|
+
options[:cwd] ||= File.dirname(command.split(" ").first)
|
|
143
|
+
process = Foreman::Process.new(command, options)
|
|
144
|
+
@names[process] = name
|
|
145
|
+
@processes << process
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Clear the processes registered to this +Engine+
|
|
149
|
+
#
|
|
150
|
+
def clear
|
|
151
|
+
@names = {}
|
|
152
|
+
@processes = []
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Register processes by reading a Procfile
|
|
156
|
+
#
|
|
157
|
+
# @param [String] filename A Procfile from which to read processes to register
|
|
158
|
+
#
|
|
159
|
+
def load_procfile(filename)
|
|
160
|
+
options[:root] ||= File.dirname(filename)
|
|
161
|
+
Foreman::Procfile.new(filename).entries do |name, command|
|
|
162
|
+
register name, command, :cwd => options[:root]
|
|
163
|
+
end
|
|
164
|
+
self
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Load a .env file into the +env+ for this +Engine+
|
|
168
|
+
#
|
|
169
|
+
# @param [String] filename A .env file to load into the environment
|
|
170
|
+
#
|
|
171
|
+
def load_env(filename)
|
|
172
|
+
Foreman::Env.new(filename).entries do |name, value|
|
|
173
|
+
@env[name] = value
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Send a signal to all processes started by this +Engine+
|
|
178
|
+
#
|
|
179
|
+
# @param [String] signal The signal to send to each process
|
|
180
|
+
#
|
|
181
|
+
def kill_children(signal="SIGTERM")
|
|
182
|
+
if Foreman.windows?
|
|
183
|
+
@running.each do |pid, (process, index)|
|
|
184
|
+
system "sending #{signal} to #{name_for(pid)} at pid #{pid}"
|
|
185
|
+
begin
|
|
186
|
+
Process.kill(signal, pid)
|
|
187
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
else
|
|
191
|
+
begin
|
|
192
|
+
Process.kill signal, *@running.keys unless @running.empty?
|
|
193
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Send a signal to the whole process group.
|
|
199
|
+
#
|
|
200
|
+
# @param [String] signal The signal to send
|
|
201
|
+
#
|
|
202
|
+
def killall(signal="SIGTERM")
|
|
203
|
+
if Foreman.windows?
|
|
204
|
+
kill_children(signal)
|
|
205
|
+
else
|
|
206
|
+
begin
|
|
207
|
+
Process.kill "-#{signal}", Process.pid
|
|
208
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Get the process formation
|
|
214
|
+
#
|
|
215
|
+
# @returns [Fixnum] The formation count for the specified process
|
|
216
|
+
#
|
|
217
|
+
def formation
|
|
218
|
+
@formation ||= parse_formation(options[:formation])
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# List the available process names
|
|
222
|
+
#
|
|
223
|
+
# @returns [Array] A list of process names
|
|
224
|
+
#
|
|
225
|
+
def process_names
|
|
226
|
+
@processes.map { |p| @names[p] }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Get the +Process+ for a specifid name
|
|
230
|
+
#
|
|
231
|
+
# @param [String] name The process name
|
|
232
|
+
#
|
|
233
|
+
# @returns [Foreman::Process] The +Process+ for the specified name
|
|
234
|
+
#
|
|
235
|
+
def process(name)
|
|
236
|
+
@names.invert[name]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Yield each +Process+ in order
|
|
240
|
+
#
|
|
241
|
+
def each_process
|
|
242
|
+
process_names.each do |name|
|
|
243
|
+
yield name, process(name)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Get the root directory for this +Engine+
|
|
248
|
+
#
|
|
249
|
+
# @returns [String] The root directory
|
|
250
|
+
#
|
|
251
|
+
def root
|
|
252
|
+
File.expand_path(options[:root] || Dir.pwd)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Get the port for a given process and offset
|
|
256
|
+
#
|
|
257
|
+
# @param [Foreman::Process] process A +Process+ associated with this engine
|
|
258
|
+
# @param [Fixnum] instance The instance of the process
|
|
259
|
+
#
|
|
260
|
+
# @returns [Fixnum] port The port to use for this instance of this process
|
|
261
|
+
#
|
|
262
|
+
def port_for(process, instance, base=nil)
|
|
263
|
+
if base
|
|
264
|
+
base + (@processes.index(process.process) * 100) + (instance - 1)
|
|
265
|
+
else
|
|
266
|
+
base_port + (@processes.index(process) * 100) + (instance - 1)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Get the base port for this foreman instance
|
|
271
|
+
#
|
|
272
|
+
# @returns [Fixnum] port The base port
|
|
273
|
+
#
|
|
274
|
+
def base_port
|
|
275
|
+
(options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# deprecated
|
|
279
|
+
def environment
|
|
280
|
+
env
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
### Engine API ######################################################
|
|
286
|
+
|
|
287
|
+
def startup
|
|
288
|
+
raise TypeError, "must use a subclass of Foreman::Engine"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def output(name, data)
|
|
292
|
+
raise TypeError, "must use a subclass of Foreman::Engine"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def shutdown
|
|
296
|
+
raise TypeError, "must use a subclass of Foreman::Engine"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
## Helpers ##########################################################
|
|
300
|
+
|
|
301
|
+
def create_pipe
|
|
302
|
+
IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def name_for(pid)
|
|
306
|
+
process, index = @running[pid]
|
|
307
|
+
name_for_index(process, index)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def name_for_index(process, index)
|
|
311
|
+
[ @names[process], index.to_s ].compact.join(".")
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def parse_formation(formation)
|
|
315
|
+
pairs = formation.to_s.gsub(/\s/, "").split(",")
|
|
316
|
+
|
|
317
|
+
pairs.inject(Hash.new(0)) do |ax, pair|
|
|
318
|
+
process, amount = pair.split("=")
|
|
319
|
+
process == "all" ? ax.default = amount.to_i : ax[process] = amount.to_i
|
|
320
|
+
ax
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def output_with_mutex(name, message)
|
|
325
|
+
@mutex.synchronize do
|
|
326
|
+
output name, message
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def system(message)
|
|
331
|
+
output_with_mutex "system", message
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def termination_message_for(status)
|
|
335
|
+
if status.exited?
|
|
336
|
+
"exited with code #{status.exitstatus}"
|
|
337
|
+
elsif status.signaled?
|
|
338
|
+
"terminated by SIG#{Signal.list.invert[status.termsig]}"
|
|
339
|
+
else
|
|
340
|
+
"died a mysterious death"
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def flush_reader(reader)
|
|
345
|
+
until reader.eof?
|
|
346
|
+
data = reader.gets
|
|
347
|
+
output_with_mutex name_for(@readers.key(reader)), data
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
## Engine ###########################################################
|
|
352
|
+
|
|
353
|
+
def spawn_processes
|
|
354
|
+
@processes.each do |process|
|
|
355
|
+
1.upto(formation[@names[process]]) do |n|
|
|
356
|
+
reader, writer = create_pipe
|
|
357
|
+
begin
|
|
358
|
+
pid = process.run(:output => writer, :env => {
|
|
359
|
+
"PORT" => port_for(process, n).to_s,
|
|
360
|
+
"PS" => name_for_index(process, n)
|
|
361
|
+
})
|
|
362
|
+
writer.puts "started with pid #{pid}"
|
|
363
|
+
rescue Errno::ENOENT
|
|
364
|
+
writer.puts "unknown command: #{process.command}"
|
|
365
|
+
end
|
|
366
|
+
@running[pid] = [process, n]
|
|
367
|
+
@readers[pid] = reader
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def read_self_pipe
|
|
373
|
+
@selfpipe[:reader].read_nonblock(11)
|
|
374
|
+
rescue Errno::EAGAIN, Errno::EINTR, Errno::EBADF
|
|
375
|
+
# ignore
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def handle_signals
|
|
379
|
+
while sig = Thread.main[:signal_queue].shift
|
|
380
|
+
self.handle_signal(sig)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def handle_io(readers)
|
|
385
|
+
readers.each do |reader|
|
|
386
|
+
next if reader == @selfpipe[:reader]
|
|
387
|
+
|
|
388
|
+
if reader.eof?
|
|
389
|
+
@readers.delete_if { |key, value| value == reader }
|
|
390
|
+
else
|
|
391
|
+
data = reader.gets
|
|
392
|
+
output_with_mutex name_for(@readers.invert[reader]), data
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def watch_for_output
|
|
398
|
+
Thread.new do
|
|
399
|
+
begin
|
|
400
|
+
loop do
|
|
401
|
+
io = IO.select([@selfpipe[:reader]] + @readers.values, nil, nil, 30)
|
|
402
|
+
read_self_pipe
|
|
403
|
+
handle_signals
|
|
404
|
+
handle_io(io ? io.first : [])
|
|
405
|
+
end
|
|
406
|
+
rescue Exception => ex
|
|
407
|
+
puts ex.message
|
|
408
|
+
puts ex.backtrace
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def watch_for_termination
|
|
414
|
+
pid, status = Process.wait2
|
|
415
|
+
output_with_mutex name_for(pid), termination_message_for(status)
|
|
416
|
+
@running.delete(pid)
|
|
417
|
+
yield if block_given?
|
|
418
|
+
pid
|
|
419
|
+
rescue Errno::ECHILD
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def terminate_gracefully
|
|
423
|
+
return if @terminating
|
|
424
|
+
restore_default_signal_handlers
|
|
425
|
+
@terminating = true
|
|
426
|
+
if Foreman.windows?
|
|
427
|
+
system "sending SIGKILL to all processes"
|
|
428
|
+
kill_children "SIGKILL"
|
|
429
|
+
else
|
|
430
|
+
system "sending SIGTERM to all processes"
|
|
431
|
+
kill_children "SIGTERM"
|
|
432
|
+
end
|
|
433
|
+
Timeout.timeout(options[:timeout]) do
|
|
434
|
+
watch_for_termination while @running.length > 0
|
|
435
|
+
end
|
|
436
|
+
rescue Timeout::Error
|
|
437
|
+
system "sending SIGKILL to all processes"
|
|
438
|
+
kill_children "SIGKILL"
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require "foreman/engine"
|
|
2
|
+
|
|
3
|
+
class Foreman::Engine::CLI < Foreman::Engine
|
|
4
|
+
|
|
5
|
+
module Color
|
|
6
|
+
|
|
7
|
+
ANSI = {
|
|
8
|
+
:reset => 0,
|
|
9
|
+
:black => 30,
|
|
10
|
+
:red => 31,
|
|
11
|
+
:green => 32,
|
|
12
|
+
:yellow => 33,
|
|
13
|
+
:blue => 34,
|
|
14
|
+
:magenta => 35,
|
|
15
|
+
:cyan => 36,
|
|
16
|
+
:white => 37,
|
|
17
|
+
:bright_black => 30,
|
|
18
|
+
:bright_red => 31,
|
|
19
|
+
:bright_green => 32,
|
|
20
|
+
:bright_yellow => 33,
|
|
21
|
+
:bright_blue => 34,
|
|
22
|
+
:bright_magenta => 35,
|
|
23
|
+
:bright_cyan => 36,
|
|
24
|
+
:bright_white => 37,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def self.enable(io, force=false)
|
|
28
|
+
io.extend(self)
|
|
29
|
+
@@color_force = force
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def color?
|
|
33
|
+
return true if @@color_force
|
|
34
|
+
return false if Foreman.windows?
|
|
35
|
+
return false unless self.respond_to?(:isatty)
|
|
36
|
+
self.isatty && ENV["TERM"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def color(name)
|
|
40
|
+
return "" unless color?
|
|
41
|
+
return "" unless ansi = ANSI[name.to_sym]
|
|
42
|
+
"\e[#{ansi}m"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
FOREMAN_COLORS = %w( cyan yellow green magenta red blue bright_cyan bright_yellow
|
|
48
|
+
bright_green bright_magenta bright_red bright_blue )
|
|
49
|
+
|
|
50
|
+
def startup
|
|
51
|
+
@colors = map_colors
|
|
52
|
+
proctitle "foreman: master" unless Foreman.windows?
|
|
53
|
+
Color.enable($stdout, options[:color])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def output(name, data)
|
|
57
|
+
data.to_s.lines.map(&:chomp).each do |message|
|
|
58
|
+
output = ""
|
|
59
|
+
output += $stdout.color(@colors[name.split(".").first].to_sym)
|
|
60
|
+
output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
|
|
61
|
+
output += $stdout.color(:reset)
|
|
62
|
+
output += message
|
|
63
|
+
$stdout.puts output
|
|
64
|
+
$stdout.flush
|
|
65
|
+
end
|
|
66
|
+
rescue Errno::EPIPE
|
|
67
|
+
terminate_gracefully
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def shutdown
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def name_padding
|
|
76
|
+
@name_padding ||= begin
|
|
77
|
+
index_padding = @names.values.map { |n| formation[n] }.max.to_s.length + 1
|
|
78
|
+
name_padding = @names.values.map { |n| n.length + index_padding }.sort.last
|
|
79
|
+
[ 6, name_padding ].max
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def pad_process_name(name)
|
|
84
|
+
name.ljust(name_padding, " ")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def map_colors
|
|
88
|
+
colors = Hash.new("white")
|
|
89
|
+
@names.values.each_with_index do |name, index|
|
|
90
|
+
colors[name] = FOREMAN_COLORS[index % FOREMAN_COLORS.length]
|
|
91
|
+
end
|
|
92
|
+
colors["system"] = "bright_white"
|
|
93
|
+
colors
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def proctitle(title)
|
|
97
|
+
$0 = title
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def termtitle(title)
|
|
101
|
+
printf("\033]0;#{title}\007") unless Foreman.windows?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
end
|