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
@@ -4,40 +4,22 @@ require "foreman/export"
4
4
  class Foreman::Export::Upstart < Foreman::Export::Base
5
5
 
6
6
  def export
7
- error("Must specify a location") unless location
8
-
9
- FileUtils.mkdir_p location
10
-
11
- app = self.app || File.basename(engine.directory)
12
- user = self.user || app
13
- log_root = self.log || "/var/log/#{app}"
14
- template_root = self.template
7
+ super
15
8
 
16
9
  Dir["#{location}/#{app}*.conf"].each do |file|
17
- say "cleaning up: #{file}"
18
- FileUtils.rm(file)
10
+ clean file
19
11
  end
20
12
 
21
- master_template = export_template("upstart", "master.conf.erb", template_root)
22
- master_config = ERB.new(master_template).result(binding)
23
- write_file "#{location}/#{app}.conf", master_config
13
+ write_template "upstart/master.conf.erb", "#{app}.conf", binding
24
14
 
25
- process_template = export_template("upstart", "process.conf.erb", template_root)
15
+ engine.each_process do |name, process|
16
+ next if engine.formation[name] < 1
17
+ write_template "upstart/process_master.conf.erb", "#{app}-#{name}.conf", binding
26
18
 
27
- engine.procfile.entries.each do |process|
28
- next if (conc = self.concurrency[process.name]) < 1
29
- process_master_template = export_template("upstart", "process_master.conf.erb", template_root)
30
- process_master_config = ERB.new(process_master_template).result(binding)
31
- write_file "#{location}/#{app}-#{process.name}.conf", process_master_config
32
-
33
- 1.upto(self.concurrency[process.name]) do |num|
34
- port = engine.port_for(process, num, self.port)
35
- process_config = ERB.new(process_template).result(binding)
36
- write_file "#{location}/#{app}-#{process.name}-#{num}.conf", process_config
19
+ 1.upto(engine.formation[name]) do |num|
20
+ port = engine.port_for(process, num)
21
+ write_template "upstart/process.conf.erb", "#{app}-#{name}-#{num}.conf", binding
37
22
  end
38
23
  end
39
-
40
- FileUtils.mkdir_p(log_root) rescue error "could not create #{log_root}"
41
- FileUtils.chown(user, nil, log_root) rescue error "could not chown #{log_root} to #{user}"
42
24
  end
43
25
  end
@@ -3,94 +3,83 @@ require "rubygems"
3
3
 
4
4
  class Foreman::Process
5
5
 
6
- attr_reader :entry
7
- attr_reader :num
8
- attr_reader :pid
9
- attr_reader :port
6
+ attr_reader :command
7
+ attr_reader :env
10
8
 
11
- def initialize(entry, num, port)
12
- @entry = entry
13
- @num = num
14
- @port = port
15
- end
9
+ # Create a Process
10
+ #
11
+ # @param [String] command The command to run
12
+ # @param [Hash] options
13
+ #
14
+ # @option options [String] :cwd (./) Change to this working directory before executing the process
15
+ # @option options [Hash] :env ({}) Environment variables to set for this process
16
+ #
17
+ def initialize(command, options={})
18
+ @command = command
19
+ @options = options.dup
16
20
 
17
- def run(pipe, basedir, environment)
18
- with_environment(environment.merge("PORT" => port.to_s)) do
19
- run_process basedir, entry.command, pipe
20
- end
21
+ @options[:env] ||= {}
21
22
  end
22
23
 
23
- def name
24
- "%s.%s" % [ entry.name, num ]
24
+ # Run a +Process+
25
+ #
26
+ # @param [Hash] options
27
+ #
28
+ # @option options :env ({}) Environment variables to set for this execution
29
+ # @option options :output ($stdout) The output stream
30
+ #
31
+ # @returns [Fixnum] pid The +pid+ of the process
32
+ #
33
+ def run(options={})
34
+ env = options[:env] ? @options[:env].merge(options[:env]) : @options[:env]
35
+ output = options[:output] || $stdout
36
+
37
+ if Foreman.windows?
38
+ Dir.chdir(cwd) do
39
+ Process.spawn env, command, :out => output, :err => output, :new_pgroup => true
40
+ end
41
+ elsif Foreman.jruby?
42
+ Dir.chdir(cwd) do
43
+ require "posix/spawn"
44
+ POSIX::Spawn.spawn env, command, :out => output, :err => output, :pgroup => 0
45
+ end
46
+ else
47
+ Dir.chdir(cwd) do
48
+ Process.spawn env, command, :out => output, :err => output, :pgroup => 0
49
+ end
50
+ end
25
51
  end
26
52
 
53
+ # Send a signal to this +Process+
54
+ #
55
+ # @param [String] signal The signal to send
56
+ #
27
57
  def kill(signal)
28
- pid && Process.kill(signal, pid)
58
+ pid && Process.kill(signal, -1 * pid)
29
59
  rescue Errno::ESRCH
30
60
  false
31
61
  end
32
62
 
33
- def detach
34
- pid && Process.detach(pid)
35
- end
36
-
63
+ # Test whether or not this +Process+ is still running
64
+ #
65
+ # @returns [Boolean]
66
+ #
37
67
  def alive?
38
68
  kill(0)
39
69
  end
40
70
 
71
+ # Test whether or not this +Process+ has terminated
72
+ #
73
+ # @returns [Boolean]
74
+ #
41
75
  def dead?
42
76
  !alive?
43
77
  end
44
78
 
45
79
  private
46
80
 
47
- def fork_with_io(command, basedir)
48
- reader, writer = IO.pipe
49
- command = replace_command_env(command)
50
- pid = if Foreman.windows?
51
- Dir.chdir(basedir) do
52
- Process.spawn command, :out => writer, :err => writer
53
- end
54
- elsif Foreman.jruby?
55
- require "posix/spawn"
56
- POSIX::Spawn.spawn(Foreman.runner, "-d", basedir, command, {
57
- :out => writer, :err => writer
58
- })
59
- else
60
- fork do
61
- writer.sync = true
62
- $stdout.reopen writer
63
- $stderr.reopen writer
64
- reader.close
65
- exec Foreman.runner, "-d", basedir, *command.shellsplit
66
- end
67
- end
68
- [ reader, pid ]
69
- end
70
-
71
- def run_process(basedir, command, pipe)
72
- io, @pid = fork_with_io(command, basedir)
73
- output pipe, "started with pid %d" % @pid
74
- Thread.new do
75
- until io.eof?
76
- output pipe, io.gets
77
- end
78
- end
79
- end
80
-
81
- def output(pipe, message)
82
- pipe.puts "%s,%s" % [ name, message ]
81
+ def cwd
82
+ @options[:cwd] || "."
83
83
  end
84
84
 
85
- def replace_command_env(command)
86
- command.gsub(/\$(\w+)/) { |e| ENV[e[1..-1]] }
87
- end
88
-
89
- def with_environment(environment)
90
- original = ENV.to_hash
91
- ENV.update environment
92
- yield
93
- ensure
94
- ENV.replace original
95
- end
96
85
  end
@@ -1,56 +1,90 @@
1
1
  require "foreman"
2
- require "foreman/procfile_entry"
3
2
 
4
- # A valid Procfile entry is captured by this regex.
5
- # All other lines are ignored.
3
+ # Reads and writes Procfiles
4
+ #
5
+ # A valid Procfile entry is captured by this regex:
6
6
  #
7
- # /^([A-Za-z0-9_]+):\s*(.+)$/
7
+ # /^([A-Za-z0-9_]+):\s*(.+)$/
8
8
  #
9
- # $1 = name
10
- # $2 = command
9
+ # All other lines are ignored.
11
10
  #
12
11
  class Foreman::Procfile
13
12
 
14
- attr_reader :entries
15
-
13
+ # Initialize a Procfile
14
+ #
15
+ # @param [String] filename (nil) An optional filename to read from
16
+ #
16
17
  def initialize(filename=nil)
17
18
  @entries = []
18
19
  load(filename) if filename
19
20
  end
20
21
 
22
+ # Yield each +Procfile+ entry in order
23
+ #
24
+ def entries(&blk)
25
+ @entries.each do |(name, command)|
26
+ yield name, command
27
+ end
28
+ end
29
+
30
+ # Retrieve a +Procfile+ command by name
31
+ #
32
+ # @param [String] name The name of the Procfile entry to retrieve
33
+ #
21
34
  def [](name)
22
- entries.detect { |entry| entry.name == name }
35
+ @entries.detect { |n,c| name == n }.last
36
+ end
37
+
38
+ # Create a +Procfile+ entry
39
+ #
40
+ # @param [String] name The name of the +Procfile+ entry to create
41
+ # @param [String] command The command of the +Procfile+ entry to create
42
+ #
43
+ def []=(name, command)
44
+ delete name
45
+ @entries << [name, command]
23
46
  end
24
47
 
25
- def process_names
26
- entries.map(&:name)
48
+ # Remove a +Procfile+ entry
49
+ #
50
+ # @param [String] name The name of the +Procfile+ entry to remove
51
+ #
52
+ def delete(name)
53
+ @entries.reject! { |n,c| name == n }
27
54
  end
28
55
 
56
+ # Load a Procfile from a file
57
+ #
58
+ # @param [String] filename The filename of the +Procfile+ to load
59
+ #
29
60
  def load(filename)
30
- entries.clear
31
- parse_procfile(filename)
61
+ @entries.replace parse(filename)
32
62
  end
33
63
 
34
- def write(filename)
35
- File.open(filename, 'w') do |io|
36
- entries.each do |ent|
37
- io.puts(ent)
38
- end
64
+ # Save a Procfile to a file
65
+ #
66
+ # @param [String] filename Save the +Procfile+ to this file
67
+ #
68
+ def save(filename)
69
+ File.open(filename, 'w') do |file|
70
+ file.puts self.to_s
39
71
  end
40
72
  end
41
73
 
42
- def <<(entry)
43
- entries << Foreman::ProcfileEntry.new(*entry)
44
- self
74
+ # Get the +Procfile+ as a +String+
75
+ #
76
+ def to_s
77
+ @entries.map do |name, command|
78
+ [ name, command ].join(": ")
79
+ end.join("\n")
45
80
  end
46
81
 
82
+ private
47
83
 
48
- protected
49
-
50
- def parse_procfile(filename)
84
+ def parse(filename)
51
85
  File.read(filename).split("\n").map do |line|
52
86
  if line =~ /^([A-Za-z0-9_]+):\s*(.+)$/
53
- self << [ $1, $2 ]
87
+ [$1, $2]
54
88
  end
55
89
  end.compact
56
90
  end
@@ -1,5 +1,5 @@
1
1
  module Foreman
2
2
 
3
- VERSION = "0.47.0"
3
+ VERSION = "0.48.0.pre1"
4
4
 
5
5
  end
data/man/foreman.1 CHANGED
@@ -46,6 +46,10 @@ Specify an alternate Procfile to load, implies \fB\-d\fR at the Procfile root\.
46
46
  \fB\-p\fR, \fB\-\-port\fR
47
47
  Specify which port to use as the base for this application\. Should be a multiple of 1000\.
48
48
  .
49
+ .TP
50
+ \fB\-t\fR, \fB\-\-tmux\fR
51
+ Runs the processes in a tmux session\. Creates one window for each process and an extra window containing the output of each window (requires gawk)\.
52
+ .
49
53
  .P
50
54
  \fBforeman run\fR is used to run one\-off commands using the same environment as your defined processes\.
51
55
  .
@@ -3,188 +3,74 @@ require "foreman/cli"
3
3
 
4
4
  describe "Foreman::CLI", :fakefs do
5
5
  subject { Foreman::CLI.new }
6
- let(:engine) { subject.send(:engine) }
7
- let(:entries) { engine.procfile.entries.inject({}) { |h,e| h.update(e.name => e) } }
8
6
 
9
- describe "start" do
10
- describe "with a non-existent Procfile" do
11
- it "prints an error" do
12
- mock_error(subject, "Procfile does not exist.") do
13
- dont_allow.instance_of(Foreman::Engine).start
14
- subject.start
15
- end
16
- end
17
- end
7
+ describe ".foreman" do
8
+ before { File.open(".foreman", "w") { |f| f.puts "formation: alpha=2" } }
18
9
 
19
- describe "with a Procfile" do
20
- before(:each) { write_procfile }
21
-
22
- it "runs successfully" do
23
- dont_allow(subject).error
24
- mock.instance_of(Foreman::Engine).start
25
- subject.start
26
- end
27
-
28
- it "can run a single process" do
29
- dont_allow(subject).error
30
- stub(engine).watch_for_output
31
- stub(engine).watch_for_termination
32
- mock(entries["alpha"]).spawn(1, is_a(IO), engine.directory, {}, 5000) { [] }
33
- mock(entries["bravo"]).spawn(0, is_a(IO), engine.directory, {}, 5100) { [] }
34
- subject.start("alpha")
35
- end
10
+ it "provides default options" do
11
+ subject.send(:options)["formation"].should == "alpha=2"
36
12
  end
37
13
 
38
- describe "with an alternate root" do
39
- it "reads the Procfile from that root" do
40
- write_procfile "/some/app/Procfile"
41
- mock(Foreman::Procfile).new("/some/app/Procfile")
42
- mock.instance_of(Foreman::Engine).start
43
- foreman %{ start -d /some/app }
44
- end
14
+ it "is overridden by options at the cli" do
15
+ subject = Foreman::CLI.new([], :formation => "alpha=3")
16
+ subject.send(:options)["formation"].should == "alpha=3"
45
17
  end
46
18
  end
47
19
 
48
- describe "export" do
49
- describe "options" do
50
- it "uses .foreman" do
51
- write_procfile
52
- File.open(".foreman", "w") { |f| f.puts "concurrency: alpha=2" }
53
- mock_export = mock(Foreman::Export::Upstart)
54
- mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "concurrency" => "alpha=2" }) { mock_export }
55
- mock_export.export
56
- foreman %{ export upstart /upstart }
57
- end
58
-
59
- it "respects --env" do
60
- write_procfile
61
- write_env("envfile")
62
- mock_export = mock(Foreman::Export::Upstart)
63
- mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "env" => "envfile" }) { mock_export }
64
- mock_export.export
65
- foreman %{ export upstart /upstart --env envfile }
66
- end
67
- end
68
-
69
- describe "with a non-existent Procfile" do
70
- it "prints an error" do
20
+ describe "start" do
21
+ describe "when a Procfile doesnt exist", :fakefs do
22
+ it "displays an error" do
71
23
  mock_error(subject, "Procfile does not exist.") do
72
- dont_allow.instance_of(Foreman::Engine).export
73
- subject.export("testapp")
24
+ dont_allow.instance_of(Foreman::Engine).start
25
+ subject.start
74
26
  end
75
27
  end
76
28
  end
77
29
 
78
- describe "with a Procfile" do
79
- before(:each) { write_procfile }
80
-
81
- describe "with a formatter with a generic error" do
82
- before do
83
- mock(Foreman::Export).formatter("errorful") { Class.new(Foreman::Export::Base) do
84
- def export
85
- raise Foreman::Export::Exception.new("foo")
86
- end
87
- end }
88
- end
89
-
90
- it "prints an error" do
91
- mock_error(subject, "foo") do
92
- subject.export("errorful")
93
- end
30
+ describe "with a valid Procfile" do
31
+ it "can run a single command" do
32
+ without_fakefs do
33
+ output = foreman("start env -f #{resource_path("Procfile")}")
34
+ output.should =~ /env.1/
35
+ output.should_not =~ /test.1/
94
36
  end
95
37
  end
96
38
 
97
- describe "with a valid config" do
98
- before(:each) { write_foreman_config("testapp") }
99
-
100
- it "runs successfully" do
101
- dont_allow(subject).error
102
- mock_export = mock(Foreman::Export::Upstart)
103
- mock(Foreman::Export::Upstart).new("/tmp/foo", is_a(Foreman::Engine), {}) { mock_export }
104
- mock_export.export
105
- subject.export("upstart", "/tmp/foo")
39
+ it "can run all commands" do
40
+ without_fakefs do
41
+ output = foreman("start -f #{resource_path("Procfile")} -e #{resource_path(".env")}")
42
+ output.should =~ /echo.1 \| echoing/
43
+ output.should =~ /env.1 \| bar/
44
+ output.should =~ /test.1 \| testing/
106
45
  end
107
46
  end
108
47
  end
109
48
  end
110
49
 
111
50
  describe "check" do
112
- describe "with a valid Procfile" do
113
- before { write_procfile }
114
-
115
- it "displays the jobs" do
116
- mock(subject).puts("valid procfile detected (alpha, bravo)")
117
- subject.check
118
- end
51
+ it "with a valid Procfile displays the jobs" do
52
+ write_procfile
53
+ foreman("check").should == "valid procfile detected (alpha, bravo)\n"
119
54
  end
120
55
 
121
- describe "with a blank Procfile" do
122
- before do
123
- FileUtils.touch("Procfile")
124
- end
125
-
126
- it "displays an error" do
127
- mock_error(subject, "no processes defined") do
128
- subject.check
129
- end
130
- end
56
+ it "with a blank Procfile displays an error" do
57
+ FileUtils.touch "Procfile"
58
+ foreman("check").should == "ERROR: no processes defined\n"
131
59
  end
132
60
 
133
- describe "without a Procfile" do
134
- it "displays an error" do
135
- mock_error(subject, "Procfile does not exist.") do
136
- subject.check
137
- end
138
- end
61
+ it "without a Procfile displays an error" do
62
+ FileUtils.rm_f "Procfile"
63
+ foreman("check").should == "ERROR: Procfile does not exist.\n"
139
64
  end
140
65
  end
141
66
 
142
67
  describe "run" do
143
- describe "with a valid Procfile" do
144
- before { write_procfile }
145
-
146
- describe "and a command" do
147
- let(:command) { ["ls", "-l", "foo bar"] }
148
-
149
- before(:each) do
150
- stub(subject).exec
151
- end
152
-
153
- it "should load the environment file" do
154
- write_env
155
- preserving_env do
156
- subject.run *command
157
- ENV["FOO"].should == "bar"
158
- end
159
-
160
- ENV["FOO"].should be_nil
161
- end
162
-
163
- it "should exec the argument list as a shell command" do
164
- mock(subject).exec(command.shelljoin)
165
- subject.run *command
166
- end
167
- end
168
-
169
- describe "and a non-existent command" do
170
- let(:command) { "iuhtngrglhulhdfg" }
171
-
172
- it "should print an error" do
173
- mock_error(subject, "command not found: #{command}") do
174
- subject.run command
175
- end
176
- end
177
- end
178
-
179
- describe "and a non-executable command" do
180
- let(:command) { __FILE__ }
68
+ it "can run a command" do
69
+ forked_foreman("run echo 1").should == "1\n"
70
+ end
181
71
 
182
- it "should print an error" do
183
- mock_error(subject, "not executable: #{command}") do
184
- subject.run command
185
- end
186
- end
187
- end
72
+ it "includes the environment" do
73
+ forked_foreman("run #{resource_path("bin/env FOO")} -e #{resource_path(".env")}").should == "bar\n"
188
74
  end
189
75
  end
190
76