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.
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