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.
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