Compare commits

...

140 Commits

Author SHA1 Message Date
David Dollar
9b704ad852 0.48.0.pre2 2012-06-17 22:02:14 -04:00
David Dollar
43428175c3 allow color to be forced on 2012-06-17 22:02:00 -04:00
David Dollar
87f65e489c terminate gracefully if stdout goes away 2012-06-17 21:55:28 -04:00
David Dollar
e5521f2cbb always flush output 2012-06-17 21:55:19 -04:00
David Dollar
8b18143281 test on more things, but don't fail 2012-06-11 13:34:08 -04:00
David Dollar
e06886ed57 changelog 2012-06-11 12:34:16 -04:00
David Dollar
d6a00d7262 0.48.0.pre1 2012-06-11 12:27:20 -04:00
David Dollar
d9d1346640 foreman doesn't work on ruby 1.8, may try to fix later 2012-06-11 10:31:07 -04:00
David Dollar
0df1a4d784 use bash 2012-06-10 23:12:44 -04:00
David Dollar
51a704939e massive refactoring for programmatic control and stability 2012-06-10 22:58:09 -04:00
David Dollar
f41cc552c7 Merge pull request #164 from hsume2/master
Add support for running procfile in tmux session
2012-06-07 22:29:36 -07:00
Henry Hsu
065bbf1cd8 Only run tmux specs if tmux is installed 2012-06-07 22:14:47 -07:00
Henry Hsu
b628ddc608 Do not assume BUNDLE_GEMFILE 2012-06-07 22:12:52 -07:00
Henry Hsu
f745b16217 Add support for starting procfile in tmux session 2012-06-07 22:12:52 -07:00
David Dollar
a87a882e60 0.47.0 2012-06-07 22:49:51 -04:00
David Dollar
6ba9252d0f Merge pull request #165 from elf-pavlik/master
list of ports to other languages added to README
2012-06-07 19:45:11 -07:00
David Dollar
efd9e2119b Merge pull request #173 from Viximo/feature/escape-env
Quote and escape environment variables in upstart templates
2012-06-07 19:43:20 -07:00
David Dollar
bf9bdbf118 Merge pull request #194 from maxpow4h/master
Added launchd to exporters
2012-06-07 19:41:23 -07:00
David Dollar
929a138e54 Merge pull request #195 from aneeth/patch-1
Updated data/export/bluepill/master.pill.erb
2012-06-07 19:41:09 -07:00
David Dollar
d6514b4f33 Merge pull request #201 from sos4nt/patch-1
Terminate gracefully upon SIGHUP
2012-06-07 19:40:38 -07:00
David Dollar
8696a36833 Merge pull request #208 from dbrock/master
Fix multi-word argument handling in `foreman run`.
2012-06-06 07:48:44 -07:00
Daniel Brockman
3ea5de42aa Fix multi-word argument handling in foreman run. 2012-06-06 15:36:59 +02:00
David Dollar
4a13122082 Merge pull request #199 from atog/master
set port from .env if specified
2012-05-16 15:06:56 -07:00
Koen Van der Auwera
9b2987c3f0 make 'PORT=5000 foreman start' work 2012-05-16 23:59:09 +02:00
Stefan Schüßler
6274f99225 Terminate gracefully upon SIGHUP
Tmux sends SIGHUP when a session is killed which can result in orphaned processes. Adding a SIGHUP handler terminates the processes as expected.
2012-05-14 13:04:46 +03:00
Koen Van der Auwera
0b0324fed9 set port from .env if specified 2012-05-11 14:17:03 +02:00
Aneeth
32db70b778 Updated data/export/bluepill/master.pill.erb to read in the environment variables from the foreman .env file and reflect it in the pill file 2012-05-04 15:53:16 +08:00
Maxwell Swadling
3db1ad6fbc Added launchd exporter 2012-05-04 15:21:16 +10:00
David Dollar
003b466a17 Merge pull request #193 from pat2man/patch-1
Add stop_grace_time to bluepill config
2012-05-03 12:16:23 -07:00
Patrick Tescher
2a896a0fb5 Add stop_grace_time to bluepill config. Fixes this error message:
Config Error: Stop_grace_time should be greater than the sum of stop_signals delays!

Changed bluepill spec example files to include stop_grace_time
2012-05-03 12:11:24 -07:00
David Dollar
6a8c81a38b Revert "Merge pull request #192 from pat2man/patch-1"
This reverts commit a83dab363e, reversing
changes made to e0fe5baf1b.
2012-05-03 14:53:31 -04:00
David Dollar
a83dab363e Merge pull request #192 from pat2man/patch-1
Add stop_grace_time to bluepill config
2012-05-03 11:52:02 -07:00
Patrick Tescher
53e0f4ecf9 Add stop_grace_time to bluepill config. Fixes this error message:
Config Error: Stop_grace_time should be greater than the sum of stop_signals delays!
2012-05-03 11:50:29 -07:00
David Dollar
e0fe5baf1b update docs 2012-05-02 13:21:02 -04:00
David Dollar
1aa1f15b8f update changelog 2012-05-02 13:20:54 -04:00
David Dollar
fa46a605bb 0.46.0 2012-05-02 13:19:11 -04:00
David Dollar
3077857ab7 Merge pull request #191 from ged/profile_load_write_append
Add Procfile load/write/append API
2012-05-02 10:03:31 -07:00
David Dollar
08aa8f9d5d Merge pull request #190 from ged/remove_obsolete_spec
Remove spec obsoleted by 91a8704
2012-05-02 10:01:30 -07:00
Michael Granger
771489dec9 Remove spec obsoleted by 91a8704 2012-05-02 09:37:32 -07:00
Michael Granger
053ae8f0be Add Profile load/write/append API 2012-05-02 09:30:57 -07:00
David Dollar
af58af1c60 Merge pull request #187 from bkaney/master
Guard against missing Procfile in engine.rb
2012-04-29 21:00:58 -07:00
David Dollar
9075d93370 update docs 2012-04-26 17:50:03 -04:00
David Dollar
0c2e9df722 changelog 2012-04-26 17:50:00 -04:00
David Dollar
047f1ff5c4 0.45.0 2012-04-26 17:49:35 -04:00
David Dollar
26b54a62c5 Merge pull request #188 from technomancy/upstart-log-dir
Create and chown log dir in upstart export.
2012-04-24 17:50:42 -07:00
Phil Hagelberg
7b85ad7c1a Create and chown log dir in upstart export. 2012-04-24 17:24:53 -07:00
Brian Kaney
91a87049db Guard against missing Procfile in engine.rb
Should be able to 'foreman run <task>' without a Procfile.  This seems true to the orig. intent as the cli does not directly check with 'run' (as it  does for 'start').
2012-04-24 15:51:14 -04:00
David Dollar
f46408e8be remove parka from dist files 2012-04-23 16:00:27 -04:00
David Dollar
93f04e42ac update docs 2012-04-23 15:57:06 -04:00
David Dollar
97c4582acc changelog 2012-04-23 15:57:00 -04:00
David Dollar
5689d75a87 0.44.0 2012-04-23 15:55:56 -04:00
David Dollar
1325b6750e make var output order repeatable 2012-04-23 15:53:59 -04:00
David Dollar
638005403f clean up file/directory interaction, add some tests 2012-04-23 15:50:23 -04:00
David Dollar
55f274532f Merge pull request #183 from technomancy/app_root
Look for .env and app_root in the same dir as the Procfile.
2012-04-23 12:20:10 -07:00
Phil Hagelberg
66f76c2036 Look for .env and app_root in the same dir as the Procfile. 2012-04-20 20:59:37 -07:00
David Dollar
865cabb525 update docs 2012-04-20 19:33:14 -04:00
David Dollar
7c3c4bc58f add more options to the start docs 2012-04-20 19:33:11 -04:00
David Dollar
2dbe8c733b update docs 2012-04-20 17:42:47 -04:00
David Dollar
84352b82cc changelog 2012-04-20 17:42:33 -04:00
David Dollar
383c1f87af 0.43.0 2012-04-20 17:41:08 -04:00
David Dollar
ce3003b026 factor out wrap_environment 2012-04-20 17:40:53 -04:00
David Dollar
e06f4b2f9e Merge pull request #182 from leahpar/master
Supervisord Export fix for environments containing commas
2012-04-20 14:36:26 -07:00
Raphael Randschau
b721fd894e per default wrap every environment value in quotes 2012-04-20 21:15:43 +02:00
Raphael Randschau
b8ea6fd4b3 fix typo in spec description 2012-04-20 09:14:12 +02:00
Raphael Randschau
dbda63263b fix supervisord export for environments containing commas 2012-04-20 09:11:25 +02:00
David Dollar
93cdc31be0 update docs 2012-04-18 13:04:45 -04:00
David Dollar
4dfbe46690 update changelog 2012-04-18 13:03:58 -04:00
David Dollar
0033f9caeb 0.42.0 2012-04-18 13:02:51 -04:00
David Dollar
74839800a9 Merge pull request #181 from technomancy/master
Public read_environment
2012-04-18 10:01:38 -07:00
Phil Hagelberg
b75337e21e Move read_environment to a public class method. 2012-04-18 09:56:17 -07:00
David Dollar
d94f941189 update gemfile.lock 2012-04-18 12:49:33 -04:00
David Dollar
0b34f067cb Revert "Merge pull request #176 from rtyler/feature/156-foreman-stop-command"
Rewriting using a better Process API

This reverts commit dbe51832b0, reversing
changes made to 69216b4c5e.
2012-04-18 12:48:51 -04:00
David Dollar
dbe51832b0 Merge pull request #176 from rtyler/feature/156-foreman-stop-command
Adding #stop method to Foreman::Engine
2012-04-11 10:29:54 -07:00
R. Tyler Croy
3a2a53be95 Remove ALL_PROCESSES and default the name arguments to #start/#stop to nil 2012-04-11 10:25:58 -07:00
R. Tyler Croy
b2bf95479e Refactor #stop to reference ALL_PROCESSES for a bit clearer readability 2012-04-10 20:35:59 -07:00
R. Tyler Croy
48f764e347 Refactor #spawn_processes into #start(name)
When passed nil (aka ALL_PROCESSES) #start will start all processes in the
Procfile as per existing behavior
2012-04-10 20:35:59 -07:00
R. Tyler Croy
de62d0655e Re-name the main Foreman::Engine method to #run to avoid a name collision with #start(name) 2012-04-10 20:35:59 -07:00
R. Tyler Croy
38aecff886 When executing #stop(nil), all processes should be sent the signal 2012-04-10 20:35:58 -07:00
R. Tyler Croy
e4a3215257 Re-implement #terminate_gracefully with #stop(name) 2012-04-10 20:19:23 -07:00
R. Tyler Croy
c705b5fbef Add #stop method on Foreman::Engine for stopping certain named processes
This will make embedding foreman "nicer" since the embedder can then stop
a specific process (e.g. turning off a service for an integration fail-over test)
2012-04-10 20:10:51 -07:00
Matt Griffin
2fcb64959b Quote and escape environment variables in upstart templates 2012-04-03 16:36:18 -04:00
David Dollar
69216b4c5e Merge pull request #171 from technomancy/0.41.0-deparkaed
Drop parka dependency to make things easier on the build slaves.
2012-03-26 15:05:35 -07:00
Phil Hagelberg
5d2930745a Drop parka dependency to make things easier on the build slaves. 2012-03-26 15:01:04 -07:00
elf Pavlik
a5465bf55e added list of ports to other languages to README 2012-03-18 09:35:51 +01:00
David Dollar
8fc3d1ef24 Merge pull request #161 from leahpar/master
Supervisord Support (Update & Bugfix)
2012-03-16 18:47:15 -07:00
David Dollar
d33e4fb0ed update changelog 2012-03-16 16:36:16 -04:00
David Dollar
39b48b566f 0.41.0 2012-03-16 16:34:32 -04:00
David Dollar
2f982ff9c7 replace term-ansicolor with built-in colorization 2012-03-16 16:34:14 -04:00
Raphael Randschau
1217ef1b56 fix typo 2012-03-05 15:51:10 +01:00
Raphael Randschau
c9943d70ec add group support for supervisord 2012-03-05 15:48:40 +01:00
Raphael Randschau
08dca57eb4 fix enviroment export 2012-02-25 17:24:27 +01:00
David Dollar
bb3377407a Merge pull request #160 from leahpar/master
supervisord support
2012-02-24 11:49:55 -08:00
Raphael Randschau
084b9493d1 revert bundle update 2012-02-24 20:31:07 +01:00
Raphael Randschau
279a251c78 fix whitespaces 2012-02-24 20:29:16 +01:00
Raphael Randschau
a49ef286e8 all specs are passing 2012-02-24 20:18:38 +01:00
Raphael Randschau
0b3da59947 passing first export 2012-02-24 19:52:18 +01:00
David Dollar
1f725dd68a let github show authors 2012-02-24 11:34:34 -05:00
David Dollar
2272f76479 authors 2012-02-24 11:33:33 -05:00
David Dollar
cf4762071c changelog 2012-02-24 11:33:18 -05:00
David Dollar
d86b0bed1f 0.40.0 2012-02-24 11:32:00 -05:00
David Dollar
12f825204b support various quoting styles in .env 2012-02-24 11:31:21 -05:00
David Dollar
62c9d1db45 remove load_env! as it's made unnecessary by foreman run 2012-02-24 11:31:21 -05:00
Raphael Randschau
1127551369 rename supervisord base template to app
add supervisord stub
2012-02-23 17:24:08 +01:00
Raphael Randschau
8cb58b8517 require supervisord export 2012-02-23 17:11:19 +01:00
Raphael Randschau
d03e931b67 add supervisord export engine 2012-02-23 17:10:57 +01:00
Raphael Randschau
9ba2b32b36 add supervisord base template 2012-02-23 17:02:59 +01:00
David Dollar
cf5689a77a Merge pull request #155 from rtyler/bug/152-check-exception-no-procfile
Provide a useful error if `foreman check` fails to find a Procfile
2012-02-21 17:40:20 -08:00
R. Tyler Croy
c23dbb79af Provide a useful error if foreman check fails to find a Procfile
Fixes #152
2012-02-21 17:27:15 -08:00
David Dollar
7e55d8d3e2 update docs 2012-02-21 11:58:57 -05:00
David Dollar
d4f23d45a4 add run to docs 2012-02-21 11:58:52 -05:00
David Dollar
93fa1645e7 update docs 2012-02-21 11:50:11 -05:00
David Dollar
7bdada4a10 update docs 2012-02-21 11:49:57 -05:00
David Dollar
2b47d24ab7 Travis doesn't support cext. Revert "try java again"
This reverts commit 24695348fb.
2012-02-09 19:16:14 -05:00
David Dollar
24695348fb try java again 2012-02-09 19:12:11 -05:00
David Dollar
38b6482af5 changelog 2012-02-07 11:19:12 -05:00
David Dollar
501bc138c5 authors 2012-02-07 11:18:06 -05:00
David Dollar
df8c05cd6c 0.39.0 2012-02-07 11:17:59 -05:00
David Dollar
edcc4f3567 move to foreman-runner 2012-02-07 11:17:44 -05:00
David Dollar
3759dbb463 fix tgz release 2012-02-07 11:16:07 -05:00
David Dollar
b673931c05 Double Revert "Revert "bundle update hpricot""
This reverts commit 137e43b040.
2012-02-04 19:47:48 -05:00
David Dollar
137e43b040 Revert "bundle update hpricot"
This reverts commit 16d4b84a5d.
2012-02-04 19:25:01 -05:00
David Dollar
79211d9bbf Merge pull request #147 from jfirebaugh/rubinius
bundle update hpricot
2012-02-04 14:05:06 -08:00
John Firebaugh
16d4b84a5d bundle update hpricot
0.8.2 doesn't compile on Rubinius; 0.8.6 does.
2012-02-04 13:46:48 -08:00
David Dollar
5ea4537046 remove version from inner pkg 2012-02-02 21:31:58 -05:00
David Dollar
d51433ff82 fix foreman pkg builder 2012-02-02 21:08:01 -05:00
David Dollar
54ab74d305 fix release tasks 2012-02-02 17:32:52 -05:00
David Dollar
e8b8f34f41 changelog 2012-02-02 17:27:56 -05:00
David Dollar
7535f1d3d8 0.38.0 2012-02-02 17:26:42 -05:00
David Dollar
3af0dfb4ae bring back single process starting 2012-02-02 17:26:25 -05:00
David Dollar
78547b8175 foreman + jruby + travis = nope 2012-02-01 13:24:43 -05:00
David Dollar
976fbc0bb0 stub out proctitle/termtitle 2012-02-01 10:19:33 -05:00
David Dollar
28a9aa774f try ci on jruby 2012-02-01 10:06:25 -05:00
David Dollar
51f5ff3842 latest jruby fixed non-excutable to raise EACCESS 2012-01-31 18:01:53 -05:00
David Dollar
e1e18f62bb remove jruby until we figure out what's going on with travis 2012-01-30 11:42:41 -05:00
David Dollar
0a09117328 set JRUBY_OPTS for travis 2012-01-29 23:02:48 -05:00
David Dollar
c3df12746f ignore .rbenv-version 2012-01-29 23:02:40 -05:00
David Dollar
2ec6a23fb3 force to binary encoding if supported 2012-01-29 23:02:20 -05:00
David Dollar
0d6b784de1 disable test in jruby 2012-01-29 23:02:10 -05:00
David Dollar
2dcd2c03db wait for process termination instead of sleeping 2012-01-29 23:02:03 -05:00
David Dollar
9d6d0bbb7d add bundler setup to rakefile 2012-01-29 22:45:50 -05:00
80 changed files with 1411 additions and 925 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,12 +1,11 @@
script: bundle exec rake spec script: bundle exec rake spec
env: JRUBY_OPTS="--debug -X+O" matrix:
allow_failures:
rvm: - rvm: 1.8.7
- 1.8.7 - rvm: jruby
- 1.9.2 - rvm: rbx
- 1.9.3 - rvm: ree
- jruby-head
notifications: notifications:
email: false email: false
@@ -15,3 +14,11 @@ notifications:
on_failure: always on_failure: always
urls: urls:
- http://dx-helper.herokuapp.com/travis - http://dx-helper.herokuapp.com/travis
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
- jruby
- rbx
- ree

View File

@@ -1,3 +1,73 @@
## 0.48.0.pre1 (2012-06-11)
* Massive refactoring for programmatic control and stability [David Dollar]
* Procfile commands with shell interpolations now work again [David Dollar]
* Stop trying to test on Ruby 1.8 [David Dollar]
## 0.47.0 (2012-06-07)
* Fix multi-word argument handling in `foreman run`. [Daniel Brockman]
* Make 'PORT=5000 foreman start' work [Koen Van der Auwera]
* Terminate gracefully upon SIGHUP [Stefan Schüßler]
* Set port from .env if specified [Koen Van der Auwera]
* Updated bluepill exporter to use environment variables from .env [Aneeth]
* Added launchd exporter [Maxwell Swadling]
* Quote and escape environment variables in upstart templates [Matt Griffin]
* Added list of ports to other languages to README [elf Pavlik]
## 0.46.0 (2012-05-02)
* Add Profile load/write/append API [Michael Granger]
* Guard against missing Procfile in engine.rb [Brian Kaney]
## 0.45.0 (2012-04-26)
* create and chown log dir in upstart export. [Phil Hagelberg]
* remove parka from dist files [David Dollar]
## 0.44.0 (2012-04-23)
* make var output order repeatable in supervisord export [David Dollar]
* make --procfile and --app-root influence each other in a more intuitive way [David Dollar]
* Look for .env and app_root in the same dir as the Procfile. [Phil Hagelberg]
## 0.43.0 (2012-04-20)
* wrap supervisord env vars in quotes [Raphael Randschau]
## 0.42.0 (2012-04-18)
* Move read_environment to a public class method. [Phil Hagelberg]
* Drop parka dependency [Phil Hagelberg]
* add group support for supervisord [Raphael Randschau]
* fix enviroment export [Raphael Randschau]
## 0.41.0 (2012-03-16)
* replace term-ansicolor with built-in colorization [David Dollar]
* supervisord export template [Raphael Randschau]
## 0.40.0 (2012-02-24)
* support various quoting styles in .env [David Dollar]
* remove load_env! as it's made unnecessary by foreman run [David Dollar]
* Provide a useful error if `foreman check` fails to find a Procfile [R. Tyler Croy]
* update docs [David Dollar]
## 0.39.0 (2012-02-07)
* rename bin/runner to bin/foreman-runner [David Dollar]
* fix tgz release [David Dollar]
* bundle update hpricot [John Firebaugh]
* touch up .pkg release tasks [David Dollar]
## 0.38.0 (2012-02-02)
* bring back single process starting [David Dollar]
* more attempts at getting ci working with jruby [David Dollar]
* ignore .rbenv-version [David Dollar]
* force to binary encoding if supported [David Dollar]
## 0.37.2 (2012-01-29) ## 0.37.2 (2012-01-29)
* handle directories with spaces in runner [David Dollar] * handle directories with spaces in runner [David Dollar]

View File

@@ -11,11 +11,13 @@ platform :jruby do
end end
group :development do group :development do
gem 'parka' gem 'aws-s3'
gem 'rake' gem 'rake'
gem 'ronn' gem 'ronn'
gem 'fakefs', '~> 0.3.2' gem 'fakefs', '~> 0.3.2'
gem 'rr', '~> 1.0.2' gem 'rr', '~> 1.0.2'
gem 'rspec', '~> 2.0' gem 'rspec', '~> 2.0'
gem "simplecov", :require => false gem "simplecov", :require => false
gem 'timecop'
gem 'yard'
end end

View File

@@ -1,30 +1,27 @@
PATH PATH
remote: . remote: .
specs: specs:
foreman (0.37.2) foreman (0.48.0.pre2)
term-ansicolor (~> 1.0.7)
thor (>= 0.13.6) thor (>= 0.13.6)
GEM GEM
remote: http://rubygems.org/ remote: http://rubygems.org/
specs: specs:
crack (0.1.8) aws-s3 (0.6.2)
builder
mime-types
xml-simple
builder (3.0.0)
diff-lcs (1.1.3) diff-lcs (1.1.3)
fakefs (0.3.2) fakefs (0.3.2)
hpricot (0.8.2) hpricot (0.8.6)
hpricot (0.8.2-java) hpricot (0.8.6-java)
mime-types (1.16) mime-types (1.16)
multi_json (1.0.4) multi_json (1.0.4)
mustache (0.11.2) mustache (0.11.2)
parka (0.6.2)
crack
rest-client
thor
posix-spawn (0.3.6) posix-spawn (0.3.6)
rake (0.9.2.2) rake (0.9.2.2)
rdiscount (1.6.5) rdiscount (1.6.5)
rest-client (1.6.1)
mime-types (>= 1.16)
ronn (0.7.3) ronn (0.7.3)
hpricot (>= 0.8.2) hpricot (>= 0.8.2)
mustache (>= 0.7.0) mustache (>= 0.7.0)
@@ -42,9 +39,11 @@ GEM
multi_json (~> 1.0.3) multi_json (~> 1.0.3)
simplecov-html (~> 0.5.3) simplecov-html (~> 0.5.3)
simplecov-html (0.5.3) simplecov-html (0.5.3)
term-ansicolor (1.0.7) thor (0.15.2)
thor (0.14.6) timecop (0.3.5)
win32console (1.3.0-x86-mingw32) win32console (1.3.0-x86-mingw32)
xml-simple (1.0.15)
yard (0.8.2)
PLATFORMS PLATFORMS
java java
@@ -52,13 +51,15 @@ PLATFORMS
x86-mingw32 x86-mingw32
DEPENDENCIES DEPENDENCIES
aws-s3
fakefs (~> 0.3.2) fakefs (~> 0.3.2)
foreman! foreman!
parka
posix-spawn (~> 0.3.6) posix-spawn (~> 0.3.6)
rake rake
ronn ronn
rr (~> 1.0.2) rr (~> 1.0.2)
rspec (~> 2.0) rspec (~> 2.0)
simplecov simplecov
timecop
win32console (~> 1.3.0) win32console (~> 1.3.0)
yard

View File

@@ -27,13 +27,19 @@ Manage Procfile-based applications
* [wiki](http://github.com/ddollar/foreman/wiki) * [wiki](http://github.com/ddollar/foreman/wiki)
* [changelog](https://github.com/ddollar/foreman/blob/master/Changelog.md) * [changelog](https://github.com/ddollar/foreman/blob/master/Changelog.md)
## Ports
* [shoreman](https://github.com/hecticjeff/shoreman) - shell
* [honcho](https://github.com/nickstenning/honcho) - python
* [norman](https://github.com/josh/norman) - node.js
## Authors ## Authors
#### Created and maintained by #### Created and maintained by
David Dollar David Dollar
#### Patches contributed by #### Patches contributed by
Adam Wiggins, Chris Continanza, Chris Lowder, Craig R Webster, Dan Farina, Dan Peterson, David Dollar, Fletcher Nichol, Florian Apolloner, Gabriel Burt, Gamaliel Toro, Greg Reinacker, Hugues Le Gendre, Hunter Nield, Iain Hecker, Jay Zeschin, Keith Rarick, Khaja Minhajuddin, Lincoln Stoll, Marcos Muino Garcia, Mark McGranaghan, Matt Griffin, Matt Haynes, Matthijs Langenberg, Michael Dwan, Michael van Rooijen, Mike Javorski, Nathan Broadbent, Nathan L Smith, Nick Zadrozny, Phil Hagelberg, Ricardo Chimal, Jr, Thom May, Tom Ward, brainopia, clifff, jc00ke [Contributor List](https://github.com/ddollar/foreman/contributors)
## License ## License

View File

@@ -1,6 +1,8 @@
$:.unshift File.expand_path("../lib", __FILE__) $:.unshift File.expand_path("../lib", __FILE__)
require "foreman" require "foreman"
require "bundler/setup"
Dir[File.expand_path("../tasks/*.rake", __FILE__)].each do |task| Dir[File.expand_path("../tasks/*.rake", __FILE__)].each do |task|
load task load task
end end

View File

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

8
bin/taskman Executable file
View File

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

View File

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

14
data/example/spawnee Executable file
View File

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

7
data/example/spawner Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
<%
app_names = []
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:<%= full_name %>]
command=<%= process.command %>
autostart=true
autorestart=true
stopsignal=QUIT
stdout_logfile=<%= log %>/<%= name %>-<%= num %>.log
stderr_logfile=<%= log %>/<%= name %>-<%= num %>.error.log
user=<%= user %>
directory=<%= engine.root %>
environment=<%= environment.join(',') %><%
end
end
%>
[group:<%= app %>]
programs=<%= app_names.join(',') %>

View File

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

View File

@@ -1,5 +1,5 @@
start on starting <%= app %>-<%= process.name %> start on starting <%= app %>-<%= name %>
stop on stopping <%= app %>-<%= process.name %> stop on stopping <%= app %>-<%= name %>
respawn respawn
exec su - <%= user %> -c 'cd <%= engine.directory %>; export PORT=<%= port %>;<% engine.environment.each_pair do |var,env| %> export <%= var.upcase %>=<%= 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'

2
dist/deb.rake vendored
View File

@@ -4,7 +4,7 @@ file pkg("/apt-#{version}/foreman-#{version}.deb") => distribution_files("deb")
assemble_distribution assemble_distribution
assemble_gems assemble_gems
assemble resource("deb/foreman"), "bin/foreman", 0755 assemble resource("deb/foreman"), "bin/foreman", 0755
File.chmod 0755, "bin/runner" File.chmod 0755, "bin/foreman-runner"
end end
assemble resource("deb/control"), "control" assemble resource("deb/control"), "control"

2
dist/gem.rake vendored
View File

@@ -10,5 +10,5 @@ task "gem:clean" do
end end
task "gem:release" => "gem:build" do |t| task "gem:release" => "gem:build" do |t|
sh "parka push -f #{pkg("foreman-#{version}.gem")}" sh "gem push #{pkg("foreman-#{version}.gem")} || echo 'error'"
end end

6
dist/jruby.rake vendored
View File

@@ -1,5 +1,7 @@
file pkg("foreman-#{version}-jruby.gem") => distribution_files do |t| file pkg("foreman-#{version}-jruby.gem") => distribution_files do |t|
sh "env PLATFORM=java gem build foreman.gemspec" Bundler.with_clean_env do
sh "env PLATFORM=java gem build foreman.gemspec"
end
sh "mv foreman-#{version}-java.gem #{t.name}" sh "mv foreman-#{version}-java.gem #{t.name}"
end end
@@ -10,5 +12,5 @@ task "jruby:clean" do
end end
task "jruby:release" => "jruby:build" do |t| task "jruby:release" => "jruby:build" do |t|
sh "parka push -f #{pkg("foreman-#{version}-jruby.gem")}" sh "gem push #{pkg("foreman-#{version}-jruby.gem")} || echo 'error'"
end end

6
dist/mingw32.rake vendored
View File

@@ -1,5 +1,7 @@
file pkg("foreman-#{version}-mingw32.gem") => distribution_files do |t| file pkg("foreman-#{version}-mingw32.gem") => distribution_files do |t|
sh "env PLATFORM=mingw32 gem build foreman.gemspec" Bundler.with_clean_env do
sh "env PLATFORM=mingw32 gem build foreman.gemspec"
end
sh "mv foreman-#{version}-mingw32.gem #{t.name}" sh "mv foreman-#{version}-mingw32.gem #{t.name}"
end end
@@ -10,5 +12,5 @@ task "mingw32:clean" do
end end
task "mingw32:release" => "mingw32:build" do |t| task "mingw32:release" => "mingw32:build" do |t|
sh "parka push -f #{pkg("foreman-#{version}-mingw32.gem")}" sh "gem push #{pkg("foreman-#{version}-mingw32.gem")} || echo 'error'"
end end

15
dist/pkg.rake vendored
View File

@@ -13,7 +13,7 @@ file pkg("foreman-#{version}.pkg") => distribution_files do |t|
mkdir_p "pkg" mkdir_p "pkg"
mkdir_p "pkg/Resources" mkdir_p "pkg/Resources"
mkdir_p "pkg/foreman-#{version}.pkg" mkdir_p "pkg/foreman.pkg"
dist = File.read(resource("pkg/Distribution.erb")) dist = File.read(resource("pkg/Distribution.erb"))
dist = ERB.new(dist).result(binding) dist = ERB.new(dist).result(binding)
@@ -21,20 +21,21 @@ file pkg("foreman-#{version}.pkg") => distribution_files do |t|
dist = File.read(resource("pkg/PackageInfo.erb")) dist = File.read(resource("pkg/PackageInfo.erb"))
dist = ERB.new(dist).result(binding) dist = ERB.new(dist).result(binding)
File.open("pkg/foreman-#{version}.pkg/PackageInfo", "w") { |f| f.puts dist } File.open("pkg/foreman.pkg/PackageInfo", "w") { |f| f.puts dist }
mkdir_p "pkg/foreman-#{version}.pkg/Scripts" mkdir_p "pkg/foreman.pkg/Scripts"
cp resource("pkg/postinstall"), "pkg/foreman-#{version}.pkg/Scripts/postinstall" cp resource("pkg/postinstall"), "pkg/foreman.pkg/Scripts/postinstall"
chmod 0755, "pkg/foreman-#{version}.pkg/Scripts/postinstall" chmod 0755, "pkg/foreman.pkg/Scripts/postinstall"
sh %{ mkbom -s foreman pkg/foreman-#{version}.pkg/Bom } sh %{ mkbom -s foreman pkg/foreman.pkg/Bom }
Dir.chdir("foreman") do Dir.chdir("foreman") do
sh %{ pax -wz -x cpio . > ../pkg/foreman-#{version}.pkg/Payload } sh %{ pax -wz -x cpio . > ../pkg/foreman.pkg/Payload }
end end
sh %{ pkgutil --flatten pkg foreman-#{version}.pkg } sh %{ pkgutil --flatten pkg foreman-#{version}.pkg }
FileUtils.mkdir_p(File.dirname(t.name))
cp_r "foreman-#{version}.pkg", t.name cp_r "foreman-#{version}.pkg", t.name
end end
end end

View File

@@ -10,14 +10,14 @@
]]></script> ]]></script>
<choices-outline> <choices-outline>
<line choice="git"/> <line choice="git"/>
<line choice="foreman-<%= version %>"/> <line choice="foreman"/>
</choices-outline> </choices-outline>
<choice id="git" title="git" start_selected="false" start_enabled="false" selected="needs_git()" enabled="needs_git()"> <choice id="git" title="git" start_selected="false" start_enabled="false" selected="needs_git()" enabled="needs_git()">
<pkg-ref id="git.pkg" /> <pkg-ref id="git.pkg" />
</choice> </choice>
<choice id="foreman-<%= version %>" title="foreman"> <choice id="foreman" title="foreman">
<pkg-ref id="io.foreman.installer"/> <pkg-ref id="io.foreman.installer"/>
</choice> </choice>
<pkg-ref id="io.foreman.installer" installKBytes="<%= kbytes %>" version="<%= version %>" auth="Root">#foreman-<%= version %>.pkg</pkg-ref> <pkg-ref id="io.foreman.installer" installKBytes="<%= kbytes %>" version="<%= version %>" auth="Root">#foreman.pkg</pkg-ref>
</installer-script> </installer-script>

View File

@@ -4,4 +4,3 @@
<postinstall file="./postinstall"/> <postinstall file="./postinstall"/>
</scripts> </scripts>
</pkg-info> </pkg-info>

2
dist/tgz.rake vendored
View File

@@ -3,7 +3,7 @@ file pkg("foreman-#{version}.tgz") => distribution_files do |t|
mkchdir("foreman") do mkchdir("foreman") do
assemble_distribution assemble_distribution
assemble_gems assemble_gems
rm_rf "bin" rm_f "bin/foreman"
assemble resource("tgz/foreman"), "foreman", 0755 assemble resource("tgz/foreman"), "foreman", 0755
end end

View File

@@ -16,8 +16,7 @@ Gem::Specification.new do |gem|
gem.files = Dir["**/*"].select { |d| d =~ %r{^(README|bin/|data/|ext/|lib/|spec/|test/)} } gem.files = Dir["**/*"].select { |d| d =~ %r{^(README|bin/|data/|ext/|lib/|spec/|test/)} }
gem.files << "man/foreman.1" gem.files << "man/foreman.1"
gem.add_dependency 'term-ansicolor', '~> 1.0.7' gem.add_dependency 'thor', '>= 0.13.6'
gem.add_dependency 'thor', '>= 0.13.6'
if ENV["PLATFORM"] == "java" if ENV["PLATFORM"] == "java"
gem.add_dependency "posix-spawn", "~> 0.3.6" gem.add_dependency "posix-spawn", "~> 0.3.6"

View File

@@ -4,14 +4,8 @@ module Foreman
class AppDoesNotExist < Exception; end class AppDoesNotExist < Exception; end
# load contents of env_file into ENV
def self.load_env!(env_file = './.env')
require 'foreman/engine'
Foreman::Engine.load_env!(env_file)
end
def self.runner def self.runner
File.expand_path("../../bin/runner", __FILE__) File.expand_path("../../bin/foreman-runner", __FILE__)
end end
def self.jruby? def self.jruby?

View File

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

View File

@@ -1,116 +1,260 @@
require "foreman" require "foreman"
require "foreman/env"
require "foreman/process" require "foreman/process"
require "foreman/procfile" require "foreman/procfile"
require "foreman/utils"
require "tempfile" require "tempfile"
require "timeout" require "timeout"
require "term/ansicolor"
require "fileutils" require "fileutils"
require "thread" require "thread"
class Foreman::Engine class Foreman::Engine
attr_reader :procfile attr_reader :env
attr_reader :directory
attr_reader :options attr_reader :options
attr_reader :processes
extend Term::ANSIColor # 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
COLORS = [ cyan, yellow, green, magenta, red, blue, @options[:formation] ||= "all=1"
intense_cyan, intense_yellow, intense_green, intense_magenta,
intense_red, intense_blue ]
def initialize(procfile, options={}) @env = {}
@procfile = Foreman::Procfile.new(procfile) @mutex = Mutex.new
@directory = options[:app_root] || File.expand_path(File.dirname(procfile)) @names = {}
@options = options @processes = []
@environment = read_environment_files(options[:env]) @running = {}
@output_mutex = Mutex.new @readers = {}
end
def self.load_env!(env_file)
@environment = read_environment_files(env_file)
apply_environment!
end end
# Start the processes registered to this +Engine+
#
def start def start
proctitle "ruby: foreman master"
termtitle "#{File.basename(@directory)} - foreman"
trap("TERM") { puts "SIGTERM received"; terminate_gracefully } trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
trap("INT") { puts "SIGINT received"; terminate_gracefully } trap("INT") { puts "SIGINT received"; terminate_gracefully }
trap("HUP") { puts "SIGHUP received"; terminate_gracefully }
assign_colors startup
spawn_processes spawn_processes
watch_for_output watch_for_output
watch_for_termination sleep 0.1
watch_for_termination { terminate_gracefully }
shutdown
end end
def port_for(process, num, base_port=nil) # Register a process to be run by this +Engine+
base_port ||= 5000 #
offset = procfile.process_names.index(process.name) * 100 # @param [String] name A name for this process
base_port.to_i + offset + num - 1 # @param [String] command The command to run
# @param [Hash] options
#
# @option options [Hash] :env A custom environment for this process
#
def register(name, command, options={})
options[:env] ||= env
options[:cwd] ||= File.dirname(command.split(" ").first)
process = Foreman::Process.new(command, options)
@names[process] = name
@processes << process
end end
private ###################################################################### # Clear the processes registered to this +Engine+
#
def clear
@names = {}
@processes = []
end
def spawn_processes # Register processes by reading a Procfile
concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency]) #
# @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
procfile.entries.each do |entry| # Load a .env file into the +env+ for this +Engine+
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| # @param [String] filename A .env file to load into the environment
running_processes[process.pid] = process #
readers[process] = reader 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")
@running.each do |pid, (process, index)|
system "sending #{signal} to #{name_for(pid)} at pid #{pid}"
begin
Process.kill(signal, -1 * pid)
rescue Errno::ESRCH, Errno::EPERM
end end
end end
end end
# 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_port + (@processes.index(process) * 100) + (instance - 1)
end
private
### Engine API ######################################################
def startup
raise TypeError, "must use a subclass of Foreman::Engine"
end
def output(name, data)
raise TypeError, "must use a subclass of Foreman::Engine"
end
def shutdown
raise TypeError, "must use a subclass of Foreman::Engine"
end
## Helpers ##########################################################
def base_port def base_port
options[:port] || 5000 (options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i
end end
def kill_all(signal="SIGTERM") def create_pipe
running_processes.each do |pid, process| IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
info "sending #{signal} to pid #{pid}" end
process.kill signal
def name_for(pid)
process, index = @running[pid]
[ @names[process], index.to_s ].compact.join(".")
end
def parse_formation(formation)
pairs = @options[:formation].to_s.gsub(/\s/, "").split(",")
pairs.inject(Hash.new(0)) do |ax, pair|
process, amount = pair.split("=")
process == "all" ? ax.default = amount.to_i : ax[process] = amount.to_i
ax
end end
end end
def terminate_gracefully def output_with_mutex(name, message)
return if @terminating @mutex.synchronize do
@terminating = true output name, message
info "sending SIGTERM to all processes" end
kill_all "SIGTERM" end
Timeout.timeout(5) do
while running_processes.length > 0 def system(message)
pid, status = Process.wait2 output_with_mutex "system", message
process = running_processes.delete(pid) end
info "process terminated", process.name
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
@processes.each do |process|
1.upto(formation[@names[process]]) do |n|
reader, writer = create_pipe
begin
pid = process.run(:output => writer, :env => { "PORT" => port_for(process, n).to_s })
writer.puts "started with pid #{pid}"
rescue Errno::ENOENT
writer.puts "unknown command: #{process.command}"
end
@running[pid] = [process, n]
@readers[pid] = reader
end end
end end
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
ps, message = data.split(",", 2)
color = colors[ps.split(".").first]
info message, ps, color
end
end end
def watch_for_output def watch_for_output
Thread.new do Thread.new do
require "win32console" if Foreman.windows?
begin begin
loop do loop do
poll_readers (IO.select(@readers.values).first || []).each do |reader|
data = reader.gets
output_with_mutex name_for(@readers.key(reader)), data
end
end end
rescue Exception => ex rescue Exception => ex
puts ex.message puts ex.message
@@ -121,118 +265,24 @@ private ######################################################################
def watch_for_termination def watch_for_termination
pid, status = Process.wait2 pid, status = Process.wait2
process = running_processes.delete(pid) output_with_mutex name_for(pid), termination_message_for(status)
info "process terminated", process.name @running.delete(pid)
terminate_gracefully yield if block_given?
pid
rescue Errno::ECHILD rescue Errno::ECHILD
end end
def info(message, name="system", color=Term::ANSIColor.white) def terminate_gracefully
output = "" return if @terminating
output += color @terminating = true
output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | " system "sending SIGTERM to all processes"
output += Term::ANSIColor.reset killall "SIGTERM"
output += message.chomp Timeout.timeout(5) do
puts output watch_for_termination while @running.length > 0
end
def print(message=nil)
@output_mutex.synchronize do
$stdout.print message
end end
rescue Timeout::Error
system "sending SIGKILL to all processes"
killall "SIGKILL"
end end
def puts(message=nil)
@output_mutex.synchronize do
$stdout.puts message
end
end
def longest_process_name
@longest_process_name ||= begin
longest = procfile.process_names.map { |name| name.length }.sort.last
longest = 6 if longest < 6 # system
longest
end
end
def pad_process_name(name="system")
name.to_s.ljust(longest_process_name + 3) # add 3 for process number padding
end
def proctitle(title)
$0 = title
end
def termtitle(title)
printf("\033]0;#{title}\007") unless Foreman.windows?
end
def running_processes
@running_processes ||= {}
end
def readers
@readers ||= {}
end
def colors
@colors ||= {}
end
def assign_colors
procfile.entries.each do |entry|
colors[entry.name] = next_color
end
end
def process_by_reader(reader)
readers.invert[reader]
end
def next_color
@current_color ||= -1
@current_color += 1
@current_color = 0 if COLORS.length < @current_color
COLORS[@current_color]
end
module Env
attr_reader :environment
def read_environment_files(filenames)
environment = {}
(filenames || "").split(",").map(&:strip).each do |filename|
error "No such file: #{filename}" unless File.exists?(filename)
environment.merge!(read_environment(filename))
end
environment.merge!(read_environment(".env")) unless filenames
environment
end
def read_environment(filename)
return {} unless File.exists?(filename)
File.read(filename).split("\n").inject({}) do |hash, line|
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
hash[$1] = $2
end
hash
end
end
def apply_environment!
@environment.each { |k,v| ENV[k] = v }
end
def error(message)
puts "ERROR: #{message}"
exit 1
end
end
include Env
extend Env
end end

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

@@ -0,0 +1,103 @@
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 unless self.respond_to?(:isatty)
self.isatty && ENV["TERM"]
end
def color(name)
return "" unless color?
return "" unless ansi = ANSI[name.to_sym]
"\e[#{ansi}m"
end
end
FOREMAN_COLORS = %w( cyan yellow green magenta red blue intense_cyan intense_yellow
intense_green intense_magenta intense_red, intense_blue )
def startup
@colors = map_colors
proctitle "foreman: master"
end
def output(name, data)
data.to_s.chomp.split("\n").each do |message|
Color.enable($stdout, options[:color]) unless $stdout.respond_to?(:color?)
output = ""
output += $stdout.color(@colors[name.split(".").first].to_sym)
output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
output += $stdout.color(:reset)
output += message
$stdout.puts output
$stdout.flush
end
rescue Errno::EPIPE
terminate_gracefully
end
def shutdown
end
private
def name_padding
@name_padding ||= begin
index_padding = @names.values.map { |n| formation[n] }.max.to_s.length + 1
name_padding = @names.values.map { |n| n.length + index_padding }.sort.last
[ 6, name_padding ].max
end
end
def pad_process_name(name)
name.ljust(name_padding, " ")
end
def map_colors
colors = Hash.new("white")
@names.values.each_with_index do |name, index|
colors[name] = FOREMAN_COLORS[index % FOREMAN_COLORS.length]
end
colors["system"] = "intense_white"
colors
end
def proctitle(title)
$0 = title
end
def termtitle(title)
printf("\033]0;#{title}\007") unless Foreman.windows?
end
end

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

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

View File

@@ -24,9 +24,11 @@ module Foreman::Export
end end
require "foreman/export/base" require "foreman/export/base"
require "foreman/export/inittab" require "foreman/export/inittab"
require "foreman/export/upstart" require "foreman/export/upstart"
require "foreman/export/bluepill" require "foreman/export/bluepill"
require "foreman/export/runit" require "foreman/export/runit"
require "foreman/export/supervisord"
require "foreman/export/launchd"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
require "erb"
require "foreman/export"
class Foreman::Export::Supervisord < Foreman::Export::Base
def export
super
Dir["#{location}/#{app}*.conf"].each do |file|
clean file
end
write_template "supervisord/app.conf.erb", "#{app}.conf", binding
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
module Foreman module Foreman
VERSION = "0.37.2" VERSION = "0.48.0.pre2"
end end

View File

@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3 .\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3
. .
.TH "FOREMAN" "1" "January 2012" "Foreman 0.37.2" "Foreman Manual" .TH "FOREMAN" "1" "April 2012" "Foreman 0.46.0" "Foreman Manual"
. .
.SH "NAME" .SH "NAME"
\fBforeman\fR \- manage Procfile\-based applications \fBforeman\fR \- manage Procfile\-based applications
@@ -10,10 +10,13 @@
\fBforeman start [process]\fR \fBforeman start [process]\fR
. .
.br .br
\fBforeman run <command>\fR
.
.br
\fBforeman export <format> [location]\fR \fBforeman export <format> [location]\fR
. .
.SH "DESCRIPTION" .SH "DESCRIPTION"
\fBForeman\fR is a manager for Procfile\-based applications\. Its aim is to abstract away the details of the Procfile format, and allow you to either run your application directly or export it to some other process management format\. Foreman is a manager for Procfile\-based applications\. Its aim is to abstract away the details of the Procfile format, and allow you to either run your application directly or export it to some other process management format\.
. .
.SH "RUNNING" .SH "RUNNING"
\fBforeman start\fR is used to run your application directly from the command line\. \fBforeman start\fR is used to run your application directly from the command line\.
@@ -32,9 +35,24 @@ The following options control how the application is run:
Specify the number of each process type to run\. The value passed in should be in the format \fBprocess=num,process=num\fR Specify the number of each process type to run\. The value passed in should be in the format \fBprocess=num,process=num\fR
. .
.TP .TP
\fB\-e\fR, \fB\-\-env\fR
Specify one or more \.env files to load
.
.TP
\fB\-f\fR, \fB\-\-procfile\fR
Specify an alternate Procfile to load, implies \fB\-d\fR at the Procfile root\.
.
.TP
\fB\-p\fR, \fB\-\-port\fR \fB\-p\fR, \fB\-\-port\fR
Specify which port to use as the base for this application\. Should be a multiple of 1000\. 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\.
.
.SH "EXPORTING" .SH "EXPORTING"
\fBforeman export\fR is used to export your application to another process management format\. \fBforeman export\fR is used to export your application to another process management format\.
. .
@@ -61,10 +79,14 @@ Specify the directory to place process logs in\.
Specify which port to use as the base for this application\. Should be a multiple of 1000\. Specify which port to use as the base for this application\. Should be a multiple of 1000\.
. .
.TP .TP
\fB\-t\fR, \fB\-\-template\fR
Specify an alternate template to use for creating export files\. See \fIhttps://github\.com/ddollar/foreman/tree/master/data/export\fR for examples\.
.
.TP
\fB\-u\fR, \fB\-\-user\fR \fB\-u\fR, \fB\-\-user\fR
Specify the user the application should be run as\. Defaults to the app name Specify the user the application should be run as\. Defaults to the app name
. .
.SH "OPTIONS" .SH "GLOBAL OPTIONS"
These options control all modes of foreman\'s operation\. These options control all modes of foreman\'s operation\.
. .
.TP .TP

View File

@@ -4,11 +4,12 @@ foreman(1) -- manage Procfile-based applications
## SYNOPSIS ## SYNOPSIS
`foreman start [process]`<br> `foreman start [process]`<br>
`foreman run <command>`<br>
`foreman export <format> [location]` `foreman export <format> [location]`
## DESCRIPTION ## DESCRIPTION
**Foreman** is a manager for Procfile-based applications. Its aim is to Foreman is a manager for Procfile-based applications. Its aim is to
abstract away the details of the Procfile format, and allow you to either run abstract away the details of the Procfile format, and allow you to either run
your application directly or export it to some other process management your application directly or export it to some other process management
format. format.
@@ -29,10 +30,23 @@ The following options control how the application is run:
Specify the number of each process type to run. The value passed in Specify the number of each process type to run. The value passed in
should be in the format `process=num,process=num` should be in the format `process=num,process=num`
* `-e`, `--env`:
Specify one or more .env files to load
* `-f`, `--procfile`:
Specify an alternate Procfile to load, implies `-d` at the Procfile root.
* `-p`, `--port`: * `-p`, `--port`:
Specify which port to use as the base for this application. Should be Specify which port to use as the base for this application. Should be
a multiple of 1000. 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.
## EXPORTING ## EXPORTING
`foreman export` is used to export your application to another process `foreman export` is used to export your application to another process
@@ -58,11 +72,15 @@ The following options control how the application is run:
Specify which port to use as the base for this application. Should be Specify which port to use as the base for this application. Should be
a multiple of 1000. a multiple of 1000.
* `-t`, `--template`:
Specify an alternate template to use for creating export files.
See <https://github.com/ddollar/foreman/tree/master/data/export> for examples.
* `-u`, `--user`: * `-u`, `--user`:
Specify the user the application should be run as. Defaults to the Specify the user the application should be run as. Defaults to the
app name app name
## OPTIONS ## GLOBAL OPTIONS
These options control all modes of foreman's operation. These options control all modes of foreman's operation.

View File

@@ -4,9 +4,22 @@ require "foreman/cli"
describe "Foreman::CLI", :fakefs do describe "Foreman::CLI", :fakefs do
subject { Foreman::CLI.new } subject { Foreman::CLI.new }
describe ".foreman" do
before { File.open(".foreman", "w") { |f| f.puts "formation: alpha=2" } }
it "provides default options" do
subject.send(:options)["formation"].should == "alpha=2"
end
it "is overridden by options at the cli" do
subject = Foreman::CLI.new([], :formation => "alpha=3")
subject.send(:options)["formation"].should == "alpha=3"
end
end
describe "start" do describe "start" do
describe "with a non-existent Procfile" do describe "when a Procfile doesnt exist", :fakefs do
it "prints an error" do it "displays an error" do
mock_error(subject, "Procfile does not exist.") do mock_error(subject, "Procfile does not exist.") do
dont_allow.instance_of(Foreman::Engine).start dont_allow.instance_of(Foreman::Engine).start
subject.start subject.start
@@ -14,149 +27,50 @@ describe "Foreman::CLI", :fakefs do
end end
end end
describe "with a Procfile" do describe "with a valid Procfile" do
before(:each) { write_procfile } it "can run a single command" do
without_fakefs do
it "runs successfully" do output = foreman("start env -f #{resource_path("Procfile")}")
dont_allow(subject).error output.should =~ /env.1/
mock.instance_of(Foreman::Engine).start output.should_not =~ /test.1/
subject.start
end
end
end
describe "export" do
describe "options" do
it "uses .foreman" do
write_procfile
File.open(".foreman", "w") { |f| f.puts "concurrency: alpha=2" }
mock_export = mock(Foreman::Export::Upstart)
mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "concurrency" => "alpha=2" }) { mock_export }
mock_export.export
foreman %{ export upstart /upstart }
end
it "respects --env" do
write_procfile
write_env("envfile")
mock_export = mock(Foreman::Export::Upstart)
mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "env" => "envfile" }) { mock_export }
mock_export.export
foreman %{ export upstart /upstart --env envfile }
end
end
describe "with a non-existent Procfile" do
it "prints an error" do
mock_error(subject, "Procfile does not exist.") do
dont_allow.instance_of(Foreman::Engine).export
subject.export("testapp")
end
end
end
describe "with a Procfile" do
before(:each) { write_procfile }
describe "with a formatter with a generic error" do
before do
mock(Foreman::Export).formatter("errorful") { Class.new(Foreman::Export::Base) do
def export
raise Foreman::Export::Exception.new("foo")
end
end }
end
it "prints an error" do
mock_error(subject, "foo") do
subject.export("errorful")
end
end end
end end
describe "with a valid config" do it "can run all commands" do
before(:each) { write_foreman_config("testapp") } without_fakefs do
output = foreman("start -f #{resource_path("Procfile")} -e #{resource_path(".env")}")
it "runs successfully" do output.should =~ /echo.1 \| echoing/
dont_allow(subject).error output.should =~ /env.1 \| bar/
mock_export = mock(Foreman::Export::Upstart) output.should =~ /test.1 \| testing/
mock(Foreman::Export::Upstart).new("/tmp/foo", is_a(Foreman::Engine), {}) { mock_export }
mock_export.export
subject.export("upstart", "/tmp/foo")
end end
end end
end end
end end
describe "check" do describe "check" do
describe "with a valid Procfile" do it "with a valid Procfile displays the jobs" do
before { write_procfile } write_procfile
foreman("check").should == "valid procfile detected (alpha, bravo)\n"
it "displays the jobs" do
mock(subject).puts("valid procfile detected (alpha, bravo)")
subject.check
end
end end
describe "with a blank Procfile" do it "with a blank Procfile displays an error" do
before do FileUtils.touch "Procfile"
FileUtils.touch("Procfile") foreman("check").should == "ERROR: no processes defined\n"
end end
it "displays an error" do it "without a Procfile displays an error" do
mock_error(subject, "no processes defined") do FileUtils.rm_f "Procfile"
subject.check foreman("check").should == "ERROR: Procfile does not exist.\n"
end
end
end end
end end
describe "run" do describe "run" do
describe "with a valid Procfile" do it "can run a command" do
before { write_procfile } forked_foreman("run echo 1").should == "1\n"
end
describe "and a command" do it "includes the environment" do
let(:command) { ["ls", "-l"] } forked_foreman("run #{resource_path("bin/env FOO")} -e #{resource_path(".env")}").should == "bar\n"
before(:each) do
stub(subject).exec
end
it "should load the environment file" do
write_env
preserving_env do
subject.run *command
ENV["FOO"].should == "bar"
end
ENV["FOO"].should be_nil
end
it "should runute the command as a string" do
mock(subject).exec(command.join(" "))
subject.run *command
end
end
describe "and a non-existent command" do
let(:command) { "iuhtngrglhulhdfg" }
it "should print an error" do
mock_error(subject, "command not found: #{command}") do
subject.run command
end
end
end
describe "and a non-executable command" do
let(:command) { __FILE__ }
it "should print an error" do
mock_error(subject, "not executable: #{command}") do
subject.run command
end
end
end
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
require "spec_helper"
require "foreman/engine"
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(: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-alpha-1.conf")
end
it "cleans up if exporting into an existing dir" do
mock(FileUtils).rm("/tmp/init/app.conf")
supervisord.export
supervisord.export
end
context "with concurrency" do
let(:formation) { "alpha=2" }
it "exports to the filesystem with concurrency" do
supervisord.export
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app-alpha-2.conf")
end
end
end

View File

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

View File

@@ -5,127 +5,44 @@ require 'timeout'
require 'tmpdir' require 'tmpdir'
describe Foreman::Process do describe Foreman::Process do
subject { described_class.new entry, number, port }
let(:number) { 1 } def run(process, options={})
let(:port) { 777 } rd, wr = IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
let(:command) { "script" } process.run(options.merge(:output => wr))
let(:name) { "foobar" } rd.gets
let(:entry) { OpenStruct.new :name => name, :command => command } end
its(:entry) { entry } describe "#run" do
its(:num) { number }
its(:port) { port }
its(:name) { "#{name}.#{port}" }
its(:pid) { nil }
describe '#run' do it "runs the process" do
let(:pipe) { :pipe } process = Foreman::Process.new(resource_path("bin/test"))
let(:basedir) { Dir.mktmpdir } run(process).should == "testing\n"
let(:env) {{ 'foo' => 'bar' }}
let(:init_delta) { 0.1 }
after { FileUtils.remove_entry_secure basedir }
def run(cmd=command)
entry.command = cmd
subject.run pipe, basedir, env
subject.detach && sleep(init_delta)
end end
def run_file(executable, code) it "can set environment" do
file = File.open("#{basedir}/script", 'w') {|it| it << code } process = Foreman::Process.new(resource_path("bin/env FOO"), :env => { "FOO" => "bar" })
run "#{executable} #{file.path}" run(process).should == "bar\n"
sleep 1
end end
context 'options' do it "can set per-run environment" do
it 'should set PORT for environment' do process = Foreman::Process.new(resource_path("bin/env FOO"))
mock(subject).run_process(basedir, command, pipe) do run(process, :env => { "FOO" => "bar "}).should == "bar\n"
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
end end
context 'process' do it "can handle env vars in the command" do
around do |spec| process = Foreman::Process.new(resource_path("bin/echo $FOO"), :env => { "FOO" => "bar" })
IO.pipe do |reader, writer| run(process).should == "bar\n"
@reader, @writer = reader, writer end
spec.run
end
end
let(:pipe) { @writer } it "can handle per-run env vars in the command" do
let(:output) { @reader.read_nonblock 1024 } process = Foreman::Process.new(resource_path("bin/echo $FOO"))
run(process, :env => { "FOO" => "bar" }).should == "bar\n"
end
it 'should not block' do it "should output utf8 properly" do
expect { process = Foreman::Process.new(resource_path("bin/utf8"))
Timeout.timeout(2*init_delta) { run 'sleep 2' } run(process).should == "\xFF\x03\n"
}.should_not raise_exception
end
it 'should be alive' do
run 'sleep 1'
subject.should be_alive
end
it 'should be dead' do
run 'exit'
subject.should be_dead
end
it 'should be killable' do
run 'sleep 1'
subject.kill 'TERM'
subject.should be_dead
end
it 'should send different signals' do
run_file 'ruby', <<-CODE
trap "TERM", "IGNORE"
loop { sleep 1 }
CODE
subject.should be_alive
subject.kill 'TERM'
subject.should be_alive
subject.kill 'KILL'
subject.should be_dead
end
it 'should redirect stdout' do
run 'echo hey'
output.should include('hey')
end
it 'should redirect stderr' do
run 'echo hey >2'
output.should include('hey')
end
it 'should handle variables' do
run 'echo $PORT'
output.should include('777')
end
it 'should handle arguments' do
pending
run %{ sh -c "trap '' TERM; sleep 10" }
subject.should be_alive
end
end end
end end
end end

View File

@@ -0,0 +1,41 @@
require 'spec_helper'
require 'foreman/procfile'
require 'pathname'
require 'tmpdir'
describe Foreman::Procfile, :fakefs do
subject { Foreman::Procfile.new }
it "can load from a file" do
write_procfile
subject.load "Procfile"
subject["alpha"].should == "./alpha"
subject["bravo"].should == "./bravo"
end
it "loads a passed-in Procfile" do
write_procfile
procfile = Foreman::Procfile.new("Procfile")
procfile["alpha"].should == "./alpha"
procfile["bravo"].should == "./bravo"
end
it "can have a process appended to it" do
subject["charlie"] = "./charlie"
subject["charlie"].should == "./charlie"
end
it "can write to a string" do
subject["foo"] = "./foo"
subject["bar"] = "./bar"
subject.to_s.should == "foo: ./foo\nbar: ./bar"
end
it "can write to a file" do
subject["foo"] = "./foo"
subject["bar"] = "./bar"
subject.save "/tmp/proc"
File.read("/tmp/proc").should == "foo: ./foo\nbar: ./bar\n"
end
end

View File

@@ -8,24 +8,6 @@ describe Foreman do
it { should be_a String } it { should be_a String }
end end
describe "::load_env!(env_file)", :fakefs do
after do
ENV['FOO'] = nil
end
it "should load env_file into ENV" do
File.open("/tmp/env1", "w") { |f| f.puts("FOO=bar") }
Foreman.load_env!("/tmp/env1")
ENV['FOO'].should == 'bar'
end
it "should assume env_file in ./.env" do
File.open("./.env", "w") { |f| f.puts("FOO=bar") }
Foreman.load_env!
ENV['FOO'].should == 'bar'
end
end
describe "runner" do describe "runner" do
it "should exist" do it "should exist" do
File.exists?(Foreman.runner).should == true File.exists?(Foreman.runner).should == true

1
spec/resources/.env Normal file
View File

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

4
spec/resources/Procfile Normal file
View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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-alpha-2]
command=./alpha
autostart=true
autorestart=true
stopsignal=QUIT
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"
[group:app]
programs=app-alpha-1,app-alpha-2

View File

@@ -6,6 +6,7 @@ SimpleCov.start do
end end
require "rspec" require "rspec"
require "timecop"
require "fakefs/safe" require "fakefs/safe"
require "fakefs/spec_helpers" require "fakefs/spec_helpers"
@@ -23,7 +24,39 @@ def mock_error(subject, message)
end end
def foreman(args) 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?
p [:foo]
buffer += rd.gets
end
end end
def mock_exit(&block) def mock_exit(&block)
@@ -55,13 +88,21 @@ def write_env(env=".env", options={"FOO"=>"bar"})
end end
end end
def load_export_templates_into_fakefs(type) def without_fakefs
FakeFS.deactivate! FakeFS.deactivate!
files = Dir[File.expand_path("../../data/export/#{type}/**", __FILE__)].inject({}) do |hash, file| ret = yield
hash.update(file => File.read(file))
end
FakeFS.activate! 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| File.open(filename, "w") do |f|
f.puts contents f.puts contents
end end
@@ -93,6 +134,17 @@ def normalize_space(s)
s.gsub(/\n[\n\s]*/, "\n") s.gsub(/\n[\n\s]*/, "\n")
end end
def capture_stdout
old_stdout = $stdout.dup
rd, wr = IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
$stdout = wr
yield
wr.close
rd.read
ensure
$stdout = old_stdout
end
RSpec.configure do |config| RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true config.treat_symbols_as_metadata_keys_with_true_values = true
config.color_enabled = true config.color_enabled = true

View File

@@ -27,14 +27,6 @@ task :pages => "man:commit" do
} }
end end
desc "Generate an authors list"
task :authors do
authors = %x{ git log --pretty=format:"%an" | sort -u }.split("\n")
readme = File.read("README.md")
readme.gsub!(/#### Patches contributed by\n([^\n]*)\n/m, "#### Patches contributed by\n#{authors.join(", ")}\n")
File.open("README.md", "w") { |f| f.print readme }
end
def latest_release def latest_release
latest = File.read("Changelog.md").split("\n").first.split(" ")[1] latest = File.read("Changelog.md").split("\n").first.split(" ")[1]
end end
@@ -43,7 +35,7 @@ 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} }.split("\n").sort_by do |tag|
Gem::Version.new(tag[1..-1]) Gem::Version.new(tag[1..-1])
end end
tags.reject { |tag| Gem::Version.new(tag[1..-1]).prerelease? }[1] tags[1]
end end
desc "Generate a Changelog" desc "Generate a Changelog"