foreman 0.46.0-mingw32 → 0.50.0-mingw32

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 (68) 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.rb +4 -0
  15. data/lib/foreman/cli.rb +59 -23
  16. data/lib/foreman/engine.rb +233 -147
  17. data/lib/foreman/engine/cli.rb +105 -0
  18. data/lib/foreman/env.rb +27 -0
  19. data/lib/foreman/export.rb +2 -2
  20. data/lib/foreman/export/base.rb +107 -12
  21. data/lib/foreman/export/bluepill.rb +3 -17
  22. data/lib/foreman/export/inittab.rb +8 -11
  23. data/lib/foreman/export/launchd.rb +15 -0
  24. data/lib/foreman/export/runit.rb +14 -39
  25. data/lib/foreman/export/supervisord.rb +3 -13
  26. data/lib/foreman/export/upstart.rb +9 -27
  27. data/lib/foreman/process.rb +73 -67
  28. data/lib/foreman/procfile.rb +59 -25
  29. data/lib/foreman/version.rb +1 -1
  30. data/man/foreman.1 +5 -1
  31. data/spec/foreman/cli_spec.rb +46 -150
  32. data/spec/foreman/engine_spec.rb +47 -74
  33. data/spec/foreman/export/base_spec.rb +4 -7
  34. data/spec/foreman/export/bluepill_spec.rb +7 -6
  35. data/spec/foreman/export/inittab_spec.rb +7 -7
  36. data/spec/foreman/export/launchd_spec.rb +21 -0
  37. data/spec/foreman/export/runit_spec.rb +12 -17
  38. data/spec/foreman/export/supervisord_spec.rb +7 -56
  39. data/spec/foreman/export/upstart_spec.rb +22 -21
  40. data/spec/foreman/process_spec.rb +27 -110
  41. data/spec/foreman/procfile_spec.rb +26 -16
  42. data/spec/resources/Procfile +4 -0
  43. data/spec/resources/bin/echo +2 -0
  44. data/spec/resources/bin/env +2 -0
  45. data/spec/resources/bin/test +2 -0
  46. data/spec/resources/export/bluepill/app-concurrency.pill +6 -4
  47. data/spec/resources/export/bluepill/app.pill +6 -4
  48. data/spec/resources/export/launchd/launchd-a.default +22 -0
  49. data/spec/resources/export/launchd/launchd-b.default +22 -0
  50. data/spec/resources/export/runit/{app-alpha-1-log-run → app-alpha-1/log/run} +0 -0
  51. data/spec/resources/export/runit/{app-alpha-1-run → app-alpha-1/run} +0 -0
  52. data/spec/resources/export/runit/{app-alpha-2-log-run → app-alpha-2/log/run} +0 -0
  53. data/spec/resources/export/runit/{app-alpha-2-run → app-alpha-2/run} +0 -0
  54. data/spec/resources/export/runit/{app-bravo-1-log-run → app-bravo-1/log/run} +0 -0
  55. data/spec/resources/export/runit/{app-bravo-1-run → app-bravo-1/run} +0 -0
  56. data/spec/resources/export/supervisord/app-alpha-1.conf +24 -0
  57. data/spec/resources/export/supervisord/app-alpha-2.conf +4 -4
  58. data/spec/spec_helper.rb +57 -6
  59. metadata +29 -21
  60. data/data/export/runit/log_run.erb +0 -7
  61. data/lib/foreman/color.rb +0 -40
  62. data/lib/foreman/procfile_entry.rb +0 -26
  63. data/lib/foreman/utils.rb +0 -18
  64. data/spec/foreman/color_spec.rb +0 -31
  65. data/spec/foreman/procfile_entry_spec.rb +0 -13
  66. data/spec/resources/export/supervisord/app-env-with-comma.conf +0 -24
  67. data/spec/resources/export/supervisord/app-env.conf +0 -21
  68. 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.rb CHANGED
@@ -12,6 +12,10 @@ module Foreman
12
12
  defined?(RUBY_PLATFORM) and RUBY_PLATFORM == "java"
13
13
  end
14
14
 
15
+ def self.ruby_18?
16
+ defined?(RUBY_VERSION) and RUBY_VERSION =~ /^1\.8\.\d+/
17
+ end
18
+
15
19
  def self.windows?
16
20
  defined?(RUBY_PLATFORM) and RUBY_PLATFORM =~ /(win|w)32$/
17
21
  end
data/lib/foreman/cli.rb CHANGED
@@ -1,35 +1,42 @@
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
- require "thor"
6
+ require "foreman/version"
7
+ require "shellwords"
6
8
  require "yaml"
9
+ require "thor"
7
10
 
8
11
  class Foreman::CLI < Thor
12
+
9
13
  include Foreman::Helpers
10
14
 
15
+ map ["-v", "--version"] => :version
16
+
11
17
  class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
18
+ class_option :root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
12
19
 
13
20
  desc "start [PROCESS]", "Start the application (or a specific PROCESS)"
14
21
 
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"'
22
+ method_option :color, :type => :boolean, :aliases => "-c", :desc => "Force color to be enabled"
23
+ method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
24
+ method_option :formation, :type => :string, :aliases => "-m", :banner => '"alpha=5,bar=3"'
25
+ method_option :port, :type => :numeric, :aliases => "-p"
21
26
 
22
27
  class << self
23
28
  # Hackery. Take the run method away from Thor so that we can redefine it.
24
29
  def is_thor_reserved_word?(word, type)
25
- return false if word == 'run'
30
+ return false if word == "run"
26
31
  super
27
32
  end
28
33
  end
29
34
 
30
35
  def start(process=nil)
31
36
  check_procfile!
32
- engine.options[:concurrency] = "#{process}=1" if process
37
+ load_environment!
38
+ engine.load_procfile(procfile)
39
+ engine.options[:formation] = "#{process}=1" if process
33
40
  engine.start
34
41
  end
35
42
 
@@ -45,6 +52,8 @@ class Foreman::CLI < Thor
45
52
 
46
53
  def export(format, location=nil)
47
54
  check_procfile!
55
+ load_environment!
56
+ engine.load_procfile(procfile)
48
57
  formatter = Foreman::Export.formatter(format)
49
58
  formatter.new(location, engine, options).export
50
59
  rescue Foreman::Export::Exception => ex
@@ -55,16 +64,19 @@ class Foreman::CLI < Thor
55
64
 
56
65
  def check
57
66
  check_procfile!
58
- error "no processes defined" unless engine.procfile.entries.length > 0
59
- puts "valid procfile detected (#{engine.procfile.process_names.join(', ')})"
67
+ engine.load_procfile(procfile)
68
+ error "no processes defined" unless engine.processes.length > 0
69
+ puts "valid procfile detected (#{engine.process_names.join(', ')})"
60
70
  end
61
71
 
62
- desc "run COMMAND", "Run a command using your application's environment"
72
+ desc "run COMMAND [ARGS...]", "Run a command using your application's environment"
73
+
74
+ method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
63
75
 
64
76
  def run(*args)
65
- engine.apply_environment!
77
+ load_environment!
66
78
  begin
67
- exec args.join(" ")
79
+ exec engine.env, args.shelljoin
68
80
  rescue Errno::EACCES
69
81
  error "not executable: #{args.first}"
70
82
  rescue Errno::ENOENT
@@ -72,33 +84,57 @@ class Foreman::CLI < Thor
72
84
  end
73
85
  end
74
86
 
87
+ desc "version", "Display Foreman gem version"
88
+
89
+ def version
90
+ puts Foreman::VERSION
91
+ end
92
+
93
+ no_tasks do
94
+ def engine
95
+ @engine ||= begin
96
+ engine_class = Foreman::Engine::CLI
97
+ engine = engine_class.new(options)
98
+ engine
99
+ end
100
+ end
101
+ end
102
+
75
103
  private ######################################################################
76
104
 
105
+ def error(message)
106
+ puts "ERROR: #{message}"
107
+ exit 1
108
+ end
109
+
77
110
  def check_procfile!
78
111
  error("#{procfile} does not exist.") unless File.exist?(procfile)
79
112
  end
80
113
 
81
- def engine
82
- @engine ||= Foreman::Engine.new(procfile, options)
114
+ def load_environment!
115
+ if options[:env]
116
+ options[:env].split(",").each do |file|
117
+ engine.load_env file
118
+ end
119
+ else
120
+ default_env = File.join(engine.root, ".env")
121
+ engine.load_env default_env if File.exists?(default_env)
122
+ end
83
123
  end
84
124
 
85
125
  def procfile
86
126
  case
87
127
  when options[:procfile] then options[:procfile]
88
- when options[:app_root] then File.expand_path(File.join(options[:app_root], "Procfile"))
128
+ when options[:root] then File.expand_path(File.join(options[:app_root], "Procfile"))
89
129
  else "Procfile"
90
130
  end
91
131
  end
92
132
 
93
- def error(message)
94
- puts "ERROR: #{message}"
95
- exit 1
96
- end
97
-
98
133
  def options
99
134
  original_options = super
100
135
  return original_options unless File.exists?(".foreman")
101
- defaults = YAML::load_file(".foreman") || {}
136
+ defaults = ::YAML::load_file(".foreman") || {}
102
137
  Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
103
138
  end
139
+
104
140
  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,305 @@ 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] ||= (options[:concurrency] || "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 } if ::Signal.list.keys.include? 'HUP'
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)
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]
83
+ end
84
+ self
85
+ end
86
+
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
58
96
 
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
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
+ if Foreman.windows?
103
+ @running.each do |pid, (process, index)|
104
+ system "sending #{signal} to #{name_for(pid)} at pid #{pid}"
105
+ begin
106
+ Process.kill(signal, pid)
107
+ rescue Errno::ESRCH, Errno::EPERM
66
108
  end
67
109
  end
68
- hash
110
+ else
111
+ begin
112
+ Process.kill "-#{signal}", Process.pid
113
+ rescue Errno::ESRCH, Errno::EPERM
114
+ end
69
115
  end
70
116
  end
71
117
 
72
- private ######################################################################
73
-
74
- def spawn_processes
75
- concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency])
118
+ # Get the process formation
119
+ #
120
+ # @returns [Fixnum] The formation count for the specified process
121
+ #
122
+ def formation
123
+ @formation ||= parse_formation(options[:formation])
124
+ end
76
125
 
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
82
- end
83
- end
126
+ # List the available process names
127
+ #
128
+ # @returns [Array] A list of process names
129
+ #
130
+ def process_names
131
+ @processes.map { |p| @names[p] }
84
132
  end
85
133
 
86
- def base_port
87
- options[:port] || 5000
134
+ # Get the +Process+ for a specifid name
135
+ #
136
+ # @param [String] name The process name
137
+ #
138
+ # @returns [Foreman::Process] The +Process+ for the specified name
139
+ #
140
+ def process(name)
141
+ @names.invert[name]
88
142
  end
89
143
 
90
- def kill_all(signal="SIGTERM")
91
- running_processes.each do |pid, process|
92
- info "sending #{signal} to pid #{pid}"
93
- process.kill signal
144
+ # Yield each +Process+ in order
145
+ #
146
+ def each_process
147
+ process_names.each do |name|
148
+ yield name, process(name)
94
149
  end
95
150
  end
96
151
 
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
152
+ # Get the root directory for this +Engine+
153
+ #
154
+ # @returns [String] The root directory
155
+ #
156
+ def root
157
+ File.expand_path(options[:root] || Dir.pwd)
124
158
  end
125
159
 
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
160
+ # Get the port for a given process and offset
161
+ #
162
+ # @param [Foreman::Process] process A +Process+ associated with this engine
163
+ # @param [Fixnum] instance The instance of the process
164
+ #
165
+ # @returns [Fixnum] port The port to use for this instance of this process
166
+ #
167
+ def port_for(process, instance, base=nil)
168
+ if base
169
+ base + (@processes.index(process.process) * 100) + (instance - 1)
170
+ else
171
+ base_port + (@processes.index(process) * 100) + (instance - 1)
137
172
  end
138
173
  end
139
174
 
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
175
+ # Get the base port for this foreman instance
176
+ #
177
+ # @returns [Fixnum] port The base port
178
+ #
179
+ def base_port
180
+ (options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i
146
181
  end
147
182
 
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
183
+ # deprecated
184
+ def environment
185
+ env
155
186
  end
156
187
 
157
- def print(message=nil)
158
- @output_mutex.synchronize do
159
- $stdout.print message
160
- end
161
- end
188
+ private
162
189
 
163
- def puts(message=nil)
164
- @output_mutex.synchronize do
165
- $stdout.puts message
166
- end
190
+ ### Engine API ######################################################
191
+
192
+ def startup
193
+ raise TypeError, "must use a subclass of Foreman::Engine"
167
194
  end
168
195
 
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
196
+ def output(name, data)
197
+ raise TypeError, "must use a subclass of Foreman::Engine"
175
198
  end
176
199
 
177
- def pad_process_name(name="system")
178
- name.to_s.ljust(longest_process_name + 3) # add 3 for process number padding
200
+ def shutdown
201
+ raise TypeError, "must use a subclass of Foreman::Engine"
179
202
  end
180
203
 
181
- def proctitle(title)
182
- $0 = title
204
+ ## Helpers ##########################################################
205
+
206
+ def create_pipe
207
+ IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
183
208
  end
184
209
 
185
- def termtitle(title)
186
- printf("\033]0;#{title}\007") unless Foreman.windows?
210
+ def name_for(pid)
211
+ process, index = @running[pid]
212
+ [ @names[process], index.to_s ].compact.join(".")
187
213
  end
188
214
 
189
- def running_processes
190
- @running_processes ||= {}
215
+ def parse_formation(formation)
216
+ pairs = formation.to_s.gsub(/\s/, "").split(",")
217
+
218
+ pairs.inject(Hash.new(0)) do |ax, pair|
219
+ process, amount = pair.split("=")
220
+ process == "all" ? ax.default = amount.to_i : ax[process] = amount.to_i
221
+ ax
222
+ end
191
223
  end
192
224
 
193
- def readers
194
- @readers ||= {}
225
+ def output_with_mutex(name, message)
226
+ @mutex.synchronize do
227
+ output name, message
228
+ end
195
229
  end
196
230
 
197
- def colors
198
- @colors ||= {}
231
+ def system(message)
232
+ output_with_mutex "system", message
199
233
  end
200
234
 
201
- def assign_colors
202
- procfile.entries.each_with_index do |entry, idx|
203
- colors[entry.name] = COLORS[idx % COLORS.length]
235
+ def termination_message_for(status)
236
+ if status.exited?
237
+ "exited with code #{status.exitstatus}"
238
+ elsif status.signaled?
239
+ "terminated by SIG#{Signal.list.invert[status.termsig]}"
240
+ else
241
+ "died a mysterious death"
204
242
  end
205
243
  end
206
244
 
207
- def process_by_reader(reader)
208
- readers.invert[reader]
245
+ def flush_reader(reader)
246
+ until reader.eof?
247
+ data = reader.gets
248
+ output_with_mutex name_for(@readers.key(reader)), data
249
+ end
209
250
  end
210
251
 
211
- def read_environment_files(filenames)
212
- environment = {}
252
+ ## Engine ###########################################################
213
253
 
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))
254
+ def spawn_processes
255
+ @processes.each do |process|
256
+ 1.upto(formation[@names[process]]) do |n|
257
+ reader, writer = create_pipe
258
+ begin
259
+ pid = process.run(:output => writer, :env => { "PORT" => port_for(process, n).to_s })
260
+ writer.puts "started with pid #{pid}"
261
+ rescue Errno::ENOENT
262
+ writer.puts "unknown command: #{process.command}"
263
+ end
264
+ @running[pid] = [process, n]
265
+ @readers[pid] = reader
266
+ end
217
267
  end
268
+ end
218
269
 
219
- environment
270
+ def watch_for_output
271
+ Thread.new do
272
+ begin
273
+ loop do
274
+ (IO.select(@readers.values).first || []).each do |reader|
275
+ data = reader.gets
276
+ output_with_mutex name_for(@readers.invert[reader]), data
277
+ end
278
+ end
279
+ rescue Exception => ex
280
+ puts ex.message
281
+ puts ex.backtrace
282
+ end
283
+ end
284
+ end
285
+
286
+ def watch_for_termination
287
+ pid, status = Process.wait2
288
+ output_with_mutex name_for(pid), termination_message_for(status)
289
+ @running.delete(pid)
290
+ yield if block_given?
291
+ pid
292
+ rescue Errno::ECHILD
220
293
  end
221
294
 
222
- def default_env
223
- env = File.join(directory, ".env")
224
- File.exists?(env) ? env : ""
295
+ def terminate_gracefully
296
+ return if @terminating
297
+ @terminating = true
298
+ if Foreman.windows?
299
+ system "sending SIGKILL to all processes"
300
+ killall "SIGKILL"
301
+ else
302
+ system "sending SIGTERM to all processes"
303
+ killall "SIGTERM"
304
+ end
305
+ Timeout.timeout(5) do
306
+ watch_for_termination while @running.length > 0
307
+ end
308
+ rescue Timeout::Error
309
+ system "sending SIGKILL to all processes"
310
+ killall "SIGKILL"
225
311
  end
226
312
 
227
313
  end