HeSYINUvSBZfxqA-foreman 0.20.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +30 -0
- data/bin/foreman +7 -0
- data/bin/foreman-runner +2 -0
- data/data/example/Procfile +2 -0
- data/data/example/Procfile.without_colon +2 -0
- data/data/example/error +5 -0
- data/data/example/log/neverdie.log +4 -0
- data/data/example/ticker +12 -0
- data/data/export/upstart/master.conf.erb +8 -0
- data/data/export/upstart/process.conf.erb +5 -0
- data/data/export/upstart/process_master.conf.erb +2 -0
- data/lib/foreman.rb +8 -0
- data/lib/foreman/cli.rb +94 -0
- data/lib/foreman/engine.rb +233 -0
- data/lib/foreman/export.rb +9 -0
- data/lib/foreman/export/base.rb +44 -0
- data/lib/foreman/export/inittab.rb +38 -0
- data/lib/foreman/export/upstart.rb +42 -0
- data/lib/foreman/process.rb +14 -0
- data/lib/foreman/utils.rb +15 -0
- data/lib/foreman/version.rb +5 -0
- data/man/foreman.1 +212 -0
- data/spec/foreman/cli_spec.rb +84 -0
- data/spec/foreman/engine_spec.rb +89 -0
- data/spec/foreman/export/upstart_spec.rb +55 -0
- data/spec/foreman/export_spec.rb +2 -0
- data/spec/foreman/process_spec.rb +2 -0
- data/spec/foreman_spec.rb +11 -0
- data/spec/resources/export/upstart/app-alpha-1.conf +5 -0
- data/spec/resources/export/upstart/app-alpha-2.conf +5 -0
- data/spec/resources/export/upstart/app-alpha.conf +2 -0
- data/spec/resources/export/upstart/app-bravo-1.conf +5 -0
- data/spec/resources/export/upstart/app-bravo.conf +2 -0
- data/spec/resources/export/upstart/app.conf +8 -0
- data/spec/spec_helper.rb +60 -0
- metadata +238 -0
data/README.markdown
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Foreman
|
2
|
+
|
3
|
+
## Installation
|
4
|
+
|
5
|
+
gem install foreman
|
6
|
+
|
7
|
+
## Description
|
8
|
+
|
9
|
+
http://blog.daviddollar.org/2011/05/06/introducing-foreman.html
|
10
|
+
|
11
|
+
## Manual
|
12
|
+
|
13
|
+
See the [man page](http://ddollar.github.com/foreman) for usage.
|
14
|
+
|
15
|
+
## Authorship
|
16
|
+
|
17
|
+
Created by David Dollar
|
18
|
+
|
19
|
+
Patches contributed by:
|
20
|
+
|
21
|
+
* Adam Wiggins
|
22
|
+
* clifff
|
23
|
+
* Dan Peterson
|
24
|
+
* Jay Zeschin
|
25
|
+
* Keith Rarick
|
26
|
+
* Ricardo Chimal, Jr
|
27
|
+
|
28
|
+
## License
|
29
|
+
|
30
|
+
MIT
|
data/bin/foreman
ADDED
data/bin/foreman-runner
ADDED
data/data/example/error
ADDED
data/data/example/ticker
ADDED
data/lib/foreman.rb
ADDED
data/lib/foreman/cli.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
require "foreman"
|
2
|
+
require "foreman/engine"
|
3
|
+
require "foreman/export"
|
4
|
+
require "thor"
|
5
|
+
require "yaml"
|
6
|
+
|
7
|
+
class Foreman::CLI < Thor
|
8
|
+
|
9
|
+
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
|
10
|
+
|
11
|
+
desc "start [PROCESS]", "Start the application, or a specific process"
|
12
|
+
|
13
|
+
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
|
14
|
+
method_option :port, :type => :numeric, :aliases => "-p"
|
15
|
+
method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
|
16
|
+
|
17
|
+
def start(process=nil)
|
18
|
+
check_procfile!
|
19
|
+
|
20
|
+
if process
|
21
|
+
engine.execute(process, options)
|
22
|
+
else
|
23
|
+
engine.start(options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "export FORMAT LOCATION", "Export the application to another process management format"
|
28
|
+
|
29
|
+
method_option :app, :type => :string, :aliases => "-a"
|
30
|
+
method_option :log, :type => :string, :aliases => "-l"
|
31
|
+
method_option :port, :type => :numeric, :aliases => "-p"
|
32
|
+
method_option :user, :type => :string, :aliases => "-u"
|
33
|
+
method_option :template, :type => :string, :aliases => "-t"
|
34
|
+
method_option :concurrency, :type => :string, :aliases => "-c",
|
35
|
+
:banner => '"alpha=5,bar=3"'
|
36
|
+
|
37
|
+
def export(format, location=nil)
|
38
|
+
check_procfile!
|
39
|
+
|
40
|
+
formatter = case format
|
41
|
+
when "inittab" then Foreman::Export::Inittab
|
42
|
+
when "upstart" then Foreman::Export::Upstart
|
43
|
+
else error "Unknown export format: #{format}."
|
44
|
+
end
|
45
|
+
|
46
|
+
formatter.new(engine).export(location, options)
|
47
|
+
|
48
|
+
rescue Foreman::Export::Exception => ex
|
49
|
+
error ex.message
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "check", "Validate your application's Procfile"
|
53
|
+
|
54
|
+
def check
|
55
|
+
processes = engine.processes_in_order.map { |p| p.first }
|
56
|
+
error "no processes defined" unless processes.length > 0
|
57
|
+
display "valid procfile detected (#{processes.join(', ')})"
|
58
|
+
end
|
59
|
+
|
60
|
+
private ######################################################################
|
61
|
+
|
62
|
+
def check_procfile!
|
63
|
+
error("#{procfile} does not exist.") unless File.exist?(procfile)
|
64
|
+
end
|
65
|
+
|
66
|
+
def engine
|
67
|
+
@engine ||= Foreman::Engine.new(procfile)
|
68
|
+
end
|
69
|
+
|
70
|
+
def procfile
|
71
|
+
options[:procfile] || "Procfile"
|
72
|
+
end
|
73
|
+
|
74
|
+
def display(message)
|
75
|
+
puts message
|
76
|
+
end
|
77
|
+
|
78
|
+
def error(message)
|
79
|
+
puts "ERROR: #{message}"
|
80
|
+
exit 1
|
81
|
+
end
|
82
|
+
|
83
|
+
def procfile_exists?(procfile)
|
84
|
+
File.exist?(procfile)
|
85
|
+
end
|
86
|
+
|
87
|
+
def options
|
88
|
+
original_options = super
|
89
|
+
return original_options unless File.exists?(".foreman")
|
90
|
+
defaults = YAML::load_file(".foreman") || {}
|
91
|
+
Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require "foreman"
|
2
|
+
require "foreman/process"
|
3
|
+
require "foreman/utils"
|
4
|
+
require "pty"
|
5
|
+
require "tempfile"
|
6
|
+
require "timeout"
|
7
|
+
require "term/ansicolor"
|
8
|
+
require "fileutils"
|
9
|
+
|
10
|
+
class Foreman::Engine
|
11
|
+
|
12
|
+
attr_reader :procfile
|
13
|
+
attr_reader :directory
|
14
|
+
|
15
|
+
extend Term::ANSIColor
|
16
|
+
|
17
|
+
COLORS = [ cyan, yellow, green, magenta, red ]
|
18
|
+
|
19
|
+
def initialize(procfile)
|
20
|
+
@procfile = read_procfile(procfile)
|
21
|
+
@directory = File.expand_path(File.dirname(procfile))
|
22
|
+
end
|
23
|
+
|
24
|
+
def processes
|
25
|
+
@processes ||= begin
|
26
|
+
@order = []
|
27
|
+
procfile.split("\n").inject({}) do |hash, line|
|
28
|
+
next hash if line.strip == ""
|
29
|
+
name, command = line.split(/ *: +/, 2)
|
30
|
+
unless command
|
31
|
+
warn_deprecated_procfile!
|
32
|
+
name, command = line.split(/ +/, 2)
|
33
|
+
end
|
34
|
+
process = Foreman::Process.new(name, command)
|
35
|
+
process.color = next_color
|
36
|
+
@order << process.name
|
37
|
+
hash.update(process.name => process)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def process_order
|
43
|
+
processes
|
44
|
+
@order.uniq
|
45
|
+
end
|
46
|
+
|
47
|
+
def processes_in_order
|
48
|
+
process_order.map do |name|
|
49
|
+
[name, processes[name]]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def start(options={})
|
54
|
+
environment = read_environment(options[:env])
|
55
|
+
|
56
|
+
proctitle "ruby: foreman master"
|
57
|
+
|
58
|
+
processes_in_order.each do |name, process|
|
59
|
+
fork process, options, environment
|
60
|
+
end
|
61
|
+
|
62
|
+
trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
|
63
|
+
trap("INT") { puts "SIGINT received"; terminate_gracefully }
|
64
|
+
|
65
|
+
watch_for_termination
|
66
|
+
end
|
67
|
+
|
68
|
+
def execute(name, options={})
|
69
|
+
environment = read_environment(options[:env])
|
70
|
+
|
71
|
+
fork processes[name], options, environment
|
72
|
+
|
73
|
+
trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
|
74
|
+
trap("INT") { puts "SIGINT received"; terminate_gracefully }
|
75
|
+
|
76
|
+
watch_for_termination
|
77
|
+
end
|
78
|
+
|
79
|
+
def port_for(process, num, base_port=nil)
|
80
|
+
base_port ||= 5000
|
81
|
+
offset = processes_in_order.map { |p| p.first }.index(process.name) * 100
|
82
|
+
base_port.to_i + offset + num - 1
|
83
|
+
end
|
84
|
+
|
85
|
+
private ######################################################################
|
86
|
+
|
87
|
+
def fork(process, options={}, environment={})
|
88
|
+
concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
|
89
|
+
|
90
|
+
1.upto(concurrency[process.name]) do |num|
|
91
|
+
fork_individual(process, num, port_for(process, num, options[:port]), environment)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def fork_individual(process, num, port, environment)
|
96
|
+
environment.each { |k,v| ENV[k] = v }
|
97
|
+
|
98
|
+
ENV["PORT"] = port.to_s
|
99
|
+
ENV["PS"] = "#{process.name}.#{num}"
|
100
|
+
|
101
|
+
pid = Process.fork do
|
102
|
+
run(process)
|
103
|
+
end
|
104
|
+
|
105
|
+
info "started with pid #{pid}", process
|
106
|
+
running_processes[pid] = process
|
107
|
+
end
|
108
|
+
|
109
|
+
def run(process)
|
110
|
+
proctitle "ruby: foreman #{process.name}"
|
111
|
+
trap("SIGINT", "IGNORE")
|
112
|
+
|
113
|
+
begin
|
114
|
+
Dir.chdir directory do
|
115
|
+
PTY.spawn(runner, process.command) do |stdin, stdout, pid|
|
116
|
+
trap("SIGTERM") { Process.kill("SIGTERM", pid) }
|
117
|
+
until stdin.eof?
|
118
|
+
info stdin.gets, process
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
rescue PTY::ChildExited, Interrupt, Errno::EIO
|
123
|
+
begin
|
124
|
+
info "process exiting", process
|
125
|
+
rescue Interrupt
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def kill_all(signal="SIGTERM")
|
131
|
+
running_processes.each do |pid, process|
|
132
|
+
Process.kill(signal, pid) rescue Errno::ESRCH
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def info(message, process=nil)
|
137
|
+
print process.color if process
|
138
|
+
print "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(process)} | "
|
139
|
+
print Term::ANSIColor.reset
|
140
|
+
print message.chomp
|
141
|
+
puts
|
142
|
+
end
|
143
|
+
|
144
|
+
def error(message)
|
145
|
+
puts "ERROR: #{message}"
|
146
|
+
exit 1
|
147
|
+
end
|
148
|
+
|
149
|
+
def longest_process_name
|
150
|
+
@longest_process_name ||= begin
|
151
|
+
longest = processes.keys.map { |name| name.length }.sort.last
|
152
|
+
longest = 6 if longest < 6 # system
|
153
|
+
longest
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def pad_process_name(process)
|
158
|
+
name = process ? "#{ENV["PS"]}" : "system"
|
159
|
+
name.ljust(longest_process_name + 3) # add 3 for process number padding
|
160
|
+
end
|
161
|
+
|
162
|
+
def print_info
|
163
|
+
info "currently running processes:"
|
164
|
+
running_processes.each do |pid, process|
|
165
|
+
info "pid #{pid}", process
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def proctitle(title)
|
170
|
+
$0 = title
|
171
|
+
end
|
172
|
+
|
173
|
+
def read_procfile(procfile)
|
174
|
+
File.read(procfile)
|
175
|
+
end
|
176
|
+
|
177
|
+
def watch_for_termination
|
178
|
+
pid, status = Process.wait2
|
179
|
+
process = running_processes.delete(pid)
|
180
|
+
info "process terminated", process
|
181
|
+
terminate_gracefully
|
182
|
+
kill_all
|
183
|
+
rescue Errno::ECHILD
|
184
|
+
end
|
185
|
+
|
186
|
+
def running_processes
|
187
|
+
@running_processes ||= {}
|
188
|
+
end
|
189
|
+
|
190
|
+
def next_color
|
191
|
+
@current_color ||= -1
|
192
|
+
@current_color += 1
|
193
|
+
@current_color >= COLORS.length ? "" : COLORS[@current_color]
|
194
|
+
end
|
195
|
+
|
196
|
+
def warn_deprecated_procfile!
|
197
|
+
return if @already_warned_deprecated
|
198
|
+
@already_warned_deprecated = true
|
199
|
+
puts "!!! This format of Procfile is deprecated, and will not work starting in v0.12"
|
200
|
+
puts "!!! Use a colon to separate the process name from the command"
|
201
|
+
puts "!!! e.g. web: thin start"
|
202
|
+
end
|
203
|
+
|
204
|
+
def read_environment(filename)
|
205
|
+
error "No such file: #{filename}" if filename && !File.exists?(filename)
|
206
|
+
filename ||= ".env"
|
207
|
+
environment = {}
|
208
|
+
|
209
|
+
if File.exists?(filename)
|
210
|
+
File.read(filename).split("\n").each do |line|
|
211
|
+
if line =~ /\A([A-Za-z_]+)=(.*)\z/
|
212
|
+
environment[$1] = $2
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
environment
|
218
|
+
end
|
219
|
+
|
220
|
+
def runner
|
221
|
+
File.expand_path("../../../bin/foreman-runner", __FILE__)
|
222
|
+
end
|
223
|
+
|
224
|
+
def terminate_gracefully
|
225
|
+
info "sending SIGTERM to all processes"
|
226
|
+
kill_all "SIGTERM"
|
227
|
+
Timeout.timeout(3) { Process.waitall }
|
228
|
+
rescue Timeout::Error
|
229
|
+
info "sending SIGKILL to all processes"
|
230
|
+
kill_all "SIGKILL"
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|