fiveman 0.1.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 +11 -0
- data/bin/fiveman +7 -0
- data/bin/fiveman-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 +31 -0
- data/data/export/systemd/master.target.erb +5 -0
- data/data/export/systemd/process.service.erb +21 -0
- data/data/export/upstart/master.conf.erb +2 -0
- data/data/export/upstart/process.conf.erb +15 -0
- data/data/export/upstart/process_master.conf.erb +2 -0
- data/lib/fiveman/cli.rb +162 -0
- data/lib/fiveman/distribution.rb +9 -0
- data/lib/fiveman/engine/cli.rb +101 -0
- data/lib/fiveman/engine.rb +494 -0
- data/lib/fiveman/env.rb +29 -0
- data/lib/fiveman/export/base.rb +171 -0
- data/lib/fiveman/export/bluepill.rb +12 -0
- data/lib/fiveman/export/daemon.rb +28 -0
- data/lib/fiveman/export/inittab.rb +42 -0
- data/lib/fiveman/export/launchd.rb +22 -0
- data/lib/fiveman/export/runit.rb +34 -0
- data/lib/fiveman/export/supervisord.rb +16 -0
- data/lib/fiveman/export/systemd.rb +34 -0
- data/lib/fiveman/export/upstart.rb +46 -0
- data/lib/fiveman/export.rb +36 -0
- data/lib/fiveman/helpers.rb +45 -0
- data/lib/fiveman/process.rb +80 -0
- data/lib/fiveman/procfile.rb +94 -0
- data/lib/fiveman/vendor/thor/lib/thor/actions/create_file.rb +103 -0
- data/lib/fiveman/vendor/thor/lib/thor/actions/create_link.rb +59 -0
- data/lib/fiveman/vendor/thor/lib/thor/actions/directory.rb +118 -0
- data/lib/fiveman/vendor/thor/lib/thor/actions/empty_directory.rb +135 -0
- data/lib/fiveman/vendor/thor/lib/thor/actions/file_manipulation.rb +327 -0
- data/lib/fiveman/vendor/thor/lib/thor/actions/inject_into_file.rb +103 -0
- data/lib/fiveman/vendor/thor/lib/thor/actions.rb +318 -0
- data/lib/fiveman/vendor/thor/lib/thor/base.rb +656 -0
- data/lib/fiveman/vendor/thor/lib/thor/command.rb +133 -0
- data/lib/fiveman/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb +85 -0
- data/lib/fiveman/vendor/thor/lib/thor/core_ext/io_binary_read.rb +12 -0
- data/lib/fiveman/vendor/thor/lib/thor/core_ext/ordered_hash.rb +129 -0
- data/lib/fiveman/vendor/thor/lib/thor/error.rb +32 -0
- data/lib/fiveman/vendor/thor/lib/thor/group.rb +281 -0
- data/lib/fiveman/vendor/thor/lib/thor/invocation.rb +177 -0
- data/lib/fiveman/vendor/thor/lib/thor/line_editor/basic.rb +35 -0
- data/lib/fiveman/vendor/thor/lib/thor/line_editor/readline.rb +88 -0
- data/lib/fiveman/vendor/thor/lib/thor/line_editor.rb +17 -0
- data/lib/fiveman/vendor/thor/lib/thor/parser/argument.rb +70 -0
- data/lib/fiveman/vendor/thor/lib/thor/parser/arguments.rb +175 -0
- data/lib/fiveman/vendor/thor/lib/thor/parser/option.rb +146 -0
- data/lib/fiveman/vendor/thor/lib/thor/parser/options.rb +220 -0
- data/lib/fiveman/vendor/thor/lib/thor/parser.rb +4 -0
- data/lib/fiveman/vendor/thor/lib/thor/rake_compat.rb +71 -0
- data/lib/fiveman/vendor/thor/lib/thor/runner.rb +322 -0
- data/lib/fiveman/vendor/thor/lib/thor/shell/basic.rb +436 -0
- data/lib/fiveman/vendor/thor/lib/thor/shell/color.rb +149 -0
- data/lib/fiveman/vendor/thor/lib/thor/shell/html.rb +126 -0
- data/lib/fiveman/vendor/thor/lib/thor/shell.rb +81 -0
- data/lib/fiveman/vendor/thor/lib/thor/util.rb +268 -0
- data/lib/fiveman/vendor/thor/lib/thor/version.rb +3 -0
- data/lib/fiveman/vendor/thor/lib/thor.rb +492 -0
- data/lib/fiveman/version.rb +5 -0
- data/lib/fiveman.rb +17 -0
- data/man/fiveman.1 +284 -0
- data/spec/fiveman/cli_spec.rb +111 -0
- data/spec/fiveman/engine_spec.rb +114 -0
- data/spec/fiveman/export/base_spec.rb +19 -0
- data/spec/fiveman/export/bluepill_spec.rb +37 -0
- data/spec/fiveman/export/daemon_spec.rb +97 -0
- data/spec/fiveman/export/inittab_spec.rb +40 -0
- data/spec/fiveman/export/launchd_spec.rb +31 -0
- data/spec/fiveman/export/runit_spec.rb +36 -0
- data/spec/fiveman/export/supervisord_spec.rb +38 -0
- data/spec/fiveman/export/systemd_spec.rb +155 -0
- data/spec/fiveman/export/upstart_spec.rb +118 -0
- data/spec/fiveman/export_spec.rb +24 -0
- data/spec/fiveman/helpers_spec.rb +26 -0
- data/spec/fiveman/process_spec.rb +71 -0
- data/spec/fiveman/procfile_spec.rb +57 -0
- data/spec/fiveman_spec.rb +16 -0
- data/spec/helper_spec.rb +19 -0
- data/spec/resources/Procfile +5 -0
- data/spec/resources/Procfile.bad +2 -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 +42 -0
- data/spec/resources/export/supervisord/app-alpha-2.conf +22 -0
- data/spec/resources/export/systemd/app-alpha.1.service +18 -0
- data/spec/resources/export/systemd/app-alpha.2.service +18 -0
- data/spec/resources/export/systemd/app-alpha.target +2 -0
- data/spec/resources/export/systemd/app-bravo.1.service +18 -0
- data/spec/resources/export/systemd/app-bravo.target +2 -0
- data/spec/resources/export/systemd/app.target +5 -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 +177 -0
- metadata +177 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
require "fiveman"
|
|
2
|
+
require "fiveman/env"
|
|
3
|
+
require "fiveman/process"
|
|
4
|
+
require "fiveman/procfile"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "thread"
|
|
8
|
+
|
|
9
|
+
class Fiveman::Engine
|
|
10
|
+
|
|
11
|
+
# The signals that the engine cares about.
|
|
12
|
+
#
|
|
13
|
+
HANDLED_SIGNALS = [ :TERM, :INT, :HUP, :USR1, :USR2 ]
|
|
14
|
+
|
|
15
|
+
attr_reader :env
|
|
16
|
+
attr_reader :options
|
|
17
|
+
attr_reader :processes
|
|
18
|
+
|
|
19
|
+
# Create an +Engine+ for running processes
|
|
20
|
+
#
|
|
21
|
+
# @param [Hash] options
|
|
22
|
+
#
|
|
23
|
+
# @option options [String] :formation (all=1) The process formation to use
|
|
24
|
+
# @option options [Fixnum] :port (5000) The base port to assign to processes
|
|
25
|
+
# @option options [String] :root (Dir.pwd) The root directory from which to run processes
|
|
26
|
+
#
|
|
27
|
+
def initialize(options={})
|
|
28
|
+
@options = options.dup
|
|
29
|
+
|
|
30
|
+
@options[:formation] ||= "all=1"
|
|
31
|
+
@options[:timeout] ||= 5
|
|
32
|
+
|
|
33
|
+
@env = {}
|
|
34
|
+
@mutex = Mutex.new
|
|
35
|
+
@names = {}
|
|
36
|
+
@processes = []
|
|
37
|
+
@running = {}
|
|
38
|
+
@readers = {}
|
|
39
|
+
@shutdown = false
|
|
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
|
+
wait_for_shutdown_or_child_termination
|
|
61
|
+
shutdown
|
|
62
|
+
exit(@exitstatus) if @exitstatus
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Set up deferred signal handlers
|
|
66
|
+
#
|
|
67
|
+
def register_signal_handlers
|
|
68
|
+
HANDLED_SIGNALS.each do |sig|
|
|
69
|
+
if ::Signal.list.include? sig.to_s
|
|
70
|
+
trap(sig) { Thread.main[:signal_queue] << sig ; notice_signal }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Unregister deferred signal handlers
|
|
76
|
+
#
|
|
77
|
+
def restore_default_signal_handlers
|
|
78
|
+
HANDLED_SIGNALS.each do |sig|
|
|
79
|
+
trap(sig, :DEFAULT) if ::Signal.list.include? sig.to_s
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Wake the main thread up via the selfpipe when there's a signal
|
|
84
|
+
#
|
|
85
|
+
def notice_signal
|
|
86
|
+
@selfpipe[:writer].write_nonblock( '.' )
|
|
87
|
+
rescue Errno::EAGAIN
|
|
88
|
+
# Ignore writes that would block
|
|
89
|
+
rescue Errno::EINTR
|
|
90
|
+
# Retry if another signal arrived while writing
|
|
91
|
+
retry
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Invoke the real handler for signal +sig+. This shouldn't be called directly
|
|
95
|
+
# by signal handlers, as it might invoke code which isn't re-entrant.
|
|
96
|
+
#
|
|
97
|
+
# @param [Symbol] sig the name of the signal to be handled
|
|
98
|
+
#
|
|
99
|
+
def handle_signal(sig)
|
|
100
|
+
case sig
|
|
101
|
+
when :TERM
|
|
102
|
+
handle_term_signal
|
|
103
|
+
when :INT
|
|
104
|
+
handle_interrupt
|
|
105
|
+
when :HUP
|
|
106
|
+
handle_hangup
|
|
107
|
+
when *HANDLED_SIGNALS
|
|
108
|
+
handle_signal_forward(sig)
|
|
109
|
+
else
|
|
110
|
+
system "unhandled signal #{sig}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Handle a TERM signal
|
|
115
|
+
#
|
|
116
|
+
def handle_term_signal
|
|
117
|
+
system "SIGTERM received, starting shutdown"
|
|
118
|
+
@shutdown = true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Handle an INT signal
|
|
122
|
+
#
|
|
123
|
+
def handle_interrupt
|
|
124
|
+
system "SIGINT received, starting shutdown"
|
|
125
|
+
@shutdown = true
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Handle a HUP signal
|
|
129
|
+
#
|
|
130
|
+
def handle_hangup
|
|
131
|
+
system "SIGHUP received, starting shutdown"
|
|
132
|
+
@shutdown = true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def handle_signal_forward(signal)
|
|
136
|
+
system "#{signal} received, forwarding it to children"
|
|
137
|
+
kill_children signal
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Register a process to be run by this +Engine+
|
|
141
|
+
#
|
|
142
|
+
# @param [String] name A name for this process
|
|
143
|
+
# @param [String] command The command to run
|
|
144
|
+
# @param [Hash] options
|
|
145
|
+
#
|
|
146
|
+
# @option options [Hash] :env A custom environment for this process
|
|
147
|
+
#
|
|
148
|
+
def register(name, command, options={})
|
|
149
|
+
options[:env] ||= env
|
|
150
|
+
options[:cwd] ||= File.dirname(command.split(" ").first)
|
|
151
|
+
process = Fiveman::Process.new(command, options)
|
|
152
|
+
@names[process] = name
|
|
153
|
+
@processes << process
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Clear the processes registered to this +Engine+
|
|
157
|
+
#
|
|
158
|
+
def clear
|
|
159
|
+
@names = {}
|
|
160
|
+
@processes = []
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Register processes by reading a Procfile
|
|
164
|
+
#
|
|
165
|
+
# @param [String] filename A Procfile from which to read processes to register
|
|
166
|
+
#
|
|
167
|
+
def load_procfile(filename)
|
|
168
|
+
options[:root] ||= File.dirname(filename)
|
|
169
|
+
Fiveman::Procfile.new(filename).entries do |name, command|
|
|
170
|
+
register name, command, :cwd => options[:root]
|
|
171
|
+
end
|
|
172
|
+
self
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Load a .env file into the +env+ for this +Engine+
|
|
176
|
+
#
|
|
177
|
+
# @param [String] filename A .env file to load into the environment
|
|
178
|
+
#
|
|
179
|
+
def load_env(filename)
|
|
180
|
+
Fiveman::Env.new(filename).entries do |name, value|
|
|
181
|
+
@env[name] = value
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Send a signal to all processes started by this +Engine+
|
|
186
|
+
#
|
|
187
|
+
# @param [String] signal The signal to send to each process
|
|
188
|
+
#
|
|
189
|
+
def kill_children(signal="SIGTERM")
|
|
190
|
+
if Fiveman.windows?
|
|
191
|
+
@running.each do |pid, (process, index)|
|
|
192
|
+
system "sending #{signal} to #{name_for(pid)} at pid #{pid}"
|
|
193
|
+
begin
|
|
194
|
+
Process.kill(signal, pid)
|
|
195
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
else
|
|
199
|
+
begin
|
|
200
|
+
pids = @running.keys.compact
|
|
201
|
+
Process.kill signal, *pids unless pids.empty?
|
|
202
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Send a signal to the whole process group.
|
|
208
|
+
#
|
|
209
|
+
# @param [String] signal The signal to send
|
|
210
|
+
#
|
|
211
|
+
def killall(signal="SIGTERM")
|
|
212
|
+
if Fiveman.windows?
|
|
213
|
+
kill_children(signal)
|
|
214
|
+
else
|
|
215
|
+
begin
|
|
216
|
+
Process.kill "-#{signal}", Process.pid
|
|
217
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Get the process formation
|
|
223
|
+
#
|
|
224
|
+
# @returns [Fixnum] The formation count for the specified process
|
|
225
|
+
#
|
|
226
|
+
def formation
|
|
227
|
+
@formation ||= parse_formation(options[:formation])
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# List the available process names
|
|
231
|
+
#
|
|
232
|
+
# @returns [Array] A list of process names
|
|
233
|
+
#
|
|
234
|
+
def process_names
|
|
235
|
+
@processes.map { |p| @names[p] }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Get the +Process+ for a specifid name
|
|
239
|
+
#
|
|
240
|
+
# @param [String] name The process name
|
|
241
|
+
#
|
|
242
|
+
# @returns [Fiveman::Process] The +Process+ for the specified name
|
|
243
|
+
#
|
|
244
|
+
def process(name)
|
|
245
|
+
@names.invert[name]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Yield each +Process+ in order
|
|
249
|
+
#
|
|
250
|
+
def each_process
|
|
251
|
+
process_names.each do |name|
|
|
252
|
+
yield name, process(name)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Get the root directory for this +Engine+
|
|
257
|
+
#
|
|
258
|
+
# @returns [String] The root directory
|
|
259
|
+
#
|
|
260
|
+
def root
|
|
261
|
+
File.expand_path(options[:root] || Dir.pwd)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Get the port for a given process and offset
|
|
265
|
+
#
|
|
266
|
+
# @param [Fiveman::Process] process A +Process+ associated with this engine
|
|
267
|
+
# @param [Fixnum] instance The instance of the process
|
|
268
|
+
#
|
|
269
|
+
# @returns [Fixnum] port The port to use for this instance of this process
|
|
270
|
+
#
|
|
271
|
+
def port_for(process, instance, base=nil)
|
|
272
|
+
if base
|
|
273
|
+
base + (@processes.index(process.process) * 100) + (instance - 1)
|
|
274
|
+
else
|
|
275
|
+
base_port + (@processes.index(process) * 100) + (instance - 1)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Get the base port for this fiveman instance
|
|
280
|
+
#
|
|
281
|
+
# @returns [Fixnum] port The base port
|
|
282
|
+
#
|
|
283
|
+
def base_port
|
|
284
|
+
(options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# deprecated
|
|
288
|
+
def environment
|
|
289
|
+
env
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private
|
|
293
|
+
|
|
294
|
+
### Engine API ######################################################
|
|
295
|
+
|
|
296
|
+
def startup
|
|
297
|
+
raise TypeError, "must use a subclass of Fiveman::Engine"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def output(name, data)
|
|
301
|
+
raise TypeError, "must use a subclass of Fiveman::Engine"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def shutdown
|
|
305
|
+
raise TypeError, "must use a subclass of Fiveman::Engine"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
## Helpers ##########################################################
|
|
309
|
+
|
|
310
|
+
def create_pipe
|
|
311
|
+
IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def name_for(pid)
|
|
315
|
+
process, index = @running[pid]
|
|
316
|
+
name_for_index(process, index)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def name_for_index(process, index)
|
|
320
|
+
[ @names[process], index.to_s ].compact.join(".")
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def parse_formation(formation)
|
|
324
|
+
pairs = formation.to_s.gsub(/\s/, "").split(",")
|
|
325
|
+
|
|
326
|
+
pairs.inject(Hash.new(0)) do |ax, pair|
|
|
327
|
+
process, amount = pair.split("=")
|
|
328
|
+
process == "all" ? ax.default = amount.to_i : ax[process] = amount.to_i
|
|
329
|
+
ax
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def output_with_mutex(name, message)
|
|
334
|
+
@mutex.synchronize do
|
|
335
|
+
output name, message
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def system(message)
|
|
340
|
+
output_with_mutex "system", message
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def termination_message_for(status)
|
|
344
|
+
if status.exited?
|
|
345
|
+
"exited with code #{status.exitstatus}"
|
|
346
|
+
elsif status.signaled?
|
|
347
|
+
"terminated by SIG#{Signal.list.invert[status.termsig]}"
|
|
348
|
+
else
|
|
349
|
+
"died a mysterious death"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def flush_reader(reader)
|
|
354
|
+
until reader.eof?
|
|
355
|
+
data = reader.gets
|
|
356
|
+
output_with_mutex name_for(@readers.key(reader)), data
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
## Engine ###########################################################
|
|
361
|
+
|
|
362
|
+
def spawn_processes
|
|
363
|
+
@processes.each do |process|
|
|
364
|
+
1.upto(formation[@names[process]]) do |n|
|
|
365
|
+
reader, writer = create_pipe
|
|
366
|
+
begin
|
|
367
|
+
pid = process.run(:output => writer, :env => {
|
|
368
|
+
"PORT" => port_for(process, n).to_s,
|
|
369
|
+
"PS" => name_for_index(process, n)
|
|
370
|
+
})
|
|
371
|
+
writer.puts "started with pid #{pid}"
|
|
372
|
+
rescue Errno::ENOENT
|
|
373
|
+
writer.puts "unknown command: #{process.command}"
|
|
374
|
+
end
|
|
375
|
+
@running[pid] = [process, n]
|
|
376
|
+
@readers[pid] = reader
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def read_self_pipe
|
|
382
|
+
@selfpipe[:reader].read_nonblock(11)
|
|
383
|
+
rescue Errno::EAGAIN, Errno::EINTR, Errno::EBADF, Errno::EWOULDBLOCK
|
|
384
|
+
# ignore
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def handle_signals
|
|
388
|
+
while sig = Thread.main[:signal_queue].shift
|
|
389
|
+
self.handle_signal(sig)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def handle_io(readers)
|
|
394
|
+
readers.each do |reader|
|
|
395
|
+
next if reader == @selfpipe[:reader]
|
|
396
|
+
|
|
397
|
+
if reader.eof?
|
|
398
|
+
@readers.delete_if { |key, value| value == reader }
|
|
399
|
+
else
|
|
400
|
+
data = reader.gets
|
|
401
|
+
output_with_mutex name_for(@readers.invert[reader]), data
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def watch_for_output
|
|
407
|
+
Thread.new do
|
|
408
|
+
begin
|
|
409
|
+
loop do
|
|
410
|
+
io = IO.select([@selfpipe[:reader]] + @readers.values, nil, nil, 30)
|
|
411
|
+
read_self_pipe
|
|
412
|
+
handle_signals
|
|
413
|
+
handle_io(io ? io.first : [])
|
|
414
|
+
end
|
|
415
|
+
rescue Exception => ex
|
|
416
|
+
puts ex.message
|
|
417
|
+
puts ex.backtrace
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def wait_for_shutdown_or_child_termination
|
|
423
|
+
loop do
|
|
424
|
+
# Stop if it is time to shut down (asked via a signal)
|
|
425
|
+
break if @shutdown
|
|
426
|
+
|
|
427
|
+
# Stop if any of the children died
|
|
428
|
+
break if check_for_termination
|
|
429
|
+
|
|
430
|
+
# Sleep for a moment and do not blow up if any signals are coming our way
|
|
431
|
+
begin
|
|
432
|
+
sleep(1)
|
|
433
|
+
rescue Exception
|
|
434
|
+
# noop
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Ok, we have exited from the main loop, time to shut down gracefully
|
|
439
|
+
terminate_gracefully
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def check_for_termination
|
|
443
|
+
# Check if any of the children have died off
|
|
444
|
+
pid, status = begin
|
|
445
|
+
Process.wait2(-1, Process::WNOHANG)
|
|
446
|
+
rescue Errno::ECHILD
|
|
447
|
+
return nil
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# record the exit status
|
|
451
|
+
@exitstatus ||= status.exitstatus if status
|
|
452
|
+
|
|
453
|
+
# If no childred have died, nothing to do here
|
|
454
|
+
return nil unless pid
|
|
455
|
+
|
|
456
|
+
# Log the information about the process that exited
|
|
457
|
+
output_with_mutex name_for(pid), termination_message_for(status)
|
|
458
|
+
|
|
459
|
+
# Delete it from the list of running processes and return its pid
|
|
460
|
+
@running.delete(pid)
|
|
461
|
+
return pid
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def terminate_gracefully
|
|
465
|
+
restore_default_signal_handlers
|
|
466
|
+
|
|
467
|
+
# Tell all children to stop gracefully
|
|
468
|
+
if Fiveman.windows?
|
|
469
|
+
system "sending SIGKILL to all processes"
|
|
470
|
+
kill_children "SIGKILL"
|
|
471
|
+
else
|
|
472
|
+
system "sending SIGTERM to all processes"
|
|
473
|
+
kill_children "SIGTERM"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Wait for all children to stop or until the time comes to kill them all
|
|
477
|
+
start_time = Time.now
|
|
478
|
+
while Time.now - start_time <= options[:timeout]
|
|
479
|
+
return if @running.empty?
|
|
480
|
+
check_for_termination
|
|
481
|
+
|
|
482
|
+
# Sleep for a moment and do not blow up if more signals are coming our way
|
|
483
|
+
begin
|
|
484
|
+
sleep(0.1)
|
|
485
|
+
rescue Exception
|
|
486
|
+
# noop
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Ok, we have no other option than to kill all of our children
|
|
491
|
+
system "sending SIGKILL to all processes"
|
|
492
|
+
kill_children "SIGKILL"
|
|
493
|
+
end
|
|
494
|
+
end
|
data/lib/fiveman/env.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "fiveman"
|
|
2
|
+
|
|
3
|
+
class Fiveman::Env
|
|
4
|
+
|
|
5
|
+
attr_reader :entries
|
|
6
|
+
|
|
7
|
+
def initialize(filename)
|
|
8
|
+
@entries = File.read(filename).gsub("\r\n","\n").split("\n").inject({}) do |ax, line|
|
|
9
|
+
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
|
|
10
|
+
key = $1
|
|
11
|
+
case val = $2
|
|
12
|
+
# Remove single quotes
|
|
13
|
+
when /\A'(.*)'\z/ then ax[key] = $1
|
|
14
|
+
# Remove double quotes and unescape string preserving newline characters
|
|
15
|
+
when /\A"(.*)"\z/ then ax[key] = $1.gsub('\n', "\n").gsub(/\\(.)/, '\1')
|
|
16
|
+
else ax[key] = val
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
ax
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def entries
|
|
24
|
+
@entries.each do |key, value|
|
|
25
|
+
yield key, value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require "fiveman/export"
|
|
2
|
+
require "ostruct"
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
class Fiveman::Export::Base
|
|
7
|
+
|
|
8
|
+
attr_reader :location
|
|
9
|
+
attr_reader :engine
|
|
10
|
+
attr_reader :options
|
|
11
|
+
attr_reader :formation
|
|
12
|
+
|
|
13
|
+
# deprecated
|
|
14
|
+
attr_reader :port
|
|
15
|
+
|
|
16
|
+
def initialize(location, engine, options={})
|
|
17
|
+
@location = location
|
|
18
|
+
@engine = engine
|
|
19
|
+
@options = options.dup
|
|
20
|
+
@formation = engine.formation
|
|
21
|
+
|
|
22
|
+
# deprecated
|
|
23
|
+
def port
|
|
24
|
+
Fiveman::Export::Base.warn_deprecation!
|
|
25
|
+
engine.base_port
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# deprecated
|
|
29
|
+
def template
|
|
30
|
+
Fiveman::Export::Base.warn_deprecation!
|
|
31
|
+
options[:template]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# deprecated
|
|
35
|
+
def @engine.procfile
|
|
36
|
+
Fiveman::Export::Base.warn_deprecation!
|
|
37
|
+
@processes.map do |process|
|
|
38
|
+
OpenStruct.new(
|
|
39
|
+
:name => @names[process],
|
|
40
|
+
:process => process
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def export
|
|
47
|
+
error("Must specify a location") unless location
|
|
48
|
+
FileUtils.mkdir_p(location) rescue error("Could not create: #{location}")
|
|
49
|
+
chown user, log
|
|
50
|
+
chown user, run
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def app
|
|
54
|
+
options[:app] || "app"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def log
|
|
58
|
+
options[:log] || "/var/log/#{app}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run
|
|
62
|
+
options[:run] || "/var/run/#{app}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def user
|
|
66
|
+
options[:user] || app
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private ######################################################################
|
|
70
|
+
|
|
71
|
+
def self.warn_deprecation!
|
|
72
|
+
@@deprecation_warned ||= false
|
|
73
|
+
return if @@deprecation_warned
|
|
74
|
+
puts "WARNING: Using deprecated exporter interface. Please update your exporter"
|
|
75
|
+
puts "the interface shown in the upstart exporter:"
|
|
76
|
+
puts
|
|
77
|
+
puts "https://github.com/ddollar/fiveman/blob/master/lib/fiveman/export/upstart.rb"
|
|
78
|
+
puts "https://github.com/ddollar/fiveman/blob/master/data/export/upstart/process.conf.erb"
|
|
79
|
+
puts
|
|
80
|
+
@@deprecation_warned = true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def chown user, dir
|
|
84
|
+
FileUtils.chown user, nil, dir
|
|
85
|
+
rescue
|
|
86
|
+
error("Could not chown #{dir} to #{user}") unless File.writable?(dir) || ! File.exists?(dir)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def error(message)
|
|
90
|
+
raise Fiveman::Export::Exception.new(message)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def say(message)
|
|
94
|
+
puts "[fiveman export] %s" % message
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def clean(filename)
|
|
98
|
+
return unless File.exists?(filename)
|
|
99
|
+
say "cleaning up: #{filename}"
|
|
100
|
+
FileUtils.rm(filename)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def clean_dir(dirname)
|
|
104
|
+
return unless File.exists?(dirname)
|
|
105
|
+
say "cleaning up directory: #{dirname}"
|
|
106
|
+
FileUtils.rm_r(dirname)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def shell_quote(value)
|
|
110
|
+
Shellwords.escape(value)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# deprecated
|
|
114
|
+
def old_export_template(exporter, file, template_root)
|
|
115
|
+
if template_root && File.exist?(file_path = File.join(template_root, file))
|
|
116
|
+
File.read(file_path)
|
|
117
|
+
elsif File.exist?(file_path = File.expand_path(File.join("~/.fiveman/templates", file)))
|
|
118
|
+
File.read(file_path)
|
|
119
|
+
else
|
|
120
|
+
File.read(File.expand_path("../../../../data/export/#{exporter}/#{file}", __FILE__))
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def export_template(name, file=nil, template_root=nil)
|
|
125
|
+
if file && template_root
|
|
126
|
+
old_export_template name, file, template_root
|
|
127
|
+
else
|
|
128
|
+
name_without_first = name.split("/")[1..-1].join("/")
|
|
129
|
+
matchers = []
|
|
130
|
+
matchers << File.join(options[:template], name_without_first) if options[:template]
|
|
131
|
+
matchers << File.expand_path("~/.fiveman/templates/#{name}")
|
|
132
|
+
matchers << File.expand_path("../../../../data/export/#{name}", __FILE__)
|
|
133
|
+
File.read(matchers.detect { |m| File.exists?(m) })
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def write_template(name, target, binding)
|
|
138
|
+
compiled = if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
|
|
139
|
+
ERB.new(export_template(name), trim_mode: '-').result(binding)
|
|
140
|
+
else
|
|
141
|
+
ERB.new(export_template(name), nil, '-').result(binding)
|
|
142
|
+
end
|
|
143
|
+
write_file target, compiled
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def chmod(mode, file)
|
|
147
|
+
say "setting #{file} to mode #{mode}"
|
|
148
|
+
FileUtils.chmod mode, File.join(location, file)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def create_directory(dir)
|
|
152
|
+
say "creating: #{dir}"
|
|
153
|
+
FileUtils.mkdir_p(File.join(location, dir))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def create_symlink(link, target)
|
|
157
|
+
say "symlinking: #{link} -> #{target}"
|
|
158
|
+
FileUtils.symlink(target, File.join(location, link))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def write_file(filename, contents)
|
|
162
|
+
say "writing: #{filename}"
|
|
163
|
+
|
|
164
|
+
filename = File.join(location, filename) unless Pathname.new(filename).absolute?
|
|
165
|
+
|
|
166
|
+
File.open(filename, "w") do |file|
|
|
167
|
+
file.puts contents
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
end
|