Compare commits

...
62 Commits
Author SHA1 Message Date
David Dollar a278755ae4 0.34.0 2012-01-16 09:42:07 -05:00
David Dollar 3367a060a7 update man page 2012-01-16 09:41:36 -05:00
David Dollar ac7e0743ac update docs for -d 2012-01-16 09:39:54 -05:00
David Dollar e574880814 Merge pull request #101 from ndbroadbent/foreman
---

I just discovered the LiveReload gem, and wanted to use foreman to help me set up my development environments.

I didnt want to check in my custom development Procfiles, so I needed to alter the behaviour of:

> [The Procfiles] containing directory will be assumed to be the root directory of the application.

Ive set up some shared `Procfiles` for development, such as `Rails3Dev`, `Rails31Dev`, `JekyllDev`, etc.

Then I set up a bash alias for each of these Procfiles, such as:

```bash
alias rd31="foreman start -d . -f ~/dev/procfiles/Rails31Dev"
```

The only thing missing was the `-d` flag.

My `Rails31Dev` file looks like this:

```yaml
compass: compass watch --sass-dir app/assets/stylesheets --css-dir public/assets
livereload: livereload
passenger: passenger start
```

Thanks!

Conflicts:
	lib/foreman/cli.rb
2012-01-16 09:38:25 -05:00
Craig R Webster 7132cacbf6 Wrap around to the first colour when all the colours are used 2012-01-16 09:35:51 -05:00
David Dollar c1f279aa6f run specs in random order 2012-01-16 09:33:34 -05:00
David Dollar 34cfe9ef9d update rspec 2012-01-16 09:33:34 -05:00
David Dollar 79fc3b8029 pedantry 2012-01-16 09:33:34 -05:00
Matthijs Langenberg 91140638e1 Set executable bit on runit run scripts. 2012-01-16 09:33:34 -05:00
David Dollar 48cc60c30f Merge pull request #114 from gburt/master
add more colors
2012-01-16 06:21:28 -08:00
David Dollar 533139ea9f 0.33.1 2012-01-16 09:18:48 -05:00
David Dollar 86e2056a24 Merge pull request #129 from fnichol/resolve-home-template
Expand template path under user's home directory (foreman export).
2012-01-16 06:17:43 -08:00
Fletcher Nichol ab29963ee4 Expand template path under user's home directory.
* File.join won't expand `~` into `ENV['HOME']`
  (http://ruby-doc.org/core-1.9.3/File.html#method-c-expand_path)
* The FakeFS File.exists? implementation calls FileSystem#find
  (https://github.com/defunkt/fakefs/blob/master/lib/fakefs/file_system.rb#L22-33)
  containing a call to FileSystem#normalize_path which expands the
  path variable passed in
  (https://github.com/defunkt/fakefs/blob/master/lib/fakefs/file_system.rb#L91-98)
* The file system mocking library sets up a false expectation that `~`
  will be expanded in the #export_template method and consequently the
  production code can't use the template directory
* To guard against future regressions such as fixes/updates to FakeFS or
  using an alternate file system mocking library, the specs were updated
  to explicitly set `ENV['HOME']`
2012-01-15 19:09:52 -07:00
David Dollar cf269c39da 0.33.0 2012-01-15 13:00:45 -05:00
David Dollar 76cd2e794b Revert "Merge pull request #125 from brainopia/master"
It appears that this is causing issues with process termination.

This reverts commit d2c9ce0f34, reversing
changes made to 98337c92e1.
2012-01-15 12:59:47 -05:00
David Dollar 83748cb538 0.32.0 2012-01-12 15:25:43 -08:00
David Dollar d2c9ce0f34 Merge pull request #125 from brainopia/master
Support for complex cmds in Procfile
2012-01-12 15:23:15 -08:00
David Dollar 98337c92e1 Merge pull request #121 from Viximo/feature/run
Add "exec" action to allow execution of commands within the app environment
2012-01-09 16:02:42 -08:00
Matt Griffin 33d738b3f8 Return some whitespace that was accidentally removed 2012-01-09 17:15:20 -05:00
Matt Griffin 9432989fbe Steal the run method back from Thor so that it can be used in place for exec for running commands in the foreman environment.
Fix some error reporting.
2012-01-09 17:11:32 -05:00
brainopia 66b1483a75 Remove old cruft 2012-01-08 10:18:48 +07:00
brainopia 64bd4db128 In case someone wants to use bin/runner directly 2012-01-08 10:15:23 +07:00
brainopia b561555f3a Fix for double fork 2012-01-08 09:42:51 +07:00
brainopia baa7b7685c Use ruby exec which works with escaped cmd and replaces shell 2012-01-07 20:19:57 +07:00
brainopia cfa6e6f259 Fix foreman to work with cmds containing pipes and redirects 2012-01-07 18:19:54 +07:00
Matt Griffin a34bc59721 Add "exec" action to allow execution of arbitrary commands with the app's environment. 2012-01-04 15:22:10 -05:00
David Dollar 07e8ca4a4b tweak readme 2012-01-04 12:36:34 -05:00
David Dollar 342d30bbb8 0.31.0 2012-01-04 12:16:51 -05:00
David Dollar 268dd6240e make fork more robust 2012-01-04 12:15:55 -05:00
David Dollar 9e60b3e1a4 remove unnecessary debug 2012-01-04 12:15:38 -05:00
David Dollar 1c6285f8af add more information when shutting down 2012-01-04 12:15:17 -05:00
Gabriel Burt 5de1bd18ac add more colors 2011-12-30 13:55:46 -06:00
David Dollar fff15bc627 Merge pull request #110 from lstoll/master
Different port range for each process type on 'foreman start'
2011-12-24 22:47:36 -08:00
Lincoln Stoll a66157d611 Use different port ranges for each process type 2011-12-25 15:46:21 +11:00
David Dollar fcfa913fb0 0.30.1 2011-12-23 08:48:34 -05:00
David Dollar fc438472f9 require thread for mutex 2011-12-23 08:48:17 -05:00
David Dollar fc95936327 0.30.0 2011-12-22 16:34:02 -05:00
David Dollar 0c27f78d46 compatibility with ruby 1.8 2011-12-22 16:33:49 -05:00
David Dollar 356c61f471 0.29.0 2011-12-22 01:03:11 -05:00
David Dollar dcff4da220 0.28.0.pre2 2011-12-08 17:59:40 -08:00
David Dollar 888520ee99 fix pipe error 2011-12-08 17:59:27 -08:00
David Dollar c7b6b334fd 0.28.0.pre1 2011-12-08 17:54:19 -08:00
David Dollar f476920a05 Merge branch 'fork' 2011-12-08 17:53:50 -08:00
David Dollar 5436b68cf1 wip 2011-12-08 17:53:13 -08:00
David Dollar c9411cd2b1 wip 2011-12-08 17:18:21 -08:00
David Dollar 6e95d1ce94 wip 2011-12-08 16:57:33 -08:00
David Dollar c5548a345e wip 2011-12-08 16:19:19 -08:00
David Dollar f668b87660 wip 2011-12-08 14:04:29 -08:00
David Dollar 914a1ee958 0.27.0 2011-12-05 15:46:40 -05:00
David Dollar e1c2946718 add changelog 2011-12-05 15:46:22 -05:00
David Dollar 6160246da0 Merge pull request #103 from csquared/load_env_from_irb
Load env from irb
2011-12-05 12:37:05 -08:00
Chris Continanza 2e8030dbd4 refactor load_env to apply_environment 2011-12-05 12:36:23 -08:00
Chris Continanza 58e4cdafd7 rename load! to load_env! 2011-12-05 12:27:32 -08:00
Chris Continanza 44a5dff724 use ./.env as default 2011-12-05 11:37:51 -08:00
Chris Continanza e33921f083 load contents from env file 2011-12-03 15:43:51 -08:00
Chris Continanza 79cf541ebe refactor engine to expose env methods 2011-12-03 15:43:07 -08:00
Nathan Broadbent 8bc8cb4b2e Added option to specify app_root, if executing a Procfile from a shared location 2011-12-03 15:16:06 +08:00
David Dollar 39ace26ae1 disable email notifications 2011-11-14 12:23:50 -05:00
David Dollar c383359136 add travis config 2011-11-11 23:16:59 -05:00
David Dollar a5e094353c 0.26.1 2011-11-10 15:02:19 -05:00
David Dollar 12720b4c12 fix colors during execution of single process 2011-11-10 15:02:09 -05:00
David Dollar 1c732a4658 add runit export to docs 2011-11-08 18:00:23 -05:00
30 changed files with 459 additions and 180 deletions
+14
View File
@@ -0,0 +1,14 @@
script: bundle exec rake spec
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
notifications:
email: false
webhooks:
on_success: always
on_failure: always
urls:
- https://dx-helper.herokuapp.com/travis
+11
View File
@@ -0,0 +1,11 @@
0.26.1 12/05/2011 6160246da0fafe9cf8fde188d94bbc6babc667dc
==========================================================
Merge pull request #103 from csquared/load_env_from_irb [David Dollar]
refactor load_env to apply_environment [Chris Continanza]
rename load! to load_env! [Chris Continanza]
use ./.env as default [Chris Continanza]
load contents from env file [Chris Continanza]
refactor engine to expose env methods [Chris Continanza]
disable email notifications [David Dollar]
add travis config [David Dollar]
+1 -1
View File
@@ -9,7 +9,7 @@ group :development do
gem 'fakefs', '~> 0.3.2' gem 'fakefs', '~> 0.3.2'
gem 'rcov', '~> 0.9.8' gem 'rcov', '~> 0.9.8'
gem 'rr', '~> 1.0.2' gem 'rr', '~> 1.0.2'
gem 'rspec', '~> 2.6.0' gem 'rspec', '~> 2.0'
gem 'aws-s3' gem 'aws-s3'
gem "rubyzip" gem "rubyzip"
end end
+11 -11
View File
@@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
foreman (0.26.0) foreman (0.34.0)
term-ansicolor (~> 1.0.5) term-ansicolor (~> 1.0.5)
thor (>= 0.13.6) thor (>= 0.13.6)
@@ -14,7 +14,7 @@ GEM
xml-simple xml-simple
builder (3.0.0) builder (3.0.0)
crack (0.1.8) crack (0.1.8)
diff-lcs (1.1.2) diff-lcs (1.1.3)
fakefs (0.3.2) fakefs (0.3.2)
hpricot (0.8.2) hpricot (0.8.2)
mime-types (1.16) mime-types (1.16)
@@ -33,16 +33,16 @@ GEM
mustache (>= 0.7.0) mustache (>= 0.7.0)
rdiscount (>= 1.5.8) rdiscount (>= 1.5.8)
rr (1.0.2) rr (1.0.2)
rspec (2.6.0) rspec (2.8.0)
rspec-core (~> 2.6.0) rspec-core (~> 2.8.0)
rspec-expectations (~> 2.6.0) rspec-expectations (~> 2.8.0)
rspec-mocks (~> 2.6.0) rspec-mocks (~> 2.8.0)
rspec-core (2.6.4) rspec-core (2.8.0)
rspec-expectations (2.6.0) rspec-expectations (2.8.0)
diff-lcs (~> 1.1.2) diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0) rspec-mocks (2.8.0)
rubyzip (0.9.4) rubyzip (0.9.4)
term-ansicolor (1.0.6) term-ansicolor (1.0.7)
thor (0.14.6) thor (0.14.6)
xml-simple (1.0.15) xml-simple (1.0.15)
@@ -58,5 +58,5 @@ DEPENDENCIES
rcov (~> 0.9.8) rcov (~> 0.9.8)
ronn ronn
rr (~> 1.0.2) rr (~> 1.0.2)
rspec (~> 2.6.0) rspec (~> 2.0)
rubyzip rubyzip
+2 -1
View File
@@ -20,7 +20,8 @@ http://blog.daviddollar.org/2011/05/06/introducing-foreman.html
## Manual ## Manual
See the [man page](http://ddollar.github.com/foreman) for usage. * [man page](http://ddollar.github.com/foreman)
* [wiki](http://github.com/ddollar/foreman/wiki)
## Authorship ## Authorship
+19
View File
@@ -166,3 +166,22 @@ end
Dir[File.expand_path("../dist/**/*.rake", __FILE__)].each do |rake| Dir[File.expand_path("../dist/**/*.rake", __FILE__)].each do |rake|
import rake import rake
end end
task :changelog do
timestamp = Time.now.utc.strftime('%m/%d/%Y')
sha = `git log | head -1`.split(' ').last
changelog = ["#{version} #{timestamp} #{sha}"]
changelog << ('=' * changelog[0].length)
changelog << ''
last_sha = `cat Changelog | head -1`.split(' ').last
shortlog = `git log #{last_sha}..HEAD --pretty=format:'%s [%an]'`
changelog << shortlog.split("\n")
changelog.concat ['', '', '']
old_changelog = File.read('Changelog')
File.open('Changelog', 'w') do |file|
file.write(changelog.join("\n"))
file.write(old_changelog)
end
end
Executable
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
exec $1 2>&1
+2 -2
View File
@@ -3,7 +3,7 @@ Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/
app.uid = "<%= user %>" app.uid = "<%= user %>"
app.gid = "<%= user %>" app.gid = "<%= user %>"
<% engine.processes.each do |process| %> <% engine.procfile.entries.each do |process| %>
<% 1.upto(concurrency[process.name]) do |num| %> <% 1.upto(concurrency[process.name]) do |num| %>
<% port = engine.port_for(process, num, options[:port]) %> <% port = engine.port_for(process, num, options[:port]) %>
app.process("<%= process.name %>-<%=num%>") do |process| app.process("<%= process.name %>-<%=num%>") do |process|
@@ -19,7 +19,7 @@ Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/
process.monitor_children do |children| process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}" children.stop_command "kill -QUIT {{PID}}"
end end
process.group = "<%= app %>-<%= process.name %>" process.group = "<%= app %>-<%= process.name %>"
end end
<% end %> <% end %>
+10
View File
@@ -4,5 +4,15 @@ module Foreman
class AppDoesNotExist < Exception; end class AppDoesNotExist < Exception; end
# load contents of env_file into ENV
def self.load_env!(env_file = './.env')
require 'foreman/engine'
Foreman::Engine.load_env!(env_file)
end
def self.runner
File.expand_path("../../bin/runner", __FILE__)
end
end end
+27 -13
View File
@@ -5,24 +5,26 @@ require "thor"
require "yaml" require "yaml"
class Foreman::CLI < Thor class Foreman::CLI < Thor
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile" class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
class_option :app_root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
desc "start [PROCESS]", "Start the application, or a specific process"
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env" method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
method_option :port, :type => :numeric, :aliases => "-p" method_option :port, :type => :numeric, :aliases => "-p"
method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"' method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
def start(process=nil) class << self
check_procfile! # Hackery. Take the run method away from Thor so that we can redefine it.
def is_thor_reserved_word?(word, type)
if process return false if word == 'run'
engine.execute(process) super
else
engine.start
end end
end end
def start
check_procfile!
engine.start
end
desc "export FORMAT LOCATION", "Export the application to another process management format" desc "export FORMAT LOCATION", "Export the application to another process management format"
@@ -55,10 +57,23 @@ class Foreman::CLI < Thor
desc "check", "Validate your application's Procfile" desc "check", "Validate your application's Procfile"
def check def check
error "no processes defined" unless engine.processes.length > 0 error "no processes defined" unless engine.procfile.entries.length > 0
display "valid procfile detected (#{engine.processes.map(&:name).join(', ')})" display "valid procfile detected (#{engine.procfile.process_names.join(', ')})"
end end
desc "run COMMAND", "Run a command using your application's environment"
def run(*args)
engine.apply_environment!
begin
exec args.join(" ")
rescue Errno::EACCES
error "not executable: #{args.first}"
rescue Errno::ENOENT
error "command not found: #{args.first}"
end
end
private ###################################################################### private ######################################################################
def check_procfile! def check_procfile!
@@ -92,5 +107,4 @@ private ######################################################################
defaults = YAML::load_file(".foreman") || {} defaults = YAML::load_file(".foreman") || {}
Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options)) Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
end end
end end
+110 -86
View File
@@ -7,54 +7,46 @@ require "tempfile"
require "timeout" require "timeout"
require "term/ansicolor" require "term/ansicolor"
require "fileutils" require "fileutils"
require "thread"
class Foreman::Engine class Foreman::Engine
attr_reader :procfile attr_reader :procfile
attr_reader :directory attr_reader :directory
attr_reader :environment
attr_reader :options attr_reader :options
extend Term::ANSIColor extend Term::ANSIColor
COLORS = [ cyan, yellow, green, magenta, red ] COLORS = [ cyan, yellow, green, magenta, red, blue,
intense_cyan, intense_yellow, intense_green, intense_magenta,
intense_red, intense_blue ]
def initialize(procfile, options={}) def initialize(procfile, options={})
@procfile = Foreman::Procfile.new(procfile) @procfile = Foreman::Procfile.new(procfile)
@directory = File.expand_path(File.dirname(procfile)) @directory = options[:app_root] || File.expand_path(File.dirname(procfile))
@options = options @options = options
@environment = read_environment_files(options[:env]) @environment = read_environment_files(options[:env])
@output_mutex = Mutex.new
end
def self.load_env!(env_file)
@environment = read_environment_files(env_file)
apply_environment!
end end
def start def start
proctitle "ruby: foreman master" proctitle "ruby: foreman master"
termtitle "#{File.basename(@directory)} - foreman (#{processes.size} processes)" termtitle "#{File.basename(@directory)} - foreman"
processes.each do |process|
process.color = next_color
fork process
end
trap("TERM") { puts "SIGTERM received"; terminate_gracefully } trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
trap("INT") { puts "SIGINT received"; terminate_gracefully } trap("INT") { puts "SIGINT received"; terminate_gracefully }
assign_colors
spawn_processes
watch_for_output
watch_for_termination watch_for_termination
end end
def execute(name)
error "no such process: #{name}" unless procfile[name]
fork procfile[name]
trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
trap("INT") { puts "SIGINT received"; terminate_gracefully }
watch_for_termination
end
def processes
procfile.processes
end
def port_for(process, num, base_port=nil) def port_for(process, num, base_port=nil)
base_port ||= 5000 base_port ||= 5000
offset = procfile.process_names.index(process.name) * 100 offset = procfile.process_names.index(process.name) * 100
@@ -63,51 +55,25 @@ class Foreman::Engine
private ###################################################################### private ######################################################################
def fork(process) def spawn_processes
concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency]) concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency])
1.upto(concurrency[process.name]) do |num| procfile.entries.each do |entry|
fork_individual(process, num, port_for(process, num, @options[:port])) reader, writer = IO.pipe
entry.spawn(concurrency[entry.name], writer, @directory, @environment, port_for(entry, 1, base_port)).each do |process|
running_processes[process.pid] = process
readers[process] = reader
end
end end
end end
def fork_individual(process, num, port) def base_port
@environment.each { |k,v| ENV[k] = v } options[:port] || 5000
ENV["PORT"] = port.to_s
ENV["PS"] = "#{process.name}.#{num}"
pid = Process.fork do
run(process)
end
info "started with pid #{pid}", process
running_processes[pid] = process
end
def run(process)
proctitle "ruby: foreman #{process.name}"
trap("SIGINT", "IGNORE")
begin
Dir.chdir directory do
PTY.spawn(process.command) do |stdin, stdout, pid|
trap("SIGTERM") { Process.kill("SIGTERM", pid) }
until stdin.eof?
info stdin.gets, process
end
end
end
rescue PTY::ChildExited, Interrupt, Errno::EIO, Errno::ENOENT
begin
info "process exiting", process
rescue Interrupt
end
end
end end
def kill_all(signal="SIGTERM") def kill_all(signal="SIGTERM")
running_processes.each do |pid, process| running_processes.each do |pid, process|
info "sending #{signal} to pid #{pid}"
Process.kill(signal, pid) rescue Errno::ESRCH Process.kill(signal, pid) rescue Errno::ESRCH
end end
end end
@@ -115,27 +81,57 @@ private ######################################################################
def terminate_gracefully def terminate_gracefully
info "sending SIGTERM to all processes" info "sending SIGTERM to all processes"
kill_all "SIGTERM" kill_all "SIGTERM"
Timeout.timeout(3) { Process.waitall } Timeout.timeout(5) { Process.waitall }
rescue Timeout::Error rescue Timeout::Error
info "sending SIGKILL to all processes" info "sending SIGKILL to all processes"
kill_all "SIGKILL" kill_all "SIGKILL"
end end
def watch_for_output
Thread.new do
begin
loop do
rs, ws = IO.select(readers.values, [], [], 1)
(rs || []).each do |r|
ps, message = r.gets.split(",", 2)
color = colors[ps.split(".").first]
info message, ps, color
end
end
rescue Exception => ex
puts ex.message
puts ex.backtrace
end
end
end
def watch_for_termination def watch_for_termination
pid, status = Process.wait2 pid, status = Process.wait2
process = running_processes.delete(pid) process = running_processes.delete(pid)
info "process terminated", process info "process terminated", process.name
terminate_gracefully terminate_gracefully
kill_all kill_all
rescue Errno::ECHILD rescue Errno::ECHILD
end end
def info(message, process=nil) def info(message, name="system", color=Term::ANSIColor.white)
print process.color if process print color
print "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(process)} | " print "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
print Term::ANSIColor.reset print Term::ANSIColor.reset
print message.chomp print message.chomp
puts puts ""
end
def print(message=nil)
@output_mutex.synchronize do
$stdout.print message
end
end
def puts(message=nil)
@output_mutex.synchronize do
$stdout.puts message
end
end end
def error(message) def error(message)
@@ -151,9 +147,8 @@ private ######################################################################
end end
end end
def pad_process_name(process) def pad_process_name(name="system")
name = process ? "#{ENV["PS"]}" : "system" name.to_s.ljust(longest_process_name + 3) # add 3 for process number padding
name.ljust(longest_process_name + 3) # add 3 for process number padding
end end
def proctitle(title) def proctitle(title)
@@ -168,33 +163,62 @@ private ######################################################################
@running_processes ||= {} @running_processes ||= {}
end end
def readers
@readers ||= {}
end
def colors
@colors ||= {}
end
def assign_colors
procfile.entries.each do |entry|
colors[entry.name] = next_color
end
end
def process_by_reader(reader)
readers.invert[reader]
end
def next_color def next_color
@current_color ||= -1 @current_color ||= -1
@current_color += 1 @current_color += 1
@current_color >= COLORS.length ? "" : COLORS[@current_color] @current_color = 0 if COLORS.length < @current_color
COLORS[@current_color]
end end
def read_environment_files(filenames) module Env
environment = {} attr_reader :environment
(filenames || "").split(",").map(&:strip).each do |filename| def read_environment_files(filenames)
error "No such file: #{filename}" unless File.exists?(filename) environment = {}
environment.merge!(read_environment(filename))
end
environment.merge!(read_environment(".env")) unless filenames (filenames || "").split(",").map(&:strip).each do |filename|
environment error "No such file: #{filename}" unless File.exists?(filename)
end environment.merge!(read_environment(filename))
def read_environment(filename)
return {} unless File.exists?(filename)
File.read(filename).split("\n").inject({}) do |hash, line|
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
hash[$1] = $2
end end
hash
environment.merge!(read_environment(".env")) unless filenames
environment
end
def read_environment(filename)
return {} unless File.exists?(filename)
File.read(filename).split("\n").inject({}) do |hash, line|
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
hash[$1] = $2
end
hash
end
end
def apply_environment!
@environment.each { |k,v| ENV[k] = v }
end end
end end
include Env
extend Env
end end
+1 -1
View File
@@ -26,7 +26,7 @@ private ######################################################################
def export_template(exporter, file, template_root) def export_template(exporter, file, template_root)
if template_root && File.exist?(file_path = File.join(template_root, file)) if template_root && File.exist?(file_path = File.join(template_root, file))
File.read(file_path) File.read(file_path)
elsif File.exist?(file_path = File.join("~/.foreman/templates", file)) elsif File.exist?(file_path = File.expand_path(File.join("~/.foreman/templates", file)))
File.read(file_path) File.read(file_path)
else else
File.read(File.expand_path("../../../../data/export/#{exporter}/#{file}", __FILE__)) File.read(File.expand_path("../../../../data/export/#{exporter}/#{file}", __FILE__))
+1 -1
View File
@@ -12,7 +12,7 @@ class Foreman::Export::Inittab < Foreman::Export::Base
inittab = [] inittab = []
inittab << "# ----- foreman #{app} processes -----" inittab << "# ----- foreman #{app} processes -----"
engine.processes.inject(1) do |index, process| engine.procfile.entries.inject(1) do |index, process|
1.upto(concurrency[process.name]) do |num| 1.upto(concurrency[process.name]) do |num|
id = app.slice(0, 2).upcase + sprintf("%02d", index) id = app.slice(0, 2).upcase + sprintf("%02d", index)
port = engine.port_for(process, num, options[:port]) port = engine.port_for(process, num, options[:port])
+17 -16
View File
@@ -3,58 +3,59 @@ require "foreman/export"
class Foreman::Export::Runit < Foreman::Export::Base class Foreman::Export::Runit < Foreman::Export::Base
ENV_VARIABLE_REGEX = /([a-zA-Z_]+[a-zA-Z0-9_]*)=(\S+)/ ENV_VARIABLE_REGEX = /([a-zA-Z_]+[a-zA-Z0-9_]*)=(\S+)/
def export(location, options={}) def export(location, options={})
error("Must specify a location") unless location error("Must specify a location") unless location
app = options[:app] || File.basename(engine.directory) app = options[:app] || File.basename(engine.directory)
user = options[:user] || app user = options[:user] || app
log_root = options[:log] || "/var/log/#{app}" log_root = options[:log] || "/var/log/#{app}"
template_root = options[:template] template_root = options[:template]
concurrency = Foreman::Utils.parse_concurrency(options[:concurrency]) concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
run_template = export_template('runit', 'run.erb', template_root) run_template = export_template('runit', 'run.erb', template_root)
log_run_template = export_template('runit', 'log_run.erb', template_root) log_run_template = export_template('runit', 'log_run.erb', template_root)
engine.processes.each do |process| engine.procfile.entries.each do |process|
1.upto(concurrency[process.name]) do |num| 1.upto(concurrency[process.name]) do |num|
process_directory = "#{location}/#{app}-#{process.name}-#{num}" process_directory = "#{location}/#{app}-#{process.name}-#{num}"
process_env_directory = "#{process_directory}/env" process_env_directory = "#{process_directory}/env"
process_log_directory = "#{process_directory}/log" process_log_directory = "#{process_directory}/log"
create_directory process_directory create_directory process_directory
create_directory process_env_directory create_directory process_env_directory
create_directory process_log_directory create_directory process_log_directory
run = ERB.new(run_template).result(binding) run = ERB.new(run_template).result(binding)
write_file "#{process_directory}/run", run write_file "#{process_directory}/run", run
FileUtils.chmod 0755, "#{process_directory}/run"
port = engine.port_for(process, num, options[:port]) port = engine.port_for(process, num, options[:port])
environment_variables = {'PORT' => port}. environment_variables = {'PORT' => port}.
merge(engine.environment). merge(engine.environment).
merge(inline_variables(process.command)) merge(inline_variables(process.command))
environment_variables.each_pair do |var, env| environment_variables.each_pair do |var, env|
write_file "#{process_env_directory}/#{var.upcase}", env write_file "#{process_env_directory}/#{var.upcase}", env
end end
log_run = ERB.new(log_run_template).result(binding) log_run = ERB.new(log_run_template).result(binding)
write_file "#{process_log_directory}/run", log_run write_file "#{process_log_directory}/run", log_run
FileUtils.chmod 0755, "#{process_log_directory}/run"
end end
end end
end end
private private
def create_directory(location) def create_directory(location)
say "creating: #{location}" say "creating: #{location}"
FileUtils.mkdir(location) FileUtils.mkdir(location)
end end
def inline_variables(command) def inline_variables(command)
variable_name_regex = variable_name_regex =
Hash[*command.scan(ENV_VARIABLE_REGEX).flatten] Hash[*command.scan(ENV_VARIABLE_REGEX).flatten]
end end
end end
+1 -1
View File
@@ -26,7 +26,7 @@ class Foreman::Export::Upstart < Foreman::Export::Base
process_template = export_template("upstart", "process.conf.erb", template_root) process_template = export_template("upstart", "process.conf.erb", template_root)
engine.processes.each do |process| engine.procfile.entries.each do |process|
next if (conc = concurrency[process.name]) < 1 next if (conc = concurrency[process.name]) < 1
process_master_template = export_template("upstart", "process_master.conf.erb", template_root) process_master_template = export_template("upstart", "process_master.conf.erb", template_root)
process_master_config = ERB.new(process_master_template).result(binding) process_master_config = ERB.new(process_master_template).result(binding)
+60 -6
View File
@@ -2,13 +2,67 @@ require "foreman"
class Foreman::Process class Foreman::Process
attr_reader :name attr_reader :entry
attr_reader :command attr_reader :num
attr_accessor :color attr_reader :pid
attr_reader :port
def initialize(name, command) def initialize(entry, num, port)
@name = name @entry = entry
@command = command @num = num
@port = port
end
def run(pipe, basedir, environment)
Dir.chdir(basedir) do
with_environment(environment.merge("PORT" => port.to_s)) do
run_process entry.command, pipe
end
end
end
def name
"%s.%s" % [ entry.name, num ]
end
private
def fork_with_io(command)
reader, writer = IO.pipe
pid = fork do
trap("INT", "IGNORE")
$stdout.reopen writer
reader.close
exec Foreman.runner, replace_command_env(command)
end
[ reader, pid ]
end
def run_process(command, pipe)
io, @pid = fork_with_io(command)
output pipe, "started with pid %d" % @pid
Thread.new do
until io.eof?
output pipe, io.gets
end
end
end
def output(pipe, message)
pipe.puts "%s,%s" % [ name, message ]
end
def replace_command_env(command)
command.gsub(/\$(\w+)/) { |e| ENV[e[1..-1]] }
end
def with_environment(environment)
old_env = ENV.each_pair.inject({}) { |h,(k,v)| h.update(k => v) }
environment.each { |k,v| ENV[k] = v }
ret = yield
ENV.clear
old_env.each { |k,v| ENV[k] = v}
ret
end end
end end
+9 -8
View File
@@ -1,4 +1,5 @@
require "foreman" require "foreman"
require "foreman/procfile_entry"
# A valid Procfile entry is captured by this regex. # A valid Procfile entry is captured by this regex.
# All other lines are ignored. # All other lines are ignored.
@@ -10,18 +11,18 @@ require "foreman"
# #
class Foreman::Procfile class Foreman::Procfile
attr_reader :processes attr_reader :entries
def initialize(filename) def initialize(filename)
@processes = parse_procfile(filename) @entries = parse_procfile(filename)
end
def process_names
processes.map(&:name)
end end
def [](name) def [](name)
processes.detect { |process| process.name == name } entries.detect { |entry| entry.name == name }
end
def process_names
entries.map(&:name)
end end
private private
@@ -29,7 +30,7 @@ private
def parse_procfile(filename) def parse_procfile(filename)
File.read(filename).split("\n").map do |line| File.read(filename).split("\n").map do |line|
if line =~ /^([A-Za-z0-9_]+):\s*(.+)$/ if line =~ /^([A-Za-z0-9_]+):\s*(.+)$/
Foreman::Process.new($1, $2) Foreman::ProcfileEntry.new($1, $2)
end end
end.compact end.compact
end end
+22
View File
@@ -0,0 +1,22 @@
require "foreman"
class Foreman::ProcfileEntry
attr_reader :name
attr_reader :command
attr_accessor :color
def initialize(name, command)
@name = name
@command = command
end
def spawn(num, pipe, basedir, environment, base_port)
(1..num).to_a.map do |n|
process = Foreman::Process.new(self, n, base_port + (n-1))
process.run(pipe, basedir, environment)
process
end
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
module Foreman module Foreman
VERSION = "0.26.0" VERSION = "0.34.0"
end end
+8 -1
View File
@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3 .\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3
. .
.TH "FOREMAN" "1" "November 2011" "Foreman 0.25.0" "Foreman Manual" .TH "FOREMAN" "1" "January 2012" "Foreman 0.33.1" "Foreman Manual"
. .
.SH "NAME" .SH "NAME"
\fBforeman\fR \- manage Procfile\-based applications \fBforeman\fR \- manage Procfile\-based applications
@@ -68,6 +68,10 @@ Specify the user the application should be run as\. Defaults to the app name
These options control all modes of foreman\'s operation\. These options control all modes of foreman\'s operation\.
. .
.TP .TP
\fB\-d\fR, \fB\-\-directory\fR
Specify an alternate application root\. This defaults to the directory containing the Procfile\.
.
.TP
\fB\-e\fR, \fB\-\-env\fR \fB\-e\fR, \fB\-\-env\fR
Specify an alternate environment file\. You can specify more than one file by using: \fB\-\-env file1,file2\fR\. Specify an alternate environment file\. You can specify more than one file by using: \fB\-\-env file1,file2\fR\.
. .
@@ -85,6 +89,9 @@ bluepill
inittab inittab
. .
.IP "\(bu" 4 .IP "\(bu" 4
runit
.
.IP "\(bu" 4
upstart upstart
. .
.IP "" 0 .IP "" 0
+6
View File
@@ -66,6 +66,10 @@ The following options control how the application is run:
These options control all modes of foreman's operation. These options control all modes of foreman's operation.
* `-d`, `--directory`:
Specify an alternate application root. This defaults to the directory
containing the Procfile.
* `-e`, `--env`: * `-e`, `--env`:
Specify an alternate environment file. You can specify more than one Specify an alternate environment file. You can specify more than one
file by using: `--env file1,file2`. file by using: `--env file1,file2`.
@@ -83,6 +87,8 @@ foreman currently supports the following output formats:
* inittab * inittab
* runit
* upstart * upstart
## INITTAB EXPORT ## INITTAB EXPORT
+49
View File
@@ -89,5 +89,54 @@ describe "Foreman::CLI" do
end end
end end
end end
describe "run" do
describe "with a valid Procfile" do
before { write_procfile }
describe "and a command" do
let(:command) { ["ls", "-l"] }
before(:each) do
stub(subject).exec
end
it "should load the environment file" do
write_env
preserving_env do
subject.run *command
ENV["FOO"].should == "bar"
end
ENV["FOO"].should be_nil
end
it "should runute the command as a string" do
mock(subject).exec(command.join(" "))
subject.run *command
end
end
describe "and a non-existent command" do
let(:command) { "iuhtngrglhulhdfg" }
it "should print an error" do
mock_error(subject, "command not found: #{command}") do
subject.run command
end
end
end
describe "and a non-executable command" do
let(:command) { __FILE__ }
it "should print an error" do
mock_error(subject, "not executable: #{command}") do
subject.run command
end
end
end
end
end
end end
+12 -25
View File
@@ -24,8 +24,9 @@ describe "Foreman::Engine" do
describe "start" do describe "start" do
it "forks the processes" do it "forks the processes" do
write_procfile write_procfile
mock(subject).fork(subject.procfile["alpha"]) mock.instance_of(Foreman::Process).run_process("./alpha", is_a(IO))
mock(subject).fork(subject.procfile["bravo"]) mock.instance_of(Foreman::Process).run_process("./bravo", is_a(IO))
mock(subject).watch_for_output
mock(subject).watch_for_termination mock(subject).watch_for_termination
subject.start subject.start
end end
@@ -33,29 +34,14 @@ describe "Foreman::Engine" do
it "handles concurrency" do it "handles concurrency" do
write_procfile write_procfile
engine = Foreman::Engine.new("Procfile",:concurrency => "alpha=2") engine = Foreman::Engine.new("Procfile",:concurrency => "alpha=2")
mock(engine).fork_individual(engine.procfile["alpha"], 1, 5000) mock.instance_of(Foreman::Process).run_process("./alpha", is_a(IO)).twice
mock(engine).fork_individual(engine.procfile["alpha"], 2, 5001) mock.instance_of(Foreman::Process).run_process("./bravo", is_a(IO))
mock(engine).fork_individual(engine.procfile["bravo"], 1, 5100) mock(engine).watch_for_output
mock(engine).watch_for_termination mock(engine).watch_for_termination
engine.start engine.start
end end
end end
describe "execute" do
it "runs the processes" do
write_procfile
mock(subject).fork(subject.procfile["alpha"])
mock(subject).watch_for_termination
subject.execute("alpha")
end
it "shows an error running a process that doesnt exist" do
write_procfile
mock(subject).puts("ERROR: no such process: foo")
lambda { subject.execute("foo") }.should raise_error(SystemExit)
end
end
describe "environment" do describe "environment" do
before(:each) do before(:each) do
write_procfile write_procfile
@@ -66,9 +52,10 @@ describe "Foreman::Engine" do
File.open("/tmp/env", "w") { |f| f.puts("FOO=baz") } File.open("/tmp/env", "w") { |f| f.puts("FOO=baz") }
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env") engine = Foreman::Engine.new("Procfile", :env => "/tmp/env")
stub(engine).info stub(engine).info
mock(engine).spawn_processes
mock(engine).watch_for_termination mock(engine).watch_for_termination
engine.environment.should == {"FOO"=>"baz"} engine.environment.should == {"FOO"=>"baz"}
engine.execute("alpha") engine.start
end end
it "should read more than one if specified" do it "should read more than one if specified" do
@@ -76,9 +63,10 @@ describe "Foreman::Engine" do
File.open("/tmp/env2", "w") { |f| f.puts("BAZ=qux") } File.open("/tmp/env2", "w") { |f| f.puts("BAZ=qux") }
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env1,/tmp/env2") engine = Foreman::Engine.new("Procfile", :env => "/tmp/env1,/tmp/env2")
stub(engine).info stub(engine).info
mock(engine).spawn_processes
mock(engine).watch_for_termination mock(engine).watch_for_termination
engine.environment.should == { "FOO"=>"bar", "BAZ"=>"qux" } engine.environment.should == { "FOO"=>"bar", "BAZ"=>"qux" }
engine.execute("alpha") engine.start
end end
it "should fail if specified and doesnt exist" do it "should fail if specified and doesnt exist" do
@@ -89,11 +77,10 @@ describe "Foreman::Engine" do
it "should read .env if none specified" do it "should read .env if none specified" do
File.open(".env", "w") { |f| f.puts("FOO=qoo") } File.open(".env", "w") { |f| f.puts("FOO=qoo") }
engine = Foreman::Engine.new("Procfile") engine = Foreman::Engine.new("Procfile")
stub(engine).info mock(engine).spawn_processes
mock(engine).watch_for_termination mock(engine).watch_for_termination
mock(engine).fork_individual(anything, anything, anything)
engine.environment.should == {"FOO"=>"qoo"} engine.environment.should == {"FOO"=>"qoo"}
engine.execute("bravo") engine.start
end end
end end
end end
+1 -2
View File
@@ -13,8 +13,7 @@ describe Foreman::Export::Bluepill do
it "exports to the filesystem" do it "exports to the filesystem" do
bluepill.export("/tmp/init", :concurrency => "alpha=2") bluepill.export("/tmp/init", :concurrency => "alpha=2")
File.read("/tmp/init/app.pill").should == example_export_file("bluepill/app.pill") File.read("/tmp/init/app.pill").should == example_export_file("bluepill/app.pill")
end end
end end
+1
View File
@@ -10,6 +10,7 @@ describe Foreman::Export::Runit do
before(:each) { load_export_templates_into_fakefs("runit") } before(:each) { load_export_templates_into_fakefs("runit") }
before(:each) { stub(runit).say } before(:each) { stub(runit).say }
before(:each) { stub(FakeFS::FileUtils).chmod }
it "exports to the filesystem" do it "exports to the filesystem" do
FileUtils.mkdir_p('/tmp/init') FileUtils.mkdir_p('/tmp/init')
+7 -1
View File
@@ -38,13 +38,19 @@ describe Foreman::Export::Upstart do
end end
context "with alternate templates from home dir" do context "with alternate templates from home dir" do
let(:default_template_root) {File.expand_path("~/.foreman/templates")} let(:default_template_root) {File.expand_path("#{ENV['HOME']}/.foreman/templates")}
before do before do
ENV['_FOREMAN_SPEC_HOME'] = ENV['HOME']
ENV['HOME'] = "/home/appuser"
FileUtils.mkdir_p default_template_root FileUtils.mkdir_p default_template_root
File.open("#{default_template_root}/master.conf.erb", "w") { |f| f.puts "default_alternate_template" } File.open("#{default_template_root}/master.conf.erb", "w") { |f| f.puts "default_alternate_template" }
end end
after do
ENV['HOME'] = ENV.delete('_FOREMAN_SPEC_HOME')
end
it "can export with alternate template files" do it "can export with alternate template files" do
upstart.export("/tmp/init") upstart.export("/tmp/init")
+22
View File
@@ -8,4 +8,26 @@ describe Foreman do
it { should be_a String } it { should be_a String }
end end
describe "::load_env!(env_file)" do
before do
FakeFS.activate!
end
after do
FakeFS.deactivate!
ENV['FOO'] = nil
end
it "should load env_file into ENV" do
File.open("/tmp/env1", "w") { |f| f.puts("FOO=bar") }
Foreman.load_env!("/tmp/env1")
ENV['FOO'].should == 'bar'
end
it "should assume env_file in ./.env" do
File.open("./.env", "w") { |f| f.puts("FOO=bar") }
Foreman.load_env!
ENV['FOO'].should == 'bar'
end
end
end end
+18
View File
@@ -0,0 +1,18 @@
require "spec_helper"
describe "spec helpers" do
describe "#preserving_env" do
after { ENV.delete "FOO" }
it "should remove added environment vars" do
preserving_env { ENV["FOO"] = "baz" }
ENV["FOO"].should == nil
end
it "should reset modified environment vars" do
ENV["FOO"] = "bar"
preserving_env { ENV["FOO"] = "baz"}
ENV["FOO"].should == "bar"
end
end
end
+3 -3
View File
@@ -19,7 +19,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.monitor_children do |children| process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}" children.stop_command "kill -QUIT {{PID}}"
end end
process.group = "app-alpha" process.group = "app-alpha"
end end
@@ -37,7 +37,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.monitor_children do |children| process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}" children.stop_command "kill -QUIT {{PID}}"
end end
process.group = "app-alpha" process.group = "app-alpha"
end end
@@ -57,7 +57,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.monitor_children do |children| process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}" children.stop_command "kill -QUIT {{PID}}"
end end
process.group = "app-bravo" process.group = "app-bravo"
end end
+11
View File
@@ -63,8 +63,19 @@ def example_export_file(filename)
data data
end end
def preserving_env
old_env = ENV.to_hash
begin
yield
ensure
ENV.clear
ENV.update(old_env)
end
end
RSpec.configure do |config| RSpec.configure do |config|
config.color_enabled = true config.color_enabled = true
config.order = 'rand'
config.include FakeFS::SpecHelpers config.include FakeFS::SpecHelpers
config.mock_with :rr config.mock_with :rr
end end