foreman 0.46.0-mingw32 → 0.50.0-mingw32

Sign up to get free protection for your applications and to get access to all the features.
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