foreman 0.47.0 → 0.48.0.pre1

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.
Files changed (63) hide show
  1. data/bin/taskman +8 -0
  2. data/data/example/Procfile +4 -3
  3. data/data/example/spawnee +14 -0
  4. data/data/example/spawner +7 -0
  5. data/data/export/bluepill/master.pill.erb +10 -10
  6. data/data/export/launchd/launchd.plist.erb +3 -3
  7. data/data/export/runit/log/run.erb +7 -0
  8. data/data/export/runit/run.erb +2 -2
  9. data/data/export/supervisord/app.conf.erb +12 -12
  10. data/data/export/upstart/master.conf.erb +2 -2
  11. data/data/export/upstart/process.conf.erb +3 -3
  12. data/lib/foreman/cli.rb +49 -21
  13. data/lib/foreman/engine.rb +208 -148
  14. data/lib/foreman/engine/cli.rb +98 -0
  15. data/lib/foreman/env.rb +27 -0
  16. data/lib/foreman/export.rb +0 -1
  17. data/lib/foreman/export/base.rb +58 -35
  18. data/lib/foreman/export/bluepill.rb +3 -17
  19. data/lib/foreman/export/inittab.rb +8 -11
  20. data/lib/foreman/export/launchd.rb +4 -16
  21. data/lib/foreman/export/runit.rb +14 -39
  22. data/lib/foreman/export/supervisord.rb +3 -13
  23. data/lib/foreman/export/upstart.rb +9 -27
  24. data/lib/foreman/process.rb +56 -67
  25. data/lib/foreman/procfile.rb +59 -25
  26. data/lib/foreman/version.rb +1 -1
  27. data/man/foreman.1 +4 -0
  28. data/spec/foreman/cli_spec.rb +38 -152
  29. data/spec/foreman/engine_spec.rb +46 -80
  30. data/spec/foreman/export/base_spec.rb +4 -7
  31. data/spec/foreman/export/bluepill_spec.rb +7 -6
  32. data/spec/foreman/export/inittab_spec.rb +7 -7
  33. data/spec/foreman/export/launchd_spec.rb +4 -7
  34. data/spec/foreman/export/runit_spec.rb +12 -17
  35. data/spec/foreman/export/supervisord_spec.rb +7 -56
  36. data/spec/foreman/export/upstart_spec.rb +18 -23
  37. data/spec/foreman/process_spec.rb +27 -124
  38. data/spec/foreman/procfile_spec.rb +26 -16
  39. data/spec/resources/Procfile +4 -0
  40. data/spec/resources/bin/echo +2 -0
  41. data/spec/resources/bin/env +2 -0
  42. data/spec/resources/bin/test +2 -0
  43. data/spec/resources/export/bluepill/app-concurrency.pill +4 -4
  44. data/spec/resources/export/bluepill/app.pill +4 -4
  45. data/spec/resources/export/runit/{app-alpha-1-log-run → app-alpha-1/log/run} +0 -0
  46. data/spec/resources/export/runit/{app-alpha-1-run → app-alpha-1/run} +0 -0
  47. data/spec/resources/export/runit/{app-alpha-2-log-run → app-alpha-2/log/run} +0 -0
  48. data/spec/resources/export/runit/{app-alpha-2-run → app-alpha-2/run} +0 -0
  49. data/spec/resources/export/runit/{app-bravo-1-log-run → app-bravo-1/log/run} +0 -0
  50. data/spec/resources/export/runit/{app-bravo-1-run → app-bravo-1/run} +0 -0
  51. data/spec/resources/export/supervisord/app-alpha-1.conf +24 -0
  52. data/spec/resources/export/supervisord/app-alpha-2.conf +4 -4
  53. data/spec/spec_helper.rb +58 -6
  54. metadata +24 -22
  55. data/data/export/runit/log_run.erb +0 -7
  56. data/lib/foreman/color.rb +0 -40
  57. data/lib/foreman/procfile_entry.rb +0 -26
  58. data/lib/foreman/utils.rb +0 -18
  59. data/spec/foreman/color_spec.rb +0 -31
  60. data/spec/foreman/procfile_entry_spec.rb +0 -13
  61. data/spec/resources/export/supervisord/app-env-with-comma.conf +0 -24
  62. data/spec/resources/export/supervisord/app-env.conf +0 -21
  63. data/spec/resources/export/supervisord/app.conf +0 -24
data/bin/taskman ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require "foreman/cli"
6
+
7
+ Foreman::CLI.engine_class = Foreman::TmuxEngine
8
+ Foreman::CLI.start
@@ -1,3 +1,4 @@
1
- ticker: ruby ./ticker $PORT
2
- error: ruby ./error
3
- utf8: ruby ./utf8
1
+ ticker: ruby ./ticker $PORT
2
+ error: ruby ./error
3
+ utf8: ruby ./utf8
4
+ spawner: ./spawner
@@ -0,0 +1,14 @@
1
+ #!/bin/sh
2
+
3
+ NAME="$1"
4
+
5
+ sigterm() {
6
+ echo "$NAME: got sigterm"
7
+ }
8
+
9
+ #trap sigterm SIGTERM
10
+
11
+ while true; do
12
+ echo "$NAME: ping"
13
+ sleep 1
14
+ done
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ ./spawnee A &
4
+ ./spawnee B &
5
+ ./spawnee C &
6
+
7
+ wait
@@ -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.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) %>"
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.directory %>"
12
+ process.working_dir = "<%= engine.root %>"
13
13
  process.daemonize = true
14
- process.environment = {"PORT" => "<%= port %>"<% engine.environment.each_pair do |var,env| %> , "<%= var.upcase %>" => "<%= env %>" <% end %>}
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 = "<%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log"
18
+ process.stdout = process.stderr = "<%= log %>/<%= app %>-<%= name %>-<%= num %>.log"
19
19
 
20
20
  process.monitor_children do |children|
21
- children.stop_command "kill -QUIT {{PID}}"
21
+ children.stop_command "kill {{PID}}"
22
22
  end
23
23
 
24
- process.group = "<%= app %>-<%= process.name %>"
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}-#{process.name}-#{num}" %></string>
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><%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log</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.directory %></string>
20
+ <string><%= engine.root %></string>
21
21
  </dict>
22
22
  </plist>
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ LOG=<%= log %>/<%= name %>-<%= num %>
5
+
6
+ test -d "$LOG" || mkdir -p m2750 "$LOG" && chown <%= user %> "$LOG"
7
+ exec chpst -u <%= user %> svlogd "$LOG"
@@ -1,3 +1,3 @@
1
1
  #!/bin/sh
2
- cd <%= engine.directory %>
3
- exec chpst -u <%= user %> -e <%= process_env_directory %> <%= process.command %>
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.procfile.entries.each do |process|
4
- next if (conc = self.concurrency[process.name]) < 1
5
- 1.upto(self.concurrency[process.name]) do |num|
6
- port = engine.port_for(process, num, self.port)
7
- name = if (conc > 1); "#{process.name}-#{num}" else process.name; end
8
- environment = (engine.environment.keys.sort.map{ |var| %{#{var.upcase}="#{engine.environment[var]}"} } + [%{PORT="#{port}"}])
9
- app_name = "#{app}-#{name}"
10
- app_names << app_name
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:<%= app_name %>]
12
+ [program:<%= full_name %>]
13
13
  command=<%= process.command %>
14
14
  autostart=true
15
15
  autorestart=true
16
16
  stopsignal=QUIT
17
- stdout_logfile=<%= log_root %>/<%=process.name%>-<%=num%>-out.log
18
- stderr_logfile=<%= log_root %>/<%=process.name%>-<%=num%>-err.log
17
+ stdout_logfile=<%= log %>/<%= name %>-<%= num %>.log
18
+ stderr_logfile=<%= log %>/<%= name %>-<%= num %>.error.log
19
19
  user=<%= user %>
20
- directory=<%= engine.directory %>
20
+ directory=<%= engine.root %>
21
21
  environment=<%= environment.join(',') %><%
22
22
  end
23
23
  end
@@ -1,8 +1,8 @@
1
1
  pre-start script
2
2
 
3
3
  bash << "EOF"
4
- mkdir -p <%= log_root %>
5
- chown -R <%= user %> <%= log_root %>
4
+ mkdir -p <%= log %>
5
+ chown -R <%= user %> <%= log %>
6
6
  EOF
7
7
 
8
8
  end script
@@ -1,5 +1,5 @@
1
- start on starting <%= app %>-<%= process.name %>
2
- stop on stopping <%= app %>-<%= process.name %>
1
+ start on starting <%= app %>-<%= name %>
2
+ stop on stopping <%= app %>-<%= name %>
3
3
  respawn
4
4
 
5
- exec su - <%= user %> -c 'cd <%= engine.directory %>; export PORT=<%= port %>;<% engine.environment.each_pair do |var,env| %> export <%= var.upcase %>=<%= shell_quote(env) %>; <% end %> <%= process.command %> >> <%= log_root %>/<%=process.name%>-<%=num%>.log 2>&1'
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
- class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
17
- class_option :app_root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
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 == 'run'
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
- engine.options[:concurrency] = "#{process}=1" if process
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
- error "no processes defined" unless engine.procfile.entries.length > 0
60
- puts "valid procfile detected (#{engine.procfile.process_names.join(', ')})"
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
- engine.apply_environment!
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 engine
83
- @engine ||= Foreman::Engine.new(procfile, options)
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[:app_root] then File.expand_path(File.join(options[:app_root], "Procfile"))
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
@@ -1,8 +1,7 @@
1
1
  require "foreman"
2
- require "foreman/color"
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 :environment
14
- attr_reader :procfile
15
- attr_reader :directory
12
+ attr_reader :env
16
13
  attr_reader :options
17
-
18
- COLORS = %w( cyan yellow green magenta red blue intense_cyan intense_yellow
19
- intense_green intense_magenta intense_red, intense_blue )
20
-
21
- Foreman::Color.enable($stdout)
22
-
23
- def initialize(procfile, options={})
24
- @procfile = Foreman::Procfile.new(procfile) if File.exists?(procfile)
25
- @directory = options[:app_root] || File.expand_path(File.dirname(procfile))
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[:env] ||= default_env
30
- @environment = read_environment_files(@options[:env])
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
- assign_colors
44
+ startup
42
45
  spawn_processes
43
46
  watch_for_output
44
- watch_for_termination
47
+ sleep 0.1
48
+ watch_for_termination { terminate_gracefully }
49
+ shutdown
45
50
  end
46
51
 
47
- def port_for(process, num, base_port=nil)
48
- base_port ||= 5000
49
- offset = procfile.process_names.index(process.name) * 100
50
- base_port.to_i + offset + num - 1
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
- def apply_environment!
54
- environment.each { |k,v| ENV[k] = v }
68
+ # Clear the processes registered to this +Engine+
69
+ #
70
+ def clear
71
+ @names = {}
72
+ @processes = []
55
73
  end
56
74
 
57
- def self.read_environment(filename)
58
- return {} unless File.exists?(filename)
59
-
60
- File.read(filename).split("\n").inject({}) do |hash, line|
61
- if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
62
- key, val = [$1, $2]
63
- case val
64
- when /\A'(.*)'\z/ then hash[key] = $1
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
- private ######################################################################
74
-
75
- def spawn_processes
76
- concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency])
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
- procfile.entries.each do |entry|
79
- reader, writer = (IO.method(:pipe).arity == 0 ? IO.pipe : IO.pipe("BINARY"))
80
- entry.spawn(concurrency[entry.name], writer, @directory, @environment, port_for(entry, 1, base_port)).each do |process|
81
- running_processes[process.pid] = process
82
- readers[process] = reader
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
- def base_port
88
- options[:port] || environment["PORT"] || ENV["PORT"] || 5000
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
- def kill_all(signal="SIGTERM")
92
- running_processes.each do |pid, process|
93
- info "sending #{signal} to pid #{pid}"
94
- process.kill signal
95
- end
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
- def terminate_gracefully
99
- return if @terminating
100
- @terminating = true
101
- info "sending SIGTERM to all processes"
102
- kill_all "SIGTERM"
103
- Timeout.timeout(5) do
104
- while running_processes.length > 0
105
- pid, status = Process.wait2
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
- def watch_for_output
128
- Thread.new do
129
- require "win32console" if Foreman.windows?
130
- begin
131
- loop do
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
- def watch_for_termination
142
- pid, status = Process.wait2
143
- process = running_processes.delete(pid)
144
- info "process terminated", process.name
145
- terminate_gracefully
146
- rescue Errno::ECHILD
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
- def info(message, name="system", color=:white)
150
- output = ""
151
- output += $stdout.color(color)
152
- output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
153
- output += $stdout.color(:reset)
154
- output += message.chomp
155
- puts output
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
- def print(message=nil)
159
- @output_mutex.synchronize do
160
- $stdout.print message
161
- end
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 puts(message=nil)
165
- @output_mutex.synchronize do
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 longest_process_name
171
- @longest_process_name ||= begin
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
- def pad_process_name(name="system")
179
- name.to_s.ljust(longest_process_name + 3) # add 3 for process number padding
180
+ ## Helpers ##########################################################
181
+
182
+ def base_port
183
+ (options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i
180
184
  end
181
185
 
182
- def proctitle(title)
183
- $0 = title
186
+ def create_pipe
187
+ IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
184
188
  end
185
189
 
186
- def termtitle(title)
187
- printf("\033]0;#{title}\007") unless Foreman.windows?
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 running_processes
191
- @running_processes ||= {}
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 readers
195
- @readers ||= {}
205
+ def output_with_mutex(name, message)
206
+ @mutex.synchronize do
207
+ output name, message
208
+ end
196
209
  end
197
210
 
198
- def colors
199
- @colors ||= {}
211
+ def system(message)
212
+ output_with_mutex "system", message
200
213
  end
201
214
 
202
- def assign_colors
203
- procfile.entries.each_with_index do |entry, idx|
204
- colors[entry.name] = COLORS[idx % COLORS.length]
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 process_by_reader(reader)
209
- readers.invert[reader]
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
- def read_environment_files(filenames)
213
- environment = {}
232
+ ## Engine ###########################################################
214
233
 
215
- (filenames || "").split(",").map(&:strip).each do |filename|
216
- error "No such file: #{filename}" unless File.exists?(filename)
217
- environment.merge!(Foreman::Engine.read_environment(filename))
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
- environment
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 default_env
224
- env = File.join(directory, ".env")
225
- File.exists?(env) ? env : ""
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