Compare commits

...

14 Commits

Author SHA1 Message Date
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
16 changed files with 215 additions and 142 deletions

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
foreman (0.27.0)
foreman (0.30.1)
term-ansicolor (~> 1.0.5)
thor (>= 0.13.6)

2
bin/runner Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
exec $1 2>&1

View File

@@ -3,7 +3,7 @@ Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/
app.uid = "<%= user %>"
app.gid = "<%= user %>"
<% engine.processes.each do |process| %>
<% engine.procfile.entries.each do |process| %>
<% 1.upto(concurrency[process.name]) do |num| %>
<% port = engine.port_for(process, num, options[:port]) %>
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|
children.stop_command "kill -QUIT {{PID}}"
end
process.group = "<%= app %>-<%= process.name %>"
end
<% end %>

View File

@@ -9,5 +9,10 @@ module Foreman
require 'foreman/engine'
Foreman::Engine.load_env!(env_file)
end
def self.runner
File.expand_path("../../bin/runner", __FILE__)
end
end

View File

@@ -8,20 +8,15 @@ class Foreman::CLI < Thor
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
desc "start [PROCESS]", "Start the application, or a specific process"
desc "start", "Start the application"
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 :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
def start(process=nil)
def start
check_procfile!
if process
engine.execute(process)
else
engine.start
end
engine.start
end
desc "export FORMAT LOCATION", "Export the application to another process management format"
@@ -55,8 +50,8 @@ class Foreman::CLI < Thor
desc "check", "Validate your application's Procfile"
def check
error "no processes defined" unless engine.processes.length > 0
display "valid procfile detected (#{engine.processes.map(&:name).join(', ')})"
error "no processes defined" unless engine.procfile.entries.length > 0
display "valid procfile detected (#{engine.procfile.process_names.join(', ')})"
end
private ######################################################################

View File

@@ -7,6 +7,7 @@ require "tempfile"
require "timeout"
require "term/ansicolor"
require "fileutils"
require "thread"
class Foreman::Engine
@@ -23,6 +24,7 @@ class Foreman::Engine
@directory = File.expand_path(File.dirname(procfile))
@options = options
@environment = read_environment_files(options[:env])
@output_mutex = Mutex.new
end
def self.load_env!(env_file)
@@ -32,34 +34,17 @@ class Foreman::Engine
def start
proctitle "ruby: foreman master"
termtitle "#{File.basename(@directory)} - foreman (#{processes.size} processes)"
processes.each do |process|
process.color = next_color
fork process
end
termtitle "#{File.basename(@directory)} - foreman"
trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
trap("INT") { puts "SIGINT received"; terminate_gracefully }
assign_colors
spawn_processes
watch_for_output
watch_for_termination
end
def execute(name)
error "no such process: #{name}" unless process = procfile[name]
process.color = next_color
fork process
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)
base_port ||= 5000
offset = procfile.process_names.index(process.name) * 100
@@ -68,47 +53,20 @@ class Foreman::Engine
private ######################################################################
def fork(process)
def spawn_processes
concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency])
1.upto(concurrency[process.name]) do |num|
fork_individual(process, num, port_for(process, num, @options[:port]))
procfile.entries.each do |entry|
reader, writer = IO.pipe
entry.spawn(concurrency[entry.name], writer, @directory, @environment, base_port).each do |process|
running_processes[process.pid] = process
readers[process] = reader
end
end
end
def fork_individual(process, num, port)
apply_environment!
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
def base_port
options[:port] || 5000
end
def kill_all(signal="SIGTERM")
@@ -120,27 +78,57 @@ private ######################################################################
def terminate_gracefully
info "sending SIGTERM to all processes"
kill_all "SIGTERM"
Timeout.timeout(3) { Process.waitall }
Timeout.timeout(5) { Process.waitall }
rescue Timeout::Error
info "sending SIGKILL to all processes"
kill_all "SIGKILL"
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
pid, status = Process.wait2
process = running_processes.delete(pid)
info "process terminated", process
info "process terminated", process.name
terminate_gracefully
kill_all
rescue Errno::ECHILD
end
def info(message, process=nil)
print process.color if process
print "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(process)} | "
def info(message, name="system", color=Term::ANSIColor.white)
print color
print "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
print Term::ANSIColor.reset
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
def error(message)
@@ -156,9 +144,8 @@ private ######################################################################
end
end
def pad_process_name(process)
name = process ? "#{ENV["PS"]}" : "system"
name.ljust(longest_process_name + 3) # add 3 for process number padding
def pad_process_name(name="system")
name.to_s.ljust(longest_process_name + 3) # add 3 for process number padding
end
def proctitle(title)
@@ -173,6 +160,24 @@ private ######################################################################
@running_processes ||= {}
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
@current_color ||= -1
@current_color += 1

View File

@@ -12,7 +12,7 @@ class Foreman::Export::Inittab < Foreman::Export::Base
inittab = []
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|
id = app.slice(0, 2).upcase + sprintf("%02d", index)
port = engine.port_for(process, num, options[:port])

View File

@@ -3,58 +3,58 @@ require "foreman/export"
class Foreman::Export::Runit < Foreman::Export::Base
ENV_VARIABLE_REGEX = /([a-zA-Z_]+[a-zA-Z0-9_]*)=(\S+)/
def export(location, options={})
error("Must specify a location") unless location
app = options[:app] || File.basename(engine.directory)
user = options[:user] || app
log_root = options[:log] || "/var/log/#{app}"
template_root = options[:template]
concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
run_template = export_template('runit', '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|
process_directory = "#{location}/#{app}-#{process.name}-#{num}"
process_env_directory = "#{process_directory}/env"
process_log_directory = "#{process_directory}/log"
create_directory process_directory
create_directory process_env_directory
create_directory process_log_directory
run = ERB.new(run_template).result(binding)
write_file "#{process_directory}/run", run
port = engine.port_for(process, num, options[:port])
environment_variables = {'PORT' => port}.
merge(engine.environment).
merge(inline_variables(process.command))
environment_variables.each_pair do |var, env|
write_file "#{process_env_directory}/#{var.upcase}", env
end
log_run = ERB.new(log_run_template).result(binding)
write_file "#{process_log_directory}/run", log_run
end
end
end
private
def create_directory(location)
say "creating: #{location}"
FileUtils.mkdir(location)
end
def inline_variables(command)
variable_name_regex =
variable_name_regex =
Hash[*command.scan(ENV_VARIABLE_REGEX).flatten]
end
end
end

View File

@@ -26,7 +26,7 @@ class Foreman::Export::Upstart < Foreman::Export::Base
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
process_master_template = export_template("upstart", "process_master.conf.erb", template_root)
process_master_config = ERB.new(process_master_template).result(binding)

View File

@@ -2,13 +2,70 @@ require "foreman"
class Foreman::Process
attr_reader :name
attr_reader :command
attr_accessor :color
attr_reader :entry
attr_reader :num
attr_reader :pid
attr_reader :port
def initialize(name, command)
@name = name
@command = command
def initialize(entry, num, port)
@entry = entry
@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)
io = case RUBY_VERSION
when /^1\.9\./
IO.popen([Foreman.runner, replace_command_env(command)], "w+")
when /^1\.8\./
full_command = replace_command_env(command).gsub("'", "\\'")
IO.popen("#{Foreman.runner} '#{full_command}'", "w+")
else
raise "Unknown Ruby version: #{RUBY_VERSION}"
end
[ io, io.pid ]
end
def run_process(command, pipe)
io, @pid = fork_with_io(command)
trap("SIGTERM") { "got sigterm for %d" % @pid }
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

View File

@@ -1,4 +1,5 @@
require "foreman"
require "foreman/procfile_entry"
# A valid Procfile entry is captured by this regex.
# All other lines are ignored.
@@ -10,18 +11,18 @@ require "foreman"
#
class Foreman::Procfile
attr_reader :processes
attr_reader :entries
def initialize(filename)
@processes = parse_procfile(filename)
end
def process_names
processes.map(&:name)
@entries = parse_procfile(filename)
end
def [](name)
processes.detect { |process| process.name == name }
entries.detect { |entry| entry.name == name }
end
def process_names
entries.map(&:name)
end
private
@@ -29,7 +30,7 @@ private
def parse_procfile(filename)
File.read(filename).split("\n").map do |line|
if line =~ /^([A-Za-z0-9_]+):\s*(.+)$/
Foreman::Process.new($1, $2)
Foreman::ProcfileEntry.new($1, $2)
end
end.compact
end

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

View File

@@ -1,5 +1,5 @@
module Foreman
VERSION = "0.27.0"
VERSION = "0.30.1"
end

View File

@@ -24,8 +24,9 @@ describe "Foreman::Engine" do
describe "start" do
it "forks the processes" do
write_procfile
mock(subject).fork(subject.procfile["alpha"])
mock(subject).fork(subject.procfile["bravo"])
mock.instance_of(Foreman::Process).run_process("./alpha", is_a(IO))
mock.instance_of(Foreman::Process).run_process("./bravo", is_a(IO))
mock(subject).watch_for_output
mock(subject).watch_for_termination
subject.start
end
@@ -33,29 +34,14 @@ describe "Foreman::Engine" do
it "handles concurrency" do
write_procfile
engine = Foreman::Engine.new("Procfile",:concurrency => "alpha=2")
mock(engine).fork_individual(engine.procfile["alpha"], 1, 5000)
mock(engine).fork_individual(engine.procfile["alpha"], 2, 5001)
mock(engine).fork_individual(engine.procfile["bravo"], 1, 5100)
mock.instance_of(Foreman::Process).run_process("./alpha", is_a(IO)).twice
mock.instance_of(Foreman::Process).run_process("./bravo", is_a(IO))
mock(engine).watch_for_output
mock(engine).watch_for_termination
engine.start
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
before(:each) do
write_procfile
@@ -66,9 +52,10 @@ describe "Foreman::Engine" do
File.open("/tmp/env", "w") { |f| f.puts("FOO=baz") }
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env")
stub(engine).info
mock(engine).spawn_processes
mock(engine).watch_for_termination
engine.environment.should == {"FOO"=>"baz"}
engine.execute("alpha")
engine.start
end
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") }
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env1,/tmp/env2")
stub(engine).info
mock(engine).spawn_processes
mock(engine).watch_for_termination
engine.environment.should == { "FOO"=>"bar", "BAZ"=>"qux" }
engine.execute("alpha")
engine.start
end
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
File.open(".env", "w") { |f| f.puts("FOO=qoo") }
engine = Foreman::Engine.new("Procfile")
stub(engine).info
mock(engine).spawn_processes
mock(engine).watch_for_termination
mock(engine).fork_individual(anything, anything, anything)
engine.environment.should == {"FOO"=>"qoo"}
engine.execute("bravo")
engine.start
end
end
end

View File

@@ -13,8 +13,7 @@ describe Foreman::Export::Bluepill do
it "exports to the filesystem" do
bluepill.export("/tmp/init", :concurrency => "alpha=2")
File.read("/tmp/init/app.pill").should == example_export_file("bluepill/app.pill")
end
end
end

View File

@@ -19,7 +19,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}"
end
process.group = "app-alpha"
end
@@ -37,7 +37,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}"
end
process.group = "app-alpha"
end
@@ -57,7 +57,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}"
end
process.group = "app-bravo"
end