massive refactoring for programmatic control and stability

This commit is contained in:
David Dollar
2012-06-10 22:58:09 -04:00
parent f41cc552c7
commit 51a704939e
65 changed files with 984 additions and 1317 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/.bundle /.bundle
/.rbenv-version /.rbenv-version
/.yardoc
/coverage /coverage
/example/log/* /example/log/*
/man/*.html /man/*.html

View File

@@ -19,4 +19,5 @@ group :development do
gem 'rspec', '~> 2.0' gem 'rspec', '~> 2.0'
gem "simplecov", :require => false gem "simplecov", :require => false
gem 'timecop' gem 'timecop'
gem 'yard'
end end

View File

@@ -43,6 +43,7 @@ GEM
timecop (0.3.5) timecop (0.3.5)
win32console (1.3.0-x86-mingw32) win32console (1.3.0-x86-mingw32)
xml-simple (1.0.15) xml-simple (1.0.15)
yard (0.8.2)
PLATFORMS PLATFORMS
java java
@@ -61,3 +62,4 @@ DEPENDENCIES
simplecov simplecov
timecop timecop
win32console (~> 1.3.0) win32console (~> 1.3.0)
yard

View File

@@ -1,3 +1,4 @@
ticker: ruby ./ticker $PORT ticker: ruby ./ticker $PORT
error: ruby ./error error: ruby ./error
utf8: ruby ./utf8 utf8: ruby ./utf8
spawner: ./spawner

14
data/example/spawnee Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
NAME="$1"
sigterm() {
echo "$NAME: got sigterm"
}
#trap sigterm SIGTERM
while true; do
echo "$NAME: ping"
sleep 1
done

7
data/example/spawner Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
./spawnee A &
./spawnee B &
./spawnee C &
wait

View File

@@ -3,25 +3,25 @@ Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/
app.uid = "<%= user %>" app.uid = "<%= user %>"
app.gid = "<%= user %>" app.gid = "<%= user %>"
<% engine.procfile.entries.each do |process| %> <% engine.each_process do |name, process| %>
<% 1.upto(concurrency[process.name]) do |num| %> <% 1.upto(engine.formation[name]) do |num| %>
<% port = engine.port_for(process, num, self.port) %> <% port = engine.port_for(process, num) %>
app.process("<%= process.name %>-<%=num%>") do |process| app.process("<%= name %>-<%= num %>") do |process|
process.start_command = "<%= process.command.gsub("$PORT", port.to_s) %>" process.start_command = "<%= process.command %>"
process.working_dir = "<%= engine.directory %>" process.working_dir = "<%= engine.root %>"
process.daemonize = true process.daemonize = true
process.environment = {"PORT" => "<%= port %>"<% engine.environment.each_pair do |var,env| %> , "<%= var.upcase %>" => "<%= env %>" <% end %>} process.environment = <%= engine.env.merge("PORT" => port.to_s).inspect %>
process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
process.stop_grace_time = 45.seconds process.stop_grace_time = 45.seconds
process.stdout = process.stderr = "<%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log" process.stdout = process.stderr = "<%= log %>/<%= app %>-<%= name %>-<%= num %>.log"
process.monitor_children do |children| process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}" children.stop_command "kill {{PID}}"
end end
process.group = "<%= app %>-<%= process.name %>" process.group = "<%= app %>-<%= name %>"
end end
<% end %> <% end %>
<% end %> <% end %>

View File

@@ -3,7 +3,7 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>Label</key> <key>Label</key>
<string><%= "#{app}-#{process.name}-#{num}" %></string> <string><%= "#{app}-#{name}-#{num}" %></string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string><%= process.command %></string> <string><%= process.command %></string>
@@ -13,10 +13,10 @@
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>
<key>StandardErrorPath</key> <key>StandardErrorPath</key>
<string><%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log</string> <string><%= log %>/<%= app %>-<%= name %>-<%=num%>.log</string>
<key>UserName</key> <key>UserName</key>
<string><%= user %></string> <string><%= user %></string>
<key>WorkingDirectory</key> <key>WorkingDirectory</key>
<string><%= engine.directory %></string> <string><%= engine.root %></string>
</dict> </dict>
</plist> </plist>

View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -e
LOG=<%= log %>/<%= name %>-<%= num %>
test -d "$LOG" || mkdir -p m2750 "$LOG" && chown <%= user %> "$LOG"
exec chpst -u <%= user %> svlogd "$LOG"

View File

@@ -1,7 +0,0 @@
#!/bin/sh
set -e
LOG=<%= log_root %>/<%= process.name %>-<%= num %>
test -d "$LOG" || mkdir -p m2750 "$LOG" && chown <%= user %> "$LOG"
exec chpst -u <%= user %> svlogd "$LOG"

View File

@@ -1,3 +1,3 @@
#!/bin/sh #!/bin/sh
cd <%= engine.directory %> cd <%= engine.root %>
exec chpst -u <%= user %> -e <%= process_env_directory %> <%= process.command %> exec chpst -u <%= user %> -e <%= File.join(location, "#{process_directory}/env") %> <%= process.command %>

View File

@@ -1,23 +1,23 @@
<% <%
app_names = [] app_names = []
engine.procfile.entries.each do |process| engine.each_process do |name, process|
next if (conc = self.concurrency[process.name]) < 1 1.upto(engine.formation[name]) do |num|
1.upto(self.concurrency[process.name]) do |num| port = engine.port_for(process, num)
port = engine.port_for(process, num, self.port) full_name = "#{app}-#{name}-#{num}"
name = if (conc > 1); "#{process.name}-#{num}" else process.name; end environment = engine.env.merge("PORT" => port.to_s).map do |key, value|
environment = (engine.environment.keys.sort.map{ |var| %{#{var.upcase}="#{engine.environment[var]}"} } + [%{PORT="#{port}"}]) "#{key}=#{shell_quote(value)}"
app_name = "#{app}-#{name}" end
app_names << app_name app_names << full_name
%> %>
[program:<%= app_name %>] [program:<%= full_name %>]
command=<%= process.command %> command=<%= process.command %>
autostart=true autostart=true
autorestart=true autorestart=true
stopsignal=QUIT stopsignal=QUIT
stdout_logfile=<%= log_root %>/<%=process.name%>-<%=num%>-out.log stdout_logfile=<%= log %>/<%= name %>-<%= num %>.log
stderr_logfile=<%= log_root %>/<%=process.name%>-<%=num%>-err.log stderr_logfile=<%= log %>/<%= name %>-<%= num %>.error.log
user=<%= user %> user=<%= user %>
directory=<%= engine.directory %> directory=<%= engine.root %>
environment=<%= environment.join(',') %><% environment=<%= environment.join(',') %><%
end end
end end

View File

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

View File

@@ -1,5 +1,5 @@
start on starting <%= app %>-<%= process.name %> start on starting <%= app %>-<%= name %>
stop on stopping <%= app %>-<%= process.name %> stop on stopping <%= app %>-<%= name %>
respawn respawn
exec su - <%= user %> -c 'cd <%= engine.directory %>; export PORT=<%= port %>;<% engine.environment.each_pair do |var,env| %> export <%= var.upcase %>=<%= shell_quote(env) %>; <% end %> <%= process.command %> >> <%= log_root %>/<%=process.name%>-<%=num%>.log 2>&1' exec su - <%= user %> -c 'cd <%= engine.root %>; export PORT=<%= port %>;<% engine.env.each_pair do |var,env| %> export <%= var.upcase %>=<%= shell_quote(env) %>; <% end %> <%= process.command %> >> <%= log %>/<%=name%>-<%=num%>.log 2>&1'

View File

@@ -1,38 +1,37 @@
require "foreman" require "foreman"
require "foreman/helpers" require "foreman/helpers"
require "foreman/engine" require "foreman/engine"
require "foreman/tmux_engine" require "foreman/engine/cli"
require "foreman/export" require "foreman/export"
require "shellwords" require "shellwords"
require "thor" require "thor"
require "yaml"
class Foreman::CLI < Thor class Foreman::CLI < Thor
include Foreman::Helpers include Foreman::Helpers
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile" class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
class_option :tmux, :type => :boolean, :aliases => "-t", :desc => "Run in tmux session" class_option :root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
desc "start [PROCESS]", "Start the application (or a specific PROCESS)" desc "start [PROCESS]", "Start the application (or a specific PROCESS)"
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
class_option :app_root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
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 :formation, :type => :string, :aliases => "-m", :banner => '"alpha=5,bar=3"'
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"'
class << self class << self
# Hackery. Take the run method away from Thor so that we can redefine it. # Hackery. Take the run method away from Thor so that we can redefine it.
def is_thor_reserved_word?(word, type) def is_thor_reserved_word?(word, type)
return false if word == 'run' return false if word == "run"
super super
end end
end end
def start(process=nil) def start(process=nil)
check_procfile! check_procfile!
engine.options[:concurrency] = "#{process}=1" if process load_environment!
engine.load_procfile(procfile)
engine.options[:formation] = "#{process}=1" if process
engine.start engine.start
end end
@@ -48,6 +47,8 @@ class Foreman::CLI < Thor
def export(format, location=nil) def export(format, location=nil)
check_procfile! check_procfile!
load_environment!
engine.load_procfile(procfile)
formatter = Foreman::Export.formatter(format) formatter = Foreman::Export.formatter(format)
formatter.new(location, engine, options).export formatter.new(location, engine, options).export
rescue Foreman::Export::Exception => ex rescue Foreman::Export::Exception => ex
@@ -58,16 +59,19 @@ class Foreman::CLI < Thor
def check def check
check_procfile! check_procfile!
error "no processes defined" unless engine.procfile.entries.length > 0 engine.load_procfile(procfile)
puts "valid procfile detected (#{engine.procfile.process_names.join(', ')})" error "no processes defined" unless engine.processes.length > 0
puts "valid procfile detected (#{engine.process_names.join(', ')})"
end end
desc "run COMMAND [ARGS...]", "Run a command using your application's environment" desc "run COMMAND [ARGS...]", "Run a command using your application's environment"
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
def run(*args) def run(*args)
engine.apply_environment! load_environment!
begin begin
exec args.shelljoin exec engine.env, args.shelljoin
rescue Errno::EACCES rescue Errno::EACCES
error "not executable: #{args.first}" error "not executable: #{args.first}"
rescue Errno::ENOENT rescue Errno::ENOENT
@@ -75,44 +79,55 @@ class Foreman::CLI < Thor
end end
end end
class << self no_tasks do
def new_engine(procfile, options) def engine
@engine_class ||= options[:tmux] ? Foreman::TmuxEngine : Foreman::Engine @engine ||= begin
@engine_class.new(procfile, options) engine_class = Foreman::Engine::CLI
engine = engine_class.new(
:formation => options[:formation],
:port => options[:port],
:root => options[:root]
)
engine
end end
def engine_class=(klass)
@engine_class = klass
end end
end end
private ###################################################################### private ######################################################################
def error(message)
puts "ERROR: #{message}"
exit 1
end
def check_procfile! def check_procfile!
error("#{procfile} does not exist.") unless File.exist?(procfile) error("#{procfile} does not exist.") unless File.exist?(procfile)
end end
def engine def load_environment!
@engine ||= self.class.new_engine(procfile, options) if options[:env]
options[:env].split(",").each do |file|
engine.load_env file
end
else
default_env = File.join(engine.root, ".env")
engine.load_env default_env if File.exists?(default_env)
end
end end
def procfile def procfile
case case
when options[:procfile] then options[:procfile] when options[:procfile] then options[:procfile]
when options[:app_root] then File.expand_path(File.join(options[:app_root], "Procfile")) when options[:root] then File.expand_path(File.join(options[:app_root], "Procfile"))
else "Procfile" else "Procfile"
end end
end end
def error(message)
puts "ERROR: #{message}"
exit 1
end
def options def options
original_options = super original_options = super
return original_options unless File.exists?(".foreman") return original_options unless File.exists?(".foreman")
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

View File

@@ -1,40 +0,0 @@
require "foreman"
module Foreman::Color
ANSI = {
:reset => 0,
:black => 30,
:red => 31,
:green => 32,
:yellow => 33,
:blue => 34,
:magenta => 35,
:cyan => 36,
:white => 37,
:bright_black => 30,
:bright_red => 31,
:bright_green => 32,
:bright_yellow => 33,
:bright_blue => 34,
:bright_magenta => 35,
:bright_cyan => 36,
:bright_white => 37,
}
def self.enable(io)
io.extend(self)
end
def color?
return false unless self.respond_to?(:isatty)
self.isatty && ENV["TERM"]
end
def color(name)
return "" unless color?
return "" unless ansi = ANSI[name.to_sym]
"\e[#{ansi}m"
end
end

View File

@@ -1,8 +1,7 @@
require "foreman" require "foreman"
require "foreman/color" require "foreman/env"
require "foreman/process" require "foreman/process"
require "foreman/procfile" require "foreman/procfile"
require "foreman/utils"
require "tempfile" require "tempfile"
require "timeout" require "timeout"
require "fileutils" require "fileutils"
@@ -10,126 +9,252 @@ require "thread"
class Foreman::Engine class Foreman::Engine
attr_reader :environment attr_reader :env
attr_reader :procfile
attr_reader :directory
attr_reader :options attr_reader :options
attr_reader :processes
COLORS = %w( cyan yellow green magenta red blue intense_cyan intense_yellow # Create an +Engine+ for running processes
intense_green intense_magenta intense_red, intense_blue ) #
# @param [Hash] options
Foreman::Color.enable($stdout) #
# @option options [String] :formation (all=1) The process formation to use
def initialize(procfile, options={}) # @option options [Fixnum] :port (5000) The base port to assign to processes
@procfile = Foreman::Procfile.new(procfile) if File.exists?(procfile) # @option options [String] :root (Dir.pwd) The root directory from which to run processes
@directory = options[:app_root] || File.expand_path(File.dirname(procfile)) #
def initialize(options={})
@options = options.dup @options = options.dup
@output_mutex = Mutex.new
@options[:env] ||= default_env @options[:formation] ||= "all=1"
@environment = read_environment_files(@options[:env])
@env = {}
@mutex = Mutex.new
@names = {}
@processes = []
@running = {}
@readers = {}
end end
# Start the processes registered to this +Engine+
#
def start def start
proctitle "ruby: foreman master"
termtitle "#{File.basename(@directory)} - foreman"
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 }
trap("HUP") { puts "SIGHUP received"; terminate_gracefully } trap("HUP") { puts "SIGHUP received"; terminate_gracefully }
assign_colors startup
spawn_processes spawn_processes
watch_for_output watch_for_output
watch_for_termination sleep 0.1
watch_for_termination { terminate_gracefully }
shutdown
end end
def port_for(process, num, base_port=nil) # Register a process to be run by this +Engine+
base_port ||= 5000 #
offset = procfile.process_names.index(process.name) * 100 # @param [String] name A name for this process
base_port.to_i + offset + num - 1 # @param [String] command The command to run
# @param [Hash] options
#
# @option options [Hash] :env A custom environment for this process
#
def register(name, command, options={})
options[:env] ||= env
options[:cwd] ||= File.dirname(command.split(" ").first)
process = Foreman::Process.new(command, options)
@names[process] = name
@processes << process
end end
def apply_environment! # Clear the processes registered to this +Engine+
environment.each { |k,v| ENV[k] = v } #
def clear
@names = {}
@processes = []
end end
def self.read_environment(filename) # Register processes by reading a Procfile
return {} unless File.exists?(filename) #
# @param [String] filename A Procfile from which to read processes to register
#
def load_procfile(filename)
options[:root] ||= File.dirname(filename)
Foreman::Procfile.new(filename).entries do |name, command|
register name, command, :cwd => options[:root]
end
self
end
File.read(filename).split("\n").inject({}) do |hash, line| # Load a .env file into the +env+ for this +Engine+
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/ #
key, val = [$1, $2] # @param [String] filename A .env file to load into the environment
case val #
when /\A'(.*)'\z/ then hash[key] = $1 def load_env(filename)
when /\A"(.*)"\z/ then hash[key] = $1.gsub(/\\(.)/, '\1') Foreman::Env.new(filename).entries do |name, value|
else hash[key] = val @env[name] = value
end
end
hash
end end
end end
private ###################################################################### # Send a signal to all processesstarted by this +Engine+
#
# @param [String] signal The signal to send to each process
#
def killall(signal="SIGTERM")
@running.each do |pid, (process, index)|
system "sending #{signal} to #{name_for(pid)} at pid #{pid}"
begin
Process.kill(signal, -1 * pid)
rescue Errno::ESRCH, Errno::EPERM
end
end
end
def spawn_processes # Get the process formation
concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency]) #
# @returns [Fixnum] The formation count for the specified process
#
def formation
@formation ||= parse_formation(options[:formation])
end
procfile.entries.each do |entry| # List the available process names
reader, writer = (IO.method(:pipe).arity == 0 ? IO.pipe : IO.pipe("BINARY")) #
entry.spawn(concurrency[entry.name], writer, @directory, @environment, port_for(entry, 1, base_port)).each do |process| # @returns [Array] A list of process names
running_processes[process.pid] = process #
readers[process] = reader def process_names
@processes.map { |p| @names[p] }
end
# Get the +Process+ for a specifid name
#
# @param [String] name The process name
#
# @returns [Foreman::Process] The +Process+ for the specified name
#
def process(name)
@names.invert[name]
end
# Yield each +Process+ in order
#
def each_process
process_names.each do |name|
yield name, process(name)
end end
end end
# Get the root directory for this +Engine+
#
# @returns [String] The root directory
#
def root
File.expand_path(options[:root] || Dir.pwd)
end end
# Get the port for a given process and offset
#
# @param [Foreman::Process] process A +Process+ associated with this engine
# @param [Fixnum] instance The instance of the process
#
# @returns [Fixnum] port The port to use for this instance of this process
#
def port_for(process, instance)
base_port + (@processes.index(process) * 100) + (instance - 1)
end
private
### Engine API ######################################################
def startup
raise TypeError, "must use a subclass of Foreman::Engine"
end
def output(name, data)
raise TypeError, "must use a subclass of Foreman::Engine"
end
def shutdown
raise TypeError, "must use a subclass of Foreman::Engine"
end
## Helpers ##########################################################
def base_port def base_port
options[:port] || environment["PORT"] || ENV["PORT"] || 5000 (options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i
end end
def kill_all(signal="SIGTERM") def create_pipe
running_processes.each do |pid, process| IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
info "sending #{signal} to pid #{pid}" end
process.kill signal
def name_for(pid)
process, index = @running[pid]
[ @names[process], index.to_s ].compact.join(".")
end
def parse_formation(formation)
pairs = @options[:formation].to_s.gsub(/\s/, "").split(",")
pairs.inject(Hash.new(0)) do |ax, pair|
process, amount = pair.split("=")
process == "all" ? ax.default = amount.to_i : ax[process] = amount.to_i
ax
end end
end end
def terminate_gracefully def output_with_mutex(name, message)
return if @terminating @mutex.synchronize do
@terminating = true output name, message
info "sending SIGTERM to all processes"
kill_all "SIGTERM"
Timeout.timeout(5) do
while running_processes.length > 0
pid, status = Process.wait2
process = running_processes.delete(pid)
info "process terminated", process.name
end end
end end
rescue Timeout::Error
info "sending SIGKILL to all processes"
kill_all "SIGKILL"
end
def poll_readers def system(message)
rs, ws = IO.select(readers.values, [], [], 1) output_with_mutex "system", message
(rs || []).each do |r| end
data = r.gets
next unless data def termination_message_for(status)
data.force_encoding("BINARY") if data.respond_to?(:force_encoding) if status.exited?
ps, message = data.split(",", 2) "exited with code #{status.exitstatus}"
color = colors[ps.split(".").first] elsif status.signaled?
info message, ps, color "terminated by SIG#{Signal.list.invert[status.termsig]}"
else
"died a mysterious death"
end
end
def flush_reader(reader)
until reader.eof?
data = reader.gets
output_with_mutex name_for(@readers.key(reader)), data
end
end
## Engine ###########################################################
def spawn_processes
@processes.each do |process|
1.upto(formation[@names[process]]) do |n|
reader, writer = create_pipe
begin
pid = process.run(:output => writer, :env => { "PORT" => port_for(process, n).to_s })
writer.puts "started with pid #{pid}"
rescue Errno::ENOENT
writer.puts "unknown command: #{process.command}"
end
@running[pid] = [process, n]
@readers[pid] = reader
end
end end
end end
def watch_for_output def watch_for_output
Thread.new do Thread.new do
require "win32console" if Foreman.windows?
begin begin
loop do loop do
poll_readers (IO.select(@readers.values).first || []).each do |reader|
data = reader.gets
output_with_mutex name_for(@readers.key(reader)), data
end
end end
rescue Exception => ex rescue Exception => ex
puts ex.message puts ex.message
@@ -140,89 +265,24 @@ private ######################################################################
def watch_for_termination def watch_for_termination
pid, status = Process.wait2 pid, status = Process.wait2
process = running_processes.delete(pid) output_with_mutex name_for(pid), termination_message_for(status)
info "process terminated", process.name @running.delete(pid)
terminate_gracefully yield if block_given?
pid
rescue Errno::ECHILD rescue Errno::ECHILD
end end
def info(message, name="system", color=:white) def terminate_gracefully
output = "" return if @terminating
output += $stdout.color(color) @terminating = true
output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | " system "sending SIGTERM to all processes"
output += $stdout.color(:reset) killall "SIGTERM"
output += message.chomp Timeout.timeout(5) do
puts output watch_for_termination while @running.length > 0
end end
rescue Timeout::Error
def print(message=nil) system "sending SIGKILL to all processes"
@output_mutex.synchronize do killall "SIGKILL"
$stdout.print message
end
end
def puts(message=nil)
@output_mutex.synchronize do
$stdout.puts message
end
end
def longest_process_name
@longest_process_name ||= begin
longest = procfile.process_names.map { |name| name.length }.sort.last
longest = 6 if longest < 6 # system
longest
end
end
def pad_process_name(name="system")
name.to_s.ljust(longest_process_name + 3) # add 3 for process number padding
end
def proctitle(title)
$0 = title
end
def termtitle(title)
printf("\033]0;#{title}\007") unless Foreman.windows?
end
def running_processes
@running_processes ||= {}
end
def readers
@readers ||= {}
end
def colors
@colors ||= {}
end
def assign_colors
procfile.entries.each_with_index do |entry, idx|
colors[entry.name] = COLORS[idx % COLORS.length]
end
end
def process_by_reader(reader)
readers.invert[reader]
end
def read_environment_files(filenames)
environment = {}
(filenames || "").split(",").map(&:strip).each do |filename|
error "No such file: #{filename}" unless File.exists?(filename)
environment.merge!(Foreman::Engine.read_environment(filename))
end
environment
end
def default_env
env = File.join(directory, ".env")
File.exists?(env) ? env : ""
end end
end end

98
lib/foreman/engine/cli.rb Normal file
View File

@@ -0,0 +1,98 @@
require "foreman/engine"
class Foreman::Engine::CLI < Foreman::Engine
module Color
ANSI = {
:reset => 0,
:black => 30,
:red => 31,
:green => 32,
:yellow => 33,
:blue => 34,
:magenta => 35,
:cyan => 36,
:white => 37,
:bright_black => 30,
:bright_red => 31,
:bright_green => 32,
:bright_yellow => 33,
:bright_blue => 34,
:bright_magenta => 35,
:bright_cyan => 36,
:bright_white => 37,
}
def self.enable(io)
io.extend(self)
end
def color?
return false unless self.respond_to?(:isatty)
self.isatty && ENV["TERM"]
end
def color(name)
return "" unless color?
return "" unless ansi = ANSI[name.to_sym]
"\e[#{ansi}m"
end
end
FOREMAN_COLORS = %w( cyan yellow green magenta red blue intense_cyan intense_yellow
intense_green intense_magenta intense_red, intense_blue )
def startup
@colors = map_colors
proctitle "foreman: master"
end
def output(name, data)
data.to_s.chomp.split("\n").each do |message|
Color.enable($stdout) unless $stdout.respond_to?(:color?)
output = ""
output += $stdout.color(@colors[name.split(".").first].to_sym)
output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
output += $stdout.color(:reset)
output += message
$stdout.puts output
end
end
def shutdown
end
private
def name_padding
@name_padding ||= begin
index_padding = @names.values.map { |n| formation[n] }.max.to_s.length + 1
name_padding = @names.values.map { |n| n.length + index_padding }.sort.last
[ 6, name_padding ].max
end
end
def pad_process_name(name)
name.ljust(name_padding, " ")
end
def map_colors
colors = Hash.new("white")
@names.values.each_with_index do |name, index|
colors[name] = FOREMAN_COLORS[index % FOREMAN_COLORS.length]
end
colors["system"] = "intense_white"
colors
end
def proctitle(title)
$0 = title
end
def termtitle(title)
printf("\033]0;#{title}\007") unless Foreman.windows?
end
end

27
lib/foreman/env.rb Normal file
View File

@@ -0,0 +1,27 @@
require "foreman"
class Foreman::Env
attr_reader :entries
def initialize(filename)
@entries = File.read(filename).split("\n").inject({}) do |ax, line|
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
key = $1
case val = $2
when /\A'(.*)'\z/ then ax[key] = $1
when /\A"(.*)"\z/ then ax[key] = $1.gsub(/\\(.)/, '\1')
else ax[key] = val
end
end
ax
end
end
def entries
@entries.each do |key, value|
yield key, value
end
end
end

View File

@@ -24,7 +24,6 @@ module Foreman::Export
end end
require "foreman/export/base" require "foreman/export/base"
require "foreman/export/inittab" require "foreman/export/inittab"
require "foreman/export/upstart" require "foreman/export/upstart"

View File

@@ -1,23 +1,37 @@
require "foreman/export" require "foreman/export"
require "foreman/utils" require "shellwords"
class Foreman::Export::Base class Foreman::Export::Base
attr_reader :location, :engine, :app, :log, :port, :user, :template, :concurrency attr_reader :location
attr_reader :engine
attr_reader :options
attr_reader :formation
def initialize(location, engine, options={}) def initialize(location, engine, options={})
@location = location @location = location
@engine = engine @engine = engine
@app = options[:app] @options = options.dup
@log = options[:log] @formation = engine.formation
@port = options[:port]
@user = options[:user]
@template = options[:template]
@concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
end end
def export def export
raise "export method must be overridden" error("Must specify a location") unless location
FileUtils.mkdir_p(location) rescue error("Could not create: #{location}")
FileUtils.mkdir_p(log) rescue error("Could not create: #{log}")
FileUtils.chown(user, nil, log) rescue error("Could not chown #{log} to #{user}")
end
def app
options[:app] || "app"
end
def log
options[:log] || "/var/log/#{app}"
end
def user
options[:user] || app
end end
private ###################################################################### private ######################################################################
@@ -30,37 +44,46 @@ private ######################################################################
puts "[foreman export] %s" % message puts "[foreman export] %s" % message
end end
def export_template(exporter, file, template_root) def clean(filename)
if template_root && File.exist?(file_path = File.join(template_root, file)) return unless File.exists?(filename)
File.read(file_path) say "cleaning up: #{filename}"
elsif File.exist?(file_path = File.expand_path(File.join("~/.foreman/templates", file))) FileUtils.rm(filename)
File.read(file_path)
else
File.read(File.expand_path("../../../../data/export/#{exporter}/#{file}", __FILE__))
end end
def shell_quote(value)
'"' + Shellwords.escape(value) + '"'
end
def export_template(name)
name_without_first = name.split("/")[1..-1].join("/")
matchers = []
matchers << File.join(options[:template], name_without_first) if options[:template]
matchers << File.expand_path("~/.foreman/templates/#{name}")
matchers << File.expand_path("../../../../data/export/#{name}", __FILE__)
File.read(matchers.detect { |m| File.exists?(m) })
end
def write_template(name, target, binding)
compiled = ERB.new(export_template(name)).result(binding)
write_file target, compiled
end
def chmod(mode, file)
say "setting #{file} to mode #{mode}"
FileUtils.chmod mode, File.join(location, file)
end
def create_directory(dir)
say "creating: #{dir}"
FileUtils.mkdir_p(File.join(location, dir))
end end
def write_file(filename, contents) def write_file(filename, contents)
say "writing: #{filename}" say "writing: #{filename}"
File.open(filename, "w") do |file| File.open(File.join(location, filename), "w") do |file|
file.puts contents file.puts contents
end end
end end
# Quote a string to be used on the command line. Backslashes are escapde to \\ and quotes
# escaped to \"
#
# str - string to be quoted
#
# Examples
#
# shell_quote("FB|123\"\\1")
# # => "\"FB|123\"\\"\\\\1\""
#
# Returns the the escaped string surrounded by quotes
def shell_quote(str)
"\"#{str.gsub(/\\/){ '\\\\' }.gsub(/["]/){ "\\\"" }}\""
end
end end

View File

@@ -4,23 +4,9 @@ require "foreman/export"
class Foreman::Export::Bluepill < Foreman::Export::Base class Foreman::Export::Bluepill < Foreman::Export::Base
def export def export
error("Must specify a location") unless location super
clean "#{location}/#{app}.pill"
FileUtils.mkdir_p location write_template "bluepill/master.pill.erb", "#{app}.pill", binding
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
template_root = self.template
Dir["#{location}/#{app}.pill"].each do |file|
say "cleaning up: #{file}"
FileUtils.rm(file)
end
master_template = export_template("bluepill", "master.pill.erb", template_root)
master_config = ERB.new(master_template).result(binding)
write_file "#{location}/#{app}.pill", master_config
end end
end end

View File

@@ -3,21 +3,19 @@ require "foreman/export"
class Foreman::Export::Inittab < Foreman::Export::Base class Foreman::Export::Inittab < Foreman::Export::Base
def export def export
app = self.app || File.basename(engine.directory) error("Must specify a location") unless location
user = self.user || app
log_root = self.log || "/var/log/#{app}"
inittab = [] inittab = []
inittab << "# ----- foreman #{app} processes -----" inittab << "# ----- foreman #{app} processes -----"
engine.procfile.entries.inject(1) do |index, process| index = 1
1.upto(self.concurrency[process.name]) do |num| engine.each_process do |name, process|
1.upto(engine.formation[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, self.port) port = engine.port_for(process, num)
inittab << "#{id}:4:respawn:/bin/su - #{user} -c 'PORT=#{port} #{process.command} >> #{log_root}/#{process.name}-#{num}.log 2>&1'" inittab << "#{id}:4:respawn:/bin/su - #{user} -c 'PORT=#{port} #{process.command} >> #{log}/#{name}-#{num}.log 2>&1'"
index += 1 index += 1
end end
index
end end
inittab << "# ----- end foreman #{app} processes -----" inittab << "# ----- end foreman #{app} processes -----"
@@ -27,9 +25,8 @@ class Foreman::Export::Inittab < Foreman::Export::Base
if location == "-" if location == "-"
puts inittab puts inittab
else else
FileUtils.mkdir_p(log_root) rescue error "could not create #{log_root}" say "writing: #{location}"
FileUtils.chown(user, nil, log_root) rescue error "could not chown #{log_root} to #{user}" File.open(location, "w") { |file| file.puts inittab }
write_file(location, inittab)
end end
end end

View File

@@ -4,24 +4,12 @@ require "foreman/export"
class Foreman::Export::Launchd < Foreman::Export::Base class Foreman::Export::Launchd < Foreman::Export::Base
def export def export
error("Must specify a location") unless location super
engine.each_process do |name, process|
app = self.app || File.basename(engine.directory) 1.upto(engine.formation[name]) do |num|
user = self.user || app write_template "launchd/launchd.plist.erb", "#{app}-#{name}-#{num}.plist", binding
log_root = self.log || "/var/log/#{app}"
template_root = self.template
FileUtils.mkdir_p(location)
engine.procfile.entries.each do |process|
1.upto(self.concurrency[process.name]) do |num|
master_template = export_template("launchd", "launchd.plist.erb", template_root)
master_config = ERB.new(master_template).result(binding)
write_file "#{location}/#{app}-#{process.name}-#{num}.plist", master_config
end end
end end
end end
end end

View File

@@ -2,58 +2,33 @@ require "erb"
require "foreman/export" 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 def export
error("Must specify a location") unless location super
app = self.app || File.basename(engine.directory) engine.each_process do |name, process|
user = self.user || app 1.upto(engine.formation[name]) do |num|
log_root = self.log || "/var/log/#{app}" process_directory = "#{app}-#{name}-#{num}"
template_root = self.template
run_template = export_template('runit', 'run.erb', template_root)
log_run_template = export_template('runit', 'log_run.erb', template_root)
engine.procfile.entries.each do |process|
1.upto(self.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_directory
create_directory process_env_directory create_directory "#{process_directory}/env"
create_directory process_log_directory create_directory "#{process_directory}/log"
run = ERB.new(run_template).result(binding) write_template "runit/run.erb", "#{process_directory}/run", binding
write_file "#{process_directory}/run", run chmod 0755, "#{process_directory}/run"
FileUtils.chmod 0755, "#{process_directory}/run"
port = engine.port_for(process, num, self.port) port = engine.port_for(process, num)
environment_variables = {'PORT' => port}. engine.env.merge("PORT" => port.to_s).each do |key, value|
merge(engine.environment). write_file "#{process_directory}/env/#{key}", value
merge(inline_variables(process.command))
environment_variables.each_pair do |var, env|
write_file "#{process_env_directory}/#{var.upcase}", env
end end
log_run = ERB.new(log_run_template).result(binding) write_template "runit/log/run.erb", "#{process_directory}/log/run", binding
write_file "#{process_log_directory}/run", log_run chmod 0755, "#{process_directory}/log/run"
FileUtils.chmod 0755, "#{process_log_directory}/run"
end end
end end
end end
private
def create_directory(location)
say "creating: #{location}"
FileUtils.mkdir_p(location)
end
def inline_variables(command)
variable_name_regex =
Hash[*command.scan(ENV_VARIABLE_REGEX).flatten]
end
end end

View File

@@ -4,23 +4,13 @@ require "foreman/export"
class Foreman::Export::Supervisord < Foreman::Export::Base class Foreman::Export::Supervisord < Foreman::Export::Base
def export def export
error("Must specify a location") unless location super
FileUtils.mkdir_p location
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
template_root = self.template
Dir["#{location}/#{app}*.conf"].each do |file| Dir["#{location}/#{app}*.conf"].each do |file|
say "cleaning up: #{file}" clean file
FileUtils.rm(file)
end end
app_template = export_template("supervisord", "app.conf.erb", template_root) write_template "supervisord/app.conf.erb", "#{app}.conf", binding
app_config = ERB.new(app_template, 0, '<').result(binding)
write_file "#{location}/#{app}.conf", app_config
end end
end end

View File

@@ -4,40 +4,22 @@ require "foreman/export"
class Foreman::Export::Upstart < Foreman::Export::Base class Foreman::Export::Upstart < Foreman::Export::Base
def export def export
error("Must specify a location") unless location super
FileUtils.mkdir_p location
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
template_root = self.template
Dir["#{location}/#{app}*.conf"].each do |file| Dir["#{location}/#{app}*.conf"].each do |file|
say "cleaning up: #{file}" clean file
FileUtils.rm(file)
end end
master_template = export_template("upstart", "master.conf.erb", template_root) write_template "upstart/master.conf.erb", "#{app}.conf", binding
master_config = ERB.new(master_template).result(binding)
write_file "#{location}/#{app}.conf", master_config
process_template = export_template("upstart", "process.conf.erb", template_root) engine.each_process do |name, process|
next if engine.formation[name] < 1
write_template "upstart/process_master.conf.erb", "#{app}-#{name}.conf", binding
engine.procfile.entries.each do |process| 1.upto(engine.formation[name]) do |num|
next if (conc = self.concurrency[process.name]) < 1 port = engine.port_for(process, num)
process_master_template = export_template("upstart", "process_master.conf.erb", template_root) write_template "upstart/process.conf.erb", "#{app}-#{name}-#{num}.conf", binding
process_master_config = ERB.new(process_master_template).result(binding)
write_file "#{location}/#{app}-#{process.name}.conf", process_master_config
1.upto(self.concurrency[process.name]) do |num|
port = engine.port_for(process, num, self.port)
process_config = ERB.new(process_template).result(binding)
write_file "#{location}/#{app}-#{process.name}-#{num}.conf", process_config
end end
end end
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}"
end end
end end

View File

@@ -3,94 +3,83 @@ require "rubygems"
class Foreman::Process class Foreman::Process
attr_reader :entry attr_reader :command
attr_reader :num attr_reader :env
attr_reader :pid
attr_reader :port
def initialize(entry, num, port) # Create a Process
@entry = entry #
@num = num # @param [String] command The command to run
@port = port # @param [Hash] options
#
# @option options [String] :cwd (./) Change to this working directory before executing the process
# @option options [Hash] :env ({}) Environment variables to set for this process
#
def initialize(command, options={})
@command = command
@options = options.dup
@options[:env] ||= {}
end end
def run(pipe, basedir, environment) # Run a +Process+
with_environment(environment.merge("PORT" => port.to_s)) do #
run_process basedir, entry.command, pipe # @param [Hash] options
#
# @option options :env ({}) Environment variables to set for this execution
# @option options :output ($stdout) The output stream
#
# @returns [Fixnum] pid The +pid+ of the process
#
def run(options={})
env = options[:env] ? @options[:env].merge(options[:env]) : @options[:env]
output = options[:output] || $stdout
if Foreman.windows?
Dir.chdir(cwd) do
Process.spawn env, command, :out => output, :err => output, :new_pgroup => true
end
elsif Foreman.jruby?
Dir.chdir(cwd) do
require "posix/spawn"
POSIX::Spawn.spawn env, command, :out => output, :err => output, :pgroup => 0
end
else
Dir.chdir(cwd) do
Process.spawn env, command, :out => output, :err => output, :pgroup => 0
end
end end
end end
def name # Send a signal to this +Process+
"%s.%s" % [ entry.name, num ] #
end # @param [String] signal The signal to send
#
def kill(signal) def kill(signal)
pid && Process.kill(signal, pid) pid && Process.kill(signal, -1 * pid)
rescue Errno::ESRCH rescue Errno::ESRCH
false false
end end
def detach # Test whether or not this +Process+ is still running
pid && Process.detach(pid) #
end # @returns [Boolean]
#
def alive? def alive?
kill(0) kill(0)
end end
# Test whether or not this +Process+ has terminated
#
# @returns [Boolean]
#
def dead? def dead?
!alive? !alive?
end end
private private
def fork_with_io(command, basedir) def cwd
reader, writer = IO.pipe @options[:cwd] || "."
command = replace_command_env(command)
pid = if Foreman.windows?
Dir.chdir(basedir) do
Process.spawn command, :out => writer, :err => writer
end
elsif Foreman.jruby?
require "posix/spawn"
POSIX::Spawn.spawn(Foreman.runner, "-d", basedir, command, {
:out => writer, :err => writer
})
else
fork do
writer.sync = true
$stdout.reopen writer
$stderr.reopen writer
reader.close
exec Foreman.runner, "-d", basedir, *command.shellsplit
end
end
[ reader, pid ]
end end
def run_process(basedir, command, pipe)
io, @pid = fork_with_io(command, basedir)
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)
original = ENV.to_hash
ENV.update environment
yield
ensure
ENV.replace original
end
end end

View File

@@ -1,56 +1,90 @@
require "foreman" require "foreman"
require "foreman/procfile_entry"
# A valid Procfile entry is captured by this regex. # Reads and writes Procfiles
# All other lines are ignored. #
# A valid Procfile entry is captured by this regex:
# #
# /^([A-Za-z0-9_]+):\s*(.+)$/ # /^([A-Za-z0-9_]+):\s*(.+)$/
# #
# $1 = name # All other lines are ignored.
# $2 = command
# #
class Foreman::Procfile class Foreman::Procfile
attr_reader :entries # Initialize a Procfile
#
# @param [String] filename (nil) An optional filename to read from
#
def initialize(filename=nil) def initialize(filename=nil)
@entries = [] @entries = []
load(filename) if filename load(filename) if filename
end end
# Yield each +Procfile+ entry in order
#
def entries(&blk)
@entries.each do |(name, command)|
yield name, command
end
end
# Retrieve a +Procfile+ command by name
#
# @param [String] name The name of the Procfile entry to retrieve
#
def [](name) def [](name)
entries.detect { |entry| entry.name == name } @entries.detect { |n,c| name == n }.last
end end
def process_names # Create a +Procfile+ entry
entries.map(&:name) #
# @param [String] name The name of the +Procfile+ entry to create
# @param [String] command The command of the +Procfile+ entry to create
#
def []=(name, command)
delete name
@entries << [name, command]
end end
# Remove a +Procfile+ entry
#
# @param [String] name The name of the +Procfile+ entry to remove
#
def delete(name)
@entries.reject! { |n,c| name == n }
end
# Load a Procfile from a file
#
# @param [String] filename The filename of the +Procfile+ to load
#
def load(filename) def load(filename)
entries.clear @entries.replace parse(filename)
parse_procfile(filename)
end end
def write(filename) # Save a Procfile to a file
File.open(filename, 'w') do |io| #
entries.each do |ent| # @param [String] filename Save the +Procfile+ to this file
io.puts(ent) #
end def save(filename)
File.open(filename, 'w') do |file|
file.puts self.to_s
end end
end end
def <<(entry) # Get the +Procfile+ as a +String+
entries << Foreman::ProcfileEntry.new(*entry) #
self def to_s
@entries.map do |name, command|
[ name, command ].join(": ")
end.join("\n")
end end
private
protected def parse(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*(.+)$/
self << [ $1, $2 ] [$1, $2]
end end
end.compact end.compact
end end

View File

@@ -1,26 +0,0 @@
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
def to_s
"#{name}: #{command}"
end
end

View File

@@ -1,37 +0,0 @@
require "foreman"
require "foreman/engine"
class Foreman::TmuxEngine < Foreman::Engine
attr_reader :procfile
attr_reader :session
def initialize(procfile, options={})
@procfile = Foreman::Procfile.new(procfile)
@options = options.dup
@session = Time.now.to_i
end
def start
assign_colors
concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency])
ENV['BUNDLE_GEMFILE'] = nil
%x{tmux new-session -d -s #{session}}
procfile.entries.each_with_index do |entry, index|
name = "#{entry.name}.#{concurrency[entry.name]}"
if index == 0
%x{tmux rename-window -t #{session}:#{index} #{name}}
else
%x{tmux new-window -t #{session}:#{index} -n #{name}}
end
%x{tmux pipe-pane -o -t #{session}:#{index} "gawk '{ printf \\"%%s\\", \\"#{$stdout.color(colors[entry.name])}\\"; print strftime(\\"%%H:%%M:%%S\\"), \\"#{pad_process_name(name)} | #{$stdout.color(:reset)}\\", \\$0; fflush(); }' >> /tmp/foreman.#{session}.log"}
%x{tmux send-keys -t #{session}:#{index} "#{entry.command}" C-m}
end
last_index = procfile.entries.length
%x{tmux new-window -t #{session}:#{last_index} -n all}
%x{tmux send-keys -t #{session}:#{last_index} "tail -f /tmp/foreman.#{session}.log" C-m}
Kernel.exec("tmux attach-session -t #{session}")
end
end

View File

@@ -1,18 +0,0 @@
require "foreman"
class Foreman::Utils
def self.parse_concurrency(concurrency)
begin
pairs = concurrency.to_s.gsub(/\s/, "").split(",")
default = concurrency.nil? ? 1 : 0
pairs.inject(Hash.new(default)) do |hash, pair|
process, amount = pair.split("=")
hash.update(process => amount.to_i)
end
end
end
end

View File

@@ -3,12 +3,23 @@ require "foreman/cli"
describe "Foreman::CLI", :fakefs do describe "Foreman::CLI", :fakefs do
subject { Foreman::CLI.new } subject { Foreman::CLI.new }
let(:engine) { subject.send(:engine) }
let(:entries) { engine.procfile.entries.inject({}) { |h,e| h.update(e.name => e) } } describe ".foreman" do
before { File.open(".foreman", "w") { |f| f.puts "formation: alpha=2" } }
it "provides default options" do
subject.send(:options)["formation"].should == "alpha=2"
end
it "is overridden by options at the cli" do
subject = Foreman::CLI.new([], :formation => "alpha=3")
subject.send(:options)["formation"].should == "alpha=3"
end
end
describe "start" do describe "start" do
describe "with a non-existent Procfile" do describe "when a Procfile doesnt exist", :fakefs do
it "prints an error" do it "displays an error" do
mock_error(subject, "Procfile does not exist.") do mock_error(subject, "Procfile does not exist.") do
dont_allow.instance_of(Foreman::Engine).start dont_allow.instance_of(Foreman::Engine).start
subject.start subject.start
@@ -16,175 +27,50 @@ describe "Foreman::CLI", :fakefs do
end end
end end
describe "with a Procfile" do describe "with a valid Procfile" do
before(:each) { write_procfile } it "can run a single command" do
without_fakefs do
it "runs successfully" do output = foreman("start env -f #{resource_path("Procfile")}")
dont_allow(subject).error output.should =~ /env.1/
mock.instance_of(Foreman::Engine).start output.should_not =~ /test.1/
subject.start
end
it "can run a single process" do
dont_allow(subject).error
stub(engine).watch_for_output
stub(engine).watch_for_termination
mock(entries["alpha"]).spawn(1, is_a(IO), engine.directory, {}, 5000) { [] }
mock(entries["bravo"]).spawn(0, is_a(IO), engine.directory, {}, 5100) { [] }
subject.start("alpha")
end end
end end
describe "with an alternate root" do it "can run all commands" do
it "reads the Procfile from that root" do without_fakefs do
write_procfile "/some/app/Procfile" output = foreman("start -f #{resource_path("Procfile")} -e #{resource_path(".env")}")
mock(Foreman::Procfile).new("/some/app/Procfile") output.should =~ /echo.1 \| echoing/
mock.instance_of(Foreman::Engine).start output.should =~ /env.1 \| bar/
foreman %{ start -d /some/app } output.should =~ /test.1 \| testing/
end
end
end
describe "export" do
describe "options" do
it "uses .foreman" do
write_procfile
File.open(".foreman", "w") { |f| f.puts "concurrency: alpha=2" }
mock_export = mock(Foreman::Export::Upstart)
mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "concurrency" => "alpha=2" }) { mock_export }
mock_export.export
foreman %{ export upstart /upstart }
end
it "respects --env" do
write_procfile
write_env("envfile")
mock_export = mock(Foreman::Export::Upstart)
mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "env" => "envfile" }) { mock_export }
mock_export.export
foreman %{ export upstart /upstart --env envfile }
end
end
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
subject.export("testapp")
end
end
end
describe "with a Procfile" do
before(:each) { write_procfile }
describe "with a formatter with a generic error" do
before do
mock(Foreman::Export).formatter("errorful") { Class.new(Foreman::Export::Base) do
def export
raise Foreman::Export::Exception.new("foo")
end
end }
end
it "prints an error" do
mock_error(subject, "foo") do
subject.export("errorful")
end
end
end
describe "with a valid config" do
before(:each) { write_foreman_config("testapp") }
it "runs successfully" do
dont_allow(subject).error
mock_export = mock(Foreman::Export::Upstart)
mock(Foreman::Export::Upstart).new("/tmp/foo", is_a(Foreman::Engine), {}) { mock_export }
mock_export.export
subject.export("upstart", "/tmp/foo")
end end
end end
end end
end end
describe "check" do describe "check" do
describe "with a valid Procfile" do it "with a valid Procfile displays the jobs" do
before { write_procfile } write_procfile
foreman("check").should == "valid procfile detected (alpha, bravo)\n"
it "displays the jobs" do
mock(subject).puts("valid procfile detected (alpha, bravo)")
subject.check
end
end end
describe "with a blank Procfile" do it "with a blank Procfile displays an error" do
before do FileUtils.touch "Procfile"
FileUtils.touch("Procfile") foreman("check").should == "ERROR: no processes defined\n"
end end
it "displays an error" do it "without a Procfile displays an error" do
mock_error(subject, "no processes defined") do FileUtils.rm_f "Procfile"
subject.check foreman("check").should == "ERROR: Procfile does not exist.\n"
end
end
end
describe "without a Procfile" do
it "displays an error" do
mock_error(subject, "Procfile does not exist.") do
subject.check
end
end
end end
end end
describe "run" do describe "run" do
describe "with a valid Procfile" do it "can run a command" do
before { write_procfile } forked_foreman("run echo 1").should == "1\n"
describe "and a command" do
let(:command) { ["ls", "-l", "foo bar"] }
before(:each) do
stub(subject).exec
end end
it "should load the environment file" do it "includes the environment" do
write_env forked_foreman("run #{resource_path("bin/env FOO")} -e #{resource_path(".env")}").should == "bar\n"
preserving_env do
subject.run *command
ENV["FOO"].should == "bar"
end
ENV["FOO"].should be_nil
end
it "should exec the argument list as a shell command" do
mock(subject).exec(command.shelljoin)
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

View File

@@ -1,31 +0,0 @@
require "spec_helper"
require "foreman/color"
describe Foreman::Color do
let(:io) { Object.new }
it "should extend an object with colorization" do
Foreman::Color.enable(io)
io.should respond_to(:color)
end
it "should not colorize if the object does not respond to isatty" do
mock(io).respond_to?(:isatty) { false }
Foreman::Color.enable(io)
io.color(:white).should == ""
end
it "should not colorize if the object is not a tty" do
mock(io).isatty { false }
Foreman::Color.enable(io)
io.color(:white).should == ""
end
it "should colorize if the object is a tty" do
mock(io).isatty { true }
Foreman::Color.enable(io)
io.color(:white).should == "\e[37m"
end
end

View File

@@ -1,14 +1,25 @@
require "spec_helper" require "spec_helper"
require "foreman/engine" require "foreman/engine"
describe "Foreman::Engine", :fakefs do class Foreman::Engine::Tester < Foreman::Engine
subject { Foreman::Engine.new("Procfile", {}) } attr_reader :buffer
before do def startup
any_instance_of(Foreman::Engine) do |engine| @buffer = ""
stub(engine).proctitle
stub(engine).termtitle
end end
def output(name, data)
@buffer += "#{name}: #{data}"
end
def shutdown
end
end
describe "Foreman::Engine", :fakefs do
subject do
write_procfile "Procfile"
Foreman::Engine::Tester.new.load_procfile("Procfile")
end end
describe "initialize" do describe "initialize" do
@@ -16,65 +27,53 @@ describe "Foreman::Engine", :fakefs do
before { write_procfile } before { write_procfile }
it "reads the processes" do it "reads the processes" do
subject.procfile["alpha"].command.should == "./alpha" subject.process("alpha").command.should == "./alpha"
subject.procfile["bravo"].command.should == "./bravo" subject.process("bravo").command.should == "./bravo"
end end
end end
end end
describe "start" do describe "start" do
it "forks the processes" do it "forks the processes" do
write_procfile mock(subject.process("alpha")).run(anything)
mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./alpha", is_a(IO)) mock(subject.process("bravo")).run(anything)
mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./bravo", is_a(IO))
mock(subject).watch_for_output mock(subject).watch_for_output
mock(subject).watch_for_termination mock(subject).watch_for_termination
subject.start subject.start
end end
it "handles concurrency" do it "handles concurrency" do
write_procfile subject.options[:formation] = "alpha=2"
engine = Foreman::Engine.new("Procfile",:concurrency => "alpha=2") mock(subject.process("alpha")).run(anything).twice
mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./alpha", is_a(IO)).twice mock(subject.process("bravo")).run(anything).never
mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./bravo", is_a(IO)).never mock(subject).watch_for_output
mock(engine).watch_for_output mock(subject).watch_for_termination
mock(engine).watch_for_termination subject.start
engine.start
end end
end end
describe "directories" do describe "directories" do
it "has the directory default relative to the Procfile" do it "has the directory default relative to the Procfile" do
write_procfile "/some/app/Procfile" write_procfile "/some/app/Procfile"
engine = Foreman::Engine.new("/some/app/Procfile") engine = Foreman::Engine.new.load_procfile("/some/app/Procfile")
engine.directory.should == "/some/app" engine.root.should == "/some/app"
end end
end end
describe "environment" do describe "environment" do
before(:each) do it "should read env files" do
write_procfile
stub(Process).fork
any_instance_of(Foreman::Engine) do |engine|
stub(engine).info
stub(engine).spawn_processes
stub(engine).watch_for_termination
end
end
it "should read if specified" 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") subject.load_env("/tmp/env")
engine.environment.should == {"FOO"=>"baz"} subject.env["FOO"].should == "baz"
engine.start
end end
it "should read more than one if specified" do it "should read more than one if specified" do
File.open("/tmp/env1", "w") { |f| f.puts("FOO=bar") } File.open("/tmp/env1", "w") { |f| f.puts("FOO=bar") }
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") subject.load_env "/tmp/env1"
engine.environment.should == { "FOO"=>"bar", "BAZ"=>"qux" } subject.load_env "/tmp/env2"
engine.start subject.env["FOO"].should == "bar"
subject.env["BAZ"].should == "qux"
end end
it "should handle quoted values" do it "should handle quoted values" do
@@ -84,55 +83,22 @@ describe "Foreman::Engine", :fakefs do
f.puts "FRED='barney'" f.puts "FRED='barney'"
f.puts 'OTHER="escaped\"quote"' f.puts 'OTHER="escaped\"quote"'
end end
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env") subject.load_env "/tmp/env"
engine.environment.should == { "FOO" => "bar", "BAZ" => "qux", "FRED" => "barney", "OTHER" => 'escaped"quote' } subject.env["FOO"].should == "bar"
subject.env["BAZ"].should == "qux"
subject.env["FRED"].should == "barney"
subject.env["OTHER"].should == 'escaped"quote'
end end
it "should fail if specified and doesnt exist" do it "should fail if specified and doesnt exist" do
mock.instance_of(Foreman::Engine).error("No such file: /tmp/env") lambda { subject.load_env "/tmp/env" }.should raise_error(Errno::ENOENT)
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env")
end
it "should read .env if none specified" do
File.open(".env", "w") { |f| f.puts("FOO=qoo") }
engine = Foreman::Engine.new("Procfile")
engine.environment.should == {"FOO"=>"qoo"}
engine.start
end end
it "should set port from .env if specified" do it "should set port from .env if specified" do
File.open(".env", "w") { |f| f.puts("PORT=8017") } File.open("/tmp/env", "w") { |f| f.puts("PORT=9000") }
engine = Foreman::Engine.new("Procfile") subject.load_env "/tmp/env"
engine.send(:base_port).should == "8017" subject.send(:base_port).should == 9000
engine.start
end
it "should be loaded relative to the Procfile" do
FileUtils.mkdir_p "/some/app"
File.open("/some/app/.env", "w") { |f| f.puts("FOO=qoo") }
write_procfile "/some/app/Procfile"
engine = Foreman::Engine.new("/some/app/Procfile")
engine.environment.should == {"FOO"=>"qoo"}
engine.start
end end
end end
describe "utf8" do
before(:each) do
File.open("Procfile", "w") do |file|
file.puts "utf8: #{resource_path("bin/utf8")}"
end
end
it "should spawn" do
stub(subject).watch_for_output
stub(subject).watch_for_termination
subject.start
Process.waitall
mock(subject).info(/started with pid \d+/, "utf8.1", anything)
mock(subject).info("\xff\x03\n", "utf8.1", anything)
subject.send(:poll_readers)
subject.send(:poll_readers)
end
end
end end

View File

@@ -1,10 +1,11 @@
require "spec_helper" require "spec_helper"
require "foreman/export/base" require "foreman/engine"
require "foreman/export"
describe "Foreman::Export::Base" do describe "Foreman::Export::Base", :fakefs do
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") }
let(:location) { "/tmp/init" } let(:location) { "/tmp/init" }
let(:engine) { Foreman::Engine.new(procfile) } let(:engine) { Foreman::Engine.new().load_procfile(procfile) }
let(:subject) { Foreman::Export::Base.new(location, engine) } let(:subject) { Foreman::Export::Base.new(location, engine) }
it "has a say method for displaying info" do it "has a say method for displaying info" do
@@ -12,10 +13,6 @@ describe "Foreman::Export::Base" do
subject.send(:say, "foo") subject.send(:say, "foo")
end end
it "export needs to be overridden" do
lambda { subject.export }.should raise_error("export method must be overridden")
end
it "raises errors as a Foreman::Export::Exception" do it "raises errors as a Foreman::Export::Exception" do
lambda { subject.send(:error, "foo") }.should raise_error(Foreman::Export::Exception, "foo") lambda { subject.send(:error, "foo") }.should raise_error(Foreman::Export::Exception, "foo")
end end

View File

@@ -5,7 +5,8 @@ require "tmpdir"
describe Foreman::Export::Bluepill, :fakefs do describe Foreman::Export::Bluepill, :fakefs do
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") }
let(:engine) { Foreman::Engine.new(procfile) } let(:formation) { nil }
let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) }
let(:options) { Hash.new } let(:options) { Hash.new }
let(:bluepill) { Foreman::Export::Bluepill.new("/tmp/init", engine, options) } let(:bluepill) { Foreman::Export::Bluepill.new("/tmp/init", engine, options) }
@@ -24,8 +25,8 @@ describe Foreman::Export::Bluepill, :fakefs do
bluepill.export bluepill.export
end end
context "with concurrency" do context "with a process formation" do
let(:options) { Hash[:concurrency => "alpha=2"] } let(:formation) { "alpha=2" }
it "exports to the filesystem with concurrency" do it "exports to the filesystem with concurrency" do
bluepill.export bluepill.export

View File

@@ -4,10 +4,10 @@ require "foreman/export/inittab"
require "tmpdir" require "tmpdir"
describe Foreman::Export::Inittab, :fakefs do describe Foreman::Export::Inittab, :fakefs do
let(:location) { "/tmp/inittab" }
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") }
let(:location) { "/tmp/inittab" } let(:location) { "/tmp/inittab" }
let(:engine) { Foreman::Engine.new(procfile) } let(:formation) { nil }
let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) }
let(:options) { Hash.new } let(:options) { Hash.new }
let(:inittab) { Foreman::Export::Inittab.new(location, engine, options) } let(:inittab) { Foreman::Export::Inittab.new(location, engine, options) }
@@ -29,7 +29,7 @@ describe Foreman::Export::Inittab, :fakefs do
end end
context "with concurrency" do context "with concurrency" do
let(:options) { Hash[:concurrency => "alpha=2"] } let(:formation) { "alpha=2" }
it "exports to the filesystem with concurrency" do it "exports to the filesystem with concurrency" do
inittab.export inittab.export

View File

@@ -5,8 +5,8 @@ require "tmpdir"
describe Foreman::Export::Launchd, :fakefs do describe Foreman::Export::Launchd, :fakefs do
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") }
let(:engine) { Foreman::Engine.new(procfile) }
let(:options) { Hash.new } let(:options) { Hash.new }
let(:engine) { Foreman::Engine.new().load_procfile(procfile) }
let(:launchd) { Foreman::Export::Launchd.new("/tmp/init", engine, options) } let(:launchd) { Foreman::Export::Launchd.new("/tmp/init", engine, options) }
before(:each) { load_export_templates_into_fakefs("launchd") } before(:each) { load_export_templates_into_fakefs("launchd") }
@@ -14,11 +14,8 @@ describe Foreman::Export::Launchd, :fakefs do
it "exports to the filesystem" do it "exports to the filesystem" do
launchd.export launchd.export
File.read("/tmp/init/app-alpha-1.plist").should == example_export_file("launchd/launchd-a.default")
normalize_space(File.read("/tmp/init/app-alpha-1.plist")).should == normalize_space(example_export_file("launchd/launchd-a.default")) File.read("/tmp/init/app-bravo-1.plist").should == example_export_file("launchd/launchd-b.default")
normalize_space(File.read("/tmp/init/app-bravo-1.plist")).should == normalize_space(example_export_file("launchd/launchd-b.default"))
end end
end end

View File

@@ -5,33 +5,28 @@ require "tmpdir"
describe Foreman::Export::Runit, :fakefs do describe Foreman::Export::Runit, :fakefs do
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile", 'bar=baz') } let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile", 'bar=baz') }
let(:engine) { Foreman::Engine.new(procfile) } let(:engine) { Foreman::Engine.new(:formation => "alpha=2,bravo=1").load_procfile(procfile) }
let(:runit) { Foreman::Export::Runit.new('/tmp/init', engine, :concurrency => 'alpha=2,bravo=1') } let(:options) { Hash.new }
let(:runit) { Foreman::Export::Runit.new('/tmp/init', engine, options) }
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 } before(:each) { stub(FakeFS::FileUtils).chmod }
it "exports to the filesystem" do it "exports to the filesystem" do
FileUtils.mkdir_p('/tmp/init') engine.env["BAR"] = "baz"
runit.export runit.export
File.read("/tmp/init/app-alpha-1/run").should == example_export_file('runit/app-alpha-1-run') File.read("/tmp/init/app-alpha-1/run").should == example_export_file('runit/app-alpha-1/run')
File.read("/tmp/init/app-alpha-1/log/run").should == File.read("/tmp/init/app-alpha-1/log/run").should == example_export_file('runit/app-alpha-1/log/run')
example_export_file('runit/app-alpha-1-log-run')
File.read("/tmp/init/app-alpha-1/env/PORT").should == "5000\n" File.read("/tmp/init/app-alpha-1/env/PORT").should == "5000\n"
File.read("/tmp/init/app-alpha-1/env/BAR").should == "baz\n" File.read("/tmp/init/app-alpha-1/env/BAR").should == "baz\n"
File.read("/tmp/init/app-alpha-2/run").should == example_export_file('runit/app-alpha-2/run')
File.read("/tmp/init/app-alpha-2/run").should == example_export_file('runit/app-alpha-2-run') File.read("/tmp/init/app-alpha-2/log/run").should == example_export_file('runit/app-alpha-2/log/run')
File.read("/tmp/init/app-alpha-2/log/run").should ==
example_export_file('runit/app-alpha-2-log-run')
File.read("/tmp/init/app-alpha-2/env/PORT").should == "5001\n" File.read("/tmp/init/app-alpha-2/env/PORT").should == "5001\n"
File.read("/tmp/init/app-alpha-2/env/BAR").should == "baz\n" File.read("/tmp/init/app-alpha-2/env/BAR").should == "baz\n"
File.read("/tmp/init/app-bravo-1/run").should == example_export_file('runit/app-bravo-1/run')
File.read("/tmp/init/app-bravo-1/run").should == example_export_file('runit/app-bravo-1-run') File.read("/tmp/init/app-bravo-1/log/run").should == example_export_file('runit/app-bravo-1/log/run')
File.read("/tmp/init/app-bravo-1/log/run").should ==
example_export_file('runit/app-bravo-1-log-run')
File.read("/tmp/init/app-bravo-1/env/PORT").should == "5100\n" File.read("/tmp/init/app-bravo-1/env/PORT").should == "5100\n"
end end

View File

@@ -5,7 +5,8 @@ require "tmpdir"
describe Foreman::Export::Supervisord, :fakefs do describe Foreman::Export::Supervisord, :fakefs do
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") }
let(:engine) { Foreman::Engine.new(procfile) } let(:formation) { nil }
let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) }
let(:options) { Hash.new } let(:options) { Hash.new }
let(:supervisord) { Foreman::Export::Supervisord.new("/tmp/init", engine, options) } let(:supervisord) { Foreman::Export::Supervisord.new("/tmp/init", engine, options) }
@@ -14,8 +15,7 @@ describe Foreman::Export::Supervisord, :fakefs do
it "exports to the filesystem" do it "exports to the filesystem" do
supervisord.export supervisord.export
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app-alpha-1.conf")
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app.conf")
end end
it "cleans up if exporting into an existing dir" do it "cleans up if exporting into an existing dir" do
@@ -25,7 +25,7 @@ describe Foreman::Export::Supervisord, :fakefs do
end end
context "with concurrency" do context "with concurrency" do
let(:options) { Hash[:concurrency => "alpha=2"] } let(:formation) { "alpha=2" }
it "exports to the filesystem with concurrency" do it "exports to the filesystem with concurrency" do
supervisord.export supervisord.export
@@ -33,53 +33,4 @@ describe Foreman::Export::Supervisord, :fakefs do
end end
end end
context "with alternate templates" do
let(:template_root) { "/tmp/alternate" }
let(:supervisord) { Foreman::Export::Supervisord.new("/tmp/init", engine, :template => template_root) }
before do
FileUtils.mkdir_p template_root
File.open("#{template_root}/app.conf.erb", "w") { |f| f.puts "alternate_template" }
end
it "can export with alternate template files" do
supervisord.export
File.read("/tmp/init/app.conf").should == "alternate_template\n"
end
end
context "with alternate templates from home dir" do
let(:default_template_root) {File.expand_path("#{ENV['HOME']}/.foreman/templates")}
before do
ENV['_FOREMAN_SPEC_HOME'] = ENV['HOME']
ENV['HOME'] = "/home/appuser"
FileUtils.mkdir_p default_template_root
File.open("#{default_template_root}/app.conf.erb", "w") { |f| f.puts "default_alternate_template" }
end
after do
ENV['HOME'] = ENV.delete('_FOREMAN_SPEC_HOME')
end
it "can export with alternate template files" do
supervisord.export
File.read("/tmp/init/app.conf").should == "default_alternate_template\n"
end
end
context "environment export" do
it "correctly translates environment when exporting" do
File.open("/tmp/supervisord_env", "w") { |f| f.puts("QUEUE=fastqueue,slowqueue\nVERBOSE=1") }
engine = Foreman::Engine.new(procfile,:env => "/tmp/supervisord_env")
supervisor = Foreman::Export::Supervisord.new("/tmp/init", engine, options)
stub(supervisor).say
supervisor.export
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app-env-with-comma.conf")
end
end
end end

View File

@@ -4,8 +4,9 @@ require "foreman/export/upstart"
require "tmpdir" require "tmpdir"
describe Foreman::Export::Upstart, :fakefs do describe Foreman::Export::Upstart, :fakefs do
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } let(:procfile) { write_procfile("/tmp/app/Procfile") }
let(:engine) { Foreman::Engine.new(procfile) } let(:formation) { nil }
let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) }
let(:options) { Hash.new } let(:options) { Hash.new }
let(:upstart) { Foreman::Export::Upstart.new("/tmp/init", engine, options) } let(:upstart) { Foreman::Export::Upstart.new("/tmp/init", engine, options) }
@@ -34,13 +35,14 @@ describe Foreman::Export::Upstart, :fakefs do
end end
it "quotes and escapes environment variables" do it "quotes and escapes environment variables" do
engine.environment['KEY'] = 'd"\|d' engine.env['KEY'] = 'd"\|d'
upstart.export upstart.export
File.read("/tmp/init/app-alpha-1.conf").should include('KEY="d\"\\\\|d"') "foobarfoo".should include "bar"
File.read("/tmp/init/app-alpha-1.conf").should =~ /KEY="d\\"\\\\\\\|d/
end end
context "with concurrency" do context "with a formation" do
let(:options) { Hash[:concurrency => "alpha=2"] } let(:formation) { "alpha=2" }
it "exports to the filesystem with concurrency" do it "exports to the filesystem with concurrency" do
upstart.export upstart.export
@@ -54,38 +56,31 @@ describe Foreman::Export::Upstart, :fakefs do
end end
context "with alternate templates" do context "with alternate templates" do
let(:template_root) { "/tmp/alternate" } let(:template) { "/tmp/alternate" }
let(:upstart) { Foreman::Export::Upstart.new("/tmp/init", engine, :template => template_root) } let(:options) { { :app => "app", :template => template } }
before do before do
FileUtils.mkdir_p template_root FileUtils.mkdir_p template
File.open("#{template_root}/master.conf.erb", "w") { |f| f.puts "alternate_template" } File.open("#{template}/master.conf.erb", "w") { |f| f.puts "alternate_template" }
end end
it "can export with alternate template files" do it "can export with alternate template files" do
upstart.export upstart.export
File.read("/tmp/init/app.conf").should == "alternate_template\n" File.read("/tmp/init/app.conf").should == "alternate_template\n"
end end
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("#{ENV['HOME']}/.foreman/templates")}
before do before do
ENV['_FOREMAN_SPEC_HOME'] = ENV['HOME'] FileUtils.mkdir_p File.expand_path("~/.foreman/templates/upstart")
ENV['HOME'] = "/home/appuser" File.open(File.expand_path("~/.foreman/templates/upstart/master.conf.erb"), "w") do |file|
FileUtils.mkdir_p default_template_root file.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 end
it "can export with alternate template files" do it "can export with alternate template files" do
upstart.export upstart.export
File.read("/tmp/init/app.conf").should == "default_alternate_template\n" File.read("/tmp/init/app.conf").should == "default_alternate_template\n"
end end
end end

View File

@@ -5,141 +5,44 @@ require 'timeout'
require 'tmpdir' require 'tmpdir'
describe Foreman::Process do describe Foreman::Process do
subject { described_class.new entry, number, port }
let(:number) { 1 } def run(process, options={})
let(:port) { 777 } rd, wr = IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
let(:command) { "script" } process.run(options.merge(:output => wr))
let(:name) { "foobar" } rd.gets
let(:entry) { OpenStruct.new :name => name, :command => command }
its(:entry) { entry }
its(:num) { number }
its(:port) { port }
its(:name) { "#{name}.#{port}" }
its(:pid) { nil }
describe '#run' do
let(:pipe) { :pipe }
let(:basedir) { Dir.mktmpdir }
let(:env) {{ 'foo' => 'bar' }}
let(:init_delta) { 0.1 }
after { FileUtils.remove_entry_secure basedir }
def run(cmd=command)
entry.command = cmd
subject.run pipe, basedir, env
subject.detach && sleep(init_delta)
end end
def run_file(executable, code) describe "#run" do
file = File.open("#{basedir}/script", 'w') {|it| it << code }
run "#{executable} #{file.path}" it "runs the process" do
sleep 1 process = Foreman::Process.new(resource_path("bin/test"))
run(process).should == "testing\n"
end end
context 'options' do it "can set environment" do
it 'should set PORT for environment' do process = Foreman::Process.new(resource_path("bin/env FOO"), :env => { "FOO" => "bar" })
mock(subject).run_process(basedir, command, pipe) do run(process).should == "bar\n"
ENV['PORT'].should == port.to_s
end
run
end end
it 'should set custom variables for environment' do it "can set per-run environment" do
mock(subject).run_process(basedir, command, pipe) do process = Foreman::Process.new(resource_path("bin/env FOO"))
ENV['foo'].should == 'bar' run(process, :env => { "FOO" => "bar "}).should == "bar\n"
end
run
end end
it 'should restore environment afterwards' do it "can handle env vars in the command" do
mock(subject).run_process(basedir, command, pipe) process = Foreman::Process.new(resource_path("bin/echo $FOO"), :env => { "FOO" => "bar" })
run run(process).should == "bar\n"
ENV.should_not include('PORT', 'foo') end
it "can handle per-run env vars in the command" do
process = Foreman::Process.new(resource_path("bin/echo $FOO"))
run(process, :env => { "FOO" => "bar" }).should == "bar\n"
end
it "should output utf8 properly" do
process = Foreman::Process.new(resource_path("bin/utf8"))
run(process).should == "\xFF\x03\n"
end end
end end
context 'process' do
around do |spec|
IO.pipe do |reader, writer|
@reader, @writer = reader, writer
spec.run
end
end
let(:pipe) { @writer }
let(:output) { @reader.read_nonblock 1024 }
it 'should not block' do
expect {
Timeout.timeout(2*init_delta) { run 'sleep 2' }
}.should_not raise_exception
end
it 'should be alive' do
run 'sleep 1'
subject.should be_alive
end
it 'should be dead' do
run 'exit'
subject.should be_dead
end
it 'should be killable' do
run 'sleep 1'
subject.kill 'TERM'
subject.should be_dead
end
it 'should send different signals' do
run_file 'ruby', <<-CODE
trap "TERM", "IGNORE"
loop { sleep 1 }
CODE
subject.should be_alive
subject.kill 'TERM'
subject.should be_alive
subject.kill 'KILL'
subject.should be_dead
end
it 'should redirect stdout' do
run 'echo hey'
output.should include('hey')
end
it 'should redirect stderr' do
run 'echo hey >2'
output.should include('hey')
end
it 'should handle variables' do
run 'echo $PORT'
output.should include('777')
end
it 'should handle multi-word arguments (old test)' do
# TODO: This test used to be marked pending; it now passes,
# but is very slow. The next test is a fast replacement.
run %{ sh -c "trap '' TERM; sleep 10" }
subject.should be_alive
end
it 'should handle multi-word arguments' do
# We have to be a little clever here since Foreman will always
# print a status message that includes the command.
run %{ sh -c 'echo abcdef | tr a-c x | tr d-f y' }
output.should include('xxxyyy')
end
it 'should not clobber "$x"-subexpressions' do
pending 'this conflicts with the variable interpolation hack'
run %{ sh -c 'echo \$abcdef | tr \$ %' }
output.should include('%abcdef')
end
end
end
end end

View File

@@ -1,13 +0,0 @@
require 'spec_helper'
require 'foreman/procfile_entry'
require 'pathname'
require 'tmpdir'
describe Foreman::ProcfileEntry do
subject { described_class.new('alpha', './alpha') }
it "stringifies as a Procfile line" do
subject.to_s.should == 'alpha: ./alpha'
end
end

View File

@@ -3,29 +3,39 @@ require 'foreman/procfile'
require 'pathname' require 'pathname'
require 'tmpdir' require 'tmpdir'
describe Foreman::Procfile do describe Foreman::Procfile, :fakefs do
subject { described_class.new } subject { Foreman::Procfile.new }
let(:testdir) { Pathname(Dir.tmpdir) } it "can load from a file" do
let(:procfile) { testdir + 'Procfile' } write_procfile
subject.load "Procfile"
subject["alpha"].should == "./alpha"
subject["bravo"].should == "./bravo"
end
it "loads a passed-in Procfile" do
write_procfile
procfile = Foreman::Procfile.new("Procfile")
procfile["alpha"].should == "./alpha"
procfile["bravo"].should == "./bravo"
end
it "can have a process appended to it" do it "can have a process appended to it" do
subject << ['alpha', './alpha'] subject["charlie"] = "./charlie"
subject['alpha'].should be_a(Foreman::ProcfileEntry) subject["charlie"].should == "./charlie"
end end
it "can write itself out to a file" do it "can write to a string" do
subject << ['alpha', './alpha'] subject["foo"] = "./foo"
subject.write(procfile) subject["bar"] = "./bar"
procfile.read.should == "alpha: ./alpha\n" subject.to_s.should == "foo: ./foo\nbar: ./bar"
end end
it "can re-read entries from a file" do it "can write to a file" do
procfile.open('w') { |io| io.puts "gamma: ./radiation", "theta: ./rate" } subject["foo"] = "./foo"
subject << ['alpha', './alpha'] subject["bar"] = "./bar"
subject.load(procfile) subject.save "/tmp/proc"
subject.process_names.should have(2).members File.read("/tmp/proc").should == "foo: ./foo\nbar: ./bar\n"
subject.process_names.should include('gamma', 'theta')
end end
end end

View File

@@ -1,75 +0,0 @@
require "spec_helper"
require "foreman/tmux_engine"
describe "Foreman::TmuxEngine", :fakefs do
subject { Foreman::TmuxEngine.new("Procfile", {}) }
let(:session) { Time.now.to_i }
before do
any_instance_of(Foreman::TmuxEngine) do |engine|
stub(engine).proctitle
stub(engine).termtitle
end
Timecop.freeze(Time.now)
end
after do
FileUtils.rm_f("foreman.#{session}.log")
Timecop.return
end
describe "initialize" do
describe "without an existing Procfile" do
it "raises an error" do
lambda { subject }.should raise_error
end
end
describe "with a Procfile" do
before { write_procfile }
it "reads the processes" do
subject.procfile["alpha"].command.should == "./alpha"
subject.procfile["bravo"].command.should == "./bravo"
end
end
end
if system("which tmux")
describe "start" do
before do
write_procfile
@pid = fork do
exec("tmux start-server")
end
end
after do
Process.waitpid(@pid)
%x{tmux kill-session -t #{session} &> /dev/null}
end
it "creates a tmux session and attaches" do
%x{tmux has-session -t #{session} &> /dev/null}
$?.exitstatus.should == 1
mock(Kernel).exec("tmux attach-session -t #{session}")
subject.start
%x{tmux has-session -t #{session}}
$?.exitstatus.should == 0
%x{tmux list-windows -t #{session}}.split("\n").map { |window|
if window =~ /[0-9]+:\s(.+?)\s/
$1
end
}.should == ["alpha.1", "bravo.1", "all"]
sleep 1
%x{tmux kill-session -t #{session}}
output = %x[cat /tmp/foreman.#{session}.log]
output.should =~ /alpha\.1.+\.\/alpha: No such file or directory/
output.should =~ /bravo\.1.+\.\/bravo: No such file or directory/
end
end
end
end

1
spec/resources/.env Normal file
View File

@@ -0,0 +1 @@
FOO=bar

4
spec/resources/Procfile Normal file
View File

@@ -0,0 +1,4 @@
echo: bin/echo echoing
env: bin/env FOO
test: bin/test
utf8: bin/utf8

2
spec/resources/bin/echo Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
echo $*

2
spec/resources/bin/env Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
echo ${!1}

2
spec/resources/bin/test Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
echo "testing"

View File

@@ -11,14 +11,14 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.working_dir = "/tmp/app" process.working_dir = "/tmp/app"
process.daemonize = true process.daemonize = true
process.environment = {"PORT" => "5000"} process.environment = {"PORT"=>"5000"}
process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
process.stop_grace_time = 45.seconds process.stop_grace_time = 45.seconds
process.stdout = process.stderr = "/var/log/app/app-alpha-1.log" process.stdout = process.stderr = "/var/log/app/app-alpha-1.log"
process.monitor_children do |children| process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}" children.stop_command "kill {{PID}}"
end end
process.group = "app-alpha" process.group = "app-alpha"
@@ -30,14 +30,14 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.working_dir = "/tmp/app" process.working_dir = "/tmp/app"
process.daemonize = true process.daemonize = true
process.environment = {"PORT" => "5001"} process.environment = {"PORT"=>"5001"}
process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
process.stop_grace_time = 45.seconds process.stop_grace_time = 45.seconds
process.stdout = process.stderr = "/var/log/app/app-alpha-2.log" process.stdout = process.stderr = "/var/log/app/app-alpha-2.log"
process.monitor_children do |children| process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}" children.stop_command "kill {{PID}}"
end end
process.group = "app-alpha" process.group = "app-alpha"

View File

@@ -11,14 +11,14 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.working_dir = "/tmp/app" process.working_dir = "/tmp/app"
process.daemonize = true process.daemonize = true
process.environment = {"PORT" => "5000"} process.environment = {"PORT"=>"5000"}
process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
process.stop_grace_time = 45.seconds process.stop_grace_time = 45.seconds
process.stdout = process.stderr = "/var/log/app/app-alpha-1.log" process.stdout = process.stderr = "/var/log/app/app-alpha-1.log"
process.monitor_children do |children| process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}" children.stop_command "kill {{PID}}"
end end
process.group = "app-alpha" process.group = "app-alpha"
@@ -29,14 +29,14 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
process.working_dir = "/tmp/app" process.working_dir = "/tmp/app"
process.daemonize = true process.daemonize = true
process.environment = {"PORT" => "5100"} process.environment = {"PORT"=>"5100"}
process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
process.stop_grace_time = 45.seconds process.stop_grace_time = 45.seconds
process.stdout = process.stderr = "/var/log/app/app-bravo-1.log" process.stdout = process.stderr = "/var/log/app/app-bravo-1.log"
process.monitor_children do |children| process.monitor_children do |children|
children.stop_command "kill -QUIT {{PID}}" children.stop_command "kill {{PID}}"
end end
process.group = "app-bravo" process.group = "app-bravo"

View File

@@ -0,0 +1,24 @@
[program:app-alpha-1]
command=./alpha
autostart=true
autorestart=true
stopsignal=QUIT
stdout_logfile=/var/log/app/alpha-1.log
stderr_logfile=/var/log/app/alpha-1.error.log
user=app
directory=/tmp/app
environment=PORT="5000"
[program:app-bravo-1]
command=./bravo
autostart=true
autorestart=true
stopsignal=QUIT
stdout_logfile=/var/log/app/bravo-1.log
stderr_logfile=/var/log/app/bravo-1.error.log
user=app
directory=/tmp/app
environment=PORT="5100"
[group:app]
programs=app-alpha-1,app-bravo-1

View File

@@ -4,8 +4,8 @@ command=./alpha
autostart=true autostart=true
autorestart=true autorestart=true
stopsignal=QUIT stopsignal=QUIT
stdout_logfile=/var/log/app/alpha-1-out.log stdout_logfile=/var/log/app/alpha-1.log
stderr_logfile=/var/log/app/alpha-1-err.log stderr_logfile=/var/log/app/alpha-1.error.log
user=app user=app
directory=/tmp/app directory=/tmp/app
environment=PORT="5000" environment=PORT="5000"
@@ -14,8 +14,8 @@ command=./alpha
autostart=true autostart=true
autorestart=true autorestart=true
stopsignal=QUIT stopsignal=QUIT
stdout_logfile=/var/log/app/alpha-2-out.log stdout_logfile=/var/log/app/alpha-2.log
stderr_logfile=/var/log/app/alpha-2-err.log stderr_logfile=/var/log/app/alpha-2.error.log
user=app user=app
directory=/tmp/app directory=/tmp/app
environment=PORT="5001" environment=PORT="5001"

View File

@@ -1,24 +0,0 @@
[program:app-alpha]
command=./alpha
autostart=true
autorestart=true
stopsignal=QUIT
stdout_logfile=/var/log/app/alpha-1-out.log
stderr_logfile=/var/log/app/alpha-1-err.log
user=app
directory=/tmp/app
environment=QUEUE="fastqueue,slowqueue",VERBOSE="1",PORT="5000"
[program:app-bravo]
command=./bravo
autostart=true
autorestart=true
stopsignal=QUIT
stdout_logfile=/var/log/app/bravo-1-out.log
stderr_logfile=/var/log/app/bravo-1-err.log
user=app
directory=/tmp/app
environment=QUEUE="fastqueue,slowqueue",VERBOSE="1",PORT="5100"
[group:app]
programs=app-alpha,app-bravo

View File

@@ -1,21 +0,0 @@
[program:app-alpha]
command=./alpha
autostart=true
autorestart=true
stopsignal=QUIT
stdout_logfile=/var/log/app/alpha-1-out.log
stderr_logfile=/var/log/app/alpha-1-err.log
user=app
directory=/tmp/app
environment=FOO="bar",PORT="5000"
[program:app-bravo]
command=./bravo
autostart=true
autorestart=true
stopsignal=QUIT
stdout_logfile=/var/log/app/bravo-1-out.log
stderr_logfile=/var/log/app/bravo-1-err.log
user=app
directory=/tmp/app
environment=FOO="bar",PORT="5100"

View File

@@ -1,24 +0,0 @@
[program:app-alpha]
command=./alpha
autostart=true
autorestart=true
stopsignal=QUIT
stdout_logfile=/var/log/app/alpha-1-out.log
stderr_logfile=/var/log/app/alpha-1-err.log
user=app
directory=/tmp/app
environment=PORT="5000"
[program:app-bravo]
command=./bravo
autostart=true
autorestart=true
stopsignal=QUIT
stdout_logfile=/var/log/app/bravo-1-out.log
stderr_logfile=/var/log/app/bravo-1-err.log
user=app
directory=/tmp/app
environment=PORT="5100"
[group:app]
programs=app-alpha,app-bravo

View File

@@ -24,7 +24,39 @@ def mock_error(subject, message)
end end
def foreman(args) def foreman(args)
capture_stdout do
begin
Foreman::CLI.start(args.split(" ")) Foreman::CLI.start(args.split(" "))
rescue SystemExit
end
end
end
def forked_foreman(args)
rd, wr = IO.pipe("BINARY")
Process.spawn("bundle exec bin/foreman #{args}", :out => wr, :err => wr)
wr.close
rd.read
end
def fork_and_capture(&blk)
rd, wr = IO.pipe("BINARY")
pid = fork do
rd.close
wr.sync = true
$stdout.reopen wr
$stderr.reopen wr
blk.call
$stdout.flush
$stdout.close
end
wr.close
Process.wait pid
buffer = ""
until rd.eof?
p [:foo]
buffer += rd.gets
end
end end
def mock_exit(&block) def mock_exit(&block)
@@ -56,13 +88,21 @@ def write_env(env=".env", options={"FOO"=>"bar"})
end end
end end
def load_export_templates_into_fakefs(type) def without_fakefs
FakeFS.deactivate! FakeFS.deactivate!
files = Dir[File.expand_path("../../data/export/#{type}/**", __FILE__)].inject({}) do |hash, file| ret = yield
FakeFS.activate!
ret
end
def load_export_templates_into_fakefs(type)
without_fakefs do
Dir[File.expand_path("../../data/export/#{type}/**/*", __FILE__)].inject({}) do |hash, file|
next(hash) if File.directory?(file)
hash.update(file => File.read(file)) hash.update(file => File.read(file))
end end
FakeFS.activate! end.each do |filename, contents|
files.each do |filename, contents| FileUtils.mkdir_p File.dirname(filename)
File.open(filename, "w") do |f| File.open(filename, "w") do |f|
f.puts contents f.puts contents
end end
@@ -94,6 +134,17 @@ def normalize_space(s)
s.gsub(/\n[\n\s]*/, "\n") s.gsub(/\n[\n\s]*/, "\n")
end end
def capture_stdout
old_stdout = $stdout.dup
rd, wr = IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
$stdout = wr
yield
wr.close
rd.read
ensure
$stdout = old_stdout
end
RSpec.configure do |config| RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true config.treat_symbols_as_metadata_keys_with_true_values = true
config.color_enabled = true config.color_enabled = true