Compare commits

...

88 Commits

Author SHA1 Message Date
David Dollar
64f0749d16 0.59.0 2012-09-15 11:49:46 +09:00
David Dollar
6b77ca1e46 Merge pull request #246 from jeremyevans/patch-1
Don't have foreman-runner depend on bash
2012-09-14 19:48:03 -07:00
Jeremy Evans
6a44dd3fd3 Use /bin/sh instead of bash for foreman-runner
/bin/sh should be installed by default on all unix machines, bash
is not necessarily installed on all machines.
2012-09-14 17:35:54 -07:00
David Dollar
6c04dab649 0.58.0 2012-09-14 18:10:05 +09:00
David Dollar
96e26e7412 dont set HOME 2012-09-14 17:44:32 +09:00
David Dollar
a2c03cc402 Merge pull request #217 from danielfarrell/capistrano
Add capistrano export support
2012-09-14 01:42:34 -07:00
David Dollar
803115c0c4 Merge pull request #226 from alachaum/master
CLI: fix directory option
2012-09-14 01:41:36 -07:00
David Dollar
a4343187ad Merge pull request #239 from indrekj/master
Use path and env variables in the inittab export
2012-09-14 01:41:18 -07:00
David Dollar
98af1f0943 Merge pull request #261 from martinisoft/launchd_environment
Launchd environment
2012-09-14 01:34:35 -07:00
David Dollar
af2d4762a8 Merge pull request #250 from szimek/249_multiline_strings
Handle multiline strings in .env file
2012-09-14 01:34:10 -07:00
Aaron Kalin
1d06124457 Add StandardOutPath to launchd export 2012-09-11 15:04:15 -05:00
Aaron Kalin
698e6ae092 Add command argument string splitting
launchd uses exec() under the hood when you specify ProgramArguments
or Program so you must break apart program arguments into separate
strings in an array or the command will fail
2012-09-10 19:10:47 -05:00
Aaron Kalin
c617ddb3b2 Cleanup launchd exporter
Also suppresses the deprecation warning for the launchd exporter when
using port instead of engine.port_for
2012-09-10 18:21:51 -05:00
Aaron Kalin
f6d4badcd2 Enable trim_mode via '-' in ERB templates
This adds whitespace supression optionally, similar to the default
behavior in Rails ActionView ERB templates.

Just add - to a %> like so: -%> and it will remove trailing whitespace
and newlines. This can also be done to the beginning tag for removing
newlines from the beginning of the tag line.
2012-09-10 18:18:56 -05:00
Aaron Kalin
f29bf49a35 Add support for setting environment variables 2012-09-05 18:27:10 -05:00
David Dollar
e06d36b27c dont test 1.8.7 or rbx 2012-08-24 12:49:35 -04:00
David Dollar
85e62dfbeb Merge pull request #254 from omarkhan/master
foreman run should exit with the same code as its command
2012-08-24 09:14:45 -07:00
Omar Khan
574e852710 foreman run should exit with the same code as its command 2012-08-24 16:50:05 +01:00
David Dollar
68f098c1d2 update docs 2012-08-21 10:59:21 -04:00
David Dollar
e8bdafdfc1 changelog 2012-08-21 10:58:58 -04:00
David Dollar
4abd3ebedb 0.57.0 2012-08-21 10:58:11 -04:00
David Dollar
f765436dde fix errant space 2012-08-21 10:58:00 -04:00
David Dollar
b5c513b4b5 Merge pull request #247 from asanghi/master
start on boot worked with network-interface but not network
2012-08-21 07:56:30 -07:00
Aditya Sanghi
ee761ff098 Another fix for PR #229 2012-08-21 20:16:28 +05:30
David Dollar
27c22deb6c update changelog 2012-08-19 12:05:37 -04:00
David Dollar
681a9f7e61 0.56.0 2012-08-19 12:05:07 -04:00
David Dollar
8335a2b1ba read .profile, not .profile.d 2012-08-16 12:57:22 -04:00
David Dollar
407425ca78 update changelog 2012-08-14 17:08:14 -04:00
David Dollar
6ca505b4cd 0.55.0 2012-08-14 17:07:52 -04:00
David Dollar
612eae5e21 use a forked process to exec a run with environment 2012-08-14 17:07:39 -04:00
David Dollar
497b5ea1eb update changelog 2012-08-14 17:03:28 -04:00
David Dollar
cd384e0d59 0.54.0 2012-08-14 17:00:19 -04:00
David Dollar
8921cac35b use Foreman::Process to extract command running 2012-08-14 17:00:01 -04:00
Szymon Nowak
7d6de5b2a7 Handle multiline strings in .env file 2012-08-14 11:09:21 +02:00
Aditya Sanghi
cc4306492e run on reboot works with network-interface not network 2012-08-12 14:17:39 +05:30
David Dollar
6042783e82 Merge pull request #245 from brntbeer/env_bash_fix
changed to check env for bash
2012-08-08 14:09:19 -07:00
brntbeer
b7b3a9f898 changed to check env for bash 2012-08-08 13:53:36 -07:00
Indrek Juhkam
8b204db2f0 Use path and env variables in the inittab export 2012-07-31 21:44:31 +03:00
David Dollar
15643dcb3f changelog 2012-07-24 11:18:42 -04:00
David Dollar
5f0f2f5378 0.53.0 2012-07-24 11:18:20 -04:00
David Dollar
c1b57b59cf put app root in $HOME 2012-07-24 11:18:05 -04:00
David Dollar
21d53818f2 add changelog 2012-07-24 11:11:04 -04:00
David Dollar
359d6f1c34 0.52.0 2012-07-24 11:10:16 -04:00
David Dollar
584f251e4a wrap command in a runner that sources .profile.d scripts 2012-07-24 11:09:54 -04:00
David Dollar
7d9c2b2ac4 fix upstart export specs 2012-07-24 10:50:34 -04:00
David Dollar
fba4d9beff Merge pull request #229 from danielfarrell/autostart-upstart
Make upstart export start/stop with network
2012-07-18 13:55:01 -07:00
Daniel Farrell
0bde5fdab5 Make upstart export start/stop with network 2012-07-18 16:50:49 -04:00
Arnaud Lachaume
ebe160d425 fixed the directory option 2012-07-14 11:48:24 +10:00
David Dollar
1beab80c1f update docs 2012-07-11 18:33:13 -04:00
David Dollar
7b270f9f4a changelog 2012-07-11 18:33:10 -04:00
David Dollar
03e5342067 0.51.0 2012-07-11 18:32:51 -04:00
David Dollar
b1d57426fb dont try to colorize windows 2012-07-11 18:30:51 -04:00
David Dollar
06e5c52f35 add mswin32 dist 2012-07-11 18:30:38 -04:00
David Dollar
64ca839c0b update docs 2012-07-11 16:17:13 -04:00
David Dollar
80aebda023 update changelog 2012-07-11 16:17:10 -04:00
David Dollar
5dee2281a2 fix up release tasks 2012-07-11 16:16:29 -04:00
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
Daniel Farrell
0a61b1a62f Add capistrano export support 2012-06-22 15:49:19 -04: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
81 changed files with 1445 additions and 1225 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,9 @@ notifications:
on_failure: always
urls:
- http://dx-helper.herokuapp.com/travis
rvm:
- 1.9.2
- 1.9.3
- jruby
- ree

View File

@@ -1,3 +1,79 @@
## 0.57.0 (2012-08-21)
* fix startup checks for upstart exporter [Aditya Sanghi]
## 0.56.0 (2012-08-19)
* read .profile, not .profile.d [David Dollar]
## 0.55.0 (2012-08-14)
* use a forked process to exec a run with environment [David Dollar]
## 0.54.0 (2012-08-14)
* use Foreman::Process to extract command running [David Dollar]
* changed to check env for bash [brntbeer]
## 0.53.0 (2012-07-24)
* put app root in $HOME [David Dollar]
## 0.52.0 (2012-07-24)
* wrap command in a runner that sources .profile.d scripts [David Dollar]
* fix upstart export specs [David Dollar]
* Make upstart export start/stop with network [Daniel Farrell]
## 0.51.0 (2012-07-11)
* dont try to colorize windows [David Dollar]
## 0.50.0 (2012-07-11)
* handle windows [David Dollar]
## 0.49.0 (2012-07-11)
* 1.8 compatibility [David Dollar]
* use one pgroup for all of foreman and kill that since ruby 1.8 sucks at pgroups [David Dollar]
* better debugging [David Dollar]
## 0.48.0 (2012-07-10)
* allow old exporter format to work, but with deprecation warning [David Dollar]
* remove debugging code [David Dollar]
* Merge pull request #219 from MarkDBlackwell/patch-1 [David Dollar]
* Avoid crash by verifying the existence of SIGHUP before accessing it. [Mark D. Blackwell]
* allow color to be forced on [David Dollar]
* terminate gracefully if stdout goes away [David Dollar]
* always flush output [David Dollar]
* Merge pull request #212 from morgoth/added-version-command [David Dollar]
* added command for displaying foreman version [Wojciech Wnętrzak]
* Merge pull request #211 from morgoth/fixed-yaml-usage [David Dollar]
* fixed using YAML [Wojciech Wnętrzak]
* test on more things, but don't fail [David Dollar]
* changelog [David Dollar]
* 0.48.0.pre1 [David Dollar]
* foreman doesn't work on ruby 1.8, may try to fix later [David Dollar]
* use bash [David Dollar]
* massive refactoring for programmatic control and stability [David Dollar]
* Merge pull request #164 from hsume2/master [David Dollar]
* Only run tmux specs if tmux is installed [Henry Hsu]
* Do not assume BUNDLE_GEMFILE [Henry Hsu]
* Add support for starting procfile in tmux session [Henry Hsu]
## 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]

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.47.0)
foreman (0.59.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.16.0)
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

@@ -1,6 +1,6 @@
#!/bin/sh
#
#/ Usage: foreman-runner [-d <dir>] <command> [<args>...]
#/ Usage: foreman-runner [-d <dir>] [-p] <command> [<args>...]
#/
#/ Run a command with exec, optionally changing directory first
@@ -16,9 +16,12 @@ usage() {
exit
}
while getopts ":hd:" OPT; do
read_profile=""
while getopts ":hd:p" OPT; do
case $OPT in
d) cd "$OPTARG" ;;
p) read_profile="1" ;;
h) usage ;;
\?) error "invalid option: -$OPTARG" ;;
:) error "option -$OPTARG requires an argument" ;;
@@ -29,4 +32,10 @@ shift $((OPTIND-1))
[ -z "$1" ] && usage
if [ "$read_profile" = "1" ]; then
if [ -f .profile ]; then
. .profile
fi
fi
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

@@ -0,0 +1,2 @@
#!/bin/sh
export FOO=bar

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,25 +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 %>"<% 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_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

@@ -3,20 +3,31 @@
<plist version="1.0">
<dict>
<key>Label</key>
<string><%= "#{app}-#{process.name}-#{num}" %></string>
<string><%= "#{app}-#{name}-#{num}" %></string>
<key>EnvironmentVariables</key>
<dict>
<%- engine.env.merge("PORT" => port).each_pair do |var,env| -%>
<key><%= var.upcase %></key>
<string><%= env %></string>
<%- end -%>
</dict>
<key>ProgramArguments</key>
<array>
<string><%= process.command %></string>
<%- command_args.each do |command| -%>
<string><%= command %></string>
<%- end -%>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string><%= log %>/<%= app %>-<%= name %>-<%=num%>.log</string>
<key>StandardErrorPath</key>
<string><%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log</string>
<string><%= log %>/<%= app %>-<%= name %>-<%=num%>.log</string>
<key>UserName</key>
<string><%= user %></string>
<key>WorkingDirectory</key>
<string><%= engine.directory %></string>
<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,16 @@
pre-start script
bash << "EOF"
mkdir -p <%= log_root %>
chown -R <%= user %> <%= log_root %>
mkdir -p <%= log %>
chown -R <%= user %> <%= log %>
EOF
end script
start on (started network-interface
or started network-manager
or started networking)
stop on (stopping network-interface
or stopping network-manager
or stopping networking)

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 %>=<%= 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'

16
dist/mswin32.rake vendored Normal file
View File

@@ -0,0 +1,16 @@
file pkg("foreman-#{version}-x86-mswin32.gem") => distribution_files do |t|
Bundler.with_clean_env do
sh "env PLATFORM=mswin32 gem build foreman.gemspec"
end
sh "mv foreman-#{version}-x86-mswin32.gem #{t.name}"
end
task "mswin32:build" => pkg("foreman-#{version}-x86-mswin32.gem")
task "mswin32:clean" do
clean pkg("foreman-#{version}-x86-mswin32.gem")
end
task "mswin32:release" => "mswin32:build" do |t|
sh "gem push #{pkg("foreman-#{version}-x86-mswin32.gem")} || echo 'error'"
end

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

54
lib/foreman/capistrano.rb Normal file
View File

@@ -0,0 +1,54 @@
if defined?(Capistrano)
Capistrano::Configuration.instance(:must_exist).load do
namespace :foreman do
desc <<-DESC
Export the Procfile to upstart. Will use sudo if available.
You can override any of these defaults by setting the variables shown below.
set :foreman_format, "upstart"
set :foreman_location, "/etc/init"
set :foreman_procfile, "Procfile"
set :foreman_app, application
set :foreman_user, user
set :foreman_log, "#{shared_path}/log"
set :foreman_concurrency, false
DESC
task :export, :roles => :app do
bundle_cmd = fetch(:bundle_cmd, "bundle")
foreman_format = fetch(:foreman_format, "upstart")
foreman_location = fetch(:foreman_location, "/etc/init")
foreman_procfile = fetch(:foreman_procfile, "Procfile")
foreman_app = fetch(:foreman_app, application)
foreman_user = fetch(:foreman_user, user)
foreman_log = fetch(:foreman_log, "#{shared_path}/log")
foreman_concurrency = fetch(:foreman_concurrency, false)
args = ["#{foreman_format} #{foreman_location}"]
args << "-f #{foreman_procfile}"
args << "-a #{foreman_app}"
args << "-u #{foreman_user}"
args << "-l #{foreman_log}"
args << "-c #{foreman_concurrency}" if foreman_concurrency
run "cd #{release_path} && #{sudo} #{bundle_cmd} exec foreman export #{args.join(' ')}"
end
desc "Start the application services"
task :start, :roles => :app do
run "#{sudo} start #{application}"
end
desc "Stop the application services"
task :stop, :roles => :app do
run "#{sudo} stop #{application}"
end
desc "Restart the application services"
task :restart, :roles => :app do
run "#{sudo} start #{application} || #{sudo} restart #{application}"
end
end
end
end

View File

@@ -1,36 +1,42 @@
require "foreman"
require "foreman/helpers"
require "foreman/engine"
require "foreman/engine/cli"
require "foreman/export"
require "foreman/version"
require "shellwords"
require "thor"
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
@@ -46,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
@@ -56,50 +64,82 @@ 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 [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!
begin
exec args.shelljoin
rescue Errno::EACCES
error "not executable: #{args.first}"
rescue Errno::ENOENT
error "command not found: #{args.first}"
load_environment!
pid = fork do
begin
engine.env.each { |k,v| ENV[k] = v }
exec args.shelljoin
rescue Errno::EACCES
error "not executable: #{args.first}"
rescue Errno::ENOENT
error "command not found: #{args.first}"
end
end
Process.wait(pid)
exit $?.exitstatus
end
desc "version", "Display Foreman gem version"
def version
puts Foreman::VERSION
end
no_tasks do
def engine
@engine ||= begin
engine_class = Foreman::Engine::CLI
engine = engine_class.new(options)
engine
end
end
end
private ######################################################################
def check_procfile!
error("#{procfile} does not exist.") unless File.exist?(procfile)
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"
end
end
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[: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,126 +9,274 @@ 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) if File.exists?(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 }
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] || environment["PORT"] || ENV["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
@@ -140,89 +287,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

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

@@ -0,0 +1,104 @@
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 false 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" unless Foreman.windows?
Color.enable($stdout, options[: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

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

@@ -0,0 +1,29 @@
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
# Remove single quotes
when /\A'(.*)'\z/ then ax[key] = $1
# Remove double quotes and unescape string preserving newline characters
when /\A"(.*)"\z/ then ax[key] = $1.gsub('\n', "\n").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,7 +25,6 @@ module Foreman::Export
end
require "foreman/export/base"
require "foreman/export/inittab"
require "foreman/export/upstart"
@@ -32,4 +32,3 @@ 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,27 +105,42 @@ 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), nil, '-').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
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

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,28 @@ 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)
commands = []
commands << "cd #{engine.root}"
commands << "export PORT=#{port}"
engine.env.each_pair do |var, env|
commands << "export #{var.upcase}=#{shell_quote(env)}"
end
commands << "#{process.command} >> #{log}/#{name}-#{num}.log 2>&1"
inittab << "#{id}:4:respawn:/bin/su - #{user} -c '#{commands.join(";")}'"
index += 1
end
index
end
inittab << "# ----- end foreman #{app} processes -----"
@@ -27,9 +34,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

@@ -4,24 +4,14 @@ require "foreman/export"
class Foreman::Export::Launchd < Foreman::Export::Base
def export
error("Must specify a location") unless location
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
template_root = self.template
FileUtils.mkdir_p(location)
engine.procfile.entries.each do |process|
1.upto(self.concurrency[process.name]) do |num|
master_template = export_template("launchd", "launchd.plist.erb", template_root)
master_config = ERB.new(master_template).result(binding)
write_file "#{location}/#{app}-#{process.name}-#{num}.plist", master_config
super
engine.each_process do |name, process|
1.upto(engine.formation[name]) do |num|
port = engine.port_for(process, num)
command_args = process.command.split(" ")
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,99 @@ 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?
require "posix/spawn"
wrapped_command = "#{Foreman.runner} -d '#{cwd}' -p -- #{command}"
POSIX::Spawn.spawn env, wrapped_command, :out => output, :err => output
elsif Foreman.ruby_18?
fork do
$stdout.reopen output
$stderr.reopen output
env.each { |k,v| ENV[k] = v }
wrapped_command = "#{Foreman.runner} -d '#{cwd}' -p -- #{command}"
exec wrapped_command
end
else
wrapped_command = "#{Foreman.runner} -d '#{cwd}' -p -- #{command}"
Process.spawn env, wrapped_command, :out => output, :err => output
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.shellsplit
end
end
[ reader, pid ]
# Returns the working directory for this +Process+
#
# @returns [String]
#
def cwd
File.expand_path(@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,56 +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
# Initialize a Procfile
#
# @param [String] filename (nil) An optional filename to read from
#
def initialize(filename=nil)
@entries = []
load(filename) if filename
end
def [](name)
entries.detect { |entry| entry.name == name }
end
def process_names
entries.map(&:name)
end
def load(filename)
entries.clear
parse_procfile(filename)
end
def write(filename)
File.open(filename, 'w') do |io|
entries.each do |ent|
io.puts(ent)
end
# Yield each +Procfile+ entry in order
#
def entries(&blk)
@entries.each do |(name, command)|
yield name, command
end
end
def <<(entry)
entries << Foreman::ProcfileEntry.new(*entry)
self
# Retrieve a +Procfile+ command by name
#
# @param [String] name The name of the Procfile entry to retrieve
#
def [](name)
@entries.detect { |n,c| name == n }.last
end
# 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
protected
# 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
def parse_procfile(filename)
# 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(filename)
File.read(filename).split("\n").map do |line|
if line =~ /^([A-Za-z0-9_]+):\s*(.+)$/
self << [ $1, $2 ]
[$1, $2]
end
end.compact
end

View File

@@ -1,26 +0,0 @@
require "foreman"
class Foreman::ProcfileEntry
attr_reader :name
attr_reader :command
attr_accessor :color
def initialize(name, command)
@name = name
@command = command
end
def spawn(num, pipe, basedir, environment, base_port)
(1..num).to_a.map do |n|
process = Foreman::Process.new(self, n, base_port + (n-1))
process.run(pipe, basedir, environment)
process
end
end
def to_s
"#{name}: #{command}"
end
end

View File

@@ -1,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.47.0"
VERSION = "0.59.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.46.0" "Foreman Manual"
.TH "FOREMAN" "1" "July 2012" "Foreman 0.57.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,65 @@ 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", "foo bar"] }
it "includes the environment" do
forked_foreman("run #{resource_path("bin/env FOO")} -e #{resource_path(".env")}").should == "bar\n"
end
before(:each) do
stub(subject).exec
end
it "exits with the same exit code as the command" do
fork_and_get_exitstatus("run echo 1").should == 0
fork_and_get_exitstatus("run date 'invalid_date'").should == 1
end
end
it "should load the environment file" do
write_env
preserving_env do
subject.run *command
ENV["FOO"].should == "bar"
end
describe "version" do
it "displays gem version" do
foreman("version").chomp.should == Foreman::VERSION
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
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,14 +1,25 @@
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
@@ -16,65 +27,53 @@ describe "Foreman::Engine", :fakefs 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
@@ -84,55 +83,30 @@ 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 handle multiline strings" do
File.open("/tmp/env", "w") do |f|
f.puts 'FOO="bar\nbaz"'
end
subject.load_env "/tmp/env"
subject.env["FOO"].should == "bar\nbaz"
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")
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
lambda { subject.load_env "/tmp/env" }.should raise_error(Errno::ENOENT)
end
it "should set port from .env if specified" do
File.open(".env", "w") { |f| f.puts("PORT=8017") }
engine = Foreman::Engine.new("Procfile")
engine.send(:base_port).should == "8017"
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
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

@@ -5,20 +5,27 @@ require "tmpdir"
describe Foreman::Export::Launchd, :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(:launchd) { Foreman::Export::Launchd.new("/tmp/init", engine, options) }
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
normalize_space(File.read("/tmp/init/app-alpha-1.plist")).should == normalize_space(example_export_file("launchd/launchd-a.default"))
normalize_space(File.read("/tmp/init/app-bravo-1.plist")).should == normalize_space(example_export_file("launchd/launchd-b.default"))
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
context "with multiple command arguments" do
let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile", "charlie") }
it "splits each command argument" do
launchd.export
File.read("/tmp/init/app-alpha-1.plist").should == example_export_file("launchd/launchd-c.default")
end
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 }
@@ -34,13 +35,14 @@ describe Foreman::Export::Upstart, :fakefs do
end
it "quotes and escapes environment variables" do
engine.environment['KEY'] = 'd"\|d'
engine.env['KEY'] = 'd"\|d'
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
context "with concurrency" do
let(:options) { Hash[:concurrency => "alpha=2"] }
context "with a formation" do
let(:formation) { "alpha=2" }
it "exports to the filesystem with concurrency" do
upstart.export
@@ -54,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,141 +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 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
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

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

View File

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

@@ -1,4 +1,4 @@
# ----- foreman app processes -----
AP01:4:respawn:/bin/su - app -c 'PORT=5000 ./alpha >> /var/log/app/alpha-1.log 2>&1'
AP02:4:respawn:/bin/su - app -c 'PORT=5001 ./alpha >> /var/log/app/alpha-2.log 2>&1'
AP01:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5000;./alpha >> /var/log/app/alpha-1.log 2>&1'
AP02:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5001;./alpha >> /var/log/app/alpha-2.log 2>&1'
# ----- end foreman app processes -----

View File

@@ -1,4 +1,4 @@
# ----- foreman app processes -----
AP01:4:respawn:/bin/su - app -c 'PORT=5000 ./alpha >> /var/log/app/alpha-1.log 2>&1'
AP02:4:respawn:/bin/su - app -c 'PORT=5100 ./bravo >> /var/log/app/bravo-1.log 2>&1'
AP01:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5000;./alpha >> /var/log/app/alpha-1.log 2>&1'
AP02:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5100;./bravo >> /var/log/app/bravo-1.log 2>&1'
# ----- end foreman app processes -----

View File

@@ -4,6 +4,11 @@
<dict>
<key>Label</key>
<string>app-alpha-1</string>
<key>EnvironmentVariables</key>
<dict>
<key>PORT</key>
<string>5000</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>./alpha</string>
@@ -12,6 +17,8 @@
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/app/app-alpha-1.log</string>
<key>StandardErrorPath</key>
<string>/var/log/app/app-alpha-1.log</string>
<key>UserName</key>

View File

@@ -4,6 +4,11 @@
<dict>
<key>Label</key>
<string>app-bravo-1</string>
<key>EnvironmentVariables</key>
<dict>
<key>PORT</key>
<string>5100</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>./bravo</string>
@@ -12,6 +17,8 @@
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/app/app-bravo-1.log</string>
<key>StandardErrorPath</key>
<string>/var/log/app/app-bravo-1.log</string>
<key>UserName</key>

View File

@@ -0,0 +1,30 @@
<?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>EnvironmentVariables</key>
<dict>
<key>PORT</key>
<string>5000</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>./alpha</string>
<string>charlie</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/app/app-alpha-1.log</string>
<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,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,3 +6,11 @@ bash << "EOF"
EOF
end script
start on (started network-interface
or started network-manager
or started networking)
stop on (stopping network-interface
or stopping network-manager
or stopping networking)

View File

@@ -6,6 +6,7 @@ SimpleCov.start do
end
require "rspec"
require "timecop"
require "fakefs/safe"
require "fakefs/spec_helpers"
@@ -23,7 +24,44 @@ 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 fork_and_get_exitstatus(args)
pid = Process.spawn("bundle exec bin/foreman #{args}", :out => "/dev/null", :err => "/dev/null")
Process.wait(pid)
$?.exitstatus
end
def mock_exit(&block)
@@ -55,13 +93,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 +139,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

@@ -32,10 +32,10 @@ def latest_release
end
def newer_release
tags = %x{ git tag --contains v#{latest_release} }.split("\n").sort_by do |tag|
tags = %x{ git tag --contains v#{latest_release} | grep -v pre }.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