foreman 0.37.0-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/README.md +39 -0
  2. data/bin/foreman +7 -0
  3. data/bin/runner +36 -0
  4. data/data/example/Procfile +2 -0
  5. data/data/example/Procfile.without_colon +2 -0
  6. data/data/example/error +7 -0
  7. data/data/example/log/neverdie.log +4 -0
  8. data/data/example/ticker +14 -0
  9. data/data/export/bluepill/master.pill.erb +27 -0
  10. data/data/export/runit/log_run.erb +7 -0
  11. data/data/export/runit/run.erb +3 -0
  12. data/data/export/upstart/master.conf.erb +8 -0
  13. data/data/export/upstart/process.conf.erb +5 -0
  14. data/data/export/upstart/process_master.conf.erb +2 -0
  15. data/lib/foreman.rb +25 -0
  16. data/lib/foreman/cli.rb +98 -0
  17. data/lib/foreman/distribution.rb +9 -0
  18. data/lib/foreman/engine.rb +234 -0
  19. data/lib/foreman/export.rb +32 -0
  20. data/lib/foreman/export/base.rb +51 -0
  21. data/lib/foreman/export/bluepill.rb +26 -0
  22. data/lib/foreman/export/inittab.rb +36 -0
  23. data/lib/foreman/export/runit.rb +59 -0
  24. data/lib/foreman/export/upstart.rb +41 -0
  25. data/lib/foreman/helpers.rb +45 -0
  26. data/lib/foreman/process.rb +96 -0
  27. data/lib/foreman/procfile.rb +38 -0
  28. data/lib/foreman/procfile_entry.rb +22 -0
  29. data/lib/foreman/utils.rb +18 -0
  30. data/lib/foreman/version.rb +5 -0
  31. data/man/foreman.1 +222 -0
  32. data/spec/foreman/cli_spec.rb +163 -0
  33. data/spec/foreman/engine_spec.rb +86 -0
  34. data/spec/foreman/export/base_spec.rb +22 -0
  35. data/spec/foreman/export/bluepill_spec.rb +36 -0
  36. data/spec/foreman/export/inittab_spec.rb +40 -0
  37. data/spec/foreman/export/runit_spec.rb +41 -0
  38. data/spec/foreman/export/upstart_spec.rb +87 -0
  39. data/spec/foreman/export_spec.rb +24 -0
  40. data/spec/foreman/helpers_spec.rb +26 -0
  41. data/spec/foreman/process_spec.rb +131 -0
  42. data/spec/foreman_spec.rb +34 -0
  43. data/spec/helper_spec.rb +18 -0
  44. data/spec/resources/export/bluepill/app-concurrency.pill +47 -0
  45. data/spec/resources/export/bluepill/app.pill +44 -0
  46. data/spec/resources/export/inittab/inittab.concurrency +4 -0
  47. data/spec/resources/export/inittab/inittab.default +4 -0
  48. data/spec/resources/export/runit/app-alpha-1-log-run +7 -0
  49. data/spec/resources/export/runit/app-alpha-1-run +3 -0
  50. data/spec/resources/export/runit/app-alpha-2-log-run +7 -0
  51. data/spec/resources/export/runit/app-alpha-2-run +3 -0
  52. data/spec/resources/export/runit/app-bravo-1-log-run +7 -0
  53. data/spec/resources/export/runit/app-bravo-1-run +3 -0
  54. data/spec/resources/export/upstart/app-alpha-1.conf +5 -0
  55. data/spec/resources/export/upstart/app-alpha-2.conf +5 -0
  56. data/spec/resources/export/upstart/app-alpha.conf +2 -0
  57. data/spec/resources/export/upstart/app-bravo-1.conf +5 -0
  58. data/spec/resources/export/upstart/app-bravo.conf +2 -0
  59. data/spec/resources/export/upstart/app.conf +8 -0
  60. data/spec/spec_helper.rb +98 -0
  61. 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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require "foreman/cli"
6
+
7
+ Foreman::CLI.start
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
@@ -0,0 +1,2 @@
1
+ ticker: ruby ./ticker $PORT
2
+ error: ruby ./error
@@ -0,0 +1,2 @@
1
+ ticker ./ticker $PORT
2
+ error ./error
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $stdout.sync = true
4
+
5
+ puts "will error in 10s"
6
+ sleep 5
7
+ raise "Dying"
@@ -0,0 +1,4 @@
1
+ tick
2
+ tick
3
+ ./never_die:6:in `sleep': Interrupt
4
+ from ./never_die:6
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $stdout.sync = true
4
+
5
+ %w( SIGINT SIGTERM ).each do |signal|
6
+ trap(signal) do
7
+ puts "received #{signal} but i'm ignoring it!"
8
+ end
9
+ end
10
+
11
+ while true
12
+ puts "tick: #{ARGV.inspect} -- FOO:#{ENV["FOO"]}"
13
+ sleep 1
14
+ end
@@ -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,7 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ LOG=<%= log_root %>/<%= process.name %>-<%= num %>
5
+
6
+ test -d "$LOG" || mkdir -p m2750 "$LOG" && chown <%= user %> "$LOG"
7
+ exec chpst -u <%= user %> svlogd "$LOG"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ cd <%= engine.directory %>
3
+ exec chpst -u <%= user %> -e <%= process_env_directory %> <%= process.command %>
@@ -0,0 +1,8 @@
1
+ pre-start script
2
+
3
+ bash << "EOF"
4
+ mkdir -p <%= log_root %>
5
+ chown -R <%= user %> <%= log_root %>
6
+ EOF
7
+
8
+ end script
@@ -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'
@@ -0,0 +1,2 @@
1
+ start on starting <%= app %>
2
+ stop on stopping <%= app %>
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
@@ -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,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,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