lowang-whenever 0.7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/wheneverize ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This file is based heavily on Capistrano's `capify` command
4
+
5
+ require 'optparse'
6
+ require 'fileutils'
7
+
8
+ OptionParser.new do |opts|
9
+ opts.banner = "Usage: #{File.basename($0)} [path]"
10
+
11
+ begin
12
+ opts.parse!(ARGV)
13
+ rescue OptionParser::ParseError => e
14
+ warn e.message
15
+ puts opts
16
+ exit 1
17
+ end
18
+ end
19
+
20
+ unless ARGV.empty?
21
+ if !File.exists?(ARGV.first)
22
+ abort "`#{ARGV.first}' does not exist."
23
+ elsif !File.directory?(ARGV.first)
24
+ abort "`#{ARGV.first}' is not a directory."
25
+ elsif ARGV.length > 1
26
+ abort "Too many arguments; please specify only the directory to wheneverize."
27
+ end
28
+ end
29
+
30
+ content = <<-FILE
31
+ # Use this file to easily define all of your cron jobs.
32
+ #
33
+ # It's helpful, but not entirely necessary to understand cron before proceeding.
34
+ # http://en.wikipedia.org/wiki/Cron
35
+
36
+ # Example:
37
+ #
38
+ # set :output, "/path/to/my/cron_log.log"
39
+ #
40
+ # every 2.hours do
41
+ # command "/usr/bin/some_great_command"
42
+ # runner "MyModel.some_method"
43
+ # rake "some:great:rake:task"
44
+ # end
45
+ #
46
+ # every 4.days do
47
+ # runner "AnotherModel.prune_old_records"
48
+ # end
49
+
50
+ # Learn more: http://github.com/javan/whenever
51
+ FILE
52
+
53
+ file = 'config/schedule.rb'
54
+ base = ARGV.empty? ? '.' : ARGV.shift
55
+
56
+ file = File.join(base, file)
57
+ if File.exists?(file)
58
+ warn "[skip] `#{file}' already exists"
59
+ elsif File.exists?(file.downcase)
60
+ warn "[skip] `#{file.downcase}' exists, which could conflict with `#{file}'"
61
+ elsif !File.exists?(File.dirname(file))
62
+ warn "[skip] directory `#{File.dirname(file)}' does not exist"
63
+ else
64
+ puts "[add] writing `#{file}'"
65
+ File.open(file, "w") { |f| f.write(content) }
66
+ end
67
+
68
+ puts "[done] wheneverized!"
data/lib/whenever.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'active_support'
2
+ require 'thread'
3
+
4
+ module Whenever
5
+ autoload :JobList, 'whenever/job_list'
6
+ autoload :Job, 'whenever/job'
7
+ autoload :CommandLine, 'whenever/command_line'
8
+
9
+ module Output
10
+ autoload :Cron, 'whenever/cron'
11
+ autoload :Redirection, 'whenever/output_redirection'
12
+ end
13
+
14
+ def self.cron(options)
15
+ Whenever::JobList.new(options).generate_cron_output
16
+ end
17
+
18
+ def self.path
19
+ Dir.pwd
20
+ end
21
+
22
+ def self.rails3?
23
+ File.exists?(File.join(path, 'script', 'rails'))
24
+ end
25
+
26
+ def self.bundler?
27
+ File.exists?(File.join(path, 'Gemfile'))
28
+ end
29
+ end
@@ -0,0 +1,62 @@
1
+ Capistrano::Configuration.instance(:must_exist).load do
2
+ _cset(:whenever_roles) { :db }
3
+ _cset(:whenever_command) { "whenever" }
4
+ _cset(:whenever_identifier) { fetch :application }
5
+ _cset(:whenever_environment) { fetch :rails_env, "production" }
6
+ _cset(:whenever_update_flags) { "--update-crontab #{fetch :whenever_identifier} --set environment=#{fetch :whenever_environment}" }
7
+ _cset(:whenever_clear_flags) { "--clear-crontab #{fetch :whenever_identifier}" }
8
+
9
+ # Disable cron jobs at the begining of a deploy.
10
+ after "deploy:update_code", "whenever:clear_crontab"
11
+ # Write the new cron jobs near the end.
12
+ after "deploy:symlink", "whenever:update_crontab"
13
+ # If anything goes wrong, undo.
14
+ after "deploy:rollback", "whenever:update_crontab"
15
+
16
+ namespace :whenever do
17
+ desc <<-DESC
18
+ Update application's crontab entries using Whenever. You can configure \
19
+ the command used to invoke Whenever by setting the :whenever_command \
20
+ variable, which can be used with Bundler to set the command to \
21
+ "bundle exec whenever". You can configure the identifier used by setting \
22
+ the :whenever_identifier variable, which defaults to the same value configured \
23
+ for the :application variable. You can configure the environment by setting \
24
+ the :whenever_environment variable, which defaults to the same value \
25
+ configured for the :rails_env variable which itself defaults to "production". \
26
+ Finally, you can completely override all arguments to the Whenever command \
27
+ by setting the :whenever_update_flags variable. Additionally you can configure \
28
+ which servers the crontab is updated on by setting the :whenever_roles variable.
29
+ DESC
30
+ task :update_crontab do
31
+ options = { :roles => fetch(:whenever_roles) }
32
+
33
+ if find_servers(options).any?
34
+ on_rollback do
35
+ if fetch :previous_release
36
+ run "cd #{fetch :previous_release} && #{fetch :whenever_command} #{fetch :whenever_update_flags}", options
37
+ else
38
+ run "cd #{fetch :release_path} && #{fetch :whenever_command} #{fetch :whenever_clear_flags}", options
39
+ end
40
+ end
41
+
42
+ run "cd #{fetch :current_path} && #{fetch :whenever_command} #{fetch :whenever_update_flags}", options
43
+ end
44
+ end
45
+
46
+ desc <<-DESC
47
+ Clear application's crontab entries using Whenever. You can configure \
48
+ the command used to invoke Whenever by setting the :whenever_command \
49
+ variable, which can be used with Bundler to set the command to \
50
+ "bundle exec whenever". You can configure the identifier used by setting \
51
+ the :whenever_identifier variable, which defaults to the same value configured \
52
+ for the :application variable. Finally, you can completely override all \
53
+ arguments to the Whenever command by setting the :whenever_clear_flags variable. \
54
+ Additionally you can configure which servers the crontab is cleared on by setting \
55
+ the :whenever_roles variable.
56
+ DESC
57
+ task :clear_crontab do
58
+ options = { :roles => whenever_roles }
59
+ run "cd #{fetch :release_path} && #{fetch :whenever_command} #{fetch :whenever_clear_flags}", options if find_servers(options).any?
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,133 @@
1
+ require 'fileutils'
2
+ require 'tempfile'
3
+
4
+ module Whenever
5
+ class CommandLine
6
+ def self.execute(options={})
7
+ new(options).run
8
+ end
9
+
10
+ def initialize(options={})
11
+ @options = options
12
+
13
+ @options[:file] ||= 'config/schedule.rb'
14
+ @options[:cut] ||= 0
15
+ @options[:identifier] ||= default_identifier
16
+
17
+ unless File.exists?(@options[:file])
18
+ warn("[fail] Can't find file: #{@options[:file]}")
19
+ exit(1)
20
+ end
21
+
22
+ if [@options[:update], @options[:write], @options[:clear]].compact.length > 1
23
+ warn("[fail] Can only update, write or clear. Choose one.")
24
+ exit(1)
25
+ end
26
+
27
+ unless @options[:cut].to_s =~ /[0-9]*/
28
+ warn("[fail] Can't cut negative lines from the crontab #{options[:cut]}")
29
+ exit(1)
30
+ end
31
+ @options[:cut] = @options[:cut].to_i
32
+ end
33
+
34
+ def run
35
+ if @options[:update] || @options[:clear]
36
+ write_crontab(updated_crontab)
37
+ elsif @options[:write]
38
+ write_crontab(whenever_cron)
39
+ else
40
+ puts Whenever.cron(@options)
41
+ puts "## [message] Above is your schedule file converted to cron syntax; your crontab file was not updated."
42
+ puts "## [message] Run `whenever --help' for more options."
43
+ exit(0)
44
+ end
45
+ end
46
+
47
+ protected
48
+
49
+ def default_identifier
50
+ File.expand_path(@options[:file])
51
+ end
52
+
53
+ def whenever_cron
54
+ return '' if @options[:clear]
55
+ @whenever_cron ||= [comment_open, Whenever.cron(@options), comment_close].compact.join("\n") + "\n"
56
+ end
57
+
58
+ def read_crontab
59
+ return @current_crontab if @current_crontab
60
+
61
+ command = ['crontab -l']
62
+ command << "-u #{@options[:user]}" if @options[:user]
63
+
64
+ command_results = %x[#{command.join(' ')} 2> /dev/null]
65
+ @current_crontab = $?.exitstatus.zero? ? prepare(command_results) : ''
66
+ end
67
+
68
+ def write_crontab(contents)
69
+ tmp_cron_file = Tempfile.new('whenever_tmp_cron').path
70
+ File.open(tmp_cron_file, File::WRONLY | File::APPEND) do |file|
71
+ file << contents
72
+ end
73
+
74
+ command = ['crontab']
75
+ command << "-u #{@options[:user]}" if @options[:user]
76
+ command << tmp_cron_file
77
+
78
+ if system(command.join(' '))
79
+ action = 'written' if @options[:write]
80
+ action = 'updated' if @options[:update]
81
+ puts "[write] crontab file #{action}"
82
+ exit(0)
83
+ else
84
+ warn "[fail] Couldn't write crontab; try running `whenever' with no options to ensure your schedule file is valid."
85
+ exit(1)
86
+ end
87
+ end
88
+
89
+ def updated_crontab
90
+ # Check for unopened or unclosed identifier blocks
91
+ if read_crontab =~ Regexp.new("^#{comment_open}\s*$") && (read_crontab =~ Regexp.new("^#{comment_close}\s*$")).nil?
92
+ warn "[fail] Unclosed indentifier; Your crontab file contains '#{comment_open}', but no '#{comment_close}'"
93
+ exit(1)
94
+ elsif (read_crontab =~ Regexp.new("^#{comment_open}\s*$")).nil? && read_crontab =~ Regexp.new("^#{comment_close}\s*$")
95
+ warn "[fail] Unopened indentifier; Your crontab file contains '#{comment_close}', but no '#{comment_open}'"
96
+ exit(1)
97
+ end
98
+
99
+ # If an existing identier block is found, replace it with the new cron entries
100
+ if read_crontab =~ Regexp.new("^#{comment_open}\s*$") && read_crontab =~ Regexp.new("^#{comment_close}\s*$")
101
+ # If the existing crontab file contains backslashes they get lost going through gsub.
102
+ # .gsub('\\', '\\\\\\') preserves them. Go figure.
103
+ read_crontab.gsub(Regexp.new("^#{comment_open}\s*$.+^#{comment_close}\s*$", Regexp::MULTILINE), whenever_cron.chomp.gsub('\\', '\\\\\\'))
104
+ else # Otherwise, append the new cron entries after any existing ones
105
+ [read_crontab, whenever_cron].join("\n\n")
106
+ end.gsub(/\n{3,}/, "\n\n") # More than two newlines becomes just two.
107
+ end
108
+
109
+ def prepare(contents)
110
+ # Strip n lines from the top of the file as specified by the :cut option.
111
+ # Use split with a -1 limit option to ensure the join is able to rebuild
112
+ # the file with all of the original seperators in-tact.
113
+ stripped_contents = contents.split($/,-1)[@options[:cut]..-1].join($/)
114
+
115
+ # Some cron implementations require all non-comment lines to be newline-
116
+ # terminated. (issue #95) Strip all newlines and replace with the default
117
+ # platform record seperator ($/)
118
+ stripped_contents.gsub!(/\s+$/, $/)
119
+ end
120
+
121
+ def comment_base
122
+ "Whenever generated tasks for: #{@options[:identifier]}"
123
+ end
124
+
125
+ def comment_open
126
+ "# Begin #{comment_base}"
127
+ end
128
+
129
+ def comment_close
130
+ "# End #{comment_base}"
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,149 @@
1
+ require 'chronic'
2
+
3
+ module Whenever
4
+ module Output
5
+ class Cron
6
+ REGEX = /^.+ .+ .+ .+ .+.?$/
7
+
8
+ attr_accessor :time, :task
9
+
10
+ def initialize(time = nil, task = nil, at = nil)
11
+ @time = time
12
+ @task = task
13
+ @at = at.is_a?(String) ? (Chronic.parse(at) || 0) : (at || 0)
14
+ end
15
+
16
+ def self.enumerate(item, detect_cron = true)
17
+ if item and item.is_a?(String)
18
+ items =
19
+ if detect_cron && item =~ REGEX
20
+ [item]
21
+ else
22
+ item.split(',')
23
+ end
24
+ else
25
+ items = item
26
+ items = [items] unless items and items.respond_to?(:each)
27
+ end
28
+ items
29
+ end
30
+
31
+ def self.output(times, job)
32
+ enumerate(times).each do |time|
33
+ enumerate(job.at, false).each do |at|
34
+ yield new(time, job.output, at).output
35
+ end
36
+ end
37
+ end
38
+
39
+ def output
40
+ [time_in_cron_syntax, task].compact.join(' ').strip
41
+ end
42
+
43
+ def time_in_cron_syntax
44
+ case @time
45
+ when REGEX then @time # raw cron sytax given
46
+ when Symbol then parse_symbol
47
+ when String then parse_as_string
48
+ else parse_time
49
+ end
50
+ end
51
+
52
+ protected
53
+
54
+ def parse_symbol
55
+ shortcut = case @time
56
+ when :reboot then '@reboot'
57
+ when :year then 12.months
58
+ when :yearly,
59
+ :annually then '@annually'
60
+ when :day then 1.day
61
+ when :daily then '@daily'
62
+ when :midnight then '@midnight'
63
+ when :month then 1.month
64
+ when :monthly then '@monthly'
65
+ when :week then 1.week
66
+ when :weekly then '@weekly'
67
+ when :hour then 1.hour
68
+ when :hourly then '@hourly'
69
+ end
70
+
71
+ if shortcut.is_a?(Numeric)
72
+ @time = shortcut
73
+ parse_time
74
+ elsif shortcut
75
+ if @at.is_a?(Time) || (@at.is_a?(Numeric) && @at > 0)
76
+ raise ArgumentError, "You cannot specify an ':at' when using the shortcuts for times."
77
+ else
78
+ return shortcut
79
+ end
80
+ else
81
+ parse_as_string
82
+ end
83
+ end
84
+
85
+ def parse_time
86
+ timing = Array.new(5, '*')
87
+ case @time
88
+ when 0.seconds...1.minute
89
+ raise ArgumentError, "Time must be in minutes or higher"
90
+ when 1.minute...1.hour
91
+ minute_frequency = @time / 60
92
+ timing[0] = comma_separated_timing(minute_frequency, 59, @at || 0)
93
+ when 1.hour...1.day
94
+ hour_frequency = (@time / 60 / 60).round
95
+ timing[0] = @at.is_a?(Time) ? @at.min : @at
96
+ timing[1] = comma_separated_timing(hour_frequency, 23)
97
+ when 1.day...1.month
98
+ day_frequency = (@time / 24 / 60 / 60).round
99
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
100
+ timing[1] = @at.is_a?(Time) ? @at.hour : @at
101
+ timing[2] = comma_separated_timing(day_frequency, 31, 1)
102
+ when 1.month..12.months
103
+ month_frequency = (@time / 30 / 24 / 60 / 60).round
104
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
105
+ timing[1] = @at.is_a?(Time) ? @at.hour : 0
106
+ timing[2] = @at.is_a?(Time) ? @at.day : (@at.zero? ? 1 : @at)
107
+ timing[3] = comma_separated_timing(month_frequency, 12, 1)
108
+ else
109
+ return parse_as_string
110
+ end
111
+ timing.join(' ')
112
+ end
113
+
114
+ def parse_as_string
115
+ return unless @time
116
+ string = @time.to_s
117
+
118
+ timing = Array.new(4, '*')
119
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
120
+ timing[1] = @at.is_a?(Time) ? @at.hour : 0
121
+
122
+ return (timing << '1-5') * " " if string.downcase.index('weekday')
123
+ return (timing << '6,0') * " " if string.downcase.index('weekend')
124
+
125
+ %w(sun mon tue wed thu fri sat).each_with_index do |day, i|
126
+ return (timing << i) * " " if string.downcase.index(day)
127
+ end
128
+
129
+ raise ArgumentError, "Couldn't parse: #{@time}"
130
+ end
131
+
132
+ def comma_separated_timing(frequency, max, start = 0)
133
+ return start if frequency.blank? || frequency.zero?
134
+ return '*' if frequency == 1
135
+ return frequency if frequency > (max * 0.5).ceil
136
+
137
+ original_start = start
138
+
139
+ start += frequency unless (max + 1).modulo(frequency).zero? || start > 0
140
+ output = (start..max).step(frequency).to_a
141
+
142
+ max_occurances = (max.to_f / (frequency.to_f)).round
143
+ max_occurances += 1 if original_start.zero?
144
+
145
+ output[0, max_occurances].join(',')
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,45 @@
1
+ module Whenever
2
+ class Job
3
+ attr_reader :at
4
+
5
+ def initialize(options = {})
6
+ @options = options
7
+ @at = options.delete(:at)
8
+ @template = options.delete(:template)
9
+ @job_template = options.delete(:job_template) || ":job"
10
+ @options[:output] = Whenever::Output::Redirection.new(options[:output]).to_s if options.has_key?(:output)
11
+ @options[:environment] ||= :production
12
+ @options[:path] ||= Whenever.path
13
+ end
14
+
15
+ def output
16
+ job = process_template(@template, @options).strip
17
+ process_template(@job_template, { :job => job }).strip
18
+ end
19
+
20
+ protected
21
+
22
+ def process_template(template, options)
23
+ template.gsub(/:\w+/) do |key|
24
+ before_and_after = [$`[-1..-1], $'[0..0]]
25
+ option = options[key.sub(':', '').to_sym]
26
+
27
+ if before_and_after.all? { |c| c == "'" }
28
+ escape_single_quotes(option)
29
+ elsif before_and_after.all? { |c| c == '"' }
30
+ escape_double_quotes(option)
31
+ else
32
+ option
33
+ end
34
+ end
35
+ end
36
+
37
+ def escape_single_quotes(str)
38
+ str.gsub(/'/) { "'\\''" }
39
+ end
40
+
41
+ def escape_double_quotes(str)
42
+ str.gsub(/"/) { '\"' }
43
+ end
44
+ end
45
+ end