foreman 0.46.0-java → 0.48.0.pre1-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/README.md +6 -0
  2. data/bin/foreman-runner +3 -7
  3. data/bin/taskman +8 -0
  4. data/data/example/Procfile +4 -3
  5. data/data/example/spawnee +14 -0
  6. data/data/example/spawner +7 -0
  7. data/data/export/bluepill/master.pill.erb +11 -10
  8. data/data/export/launchd/launchd.plist.erb +22 -0
  9. data/data/export/runit/log/run.erb +7 -0
  10. data/data/export/runit/run.erb +2 -2
  11. data/data/export/supervisord/app.conf.erb +12 -12
  12. data/data/export/upstart/master.conf.erb +2 -2
  13. data/data/export/upstart/process.conf.erb +3 -3
  14. data/lib/foreman/cli.rb +51 -22
  15. data/lib/foreman/engine.rb +209 -148
  16. data/lib/foreman/engine/cli.rb +98 -0
  17. data/lib/foreman/env.rb +27 -0
  18. data/lib/foreman/export.rb +1 -1
  19. data/lib/foreman/export/base.rb +58 -20
  20. data/lib/foreman/export/bluepill.rb +3 -17
  21. data/lib/foreman/export/inittab.rb +8 -11
  22. data/lib/foreman/export/launchd.rb +15 -0
  23. data/lib/foreman/export/runit.rb +14 -39
  24. data/lib/foreman/export/supervisord.rb +3 -13
  25. data/lib/foreman/export/upstart.rb +9 -27
  26. data/lib/foreman/process.rb +56 -67
  27. data/lib/foreman/procfile.rb +59 -25
  28. data/lib/foreman/version.rb +1 -1
  29. data/man/foreman.1 +5 -1
  30. data/spec/foreman/cli_spec.rb +38 -152
  31. data/spec/foreman/engine_spec.rb +47 -74
  32. data/spec/foreman/export/base_spec.rb +4 -7
  33. data/spec/foreman/export/bluepill_spec.rb +7 -6
  34. data/spec/foreman/export/inittab_spec.rb +7 -7
  35. data/spec/foreman/export/launchd_spec.rb +21 -0
  36. data/spec/foreman/export/runit_spec.rb +12 -17
  37. data/spec/foreman/export/supervisord_spec.rb +7 -56
  38. data/spec/foreman/export/upstart_spec.rb +22 -21
  39. data/spec/foreman/process_spec.rb +27 -110
  40. data/spec/foreman/procfile_spec.rb +26 -16
  41. data/spec/resources/Procfile +4 -0
  42. data/spec/resources/bin/echo +2 -0
  43. data/spec/resources/bin/env +2 -0
  44. data/spec/resources/bin/test +2 -0
  45. data/spec/resources/export/bluepill/app-concurrency.pill +6 -4
  46. data/spec/resources/export/bluepill/app.pill +6 -4
  47. data/spec/resources/export/launchd/launchd-a.default +22 -0
  48. data/spec/resources/export/launchd/launchd-b.default +22 -0
  49. data/spec/resources/export/runit/{app-alpha-1-log-run → app-alpha-1/log/run} +0 -0
  50. data/spec/resources/export/runit/{app-alpha-1-run → app-alpha-1/run} +0 -0
  51. data/spec/resources/export/runit/{app-alpha-2-log-run → app-alpha-2/log/run} +0 -0
  52. data/spec/resources/export/runit/{app-alpha-2-run → app-alpha-2/run} +0 -0
  53. data/spec/resources/export/runit/{app-bravo-1-log-run → app-bravo-1/log/run} +0 -0
  54. data/spec/resources/export/runit/{app-bravo-1-run → app-bravo-1/run} +0 -0
  55. data/spec/resources/export/supervisord/app-alpha-1.conf +24 -0
  56. data/spec/resources/export/supervisord/app-alpha-2.conf +4 -4
  57. data/spec/spec_helper.rb +58 -6
  58. metadata +32 -24
  59. data/data/export/runit/log_run.erb +0 -7
  60. data/lib/foreman/color.rb +0 -40
  61. data/lib/foreman/procfile_entry.rb +0 -26
  62. data/lib/foreman/utils.rb +0 -18
  63. data/spec/foreman/color_spec.rb +0 -31
  64. data/spec/foreman/procfile_entry_spec.rb +0 -13
  65. data/spec/resources/export/supervisord/app-env-with-comma.conf +0 -24
  66. data/spec/resources/export/supervisord/app-env.conf +0 -21
  67. data/spec/resources/export/supervisord/app.conf +0 -24
data/README.md CHANGED
@@ -27,6 +27,12 @@ Manage Procfile-based applications
27
27
  * [wiki](http://github.com/ddollar/foreman/wiki)
28
28
  * [changelog](https://github.com/ddollar/foreman/blob/master/Changelog.md)
29
29
 
30
+ ## Ports
31
+
32
+ * [shoreman](https://github.com/hecticjeff/shoreman) - shell
33
+ * [honcho](https://github.com/nickstenning/honcho) - python
34
+ * [norman](https://github.com/josh/norman) - node.js
35
+
30
36
  ## Authors
31
37
 
32
38
  #### Created and maintained by
data/bin/foreman-runner CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/bin/sh
2
2
  #
3
- #/ Usage: foreman-runner [-d <dir>] <command>
3
+ #/ Usage: foreman-runner [-d <dir>] <command> [<args>...]
4
4
  #/
5
5
  #/ Run a command with exec, optionally changing directory first
6
6
 
@@ -27,10 +27,6 @@ done
27
27
 
28
28
  shift $((OPTIND-1))
29
29
 
30
- command=$1
30
+ [ -z "$1" ] && usage
31
31
 
32
- if [ -z "$1" ]; then
33
- usage
34
- fi
35
-
36
- exec $1
32
+ exec "$@"
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,24 +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 %>"}
14
+ process.environment = <%= engine.env.merge("PORT" => port.to_s).inspect %>
15
15
  process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
16
+ process.stop_grace_time = 45.seconds
16
17
 
17
- process.stdout = process.stderr = "<%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log"
18
+ process.stdout = process.stderr = "<%= log %>/<%= app %>-<%= name %>-<%= num %>.log"
18
19
 
19
20
  process.monitor_children do |children|
20
- children.stop_command "kill -QUIT {{PID}}"
21
+ children.stop_command "kill {{PID}}"
21
22
  end
22
23
 
23
- process.group = "<%= app %>-<%= process.name %>"
24
+ process.group = "<%= app %>-<%= name %>"
24
25
  end
25
26
  <% end %>
26
27
  <% end %>
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string><%= "#{app}-#{name}-#{num}" %></string>
7
+ <key>ProgramArguments</key>
8
+ <array>
9
+ <string><%= process.command %></string>
10
+ </array>
11
+ <key>KeepAlive</key>
12
+ <true/>
13
+ <key>RunAtLoad</key>
14
+ <true/>
15
+ <key>StandardErrorPath</key>
16
+ <string><%= log %>/<%= app %>-<%= name %>-<%=num%>.log</string>
17
+ <key>UserName</key>
18
+ <string><%= user %></string>
19
+ <key>WorkingDirectory</key>
20
+ <string><%= engine.root %></string>
21
+ </dict>
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 %>=<%= 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,35 +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"
6
+ require "shellwords"
5
7
  require "thor"
6
- require "yaml"
7
8
 
8
9
  class Foreman::CLI < Thor
10
+
9
11
  include Foreman::Helpers
10
12
 
11
13
  class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
14
+ class_option :root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
12
15
 
13
16
  desc "start [PROCESS]", "Start the application (or a specific PROCESS)"
14
17
 
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"'
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"
21
21
 
22
22
  class << self
23
23
  # Hackery. Take the run method away from Thor so that we can redefine it.
24
24
  def is_thor_reserved_word?(word, type)
25
- return false if word == 'run'
25
+ return false if word == "run"
26
26
  super
27
27
  end
28
28
  end
29
29
 
30
30
  def start(process=nil)
31
31
  check_procfile!
32
- engine.options[:concurrency] = "#{process}=1" if process
32
+ load_environment!
33
+ engine.load_procfile(procfile)
34
+ engine.options[:formation] = "#{process}=1" if process
33
35
  engine.start
34
36
  end
35
37
 
@@ -45,6 +47,8 @@ class Foreman::CLI < Thor
45
47
 
46
48
  def export(format, location=nil)
47
49
  check_procfile!
50
+ load_environment!
51
+ engine.load_procfile(procfile)
48
52
  formatter = Foreman::Export.formatter(format)
49
53
  formatter.new(location, engine, options).export
50
54
  rescue Foreman::Export::Exception => ex
@@ -55,16 +59,19 @@ class Foreman::CLI < Thor
55
59
 
56
60
  def check
57
61
  check_procfile!
58
- error "no processes defined" unless engine.procfile.entries.length > 0
59
- 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(', ')})"
60
65
  end
61
66
 
62
- desc "run COMMAND", "Run a command using your application's environment"
67
+ desc "run COMMAND [ARGS...]", "Run a command using your application's environment"
68
+
69
+ method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
63
70
 
64
71
  def run(*args)
65
- engine.apply_environment!
72
+ load_environment!
66
73
  begin
67
- exec args.join(" ")
74
+ exec engine.env, args.shelljoin
68
75
  rescue Errno::EACCES
69
76
  error "not executable: #{args.first}"
70
77
  rescue Errno::ENOENT
@@ -72,33 +79,55 @@ class Foreman::CLI < Thor
72
79
  end
73
80
  end
74
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
+
75
96
  private ######################################################################
76
97
 
98
+ def error(message)
99
+ puts "ERROR: #{message}"
100
+ exit 1
101
+ end
102
+
77
103
  def check_procfile!
78
104
  error("#{procfile} does not exist.") unless File.exist?(procfile)
79
105
  end
80
106
 
81
- def engine
82
- @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
83
116
  end
84
117
 
85
118
  def procfile
86
119
  case
87
120
  when options[:procfile] then options[:procfile]
88
- 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"))
89
122
  else "Procfile"
90
123
  end
91
124
  end
92
125
 
93
- def error(message)
94
- puts "ERROR: #{message}"
95
- exit 1
96
- end
97
-
98
126
  def options
99
127
  original_options = super
100
128
  return original_options unless File.exists?(".foreman")
101
129
  defaults = YAML::load_file(".foreman") || {}
102
130
  Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
103
131
  end
132
+
104
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,218 +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 }
42
+ trap("HUP") { puts "SIGHUP received"; terminate_gracefully }
39
43
 
40
- assign_colors
44
+ startup
41
45
  spawn_processes
42
46
  watch_for_output
43
- watch_for_termination
47
+ sleep 0.1
48
+ watch_for_termination { terminate_gracefully }
49
+ shutdown
44
50
  end
45
51
 
46
- def port_for(process, num, base_port=nil)
47
- base_port ||= 5000
48
- offset = procfile.process_names.index(process.name) * 100
49
- 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
50
66
  end
51
67
 
52
- def apply_environment!
53
- environment.each { |k,v| ENV[k] = v }
68
+ # Clear the processes registered to this +Engine+
69
+ #
70
+ def clear
71
+ @names = {}
72
+ @processes = []
54
73
  end
55
74
 
56
- def self.read_environment(filename)
57
- return {} unless File.exists?(filename)
58
-
59
- File.read(filename).split("\n").inject({}) do |hash, line|
60
- if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
61
- key, val = [$1, $2]
62
- case val
63
- when /\A'(.*)'\z/ then hash[key] = $1
64
- when /\A"(.*)"\z/ then hash[key] = $1.gsub(/\\(.)/, '\1')
65
- else hash[key] = val
66
- end
67
- end
68
- 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]
69
83
  end
84
+ self
70
85
  end
71
86
 
72
- private ######################################################################
73
-
74
- def spawn_processes
75
- 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
76
96
 
77
- procfile.entries.each do |entry|
78
- reader, writer = (IO.method(:pipe).arity == 0 ? IO.pipe : IO.pipe("BINARY"))
79
- entry.spawn(concurrency[entry.name], writer, @directory, @environment, port_for(entry, 1, base_port)).each do |process|
80
- running_processes[process.pid] = process
81
- 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
82
107
  end
83
108
  end
84
109
  end
85
110
 
86
- def base_port
87
- options[: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])
88
117
  end
89
118
 
90
- def kill_all(signal="SIGTERM")
91
- running_processes.each do |pid, process|
92
- info "sending #{signal} to pid #{pid}"
93
- process.kill signal
94
- 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] }
95
125
  end
96
126
 
97
- def terminate_gracefully
98
- return if @terminating
99
- @terminating = true
100
- info "sending SIGTERM to all processes"
101
- kill_all "SIGTERM"
102
- Timeout.timeout(5) do
103
- while running_processes.length > 0
104
- pid, status = Process.wait2
105
- process = running_processes.delete(pid)
106
- info "process terminated", process.name
107
- end
108
- end
109
- rescue Timeout::Error
110
- info "sending SIGKILL to all processes"
111
- kill_all "SIGKILL"
112
- end
113
-
114
- def poll_readers
115
- rs, ws = IO.select(readers.values, [], [], 1)
116
- (rs || []).each do |r|
117
- data = r.gets
118
- next unless data
119
- data.force_encoding("BINARY") if data.respond_to?(:force_encoding)
120
- ps, message = data.split(",", 2)
121
- color = colors[ps.split(".").first]
122
- info message, ps, color
123
- 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]
124
135
  end
125
136
 
126
- def watch_for_output
127
- Thread.new do
128
- require "win32console" if Foreman.windows?
129
- begin
130
- loop do
131
- poll_readers
132
- end
133
- rescue Exception => ex
134
- puts ex.message
135
- puts ex.backtrace
136
- end
137
+ # Yield each +Process+ in order
138
+ #
139
+ def each_process
140
+ process_names.each do |name|
141
+ yield name, process(name)
137
142
  end
138
143
  end
139
144
 
140
- def watch_for_termination
141
- pid, status = Process.wait2
142
- process = running_processes.delete(pid)
143
- info "process terminated", process.name
144
- terminate_gracefully
145
- 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)
146
151
  end
147
152
 
148
- def info(message, name="system", color=:white)
149
- output = ""
150
- output += $stdout.color(color)
151
- output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
152
- output += $stdout.color(:reset)
153
- output += message.chomp
154
- 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)
155
162
  end
156
163
 
157
- def print(message=nil)
158
- @output_mutex.synchronize do
159
- $stdout.print message
160
- end
164
+ private
165
+
166
+ ### Engine API ######################################################
167
+
168
+ def startup
169
+ raise TypeError, "must use a subclass of Foreman::Engine"
161
170
  end
162
171
 
163
- def puts(message=nil)
164
- @output_mutex.synchronize do
165
- $stdout.puts message
166
- end
172
+ def output(name, data)
173
+ raise TypeError, "must use a subclass of Foreman::Engine"
167
174
  end
168
175
 
169
- def longest_process_name
170
- @longest_process_name ||= begin
171
- longest = procfile.process_names.map { |name| name.length }.sort.last
172
- longest = 6 if longest < 6 # system
173
- longest
174
- end
176
+ def shutdown
177
+ raise TypeError, "must use a subclass of Foreman::Engine"
175
178
  end
176
179
 
177
- def pad_process_name(name="system")
178
- 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
179
184
  end
180
185
 
181
- def proctitle(title)
182
- $0 = title
186
+ def create_pipe
187
+ IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
183
188
  end
184
189
 
185
- def termtitle(title)
186
- 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(".")
187
193
  end
188
194
 
189
- def running_processes
190
- @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
191
203
  end
192
204
 
193
- def readers
194
- @readers ||= {}
205
+ def output_with_mutex(name, message)
206
+ @mutex.synchronize do
207
+ output name, message
208
+ end
195
209
  end
196
210
 
197
- def colors
198
- @colors ||= {}
211
+ def system(message)
212
+ output_with_mutex "system", message
199
213
  end
200
214
 
201
- def assign_colors
202
- procfile.entries.each_with_index do |entry, idx|
203
- 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"
204
222
  end
205
223
  end
206
224
 
207
- def process_by_reader(reader)
208
- 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
209
230
  end
210
231
 
211
- def read_environment_files(filenames)
212
- environment = {}
232
+ ## Engine ###########################################################
213
233
 
214
- (filenames || "").split(",").map(&:strip).each do |filename|
215
- error "No such file: #{filename}" unless File.exists?(filename)
216
- 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
217
247
  end
248
+ end
218
249
 
219
- 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
220
264
  end
221
265
 
222
- def default_env
223
- env = File.join(directory, ".env")
224
- 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"
225
286
  end
226
287
 
227
288
  end