├── .gitignore
├── CHANGES.md
├── Gemfile
├── README.md
├── Rakefile
├── bin
└── foreverb
├── examples
├── complex
├── simple
└── stress
├── foreverb.gemspec
├── lib
├── forever.rb
└── forever
│ ├── base.rb
│ ├── extensions.rb
│ ├── job.rb
│ └── version.rb
└── spec
├── cli_spec.rb
├── foreverb_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 | examples/log
6 | examples/tmp
7 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # Version 0.3.0 - January 25, 2012
2 |
3 | * Added fork backend
4 | * Improved queue
5 | * Added before/after each/all filters
6 | * Added remove config method from our cli
7 | * Improved documentation
8 | * Remove zombies processes
9 | * Back to yaml psyck
10 | * Fix yaml config writer/reader
11 |
12 | # Version 0.2.6 - August 27, 2011
13 |
14 | * Added back support for update the daemon config
15 | * Improved a bit our outputs showing the daemon name
16 |
17 | # Version 0.2.5 - August 26, 2011
18 |
19 | * Moved stop to kill
20 | * Added a new stop method that wait until workers are idle
21 | * Improved a bit outputs
22 |
23 | # Version 0.2.4 - July 25, 2011
24 |
25 | * Ruby 1.9.2 compatibility
26 | * Stop process using pid instead of file
27 | * Added specs
28 | * Fixed `foreverb list` where in some scenarios don't return a list of processes
29 |
30 | # Version 0.2.3 - July 21, 2011
31 |
32 | * Added global monitoring, to easily watch each `foreverb` daemon
33 | * Look daemons through config file and unix command `ps`
34 | * Added `start` CLI command
35 | * Added `restart` CLI command
36 | * Added `tail` CLI command
37 | * Added `update` CLI command (useful to update daemons config)
38 | * Improved documentation
39 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in forever.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Foreverb
2 |
3 | Small daemon framework **for ruby**, with logging, error handler, scheduling and much more.
4 |
5 | My inspiration was [forever for node.js](https://raw.github.com/indexzero/forever) written by Charlie Robbins.
6 | My scheduling inspiration was taken from [clockwork](https://github.com/adamwiggins/clockwork) written by Adam Wiggins.
7 |
8 | ## Why?
9 |
10 | There are some alternatives, one of the best is [resque](https://github.com/defunkt/resque), so why another daemons framework?
11 | In my servers I've several daemons and what I need is:
12 |
13 | * easily watch the process (memory, cpu)
14 | * easily manage exceptions
15 | * easily see logs
16 | * easily start/stop/restart daemon
17 | * no blocking jobs
18 | * no blocking queue
19 |
20 | As like [sinatra](https://github.com/sinatra/sinatra) and [padrino](https://github.com/padrino/padrino-framework) I need a
21 | **thin** framework to do these jobs in few seconds. This mean that:
22 |
23 | * I can create a new job quickly
24 | * I can watch, start, stop it quickly
25 |
26 | So, if you have my needs, **Forever** can be the right choice for you.
27 |
28 | ## Install:
29 |
30 | ``` sh
31 | $ gem install foreverb
32 | ```
33 |
34 | ## Deamon Example:
35 |
36 | Place your script under your standard directory, generally on my env is _bin_ or _scripts_.
37 |
38 | In that case is: ```bin/foo```
39 |
40 | ``` rb
41 | #!/usr/bin/ruby
42 | require 'rubygems' unless defined?(Gem)
43 | require 'forever'
44 | require 'mail'
45 |
46 | Forever.run do
47 | ##
48 | # You can set these values:
49 | #
50 | # dir "foo" # Default: File.expand_path('../../', __FILE__)
51 | # file "bar" # Default: __FILE__
52 | # log "bar.log" # Default: File.expand_path(dir, '/log/[file_name].log')
53 | # pid "bar.pid" # Default: File.expand_path(dir, '/tmp/[file_name].pid')
54 | #
55 |
56 | on_error do |e|
57 | Mail.deliver do
58 | delivery_method :sendmail, :location => `which sendmail`.chomp
59 | to "d.dagostino@lipsiasoft.com"
60 | from "exceptions@lipsiasoft.com"
61 | subject "[Foo Watcher] #{e.message}"
62 | body "%s\n %s" % [e.message, e.backtrace.join("\n ")]
63 | end
64 | end
65 |
66 | before :each do # or if you prefer before :all
67 | require 'bundler/setup'
68 | require 'foo'
69 | Foo.start_loop
70 | end
71 | end
72 | ```
73 |
74 | Assign right permission:
75 |
76 | ``` sh
77 | $ chmod +x bin/foo
78 | ```
79 |
80 | start the daemon:
81 |
82 | ``` sh
83 | $ bin/foo
84 | ```
85 |
86 | you should see an output like:
87 |
88 | ``` sh
89 | $ bin/foo
90 | => Process demonized with pid 19538
91 | ```
92 |
93 | you can stop it:
94 |
95 | ``` sh
96 | $ bin/foo stop
97 | => Found pid 19538...
98 | => Killing process 19538...
99 | ```
100 |
101 | ## Scheduling
102 |
103 | You can use `every` method to schedule repetitive tasks.
104 |
105 | Every allow the option `:at` to specify hour or minute and the option `:last` to specify when the `every` must start to loop.
106 |
107 | `:last`: can be nil or a Time class. Default is 0.
108 | `:at`: can be nil, a string or an array of formatted strings. Default is nil.
109 |
110 | ``` rb
111 | every 1.second, :at => '19:30' # => every second since 19:30
112 | every 1.minute, :at => ':30' # => every minute but first call wait xx:30
113 | every 5.minutes, :at => '18:' # => every five minutes but first call was at 18:xx
114 | every 1.day, :at => ['18:30', '20:30'] # => every day only at 18:30 and 20:30
115 | every 60.seconds, :last => Time.now # => will be fired 60 seconds after you launch the app
116 | ```
117 |
118 | Remember that `:at`:
119 |
120 | * accept only 24h format
121 | * you must always provide the colon `:`
122 |
123 | So looking our [example](https://github.com/DAddYE/foreverb/blob/master/examples/sample):
124 |
125 | ``` rb
126 | Forever.run do
127 | dir File.expand_path('../', __FILE__) # Default is ../../__FILE__
128 |
129 | before :all do
130 | puts "All jobs will wait me for 1 second"; sleep 1
131 | end
132 |
133 | every 10.seconds, :at => "#{Time.now.hour}:00" do
134 | puts "Every 10 seconds but first call at #{Time.now.hour}:00"
135 | end
136 |
137 | every 1.seconds, :at => "#{Time.now.hour}:#{Time.now.min+1}" do
138 | puts "Every one second but first call at #{Time.now.hour}:#{Time.now.min}"
139 | end
140 |
141 | every 10.seconds do
142 | puts "Every 10 second"
143 | end
144 |
145 | every 20.seconds do
146 | puts "Every 20 second"
147 | end
148 |
149 | every 15.seconds do
150 | puts "Every 15 seconds, but my task require 10 seconds"; sleep 10
151 | # This doesn't block other jobs and your queue !!!!!!!
152 | end
153 |
154 | every 10.seconds, :at => [":#{Time.now.min+1}", ":#{Time.now.min+2}"] do
155 | puts "Every 10 seconds but first call at xx:#{Time.now.min}"
156 | end
157 |
158 | on_error do |e|
159 | puts "Boom raised: #{e.message}"
160 | end
161 |
162 | on_exit do
163 | puts "Bye bye"
164 | end
165 | end
166 | ```
167 |
168 | Running the example with the following code:
169 |
170 | ``` sh
171 | $ examples/sample; tail -f -n 150 examples/log/sample.log; examples/sample stop
172 | ```
173 |
174 | you should see:
175 |
176 | ```
177 | => Pid not found, process seems doesn't exist!
178 | => Process demonized with pid 11509 with Forever v.0.2.0
179 | [14/07 15:46:56] All jobs will will wait me for 1 second
180 | [14/07 15:46:57] Every 10 second
181 | [14/07 15:46:57] Every 20 second
182 | [14/07 15:46:57] Every 15 seconds, but my task require 10 seconds
183 | [14/07 15:47:00] Every one second but first call at 15:47
184 | [14/07 15:47:00] Every 10 seconds but first call at xx:47
185 | [14/07 15:47:01] Every one second but first call at 15:47
186 | [14/07 15:47:02] Every one second but first call at 15:47
187 | [14/07 15:47:03] Every one second but first call at 15:47
188 | [14/07 15:47:04] Every one second but first call at 15:47
189 | [14/07 15:47:05] Every one second but first call at 15:47
190 | [14/07 15:47:06] Every one second but first call at 15:47
191 | [14/07 15:47:07] Every 10 second
192 | [14/07 15:47:07] Every one second but first call at 15:47
193 | [14/07 15:47:08] Every one second but first call at 15:47
194 | [14/07 15:47:09] Every one second but first call at 15:47
195 | [14/07 15:47:10] Every 10 seconds but first call at xx:47
196 | [14/07 15:47:10] Every one second but first call at 15:47
197 | [14/07 15:47:11] Every one second but first call at 15:47
198 | [14/07 15:47:12] Every 15 seconds, but my task require 10 seconds
199 | ...
200 | [14/07 15:47:42] Every 15 seconds, but my task require 10 seconds
201 | [14/07 15:47:42] Every one second but first call at 15:47
202 | [14/07 15:47:43] Every one second but first call at 15:47
203 | [14/07 15:47:44] Every one second but first call at 15:47
204 | [14/07 15:47:45] Every one second but first call at 15:47
205 | [14/07 15:47:46] Every one second but first call at 15:47
206 | [14/07 15:47:47] Every 10 second
207 | ^C
208 | => Found pid 11509...
209 | => Killing process 11509...
210 | [14/07 15:48:40] Bye bye
211 | ```
212 |
213 | ## Filters
214 |
215 | In foreverb we have a couple of filters, `before` and `after`, like rspec you should be able to filter `before :all` or `before :each`.
216 |
217 | ``` rb
218 | before :all do
219 | puts "This will be ran only at start"
220 | end
221 |
222 | before :each do
223 | puts "Do that before each job"
224 | end
225 |
226 | # ... here jobs ...
227 |
228 | after :all do
229 | puts "This will be ran only at shutdown"
230 | end
231 |
232 | after :each do
233 | puts "Do that after each job"
234 | end
235 | ```
236 |
237 | ## CLI
238 |
239 | ### Help:
240 |
241 | ``` sh
242 | $ foreverb help
243 | Tasks:
244 | foreverb help [TASK] # Describe available tasks or one specific task
245 | foreverb list # List Forever running daemons
246 | foreverb restart [DAEMON] [--all] [--yes] # Restart one or more matching daemons
247 | foreverb start [DAEMON] [--all] [--yes] # Start one or more matching daemons
248 | foreverb stop [DAEMON] [--all] [--yes] # Stop one or more matching daemons
249 | foreverb tail [DAEMON] # Tail log of first matching daemon
250 | foreverb update [DAEMON] [--all] [--yes] # Update config from one or more matching daemons
251 | foreverb version # show the version number
252 | ```
253 |
254 | ### List daemons:
255 |
256 | ``` sh
257 | $ foreverb list
258 | RUNNING /Developer/src/Extras/githubwatcher/bin/githubwatcher
259 | RUNNING /Developer/src/Extras/foreverb/examples/sample
260 | Reading config from: /Users/DAddYE/.foreverb
261 | ```
262 |
263 | ### Monitor daemons (with ps):
264 |
265 | ``` sh
266 | $ foreverb list -m
267 | PID RSS CPU CMD
268 | 5528 168 Mb 0.1 % Forever: /Developer/src/Extras/githubwatcher/bin/githubwatcher
269 | 5541 18 Mb 0.0 % Forever: /Developer/src/Extras/foreverb/examples/sample
270 | ```
271 |
272 | ### Stop daemon(s):
273 |
274 | ``` sh
275 | $ foreverb stop foo
276 | Do you want really stop Forever: bin/foo with pid 19538? y
277 | Killing process Forever: bin/foo with pid 19538...
278 |
279 | $ foreverb stop --all -y
280 | Killing process Forever: /usr/bin/githubwatcher with pid 2824
281 | Killing process Forever: examples/sample with pid 2836
282 | ```
283 |
284 | ### Start daemon(s):
285 |
286 | ``` sh
287 | $ foreverb start github
288 | Do you want really start /Developer/src/Extras/githubwatcher/bin/githubwatcher? y
289 | => Found pid 5528...
290 | => Killing process 5528...
291 | => Process demonized with pid 14925 with Forever v.0.2.2
292 | ```
293 |
294 | as for stop we allow `--all` and `-y`
295 |
296 | ### Restart daemon(s)
297 |
298 | ``` sh
299 | $ foreverb restart github
300 | Do you want really restart /Developer/src/Extras/githubwatcher/bin/githubwatcher? y
301 | => Found pid 5528...
302 | => Killing process 5528...
303 | => Process demonized with pid 14925 with Forever v.0.2.2
304 | ```
305 |
306 | as for stop we allow `--all` and `-y`
307 |
308 | ### Tail logs
309 |
310 | ``` sh
311 | $ foreverb tail github
312 | [22/07 11:22:17] Quering git://github.com/DAddYE/lipsiadmin.git...
313 | [22/07 11:22:17] Quering git://github.com/DAddYE/lightbox.git...
314 | [22/07 11:22:17] Quering git://github.com/DAddYE/exception-notifier.git...
315 | [22/07 11:22:17] Quering git://github.com/DAddYE/lipsiablog.git...
316 | [22/07 11:22:17] Quering git://github.com/DAddYE/purple_ruby.git...
317 | ```
318 |
319 | you can specify how many lines show with option `-n`, default is `150`
320 |
321 | ### Update config
322 |
323 | This command would be helpful if you change `pid` `log` path, in this way the global config file `~/.foreverb` will be update
324 | using latest informations from yours deamons
325 |
326 | Note that you can personalize the config file setting `FOREVER_PATH` matching your needs.
327 |
328 | ``` sh
329 | $ foreverb update github
330 | Do you want really update config from /Developer/src/Extras/githubwatcher/bin/githubwatcher? y
331 | ```
332 |
333 | as for stop we allow `--all` and `-y`
334 |
335 | ## HACKS
336 |
337 | ### Bundler
338 |
339 | Bundler has the bad behavior to load `Gemfile` from your current path, so if your `daemons` (ex: [githubwatcher](https://github.com/daddye/githubwatcher))
340 | is shipped with their own `Gemfile` to prevent errors you must insert that line:
341 |
342 | ``` ruby
343 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__) # edit matching your Gemfile path
344 | ```
345 |
346 | ### Rails/Padrino prevent memory leaks
347 |
348 | I highly suggest to use `fork` and `before` filters when you are using `forever` with frameworks, this since running same job on our ruby will eat a lot of
349 | ram, so the better way that I found is that:
350 |
351 | ```rb
352 | Forever.run :fork => true do
353 | before :each do
354 | require '/config/boot' # here the rails/padrino environment
355 | end
356 |
357 | every 10.seconds, :at => ['12:00', '00:00'] do
358 | Project.all(&:perform_long_task)
359 | end
360 |
361 | every 1.minute do
362 | Account.all.map(&:send_emails)
363 | end
364 | end
365 | ```
366 |
367 | This is similar to create a new process i.e.:
368 |
369 | ```rb
370 | Process.fork do
371 | require '/config/boot'
372 | my_long_jobs
373 | Project.all(&:perform_long_task)
374 | end
375 | Process.waitall
376 | ```
377 |
378 | ## /etc/init.d script sample
379 |
380 | Use the following script if you want **foreverb** to fire up all of your daemons at boot time in Linux:
381 |
382 | ```#!/bin/sh
383 | ### BEGIN INIT INFO
384 | # Provides: foreverb
385 | # Required-Start: $local_fs $remote_fs
386 | # Required-Stop: $local_fs $remote_fs
387 | # Default-Start: 2 3 4 5
388 | # Default-Stop: S 0 1 6
389 | # Short-Description: foreverb initscript
390 | # Description: foreverb
391 | ### END INIT INFO
392 |
393 | # Do NOT "set -e"
394 |
395 | DAEMON="foreverb"
396 | USER="username"
397 | SCRIPT_NAME="/etc/init.d/foreverb-username"
398 |
399 | case "$1" in
400 | start)
401 | su -l $USER -c "$DAEMON start --all --yes"
402 | ;;
403 | stop)
404 | su -l $USER -c "$DAEMON stop --all --yes"
405 | ;;
406 | restart)
407 | su -l $USER -c "$DAEMON restart --all --yes"
408 | ;;
409 | *)
410 | echo "Usage: $SCRIPT_NAME {start|stop|restart}" >&2
411 | exit 3
412 | ;;
413 | esac
414 |
415 | :
416 | ```
417 |
418 | You'll have to create one script per each user foreverb runs on.
419 | After creating the file, make it executable:
420 |
421 | chmod +x /etc/init.d/foreverb-username
422 |
423 | and add it to the system's boot:
424 |
425 | * RedHat:
426 | ```
427 | sudo /sbin/chkconfig --level 345 foreverb-username on
428 | ```
429 |
430 | * Debian/Ubuntu:
431 | ```
432 | sudo /usr/sbin/update-rc.d -f foreverb-username defaults
433 | ```
434 |
435 | * Gentoo:
436 | ```
437 | sudo rc-update add foreverb-username default
438 | ```
439 |
440 |
441 | ## Extras
442 |
443 | To see a most comprensive app running _foreverb_ + _growl_ see [githubwatcher gem](https://github.com/daddye/githubwatcher)
444 |
445 | ## Author
446 |
447 | DAddYE, you can follow me on twitter [@daddye](http://twitter.com/daddye) or take a look at my site [daddye.it](http://www.daddye.it)
448 |
449 | ## Copyright
450 |
451 | Copyright (C) 2011 Davide D'Agostino - [@daddye](http://twitter.com/daddye)
452 |
453 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
454 | associated documentation files (the “Software”), to deal in the Software without restriction, including without
455 | limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
456 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
457 |
458 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
459 |
460 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
461 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM,
462 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
463 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
464 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems' unless defined?(Gem)
2 | require 'bundler/gem_tasks'
3 | require 'rspec/core/rake_task'
4 | require 'rake/testtask'
5 |
6 | %w(install release).each do |task|
7 | Rake::Task[task].enhance do
8 | sh "rm -rf pkg"
9 | end
10 | end
11 |
12 | desc 'Bump version on github'
13 | task :bump do
14 | if `git status -s`.strip == ''
15 | puts "\e[31mNothing to commit (working directory clean)\e[0m"
16 | else
17 | version = Bundler.load_gemspec(Dir[File.expand_path('../*.gemspec', __FILE__)].first).version
18 | sh "git add .; git commit -a -m \"Bump to version #{version}\""
19 | end
20 | end
21 |
22 | Rake::TestTask.new(:spec) do |t|
23 | t.test_files = Dir['spec/**/*_spec.rb']
24 | t.verbose = true
25 | end
26 |
27 | namespace :example do
28 | Dir['./examples/*'].each do |path|
29 | next if File.directory?(path)
30 | name = File.basename(path)
31 | desc "Run example #{name}"
32 | task name, :fork do |t, args|
33 | ENV['FORK'] = args[:fork]
34 | log = File.expand_path("../log/#{name}.log", path)
35 | exec "#{Gem.ruby} #{path} && sleep 5 && tail -f -n 150 #{log}; #{path} stop"
36 | end
37 | end
38 | end
39 |
40 | task :release => :bump
41 | task :default => :spec
42 |
--------------------------------------------------------------------------------
/bin/foreverb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 | require 'rubygems' unless defined?(Gem)
3 | require File.expand_path('../../lib/forever/version.rb', __FILE__)
4 | require 'thor'
5 | require 'yaml'
6 | require 'fileutils'
7 |
8 | FOREVER_PATH = ENV['FOREVER_PATH'] ||= File.expand_path("~/.foreverb") unless defined?(FOREVER_PATH)
9 |
10 | class CLI < Thor
11 |
12 | desc "list", "List Forever running daemons"
13 | method_option :monitor, :type => :boolean, :aliases => "-m", :default => false, :desc => "Show memory and cpu usage with ps"
14 | def list
15 | say "Your config is empty, so no deamons was found.", :red if config.empty? && !options.monitor
16 |
17 | if options.monitor
18 | print_table([%w(PID RSS CPU CMD), *ps])
19 | else
20 | config.each do |conf|
21 | status = begin
22 | pid = File.read(conf[:pid]).to_i
23 | Process.kill(0, pid)
24 | "RUNNING"
25 | rescue Errno::ESRCH, Errno::ENOENT
26 | "NOT RUNNING"
27 | rescue Errno::EPERM
28 | "RUNNING"
29 | end
30 | say_status status, conf[:file], status =~ /^RUNNING/ ? :green : :red
31 | end
32 | say "Reading config from: #{FOREVER_PATH}", :blue
33 | end
34 | end
35 |
36 | desc "stop [DAEMON] [--all] [--yes]", "Stop one or more matching daemons"
37 | method_option :all, :type => :boolean, :aliases => "-a", :desc => "All matching daemons"
38 | method_option :yes, :type => :boolean, :aliases => "-y", :desc => "Don't ask permission to kill daemon"
39 | def stop(daemon=nil)
40 | find(daemon, :multiple => options.all).each do |conf|
41 | stop_daemon(conf) if options.yes || yes?("Do you want really stop \e[1m#{conf[:file]}\e[0m?")
42 | end
43 | end
44 |
45 | desc "kill [DAEMON] [--all] [--yes]", "Kill one or more matching daemons"
46 | method_option :all, :type => :boolean, :aliases => "-a", :desc => "All matching daemons"
47 | method_option :yes, :type => :boolean, :aliases => "-y", :desc => "Don't ask permission to kill daemon"
48 | def kill(daemon=nil)
49 | find(daemon, :multiple => options.all).each do |conf|
50 | if options.yes || yes?("Do you want really kill \e[1m#{conf[:file]}\e[0m?")
51 | say_status "KILLING", conf[:file]
52 | begin
53 | pid = File.read(conf[:pid]).to_i
54 | Process.kill(:INT, pid)
55 | rescue Exception => e
56 | say_status "ERROR", e.message, :red
57 | end
58 | end
59 | end
60 | end
61 |
62 | desc "start [DAEMON] [--all] [--yes]", "Start one or more matching daemons"
63 | method_option :all, :type => :boolean, :aliases => "-a", :desc => "All matching daemons"
64 | method_option :yes, :type => :boolean, :aliases => "-y", :desc => "Don't ask permission to start the daemon"
65 | def start(daemon=nil)
66 | find(daemon, :multiple => options.all).each do |conf|
67 | system(conf[:file]) if options.yes || yes?("Do you want really start \e[1m#{conf[:file]}\e[0m?")
68 | end
69 | end
70 |
71 | desc "restart [DAEMON] [--all] [--yes]", "Restart one or more matching daemons"
72 | method_option :all, :type => :boolean, :aliases => "-a", :desc => "All matching daemons"
73 | method_option :yes, :type => :boolean, :aliases => "-y", :desc => "Don't ask permission to start the daemon"
74 | def restart(daemon=nil)
75 | invoke :start
76 | end
77 |
78 | desc "tail [DAEMON]", "Tail log of first matching daemon"
79 | method_option :lines, :aliases => "-n", :default => 150, :desc => "How many lines show?"
80 | def tail(daemon)
81 | found = find(daemon)[0]
82 | return unless found
83 | system "tail -f -n #{options.lines} #{found[:log]}"
84 | end
85 |
86 | desc "update [DAEMON] [--all] [--yes]", "Update config from one or more matching daemons"
87 | method_option :all, :type => :boolean, :aliases => "-a", :desc => "All matching daemons"
88 | method_option :yes, :type => :boolean, :aliases => "-y", :desc => "Don't ask permission to start the daemon"
89 | def update(daemon=nil)
90 | match = find(daemon, :multiple => options.all)
91 | return if match.empty?
92 | FileUtils.rm_rf(FOREVER_PATH)
93 | match.each do |conf|
94 | system(conf[:file], 'update') if options.yes || yes?("Do you want really update config from \e[1m#{conf[:file]}\e[0m?")
95 | end
96 | end
97 |
98 | desc "remove [DAEMON] [--all]", "Remove the config of a daemon from foreverb"
99 | method_option :all, :type => :boolean, :aliases => "-a", :desc => "All matching daemons"
100 | method_option :yes, :type => :boolean, :aliases => "-y", :desc => "Don't ask permission to remove the daemon"
101 | def remove(daemon=nil)
102 | say "You must provide a daemon name or provide --all option", :red and return if daemon.nil? && !options.all
103 | new_config = config.delete_if do |conf|
104 | if conf[:file] =~ /#{daemon}/
105 | if options.yes || yes?("Do you really want to remove the daemon \e[1m#{conf[:file]}\e[0m?")
106 | stop_daemon(conf)
107 | say "\e[1m#{conf[:file]}\e[0m removed."
108 | true
109 | else
110 | say "\e[1m#{conf[:file]}\e[0m remains on the list."
111 | false
112 | end
113 | else
114 | false
115 | end
116 | end
117 | write_config! new_config
118 | end
119 |
120 | map "--version" => :version
121 | desc "version", "show the version number"
122 | def version
123 | say "Foreverb v.#{Forever::VERSION}", :green
124 | end
125 |
126 | private
127 | def find(daemon, options={})
128 | multiple = options.delete(:multiple)
129 | say "You must provide a daemon name or provide --all option", :red and return [] if daemon.nil? && !multiple
130 | found = multiple ? config : config.find_all { |conf| conf[:file] =~ /#{daemon}/ }
131 | say "Daemon(s) matching '#{daemon}' not found", :red if found.empty? && !daemon.nil?
132 | say "Daemons not found", :red if found.empty? && nil && daemon.nil?
133 | found
134 | end
135 |
136 | def find_all(daemon)
137 | find(daemon, :multiple => true)
138 | end
139 |
140 | def config
141 | File.exist?(FOREVER_PATH) ? YAML.load_file(FOREVER_PATH) : []
142 | end
143 |
144 | def ps
145 | # This is horrible command, but how we can keep compatiblity between darwin and *unix ?
146 | result = `ps axo pid,rss,pcpu,command | grep -vE "^USER|grep" | grep Forever: | awk '{print $1"\t"$2"\t"$3"\t"$4" "$5" "$6}'`
147 | result = result.chomp.split("\n").map { |line| line.split("\t") }
148 | result = result.sort { |a,b| b[1].to_i <=> a[1].to_i }
149 | result.each { |column| column[1] = "%d Mb" % [column[1].to_i / 1024] }
150 | result.each { |column| column[2] = "%s %" % [column[2]] }
151 | result
152 | end
153 |
154 | def write_config!(new_config)
155 | File.open(FOREVER_PATH, "w") { |f| f.write new_config.to_yaml }
156 | end
157 |
158 | def stop_daemon(conf)
159 | say_status "STOPPING", conf[:file]
160 | begin
161 | pid = File.read(conf[:pid]).to_i
162 | Process.kill(:INT, pid)
163 | rescue Exception => e
164 | say_status "ERROR", e.message, :red
165 | end
166 | end
167 | end
168 |
169 | ARGV << "-h" if ARGV.empty?
170 | CLI.start(ARGV)
171 |
--------------------------------------------------------------------------------
/examples/complex:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 | require 'rubygems' unless defined?(Gem)
3 | require 'bundler/setup'
4 | require 'forever'
5 |
6 | Forever.run :fork => ENV['FORK'] do
7 | dir File.expand_path('../', __FILE__) # Default is ../../__FILE__
8 |
9 | every 5.seconds do
10 | puts 'every 5 seconds'
11 | end
12 |
13 | on_ready do
14 | puts "All jobs will will wait me for 1 second"; sleep 1
15 | end
16 |
17 | every 30.seconds do
18 | puts "Every 30 seconds from start with boom"
19 | raise "woooooa"
20 | end
21 |
22 | every 1.seconds, :at => "#{Time.now.hour}:#{Time.now.min+1}" do
23 | puts "Every one second but first call at #{Time.now.hour}:#{Time.now.min}"
24 | end
25 |
26 | every 10.seconds do
27 | puts "Every 10 second"
28 | end
29 |
30 | every 20.seconds do
31 | puts "Every 20 second"
32 | end
33 |
34 | every 15.seconds do
35 | puts "Every 15 seconds, but my task requires 10 seconds"; sleep 10
36 | end
37 |
38 | every 10.seconds, :at => [":#{Time.now.min+1}", ":#{Time.now.min+2}"] do
39 | puts "Every 10 seconds but first call at xx:#{Time.now.min}"
40 | end
41 |
42 | on_error do |e|
43 | puts "Boom raised: #{e.message}"
44 | end
45 |
46 | on_exit do
47 | puts "Bye bye"
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/examples/simple:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 | require 'rubygems' unless defined?(Gem)
3 | require 'bundler/setup'
4 | require 'forever'
5 |
6 | Forever.run :fork => !!ENV['FORK'] do
7 | dir File.expand_path('../', __FILE__) # Default is ../../__FILE__
8 | log File.join(dir, "#{name}.log")
9 | pid File.join(dir, "#{name}.pid")
10 |
11 | before :all do
12 | puts 'before all'
13 | end
14 |
15 | before :each do
16 | puts 'before each'
17 | end
18 |
19 | after :all do
20 | puts 'after all'
21 | end
22 |
23 | after :each do
24 | puts 'after each'
25 | end
26 |
27 | every 1.seconds do
28 | puts 'wait me 10 seconds'
29 | sleep 10
30 | end
31 |
32 | every 2.seconds do
33 | puts 'every 2 seconds'
34 | end
35 |
36 | every 10.seconds do
37 | raise 'Arg....'
38 | end
39 |
40 | on_ready do
41 | puts "All jobs will will wait me for 1 second"; sleep 1
42 | end
43 |
44 | on_error do |e|
45 | puts '-' * 30
46 | puts e
47 | puts '-' * 30
48 | end
49 |
50 | on_exit do
51 | puts "Bye bye"
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/examples/stress:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 | require 'rubygems' unless defined?(Gem)
3 | require 'bundler/setup'
4 | require 'forever'
5 |
6 | Forever.run :fork => ENV['FORK'] do
7 | dir File.expand_path('../', __FILE__) # Default is ../../__FILE__
8 |
9 | (1..40).each do |i|
10 | every(i.seconds) { puts 'Every %d seconds' % i; sleep i }
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/foreverb.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "forever/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "foreverb"
7 | s.version = Forever::VERSION
8 | s.authors = ["DAddYE"]
9 | s.email = ["d.dagostino@lipsiasoft.com"]
10 | s.homepage = "https://github.com/daddye/forever"
11 | s.summary = %q{Small daemon framework for ruby}
12 | s.description = %q{Small daemon framework for ruby, with logging, error handler, scheduling and much more.}
13 |
14 | s.rubyforge_project = "foreverb"
15 |
16 | s.files = `git ls-files`.split("\n")
17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19 | s.require_paths = %w(lib)
20 | s.add_dependency 'thor', '>=0.15.0'
21 | s.add_development_dependency 'minitest'
22 | s.add_development_dependency 'rspec'
23 | end
24 |
--------------------------------------------------------------------------------
/lib/forever.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 | require 'forever/extensions'
3 | require 'forever/job'
4 | require 'forever/base'
5 | require 'forever/version'
6 |
7 | FOREVER_PATH = ENV['FOREVER_PATH'] ||= File.expand_path("~/.foreverb") unless defined?(FOREVER_PATH)
8 | path = File.dirname(FOREVER_PATH)
9 | Dir.mkdir(path) unless File.exist?(path)
10 |
11 | module Forever
12 | extend self
13 |
14 | def run(options={}, &block)
15 | caller_file = caller(1).map { |line| line.split(/:(?=\d|in )/)[0,1] }.flatten.first
16 | options[:file] ||= File.expand_path(caller_file)
17 | options[:dir] ||= File.expand_path('../../', options[:file]) # => we presume we are calling it from a bin|script dir
18 | Base.new(options, &block)
19 | end # run
20 | end # Forever
21 |
--------------------------------------------------------------------------------
/lib/forever/base.rb:
--------------------------------------------------------------------------------
1 | require 'fileutils'
2 |
3 | module Forever
4 |
5 | class Base
6 | attr_reader :started_at
7 |
8 | def initialize(options={}, &block)
9 | @options = options
10 | forking = options.delete(:fork)
11 |
12 | # Run others methods
13 | options.each { |k,v| send(k, v) if respond_to?(k) }
14 |
15 | instance_eval(&block)
16 |
17 | # Setup directories
18 | Dir.chdir(dir)
19 | Dir.mkdir(tmp) unless File.exist?(tmp)
20 | Dir.mkdir(File.dirname(log)) if log && !File.exist?(File.dirname(log))
21 |
22 | write_config!
23 |
24 | case ARGV[0]
25 | when 'config'
26 | print config.to_yaml
27 | exit
28 | when 'start', 'restart', 'up', nil
29 | stop
30 | when 'run', 'live'
31 | detach = false
32 | stop
33 | when 'stop'
34 | stop
35 | exit
36 | when 'kill'
37 | stop!
38 | exit
39 | when 'update'
40 | print "[\e[90m%s\e[0m] Config written in \e[1m%s\e[0m\n" % [name, FOREVER_PATH]
41 | exit
42 | when 'remove'
43 | stop
44 | remove
45 | exit
46 | else
47 | print <<-RUBY.gsub(/ {10}/,'') % name
48 | Usage: \e[1m./%s\e[0m [start|stop|kill|restart|config|update]
49 |
50 | Commands:
51 |
52 | start stop (if present) the daemon and perform a start
53 | live run in no-deamon mode
54 | stop stop the daemon if a during when it is idle
55 | restart same as start
56 | kill force stop by sending a KILL signal to the process
57 | config show the current daemons config
58 | update update the daemon config
59 | remove removes the daemon config
60 |
61 | RUBY
62 | exit
63 | end
64 |
65 | clean_tmp!
66 |
67 | # Enable REE - http://www.rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
68 | GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
69 |
70 | maybe_fork(detach) do
71 | Process.setsid if detach != false
72 |
73 | $0 = "Forever: #{$0}" unless ENV['DONT_TOUCH_PS']
74 | print "[\e[90m%s\e[0m] Process %s with pid \e[1m%d\e[0m with \e[1m%s\e[0m and Forever v.%s\n" %
75 | [name, detach != false ? :daemonized : :running, Process.pid, forking ? :fork : :thread, Forever::VERSION]
76 |
77 | %w(INT TERM).each { |signal| trap(signal) { stop! } }
78 | trap(:HUP) do
79 | IO.open(1, 'w'){ |s| s.puts config }
80 | end
81 |
82 | File.open(pid, "w") { |f| f.write(Process.pid.to_s) } if pid
83 |
84 | stream = log ? File.new(log, @options[:append_log] ? 'a' : 'w') : File.open('/dev/null', 'w')
85 | stream.sync = true
86 |
87 | STDOUT.reopen(stream)
88 | STDERR.reopen(STDOUT)
89 |
90 | @started_at = Time.now
91 |
92 | # Invoke our before :all filters
93 | filters[:before][:all].each { |block| safe_call(block) }
94 |
95 | # Store pids of childs
96 | pids = []
97 |
98 | # Start deamons
99 | until stopping?
100 | current_queue = 1
101 |
102 | jobs.each do |job|
103 | next unless job.time?(Time.now)
104 | if queue && current_queue > queue
105 | puts "\n\nThe queue limit of #{queue} has been exceeded.\n\n"
106 | on_limit_exceeded ? on_limit_exceeded.call : sleep(60)
107 | break
108 | end
109 | if forking
110 | begin
111 | GC.start
112 | pids << Process.detach(fork { job_call(job) })
113 | rescue Errno::EAGAIN
114 | puts "\n\nWait all processes since os cannot create a new one\n\n"
115 | Process.waitall
116 | end
117 | else
118 | Thread.new { job_call(job) }
119 | end
120 | current_queue += 1
121 | end
122 |
123 | # Detach zombies, our ps will be happier
124 | pids.delete_if { |p| p.stop? }
125 |
126 | sleep 0.5
127 | end
128 |
129 |
130 | # Invoke our after :all filters
131 | filters[:after][:all].each { |block| safe_call(block) }
132 |
133 | # If we are here it means we are exiting so we can remove the pid and pending stop.txt
134 | clean_tmp!
135 | end
136 |
137 | self
138 | end
139 |
140 | ##
141 | # Define a new job task
142 | #
143 | # Example:
144 | # every 1.second, :at => '12:00' do
145 | # my_long_task
146 | # end
147 | #
148 | def every(period, options={}, &block)
149 | jobs << Forever::Job.new(period, options.merge!(:dir => dir), &block)
150 | end
151 |
152 | ##
153 | # Our job list
154 | #
155 | def jobs
156 | @_jobs ||= []
157 | end
158 |
159 | ##
160 | # Caller file
161 | #
162 | def file(value=nil)
163 | value ? @_file = value : @_file
164 | end
165 |
166 | ##
167 | # Daemon name
168 | #
169 | def name
170 | File.basename(file, '.*')
171 | end
172 |
173 | ##
174 | # Queue size
175 | #
176 | def queue(value=nil)
177 | value ? @_queue = value : @_queue
178 | end
179 |
180 | ##
181 | # Base working Directory
182 | #
183 | def dir(value=nil)
184 | value ? @_dir = value : @_dir
185 | end
186 | alias :workspace :dir
187 |
188 | ##
189 | # Temp directory, used to store pids and jobs status
190 | #
191 | def tmp
192 | File.join(dir, 'tmp')
193 | end
194 |
195 | ##
196 | # File were we redirect STOUT and STDERR, can be false.
197 | #
198 | # Default: dir + 'log/[process_name].log'
199 | #
200 | def log(value=nil)
201 | @_log ||= File.join(dir, "log/#{name}.log") if exists?(dir, file)
202 | value.nil? ? @_log : @_log = value
203 | end
204 |
205 | ##
206 | # File were we store pid
207 | #
208 | # Default: dir + 'tmp/[process_name].pid'
209 | #
210 | def pid(value=nil)
211 | @_pid ||= File.join(tmp, "#{name}.pid") if exists?(dir, file)
212 | value.nil? ? @_pid : @_pid = value
213 | end
214 |
215 | ##
216 | # Search if there is a running process and stop it
217 | #
218 | def stop!
219 | FileUtils.rm_f(stop_txt)
220 | if running?
221 | pid_was = File.read(pid).to_i
222 | print "[\e[90m%s\e[0m] Killing process \e[1m%d\e[0m...\n" % [name, pid_was]
223 | filters[:after][:all].each { |block| safe_call(block) }
224 | clean_tmp!
225 | Process.kill(:KILL, pid_was)
226 | else
227 | print "[\e[90m%s\e[0m] Process with \e[1mnot found\e[0m" % name
228 | end
229 | end
230 |
231 | ##
232 | # Perform a soft stop
233 | #
234 | def stop
235 | if running?
236 | print "[\e[90m%s\e[0m] Waiting the daemon\'s death " % name
237 | FileUtils.touch(stop_txt)
238 | while running?(true)
239 | print '.'; $stdout.flush
240 | sleep 1
241 | end
242 | print " \e[1mDONE\e[0m\n"
243 | end
244 | end
245 |
246 | ##
247 | # Remove the daemon from the config file
248 | #
249 | def remove
250 | print "[\e[90m%s\e[0m] Removed the daemon from the config " % name
251 | config_was = File.exist?(FOREVER_PATH) ? YAML.load_file(FOREVER_PATH) : []
252 | config_was.delete_if { |conf| conf[:file] == file }
253 | File.open(FOREVER_PATH, "w") { |f| f.write config_was.to_yaml }
254 | end
255 |
256 | ##
257 | # Callback raised when an error occour
258 | #
259 | def on_error(&block)
260 | block_given? ? @_on_error = block : @_on_error
261 | end
262 |
263 | ##
264 | # Callback raised when queue limit was exceeded
265 | #
266 | def on_limit_exceeded(&block)
267 | block_given? ? @_on_limit_exceeded = block : @_on_limit_exceeded
268 | end
269 |
270 | ##
271 | # Callback raised when at exit
272 | #
273 | def on_exit(&block)
274 | after(:all, &block)
275 | end
276 |
277 | ##
278 | # Callback to fire when the daemon start (blocking, not in thread)
279 | #
280 | def on_ready(&block)
281 | before(:all, &block)
282 | end
283 |
284 | ##
285 | # Returns true if the pid exist and the process is running
286 | #
287 | def running?(silent=false)
288 | if exists?(pid)
289 | current = File.read(pid).to_i
290 | print "[\e[90m%s\e[0m] Found pid \e[1m%d\e[0m...\n" % [name, current] unless silent
291 | else
292 | print "[\e[90m%s\e[0m] Pid \e[1mnot found\e[0m, process seems doesn't exist!\n" % name unless silent
293 | return false
294 | end
295 |
296 | is_running = begin
297 | Process.kill(0, current)
298 | rescue Errno::ESRCH
299 | false
300 | end
301 |
302 | is_running
303 | end
304 |
305 | ##
306 | # Before :all or :each jobs hook
307 | #
308 | def before(filter, &block)
309 | raise "Filter #{filter.inspect} not supported, available options are: :each, :all" unless [:each, :all].include?(filter)
310 | filters[:before][filter] << block
311 | end
312 |
313 | ##
314 | # After :all or :each jobs hook
315 | #
316 | def after(filter, &block)
317 | raise "Filter #{filter.inspect} not supported, available options are: :each, :all" unless [:each, :all].include?(filter)
318 | filters[:after][filter] << block
319 | end
320 |
321 | ##
322 | # Return config of current worker in a hash
323 | #
324 | def config
325 | { :dir => dir, :file => file, :log => log, :pid => pid }
326 | end
327 |
328 | ##
329 | # Convert forever object in a readable string showing current config
330 | #
331 | def to_s
332 | "#"
333 | end
334 | alias :inspect :to_s
335 |
336 | private
337 |
338 | def filters
339 | @_filters ||= {
340 | :before => { :each => [], :all => [] },
341 | :after => { :each => [], :all => [] }
342 | }
343 | end
344 |
345 | def stopping?
346 | File.exist?(stop_txt) && File.mtime(stop_txt) > started_at
347 | end
348 |
349 | def maybe_fork(detach,&block)
350 | if detach != false
351 | fork &block
352 | else
353 | yield
354 | end
355 | end
356 |
357 | def write_config!
358 | config_was = File.exist?(FOREVER_PATH) ? YAML.load_file(FOREVER_PATH) : []
359 | config_was.delete_if { |conf| conf.nil? || conf.empty? || conf[:file] == file }
360 | config_was << config
361 | File.open(FOREVER_PATH, "w") { |f| f.write config_was.to_yaml }
362 | end
363 |
364 | def exists?(*values)
365 | values.all? { |value| value && File.exist?(value) }
366 | end
367 |
368 | def job_call(job)
369 | return unless job.time?(Time.now)
370 | job.run!
371 | filters[:before][:each].each { |block| safe_call(block) }
372 | safe_call(job)
373 | filters[:after][:each].each { |block| safe_call(block) }
374 | ensure
375 | job.stop!
376 | end
377 |
378 | def safe_call(block)
379 | begin
380 | block.call
381 | rescue Exception => e
382 | puts "\n\n%s\n %s\n\n" % [e.message, e.backtrace.join("\n ")]
383 | on_error[e] if on_error
384 | end
385 | end
386 |
387 | def stop_txt
388 | @_stop_txt ||= File.join(tmp, 'stop.txt')
389 | end
390 |
391 | def clean_tmp!
392 | return unless File.exist?(tmp)
393 | Dir[File.join(tmp, '*.job')].each { |f| FileUtils.rm_rf(f) }
394 | FileUtils.rm_rf(pid)
395 | end
396 | end # Base
397 | end # Forever
398 |
--------------------------------------------------------------------------------
/lib/forever/extensions.rb:
--------------------------------------------------------------------------------
1 | LOG_FORMAT = "[%s] %s" unless defined?(LOG_FORMAT)
2 | DATE_FORMAT = "%d/%m %H:%M:%S" unless defined?(DATE_FORMAT)
3 |
4 | class Numeric
5 | def seconds; self; end
6 | alias :second :seconds
7 |
8 | def minutes; self * 60; end
9 | alias :minute :minutes
10 |
11 | def hours; self * 3600; end
12 | alias :hour :hours
13 |
14 | def days; self * 86400; end
15 | alias :day :days
16 | end
17 |
18 | module Kernel
19 | def puts(text="")
20 | text = LOG_FORMAT % [Time.now.strftime(DATE_FORMAT), text.to_s]
21 | text += "\n" unless text[-1] == ?\n
22 | print text; $stdout.flush
23 | text
24 | end
25 | alias :log :puts
26 | end
--------------------------------------------------------------------------------
/lib/forever/job.rb:
--------------------------------------------------------------------------------
1 | module Forever
2 | class Job
3 |
4 | def initialize(period, options, &block)
5 | @period = period
6 | @at = options[:at] ? parse_at(*options[:at]) : []
7 | @pid = File.join(options[:dir], "/tmp/#{object_id}.job")
8 | @block = block
9 | end
10 |
11 | def call
12 | @block.call
13 | end
14 |
15 | def run!
16 | File.open(@pid, 'w') { |f| f.write('running') }
17 | end
18 |
19 | def stop!
20 | File.open(@pid, 'w') { |f| f.write('idle') }
21 | end
22 |
23 | def running?
24 | File.exist?(@pid) && File.read(@pid) == 'running'
25 | end
26 |
27 | def last
28 | File.mtime(@pid)
29 | rescue Errno::ENOENT
30 | 0
31 | end
32 |
33 | def time?(t)
34 | elapsed_ready = (t - last).to_i >= @period
35 | time_ready = @at.empty? || @at.any? { |at| (at[0].empty? || t.hour == at[0].to_i) && (at[1].empty? || t.min == at[1].to_i) }
36 | !running? && elapsed_ready && time_ready
37 | end
38 |
39 | private
40 | def parse_at(*args)
41 | args.map do |at|
42 | raise "#{at} must be a string" unless at.is_a?(String)
43 | raise "#{at} has not a colon separator" unless at =~ /:/
44 | hour, min = at.split(":")
45 | min = '' if min.nil?
46 | raise "Failed to parse #{at}" if hour.to_i >= 24 || min.to_i >= 60
47 | [hour, min]
48 | end
49 | end
50 | end # Job
51 | end # Forever
52 |
--------------------------------------------------------------------------------
/lib/forever/version.rb:
--------------------------------------------------------------------------------
1 | module Forever
2 | VERSION = "0.3.3" unless defined?(Forever::VERSION)
3 | end
4 |
--------------------------------------------------------------------------------
/spec/cli_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../spec_helper', __FILE__)
2 |
3 | describe 'CLI' do
4 | def cli(task)
5 | `#{Gem.ruby} #{File.expand_path('../../bin/foreverb', __FILE__)} #{task}`
6 | end
7 |
8 | it 'should list daemons' do
9 | cli('list').must_match(/Your config is empty/)
10 | cli('list').must_match(FOREVER_PATH)
11 | cli('list -m').must_match(/PID RSS CPU CMD/)
12 | run_example
13 | cli('list').must_match(/RUNNING/)
14 | cli('list -m').must_match(/Forever:\s/)
15 | end
16 |
17 | it "should stop daemons" do
18 | run_example
19 | cli('list').must_match(/RUNNING/)
20 | result = cli('stop -a -y')
21 | result.must_match(/STOPPING/)
22 | result.wont_match(/ERROR/)
23 | cli('list').must_match(/NOT RUNNING/)
24 | end
25 |
26 | it 'should kill daemons' do
27 | run_example
28 | cli('list').must_match(/RUNNING/)
29 | result = cli('kill -a -y')
30 | result.must_match(/KILLING/)
31 | result.wont_match(/ERROR/)
32 | cli('list').must_match(/NOT RUNNING/)
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/foreverb_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../spec_helper', __FILE__)
2 |
3 | describe Forever do
4 |
5 | it 'should set a basic config' do
6 | run_example
7 | @forever.dir.must_equal File.expand_path("../../", __FILE__)
8 | @forever.log.must_equal File.join(@forever.dir, 'log', File.basename(example_filename, '.*') + '.log')
9 | @forever.pid.must_equal File.join(@forever.dir, 'tmp', File.basename(example_filename, '.*') + '.pid')
10 | @forever.file.must_equal example_filename
11 | config = YAML.load_file(FOREVER_PATH)
12 | config[0][:file].must_equal example_filename
13 | config[0][:log].must_equal @forever.log
14 | config[0][:pid].must_equal @forever.pid
15 | end
16 |
17 | it 'should set a custom config' do
18 | run_example(:dir => Dir.tmpdir)
19 | @forever.dir.must_equal Dir.tmpdir
20 | @forever.log.must_equal File.join(@forever.dir, 'log', File.basename(example_filename, '.*') + '.log')
21 | @forever.pid.must_equal File.join(@forever.dir, 'tmp', File.basename(example_filename, '.*') + '.pid')
22 | @forever.file.must_equal example_filename
23 | config = YAML.load_file(FOREVER_PATH)
24 | config[0][:file].must_equal example_filename
25 | config[0][:log].must_equal @forever.log
26 | config[0][:pid].must_equal @forever.pid
27 | end
28 |
29 | it 'should launch a daemon with threads with soft stop' do
30 | run_example
31 | sleep 0.1 while !File.exist?(@forever.pid)
32 | pid = File.read(@forever.pid).to_i
33 | sleep 1
34 | out, err = capture_io { @forever.stop }
35 | out.must_match(/waiting the daemon's death/i)
36 | out.must_match(/#{pid}/)
37 | end
38 |
39 | it 'should launch a daemon with threads with soft stop' do
40 | run_example(:fork => true)
41 | sleep 0.1 while !File.exist?(@forever.pid)
42 | pid = File.read(@forever.pid).to_i
43 | sleep 1
44 | out, err = capture_io { @forever.stop }
45 | out.must_match(/waiting the daemon's death/i)
46 | out.must_match(/#{pid}/)
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | FOREVER_PATH = ENV['FOREVER_PATH'] ||= File.expand_path("../tmp/db.yaml", __FILE__)
2 | require 'rubygems' unless defined?(Gem)
3 | require 'bundler/setup'
4 | require 'minitest/autorun'
5 | require 'forever'
6 | require 'fileutils'
7 | require 'tmpdir'
8 |
9 | $dir = File.expand_path('.')
10 |
11 | class MiniTest::Spec
12 | def run_example(options={}, &block)
13 | block = proc { every(1.second) { puts 'foo' } } unless block_given?
14 | capture_io { @forever = Forever.run(options, &block) }
15 | end
16 |
17 | let(:example_filename) { File.expand_path(__FILE__) }
18 |
19 | before do
20 | Dir.chdir($dir)
21 | FileUtils.rm_rf File.dirname(FOREVER_PATH)
22 | Dir.mkdir File.dirname(FOREVER_PATH)
23 | ARGV.clear
24 | end
25 |
26 | after do
27 | FileUtils.rm_rf(File.dirname(FOREVER_PATH))
28 | if @forever
29 | capture_io { @forever.stop! }
30 | FileUtils.rm_rf(File.dirname(@forever.log)) if @forever.log
31 | FileUtils.rm_rf(File.dirname(@forever.pid)) if @forever.pid # this is deleted by Forever
32 | end
33 | Dir.chdir($dir)
34 | ARGV.clear
35 | end
36 | end
37 |
--------------------------------------------------------------------------------