seisuke-whenever 0.6.3

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.
@@ -0,0 +1,31 @@
1
+ Capistrano::Configuration.instance(:must_exist).load do
2
+
3
+ _cset(:whenever_roles) { :db }
4
+ _cset(:whenever_command) { "whenever" }
5
+ _cset(:whenever_identifier) { application }
6
+ _cset(:whenever_update_flags) { "--update-crontab #{whenever_identifier}" }
7
+ _cset(:whenever_clear_flags) { "--clear-crontab #{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 "Update application's crontab entries using Whenever"
18
+ task :update_crontab, :roles => whenever_roles do
19
+ # Hack by Jamis to skip a task if the role has no servers defined. http://tinyurl.com/ckjgnz
20
+ next if find_servers_for_task(current_task).empty?
21
+ run "cd #{current_path} && #{whenever_command} #{whenever_update_flags}"
22
+ end
23
+
24
+ desc "Clear application's crontab entries using Whenever"
25
+ task :clear_crontab, :roles => whenever_roles do
26
+ next if find_servers_for_task(current_task).empty?
27
+ run "cd #{release_path} && #{whenever_command} #{whenever_clear_flags}"
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,125 @@
1
+ require 'fileutils'
2
+ require 'tempfile'
3
+
4
+ module Whenever
5
+ class CommandLine
6
+
7
+ def self.execute(options={})
8
+ new(options).run
9
+ end
10
+
11
+ def initialize(options={})
12
+ @options = options
13
+
14
+ @options[:file] ||= 'config/schedule.rb'
15
+ @options[:cut] ||= 0
16
+ @options[:identifier] ||= default_identifier
17
+
18
+ unless File.exists?(@options[:file])
19
+ warn("[fail] Can't find file: #{@options[:file]}")
20
+ exit(1)
21
+ end
22
+
23
+ if [@options[:update], @options[:write], @options[:clear]].compact.length > 1
24
+ warn("[fail] Can only update, write or clear. Choose one.")
25
+ exit(1)
26
+ end
27
+
28
+ unless @options[:cut].to_s =~ /[0-9]*/
29
+ warn("[fail] Can't cut negative lines from the crontab #{options[:cut]}")
30
+ exit(1)
31
+ end
32
+ @options[:cut] = @options[:cut].to_i
33
+ end
34
+
35
+ def run
36
+ if @options[:update] || @options[:clear]
37
+ write_crontab(updated_crontab)
38
+ elsif @options[:write]
39
+ write_crontab(whenever_cron)
40
+ else
41
+ puts Whenever.cron(@options)
42
+ exit(0)
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ def default_identifier
49
+ File.expand_path(@options[:file])
50
+ end
51
+
52
+ def whenever_cron
53
+ return '' if @options[:clear]
54
+ @whenever_cron ||= [comment_open, Whenever.cron(@options), comment_close].compact.join("\n") + "\n"
55
+ end
56
+
57
+ def read_crontab
58
+ return @current_crontab if @current_crontab
59
+
60
+ command = ['crontab -l']
61
+ command << "-u #{@options[:user]}" if @options[:user]
62
+
63
+ command_results = %x[#{command.join(' ')} 2> /dev/null]
64
+ @current_crontab = $?.exitstatus.zero? ? prepare(command_results) : ''
65
+ end
66
+
67
+ def write_crontab(contents)
68
+ tmp_cron_file = Tempfile.new('whenever_tmp_cron').path
69
+ File.open(tmp_cron_file, File::WRONLY | File::APPEND) do |file|
70
+ file << contents
71
+ end
72
+
73
+ command = ['crontab']
74
+ command << "-u #{@options[:user]}" if @options[:user]
75
+ command << tmp_cron_file
76
+
77
+ if system(command.join(' '))
78
+ action = 'written' if @options[:write]
79
+ action = 'updated' if @options[:update]
80
+ puts "[write] crontab file #{action}"
81
+ exit(0)
82
+ else
83
+ warn "[fail] Couldn't write crontab; try running `whenever' with no options to ensure your schedule file is valid."
84
+ exit(1)
85
+ end
86
+ end
87
+
88
+ def updated_crontab
89
+ # Check for unopened or unclosed identifier blocks
90
+ if read_crontab =~ Regexp.new("^#{comment_open}$") && (read_crontab =~ Regexp.new("^#{comment_close}$")).nil?
91
+ warn "[fail] Unclosed indentifier; Your crontab file contains '#{comment_open}', but no '#{comment_close}'"
92
+ exit(1)
93
+ elsif (read_crontab =~ Regexp.new("^#{comment_open}$")).nil? && read_crontab =~ Regexp.new("^#{comment_close}$")
94
+ warn "[fail] Unopened indentifier; Your crontab file contains '#{comment_close}', but no '#{comment_open}'"
95
+ exit(1)
96
+ end
97
+
98
+ # If an existing identier block is found, replace it with the new cron entries
99
+ if read_crontab =~ Regexp.new("^#{comment_open}$") && read_crontab =~ Regexp.new("^#{comment_close}$")
100
+ # If the existing crontab file contains backslashes they get lost going through gsub.
101
+ # .gsub('\\', '\\\\\\') preserves them. Go figure.
102
+ read_crontab.gsub(Regexp.new("^#{comment_open}$.+^#{comment_close}$", Regexp::MULTILINE), whenever_cron.chomp.gsub('\\', '\\\\\\'))
103
+ else # Otherwise, append the new cron entries after any existing ones
104
+ [read_crontab, whenever_cron].join("\n\n")
105
+ end.gsub(/\n{3,}/, "\n\n") # More than two newlines becomes just two.
106
+ end
107
+
108
+ def prepare(contents)
109
+ contents.split("\n")[@options[:cut]..-1].join("\n")
110
+ end
111
+
112
+ def comment_base
113
+ "Whenever generated tasks for: #{@options[:identifier]}"
114
+ end
115
+
116
+ def comment_open
117
+ "# Begin #{comment_base}"
118
+ end
119
+
120
+ def comment_close
121
+ "# End #{comment_base}"
122
+ end
123
+
124
+ end
125
+ end
@@ -0,0 +1,132 @@
1
+ module Whenever
2
+ module Output
3
+ class Cron
4
+
5
+ attr_accessor :time, :task
6
+
7
+ def initialize(time = nil, task = nil, at = nil)
8
+ @time = time
9
+ @task = task
10
+ @at = at.is_a?(String) ? (Chronic.parse(at) || 0) : (at || 0)
11
+ end
12
+
13
+ def self.enumerate(item)
14
+ if item and item.is_a?(String)
15
+ items = item.split(',')
16
+ else
17
+ items = item
18
+ items = [items] unless items and items.respond_to?(:each)
19
+ end
20
+ items
21
+ end
22
+
23
+ def self.output(times, job)
24
+ enumerate(times).each do |time|
25
+ enumerate(job.at).each do |at|
26
+ yield new(time, job.output, at).output
27
+ end
28
+ end
29
+ end
30
+
31
+ def output
32
+ [time_in_cron_syntax, task].compact.join(' ').strip
33
+ end
34
+
35
+ def time_in_cron_syntax
36
+ case @time
37
+ when Symbol then parse_symbol
38
+ when String then parse_as_string
39
+ else parse_time
40
+ end
41
+ end
42
+
43
+ protected
44
+
45
+ def parse_symbol
46
+ shortcut = case @time
47
+ when :reboot then '@reboot'
48
+ when :year, :yearly then '@annually'
49
+ when :day, :daily then '@daily'
50
+ when :midnight then '@midnight'
51
+ when :month, :monthly then '@monthly'
52
+ when :week, :weekly then '@weekly'
53
+ when :hour, :hourly then '@hourly'
54
+ end
55
+
56
+ if shortcut
57
+ if @at.is_a?(Time) || (@at.is_a?(Numeric) && @at > 0)
58
+ raise ArgumentError, "You cannot specify an ':at' when using the shortcuts for times."
59
+ else
60
+ return shortcut
61
+ end
62
+ else
63
+ parse_as_string
64
+ end
65
+ end
66
+
67
+ def parse_time
68
+ timing = Array.new(5, '*')
69
+ case @time
70
+ when 0.seconds...1.minute
71
+ raise ArgumentError, "Time must be in minutes or higher"
72
+ when 1.minute...1.hour
73
+ minute_frequency = @time / 60
74
+ timing[0] = comma_separated_timing(minute_frequency, 59, @at || 0)
75
+ when 1.hour...1.day
76
+ hour_frequency = (@time / 60 / 60).round
77
+ timing[0] = @at.is_a?(Time) ? @at.min : @at
78
+ timing[1] = comma_separated_timing(hour_frequency, 23)
79
+ when 1.day...1.month
80
+ day_frequency = (@time / 24 / 60 / 60).round
81
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
82
+ timing[1] = @at.is_a?(Time) ? @at.hour : @at
83
+ timing[2] = comma_separated_timing(day_frequency, 31, 1)
84
+ when 1.month..12.months
85
+ month_frequency = (@time / 30 / 24 / 60 / 60).round
86
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
87
+ timing[1] = @at.is_a?(Time) ? @at.hour : 0
88
+ timing[2] = @at.is_a?(Time) ? @at.day : (@at.zero? ? 1 : @at)
89
+ timing[3] = comma_separated_timing(month_frequency, 12, 1)
90
+ else
91
+ return parse_as_string
92
+ end
93
+ timing.join(' ')
94
+ end
95
+
96
+ def parse_as_string
97
+ return unless @time
98
+ string = @time.to_s
99
+
100
+ timing = Array.new(4, '*')
101
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
102
+ timing[1] = @at.is_a?(Time) ? @at.hour : 0
103
+
104
+ return (timing << '1-5') * " " if string.downcase.index('weekday')
105
+ return (timing << '6,0') * " " if string.downcase.index('weekend')
106
+
107
+ %w(sun mon tue wed thu fri sat).each_with_index do |day, i|
108
+ return (timing << i) * " " if string.downcase.index(day)
109
+ end
110
+
111
+ raise ArgumentError, "Couldn't parse: #{@time}"
112
+ end
113
+
114
+ def comma_separated_timing(frequency, max, start = 0)
115
+ return start if frequency.blank? || frequency.zero?
116
+ return '*' if frequency == 1
117
+ return frequency if frequency > (max * 0.5).ceil
118
+
119
+ original_start = start
120
+
121
+ start += frequency unless (max + 1).modulo(frequency).zero? || start > 0
122
+ output = (start..max).step(frequency).to_a
123
+
124
+ max_occurances = (max.to_f / (frequency.to_f)).round
125
+ max_occurances += 1 if original_start.zero?
126
+
127
+ output[0, max_occurances].join(',')
128
+ end
129
+
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,47 @@
1
+ module Whenever
2
+ class Job
3
+
4
+ attr_reader :at
5
+
6
+ def initialize(options = {})
7
+ @options = options
8
+ @at = options.delete(:at)
9
+ @template = options.delete(:template)
10
+ @job_template = options.delete(:job_template) || ":job"
11
+ @options[:output] = Whenever::Output::Redirection.new(options[:output]).to_s if options.has_key?(:output)
12
+ @options[:environment] ||= :production
13
+ @options[:path] ||= Whenever.path
14
+ end
15
+
16
+ def output
17
+ job = process_template(@template, @options).strip
18
+ process_template(@job_template, { :job => job }).strip
19
+ end
20
+
21
+ protected
22
+
23
+ def process_template(template, options)
24
+ template.gsub(/:\w+/) do |key|
25
+ before_and_after = [$`[-1..-1], $'[0..0]]
26
+ option = options[key.sub(':', '').to_sym]
27
+
28
+ if before_and_after.all? { |c| c == "'" }
29
+ escape_single_quotes(option)
30
+ elsif before_and_after.all? { |c| c == '"' }
31
+ escape_double_quotes(option)
32
+ else
33
+ option
34
+ end
35
+ end
36
+ end
37
+
38
+ def escape_single_quotes(str)
39
+ str.gsub(/'/) { "'\\''" }
40
+ end
41
+
42
+ def escape_double_quotes(str)
43
+ str.gsub(/"/) { '\"' }
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,156 @@
1
+ module Whenever
2
+ class JobList
3
+
4
+ def initialize(options)
5
+ @jobs, @env, @set_variables, @pre_set_variables = {}, {}, {}, {}
6
+
7
+ case options
8
+ when String
9
+ config = options
10
+ when Hash
11
+ config = if options[:string]
12
+ options[:string]
13
+ elsif options[:file]
14
+ File.read(options[:file])
15
+ end
16
+ pre_set(options[:set])
17
+ end
18
+
19
+ setup = File.read("#{File.expand_path(File.dirname(__FILE__))}/setup.rb")
20
+
21
+ eval(setup + config)
22
+ end
23
+
24
+ def set(variable, value)
25
+ variable = variable.to_sym
26
+ return if @pre_set_variables[variable]
27
+
28
+ instance_variable_set("@#{variable}".to_sym, value)
29
+ self.class.send(:attr_reader, variable.to_sym)
30
+ @set_variables[variable] = value
31
+ end
32
+
33
+ def env(variable, value)
34
+ @env[variable.to_s] = value
35
+ end
36
+
37
+ def every(frequency, options = {})
38
+ @current_time_scope = frequency
39
+ @options = options
40
+ yield
41
+ end
42
+
43
+ def job_type(name, template)
44
+ class_eval do
45
+ define_method(name) do |task, *args|
46
+ options = { :task => task, :template => template }
47
+ options.merge!(args[0]) if args[0].is_a? Hash
48
+
49
+ # :cron_log was an old option for output redirection, it remains for backwards compatibility
50
+ options[:output] = (options[:cron_log] || @cron_log) if defined?(@cron_log) || options.has_key?(:cron_log)
51
+ # :output is the newer, more flexible option.
52
+ options[:output] = @output if defined?(@output) && !options.has_key?(:output)
53
+
54
+ if @options[:second]
55
+ array = []
56
+ (60 / @options[:second]).times do
57
+ array << "#{options[:template]};"
58
+ end
59
+ options[:template] = array.join " sleep #{@options[:second].to_s}; "
60
+ end
61
+
62
+ @jobs[@current_time_scope] ||= []
63
+ @jobs[@current_time_scope] << Whenever::Job.new(@options.merge(@set_variables).merge(options))
64
+ end
65
+ end
66
+ end
67
+
68
+ def generate_cron_output
69
+ [environment_variables, cron_jobs].compact.join
70
+ end
71
+
72
+ private
73
+
74
+ #
75
+ # Takes a string like: "variable1=something&variable2=somethingelse"
76
+ # and breaks it into variable/value pairs. Used for setting variables at runtime from the command line.
77
+ # Only works for setting values as strings.
78
+ #
79
+ def pre_set(variable_string = nil)
80
+ return if variable_string.blank?
81
+
82
+ pairs = variable_string.split('&')
83
+ pairs.each do |pair|
84
+ next unless pair.index('=')
85
+ variable, value = *pair.split('=')
86
+ unless variable.blank? || value.blank?
87
+ variable = variable.strip.to_sym
88
+ set(variable, value.strip)
89
+ @pre_set_variables[variable] = value
90
+ end
91
+ end
92
+ end
93
+
94
+ def environment_variables
95
+ return if @env.empty?
96
+
97
+ output = []
98
+ @env.each do |key, val|
99
+ output << "#{key}=#{val}\n"
100
+ end
101
+ output << "\n"
102
+
103
+ output.join
104
+ end
105
+
106
+ #
107
+ # Takes the standard cron output that Whenever generates and finds
108
+ # similar entries that can be combined. For example: If a job should run
109
+ # at 3:02am and 4:02am, instead of creating two jobs this method combines
110
+ # them into one that runs on the 2nd minute at the 3rd and 4th hour.
111
+ #
112
+ def combine(entries)
113
+ entries.map! { |entry| entry.split(/ +/, 6) }
114
+ 0.upto(4) do |f|
115
+ (entries.length-1).downto(1) do |i|
116
+ next if entries[i][f] == '*'
117
+ comparison = entries[i][0...f] + entries[i][f+1..-1]
118
+ (i-1).downto(0) do |j|
119
+ next if entries[j][f] == '*'
120
+ if comparison == entries[j][0...f] + entries[j][f+1..-1]
121
+ entries[j][f] += ',' + entries[i][f]
122
+ entries.delete_at(i)
123
+ break
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ entries.map { |entry| entry.join(' ') }
130
+ end
131
+
132
+ def cron_jobs
133
+ return if @jobs.empty?
134
+
135
+ shortcut_jobs = []
136
+ regular_jobs = []
137
+
138
+ @jobs.each do |time, jobs|
139
+ jobs.each do |job|
140
+ Whenever::Output::Cron.output(time, job) do |cron|
141
+ cron << "\n\n"
142
+
143
+ if cron.starts_with?("@")
144
+ shortcut_jobs << cron
145
+ else
146
+ regular_jobs << cron
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ shortcut_jobs.join + combine(regular_jobs).join
153
+ end
154
+
155
+ end
156
+ end