Compare commits

...

75 Commits

Author SHA1 Message Date
David Dollar
be593846e2 Regenerated gemspec for version 0.4.7 2010-06-29 17:08:49 -04:00
David Dollar
2458c4e75f 0.4.7 2010-06-29 17:08:46 -04:00
David Dollar
314e2f5530 fix -l, dont append app name 2010-06-29 17:08:30 -04:00
David Dollar
d6c5e6ddea Regenerated gemspec for version 0.4.6 2010-06-29 16:49:17 -04:00
David Dollar
261164e694 update readme 2010-06-29 16:49:17 -04:00
David Dollar
92e637a231 0.4.6 2010-06-29 16:49:14 -04:00
David Dollar
08b94716f2 support -l to export for specifying log root 2010-06-29 16:49:02 -04:00
David Dollar
44d589a28f Regenerated gemspec for version 0.4.5 2010-06-29 16:01:19 -04:00
David Dollar
4bf1f26032 update readme 2010-06-29 16:01:18 -04:00
David Dollar
2a2786e676 0.4.5 2010-06-29 16:01:16 -04:00
David Dollar
91811425aa update gemspec 2010-06-29 16:00:21 -04:00
Adam Wiggins
adb40881d7 inittab export 2010-06-30 03:59:51 +08:00
David Dollar
fd3dc590d9 fix spawn command for launching as a user 2010-06-28 23:27:05 -04:00
David Dollar
8651bbdbee make sure to chown the log dir to the app's user 2010-06-28 23:26:55 -04:00
David Dollar
ced0d0aa9d fix gemspec 2010-06-28 23:09:23 -04:00
David Dollar
10e572de94 restructure init files, add optional --user flag for export 2010-06-28 22:52:44 -04:00
David Dollar
426241d461 remove unneeded code 2010-06-24 16:51:51 -04:00
David Dollar
5a258b8dc3 Regenerated gemspec for version 0.4.4 2010-06-23 19:13:54 -04:00
David Dollar
eeeef65c88 0.4.4. 2010-06-23 19:13:50 -04:00
David Dollar
d67a2f4e11 include export dir with gem 2010-06-23 19:13:40 -04:00
David Dollar
ce5c8b4c04 update readme 2010-06-23 19:11:52 -04:00
David Dollar
9d859dae92 remove potentially confusing period 2010-06-23 19:11:47 -04:00
David Dollar
04884366b3 describe procfile 2010-06-23 19:10:05 -04:00
David Dollar
8c78d1e1ee update readme 2010-06-23 19:09:14 -04:00
David Dollar
35a5f972fe more docs tweaks 2010-06-23 19:05:00 -04:00
David Dollar
868bc44a4e update readme 2010-06-23 19:04:58 -04:00
David Dollar
644956db29 update readme 2010-06-23 19:04:25 -04:00
David Dollar
bd07ed809d more docs tweaks 2010-06-23 19:04:22 -04:00
David Dollar
26bb8995c9 update readme 2010-06-23 19:03:40 -04:00
David Dollar
7005860c3c more docs tweaks 2010-06-23 19:03:33 -04:00
David Dollar
90356ca41d Regenerated gemspec for version 0.4.3 2010-06-23 19:01:56 -04:00
David Dollar
2ce3a15bb7 dont die if there are no docs to commit 2010-06-23 19:01:54 -04:00
David Dollar
f960277ae8 0.4.3 2010-06-23 19:01:07 -04:00
David Dollar
4f5402af4a update readme 2010-06-23 19:00:57 -04:00
David Dollar
a27f964881 tweak docs 2010-06-23 19:00:55 -04:00
David Dollar
cf008385b4 update readme on man build 2010-06-23 18:59:56 -04:00
David Dollar
f633a579d6 update readme 2010-06-23 18:59:41 -04:00
David Dollar
ea90bf3615 update docs 2010-06-23 18:58:37 -04:00
David Dollar
c65c71b1c0 update docs 2010-06-23 18:58:17 -04:00
David Dollar
6f10f4f014 fix up readme 2010-06-23 18:57:13 -04:00
David Dollar
be6d1b805c Regenerated gemspec for version 0.4.2 2010-06-23 18:54:31 -04:00
David Dollar
58e936a7e2 0.4.2 2010-06-23 18:54:29 -04:00
David Dollar
d4f29d6909 back to master after done 2010-06-23 18:54:17 -04:00
David Dollar
571163795f task to build pages 2010-06-23 18:53:20 -04:00
David Dollar
845ee9ef38 dont store man in repo 2010-06-23 18:50:36 -04:00
David Dollar
d5d774c9c2 Regenerated gemspec for version 0.4.1 2010-06-23 18:20:00 -04:00
David Dollar
b77952ff7f updating man pages 2010-06-23 18:19:59 -04:00
David Dollar
3495fa2fea 0.4.1 2010-06-23 18:19:57 -04:00
David Dollar
4741dceeb0 build man on release 2010-06-23 18:19:50 -04:00
David Dollar
dc1cb08d27 updating man pages 2010-06-23 18:19:38 -04:00
David Dollar
b51e8046ce 0.4.0 2010-06-23 18:19:23 -04:00
David Dollar
922eb7438e add manpage 2010-06-23 18:17:55 -04:00
David Dollar
6811a8d96a massive reworking of command line interface 2010-06-23 17:51:12 -04:00
David Dollar
0f7eec061d Regenerated gemspec for version 0.3.2 2010-06-22 17:20:21 -04:00
David Dollar
27e53eb916 0.3.2 2010-06-22 17:20:15 -04:00
David Dollar
b123e6b3c5 change the output format 2010-06-22 17:20:01 -04:00
David Dollar
c2000484bb capture PTY::ChildExited 2010-06-22 17:19:47 -04:00
David Dollar
75f0ce4b9c Regenerated gemspec for version 0.3.1 2010-06-22 16:48:41 -04:00
David Dollar
d508d44fd2 0.3.1 2010-06-22 16:48:39 -04:00
David Dollar
35cc880d40 clean up messaging 2010-06-22 16:48:16 -04:00
David Dollar
6434db289b 0.3.0 2010-06-22 16:33:48 -04:00
David Dollar
1a118ee92b fix up docs 2010-06-22 16:33:16 -04:00
David Dollar
7bd176063c fix up specs 2010-06-22 16:29:49 -04:00
David Dollar
fff958f7b6 add colorization 2010-06-22 16:28:08 -04:00
David Dollar
7888ee8c52 print to stdout, die when any child process dies 2010-06-22 16:15:27 -04:00
David Dollar
a6197c183e change example 2010-06-22 16:15:17 -04:00
David Dollar
41a0620126 Merge branch 'master' of github.com:ddollar/foreman 2010-06-11 21:52:07 -04:00
David Dollar
86654d7918 update docs 2010-06-10 15:27:18 -04:00
David Dollar
9929165d17 update docs 2010-06-10 15:26:28 -04:00
David Dollar
2b8d575aab 0.2.0 2010-06-09 12:32:33 -04:00
David Dollar
56f8603a5d first attempt at screen-based running 2010-06-09 12:13:02 -04:00
David Dollar
3f48d7c541 add execute command 2010-06-09 11:51:18 -04:00
David Dollar
675ad2630d cleanup 2010-06-09 11:51:10 -04:00
David Dollar
9004bf67e1 dont roll back to 1 every export 2010-05-21 15:38:35 -04:00
David Dollar
19c425d200 add gemcutter tasks 2010-05-21 15:32:51 -04:00
26 changed files with 502 additions and 318 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
coverage
example/log/*
man/*.?
man/*.html
man/*.markdown
pkg

103
README.markdown Normal file
View File

@@ -0,0 +1,103 @@
foreman(1) -- manage Procfile-based applications
================================================
## SYNOPSIS
`foreman start [process]`<br>
`foreman export <var>format</var> [location]`
## DESCRIPTION
**Foreman** is a manager for Procfile-based applications. Its aim is to
abstract away the details of the Procfile format, and allow you to either run
your application directly or export it to some other process management
format.
## RUNNING
`foreman start` is used to run your application directly from the command line.
If no additional parameters are passed, foreman will run one instance of each
type of process defined in your Procfile.
If a parameter is passed, foreman will run one instance of the specified
application type.
The following options control how the application is run:
* `-s`, `--screen`:
Run the application as a series of screen windows rather than interleaved
in stdout.
## EXPORTING
`foreman export` is used to export your application to another process
management format.
An location to export can be passed as an argument. This argument may be
either required or optional depending on the export format.
The following options control how the application is run:
* `-a`, `--app`:
Use this name rather than the application's root directory name as the
name of the application when exporting.
* `-l`, `--log`:
Specify the directory to place process logs in.
* `-c`, `--concurrency`:
Specify the number of each process type to run. The value passed in
should be in the format `process=num,process=num`
* `-u`, `--user`:
Specify the user the application should be run as. Defaults to the
app name
## OPTIONS
These options control all modes of foreman's operation.
* `-p`, `--procfile`
Specify an alternate location for the application's Procfile. This file's
containing directory will be assumed to be the root directory of the
application.
## PROCFILE
A Procfile should contain both a name for the process and the command used
to run it.
web bundle exec thin start
job bundle exec rake jobs:work
## EXAMPLES
Start one instance of each process type, interleave the output on stdout:
$ foreman start
Export the application in upstart format:
$ foreman export upstart /etc/init
Run one process type from the application defined in a specific Procfile:
$ foreman start alpha -p ~/app/Procfile
## COPYRIGHT
Foreman is Copyright (C) 2010 David Dollar <http://daviddollar.org>
[SYNOPSIS]: #SYNOPSIS "SYNOPSIS"
[DESCRIPTION]: #DESCRIPTION "DESCRIPTION"
[RUNNING]: #RUNNING "RUNNING"
[EXPORTING]: #EXPORTING "EXPORTING"
[OPTIONS]: #OPTIONS "OPTIONS"
[PROCFILE]: #PROCFILE "PROCFILE"
[EXAMPLES]: #EXAMPLES "EXAMPLES"
[COPYRIGHT]: #COPYRIGHT "COPYRIGHT"
[foreman(1)]: foreman.1.html

View File

@@ -1,77 +0,0 @@
= Foreman
=== Procfile
alpha ./bin/alpha
bravo ./bin/bravo some args
charlie ./bin/charlie -n 5
== Development mode
=== Running
$ foreman start
[foreman] [Tue May 18 01:27:08 UTC 2010] [alpha] started with pid 4393
[foreman] [Tue May 18 01:27:08 UTC 2010] [bravo] started with pid 4394
[foreman] [Tue May 18 01:27:08 UTC 2010] [charlie] started with pid 4395
=== Standardized Logging
log/alpha.log
log/bravo.log
log/charlie.log
== Upstart
=== Export to upstart scripts
$ foreman export sampleapp
$ initctl list | grep sampleapp
sampleapp start/running
sampleapp-alpha (1) start/running, process 4204
sampleapp-bravo (1) start/running, process 4589
sampleapp-charlie (1) start/running, process 4597
=== Change process concurrency levels
$ foreman scale sampleapp alpha 4
sampleapp-alpha (2) start/running, process 4164
sampleapp-alpha (3) start/running, process 4166
sampleapp-alpha (4) start/running, process 4168
$ initctl list | grep sampleapp
sampleapp start/running
sampleapp-alpha (4) start/running, process 4168
sampleapp-alpha (3) start/running, process 4166
sampleapp-alpha (2) start/running, process 4164
sampleapp-alpha (1) start/running, process 4204
sampleapp-bravo (1) start/running, process 4589
sampleapp-charlie (1) start/running, process 4597
$ foreman scale sampleapp alpha 1
sampleapp-alpha stop/waiting
sampleapp-alpha stop/waiting
sampleapp-alpha stop/waiting
=== Good Upstart citizen
All Upstart commands work as expected
$ start sampleapp
$ stop sampleapp
$ restart sampleapp
=== Standardized Logging
/var/log/sampleapp/alpha.log
/var/log/sampleapp/bravo.log
/var/log/sampleapp/charlie.log
== License
MIT
== Copyright
(c) 2010 David Dollar

View File

@@ -1,11 +1,12 @@
require "rubygems"
require "rake"
require "rspec"
require "rspec/core/rake_task"
$:.unshift File.expand_path("../lib", __FILE__)
require "foreman"
task :default => :spec
task :release => :man
desc "Run all specs"
Rspec::Core::RakeTask.new(:spec) do |t|
@@ -23,6 +24,29 @@ Rspec::Core::RakeTask.new("rcov:build") do |t|
t.rcov_opts = [ "--exclude", Gem.default_dir , "--exclude", "spec" ]
end
desc 'Build the manual'
task :man do
ENV['RONN_MANUAL'] = "Foreman Manual"
ENV['RONN_ORGANIZATION'] = "Foreman #{Foreman::VERSION}"
sh "ronn -w -s toc -r5 --markdown man/*.ronn"
sh "cp man/foreman.1.markdown README.markdown"
sh "git add README.markdown"
sh "git commit -m 'update readme' || echo 'nothing to commit'"
end
task :pages => :man do
sh %{
cp man/foreman.1.html /tmp/foreman.1.html
git checkout gh-pages
rm ./index.html
cp /tmp/foreman.1.html ./index.html
git add -u index.html
git commit -m "rebuilding man page"
git push origin -f gh-pages
git checkout master
}
end
######################################################
begin
@@ -40,7 +64,7 @@ begin
s.platform = Gem::Platform::RUBY
s.has_rdoc = false
s.files = %w(Rakefile README.md) + Dir["{bin,lib,spec}/**/*"]
s.files = %w(Rakefile README.md) + Dir["{bin,export,lib,spec}/**/*"]
s.require_path = "lib"
# #s.bindir = "bin"
@@ -53,8 +77,10 @@ begin
s.add_development_dependency 'rr', '~> 0.10.11'
s.add_development_dependency 'rspec', '~> 2.0.0'
s.add_dependency 'term-ansicolor', '~> 1.0.5'
s.add_dependency 'thor', '~> 0.13.6'
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler not available. Install it with: sudo gem install jeweler"
end

View File

@@ -1,3 +1,2 @@
neverdie ./never_die
diealot ./die_alot
error ./error
ticker ./ticker
error ./error

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env ruby
puts "sleeping for 2s then dying"
sleep 2
exit 0

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
puts "will error in 10s"
sleep 10
sleep 5
raise "Dying"

View File

@@ -2,6 +2,5 @@
while true
puts "tick"
$stdout.flush
sleep 5
sleep 1
end

View File

@@ -0,0 +1,8 @@
pre-start script
bash << "EOF"
mkdir -p <%= log_root %>
chown -R <%= user %> <%= log_root %>
EOF
end script

View File

@@ -0,0 +1,6 @@
start on starting <%= app %>-<%= process.name %>
stop on stopping <%= app %>-<%= process.name %>
respawn
chdir <%= engine.directory %>
exec su <%= user %> -c "<%= process.command %> >> <%= log_root %>/<%=process.name%>-<%=num%>.log 2>&1"

View File

@@ -0,0 +1,2 @@
start on starting <%= app %>
stop on stopping <%= app %>

View File

@@ -5,30 +5,33 @@
Gem::Specification.new do |s|
s.name = %q{foreman}
s.version = "0.1.0"
s.version = "0.4.7"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["David Dollar"]
s.date = %q{2010-05-21}
s.date = %q{2010-06-29}
s.default_executable = %q{foreman}
s.description = %q{Process manager for applications with multiple components}
s.email = %q{ddollar@gmail.com}
s.executables = ["foreman"]
s.extra_rdoc_files = [
"README.rdoc"
"README.markdown"
]
s.files = [
"Rakefile",
"bin/foreman",
"export/upstart/master.conf.erb",
"export/upstart/process.conf.erb",
"export/upstart/process_master.conf.erb",
"lib/foreman.rb",
"lib/foreman/cli.rb",
"lib/foreman/configuration.rb",
"lib/foreman/engine.rb",
"lib/foreman/export.rb",
"lib/foreman/export/base.rb",
"lib/foreman/export/inittab.rb",
"lib/foreman/export/upstart.rb",
"lib/foreman/process.rb",
"spec/foreman/cli_spec.rb",
"spec/foreman/configuration_spec.rb",
"spec/foreman/engine_spec.rb",
"spec/foreman/export/upstart_spec.rb",
"spec/foreman/export_spec.rb",
@@ -44,7 +47,6 @@ Gem::Specification.new do |s|
s.summary = %q{Process manager for applications with multiple components}
s.test_files = [
"spec/foreman/cli_spec.rb",
"spec/foreman/configuration_spec.rb",
"spec/foreman/engine_spec.rb",
"spec/foreman/export/upstart_spec.rb",
"spec/foreman/export_spec.rb",
@@ -63,6 +65,7 @@ Gem::Specification.new do |s|
s.add_development_dependency(%q<rcov>, ["~> 0.9.8"])
s.add_development_dependency(%q<rr>, ["~> 0.10.11"])
s.add_development_dependency(%q<rspec>, ["~> 2.0.0"])
s.add_runtime_dependency(%q<term-ansicolor>, ["~> 1.0.5"])
s.add_runtime_dependency(%q<thor>, ["~> 0.13.6"])
else
s.add_dependency(%q<fakefs>, ["~> 0.2.1"])
@@ -70,6 +73,7 @@ Gem::Specification.new do |s|
s.add_dependency(%q<rcov>, ["~> 0.9.8"])
s.add_dependency(%q<rr>, ["~> 0.10.11"])
s.add_dependency(%q<rspec>, ["~> 2.0.0"])
s.add_dependency(%q<term-ansicolor>, ["~> 1.0.5"])
s.add_dependency(%q<thor>, ["~> 0.13.6"])
end
else
@@ -78,6 +82,7 @@ Gem::Specification.new do |s|
s.add_dependency(%q<rcov>, ["~> 0.9.8"])
s.add_dependency(%q<rr>, ["~> 0.10.11"])
s.add_dependency(%q<rspec>, ["~> 2.0.0"])
s.add_dependency(%q<term-ansicolor>, ["~> 1.0.5"])
s.add_dependency(%q<thor>, ["~> 0.13.6"])
end
end

View File

@@ -1,6 +1,6 @@
module Foreman
VERSION = "0.1.0"
VERSION = "0.4.7"
class AppDoesNotExist < Exception; end

View File

@@ -1,37 +1,66 @@
require "foreman"
require "foreman/configuration"
require "foreman/engine"
require "foreman/export"
require "thor"
class Foreman::CLI < Thor
desc "start [PROCFILE]", "Run the app described in PROCFILE"
class_option :procfile, :type => :string, :aliases => "-p", :desc => "Default: ./Procfile"
def start(procfile="Procfile")
error "#{procfile} does not exist." unless procfile_exists?(procfile)
Foreman::Engine.new(procfile).start
desc "start [PROCESS]", "Start the application, or a specific process"
method_option :screen, :type => :boolean, :aliases => "-s"
def start(process=nil)
check_procfile!
if process
engine.execute(process)
elsif options[:screen]
engine.screen
else
engine.start
end
end
desc "export APP [PROCFILE] [FORMAT]", "Export the app described in PROCFILE as APP to another FORMAT"
desc "export FORMAT LOCATION", "Export the application to another process management format"
def export(app, procfile="Procfile", format="upstart")
error "#{procfile} does not exist." unless procfile_exists?(procfile)
method_option :app, :type => :string, :aliases => "-a"
method_option :log, :type => :string, :aliases => "-l"
method_option :user, :type => :string, :aliases => "-u"
method_option :concurrency, :type => :string, :aliases => "-c",
:banner => '"alpha=5,bar=3"'
def export(format, location=nil)
check_procfile!
formatter = case format
when "upstart" then Foreman::Export::Upstart
when "inittab" then Foreman::Export::Inittab
else error "Unknown export format: #{format}."
end
formatter.new(Foreman::Engine.new(procfile)).export(app)
formatter.new(engine).export(location,
:name => options[:app],
:user => options[:user],
:log => options[:log],
:concurrency => options[:concurrency]
)
rescue Foreman::Export::Exception => ex
error ex.message
end
desc "scale APP PROCESS AMOUNT", "Change the concurrency of a given process type"
private ######################################################################
def scale(app, process, amount)
config = Foreman::Configuration.new(app)
error "No such process: #{process}." unless config.processes[process]
config.scale(process, amount)
def check_procfile!
error("Procfile does not exist.") unless File.exist?(procfile)
end
def engine
@engine ||= Foreman::Engine.new(procfile)
end
def procfile
options[:procfile] || "./Procfile"
end
private ######################################################################

View File

@@ -1,55 +0,0 @@
require "foreman"
class Foreman::Configuration
attr_reader :app
attr_reader :processes
def initialize(app)
@app = app
@processes = {}
read_initial_config
end
def scale(process, amount)
old_amount = processes[process].to_i
processes[process] = amount.to_i
amount = amount.to_i
if (old_amount < amount)
((old_amount + 1) .. amount).each { |num| system "start #{app}-#{process} NUM=#{num}" }
elsif (amount < old_amount)
((amount + 1) .. old_amount).each { |num| system "stop #{app}-#{process} NUM=#{num}" }
end
write
end
def write
write_file "/etc/foreman/#{app}.conf", <<-UPSTART_CONFIG
#{app}_processes="#{processes.keys.sort.join(' ')}"
#{processes.keys.sort.map { |k| "#{app}_#{k}=\"#{processes[k]}\"" }.join("\n")}
UPSTART_CONFIG
end
private ######################################################################
def read_initial_config
config = File.read("/etc/foreman/#{app}.conf").split("\n").inject({}) do |accum, line|
key, value = line.match(/^(.+?)\s*=\s*"(.+?)"\s*$/).captures
#accum.update(parts(1) => parts(2))
accum.update(key => value)
end
config["#{app}_processes"].split(" ").each do |process|
processes[process] = config["#{app}_#{process}"].to_i
end
rescue Errno::ENOENT
end
def write_file(filename, contents)
File.open(filename, "w") do |file|
file.puts contents
end
end
end

View File

@@ -1,11 +1,18 @@
require "foreman"
require "foreman/process"
require "pty"
require "tempfile"
require "term/ansicolor"
class Foreman::Engine
attr_reader :procfile
attr_reader :directory
extend Term::ANSIColor
COLORS = [ cyan, yellow, green, magenta, on_blue ]
def initialize(procfile)
@procfile = read_procfile(procfile)
@directory = File.expand_path(File.dirname(procfile))
@@ -16,6 +23,7 @@ class Foreman::Engine
procfile.split("\n").inject({}) do |hash, line|
next if line.strip == ""
process = Foreman::Process.new(*line.split(" ", 2))
process.color = next_color
hash.update(process.name => process)
end
end
@@ -31,37 +39,84 @@ class Foreman::Engine
trap("TERM") { kill_and_exit("TERM") }
trap("INT") { kill_and_exit("INT") }
run_loop
watch_for_termination
end
def screen
tempfile = Tempfile.new("foreman")
tempfile.puts "sessionname foreman"
processes.each do |name, process|
tempfile.puts "screen -t #{name} #{process.command}"
end
tempfile.close
system "screen -c #{tempfile.path}"
tempfile.delete
end
def execute(name)
run(processes[name], false)
end
private ######################################################################
def fork(process)
pid = Process.fork do
proctitle "ruby: foreman #{process.name}"
Dir.chdir directory do
FileUtils.mkdir_p "log"
system "#{process.command} >>log/#{process.name}.log 2>&1"
exit $?.exitstatus || 255
end
run(process)
end
info "started with pid #{pid}", process
running_processes[pid] = process
end
def run(process, log_to_file=true)
proctitle "ruby: foreman #{process.name}"
Dir.chdir directory do
FileUtils.mkdir_p "log"
command = process.command
begin
PTY.spawn("#{process.command} 2>&1") do |stdin, stdout, pid|
until stdin.eof?
info stdin.gets, process
end
end
rescue PTY::ChildExited, Interrupt
info "process exiting", process
end
end
end
def kill_and_exit(signal="TERM")
info "termination requested"
info "terminating"
running_processes.each do |pid, process|
info "killing pid #{pid}", process
info "killing #{process.name} in pid #{pid}"
Process.kill(signal, pid)
end
exit 0
end
def info(message, process=nil)
puts "[foreman] [#{Time.now.utc}] [#{process ? process.name : "system"}] #{message}"
print process.color if process
print "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(process)} | "
print Term::ANSIColor.reset
print message.chomp
puts
end
def longest_process_name
@longest_process_name ||= begin
longest = processes.keys.map { |name| name.length }.sort.last
longest = 6 if longest < 6 # system
longest
end
end
def pad_process_name(process)
name = process ? process.name : "system"
name.ljust(longest_process_name)
end
def print_info
@@ -79,17 +134,21 @@ private ######################################################################
File.read(procfile)
end
def run_loop
while true
pid, status = Process.wait2
process = running_processes.delete(pid)
info "exited with code #{status}", process
fork process
end
def watch_for_termination
pid, status = Process.wait2
process = running_processes.delete(pid)
info "process terminated", process
kill_and_exit
end
def running_processes
@running_processes ||= {}
end
def next_color
@current_color ||= -1
@current_color += 1
@current_color >= COLORS.length ? "" : COLORS[@current_color]
end
end

View File

@@ -1,6 +1,8 @@
require "foreman"
module Foreman::Export
class Exception < ::Exception; end
end
require "foreman/export/upstart"
require "foreman/export/inittab"

View File

@@ -0,0 +1,47 @@
require "foreman/export"
class Foreman::Export::Base
attr_reader :engine
def initialize(engine)
@engine = engine
end
def export
raise "export method must be overridden"
end
private ######################################################################
def error(message)
raise Foreman::Export::Exception.new(message)
end
def say(message)
puts "[foreman export] %s" % message
end
def export_template(name)
File.read(File.expand_path("../../../../export/#{name}", __FILE__))
end
def parse_concurrency(concurrency)
@concurrency ||= begin
pairs = concurrency.to_s.gsub(/\s/, "").split(",")
pairs.inject(Hash.new(1)) do |hash, pair|
process, amount = pair.split("=")
hash.update(process => amount.to_i)
end
end
end
def write_file(filename, contents)
say "writing: #{filename}"
File.open(filename, "w") do |file|
file.puts contents
end
end
end

View File

@@ -0,0 +1,31 @@
require "foreman/export/base"
class Foreman::Export::Inittab < Foreman::Export::Base
def export(fname=nil, options={})
app = options[:app] || File.basename(engine.directory)
user = options[:user] || app
log_root = options[:log] || "/var/log"
concurrency = parse_concurrency(options[:concurrency])
inittab = []
inittab << "# ----- foreman #{app} processes -----"
engine.processes.values.each_with_index do |process, num|
id = app.slice(0, 2).upcase + sprintf("%02d", num+1)
inittab << "#{id}:4:respawn:/bin/su - #{user} -c '#{process.command} >> #{log_root}/#{process.name}-#{num+1}.log 2>&1'"
end
inittab << "# ----- end foreman #{app} processes -----"
inittab = inittab.join("\n") + "\n"
if fname
FileUtils.mkdir_p(log_root)
FileUtils.chown(user, nil, log_root)
write_file(fname, inittab)
else
puts inittab
end
end
end

View File

@@ -1,65 +1,39 @@
require "foreman/configuration"
require "foreman/export"
require "erb"
require "foreman/export/base"
class Foreman::Export::Upstart
class Foreman::Export::Upstart < Foreman::Export::Base
attr_reader :engine
def export(location, options={})
error("Must specify a location") unless location
def initialize(engine)
@engine = engine
end
FileUtils.mkdir_p location
def export(app)
FileUtils.mkdir_p "/etc/foreman"
FileUtils.mkdir_p "/etc/init"
app = options[:app] || File.basename(engine.directory)
user = options[:user] || app
log_root = options[:log] || "/var/log/#{app}"
config = Foreman::Configuration.new(app)
Dir["#{location}/#{app}*.conf"].each do |file|
say "cleaning up: #{file}"
FileUtils.rm(file)
end
write_file "/etc/init/#{app}.conf", <<-UPSTART_MASTER
pre-start script
concurrency = parse_concurrency(options[:concurrency])
bash << "EOF"
mkdir -p /var/log/#{app}
master_template = export_template("upstart/master.conf.erb")
master_config = ERB.new(master_template).result(binding)
write_file "#{location}/#{app}.conf", master_config
if [ -f /etc/foreman/#{app}.conf ]; then
source /etc/foreman/#{app}.conf
fi
for process in $( echo "$#{app}_processes" ); do
process_count_config="#{app}_$process"
process_count=${!process_count_config}
for ((i=1; i<=${process_count:=1}; i+=1)); do
start #{app}-$process NUM=$i
done
done
EOF
end script
UPSTART_MASTER
process_template = export_template("upstart/process.conf.erb")
engine.processes.values.each do |process|
write_file "/etc/init/#{app}-#{process.name}.conf", <<-UPSTART_CHILD
instance $NUM
stop on stopping #{app}
respawn
process_master_template = export_template("upstart/process_master.conf.erb")
process_master_config = ERB.new(process_master_template).result(binding)
write_file "#{location}/#{app}-#{process.name}.conf", process_master_config
chdir #{engine.directory}
exec #{process.command} >>/var/log/#{app}/#{process.name}.log 2>&1
UPSTART_CHILD
end
engine.processes.each do |name, process|
config.scale(name, 1)
end
config.write
end
private ######################################################################
def write_file(filename, contents)
File.open(filename, "w") do |file|
file.puts contents
1.upto(concurrency[process.name]) do |num|
process_config = ERB.new(process_template).result(binding)
write_file "#{location}/#{app}-#{process.name}-#{num}.conf", process_config
end
end
end

View File

@@ -4,6 +4,7 @@ class Foreman::Process
attr_reader :name
attr_reader :command
attr_accessor :color
def initialize(name, command)
@name = name

90
man/foreman.1.ronn Normal file
View File

@@ -0,0 +1,90 @@
foreman(1) -- manage Procfile-based applications
================================================
## SYNOPSIS
`foreman start [process]`<br>
`foreman export <format> [location]`
## DESCRIPTION
**Foreman** is a manager for Procfile-based applications. Its aim is to
abstract away the details of the Procfile format, and allow you to either run
your application directly or export it to some other process management
format.
## RUNNING
`foreman start` is used to run your application directly from the command line.
If no additional parameters are passed, foreman will run one instance of each
type of process defined in your Procfile.
If a parameter is passed, foreman will run one instance of the specified
application type.
The following options control how the application is run:
* `-s`, `--screen`:
Run the application as a series of screen windows rather than interleaved
in stdout.
## EXPORTING
`foreman export` is used to export your application to another process
management format.
An location to export can be passed as an argument. This argument may be
either required or optional depending on the export format.
The following options control how the application is run:
* `-a`, `--app`:
Use this name rather than the application's root directory name as the
name of the application when exporting.
* `-l`, `--log`:
Specify the directory to place process logs in.
* `-c`, `--concurrency`:
Specify the number of each process type to run. The value passed in
should be in the format `process=num,process=num`
* `-u`, `--user`:
Specify the user the application should be run as. Defaults to the
app name
## OPTIONS
These options control all modes of foreman's operation.
* `-p`, `--procfile`
Specify an alternate location for the application's Procfile. This file's
containing directory will be assumed to be the root directory of the
application.
## PROCFILE
A Procfile should contain both a name for the process and the command used
to run it.
web bundle exec thin start
job bundle exec rake jobs:work
## EXAMPLES
Start one instance of each process type, interleave the output on stdout:
$ foreman start
Export the application in upstart format:
$ foreman export upstart /etc/init
Run one process type from the application defined in a specific Procfile:
$ foreman start alpha -p ~/app/Procfile
## COPYRIGHT
Foreman is Copyright (C) 2010 David Dollar <http://daviddollar.org>

View File

@@ -5,9 +5,7 @@ describe "Foreman::CLI" do
subject { Foreman::CLI.new }
describe "start" do
#let(:engine) { stub_engine }
describe "with a non-existent Procifile" do
describe "with a non-existent Procfile" do
it "prints an error" do
mock_error(subject, "Procfile does not exist.") do
dont_allow.instance_of(Foreman::Engine).start
@@ -28,7 +26,7 @@ describe "Foreman::CLI" do
end
describe "export" do
describe "with a non-existent Procifile" do
describe "with a non-existent Procfile" do
it "prints an error" do
mock_error(subject, "Procfile does not exist.") do
dont_allow.instance_of(Foreman::Engine).export
@@ -43,7 +41,7 @@ describe "Foreman::CLI" do
describe "with an invalid formatter" do
it "prints an error" do
mock_error(subject, "Unknown export format: invalidformatter.") do
subject.export("testapp", "Procfile", "invalidformatter")
subject.export("invalidformatter")
end
end
end
@@ -53,33 +51,11 @@ describe "Foreman::CLI" do
it "runs successfully" do
dont_allow(subject).error
subject.export("testapp")
end
end
end
end
describe "scale" do
describe "without an existing configuration" do
it "displays an error" do
mock_error(subject, "No such process: alpha.") do
subject.scale("testapp", "alpha", "2")
end
end
end
describe "with an existing configuration" do
before(:each) { write_foreman_config("testapp") }
it "scales a process that exists" do
mock.instance_of(Foreman::Configuration).scale("alpha", "2")
subject.scale("testapp", "alpha", "2")
end
it "errors if a process that does not exist is specified" do
mock_error(subject, "No such process: invalidprocess.") do
dont_allow.instance_of(Foreman::Configuration).scale
subject.scale("testapp", "invalidprocess", "2")
mock.instance_of(Foreman::Export::Upstart).export("/tmp/foo", {
:concurrency => nil,
:name => nil
})
subject.export("upstart", "/tmp/foo")
end
end
end

View File

@@ -1,49 +0,0 @@
require "spec_helper"
require "foreman/configuration"
describe "Foreman::Configuration" do
subject { Foreman::Configuration.new("testapp") }
describe "initialize" do
describe "without an existing config" do
it "has no processes" do
subject.processes.length.should == 0
end
end
describe "with an existing config" do
it "has processes" do
write_foreman_config("testapp")
subject.processes["alpha"].should == 1
subject.processes["bravo"].should == 2
end
end
end
describe "scale" do
before(:each) { write_foreman_config("testapp") }
it "can scale up" do
mock(subject).system("start testapp-alpha NUM=2")
mock(subject).system("start testapp-alpha NUM=3")
subject.scale("alpha", 3)
end
it "can scale down" do
mock(subject).system("stop testapp-bravo NUM=2")
subject.scale("bravo", 1)
end
end
describe "wite" do
it "can write a configuration file" do
subject.scale("charlie", 3)
subject.scale("delta", 4)
File.read("/etc/foreman/testapp.conf").should == <<-FOREMAN_CONFIG
testapp_processes="charlie delta"
testapp_charlie="3"
testapp_delta="4"
FOREMAN_CONFIG
end
end
end

View File

@@ -25,8 +25,16 @@ describe "Foreman::Engine" do
write_procfile
mock(subject).fork(subject.processes["alpha"])
mock(subject).fork(subject.processes["bravo"])
mock(subject).run_loop
mock(subject).watch_for_termination
subject.start
end
end
describe "execute" do
it "runs the processes" do
write_procfile
mock(subject).run(subject.processes["alpha"], false)
subject.execute("alpha")
end
end
end

View File

@@ -1,5 +1,7 @@
require "fakefs/spec_helpers"
require "rubygems"
require "rspec"
require "fakefs/safe"
require "fakefs/spec_helpers"
$:.unshift "lib"