├── .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 | --------------------------------------------------------------------------------