foreman 0.37.0-mingw32
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +39 -0
- data/bin/foreman +7 -0
- data/bin/runner +36 -0
- data/data/example/Procfile +2 -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/ticker +14 -0
- data/data/export/bluepill/master.pill.erb +27 -0
- data/data/export/runit/log_run.erb +7 -0
- data/data/export/runit/run.erb +3 -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 +25 -0
- data/lib/foreman/cli.rb +98 -0
- data/lib/foreman/distribution.rb +9 -0
- data/lib/foreman/engine.rb +234 -0
- data/lib/foreman/export.rb +32 -0
- data/lib/foreman/export/base.rb +51 -0
- data/lib/foreman/export/bluepill.rb +26 -0
- data/lib/foreman/export/inittab.rb +36 -0
- data/lib/foreman/export/runit.rb +59 -0
- data/lib/foreman/export/upstart.rb +41 -0
- data/lib/foreman/helpers.rb +45 -0
- data/lib/foreman/process.rb +96 -0
- data/lib/foreman/procfile.rb +38 -0
- data/lib/foreman/procfile_entry.rb +22 -0
- data/lib/foreman/utils.rb +18 -0
- data/lib/foreman/version.rb +5 -0
- data/man/foreman.1 +222 -0
- data/spec/foreman/cli_spec.rb +163 -0
- data/spec/foreman/engine_spec.rb +86 -0
- data/spec/foreman/export/base_spec.rb +22 -0
- data/spec/foreman/export/bluepill_spec.rb +36 -0
- data/spec/foreman/export/inittab_spec.rb +40 -0
- data/spec/foreman/export/runit_spec.rb +41 -0
- data/spec/foreman/export/upstart_spec.rb +87 -0
- data/spec/foreman/export_spec.rb +24 -0
- data/spec/foreman/helpers_spec.rb +26 -0
- data/spec/foreman/process_spec.rb +131 -0
- data/spec/foreman_spec.rb +34 -0
- data/spec/helper_spec.rb +18 -0
- data/spec/resources/export/bluepill/app-concurrency.pill +47 -0
- data/spec/resources/export/bluepill/app.pill +44 -0
- data/spec/resources/export/inittab/inittab.concurrency +4 -0
- data/spec/resources/export/inittab/inittab.default +4 -0
- data/spec/resources/export/runit/app-alpha-1-log-run +7 -0
- data/spec/resources/export/runit/app-alpha-1-run +3 -0
- data/spec/resources/export/runit/app-alpha-2-log-run +7 -0
- data/spec/resources/export/runit/app-alpha-2-run +3 -0
- data/spec/resources/export/runit/app-bravo-1-log-run +7 -0
- data/spec/resources/export/runit/app-bravo-1-run +3 -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 +98 -0
- metadata +138 -0
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Foreman
|
2
|
+
|
3
|
+
Manage Procfile-based applications
|
4
|
+
|
5
|
+
<table>
|
6
|
+
<tr>
|
7
|
+
<th>If you have...</th>
|
8
|
+
<th>Install with...</th>
|
9
|
+
</tr>
|
10
|
+
<tr>
|
11
|
+
<td>Ruby (MRI, JRuby, Windows)</td>
|
12
|
+
<td><pre>$ gem install foreman</pre></td>
|
13
|
+
</tr>
|
14
|
+
<tr>
|
15
|
+
<td>Mac OS X</td>
|
16
|
+
<td><a href="http://assets.foreman.io/foreman/foreman.pkg">foreman.pkg</a></td>
|
17
|
+
</tr>
|
18
|
+
</table>
|
19
|
+
|
20
|
+
## Getting Started
|
21
|
+
|
22
|
+
* http://blog.daviddollar.org/2011/05/06/introducing-foreman.html
|
23
|
+
|
24
|
+
## Documentation
|
25
|
+
|
26
|
+
* [man page](http://ddollar.github.com/foreman)
|
27
|
+
* [wiki](http://github.com/ddollar/foreman/wiki)
|
28
|
+
|
29
|
+
## Authors
|
30
|
+
|
31
|
+
#### Created and maintained by
|
32
|
+
David Dollar
|
33
|
+
|
34
|
+
#### Patches contributed by
|
35
|
+
Adam Wiggins, Chris Continanza, Chris Lowder, Craig R Webster, Dan Farina, Dan Peterson, David Dollar, Fletcher Nichol, Gabriel Burt, Gamaliel Toro, Greg Reinacker, Hugues Le Gendre, Hunter Nield, Iain Hecker, Jay Zeschin, Keith Rarick, Khaja Minhajuddin, Lincoln Stoll, Marcos Muino Garcia, Mark McGranaghan, Matt Griffin, Matt Haynes, Matthijs Langenberg, Michael Dwan, Michael van Rooijen, Mike Javorski, Nathan Broadbent, Nathan L Smith, Nick Zadrozny, Phil Hagelberg, Ricardo Chimal, Jr, Thom May, Tom Ward, brainopia, clifff, jc00ke
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
MIT
|
data/bin/foreman
ADDED
data/bin/runner
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
#
|
3
|
+
#/ Usage: runner [-d <dir>] <command>
|
4
|
+
#/
|
5
|
+
#/ Run a command with exec, optionally changing directory first
|
6
|
+
|
7
|
+
set -e
|
8
|
+
|
9
|
+
error() {
|
10
|
+
echo $@ >&2
|
11
|
+
exit 1
|
12
|
+
}
|
13
|
+
|
14
|
+
usage() {
|
15
|
+
cat $0 | grep '^#/' | cut -c4-
|
16
|
+
exit
|
17
|
+
}
|
18
|
+
|
19
|
+
while getopts ":hd:" OPT; do
|
20
|
+
case $OPT in
|
21
|
+
d) cd $OPTARG ;;
|
22
|
+
h) usage ;;
|
23
|
+
\?) error "invalid option: -$OPTARG" ;;
|
24
|
+
:) error "option -$OPTARG requires an argument" ;;
|
25
|
+
esac
|
26
|
+
done
|
27
|
+
|
28
|
+
shift $((OPTIND-1))
|
29
|
+
|
30
|
+
command=$1
|
31
|
+
|
32
|
+
if [ -z "$1" ]; then
|
33
|
+
usage
|
34
|
+
fi
|
35
|
+
|
36
|
+
exec $1
|
data/data/example/error
ADDED
data/data/example/ticker
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/bluepill.log") do |app|
|
2
|
+
|
3
|
+
app.uid = "<%= user %>"
|
4
|
+
app.gid = "<%= user %>"
|
5
|
+
|
6
|
+
<% engine.procfile.entries.each do |process| %>
|
7
|
+
<% 1.upto(concurrency[process.name]) do |num| %>
|
8
|
+
<% port = engine.port_for(process, num, self.port) %>
|
9
|
+
app.process("<%= process.name %>-<%=num%>") do |process|
|
10
|
+
process.start_command = "<%= process.command.gsub("$PORT", port.to_s) %>"
|
11
|
+
|
12
|
+
process.working_dir = "<%= engine.directory %>"
|
13
|
+
process.daemonize = true
|
14
|
+
process.environment = {"PORT" => "<%= port %>"}
|
15
|
+
process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
|
16
|
+
|
17
|
+
process.stdout = process.stderr = "<%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log"
|
18
|
+
|
19
|
+
process.monitor_children do |children|
|
20
|
+
children.stop_command "kill -QUIT {{PID}}"
|
21
|
+
end
|
22
|
+
|
23
|
+
process.group = "<%= app %>-<%= process.name %>"
|
24
|
+
end
|
25
|
+
<% end %>
|
26
|
+
<% end %>
|
27
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
start on starting <%= app %>-<%= process.name %>
|
2
|
+
stop on stopping <%= app %>-<%= process.name %>
|
3
|
+
respawn
|
4
|
+
|
5
|
+
exec su - <%= user %> -c 'cd <%= engine.directory %>; export PORT=<%= port %>;<% engine.environment.each_pair do |var,env| %> export <%= var.upcase %>=<%= env %>; <% end %> <%= process.command %> >> <%= log_root %>/<%=process.name%>-<%=num%>.log 2>&1'
|
data/lib/foreman.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "foreman/version"
|
2
|
+
|
3
|
+
module Foreman
|
4
|
+
|
5
|
+
class AppDoesNotExist < Exception; end
|
6
|
+
|
7
|
+
# load contents of env_file into ENV
|
8
|
+
def self.load_env!(env_file = './.env')
|
9
|
+
require 'foreman/engine'
|
10
|
+
Foreman::Engine.load_env!(env_file)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.runner
|
14
|
+
File.expand_path("../../bin/runner", __FILE__)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.jruby?
|
18
|
+
defined?(RUBY_PLATFORM) and RUBY_PLATFORM == "java"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.windows?
|
22
|
+
defined?(RUBY_PLATFORM) and RUBY_PLATFORM =~ /(win|w)32$/
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/lib/foreman/cli.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require "foreman"
|
2
|
+
require "foreman/helpers"
|
3
|
+
require "foreman/engine"
|
4
|
+
require "foreman/export"
|
5
|
+
require "thor"
|
6
|
+
require "yaml"
|
7
|
+
|
8
|
+
class Foreman::CLI < Thor
|
9
|
+
include Foreman::Helpers
|
10
|
+
|
11
|
+
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
|
12
|
+
|
13
|
+
desc "start", "Start the application"
|
14
|
+
|
15
|
+
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
|
16
|
+
class_option :app_root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
|
17
|
+
|
18
|
+
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
|
19
|
+
method_option :port, :type => :numeric, :aliases => "-p"
|
20
|
+
method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# Hackery. Take the run method away from Thor so that we can redefine it.
|
24
|
+
def is_thor_reserved_word?(word, type)
|
25
|
+
return false if word == 'run'
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def start
|
31
|
+
check_procfile!
|
32
|
+
engine.start
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "export FORMAT LOCATION", "Export the application to another process management format"
|
36
|
+
|
37
|
+
method_option :app, :type => :string, :aliases => "-a"
|
38
|
+
method_option :log, :type => :string, :aliases => "-l"
|
39
|
+
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
|
40
|
+
method_option :port, :type => :numeric, :aliases => "-p"
|
41
|
+
method_option :user, :type => :string, :aliases => "-u"
|
42
|
+
method_option :template, :type => :string, :aliases => "-t"
|
43
|
+
method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
|
44
|
+
|
45
|
+
def export(format, location=nil)
|
46
|
+
check_procfile!
|
47
|
+
formatter = Foreman::Export.formatter(format)
|
48
|
+
formatter.new(location, engine, options).export
|
49
|
+
rescue Foreman::Export::Exception => ex
|
50
|
+
error ex.message
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "check", "Validate your application's Procfile"
|
54
|
+
|
55
|
+
def check
|
56
|
+
error "no processes defined" unless engine.procfile.entries.length > 0
|
57
|
+
puts "valid procfile detected (#{engine.procfile.process_names.join(', ')})"
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "run COMMAND", "Run a command using your application's environment"
|
61
|
+
|
62
|
+
def run(*args)
|
63
|
+
engine.apply_environment!
|
64
|
+
begin
|
65
|
+
exec args.join(" ")
|
66
|
+
rescue Errno::EACCES
|
67
|
+
error "not executable: #{args.first}"
|
68
|
+
rescue Errno::ENOENT
|
69
|
+
error "command not found: #{args.first}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private ######################################################################
|
74
|
+
|
75
|
+
def check_procfile!
|
76
|
+
error("#{procfile} does not exist.") unless File.exist?(procfile)
|
77
|
+
end
|
78
|
+
|
79
|
+
def engine
|
80
|
+
@engine ||= Foreman::Engine.new(procfile, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
def procfile
|
84
|
+
options[:procfile] || "Procfile"
|
85
|
+
end
|
86
|
+
|
87
|
+
def error(message)
|
88
|
+
puts "ERROR: #{message}"
|
89
|
+
exit 1
|
90
|
+
end
|
91
|
+
|
92
|
+
def options
|
93
|
+
original_options = super
|
94
|
+
return original_options unless File.exists?(".foreman")
|
95
|
+
defaults = YAML::load_file(".foreman") || {}
|
96
|
+
Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
require "foreman"
|
2
|
+
require "foreman/process"
|
3
|
+
require "foreman/procfile"
|
4
|
+
require "foreman/utils"
|
5
|
+
require "tempfile"
|
6
|
+
require "timeout"
|
7
|
+
require "term/ansicolor"
|
8
|
+
require "fileutils"
|
9
|
+
require "thread"
|
10
|
+
|
11
|
+
class Foreman::Engine
|
12
|
+
|
13
|
+
attr_reader :procfile
|
14
|
+
attr_reader :directory
|
15
|
+
attr_reader :options
|
16
|
+
|
17
|
+
extend Term::ANSIColor
|
18
|
+
|
19
|
+
COLORS = [ cyan, yellow, green, magenta, red, blue,
|
20
|
+
intense_cyan, intense_yellow, intense_green, intense_magenta,
|
21
|
+
intense_red, intense_blue ]
|
22
|
+
|
23
|
+
def initialize(procfile, options={})
|
24
|
+
@procfile = Foreman::Procfile.new(procfile)
|
25
|
+
@directory = options[:app_root] || File.expand_path(File.dirname(procfile))
|
26
|
+
@options = options
|
27
|
+
@environment = read_environment_files(options[:env])
|
28
|
+
@output_mutex = Mutex.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.load_env!(env_file)
|
32
|
+
@environment = read_environment_files(env_file)
|
33
|
+
apply_environment!
|
34
|
+
end
|
35
|
+
|
36
|
+
def start
|
37
|
+
proctitle "ruby: foreman master"
|
38
|
+
termtitle "#{File.basename(@directory)} - foreman"
|
39
|
+
|
40
|
+
trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
|
41
|
+
trap("INT") { puts "SIGINT received"; terminate_gracefully }
|
42
|
+
|
43
|
+
assign_colors
|
44
|
+
spawn_processes
|
45
|
+
watch_for_output
|
46
|
+
watch_for_termination
|
47
|
+
end
|
48
|
+
|
49
|
+
def port_for(process, num, base_port=nil)
|
50
|
+
base_port ||= 5000
|
51
|
+
offset = procfile.process_names.index(process.name) * 100
|
52
|
+
base_port.to_i + offset + num - 1
|
53
|
+
end
|
54
|
+
|
55
|
+
private ######################################################################
|
56
|
+
|
57
|
+
def spawn_processes
|
58
|
+
concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency])
|
59
|
+
|
60
|
+
procfile.entries.each do |entry|
|
61
|
+
reader, writer = IO.pipe
|
62
|
+
entry.spawn(concurrency[entry.name], writer, @directory, @environment, port_for(entry, 1, base_port)).each do |process|
|
63
|
+
running_processes[process.pid] = process
|
64
|
+
readers[process] = reader
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def base_port
|
70
|
+
options[:port] || 5000
|
71
|
+
end
|
72
|
+
|
73
|
+
def kill_all(signal="SIGTERM")
|
74
|
+
running_processes.each do |pid, process|
|
75
|
+
info "sending #{signal} to pid #{pid}"
|
76
|
+
process.kill signal
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def terminate_gracefully
|
81
|
+
return if @terminating
|
82
|
+
@terminating = true
|
83
|
+
info "sending SIGTERM to all processes"
|
84
|
+
kill_all "SIGTERM"
|
85
|
+
Timeout.timeout(5) do
|
86
|
+
while running_processes.length > 0
|
87
|
+
pid, status = Process.wait2
|
88
|
+
process = running_processes.delete(pid)
|
89
|
+
info "process terminated", process.name
|
90
|
+
end
|
91
|
+
end
|
92
|
+
rescue Timeout::Error
|
93
|
+
info "sending SIGKILL to all processes"
|
94
|
+
kill_all "SIGKILL"
|
95
|
+
end
|
96
|
+
|
97
|
+
def watch_for_output
|
98
|
+
Thread.new do
|
99
|
+
require "win32console" if Foreman.windows?
|
100
|
+
begin
|
101
|
+
loop do
|
102
|
+
rs, ws = IO.select(readers.values, [], [], 1)
|
103
|
+
(rs || []).each do |r|
|
104
|
+
data = r.gets
|
105
|
+
next unless data
|
106
|
+
ps, message = data.split(",", 2)
|
107
|
+
color = colors[ps.split(".").first]
|
108
|
+
info message, ps, color
|
109
|
+
end
|
110
|
+
end
|
111
|
+
rescue Exception => ex
|
112
|
+
puts ex.message
|
113
|
+
puts ex.backtrace
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def watch_for_termination
|
119
|
+
pid, status = Process.wait2
|
120
|
+
process = running_processes.delete(pid)
|
121
|
+
info "process terminated", process.name
|
122
|
+
terminate_gracefully
|
123
|
+
rescue Errno::ECHILD
|
124
|
+
end
|
125
|
+
|
126
|
+
def info(message, name="system", color=Term::ANSIColor.white)
|
127
|
+
output = ""
|
128
|
+
output += color
|
129
|
+
output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
|
130
|
+
output += Term::ANSIColor.reset
|
131
|
+
output += message.chomp
|
132
|
+
puts output
|
133
|
+
end
|
134
|
+
|
135
|
+
def print(message=nil)
|
136
|
+
@output_mutex.synchronize do
|
137
|
+
$stdout.print message
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def puts(message=nil)
|
142
|
+
@output_mutex.synchronize do
|
143
|
+
$stdout.puts message
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def longest_process_name
|
148
|
+
@longest_process_name ||= begin
|
149
|
+
longest = procfile.process_names.map { |name| name.length }.sort.last
|
150
|
+
longest = 6 if longest < 6 # system
|
151
|
+
longest
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def pad_process_name(name="system")
|
156
|
+
name.to_s.ljust(longest_process_name + 3) # add 3 for process number padding
|
157
|
+
end
|
158
|
+
|
159
|
+
def proctitle(title)
|
160
|
+
$0 = title
|
161
|
+
end
|
162
|
+
|
163
|
+
def termtitle(title)
|
164
|
+
printf("\033]0;#{title}\007") unless Foreman.windows?
|
165
|
+
end
|
166
|
+
|
167
|
+
def running_processes
|
168
|
+
@running_processes ||= {}
|
169
|
+
end
|
170
|
+
|
171
|
+
def readers
|
172
|
+
@readers ||= {}
|
173
|
+
end
|
174
|
+
|
175
|
+
def colors
|
176
|
+
@colors ||= {}
|
177
|
+
end
|
178
|
+
|
179
|
+
def assign_colors
|
180
|
+
procfile.entries.each do |entry|
|
181
|
+
colors[entry.name] = next_color
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def process_by_reader(reader)
|
186
|
+
readers.invert[reader]
|
187
|
+
end
|
188
|
+
|
189
|
+
def next_color
|
190
|
+
@current_color ||= -1
|
191
|
+
@current_color += 1
|
192
|
+
@current_color = 0 if COLORS.length < @current_color
|
193
|
+
COLORS[@current_color]
|
194
|
+
end
|
195
|
+
|
196
|
+
module Env
|
197
|
+
attr_reader :environment
|
198
|
+
|
199
|
+
def read_environment_files(filenames)
|
200
|
+
environment = {}
|
201
|
+
|
202
|
+
(filenames || "").split(",").map(&:strip).each do |filename|
|
203
|
+
error "No such file: #{filename}" unless File.exists?(filename)
|
204
|
+
environment.merge!(read_environment(filename))
|
205
|
+
end
|
206
|
+
|
207
|
+
environment.merge!(read_environment(".env")) unless filenames
|
208
|
+
environment
|
209
|
+
end
|
210
|
+
|
211
|
+
def read_environment(filename)
|
212
|
+
return {} unless File.exists?(filename)
|
213
|
+
|
214
|
+
File.read(filename).split("\n").inject({}) do |hash, line|
|
215
|
+
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
|
216
|
+
hash[$1] = $2
|
217
|
+
end
|
218
|
+
hash
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def apply_environment!
|
223
|
+
@environment.each { |k,v| ENV[k] = v }
|
224
|
+
end
|
225
|
+
|
226
|
+
def error(message)
|
227
|
+
puts "ERROR: #{message}"
|
228
|
+
exit 1
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
include Env
|
233
|
+
extend Env
|
234
|
+
end
|