diff --git a/README.rdoc b/README.rdoc index c2bcb1c..3f04992 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,15 +1,15 @@ == Introduction -Whenever is a Ruby gem that provides a clear syntax for defining cron jobs. It outputs valid cron syntax and can even write your crontab file for you. It is designed to work well with Rails applications and can be deployed with Capistrano. Whenever works fine independently as well. - -Ryan Bates created a great Railscast about Whenever: http://railscasts.com/episodes/164-cron-in-ruby - -Discussion: http://groups.google.com/group/whenever-gem +Whenever is a Ruby gem that provides a clear syntax for writing and deploying cron jobs. == Installation -$ sudo gem install whenever + $ gem install whenever +Or with Bundler in your Gemfile. + + gem 'whenever', :require => false + == Getting started $ cd /my/rails/app @@ -41,7 +41,7 @@ More examples on the wiki: http://wiki.github.com/javan/whenever/instructions-an == Define your own job types -Whenever ships with three pre-defined job types: command, runner, and rake. You can define your own with job_type. +Whenever ships with three pre-defined job types: command, runner, and rake. You can define your own with `job_type`. For example: @@ -51,15 +51,29 @@ For example: awesome "party", :fun_level => "extreme" end -Would run /usr/local/bin/awesome party extreme every two hours. :task is always replaced with the first argument, and any additional :whatevers are replaced with the options passed in or by variables that have been defined with set. +Would run `/usr/local/bin/awesome party extreme` every two hours. `:task` is always replaced with the first argument, and any additional `:whatevers` are replaced with the options passed in or by variables that have been defined with `set`. The default job types that ship with Whenever are defined like so: - job_type :command, ":task" - job_type :runner, "cd :path && script/runner -e :environment ':task'" - job_type :rake, "cd :path && RAILS_ENV=:environment /usr/bin/env rake :task" + job_type :command, ":task :output" + job_type :rake, "cd :path && RAILS_ENV=:environment rake :task :output" + job_type :runner, "cd :path && script/runner -e :environment ':task' :output" + +If a script/rails file is detected (like in a Rails 3 app), runner will be defined to fit: -If a :path is not set it will default to the directory in which whenever was executed. :environment will default to 'production'. + job_type :runner, "cd :path && script/rails runner -e :environment ':task' :output" + +If a `:path` is not set it will default to the directory in which `whenever` was executed. `:environment` will default to 'production'. `:output` will be replaced with your output redirection settings which you can read more about here: http://github.com/javan/whenever/wiki/Output-redirection-(logging-your-cron-jobs) + +All jobs are by default run with `bash -l -c 'command...'`. Among other things, this allows your cron jobs to play nice with RVM by loading the entire environment instead of cron's somewhat limited environment. Read more: http://blog.scoutapp.com/articles/2010/09/07/rvm-and-cron-in-production + +You can change this by setting your own job_template. + + set :job_template, "bash -l -c ':job'" + +Or set the job_template to nil to have your jobs execute normally. + + set :job_template, nil == Cron output @@ -81,19 +95,13 @@ In your "config/deploy.rb" file do something like: end end -This will update your crontab file, leaving any existing entries unharmed. When using the --update-crontab option, Whenever will only update the entries in your crontab file related to the current schedule.rb file. You can replace the #{application} with any identifying string you'd like. You can have any number of apps deploy to the same crontab file peacefully given they each use a different identifier. +This will update your crontab file, leaving any existing entries unharmed. When using the `--update-crontab` option, Whenever will only update the entries in your crontab file related to the current schedule.rb file. You can replace the `#{application}` with any identifying string you'd like. You can have any number of apps deploy to the same crontab file peacefully given they each use a different identifier. -If you wish to simply overwrite your crontab file each time you deploy, use the --write-crontab option. This is ideal if you are only working with one app and every crontab entry is contained in a single schedule.rb file. +If you wish to simply overwrite your crontab file each time you deploy, use the `--write-crontab` option. This is ideal if you are only working with one app and every crontab entry is contained in a single schedule.rb file. -By mixing and matching the --load-file and --user options with your various :roles in Capistrano it is entirely possible to deploy different crontab schedules under different users to all your various servers. Get creative! +By mixing and matching the `--load-file` and `--user` options with your various :roles in Capistrano it is entirely possible to deploy different crontab schedules under different users to all your various servers. Get creative! -If you want to override a variable (like your environment) at the time of deployment you can do so with the --set option: http://wiki.github.com/javan/whenever/setting-variables-on-the-fly - -== Credit - -Whenever was created for use at Inkling (http://inklingmarkets.com) where I work. Their take on it: http://blog.inklingmarkets.com/2009/02/whenever-easy-way-to-do-cron-jobs-from.html - -While building Whenever, I learned a lot by digging through the source code of Capistrano - http://github.com/jamis/capistrano +If you want to override a variable (like your environment) at the time of deployment you can do so with the `--set` option: http://wiki.github.com/javan/whenever/setting-variables-on-the-fly == Discussion / Feedback / Issues / Bugs @@ -101,6 +109,13 @@ For general discussion and questions, please use the google group: http://groups If you've found a genuine bug or issue, please use the Issues section on github: http://github.com/javan/whenever/issues +Ryan Bates created a great Railscast about Whenever: http://railscasts.com/episodes/164-cron-in-ruby +It's a little bit dated now, but remains a good introduction. + +== Credit + +Whenever was created for use at Inkling (http://inklingmarkets.com) where I work. Their take on it: http://blog.inklingmarkets.com/2009/02/whenever-easy-way-to-do-cron-jobs-from.html + == License Copyright (c) 2009+ Javan Makhmali diff --git a/lib/whenever/job.rb b/lib/whenever/job.rb index bf34632..c4be4fb 100644 --- a/lib/whenever/job.rb +++ b/lib/whenever/job.rb @@ -5,17 +5,25 @@ module Whenever def initialize(options = {}) @options = options - - @at = options[:at] + @at = options.delete(:at) + @template = options.delete(:template) + @job_template = options.delete(:job_template) || ":job" @options[:output] = Whenever::Output::Redirection.new(options[:output]).to_s if options.has_key?(:output) @options[:environment] ||= :production @options[:path] ||= Whenever.path end def output - @options[:template].dup.gsub(/:\w+/) do |key| + job = process_template(@template, @options).strip + process_template(@job_template, { :job => job }).strip + end + + protected + + def process_template(template, options) + template.gsub(/:\w+/) do |key| before_and_after = [$`[-1..-1], $'[0..0]] - option = @options[key.sub(':', '').to_sym] + option = options[key.sub(':', '').to_sym] if before_and_after.all? { |c| c == "'" } escape_single_quotes(option) @@ -26,8 +34,6 @@ module Whenever end end end - - protected def escape_single_quotes(str) str.gsub(/'/) { "'\\''" } diff --git a/lib/whenever/job_list.rb b/lib/whenever/job_list.rb index 7615b29..44565b2 100644 --- a/lib/whenever/job_list.rb +++ b/lib/whenever/job_list.rb @@ -2,7 +2,7 @@ module Whenever class JobList def initialize(options) - @jobs, @env, @set_variables = {}, {}, {} + @jobs, @env, @set_variables, @pre_set_variables = {}, {}, {}, {} case options when String @@ -25,7 +25,8 @@ module Whenever end def set(variable, value) - return if instance_variable_defined?("@#{variable}".to_sym) + variable = variable.to_sym + return if @pre_set_variables[variable] instance_variable_set("@#{variable}".to_sym, value) self.class.send(:attr_reader, variable.to_sym) @@ -59,9 +60,7 @@ module Whenever end end - def generate_cron_output - set_path_environment_variable - + def generate_cron_output [environment_variables, cron_jobs].compact.join end @@ -79,22 +78,13 @@ module Whenever pairs.each do |pair| next unless pair.index('=') variable, value = *pair.split('=') - set(variable.strip.to_sym, value.strip) unless variable.blank? || value.blank? + unless variable.blank? || value.blank? + variable = variable.strip.to_sym + set(variable, value.strip) + @pre_set_variables[variable] = value + end end end - - def set_path_environment_variable - return if path_should_not_be_set_automatically? - @env[:PATH] = read_path unless read_path.blank? - end - - def read_path - ENV['PATH'] if ENV - end - - def path_should_not_be_set_automatically? - @set_path_automatically === false || @env[:PATH] || @env["PATH"] - end def environment_variables return if @env.empty? diff --git a/lib/whenever/job_types/default.rb b/lib/whenever/job_types/default.rb index 9c69e23..1395e13 100644 --- a/lib/whenever/job_types/default.rb +++ b/lib/whenever/job_types/default.rb @@ -1,3 +1,6 @@ +# http://blog.scoutapp.com/articles/2010/09/07/rvm-and-cron-in-production +set :job_template, "/bin/bash -l -c ':job'" + job_type :command, ":task :output" job_type :rake, "cd :path && RAILS_ENV=:environment rake :task --silent :output" diff --git a/test/functional/command_line_test.rb b/test/functional/command_line_test.rb index 3281713..627424c 100644 --- a/test/functional/command_line_test.rb +++ b/test/functional/command_line_test.rb @@ -217,6 +217,7 @@ NEW_CRON setup do @output = Whenever.cron :set => 'environment=serious', :string => \ <<-file + set :job_template, nil set :environment, :silly set :path, '/my/path' every 2.hours do @@ -234,6 +235,7 @@ NEW_CRON setup do @output = Whenever.cron :set => 'environment=serious&path=/serious/path', :string => \ <<-file + set :job_template, nil set :environment, :silly set :path, '/silly/path' every 2.hours do @@ -251,6 +253,7 @@ NEW_CRON setup do @output = Whenever.cron :set => ' environment = serious& path =/serious/path', :string => \ <<-file + set :job_template, nil set :environment, :silly set :path, '/silly/path' every 2.hours do @@ -268,6 +271,7 @@ NEW_CRON setup do @output = Whenever.cron :set => ' environment=', :string => \ <<-file + set :job_template, nil set :environment, :silly set :path, '/silly/path' every 2.hours do diff --git a/test/functional/output_at_test.rb b/test/functional/output_at_test.rb index 595ccf5..a239655 100644 --- a/test/functional/output_at_test.rb +++ b/test/functional/output_at_test.rb @@ -6,6 +6,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every "weekday", :at => '5:02am' do command "blahblah" end @@ -21,6 +22,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every "weekday", :at => %w(5:02am 3:52pm) do command "blahblah" end @@ -37,6 +39,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every "weekday", :at => '5:02am, 3:52pm' do command "blahblah" end @@ -53,6 +56,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every "weekday", :at => '5:02am, 3:02pm' do command "blahblah" end @@ -68,6 +72,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every "mon,wed,fri", :at => '5:02am, 3:02pm' do command "blahblah" end @@ -83,6 +88,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :path, '/your/path' every "mon,wed,fri", :at => '5:02am, 3:02pm' do runner "blahblah" @@ -99,6 +105,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :path, '/your/path' every "mon,wed,fri", :at => '5:02am, 3:02pm' do rake "blah:blah" @@ -115,6 +122,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every [1.month, 1.day], :at => 'january 5:02am, june 17th at 2:22pm, june 3rd at 3:33am' do command "blahblah" end @@ -138,6 +146,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every :reboot do command "command_1" command "command_2" @@ -155,6 +164,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :path, '/your/path' every :day do rake "blah:blah" @@ -179,6 +189,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 5.minutes, :at => 1 do command "blahblah" end @@ -194,6 +205,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 4.minutes, :at => 2 do command "blahblah" end @@ -209,6 +221,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 3.minutes, :at => 7 do command "blahblah" end @@ -224,6 +237,7 @@ class OutputAtTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.minutes, :at => 27 do command "blahblah" end diff --git a/test/functional/output_default_defined_jobs_test.rb b/test/functional/output_default_defined_jobs_test.rb index 3b038dc..95de17c 100644 --- a/test/functional/output_default_defined_jobs_test.rb +++ b/test/functional/output_default_defined_jobs_test.rb @@ -4,10 +4,11 @@ class OutputDefaultDefinedJobsTest < Test::Unit::TestCase # command - context "A plain command" do + context "A plain command with the job template set to nil" do setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do command "blahblah" end @@ -19,12 +20,45 @@ class OutputDefaultDefinedJobsTest < Test::Unit::TestCase end end + context "A plain command with no job template set" do + setup do + @output = Whenever.cron \ + <<-file + every 2.hours do + command "blahblah" + end + file + end + + should "output the command with the default job template" do + assert_match /^.+ .+ .+ .+ \/bin\/bash -l -c 'blahblah'$/, @output + end + end + + context "A plain command that overrides the job_template set" do + setup do + @output = Whenever.cron \ + <<-file + set :job_template, "/bin/bash -l -c ':job'" + every 2.hours do + command "blahblah", :job_template => "/bin/sh -l -c ':job'" + end + file + end + + should "output the command using that job_template" do + assert_match /^.+ .+ .+ .+ \/bin\/sh -l -c 'blahblah'$/, @output + assert_no_match /bash/, @output + end + end + # runner context "A runner with path set" do setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :path, '/my/path' every 2.hours do runner 'blahblah' @@ -41,6 +75,7 @@ class OutputDefaultDefinedJobsTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :path, '/my/path' every 2.hours do runner "blahblah", :path => '/some/other/path' @@ -59,6 +94,7 @@ class OutputDefaultDefinedJobsTest < Test::Unit::TestCase File.expects(:exists?).with('/my/path/script/rails').returns(true) @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do runner 'blahblah' end @@ -76,6 +112,7 @@ class OutputDefaultDefinedJobsTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :path, '/my/path' every 2.hours do rake "blahblah" @@ -92,6 +129,7 @@ class OutputDefaultDefinedJobsTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :path, '/my/path' every 2.hours do rake "blahblah", :path => '/some/other/path' diff --git a/test/functional/output_defined_job_test.rb b/test/functional/output_defined_job_test.rb index d220893..a661590 100644 --- a/test/functional/output_defined_job_test.rb +++ b/test/functional/output_defined_job_test.rb @@ -6,6 +6,7 @@ class OutputDefinedJobTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil job_type :some_job, "before :task after" every 2.hours do some_job "during" @@ -22,6 +23,7 @@ class OutputDefinedJobTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil job_type :some_job, "before :task after :option1 :option2" every 2.hours do some_job "during", :option1 => 'happy', :option2 => 'birthday' @@ -38,6 +40,7 @@ class OutputDefinedJobTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil job_type :some_job, "before :task after :option1" set :option1, 'happy' every 2.hours do @@ -55,6 +58,7 @@ class OutputDefinedJobTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil job_type :some_job, "before :task after :option1" set :option1, 'global' every 2.hours do @@ -72,6 +76,7 @@ class OutputDefinedJobTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil job_type :some_job, "before :task after :option1" every 2.hours do some_job "during", :option2 => 'happy' @@ -90,6 +95,7 @@ class OutputDefinedJobTest < Test::Unit::TestCase @output = Whenever.cron \ <<-file + set :job_template, nil job_type :some_job, "cd :path && :task" every 2.hours do some_job 'blahblah' diff --git a/test/functional/output_env_test.rb b/test/functional/output_env_test.rb index 6deac9b..2dd3fa7 100644 --- a/test/functional/output_env_test.rb +++ b/test/functional/output_env_test.rb @@ -19,38 +19,5 @@ class OutputEnvTest < Test::Unit::TestCase assert_match "MAILTO=someone@example.com", @output end end - - context "No PATH environment variable set" do - setup do - Whenever::JobList.any_instance.expects(:read_path).at_least_once.returns('/usr/local/bin') - @output = Whenever.cron "" - end - - should "add a PATH variable based on the user's PATH" do - assert_match "PATH=/usr/local/bin", @output - end - end - - context "A PATH environment variable set" do - setup do - Whenever::JobList.stubs(:read_path).returns('/usr/local/bin') - @output = Whenever.cron "env :PATH, '/my/path'" - end - - should "use that path and the user's PATH" do - assert_match "PATH=/my/path", @output - assert_no_match /local/, @output - end - end - - context "No PATH set and instructed not to automatically load the user's path" do - setup do - @output = Whenever.cron "set :set_path_automatically, false" - end - - should "not have a PATH set" do - assert_no_match /PATH/, @output - end - end end \ No newline at end of file diff --git a/test/functional/output_redirection_test.rb b/test/functional/output_redirection_test.rb index 2b9a09f..d8a8bdf 100644 --- a/test/functional/output_redirection_test.rb +++ b/test/functional/output_redirection_test.rb @@ -6,6 +6,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :output, nil every 2.hours do command "blahblah" @@ -23,6 +24,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :output, 'logfile.log' every 2.hours do command "blahblah" @@ -39,6 +41,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do command "blahblah", :output => {:standard => 'dev_null', :error => 'dev_err'} end @@ -54,6 +57,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :output, 'logfile.log' every 2.hours do command "blahblah", :output => 'otherlog.log' @@ -71,6 +75,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :output, 'logfile.log' every 2.hours do command "blahblah", :output => {:error => 'dev_err', :standard => 'dev_null' } @@ -88,6 +93,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :output, 'logfile.log' every 2.hours do command "blahblah", :output => false @@ -105,6 +111,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron :set => 'output=otherlog.log', :string => \ <<-file + set :job_template, nil set :output, 'logfile.log' every 2.hours do command "blahblah" @@ -122,6 +129,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :output, {:error => 'dev_err', :standard => 'dev_null' } every 2.hours do command "blahblah" @@ -138,6 +146,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :output, {:error => 'dev_null'} every 2.hours do command "blahblah" @@ -154,6 +163,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :output, {:standard => 'dev_out'} every 2.hours do command "blahblah" @@ -170,6 +180,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do command "blahblah", :output => {:error => 'dev_err'} end @@ -185,6 +196,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do command "blahblah", :output => {:standard => 'dev_out'} end @@ -200,6 +212,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do command "blahblah", :output => {:standard => nil} end @@ -215,6 +228,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do command "blahblah", :output => {:error => nil} end @@ -230,6 +244,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do command "blahblah", :output => {:error => nil, :standard => nil} end @@ -245,6 +260,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do command "blahblah", :output => {:error => nil, :standard => 'my.log'} end @@ -260,6 +276,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil every 2.hours do command "blahblah", :output => {:error => 'my_error.log', :standard => nil} end @@ -275,6 +292,7 @@ class OutputRedirectionTest < Test::Unit::TestCase setup do @output = Whenever.cron \ <<-file + set :job_template, nil set :cron_log, "cron.log" every 2.hours do command "blahblah" diff --git a/test/unit/job_test.rb b/test/unit/job_test.rb index e607782..276865c 100644 --- a/test/unit/job_test.rb +++ b/test/unit/job_test.rb @@ -24,7 +24,6 @@ class JobTest < Test::Unit::TestCase context "A Job with quotes" do - should "output the :task if it's in single quotes" do job = new_job(:template => "':task'", :task => 'abc123') assert_equal %q('abc123'), job.output @@ -51,6 +50,23 @@ class JobTest < Test::Unit::TestCase assert_equal %q(before "quote -> \" <- quote" after), job.output end end + + context "A Job with a job_template" do + should "use the job template" do + job = new_job(:template => ':task', :task => 'abc123', :job_template => 'left :job right') + assert_equal 'left abc123 right', job.output + end + + should "escape single quotes" do + job = new_job(:template => "before ':task' after", :task => "quote -> ' <- quote", :job_template => "left ':job' right") + assert_equal %q(left 'before '\''quote -> '\\''\\'\\'''\\'' <- quote'\'' after' right), job.output + end + + should "escape double quotes" do + job = new_job(:template => 'before ":task" after', :task => 'quote -> " <- quote', :job_template => 'left ":job" right') + assert_equal %q(left "before \"quote -> \\\" <- quote\" after" right), job.output + end + end private