foreman 0.47.0 → 0.48.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/taskman +8 -0
- data/data/example/Procfile +4 -3
- data/data/example/spawnee +14 -0
- data/data/example/spawner +7 -0
- data/data/export/bluepill/master.pill.erb +10 -10
- data/data/export/launchd/launchd.plist.erb +3 -3
- data/data/export/runit/log/run.erb +7 -0
- data/data/export/runit/run.erb +2 -2
- data/data/export/supervisord/app.conf.erb +12 -12
- data/data/export/upstart/master.conf.erb +2 -2
- data/data/export/upstart/process.conf.erb +3 -3
- data/lib/foreman/cli.rb +49 -21
- data/lib/foreman/engine.rb +208 -148
- data/lib/foreman/engine/cli.rb +98 -0
- data/lib/foreman/env.rb +27 -0
- data/lib/foreman/export.rb +0 -1
- data/lib/foreman/export/base.rb +58 -35
- data/lib/foreman/export/bluepill.rb +3 -17
- data/lib/foreman/export/inittab.rb +8 -11
- data/lib/foreman/export/launchd.rb +4 -16
- data/lib/foreman/export/runit.rb +14 -39
- data/lib/foreman/export/supervisord.rb +3 -13
- data/lib/foreman/export/upstart.rb +9 -27
- data/lib/foreman/process.rb +56 -67
- data/lib/foreman/procfile.rb +59 -25
- data/lib/foreman/version.rb +1 -1
- data/man/foreman.1 +4 -0
- data/spec/foreman/cli_spec.rb +38 -152
- data/spec/foreman/engine_spec.rb +46 -80
- data/spec/foreman/export/base_spec.rb +4 -7
- data/spec/foreman/export/bluepill_spec.rb +7 -6
- data/spec/foreman/export/inittab_spec.rb +7 -7
- data/spec/foreman/export/launchd_spec.rb +4 -7
- data/spec/foreman/export/runit_spec.rb +12 -17
- data/spec/foreman/export/supervisord_spec.rb +7 -56
- data/spec/foreman/export/upstart_spec.rb +18 -23
- data/spec/foreman/process_spec.rb +27 -124
- data/spec/foreman/procfile_spec.rb +26 -16
- data/spec/resources/Procfile +4 -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/export/bluepill/app-concurrency.pill +4 -4
- data/spec/resources/export/bluepill/app.pill +4 -4
- data/spec/resources/export/runit/{app-alpha-1-log-run → app-alpha-1/log/run} +0 -0
- data/spec/resources/export/runit/{app-alpha-1-run → app-alpha-1/run} +0 -0
- data/spec/resources/export/runit/{app-alpha-2-log-run → app-alpha-2/log/run} +0 -0
- data/spec/resources/export/runit/{app-alpha-2-run → app-alpha-2/run} +0 -0
- data/spec/resources/export/runit/{app-bravo-1-log-run → app-bravo-1/log/run} +0 -0
- data/spec/resources/export/runit/{app-bravo-1-run → app-bravo-1/run} +0 -0
- data/spec/resources/export/supervisord/app-alpha-1.conf +24 -0
- data/spec/resources/export/supervisord/app-alpha-2.conf +4 -4
- data/spec/spec_helper.rb +58 -6
- metadata +24 -22
- data/data/export/runit/log_run.erb +0 -7
- data/lib/foreman/color.rb +0 -40
- data/lib/foreman/procfile_entry.rb +0 -26
- data/lib/foreman/utils.rb +0 -18
- data/spec/foreman/color_spec.rb +0 -31
- data/spec/foreman/procfile_entry_spec.rb +0 -13
- data/spec/resources/export/supervisord/app-env-with-comma.conf +0 -24
- data/spec/resources/export/supervisord/app-env.conf +0 -21
- data/spec/resources/export/supervisord/app.conf +0 -24
data/bin/taskman
ADDED
data/data/example/Procfile
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
-
ticker:
|
2
|
-
error:
|
3
|
-
utf8:
|
1
|
+
ticker: ruby ./ticker $PORT
|
2
|
+
error: ruby ./error
|
3
|
+
utf8: ruby ./utf8
|
4
|
+
spawner: ./spawner
|
@@ -3,25 +3,25 @@ Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/
|
|
3
3
|
app.uid = "<%= user %>"
|
4
4
|
app.gid = "<%= user %>"
|
5
5
|
|
6
|
-
<% engine.
|
7
|
-
<% 1.upto(
|
8
|
-
<% port = engine.port_for(process, num
|
9
|
-
app.process("<%=
|
10
|
-
process.start_command = "<%= process.command
|
6
|
+
<% engine.each_process do |name, process| %>
|
7
|
+
<% 1.upto(engine.formation[name]) do |num| %>
|
8
|
+
<% port = engine.port_for(process, num) %>
|
9
|
+
app.process("<%= name %>-<%= num %>") do |process|
|
10
|
+
process.start_command = "<%= process.command %>"
|
11
11
|
|
12
|
-
process.working_dir = "<%= engine.
|
12
|
+
process.working_dir = "<%= engine.root %>"
|
13
13
|
process.daemonize = true
|
14
|
-
process.environment =
|
14
|
+
process.environment = <%= engine.env.merge("PORT" => port.to_s).inspect %>
|
15
15
|
process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
|
16
16
|
process.stop_grace_time = 45.seconds
|
17
17
|
|
18
|
-
process.stdout = process.stderr = "<%=
|
18
|
+
process.stdout = process.stderr = "<%= log %>/<%= app %>-<%= name %>-<%= num %>.log"
|
19
19
|
|
20
20
|
process.monitor_children do |children|
|
21
|
-
children.stop_command "kill
|
21
|
+
children.stop_command "kill {{PID}}"
|
22
22
|
end
|
23
23
|
|
24
|
-
process.group = "<%= app %>-<%=
|
24
|
+
process.group = "<%= app %>-<%= name %>"
|
25
25
|
end
|
26
26
|
<% end %>
|
27
27
|
<% end %>
|
@@ -3,7 +3,7 @@
|
|
3
3
|
<plist version="1.0">
|
4
4
|
<dict>
|
5
5
|
<key>Label</key>
|
6
|
-
<string><%= "#{app}-#{
|
6
|
+
<string><%= "#{app}-#{name}-#{num}" %></string>
|
7
7
|
<key>ProgramArguments</key>
|
8
8
|
<array>
|
9
9
|
<string><%= process.command %></string>
|
@@ -13,10 +13,10 @@
|
|
13
13
|
<key>RunAtLoad</key>
|
14
14
|
<true/>
|
15
15
|
<key>StandardErrorPath</key>
|
16
|
-
<string><%=
|
16
|
+
<string><%= log %>/<%= app %>-<%= name %>-<%=num%>.log</string>
|
17
17
|
<key>UserName</key>
|
18
18
|
<string><%= user %></string>
|
19
19
|
<key>WorkingDirectory</key>
|
20
|
-
<string><%= engine.
|
20
|
+
<string><%= engine.root %></string>
|
21
21
|
</dict>
|
22
22
|
</plist>
|
data/data/export/runit/run.erb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
#!/bin/sh
|
2
|
-
cd <%= engine.
|
3
|
-
exec chpst -u <%= user %> -e <%=
|
2
|
+
cd <%= engine.root %>
|
3
|
+
exec chpst -u <%= user %> -e <%= File.join(location, "#{process_directory}/env") %> <%= process.command %>
|
@@ -1,23 +1,23 @@
|
|
1
1
|
<%
|
2
2
|
app_names = []
|
3
|
-
engine.
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
app_names <<
|
3
|
+
engine.each_process do |name, process|
|
4
|
+
1.upto(engine.formation[name]) do |num|
|
5
|
+
port = engine.port_for(process, num)
|
6
|
+
full_name = "#{app}-#{name}-#{num}"
|
7
|
+
environment = engine.env.merge("PORT" => port.to_s).map do |key, value|
|
8
|
+
"#{key}=#{shell_quote(value)}"
|
9
|
+
end
|
10
|
+
app_names << full_name
|
11
11
|
%>
|
12
|
-
[program:<%=
|
12
|
+
[program:<%= full_name %>]
|
13
13
|
command=<%= process.command %>
|
14
14
|
autostart=true
|
15
15
|
autorestart=true
|
16
16
|
stopsignal=QUIT
|
17
|
-
stdout_logfile=<%=
|
18
|
-
stderr_logfile=<%=
|
17
|
+
stdout_logfile=<%= log %>/<%= name %>-<%= num %>.log
|
18
|
+
stderr_logfile=<%= log %>/<%= name %>-<%= num %>.error.log
|
19
19
|
user=<%= user %>
|
20
|
-
directory=<%= engine.
|
20
|
+
directory=<%= engine.root %>
|
21
21
|
environment=<%= environment.join(',') %><%
|
22
22
|
end
|
23
23
|
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
start on starting <%= app %>-<%=
|
2
|
-
stop on stopping <%= app %>-<%=
|
1
|
+
start on starting <%= app %>-<%= name %>
|
2
|
+
stop on stopping <%= app %>-<%= name %>
|
3
3
|
respawn
|
4
4
|
|
5
|
-
exec su - <%= user %> -c 'cd <%= engine.
|
5
|
+
exec su - <%= user %> -c 'cd <%= engine.root %>; export PORT=<%= port %>;<% engine.env.each_pair do |var,env| %> export <%= var.upcase %>=<%= shell_quote(env) %>; <% end %> <%= process.command %> >> <%= log %>/<%=name%>-<%=num%>.log 2>&1'
|
data/lib/foreman/cli.rb
CHANGED
@@ -1,36 +1,37 @@
|
|
1
1
|
require "foreman"
|
2
2
|
require "foreman/helpers"
|
3
3
|
require "foreman/engine"
|
4
|
+
require "foreman/engine/cli"
|
4
5
|
require "foreman/export"
|
5
6
|
require "shellwords"
|
6
7
|
require "thor"
|
7
|
-
require "yaml"
|
8
8
|
|
9
9
|
class Foreman::CLI < Thor
|
10
|
+
|
10
11
|
include Foreman::Helpers
|
11
12
|
|
12
13
|
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
|
14
|
+
class_option :root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
|
13
15
|
|
14
16
|
desc "start [PROCESS]", "Start the application (or a specific PROCESS)"
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
|
20
|
-
method_option :port, :type => :numeric, :aliases => "-p"
|
21
|
-
method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
|
18
|
+
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
|
19
|
+
method_option :formation, :type => :string, :aliases => "-m", :banner => '"alpha=5,bar=3"'
|
20
|
+
method_option :port, :type => :numeric, :aliases => "-p"
|
22
21
|
|
23
22
|
class << self
|
24
23
|
# Hackery. Take the run method away from Thor so that we can redefine it.
|
25
24
|
def is_thor_reserved_word?(word, type)
|
26
|
-
return false if word ==
|
25
|
+
return false if word == "run"
|
27
26
|
super
|
28
27
|
end
|
29
28
|
end
|
30
29
|
|
31
30
|
def start(process=nil)
|
32
31
|
check_procfile!
|
33
|
-
|
32
|
+
load_environment!
|
33
|
+
engine.load_procfile(procfile)
|
34
|
+
engine.options[:formation] = "#{process}=1" if process
|
34
35
|
engine.start
|
35
36
|
end
|
36
37
|
|
@@ -46,6 +47,8 @@ class Foreman::CLI < Thor
|
|
46
47
|
|
47
48
|
def export(format, location=nil)
|
48
49
|
check_procfile!
|
50
|
+
load_environment!
|
51
|
+
engine.load_procfile(procfile)
|
49
52
|
formatter = Foreman::Export.formatter(format)
|
50
53
|
formatter.new(location, engine, options).export
|
51
54
|
rescue Foreman::Export::Exception => ex
|
@@ -56,16 +59,19 @@ class Foreman::CLI < Thor
|
|
56
59
|
|
57
60
|
def check
|
58
61
|
check_procfile!
|
59
|
-
|
60
|
-
|
62
|
+
engine.load_procfile(procfile)
|
63
|
+
error "no processes defined" unless engine.processes.length > 0
|
64
|
+
puts "valid procfile detected (#{engine.process_names.join(', ')})"
|
61
65
|
end
|
62
66
|
|
63
67
|
desc "run COMMAND [ARGS...]", "Run a command using your application's environment"
|
64
68
|
|
69
|
+
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
|
70
|
+
|
65
71
|
def run(*args)
|
66
|
-
|
72
|
+
load_environment!
|
67
73
|
begin
|
68
|
-
exec args.shelljoin
|
74
|
+
exec engine.env, args.shelljoin
|
69
75
|
rescue Errno::EACCES
|
70
76
|
error "not executable: #{args.first}"
|
71
77
|
rescue Errno::ENOENT
|
@@ -73,33 +79,55 @@ class Foreman::CLI < Thor
|
|
73
79
|
end
|
74
80
|
end
|
75
81
|
|
82
|
+
no_tasks do
|
83
|
+
def engine
|
84
|
+
@engine ||= begin
|
85
|
+
engine_class = Foreman::Engine::CLI
|
86
|
+
engine = engine_class.new(
|
87
|
+
:formation => options[:formation],
|
88
|
+
:port => options[:port],
|
89
|
+
:root => options[:root]
|
90
|
+
)
|
91
|
+
engine
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
76
96
|
private ######################################################################
|
77
97
|
|
98
|
+
def error(message)
|
99
|
+
puts "ERROR: #{message}"
|
100
|
+
exit 1
|
101
|
+
end
|
102
|
+
|
78
103
|
def check_procfile!
|
79
104
|
error("#{procfile} does not exist.") unless File.exist?(procfile)
|
80
105
|
end
|
81
106
|
|
82
|
-
def
|
83
|
-
|
107
|
+
def load_environment!
|
108
|
+
if options[:env]
|
109
|
+
options[:env].split(",").each do |file|
|
110
|
+
engine.load_env file
|
111
|
+
end
|
112
|
+
else
|
113
|
+
default_env = File.join(engine.root, ".env")
|
114
|
+
engine.load_env default_env if File.exists?(default_env)
|
115
|
+
end
|
84
116
|
end
|
85
117
|
|
86
118
|
def procfile
|
87
119
|
case
|
88
120
|
when options[:procfile] then options[:procfile]
|
89
|
-
when options[:
|
121
|
+
when options[:root] then File.expand_path(File.join(options[:app_root], "Procfile"))
|
90
122
|
else "Procfile"
|
91
123
|
end
|
92
124
|
end
|
93
125
|
|
94
|
-
def error(message)
|
95
|
-
puts "ERROR: #{message}"
|
96
|
-
exit 1
|
97
|
-
end
|
98
|
-
|
99
126
|
def options
|
100
127
|
original_options = super
|
101
128
|
return original_options unless File.exists?(".foreman")
|
102
129
|
defaults = YAML::load_file(".foreman") || {}
|
103
130
|
Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
|
104
131
|
end
|
132
|
+
|
105
133
|
end
|
data/lib/foreman/engine.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
require "foreman"
|
2
|
-
require "foreman/
|
2
|
+
require "foreman/env"
|
3
3
|
require "foreman/process"
|
4
4
|
require "foreman/procfile"
|
5
|
-
require "foreman/utils"
|
6
5
|
require "tempfile"
|
7
6
|
require "timeout"
|
8
7
|
require "fileutils"
|
@@ -10,219 +9,280 @@ require "thread"
|
|
10
9
|
|
11
10
|
class Foreman::Engine
|
12
11
|
|
13
|
-
attr_reader :
|
14
|
-
attr_reader :procfile
|
15
|
-
attr_reader :directory
|
12
|
+
attr_reader :env
|
16
13
|
attr_reader :options
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
14
|
+
attr_reader :processes
|
15
|
+
|
16
|
+
# Create an +Engine+ for running processes
|
17
|
+
#
|
18
|
+
# @param [Hash] options
|
19
|
+
#
|
20
|
+
# @option options [String] :formation (all=1) The process formation to use
|
21
|
+
# @option options [Fixnum] :port (5000) The base port to assign to processes
|
22
|
+
# @option options [String] :root (Dir.pwd) The root directory from which to run processes
|
23
|
+
#
|
24
|
+
def initialize(options={})
|
26
25
|
@options = options.dup
|
27
|
-
@output_mutex = Mutex.new
|
28
26
|
|
29
|
-
@options[:
|
30
|
-
|
27
|
+
@options[:formation] ||= "all=1"
|
28
|
+
|
29
|
+
@env = {}
|
30
|
+
@mutex = Mutex.new
|
31
|
+
@names = {}
|
32
|
+
@processes = []
|
33
|
+
@running = {}
|
34
|
+
@readers = {}
|
31
35
|
end
|
32
36
|
|
37
|
+
# Start the processes registered to this +Engine+
|
38
|
+
#
|
33
39
|
def start
|
34
|
-
proctitle "ruby: foreman master"
|
35
|
-
termtitle "#{File.basename(@directory)} - foreman"
|
36
|
-
|
37
40
|
trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
|
38
41
|
trap("INT") { puts "SIGINT received"; terminate_gracefully }
|
39
42
|
trap("HUP") { puts "SIGHUP received"; terminate_gracefully }
|
40
43
|
|
41
|
-
|
44
|
+
startup
|
42
45
|
spawn_processes
|
43
46
|
watch_for_output
|
44
|
-
|
47
|
+
sleep 0.1
|
48
|
+
watch_for_termination { terminate_gracefully }
|
49
|
+
shutdown
|
45
50
|
end
|
46
51
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
52
|
+
# Register a process to be run by this +Engine+
|
53
|
+
#
|
54
|
+
# @param [String] name A name for this process
|
55
|
+
# @param [String] command The command to run
|
56
|
+
# @param [Hash] options
|
57
|
+
#
|
58
|
+
# @option options [Hash] :env A custom environment for this process
|
59
|
+
#
|
60
|
+
def register(name, command, options={})
|
61
|
+
options[:env] ||= env
|
62
|
+
options[:cwd] ||= File.dirname(command.split(" ").first)
|
63
|
+
process = Foreman::Process.new(command, options)
|
64
|
+
@names[process] = name
|
65
|
+
@processes << process
|
51
66
|
end
|
52
67
|
|
53
|
-
|
54
|
-
|
68
|
+
# Clear the processes registered to this +Engine+
|
69
|
+
#
|
70
|
+
def clear
|
71
|
+
@names = {}
|
72
|
+
@processes = []
|
55
73
|
end
|
56
74
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
when /\A"(.*)"\z/ then hash[key] = $1.gsub(/\\(.)/, '\1')
|
66
|
-
else hash[key] = val
|
67
|
-
end
|
68
|
-
end
|
69
|
-
hash
|
75
|
+
# Register processes by reading a Procfile
|
76
|
+
#
|
77
|
+
# @param [String] filename A Procfile from which to read processes to register
|
78
|
+
#
|
79
|
+
def load_procfile(filename)
|
80
|
+
options[:root] ||= File.dirname(filename)
|
81
|
+
Foreman::Procfile.new(filename).entries do |name, command|
|
82
|
+
register name, command, :cwd => options[:root]
|
70
83
|
end
|
84
|
+
self
|
71
85
|
end
|
72
86
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
87
|
+
# Load a .env file into the +env+ for this +Engine+
|
88
|
+
#
|
89
|
+
# @param [String] filename A .env file to load into the environment
|
90
|
+
#
|
91
|
+
def load_env(filename)
|
92
|
+
Foreman::Env.new(filename).entries do |name, value|
|
93
|
+
@env[name] = value
|
94
|
+
end
|
95
|
+
end
|
77
96
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
97
|
+
# Send a signal to all processesstarted by this +Engine+
|
98
|
+
#
|
99
|
+
# @param [String] signal The signal to send to each process
|
100
|
+
#
|
101
|
+
def killall(signal="SIGTERM")
|
102
|
+
@running.each do |pid, (process, index)|
|
103
|
+
system "sending #{signal} to #{name_for(pid)} at pid #{pid}"
|
104
|
+
begin
|
105
|
+
Process.kill(signal, -1 * pid)
|
106
|
+
rescue Errno::ESRCH, Errno::EPERM
|
83
107
|
end
|
84
108
|
end
|
85
109
|
end
|
86
110
|
|
87
|
-
|
88
|
-
|
111
|
+
# Get the process formation
|
112
|
+
#
|
113
|
+
# @returns [Fixnum] The formation count for the specified process
|
114
|
+
#
|
115
|
+
def formation
|
116
|
+
@formation ||= parse_formation(options[:formation])
|
89
117
|
end
|
90
118
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
119
|
+
# List the available process names
|
120
|
+
#
|
121
|
+
# @returns [Array] A list of process names
|
122
|
+
#
|
123
|
+
def process_names
|
124
|
+
@processes.map { |p| @names[p] }
|
96
125
|
end
|
97
126
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
process = running_processes.delete(pid)
|
107
|
-
info "process terminated", process.name
|
108
|
-
end
|
109
|
-
end
|
110
|
-
rescue Timeout::Error
|
111
|
-
info "sending SIGKILL to all processes"
|
112
|
-
kill_all "SIGKILL"
|
113
|
-
end
|
114
|
-
|
115
|
-
def poll_readers
|
116
|
-
rs, ws = IO.select(readers.values, [], [], 1)
|
117
|
-
(rs || []).each do |r|
|
118
|
-
data = r.gets
|
119
|
-
next unless data
|
120
|
-
data.force_encoding("BINARY") if data.respond_to?(:force_encoding)
|
121
|
-
ps, message = data.split(",", 2)
|
122
|
-
color = colors[ps.split(".").first]
|
123
|
-
info message, ps, color
|
124
|
-
end
|
127
|
+
# Get the +Process+ for a specifid name
|
128
|
+
#
|
129
|
+
# @param [String] name The process name
|
130
|
+
#
|
131
|
+
# @returns [Foreman::Process] The +Process+ for the specified name
|
132
|
+
#
|
133
|
+
def process(name)
|
134
|
+
@names.invert[name]
|
125
135
|
end
|
126
136
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
poll_readers
|
133
|
-
end
|
134
|
-
rescue Exception => ex
|
135
|
-
puts ex.message
|
136
|
-
puts ex.backtrace
|
137
|
-
end
|
137
|
+
# Yield each +Process+ in order
|
138
|
+
#
|
139
|
+
def each_process
|
140
|
+
process_names.each do |name|
|
141
|
+
yield name, process(name)
|
138
142
|
end
|
139
143
|
end
|
140
144
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
145
|
+
# Get the root directory for this +Engine+
|
146
|
+
#
|
147
|
+
# @returns [String] The root directory
|
148
|
+
#
|
149
|
+
def root
|
150
|
+
File.expand_path(options[:root] || Dir.pwd)
|
147
151
|
end
|
148
152
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
153
|
+
# Get the port for a given process and offset
|
154
|
+
#
|
155
|
+
# @param [Foreman::Process] process A +Process+ associated with this engine
|
156
|
+
# @param [Fixnum] instance The instance of the process
|
157
|
+
#
|
158
|
+
# @returns [Fixnum] port The port to use for this instance of this process
|
159
|
+
#
|
160
|
+
def port_for(process, instance)
|
161
|
+
base_port + (@processes.index(process) * 100) + (instance - 1)
|
156
162
|
end
|
157
163
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
164
|
+
private
|
165
|
+
|
166
|
+
### Engine API ######################################################
|
167
|
+
|
168
|
+
def startup
|
169
|
+
raise TypeError, "must use a subclass of Foreman::Engine"
|
162
170
|
end
|
163
171
|
|
164
|
-
def
|
165
|
-
|
166
|
-
$stdout.puts message
|
167
|
-
end
|
172
|
+
def output(name, data)
|
173
|
+
raise TypeError, "must use a subclass of Foreman::Engine"
|
168
174
|
end
|
169
175
|
|
170
|
-
def
|
171
|
-
|
172
|
-
longest = procfile.process_names.map { |name| name.length }.sort.last
|
173
|
-
longest = 6 if longest < 6 # system
|
174
|
-
longest
|
175
|
-
end
|
176
|
+
def shutdown
|
177
|
+
raise TypeError, "must use a subclass of Foreman::Engine"
|
176
178
|
end
|
177
179
|
|
178
|
-
|
179
|
-
|
180
|
+
## Helpers ##########################################################
|
181
|
+
|
182
|
+
def base_port
|
183
|
+
(options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i
|
180
184
|
end
|
181
185
|
|
182
|
-
def
|
183
|
-
|
186
|
+
def create_pipe
|
187
|
+
IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
|
184
188
|
end
|
185
189
|
|
186
|
-
def
|
187
|
-
|
190
|
+
def name_for(pid)
|
191
|
+
process, index = @running[pid]
|
192
|
+
[ @names[process], index.to_s ].compact.join(".")
|
188
193
|
end
|
189
194
|
|
190
|
-
def
|
191
|
-
@
|
195
|
+
def parse_formation(formation)
|
196
|
+
pairs = @options[:formation].to_s.gsub(/\s/, "").split(",")
|
197
|
+
|
198
|
+
pairs.inject(Hash.new(0)) do |ax, pair|
|
199
|
+
process, amount = pair.split("=")
|
200
|
+
process == "all" ? ax.default = amount.to_i : ax[process] = amount.to_i
|
201
|
+
ax
|
202
|
+
end
|
192
203
|
end
|
193
204
|
|
194
|
-
def
|
195
|
-
@
|
205
|
+
def output_with_mutex(name, message)
|
206
|
+
@mutex.synchronize do
|
207
|
+
output name, message
|
208
|
+
end
|
196
209
|
end
|
197
210
|
|
198
|
-
def
|
199
|
-
|
211
|
+
def system(message)
|
212
|
+
output_with_mutex "system", message
|
200
213
|
end
|
201
214
|
|
202
|
-
def
|
203
|
-
|
204
|
-
|
215
|
+
def termination_message_for(status)
|
216
|
+
if status.exited?
|
217
|
+
"exited with code #{status.exitstatus}"
|
218
|
+
elsif status.signaled?
|
219
|
+
"terminated by SIG#{Signal.list.invert[status.termsig]}"
|
220
|
+
else
|
221
|
+
"died a mysterious death"
|
205
222
|
end
|
206
223
|
end
|
207
224
|
|
208
|
-
def
|
209
|
-
|
225
|
+
def flush_reader(reader)
|
226
|
+
until reader.eof?
|
227
|
+
data = reader.gets
|
228
|
+
output_with_mutex name_for(@readers.key(reader)), data
|
229
|
+
end
|
210
230
|
end
|
211
231
|
|
212
|
-
|
213
|
-
environment = {}
|
232
|
+
## Engine ###########################################################
|
214
233
|
|
215
|
-
|
216
|
-
|
217
|
-
|
234
|
+
def spawn_processes
|
235
|
+
@processes.each do |process|
|
236
|
+
1.upto(formation[@names[process]]) do |n|
|
237
|
+
reader, writer = create_pipe
|
238
|
+
begin
|
239
|
+
pid = process.run(:output => writer, :env => { "PORT" => port_for(process, n).to_s })
|
240
|
+
writer.puts "started with pid #{pid}"
|
241
|
+
rescue Errno::ENOENT
|
242
|
+
writer.puts "unknown command: #{process.command}"
|
243
|
+
end
|
244
|
+
@running[pid] = [process, n]
|
245
|
+
@readers[pid] = reader
|
246
|
+
end
|
218
247
|
end
|
248
|
+
end
|
219
249
|
|
220
|
-
|
250
|
+
def watch_for_output
|
251
|
+
Thread.new do
|
252
|
+
begin
|
253
|
+
loop do
|
254
|
+
(IO.select(@readers.values).first || []).each do |reader|
|
255
|
+
data = reader.gets
|
256
|
+
output_with_mutex name_for(@readers.key(reader)), data
|
257
|
+
end
|
258
|
+
end
|
259
|
+
rescue Exception => ex
|
260
|
+
puts ex.message
|
261
|
+
puts ex.backtrace
|
262
|
+
end
|
263
|
+
end
|
221
264
|
end
|
222
265
|
|
223
|
-
def
|
224
|
-
|
225
|
-
|
266
|
+
def watch_for_termination
|
267
|
+
pid, status = Process.wait2
|
268
|
+
output_with_mutex name_for(pid), termination_message_for(status)
|
269
|
+
@running.delete(pid)
|
270
|
+
yield if block_given?
|
271
|
+
pid
|
272
|
+
rescue Errno::ECHILD
|
273
|
+
end
|
274
|
+
|
275
|
+
def terminate_gracefully
|
276
|
+
return if @terminating
|
277
|
+
@terminating = true
|
278
|
+
system "sending SIGTERM to all processes"
|
279
|
+
killall "SIGTERM"
|
280
|
+
Timeout.timeout(5) do
|
281
|
+
watch_for_termination while @running.length > 0
|
282
|
+
end
|
283
|
+
rescue Timeout::Error
|
284
|
+
system "sending SIGKILL to all processes"
|
285
|
+
killall "SIGKILL"
|
226
286
|
end
|
227
287
|
|
228
288
|
end
|