Compare commits

..

40 Commits

Author SHA1 Message Date
David Dollar
e6b61801b1 0.10.0 2010-12-13 18:05:41 -05:00
David Dollar
dc231f072b allow optional colon after process name 2010-12-13 18:05:06 -05:00
David Dollar
b77b23b306 declare license 2010-11-16 17:47:53 -05:00
David Dollar
185617dddc 0.9.2 2010-11-09 13:42:18 -05:00
David Dollar
9e4cc02827 upgrade dependencies 2010-11-09 13:41:18 -05:00
David Dollar
8b6e2481f4 ruby 1.8.6 compatibility 2010-11-09 13:41:08 -05:00
David Dollar
79f376368c 0.9.1 2010-11-03 15:00:59 -07:00
David Dollar
3576ae82af use process order to determine port assignments 2010-11-03 15:00:35 -07:00
David Dollar
303d54155f uniq the order for safety 2010-11-03 14:13:06 -07:00
David Dollar
2e36cbf045 0.9.0 2010-11-03 14:11:37 -07:00
David Dollar
d3059ca563 always run processes in order they are defined in the procfile 2010-11-03 14:11:30 -07:00
David Dollar
9e42dfb253 change back to Procfile 2010-11-03 14:05:06 -07:00
David Dollar
eca48170a5 fixing specs 2010-10-15 17:26:18 -07:00
David Dollar
86e3cd12dd using Psfile 2010-10-15 17:25:12 -07:00
David Dollar
efd5a786f5 showing PORT here can be confusing 2010-10-15 16:29:40 -07:00
David Dollar
7e9117812f 0.9.0.beta.1 2010-10-15 16:18:54 -07:00
David Dollar
55f405a2b4 catch some common error cases 2010-10-15 16:17:55 -07:00
David Dollar
615bb0d4ba pass ENV["PS"] through to child processes (worker.1) and use it for display output 2010-10-15 16:08:33 -07:00
David Dollar
0ae144e468 better handling of Interrupt rescues 2010-10-15 16:06:06 -07:00
David Dollar
0c2b2a4ac2 prevent double interrupt 2010-10-15 15:55:53 -07:00
David Dollar
e68946f186 change to Pstypes 2010-10-15 15:53:43 -07:00
David Dollar
52edb7fd28 move data under data 2010-10-15 15:53:27 -07:00
David Dollar
8446528f5a change to Pstypes 2010-10-15 15:50:40 -07:00
David Dollar
d41a8726bd update dependencies 2010-10-15 15:50:19 -07:00
David Dollar
7bb1e58879 0.8.0 2010-09-20 16:22:31 -04:00
David Dollar
62b6cac741 print message when signal received 2010-09-20 16:22:13 -04:00
Keith Rarick
2a7dadc2b2 Wait cleanly for child processes to exit.
Signed-off-by: David Dollar <ddollar@gmail.com>
2010-09-20 16:21:41 -04:00
David Dollar
9cd772ac0f 0.7.5 2010-09-17 09:31:24 -04:00
David Dollar
2b27d0a51a include files from export in the gem 2010-09-17 09:31:08 -04:00
Keith Rarick
99204d7c1d Wait for descendant processes to exit. 2010-09-17 21:28:01 +08:00
Ricardo Chimal, Jr
e9b5ed81b8 bugfix upstart export erb templates 2010-09-17 21:27:29 +08:00
David Dollar
7d751470d2 0.7.3 2010-08-24 17:41:36 -04:00
David Dollar
68c1a01f15 add executable and man page to gem release 2010-08-24 17:41:22 -04:00
David Dollar
08f9027cb4 0.7.2 2010-08-24 17:24:43 -04:00
David Dollar
4f7692bed9 switch to parka for gem management 2010-08-24 17:23:03 -04:00
David Dollar
dd95cea997 update docs 2010-07-21 07:23:32 -07:00
David Dollar
f3988b0c52 Regenerated gemspec for version 0.7.1 2010-07-20 16:20:20 -07:00
David Dollar
fbb17dd37d 0.7.1 2010-07-20 16:20:16 -07:00
David Dollar
31a72b454b clean up development concurrency, make sure ports exist in dev mode 2010-07-20 16:20:03 -07:00
David Dollar
e5a8c38da6 clean up exports 2010-07-20 16:19:40 -07:00
22 changed files with 185 additions and 200 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.bundle
coverage
example/log/*
man/*.?

17
Gemfile Normal file
View File

@@ -0,0 +1,17 @@
source "http://rubygems.org"
group :development do
gem 'parka'
gem 'rake'
gem 'ronn'
end
group :test do
gem 'fakefs', '~> 0.2.1'
gem 'rcov', '~> 0.9.8'
gem 'rr', '~> 1.0.2'
gem 'rspec', '~> 2.0.0'
end
gem 'term-ansicolor', '~> 1.0.5'
gem 'thor', '~> 0.13.6'

45
Gemfile.lock Normal file
View File

@@ -0,0 +1,45 @@
GEM
remote: http://rubygems.org/
specs:
diff-lcs (1.1.2)
fakefs (0.2.1)
hpricot (0.8.2)
mime-types (1.16)
mustache (0.11.2)
parka (0.3.1)
rest-client
thor
rake (0.8.7)
rcov (0.9.8)
rdiscount (1.6.5)
rest-client (1.6.0)
mime-types (>= 1.16)
ronn (0.7.3)
hpricot (>= 0.8.2)
mustache (>= 0.7.0)
rdiscount (>= 1.5.8)
rr (1.0.2)
rspec (2.0.0.beta.19)
rspec-core (= 2.0.0.beta.19)
rspec-expectations (= 2.0.0.beta.19)
rspec-mocks (= 2.0.0.beta.19)
rspec-core (2.0.0.beta.19)
rspec-expectations (2.0.0.beta.19)
diff-lcs (>= 1.1.2)
rspec-mocks (2.0.0.beta.19)
term-ansicolor (1.0.5)
thor (0.13.8)
PLATFORMS
ruby
DEPENDENCIES
fakefs (~> 0.2.1)
parka
rake
rcov (~> 0.9.8)
ronn
rr (~> 1.0.2)
rspec (~> 2.0.0)
term-ansicolor (~> 1.0.5)
thor (~> 0.13.6)

View File

@@ -1,4 +1,9 @@
Foreman
=======
# Foreman
## Manual
See the [man page](http://ddollar.github.com/foreman) for usage.
## License
MIT

View File

@@ -1,3 +1,7 @@
require "rubygems"
require "bundler"
Bundler.setup
require "rake"
require "rspec"
require "rspec/core/rake_task"
@@ -45,41 +49,3 @@ task :pages => :man do
git checkout master
}
end
######################################################
begin
require 'jeweler'
Jeweler::Tasks.new do |s|
s.name = "foreman"
s.version = Foreman::VERSION
s.summary = "Process manager for applications with multiple components"
s.description = s.summary
s.author = "David Dollar"
s.email = "ddollar@gmail.com"
s.homepage = "http://github.com/ddollar/foreman"
s.platform = Gem::Platform::RUBY
s.has_rdoc = false
s.files = %w(Rakefile README.md) + Dir["{bin,export,lib,spec}/**/*"]
s.require_path = "lib"
# #s.bindir = "bin"
# s.executables = Dir["bin/*"]
s.default_executable = "foreman"
s.add_development_dependency 'fakefs', '~> 0.2.1'
s.add_development_dependency 'rake', '~> 0.8.7'
s.add_development_dependency 'rcov', '~> 0.9.8'
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

2
data/example/Procfile Normal file
View File

@@ -0,0 +1,2 @@
ticker ./ticker $PORT
error ./error

View File

@@ -0,0 +1,4 @@
tick
tick
./never_die:6:in `sleep': Interrupt
from ./never_die:6

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
while true
puts "tick"
puts "tick: #{ARGV.inspect}"
sleep 1
end

View File

@@ -3,4 +3,4 @@ stop on stopping <%= app %>-<%= process.name %>
respawn
chdir <%= engine.directory %>
exec su <%= user %> -c "PORT=<%= port %> <%= process.command %> >> <%= log_root %>/<%=process.name%>-<%=num%>.log 2>&1"
exec su - <%= user %> -c 'export PORT=<%= port %>; <%= process.command %> >> <%= log_root %>/<%=process.name%>-<%=num%>.log 2>&1'

View File

@@ -1,2 +0,0 @@
ticker ./ticker
error ./error

View File

@@ -1,89 +1,12 @@
# Generated by jeweler
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
# -*- encoding: utf-8 -*-
require "rubygems"
require "parka/specification"
Gem::Specification.new do |s|
s.name = %q{foreman}
s.version = "0.7.0"
Parka::Specification.new do |gem|
gem.name = "foreman"
gem.version = Foreman::VERSION
gem.summary = "Process manager for applications with multiple components"
gem.homepage = "http://github.com/ddollar/foreman"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["David Dollar"]
s.date = %q{2010-07-19}
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.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/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",
"lib/foreman/utils.rb",
"spec/foreman/cli_spec.rb",
"spec/foreman/engine_spec.rb",
"spec/foreman/export/upstart_spec.rb",
"spec/foreman/export_spec.rb",
"spec/foreman/process_spec.rb",
"spec/foreman_spec.rb",
"spec/spec_helper.rb"
]
s.homepage = %q{http://github.com/ddollar/foreman}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.7}
s.summary = %q{Process manager for applications with multiple components}
s.test_files = [
"spec/foreman/cli_spec.rb",
"spec/foreman/engine_spec.rb",
"spec/foreman/export/upstart_spec.rb",
"spec/foreman/export_spec.rb",
"spec/foreman/process_spec.rb",
"spec/foreman_spec.rb",
"spec/spec_helper.rb"
]
if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_development_dependency(%q<fakefs>, ["~> 0.2.1"])
s.add_development_dependency(%q<rake>, ["~> 0.8.7"])
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"])
s.add_dependency(%q<rake>, ["~> 0.8.7"])
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
s.add_dependency(%q<fakefs>, ["~> 0.2.1"])
s.add_dependency(%q<rake>, ["~> 0.8.7"])
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
gem.executables = "foreman"
gem.files << "man/foreman.1"
end

View File

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

View File

@@ -5,10 +5,11 @@ require "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"
desc "start [PROCESS]", "Start the application, or a specific process"
method_option :port, :type => :numeric, :aliases => "-p"
method_option :concurrency, :type => :string, :aliases => "-c",
:banner => '"alpha=5,bar=3"'
@@ -49,7 +50,7 @@ class Foreman::CLI < Thor
private ######################################################################
def check_procfile!
error("Procfile does not exist.") unless File.exist?(procfile)
error("#{procfile} does not exist.") unless File.exist?(procfile)
end
def engine
@@ -57,7 +58,7 @@ private ######################################################################
end
def procfile
options[:procfile] || "./Procfile"
options[:procfile] || "Procfile"
end
private ######################################################################

View File

@@ -20,60 +20,73 @@ class Foreman::Engine
@directory = File.expand_path(File.dirname(procfile))
end
def processes(concurrency=nil)
def processes
@processes ||= begin
concurrency = Foreman::Utils.parse_concurrency(concurrency)
@order = []
procfile.split("\n").inject({}) do |hash, line|
next if line.strip == ""
name, command = line.split(" ", 2)
if concurrency[name] > 1 then
1.upto(concurrency[name]) do |num|
process = Foreman::Process.new("#{name}.#{num}", command)
process.color = next_color
hash[process.name] = process
end
else
process = Foreman::Process.new(name, command)
process.color = next_color
hash[process.name] = process
end
hash
name, command = line.split(/:? +/, 2)
process = Foreman::Process.new(name, command)
process.color = next_color
@order << process.name
hash.update(process.name => process)
end
end
end
def process_order
processes
@order.uniq
end
def processes_in_order
process_order.map do |name|
[name, processes[name]]
end
end
def start(options={})
proctitle "ruby: foreman master"
processes(options[:concurrency]).each do |name, process|
fork process
processes_in_order.each do |name, process|
fork process, options
end
trap("TERM") { kill_and_exit("TERM") }
trap("INT") { kill_and_exit("INT") }
trap("TERM") { puts "SIGTERM received"; kill_all("TERM") }
trap("INT") { puts "SIGINT received"; kill_all("INT") }
watch_for_termination
end
def execute(name, options={})
processes(options[:concurrency]).values.select do |process|
process.name =~ /\A#{name}\.?\d*\Z/
end.each do |process|
fork process
end
fork processes[name], options
trap("TERM") { kill_and_exit("TERM") }
trap("INT") { kill_and_exit("INT") }
trap("TERM") { puts "SIGTERM received"; kill_all("TERM") }
trap("INT") { puts "SIGINT received"; kill_all("INT") }
watch_for_termination
end
def port_for(process, num, base_port=nil)
base_port ||= 5000
offset = processes_in_order.map { |p| p.first }.index(process.name) * 100
base_port.to_i + offset + num - 1
end
private ######################################################################
def fork(process)
def fork(process, options={})
concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
1.upto(concurrency[process.name]) do |num|
fork_individual(process, num, port_for(process, num, options[:port]))
end
end
def fork_individual(process, num, port)
ENV["PORT"] = port.to_s
ENV["PS"] = "#{process.name}.#{num}"
pid = Process.fork do
run(process)
end
@@ -85,29 +98,31 @@ private ######################################################################
def run(process, log_to_file=true)
proctitle "ruby: foreman #{process.name}"
Dir.chdir directory do
FileUtils.mkdir_p "log"
command = process.command
begin
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
end
rescue PTY::ChildExited, Interrupt
begin
info "process exiting", process
rescue Interrupt
end
end
end
def kill_and_exit(signal="TERM")
def kill_all(signal="TERM")
info "terminating"
running_processes.each do |pid, process|
info "killing #{process.name} in pid #{pid}"
Process.kill(signal, pid)
end
exit 0
end
def info(message, process=nil)
@@ -127,8 +142,8 @@ private ######################################################################
end
def pad_process_name(process)
name = process ? process.name : "system"
name.ljust(longest_process_name)
name = process ? "#{ENV["PS"]}" : "system"
name.ljust(longest_process_name + 3) # add 3 for process number padding
end
def print_info
@@ -150,7 +165,8 @@ private ######################################################################
pid, status = Process.wait2
process = running_processes.delete(pid)
info "process terminated", process
kill_and_exit
kill_all
Process.waitall
end
def running_processes

View File

@@ -24,13 +24,7 @@ private ######################################################################
end
def export_template(name)
File.read(File.expand_path("../../../../export/#{name}", __FILE__))
end
def port_for(base_port, app, num)
base_port ||= 5000
offset = engine.processes.keys.sort.index(app) * 100
base_port.to_i + offset + num - 1
File.read(File.expand_path("../../../../data/export/#{name}", __FILE__))
end
def write_file(filename, contents)

View File

@@ -7,7 +7,7 @@ class Foreman::Export::Inittab < Foreman::Export::Base
user = options[:user] || app
log_root = options[:log] || "/var/log/#{app}"
concurrency = parse_concurrency(options[:concurrency])
concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
inittab = []
inittab << "# ----- foreman #{app} processes -----"
@@ -15,7 +15,7 @@ class Foreman::Export::Inittab < Foreman::Export::Base
engine.processes.values.inject(1) do |index, process|
1.upto(concurrency[process.name]) do |num|
id = app.slice(0, 2).upcase + sprintf("%02d", index)
port = port_for(options[:port], process.name, num)
port = engine.port_for(process, num, options[:port])
inittab << "#{id}:4:respawn:/bin/su - #{user} -c 'PORT=#{port} #{process.command} >> #{log_root}/#{process.name}-#{num}.log 2>&1'"
index += 1
end
@@ -27,8 +27,8 @@ class Foreman::Export::Inittab < Foreman::Export::Base
inittab = inittab.join("\n") + "\n"
if fname
FileUtils.mkdir_p(log_root)
FileUtils.chown(user, nil, log_root)
FileUtils.mkdir_p(log_root) rescue error "could not create #{log_root}"
FileUtils.chown(user, nil, log_root) rescue error "could not chown #{log_root} to #{user}"
write_file(fname, inittab)
else
puts inittab

View File

@@ -17,7 +17,7 @@ class Foreman::Export::Upstart < Foreman::Export::Base
FileUtils.rm(file)
end
concurrency = parse_concurrency(options[:concurrency])
concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
master_template = export_template("upstart/master.conf.erb")
master_config = ERB.new(master_template).result(binding)
@@ -31,7 +31,7 @@ class Foreman::Export::Upstart < Foreman::Export::Base
write_file "#{location}/#{app}-#{process.name}.conf", process_master_config
1.upto(concurrency[process.name]) do |num|
port = port_for(options[:port], process.name, num)
port = engine.port_for(process, num, options[:port])
process_config = ERB.new(process_template).result(binding)
write_file "#{location}/#{app}-#{process.name}-#{num}.conf", process_config
end

View File

@@ -1,4 +1,4 @@
foreman(1) -- manage Procfile-based applications
foreman(1) -- manage Psfile-based applications
================================================
## SYNOPSIS
@@ -8,8 +8,8 @@ foreman(1) -- manage Procfile-based applications
## 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
**Foreman** is a manager for Psfile-based applications. Its aim is to
abstract away the details of the Psfile format, and allow you to either run
your application directly or export it to some other process management
format.
@@ -18,7 +18,7 @@ format.
`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.
type of process defined in your Psfile.
If a parameter is passed, foreman will run one instance of the specified
application type.
@@ -29,6 +29,10 @@ The following options control how the application is run:
Specify the number of each process type to run. The value passed in
should be in the format `process=num,process=num`
* `-p`, `--port`:
Specify which port to use as the base for this application. Should be
a multiple of 1000.
## EXPORTING
`foreman export` is used to export your application to another process
@@ -62,8 +66,8 @@ The following options control how the application is run:
These options control all modes of foreman's operation.
* `-f`, `--procfile`:
Specify an alternate location for the application's Procfile. This file's
* `-f`, `--psfile`:
Specify an alternate location for the application's Psfile. This file's
containing directory will be assumed to be the root directory of the
application.
@@ -95,9 +99,9 @@ will be structured to make the following commands valid:
`restart appname-processname-3`
## PROCFILE
## PSFILE
A Procfile should contain both a name for the process and the command used
A Psfile should contain both a name for the process and the command used
to run it.
web bundle exec thin start
@@ -113,9 +117,9 @@ Export the application in upstart format:
$ foreman export upstart /etc/init
Run one process type from the application defined in a specific Procfile:
Run one process type from the application defined in a specific Psfile:
$ foreman start alpha -p ~/app/Procfile
$ foreman start alpha -p ~/myapp/Psfile
## COPYRIGHT

View File

@@ -23,17 +23,26 @@ describe "Foreman::Engine" do
describe "start" do
it "forks the processes" do
write_procfile
mock(subject).fork(subject.processes["alpha"])
mock(subject).fork(subject.processes["bravo"])
mock(subject).fork(subject.processes["alpha"], {})
mock(subject).fork(subject.processes["bravo"], {})
mock(subject).watch_for_termination
subject.start
end
it "handles concurrency" do
write_procfile
mock(subject).fork_individual(subject.processes["alpha"], 1, 5000)
mock(subject).fork_individual(subject.processes["alpha"], 2, 5001)
mock(subject).fork_individual(subject.processes["bravo"], 1, 5100)
mock(subject).watch_for_termination
subject.start(:concurrency => "alpha=2")
end
end
describe "execute" do
it "runs the processes" do
write_procfile
mock(subject).fork(subject.processes["alpha"])
mock(subject).fork(subject.processes["alpha"], {})
mock(subject).watch_for_termination
subject.execute("alpha")
end