Compare commits

..

63 Commits

Author SHA1 Message Date
David Dollar
efb6d2f11d 0.50.0 2012-07-11 16:07:52 -04:00
David Dollar
ac528d3b50 update docs 2012-07-11 16:07:03 -04:00
David Dollar
cefd4e351e handle windows 2012-07-11 16:02:07 -04:00
David Dollar
9849f4558a 0.49.0 2012-07-11 15:05:47 -04:00
David Dollar
4b53b42be1 1.8 compatibility 2012-07-11 15:05:28 -04:00
David Dollar
219acaf690 use one pgroup for all of foreman and kill that since ruby 1.8 sucks at pgroups 2012-07-11 15:05:20 -04:00
David Dollar
f8118d7b40 better debugging 2012-07-11 15:04:30 -04:00
David Dollar
7fdade277c 0.48.0 2012-07-10 15:06:22 -04:00
David Dollar
1a0943c495 allow old exporter format to work, but with deprecation warning 2012-07-10 15:06:22 -04:00
David Dollar
4a732abd77 remove debugging code 2012-07-10 15:06:22 -04:00
David Dollar
3e71fea777 Merge pull request #219 from MarkDBlackwell/patch-1
Avoid crash by verifying the existence of SIGHUP before accessing it.
2012-07-09 06:30:14 -07:00
Mark D. Blackwell
ae7aeabb63 Avoid crash by verifying the existence of SIGHUP before accessing it. 2012-07-08 16:21:27 -03:00
David Dollar
9901b9f924 0.48.0.pre3 2012-06-18 00:11:43 -04:00
David Dollar
4c2d569810 0.48.0.pre2 2012-06-18 00:11:23 -04:00
David Dollar
26f9c8186d allow color to be forced on 2012-06-18 00:11:23 -04:00
David Dollar
24ed8946f3 terminate gracefully if stdout goes away 2012-06-18 00:11:23 -04:00
David Dollar
83b2a9cc50 always flush output 2012-06-18 00:11:22 -04:00
David Dollar
a0228b9fa0 Merge pull request #212 from morgoth/added-version-command
added command for displaying foreman version
2012-06-12 09:37:07 -07:00
Wojciech Wnętrzak
b1a2a4a0cd added command for displaying foreman version 2012-06-12 17:16:35 +02:00
David Dollar
7774b7f150 Merge pull request #211 from morgoth/fixed-yaml-usage
fixed using YAML
2012-06-11 12:57:24 -07:00
Wojciech Wnętrzak
b01355a093 fixed using YAML 2012-06-11 21:42:59 +02:00
David Dollar
8b18143281 test on more things, but don't fail 2012-06-11 13:34:08 -04:00
David Dollar
e06886ed57 changelog 2012-06-11 12:34:16 -04:00
David Dollar
d6a00d7262 0.48.0.pre1 2012-06-11 12:27:20 -04:00
David Dollar
d9d1346640 foreman doesn't work on ruby 1.8, may try to fix later 2012-06-11 10:31:07 -04:00
David Dollar
0df1a4d784 use bash 2012-06-10 23:12:44 -04:00
David Dollar
51a704939e massive refactoring for programmatic control and stability 2012-06-10 22:58:09 -04:00
David Dollar
f41cc552c7 Merge pull request #164 from hsume2/master
Add support for running procfile in tmux session
2012-06-07 22:29:36 -07:00
Henry Hsu
065bbf1cd8 Only run tmux specs if tmux is installed 2012-06-07 22:14:47 -07:00
Henry Hsu
b628ddc608 Do not assume BUNDLE_GEMFILE 2012-06-07 22:12:52 -07:00
Henry Hsu
f745b16217 Add support for starting procfile in tmux session 2012-06-07 22:12:52 -07:00
David Dollar
a87a882e60 0.47.0 2012-06-07 22:49:51 -04:00
David Dollar
6ba9252d0f Merge pull request #165 from elf-pavlik/master
list of ports to other languages added to README
2012-06-07 19:45:11 -07:00
David Dollar
efd9e2119b Merge pull request #173 from Viximo/feature/escape-env
Quote and escape environment variables in upstart templates
2012-06-07 19:43:20 -07:00
David Dollar
bf9bdbf118 Merge pull request #194 from maxpow4h/master
Added launchd to exporters
2012-06-07 19:41:23 -07:00
David Dollar
929a138e54 Merge pull request #195 from aneeth/patch-1
Updated data/export/bluepill/master.pill.erb
2012-06-07 19:41:09 -07:00
David Dollar
d6514b4f33 Merge pull request #201 from sos4nt/patch-1
Terminate gracefully upon SIGHUP
2012-06-07 19:40:38 -07:00
David Dollar
8696a36833 Merge pull request #208 from dbrock/master
Fix multi-word argument handling in `foreman run`.
2012-06-06 07:48:44 -07:00
Daniel Brockman
3ea5de42aa Fix multi-word argument handling in foreman run. 2012-06-06 15:36:59 +02:00
David Dollar
4a13122082 Merge pull request #199 from atog/master
set port from .env if specified
2012-05-16 15:06:56 -07:00
Koen Van der Auwera
9b2987c3f0 make 'PORT=5000 foreman start' work 2012-05-16 23:59:09 +02:00
Stefan Schüßler
6274f99225 Terminate gracefully upon SIGHUP
Tmux sends SIGHUP when a session is killed which can result in orphaned processes. Adding a SIGHUP handler terminates the processes as expected.
2012-05-14 13:04:46 +03:00
Koen Van der Auwera
0b0324fed9 set port from .env if specified 2012-05-11 14:17:03 +02:00
Aneeth
32db70b778 Updated data/export/bluepill/master.pill.erb to read in the environment variables from the foreman .env file and reflect it in the pill file 2012-05-04 15:53:16 +08:00
Maxwell Swadling
3db1ad6fbc Added launchd exporter 2012-05-04 15:21:16 +10:00
David Dollar
003b466a17 Merge pull request #193 from pat2man/patch-1
Add stop_grace_time to bluepill config
2012-05-03 12:16:23 -07:00
Patrick Tescher
2a896a0fb5 Add stop_grace_time to bluepill config. Fixes this error message:
Config Error: Stop_grace_time should be greater than the sum of stop_signals delays!

Changed bluepill spec example files to include stop_grace_time
2012-05-03 12:11:24 -07:00
David Dollar
6a8c81a38b Revert "Merge pull request #192 from pat2man/patch-1"
This reverts commit a83dab363e, reversing
changes made to e0fe5baf1b.
2012-05-03 14:53:31 -04:00
David Dollar
a83dab363e Merge pull request #192 from pat2man/patch-1
Add stop_grace_time to bluepill config
2012-05-03 11:52:02 -07:00
Patrick Tescher
53e0f4ecf9 Add stop_grace_time to bluepill config. Fixes this error message:
Config Error: Stop_grace_time should be greater than the sum of stop_signals delays!
2012-05-03 11:50:29 -07:00
David Dollar
e0fe5baf1b update docs 2012-05-02 13:21:02 -04:00
David Dollar
1aa1f15b8f update changelog 2012-05-02 13:20:54 -04:00
David Dollar
fa46a605bb 0.46.0 2012-05-02 13:19:11 -04:00
David Dollar
3077857ab7 Merge pull request #191 from ged/profile_load_write_append
Add Procfile load/write/append API
2012-05-02 10:03:31 -07:00
David Dollar
08aa8f9d5d Merge pull request #190 from ged/remove_obsolete_spec
Remove spec obsoleted by 91a8704
2012-05-02 10:01:30 -07:00
Michael Granger
771489dec9 Remove spec obsoleted by 91a8704 2012-05-02 09:37:32 -07:00
Michael Granger
053ae8f0be Add Profile load/write/append API 2012-05-02 09:30:57 -07:00
David Dollar
af58af1c60 Merge pull request #187 from bkaney/master
Guard against missing Procfile in engine.rb
2012-04-29 21:00:58 -07:00
David Dollar
9075d93370 update docs 2012-04-26 17:50:03 -04:00
David Dollar
0c2e9df722 changelog 2012-04-26 17:50:00 -04:00
Brian Kaney
91a87049db Guard against missing Procfile in engine.rb
Should be able to 'foreman run <task>' without a Procfile.  This seems true to the orig. intent as the cli does not directly check with 'run' (as it  does for 'start').
2012-04-24 15:51:14 -04:00
Matt Griffin
2fcb64959b Quote and escape environment variables in upstart templates 2012-04-03 16:36:18 -04:00
elf Pavlik
a5465bf55e added list of ports to other languages to README 2012-03-18 09:35:51 +01:00
74 changed files with 1321 additions and 1107 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,9 +1,11 @@
script: bundle exec rake spec
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
matrix:
allow_failures:
- rvm: 1.8.7
- rvm: jruby
- rvm: rbx
- rvm: ree
notifications:
email: false
@@ -12,3 +14,11 @@ notifications:
on_failure: always
urls:
- http://dx-helper.herokuapp.com/travis
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
- jruby
- rbx
- ree

View File

@@ -1,3 +1,39 @@
## 0.48.0.pre2 (2012-06-17)
* 0.48.0.pre2 [David Dollar]
* allow color to be forced on [David Dollar]
* terminate gracefully if stdout goes away [David Dollar]
* always flush output [David Dollar]
* test on more things, but don't fail [David Dollar]
* changelog [David Dollar]
## 0.48.0.pre1 (2012-06-11)
* Massive refactoring for programmatic control and stability [David Dollar]
* Procfile commands with shell interpolations now work again [David Dollar]
* Stop trying to test on Ruby 1.8 [David Dollar]
## 0.47.0 (2012-06-07)
* Fix multi-word argument handling in `foreman run`. [Daniel Brockman]
* Make 'PORT=5000 foreman start' work [Koen Van der Auwera]
* Terminate gracefully upon SIGHUP [Stefan Schüßler]
* Set port from .env if specified [Koen Van der Auwera]
* Updated bluepill exporter to use environment variables from .env [Aneeth]
* Added launchd exporter [Maxwell Swadling]
* Quote and escape environment variables in upstart templates [Matt Griffin]
* Added list of ports to other languages to README [elf Pavlik]
## 0.46.0 (2012-05-02)
* Add Profile load/write/append API [Michael Granger]
* Guard against missing Procfile in engine.rb [Brian Kaney]
## 0.45.0 (2012-04-26)
* create and chown log dir in upstart export. [Phil Hagelberg]
* remove parka from dist files [David Dollar]
## 0.44.0 (2012-04-23)
* make var output order repeatable in supervisord export [David Dollar]

View File

@@ -18,4 +18,6 @@ group :development do
gem 'rr', '~> 1.0.2'
gem 'rspec', '~> 2.0'
gem "simplecov", :require => false
gem 'timecop'
gem 'yard'
end

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
foreman (0.45.0)
foreman (0.50.0)
thor (>= 0.13.6)
GEM
@@ -39,9 +39,11 @@ GEM
multi_json (~> 1.0.3)
simplecov-html (~> 0.5.3)
simplecov-html (0.5.3)
thor (0.14.6)
thor (0.15.4)
timecop (0.3.5)
win32console (1.3.0-x86-mingw32)
xml-simple (1.0.15)
yard (0.8.2)
PLATFORMS
java
@@ -58,4 +60,6 @@ DEPENDENCIES
rr (~> 1.0.2)
rspec (~> 2.0)
simplecov
timecop
win32console (~> 1.3.0)
yard

View File

@@ -27,6 +27,12 @@ Manage Procfile-based applications
* [wiki](http://github.com/ddollar/foreman/wiki)
* [changelog](https://github.com/ddollar/foreman/blob/master/Changelog.md)
## Ports
* [shoreman](https://github.com/hecticjeff/shoreman) - shell
* [honcho](https://github.com/nickstenning/honcho) - python
* [norman](https://github.com/josh/norman) - node.js
## Authors
#### Created and maintained by

View File

@@ -1,6 +1,6 @@
#!/bin/sh
#
#/ Usage: foreman-runner [-d <dir>] <command>
#/ Usage: foreman-runner [-d <dir>] <command> [<args>...]
#/
#/ Run a command with exec, optionally changing directory first
@@ -27,10 +27,6 @@ done
shift $((OPTIND-1))
command=$1
[ -z "$1" ] && usage
if [ -z "$1" ]; then
usage
fi
exec $1
exec "$@"

8
bin/taskman Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env ruby
$:.unshift File.expand_path("../../lib", __FILE__)
require "foreman/cli"
Foreman::CLI.engine_class = Foreman::TmuxEngine
Foreman::CLI.start

View File

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

14
data/example/spawnee Executable file
View File

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

7
data/example/spawner Executable file
View File

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

View File

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

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string><%= "#{app}-#{name}-#{num}" %></string>
<key>ProgramArguments</key>
<array>
<string><%= process.command %></string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string><%= log %>/<%= app %>-<%= name %>-<%=num%>.log</string>
<key>UserName</key>
<string><%= user %></string>
<key>WorkingDirectory</key>
<string><%= engine.root %></string>
</dict>
</plist>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,10 @@ module Foreman
defined?(RUBY_PLATFORM) and RUBY_PLATFORM == "java"
end
def self.ruby_18?
defined?(RUBY_VERSION) and RUBY_VERSION =~ /^1\.8\.\d+/
end
def self.windows?
defined?(RUBY_PLATFORM) and RUBY_PLATFORM =~ /(win|w)32$/
end

View File

@@ -1,35 +1,42 @@
require "foreman"
require "foreman/helpers"
require "foreman/engine"
require "foreman/engine/cli"
require "foreman/export"
require "thor"
require "foreman/version"
require "shellwords"
require "yaml"
require "thor"
class Foreman::CLI < Thor
include Foreman::Helpers
map ["-v", "--version"] => :version
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
class_option :root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
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 :port, :type => :numeric, :aliases => "-p"
method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
method_option :color, :type => :boolean, :aliases => "-c", :desc => "Force color to be enabled"
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"
class << self
# Hackery. Take the run method away from Thor so that we can redefine it.
def is_thor_reserved_word?(word, type)
return false if word == 'run'
return false if word == "run"
super
end
end
def start(process=nil)
check_procfile!
engine.options[:concurrency] = "#{process}=1" if process
load_environment!
engine.load_procfile(procfile)
engine.options[:formation] = "#{process}=1" if process
engine.start
end
@@ -45,6 +52,8 @@ class Foreman::CLI < Thor
def export(format, location=nil)
check_procfile!
load_environment!
engine.load_procfile(procfile)
formatter = Foreman::Export.formatter(format)
formatter.new(location, engine, options).export
rescue Foreman::Export::Exception => ex
@@ -55,16 +64,19 @@ class Foreman::CLI < Thor
def check
check_procfile!
error "no processes defined" unless engine.procfile.entries.length > 0
puts "valid procfile detected (#{engine.procfile.process_names.join(', ')})"
engine.load_procfile(procfile)
error "no processes defined" unless engine.processes.length > 0
puts "valid procfile detected (#{engine.process_names.join(', ')})"
end
desc "run COMMAND", "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)
engine.apply_environment!
load_environment!
begin
exec args.join(" ")
exec engine.env, args.shelljoin
rescue Errno::EACCES
error "not executable: #{args.first}"
rescue Errno::ENOENT
@@ -72,33 +84,57 @@ class Foreman::CLI < Thor
end
end
private ######################################################################
desc "version", "Display Foreman gem version"
def check_procfile!
error("#{procfile} does not exist.") unless File.exist?(procfile)
def version
puts Foreman::VERSION
end
def engine
@engine ||= Foreman::Engine.new(procfile, options)
end
def procfile
case
when options[:procfile] then options[:procfile]
when options[:app_root] then File.expand_path(File.join(options[:app_root], "Procfile"))
else "Procfile"
no_tasks do
def engine
@engine ||= begin
engine_class = Foreman::Engine::CLI
engine = engine_class.new(options)
engine
end
end
end
private ######################################################################
def error(message)
puts "ERROR: #{message}"
exit 1
end
def check_procfile!
error("#{procfile} does not exist.") unless File.exist?(procfile)
end
def load_environment!
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
def procfile
case
when options[:procfile] then options[:procfile]
when options[:root] then File.expand_path(File.join(options[:app_root], "Procfile"))
else "Procfile"
end
end
def options
original_options = super
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))
end
end

View File

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

View File

@@ -1,8 +1,7 @@
require "foreman"
require "foreman/color"
require "foreman/env"
require "foreman/process"
require "foreman/procfile"
require "foreman/utils"
require "tempfile"
require "timeout"
require "fileutils"
@@ -10,125 +9,272 @@ require "thread"
class Foreman::Engine
attr_reader :environment
attr_reader :procfile
attr_reader :directory
attr_reader :env
attr_reader :options
attr_reader :processes
COLORS = %w( cyan yellow green magenta red blue intense_cyan intense_yellow
intense_green intense_magenta intense_red, intense_blue )
Foreman::Color.enable($stdout)
def initialize(procfile, options={})
@procfile = Foreman::Procfile.new(procfile)
@directory = options[:app_root] || File.expand_path(File.dirname(procfile))
# Create an +Engine+ for running processes
#
# @param [Hash] options
#
# @option options [String] :formation (all=1) The process formation to use
# @option options [Fixnum] :port (5000) The base port to assign to processes
# @option options [String] :root (Dir.pwd) The root directory from which to run processes
#
def initialize(options={})
@options = options.dup
@output_mutex = Mutex.new
@options[:env] ||= default_env
@environment = read_environment_files(@options[:env])
@options[:formation] ||= (options[:concurrency] || "all=1")
@env = {}
@mutex = Mutex.new
@names = {}
@processes = []
@running = {}
@readers = {}
end
# Start the processes registered to this +Engine+
#
def start
proctitle "ruby: foreman master"
termtitle "#{File.basename(@directory)} - foreman"
trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
trap("INT") { puts "SIGINT received"; terminate_gracefully }
trap("HUP") { puts "SIGHUP received"; terminate_gracefully } if ::Signal.list.keys.include? 'HUP'
assign_colors
startup
spawn_processes
watch_for_output
watch_for_termination
sleep 0.1
watch_for_termination { terminate_gracefully }
shutdown
end
def port_for(process, num, base_port=nil)
base_port ||= 5000
offset = procfile.process_names.index(process.name) * 100
base_port.to_i + offset + num - 1
# Register a process to be run by this +Engine+
#
# @param [String] name A name for this process
# @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
def apply_environment!
environment.each { |k,v| ENV[k] = v }
# Clear the processes registered to this +Engine+
#
def clear
@names = {}
@processes = []
end
def self.read_environment(filename)
return {} unless File.exists?(filename)
# Register processes by reading a Procfile
#
# @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|
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
key, val = [$1, $2]
case val
when /\A'(.*)'\z/ then hash[key] = $1
when /\A"(.*)"\z/ then hash[key] = $1.gsub(/\\(.)/, '\1')
else hash[key] = val
# Load a .env file into the +env+ for this +Engine+
#
# @param [String] filename A .env file to load into the environment
#
def load_env(filename)
Foreman::Env.new(filename).entries do |name, value|
@env[name] = value
end
end
# Send a signal to all processesstarted by this +Engine+
#
# @param [String] signal The signal to send to each process
#
def killall(signal="SIGTERM")
if Foreman.windows?
@running.each do |pid, (process, index)|
system "sending #{signal} to #{name_for(pid)} at pid #{pid}"
begin
Process.kill(signal, pid)
rescue Errno::ESRCH, Errno::EPERM
end
end
hash
else
begin
Process.kill "-#{signal}", Process.pid
rescue Errno::ESRCH, Errno::EPERM
end
end
end
private ######################################################################
# Get the process formation
#
# @returns [Fixnum] The formation count for the specified process
#
def formation
@formation ||= parse_formation(options[:formation])
end
# List the available process names
#
# @returns [Array] A list of process names
#
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
# Get the root directory for this +Engine+
#
# @returns [String] The root directory
#
def root
File.expand_path(options[:root] || Dir.pwd)
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=nil)
if base
base + (@processes.index(process.process) * 100) + (instance - 1)
else
base_port + (@processes.index(process) * 100) + (instance - 1)
end
end
# Get the base port for this foreman instance
#
# @returns [Fixnum] port The base port
#
def base_port
(options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i
end
# deprecated
def environment
env
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 create_pipe
IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
end
def name_for(pid)
process, index = @running[pid]
[ @names[process], index.to_s ].compact.join(".")
end
def parse_formation(formation)
pairs = 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
def output_with_mutex(name, message)
@mutex.synchronize do
output name, message
end
end
def system(message)
output_with_mutex "system", message
end
def termination_message_for(status)
if status.exited?
"exited with code #{status.exitstatus}"
elsif status.signaled?
"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
concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency])
procfile.entries.each do |entry|
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|
running_processes[process.pid] = process
readers[process] = reader
@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
def base_port
options[:port] || 5000
end
def kill_all(signal="SIGTERM")
running_processes.each do |pid, process|
info "sending #{signal} to pid #{pid}"
process.kill signal
end
end
def terminate_gracefully
return if @terminating
@terminating = true
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
rescue Timeout::Error
info "sending SIGKILL to all processes"
kill_all "SIGKILL"
end
def poll_readers
rs, ws = IO.select(readers.values, [], [], 1)
(rs || []).each do |r|
data = r.gets
next unless data
data.force_encoding("BINARY") if data.respond_to?(:force_encoding)
ps, message = data.split(",", 2)
color = colors[ps.split(".").first]
info message, ps, color
end
end
def watch_for_output
Thread.new do
require "win32console" if Foreman.windows?
begin
loop do
poll_readers
(IO.select(@readers.values).first || []).each do |reader|
data = reader.gets
output_with_mutex name_for(@readers.invert[reader]), data
end
end
rescue Exception => ex
puts ex.message
@@ -139,89 +285,29 @@ private ######################################################################
def watch_for_termination
pid, status = Process.wait2
process = running_processes.delete(pid)
info "process terminated", process.name
terminate_gracefully
output_with_mutex name_for(pid), termination_message_for(status)
@running.delete(pid)
yield if block_given?
pid
rescue Errno::ECHILD
end
def info(message, name="system", color=:white)
output = ""
output += $stdout.color(color)
output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
output += $stdout.color(:reset)
output += message.chomp
puts output
end
def print(message=nil)
@output_mutex.synchronize do
$stdout.print message
def terminate_gracefully
return if @terminating
@terminating = true
if Foreman.windows?
system "sending SIGKILL to all processes"
killall "SIGKILL"
else
system "sending SIGTERM to all processes"
killall "SIGTERM"
end
end
def puts(message=nil)
@output_mutex.synchronize do
$stdout.puts message
Timeout.timeout(5) do
watch_for_termination while @running.length > 0
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 : ""
rescue Timeout::Error
system "sending SIGKILL to all processes"
killall "SIGKILL"
end
end

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

@@ -0,0 +1,105 @@
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, force=false)
io.extend(self)
@@color_force = force
end
def color?
return true if @@color_force
return true if Foreman.windows?
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"
require "win32console" if Foreman.windows?
Color.enable($stdout, options[:color]) unless $stdout.respond_to?(:color?)
end
def output(name, data)
data.to_s.chomp.split("\n").each do |message|
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
$stdout.flush
end
rescue Errno::EPIPE
terminate_gracefully
end
def shutdown
end
private
def name_padding
@name_padding ||= begin
index_padding = @names.values.map { |n| formation[n] }.max.to_s.length + 1
name_padding = @names.values.map { |n| n.length + index_padding }.sort.last
[ 6, name_padding ].max
end
end
def pad_process_name(name)
name.ljust(name_padding, " ")
end
def map_colors
colors = Hash.new("white")
@names.values.each_with_index do |name, index|
colors[name] = FOREMAN_COLORS[index % FOREMAN_COLORS.length]
end
colors["system"] = "intense_white"
colors
end
def proctitle(title)
$0 = title
end
def termtitle(title)
printf("\033]0;#{title}\007") unless Foreman.windows?
end
end

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

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

View File

@@ -1,5 +1,6 @@
require "foreman"
require "foreman/helpers"
require "pathname"
module Foreman::Export
extend Foreman::Helpers
@@ -24,11 +25,10 @@ module Foreman::Export
end
require "foreman/export/base"
require "foreman/export/inittab"
require "foreman/export/upstart"
require "foreman/export/bluepill"
require "foreman/export/runit"
require "foreman/export/supervisord"
require "foreman/export/launchd"

View File

@@ -1,27 +1,81 @@
require "foreman/export"
require "foreman/utils"
require "ostruct"
require "pathname"
require "shellwords"
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
# deprecated
attr_reader :port
def initialize(location, engine, options={})
@location = location
@engine = engine
@app = options[:app]
@log = options[:log]
@port = options[:port]
@user = options[:user]
@template = options[:template]
@concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
@location = location
@engine = engine
@options = options.dup
@formation = engine.formation
# deprecated
def port
Foreman::Export::Base.warn_deprecation!
engine.base_port
end
# deprecated
def template
Foreman::Export::Base.warn_deprecation!
options[:template]
end
# deprecated
def @engine.procfile
Foreman::Export::Base.warn_deprecation!
@processes.map do |process|
OpenStruct.new(
:name => @names[process],
:process => process
)
end
end
end
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
private ######################################################################
def self.warn_deprecation!
@@deprecation_warned ||= false
return if @@deprecation_warned
puts "WARNING: Using deprecated exporter interface. Please update your exporter"
puts "the interface shown in the upstart exporter:"
puts
puts "https://github.com/ddollar/foreman/blob/master/lib/foreman/export/upstart.rb"
puts "https://github.com/ddollar/foreman/blob/master/data/export/upstart/process.conf.erb"
puts
@@deprecation_warned = true
end
def error(message)
raise Foreman::Export::Exception.new(message)
end
@@ -29,8 +83,19 @@ private ######################################################################
def say(message)
puts "[foreman export] %s" % message
end
def clean(filename)
return unless File.exists?(filename)
say "cleaning up: #{filename}"
FileUtils.rm(filename)
end
def export_template(exporter, file, template_root)
def shell_quote(value)
'"' + Shellwords.escape(value) + '"'
end
# deprecated
def old_export_template(exporter, file, template_root)
if template_root && File.exist?(file_path = File.join(template_root, file))
File.read(file_path)
elsif File.exist?(file_path = File.expand_path(File.join("~/.foreman/templates", file)))
@@ -40,9 +105,39 @@ private ######################################################################
end
end
def export_template(name, file=nil, template_root=nil)
if file && template_root
old_export_template name, file, template_root
else
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
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
def write_file(filename, contents)
say "writing: #{filename}"
filename = File.join(location, filename) unless Pathname.new(filename).absolute?
File.open(filename, "w") do |file|
file.puts contents
end

View File

@@ -4,23 +4,9 @@ require "foreman/export"
class Foreman::Export::Bluepill < Foreman::Export::Base
def export
error("Must specify a location") unless location
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}.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
super
clean "#{location}/#{app}.pill"
write_template "bluepill/master.pill.erb", "#{app}.pill", binding
end
end

View File

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

View File

@@ -0,0 +1,15 @@
require "erb"
require "foreman/export"
class Foreman::Export::Launchd < Foreman::Export::Base
def export
super
engine.each_process do |name, process|
1.upto(engine.formation[name]) do |num|
write_template "launchd/launchd.plist.erb", "#{app}-#{name}-#{num}.plist", binding
end
end
end
end

View File

@@ -2,58 +2,33 @@ require "erb"
require "foreman/export"
class Foreman::Export::Runit < Foreman::Export::Base
ENV_VARIABLE_REGEX = /([a-zA-Z_]+[a-zA-Z0-9_]*)=(\S+)/
def export
error("Must specify a location") unless location
super
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
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"
engine.each_process do |name, process|
1.upto(engine.formation[name]) do |num|
process_directory = "#{app}-#{name}-#{num}"
create_directory process_directory
create_directory process_env_directory
create_directory process_log_directory
create_directory "#{process_directory}/env"
create_directory "#{process_directory}/log"
run = ERB.new(run_template).result(binding)
write_file "#{process_directory}/run", run
FileUtils.chmod 0755, "#{process_directory}/run"
write_template "runit/run.erb", "#{process_directory}/run", binding
chmod 0755, "#{process_directory}/run"
port = engine.port_for(process, num, self.port)
environment_variables = {'PORT' => port}.
merge(engine.environment).
merge(inline_variables(process.command))
environment_variables.each_pair do |var, env|
write_file "#{process_env_directory}/#{var.upcase}", env
port = engine.port_for(process, num)
engine.env.merge("PORT" => port.to_s).each do |key, value|
write_file "#{process_directory}/env/#{key}", value
end
log_run = ERB.new(log_run_template).result(binding)
write_file "#{process_log_directory}/run", log_run
FileUtils.chmod 0755, "#{process_log_directory}/run"
write_template "runit/log/run.erb", "#{process_directory}/log/run", binding
chmod 0755, "#{process_directory}/log/run"
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

View File

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

View File

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

View File

@@ -3,94 +3,100 @@ require "rubygems"
class Foreman::Process
attr_reader :entry
attr_reader :num
attr_reader :pid
attr_reader :port
attr_reader :command
attr_reader :env
def initialize(entry, num, port)
@entry = entry
@num = num
@port = port
# Create a Process
#
# @param [String] command The command to run
# @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
def run(pipe, basedir, environment)
with_environment(environment.merge("PORT" => port.to_s)) do
run_process basedir, entry.command, pipe
# Run a +Process+
#
# @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
expanded_command = command.dup
env.each do |key, val|
expanded_command.gsub!("$#{key}", val)
end
Process.spawn env, expanded_command, :out => output, :err => output
end
elsif Foreman.jruby?
Dir.chdir(cwd) do
require "posix/spawn"
POSIX::Spawn.spawn env, command, :out => output, :err => output
end
elsif Foreman.ruby_18?
Dir.chdir(cwd) do
fork do
$stdout.reopen output
$stderr.reopen output
env.each { |k,v| ENV[k] = v }
exec command
end
end
else
Dir.chdir(cwd) do
Process.spawn env, command, :out => output, :err => output
end
end
end
def name
"%s.%s" % [ entry.name, num ]
end
# Send a signal to this +Process+
#
# @param [String] signal The signal to send
#
def kill(signal)
pid && Process.kill(signal, pid)
if Foreman.windows?
pid && Process.kill(signal, pid)
else
pid && Process.kill("-#{signal}", pid)
end
rescue Errno::ESRCH
false
end
def detach
pid && Process.detach(pid)
end
# Test whether or not this +Process+ is still running
#
# @returns [Boolean]
#
def alive?
kill(0)
end
# Test whether or not this +Process+ has terminated
#
# @returns [Boolean]
#
def dead?
!alive?
end
private
def fork_with_io(command, basedir)
reader, writer = IO.pipe
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
end
end
[ reader, pid ]
def cwd
@options[:cwd] || "."
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

View File

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

View File

@@ -1,22 +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
end

View File

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

View File

@@ -1,5 +1,5 @@
module Foreman
VERSION = "0.45.0"
VERSION = "0.50.0"
end

View File

@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "FOREMAN" "1" "April 2012" "Foreman 0.44.0" "Foreman Manual"
.TH "FOREMAN" "1" "June 2012" "Foreman 0.49.0" "Foreman Manual"
.
.SH "NAME"
\fBforeman\fR \- manage Procfile\-based applications
@@ -46,6 +46,10 @@ Specify an alternate Procfile to load, implies \fB\-d\fR at the Procfile root\.
\fB\-p\fR, \fB\-\-port\fR
Specify which port to use as the base for this application\. Should be a multiple of 1000\.
.
.TP
\fB\-t\fR, \fB\-\-tmux\fR
Runs the processes in a tmux session\. Creates one window for each process and an extra window containing the output of each window (requires gawk)\.
.
.P
\fBforeman run\fR is used to run one\-off commands using the same environment as your defined processes\.
.

View File

@@ -40,6 +40,10 @@ The following options control how the application is run:
Specify which port to use as the base for this application. Should be
a multiple of 1000.
* `-t`, `--tmux`:
Runs the processes in a tmux session. Creates one window for each process
and an extra window containing the output of each window (requires gawk).
`foreman run` is used to run one-off commands using the same environment
as your defined processes.

View File

@@ -3,12 +3,23 @@ require "foreman/cli"
describe "Foreman::CLI", :fakefs do
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 "with a non-existent Procfile" do
it "prints an error" do
describe "when a Procfile doesnt exist", :fakefs do
it "displays an error" do
mock_error(subject, "Procfile does not exist.") do
dont_allow.instance_of(Foreman::Engine).start
subject.start
@@ -16,175 +27,60 @@ describe "Foreman::CLI", :fakefs do
end
end
describe "with a Procfile" do
before(:each) { write_procfile }
it "runs successfully" do
dont_allow(subject).error
mock.instance_of(Foreman::Engine).start
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
describe "with an alternate root" do
it "reads the Procfile from that root" do
write_procfile "/some/app/Procfile"
mock(Foreman::Procfile).new("/some/app/Procfile")
mock.instance_of(Foreman::Engine).start
foreman %{ start -d /some/app }
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
describe "with a valid Procfile" do
it "can run a single command" do
without_fakefs do
output = foreman("start env -f #{resource_path("Procfile")}")
output.should =~ /env.1/
output.should_not =~ /test.1/
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")
it "can run all commands" do
without_fakefs do
output = foreman("start -f #{resource_path("Procfile")} -e #{resource_path(".env")}")
output.should =~ /echo.1 \| echoing/
output.should =~ /env.1 \| bar/
output.should =~ /test.1 \| testing/
end
end
end
end
describe "check" do
describe "with a valid Procfile" do
before { write_procfile }
it "displays the jobs" do
mock(subject).puts("valid procfile detected (alpha, bravo)")
subject.check
end
it "with a valid Procfile displays the jobs" do
write_procfile
foreman("check").should == "valid procfile detected (alpha, bravo)\n"
end
describe "with a blank Procfile" do
before do
FileUtils.touch("Procfile")
end
it "displays an error" do
mock_error(subject, "no processes defined") do
subject.check
end
end
it "with a blank Procfile displays an error" do
FileUtils.touch "Procfile"
foreman("check").should == "ERROR: no processes defined\n"
end
describe "without a Procfile" do
it "displays an error" do
mock_error(subject, "Procfile does not exist.") do
subject.check
end
end
it "without a Procfile displays an error" do
FileUtils.rm_f "Procfile"
foreman("check").should == "ERROR: Procfile does not exist.\n"
end
end
describe "run" do
describe "with a valid Procfile" do
before { write_procfile }
it "can run a command" do
forked_foreman("run echo 1").should == "1\n"
end
describe "and a command" do
let(:command) { ["ls", "-l"] }
it "includes the environment" do
forked_foreman("run #{resource_path("bin/env FOO")} -e #{resource_path(".env")}").should == "bar\n"
end
end
before(:each) do
stub(subject).exec
end
describe "version" do
it "displays gem version" do
foreman("version").chomp.should == Foreman::VERSION
end
it "should load the environment file" do
write_env
preserving_env do
subject.run *command
ENV["FOO"].should == "bar"
end
ENV["FOO"].should be_nil
end
it "should runute the command as a string" do
mock(subject).exec(command.join(" "))
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
it "displays gem version on shortcut command" do
foreman("-v").chomp.should == Foreman::VERSION
end
end

View File

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

View File

@@ -1,86 +1,79 @@
require "spec_helper"
require "foreman/engine"
describe "Foreman::Engine", :fakefs do
subject { Foreman::Engine.new("Procfile", {}) }
class Foreman::Engine::Tester < Foreman::Engine
attr_reader :buffer
before do
any_instance_of(Foreman::Engine) do |engine|
stub(engine).proctitle
stub(engine).termtitle
end
def startup
@buffer = ""
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
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"
subject.process("alpha").command.should == "./alpha"
subject.process("bravo").command.should == "./bravo"
end
end
end
describe "start" do
it "forks the processes" do
write_procfile
mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./alpha", is_a(IO))
mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./bravo", is_a(IO))
mock(subject.process("alpha")).run(anything)
mock(subject.process("bravo")).run(anything)
mock(subject).watch_for_output
mock(subject).watch_for_termination
subject.start
end
it "handles concurrency" do
write_procfile
engine = Foreman::Engine.new("Procfile",:concurrency => "alpha=2")
mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./alpha", is_a(IO)).twice
mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./bravo", is_a(IO)).never
mock(engine).watch_for_output
mock(engine).watch_for_termination
engine.start
subject.options[:formation] = "alpha=2"
mock(subject.process("alpha")).run(anything).twice
mock(subject.process("bravo")).run(anything).never
mock(subject).watch_for_output
mock(subject).watch_for_termination
subject.start
end
end
describe "directories" do
it "has the directory default relative to the Procfile" do
write_procfile "/some/app/Procfile"
engine = Foreman::Engine.new("/some/app/Procfile")
engine.directory.should == "/some/app"
engine = Foreman::Engine.new.load_procfile("/some/app/Procfile")
engine.root.should == "/some/app"
end
end
describe "environment" do
before(:each) 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
it "should read env files" do
File.open("/tmp/env", "w") { |f| f.puts("FOO=baz") }
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env")
engine.environment.should == {"FOO"=>"baz"}
engine.start
subject.load_env("/tmp/env")
subject.env["FOO"].should == "baz"
end
it "should read more than one if specified" do
File.open("/tmp/env1", "w") { |f| f.puts("FOO=bar") }
File.open("/tmp/env2", "w") { |f| f.puts("BAZ=qux") }
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env1,/tmp/env2")
engine.environment.should == { "FOO"=>"bar", "BAZ"=>"qux" }
engine.start
subject.load_env "/tmp/env1"
subject.load_env "/tmp/env2"
subject.env["FOO"].should == "bar"
subject.env["BAZ"].should == "qux"
end
it "should handle quoted values" do
@@ -90,48 +83,22 @@ describe "Foreman::Engine", :fakefs do
f.puts "FRED='barney'"
f.puts 'OTHER="escaped\"quote"'
end
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env")
engine.environment.should == { "FOO" => "bar", "BAZ" => "qux", "FRED" => "barney", "OTHER" => 'escaped"quote' }
subject.load_env "/tmp/env"
subject.env["FOO"].should == "bar"
subject.env["BAZ"].should == "qux"
subject.env["FRED"].should == "barney"
subject.env["OTHER"].should == 'escaped"quote'
end
it "should fail if specified and doesnt exist" do
mock.instance_of(Foreman::Engine).error("No such file: /tmp/env")
engine = Foreman::Engine.new("Procfile", :env => "/tmp/env")
lambda { subject.load_env "/tmp/env" }.should raise_error(Errno::ENOENT)
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
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
it "should set port from .env if specified" do
File.open("/tmp/env", "w") { |f| f.puts("PORT=9000") }
subject.load_env "/tmp/env"
subject.send(:base_port).should == 9000
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

View File

@@ -1,10 +1,11 @@
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(: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) }
it "has a say method for displaying info" do
@@ -12,10 +13,6 @@ describe "Foreman::Export::Base" do
subject.send(:say, "foo")
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
lambda { subject.send(:error, "foo") }.should raise_error(Foreman::Export::Exception, "foo")
end

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,18 +4,18 @@ require "foreman/export/supervisord"
require "tmpdir"
describe Foreman::Export::Supervisord, :fakefs do
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") }
let(:engine) { Foreman::Engine.new(procfile) }
let(:options) { Hash.new }
let(:supervisord) { Foreman::Export::Supervisord.new("/tmp/init", engine, options) }
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") }
let(:formation) { nil }
let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) }
let(:options) { Hash.new }
let(:supervisord) { Foreman::Export::Supervisord.new("/tmp/init", engine, options) }
before(:each) { load_export_templates_into_fakefs("supervisord") }
before(:each) { stub(supervisord).say }
it "exports to the filesystem" do
supervisord.export
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app.conf")
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app-alpha-1.conf")
end
it "cleans up if exporting into an existing dir" do
@@ -25,7 +25,7 @@ describe Foreman::Export::Supervisord, :fakefs do
end
context "with concurrency" do
let(:options) { Hash[:concurrency => "alpha=2"] }
let(:formation) { "alpha=2" }
it "exports to the filesystem with concurrency" do
supervisord.export
@@ -33,53 +33,4 @@ describe Foreman::Export::Supervisord, :fakefs do
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

View File

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

View File

@@ -5,127 +5,44 @@ require 'timeout'
require 'tmpdir'
describe Foreman::Process do
subject { described_class.new entry, number, port }
let(:number) { 1 }
let(:port) { 777 }
let(:command) { "script" }
let(:name) { "foobar" }
let(:entry) { OpenStruct.new :name => name, :command => command }
def run(process, options={})
rd, wr = IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
process.run(options.merge(:output => wr))
rd.gets
end
its(:entry) { entry }
its(:num) { number }
its(:port) { port }
its(:name) { "#{name}.#{port}" }
its(:pid) { nil }
describe "#run" do
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)
it "runs the process" do
process = Foreman::Process.new(resource_path("bin/test"))
run(process).should == "testing\n"
end
def run_file(executable, code)
file = File.open("#{basedir}/script", 'w') {|it| it << code }
run "#{executable} #{file.path}"
sleep 1
it "can set environment" do
process = Foreman::Process.new(resource_path("bin/env FOO"), :env => { "FOO" => "bar" })
run(process).should == "bar\n"
end
context 'options' do
it 'should set PORT for environment' do
mock(subject).run_process(basedir, command, pipe) do
ENV['PORT'].should == port.to_s
end
run
end
it 'should set custom variables for environment' do
mock(subject).run_process(basedir, command, pipe) do
ENV['foo'].should == 'bar'
end
run
end
it 'should restore environment afterwards' do
mock(subject).run_process(basedir, command, pipe)
run
ENV.should_not include('PORT', 'foo')
end
it "can set per-run environment" do
process = Foreman::Process.new(resource_path("bin/env FOO"))
run(process, :env => { "FOO" => "bar "}).should == "bar\n"
end
context 'process' do
around do |spec|
IO.pipe do |reader, writer|
@reader, @writer = reader, writer
spec.run
end
end
it "can handle env vars in the command" do
process = Foreman::Process.new(resource_path("bin/echo $FOO"), :env => { "FOO" => "bar" })
run(process).should == "bar\n"
end
let(:pipe) { @writer }
let(:output) { @reader.read_nonblock 1024 }
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 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 arguments' do
pending
run %{ sh -c "trap '' TERM; sleep 10" }
subject.should be_alive
end
it "should output utf8 properly" do
process = Foreman::Process.new(resource_path("bin/utf8"))
run(process).should == "\xFF\x03\n"
end
end
end

View File

@@ -0,0 +1,41 @@
require 'spec_helper'
require 'foreman/procfile'
require 'pathname'
require 'tmpdir'
describe Foreman::Procfile, :fakefs do
subject { Foreman::Procfile.new }
it "can load from a file" do
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
subject["charlie"] = "./charlie"
subject["charlie"].should == "./charlie"
end
it "can write to a string" do
subject["foo"] = "./foo"
subject["bar"] = "./bar"
subject.to_s.should == "foo: ./foo\nbar: ./bar"
end
it "can write to a file" do
subject["foo"] = "./foo"
subject["bar"] = "./bar"
subject.save "/tmp/proc"
File.read("/tmp/proc").should == "foo: ./foo\nbar: ./bar\n"
end
end

1
spec/resources/.env Normal file
View File

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

4
spec/resources/Procfile Normal file
View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>app-alpha-1</string>
<key>ProgramArguments</key>
<array>
<string>./alpha</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/var/log/app/app-alpha-1.log</string>
<key>UserName</key>
<string>app</string>
<key>WorkingDirectory</key>
<string>/tmp/app</string>
</dict>
</plist>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>app-bravo-1</string>
<key>ProgramArguments</key>
<array>
<string>./bravo</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/var/log/app/app-bravo-1.log</string>
<key>UserName</key>
<string>app</string>
<key>WorkingDirectory</key>
<string>/tmp/app</string>
</dict>
</plist>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ SimpleCov.start do
end
require "rspec"
require "timecop"
require "fakefs/safe"
require "fakefs/spec_helpers"
@@ -23,7 +24,38 @@ def mock_error(subject, message)
end
def foreman(args)
Foreman::CLI.start(args.split(" "))
capture_stdout do
begin
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?
buffer += rd.gets
end
end
def mock_exit(&block)
@@ -55,13 +87,21 @@ def write_env(env=".env", options={"FOO"=>"bar"})
end
end
def load_export_templates_into_fakefs(type)
def without_fakefs
FakeFS.deactivate!
files = Dir[File.expand_path("../../data/export/#{type}/**", __FILE__)].inject({}) do |hash, file|
hash.update(file => File.read(file))
end
ret = yield
FakeFS.activate!
files.each do |filename, contents|
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))
end
end.each do |filename, contents|
FileUtils.mkdir_p File.dirname(filename)
File.open(filename, "w") do |f|
f.puts contents
end
@@ -93,6 +133,17 @@ def normalize_space(s)
s.gsub(/\n[\n\s]*/, "\n")
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|
config.treat_symbols_as_metadata_keys_with_true_values = true
config.color_enabled = true

View File

@@ -35,7 +35,7 @@ def newer_release
tags = %x{ git tag --contains v#{latest_release} }.split("\n").sort_by do |tag|
Gem::Version.new(tag[1..-1])
end
tags.reject { |tag| Gem::Version.new(tag[1..-1]).prerelease? }[1]
tags[1]
end
desc "Generate a Changelog"
@@ -60,7 +60,6 @@ end
desc "Cut a release"
task :release do
Rake::Task["authors"].invoke
Rake::Task["changelog"].invoke
Rake::Task["pages"].invoke
end