massive refactoring for programmatic control and stability
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
/.bundle
|
/.bundle
|
||||||
/.rbenv-version
|
/.rbenv-version
|
||||||
|
/.yardoc
|
||||||
/coverage
|
/coverage
|
||||||
/example/log/*
|
/example/log/*
|
||||||
/man/*.html
|
/man/*.html
|
||||||
|
|||||||
1
Gemfile
1
Gemfile
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
14
data/example/spawnee
Executable 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
7
data/example/spawner
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
./spawnee A &
|
||||||
|
./spawnee B &
|
||||||
|
./spawnee C &
|
||||||
|
|
||||||
|
wait
|
||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
7
data/export/runit/log/run.erb
Normal file
7
data/export/runit/log/run.erb
Normal 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"
|
||||||
@@ -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"
|
|
||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
98
lib/foreman/engine/cli.rb
Normal 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
27
lib/foreman/env.rb
Normal 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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
end
|
||||||
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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
1
spec/resources/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
FOO=bar
|
||||||
4
spec/resources/Procfile
Normal file
4
spec/resources/Procfile
Normal 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
2
spec/resources/bin/echo
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo $*
|
||||||
2
spec/resources/bin/env
Executable file
2
spec/resources/bin/env
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo ${!1}
|
||||||
2
spec/resources/bin/test
Executable file
2
spec/resources/bin/test
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo "testing"
|
||||||
@@ -18,7 +18,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
|
|||||||
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"
|
||||||
@@ -37,7 +37,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
|
|||||||
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"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
|
|||||||
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"
|
||||||
@@ -36,7 +36,7 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil
|
|||||||
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"
|
||||||
|
|||||||
24
spec/resources/export/supervisord/app-alpha-1.conf
Normal file
24
spec/resources/export/supervisord/app-alpha-1.conf
Normal 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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user