foreman-systemd 0.78.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +64 -0
  3. data/bin/foreman +7 -0
  4. data/bin/foreman-runner +41 -0
  5. data/data/example/Procfile +4 -0
  6. data/data/example/Procfile.without_colon +2 -0
  7. data/data/example/error +7 -0
  8. data/data/example/log/neverdie.log +4 -0
  9. data/data/example/spawnee +14 -0
  10. data/data/example/spawner +7 -0
  11. data/data/example/ticker +14 -0
  12. data/data/example/utf8 +11 -0
  13. data/data/export/bluepill/master.pill.erb +28 -0
  14. data/data/export/daemon/master.conf.erb +14 -0
  15. data/data/export/daemon/process.conf.erb +8 -0
  16. data/data/export/daemon/process_master.conf.erb +2 -0
  17. data/data/export/launchd/launchd.plist.erb +33 -0
  18. data/data/export/runit/log/run.erb +7 -0
  19. data/data/export/runit/run.erb +4 -0
  20. data/data/export/supervisord/app.conf.erb +28 -0
  21. data/data/export/systemd/master.target.erb +6 -0
  22. data/data/export/systemd/process.service.erb +16 -0
  23. data/data/export/systemd/process_master.target.erb +4 -0
  24. data/data/export/upstart/master.conf.erb +2 -0
  25. data/data/export/upstart/process.conf.erb +14 -0
  26. data/data/export/upstart/process_master.conf.erb +2 -0
  27. data/lib/foreman.rb +17 -0
  28. data/lib/foreman/cli.rb +161 -0
  29. data/lib/foreman/distribution.rb +9 -0
  30. data/lib/foreman/engine.rb +441 -0
  31. data/lib/foreman/engine/cli.rb +104 -0
  32. data/lib/foreman/env.rb +29 -0
  33. data/lib/foreman/export.rb +36 -0
  34. data/lib/foreman/export/base.rb +156 -0
  35. data/lib/foreman/export/bluepill.rb +12 -0
  36. data/lib/foreman/export/daemon.rb +28 -0
  37. data/lib/foreman/export/inittab.rb +42 -0
  38. data/lib/foreman/export/launchd.rb +22 -0
  39. data/lib/foreman/export/runit.rb +34 -0
  40. data/lib/foreman/export/supervisord.rb +16 -0
  41. data/lib/foreman/export/systemd.rb +32 -0
  42. data/lib/foreman/export/upstart.rb +43 -0
  43. data/lib/foreman/helpers.rb +45 -0
  44. data/lib/foreman/process.rb +80 -0
  45. data/lib/foreman/procfile.rb +92 -0
  46. data/lib/foreman/version.rb +5 -0
  47. data/man/foreman.1 +278 -0
  48. data/spec/foreman/cli_spec.rb +107 -0
  49. data/spec/foreman/engine_spec.rb +112 -0
  50. data/spec/foreman/export/base_spec.rb +19 -0
  51. data/spec/foreman/export/bluepill_spec.rb +37 -0
  52. data/spec/foreman/export/daemon_spec.rb +97 -0
  53. data/spec/foreman/export/inittab_spec.rb +40 -0
  54. data/spec/foreman/export/launchd_spec.rb +31 -0
  55. data/spec/foreman/export/runit_spec.rb +36 -0
  56. data/spec/foreman/export/supervisord_spec.rb +36 -0
  57. data/spec/foreman/export/systemd_spec.rb +91 -0
  58. data/spec/foreman/export/upstart_spec.rb +118 -0
  59. data/spec/foreman/export_spec.rb +24 -0
  60. data/spec/foreman/helpers_spec.rb +26 -0
  61. data/spec/foreman/process_spec.rb +71 -0
  62. data/spec/foreman/procfile_spec.rb +43 -0
  63. data/spec/foreman_spec.rb +16 -0
  64. data/spec/helper_spec.rb +19 -0
  65. data/spec/resources/Procfile +5 -0
  66. data/spec/resources/bin/echo +2 -0
  67. data/spec/resources/bin/env +2 -0
  68. data/spec/resources/bin/test +2 -0
  69. data/spec/resources/bin/utf8 +2 -0
  70. data/spec/resources/export/bluepill/app-concurrency.pill +49 -0
  71. data/spec/resources/export/bluepill/app.pill +81 -0
  72. data/spec/resources/export/daemon/app-alpha-1.conf +7 -0
  73. data/spec/resources/export/daemon/app-alpha-2.conf +7 -0
  74. data/spec/resources/export/daemon/app-alpha.conf +2 -0
  75. data/spec/resources/export/daemon/app-bravo-1.conf +7 -0
  76. data/spec/resources/export/daemon/app-bravo.conf +2 -0
  77. data/spec/resources/export/daemon/app.conf +14 -0
  78. data/spec/resources/export/inittab/inittab.concurrency +4 -0
  79. data/spec/resources/export/inittab/inittab.default +6 -0
  80. data/spec/resources/export/launchd/launchd-a.default +29 -0
  81. data/spec/resources/export/launchd/launchd-b.default +29 -0
  82. data/spec/resources/export/launchd/launchd-c.default +30 -0
  83. data/spec/resources/export/runit/app-alpha-1/log/run +7 -0
  84. data/spec/resources/export/runit/app-alpha-1/run +4 -0
  85. data/spec/resources/export/runit/app-alpha-2/log/run +7 -0
  86. data/spec/resources/export/runit/app-alpha-2/run +4 -0
  87. data/spec/resources/export/runit/app-bravo-1/log/run +7 -0
  88. data/spec/resources/export/runit/app-bravo-1/run +4 -0
  89. data/spec/resources/export/supervisord/app-alpha-1.conf +46 -0
  90. data/spec/resources/export/supervisord/app-alpha-2.conf +24 -0
  91. data/spec/resources/export/systemd/concurrency/app-alpha-1.service +14 -0
  92. data/spec/resources/export/systemd/concurrency/app-alpha-2.service +14 -0
  93. data/spec/resources/export/systemd/concurrency/app-alpha.target +3 -0
  94. data/spec/resources/export/systemd/concurrency/app.target +6 -0
  95. data/spec/resources/export/systemd/standard/app-alpha-1.service +14 -0
  96. data/spec/resources/export/systemd/standard/app-alpha.target +3 -0
  97. data/spec/resources/export/systemd/standard/app-bravo-1.service +14 -0
  98. data/spec/resources/export/systemd/standard/app-bravo.target +3 -0
  99. data/spec/resources/export/systemd/standard/app.target +6 -0
  100. data/spec/resources/export/upstart/app-alpha-1.conf +11 -0
  101. data/spec/resources/export/upstart/app-alpha-2.conf +11 -0
  102. data/spec/resources/export/upstart/app-alpha.conf +2 -0
  103. data/spec/resources/export/upstart/app-bravo-1.conf +11 -0
  104. data/spec/resources/export/upstart/app-bravo.conf +2 -0
  105. data/spec/resources/export/upstart/app.conf +2 -0
  106. data/spec/spec_helper.rb +166 -0
  107. metadata +164 -0
@@ -0,0 +1,9 @@
1
+ module Foreman
2
+ module Distribution
3
+ def self.files
4
+ Dir[File.expand_path("../../../{bin,data,lib}/**/*", __FILE__)].select do |file|
5
+ File.file?(file)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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