hybridgroup-whenever 0.39

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,152 @@
1
+ module Whenever
2
+ class JobList
3
+
4
+ def initialize(options)
5
+ @jobs = Hash.new
6
+ @env = Hash.new
7
+
8
+ case options
9
+ when String
10
+ config = options
11
+ when Hash
12
+ config = if options[:string]
13
+ options[:string]
14
+ elsif options[:file]
15
+ File.read(options[:file])
16
+ end
17
+ pre_set(options[:set])
18
+ end
19
+
20
+ eval(config)
21
+ end
22
+
23
+ def set(variable, value)
24
+ return if instance_variable_defined?("@#{variable}".to_sym)
25
+
26
+ instance_variable_set("@#{variable}".to_sym, value)
27
+ self.class.send(:attr_reader, variable.to_sym)
28
+ end
29
+
30
+ def env(variable, value)
31
+ @env[variable.to_s] = value
32
+ end
33
+
34
+ def every(frequency, options = {})
35
+ @current_time_scope = frequency
36
+ @options = options
37
+ yield
38
+ end
39
+
40
+ def command(task, options = {})
41
+ options[:cron_log] ||= @cron_log unless options[:cron_log] === false
42
+ options[:class] ||= Whenever::Job::Default
43
+ @jobs[@current_time_scope] ||= []
44
+ @jobs[@current_time_scope] << options[:class].new(@options.merge(:task => task, :path => @path).merge(options))
45
+ end
46
+
47
+ def runner(task, options = {})
48
+ options.reverse_merge!(:environment => @environment, :path => @path)
49
+ options[:class] = Whenever::Job::Runner
50
+ command(task, options)
51
+ end
52
+
53
+ def rake(task, options = {})
54
+ options.reverse_merge!(:environment => @environment, :path => @path)
55
+ options[:class] = Whenever::Job::RakeTask
56
+ command(task, options)
57
+ end
58
+
59
+ def generate_cron_output
60
+ set_path_environment_variable
61
+
62
+ [environment_variables, cron_jobs].compact.join
63
+ end
64
+
65
+ private
66
+
67
+ #
68
+ # Takes a string like: "variable1=something&variable2=somethingelse"
69
+ # and breaks it into variable/value pairs. Used for setting variables at runtime from the command line.
70
+ # Only works for setting values as strings.
71
+ #
72
+ def pre_set(variable_string = nil)
73
+ return if variable_string.blank?
74
+
75
+ pairs = variable_string.split('&')
76
+ pairs.each do |pair|
77
+ next unless pair.index('=')
78
+ variable, value = *pair.split('=')
79
+ set(variable.strip, value.strip) unless variable.blank? || value.blank?
80
+ end
81
+ end
82
+
83
+ def set_path_environment_variable
84
+ return if path_should_not_be_set_automatically?
85
+ @env[:PATH] = read_path unless read_path.blank?
86
+ end
87
+
88
+ def read_path
89
+ ENV['PATH'] if ENV
90
+ end
91
+
92
+ def path_should_not_be_set_automatically?
93
+ @set_path_automatically === false || @env[:PATH] || @env["PATH"]
94
+ end
95
+
96
+ def environment_variables
97
+ return if @env.empty?
98
+
99
+ output = []
100
+ @env.each do |key, val|
101
+ output << "#{key}=#{val}\n"
102
+ end
103
+ output << "\n"
104
+
105
+ output.join
106
+ end
107
+
108
+ #
109
+ # Takes the standard cron output that Whenever generates and finds
110
+ # similar entries that can be combined. For example: If a job should run
111
+ # at 3:02am and 4:02am, instead of creating two jobs this method combines
112
+ # them into one that runs on the 2nd minute at the 3rd and 4th hour.
113
+ #
114
+ def combine(entries)
115
+ entries.map! { |entry| entry.split(/ +/,6 )}
116
+ 0.upto(4) do |f|
117
+ (entries.length-1).downto(1) do |i|
118
+ next if entries[i][f] == '*'
119
+ comparison = entries[i][0...f] + entries[i][f+1..-1]
120
+ (i-1).downto(0) do |j|
121
+ next if entries[j][f] == '*'
122
+ if comparison == entries[j][0...f] + entries[j][f+1..-1]
123
+ entries[j][f] += ',' + entries[i][f]
124
+ entries.delete_at(i)
125
+ break
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ entries.map { |entry| entry.join(' ') }
132
+ end
133
+
134
+ def cron_jobs
135
+ return if @jobs.empty?
136
+
137
+ output = []
138
+ @jobs.each do |time, jobs|
139
+ jobs.each do |job|
140
+ Whenever::Output::Cron.output(time, job) do |cron|
141
+ cron << " >> #{job.cron_log} 2>&1" if job.cron_log
142
+ cron << "\n\n"
143
+ output << cron
144
+ end
145
+ end
146
+ end
147
+
148
+ combine(output).join
149
+ end
150
+
151
+ end
152
+ end
@@ -0,0 +1,48 @@
1
+ module Whenever
2
+ module Job
3
+ class Default
4
+
5
+ attr_accessor :task, :at, :cron_log
6
+
7
+ def initialize(options = {})
8
+ @task = options[:task]
9
+ @at = options[:at]
10
+ @cron_log = options[:cron_log]
11
+ @environment = options[:environment] || :production
12
+ @path = options[:path] || Whenever.path
13
+ @lockrun = options[:lockrun]
14
+ end
15
+
16
+ def output
17
+ output = wrap_task(task)
18
+ if @lockrun
19
+ lockrunify output
20
+ else
21
+ output
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def path_required
28
+ raise ArgumentError, "No path available; set :path, '/your/path' in your schedule file" if @path.blank?
29
+ end
30
+
31
+ def wrap_task(task)
32
+ task
33
+ end
34
+
35
+ def lockrunify(output)
36
+ path_required
37
+ %Q{/usr/bin/env lockrun --lockfile=#{lockfile_path} -- sh -c '#{output}'}
38
+ end
39
+
40
+ def lockfile_path
41
+ path_required
42
+ filename_prefix = @lockrun == true ? "default" : @lockrun
43
+ "#{@path}/log/#{filename_prefix}.lockrun"
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ module Whenever
2
+ module Job
3
+ class RakeTask < Whenever::Job::Default
4
+
5
+ def output
6
+ path_required
7
+ super
8
+ end
9
+
10
+ protected
11
+
12
+ def wrap_task(task)
13
+ "cd #{@path} && RAILS_ENV=#{@environment} /usr/bin/env rake #{task}"
14
+ end
15
+
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,17 @@
1
+ module Whenever
2
+ module Job
3
+ class Runner < Whenever::Job::Default
4
+
5
+ def output
6
+ path_required
7
+ super
8
+ end
9
+
10
+ protected
11
+ def wrap_task(task)
12
+ %Q(#{File.join(@path, 'script', 'runner')} -e #{@environment} #{task.inspect})
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,131 @@
1
+ module Whenever
2
+ module Output
3
+
4
+ class Cron
5
+
6
+ attr_accessor :time, :task
7
+
8
+ def initialize(time = nil, task = nil, at = nil)
9
+ @time = time
10
+ @task = task
11
+ @at = at.is_a?(String) ? (Chronic.parse(at) || 0) : (at || 0)
12
+ end
13
+
14
+ def self.enumerate(item)
15
+ if item and item.is_a?(String)
16
+ items = item.split(',')
17
+ else
18
+ items = item
19
+ items = [items] unless items and items.respond_to?(:each)
20
+ end
21
+ items
22
+ end
23
+
24
+ def self.output(times, job)
25
+ enumerate(times).each do |time|
26
+ enumerate(job.at).each do |at|
27
+ out = new(time, job.output, at)
28
+ yield "#{out.time_in_cron_syntax} #{out.task}"
29
+ end
30
+ end
31
+ end
32
+
33
+ def time_in_cron_syntax
34
+ case @time
35
+ when Symbol then parse_symbol
36
+ when String then parse_as_string
37
+ else parse_time
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ def parse_symbol
44
+ shortcut = case @time
45
+ when :reboot then '@reboot'
46
+ when :year, :yearly then '@annually'
47
+ when :day, :daily then '@daily'
48
+ when :midnight then '@midnight'
49
+ when :month, :monthly then '@monthly'
50
+ when :week, :weekly then '@weekly'
51
+ when :hour, :hourly then '@hourly'
52
+ end
53
+
54
+ if shortcut
55
+ if @at > 0
56
+ raise ArgumentError, "You cannot specify an ':at' when using the shortcuts for times."
57
+ else
58
+ return shortcut
59
+ end
60
+ else
61
+ parse_as_string
62
+ end
63
+ end
64
+
65
+ def parse_time
66
+ timing = Array.new(5, '*')
67
+ case @time
68
+ when 0.seconds...1.minute
69
+ raise ArgumentError, "Time must be in minutes or higher"
70
+ when 1.minute...1.hour
71
+ minute_frequency = @time / 60
72
+ timing[0] = comma_separated_timing(minute_frequency, 59)
73
+ when 1.hour...1.day
74
+ hour_frequency = (@time / 60 / 60).round
75
+ timing[0] = @at.is_a?(Time) ? @at.min : @at
76
+ timing[1] = comma_separated_timing(hour_frequency, 23)
77
+ when 1.day...1.month
78
+ day_frequency = (@time / 24 / 60 / 60).round
79
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
80
+ timing[1] = @at.is_a?(Time) ? @at.hour : @at
81
+ timing[2] = comma_separated_timing(day_frequency, 31, 1)
82
+ when 1.month..12.months
83
+ month_frequency = (@time / 30 / 24 / 60 / 60).round
84
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
85
+ timing[1] = @at.is_a?(Time) ? @at.hour : 0
86
+ timing[2] = @at.is_a?(Time) ? @at.day : (@at.zero? ? 1 : @at)
87
+ timing[3] = comma_separated_timing(month_frequency, 12, 1)
88
+ else
89
+ return parse_as_string
90
+ end
91
+ timing.join(' ')
92
+ end
93
+
94
+ def parse_as_string
95
+ return unless @time
96
+ string = @time.to_s
97
+
98
+ timing = Array.new(4, '*')
99
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
100
+ timing[1] = @at.is_a?(Time) ? @at.hour : 0
101
+
102
+ return (timing << '1-5') * " " if string.downcase.index('weekday')
103
+ return (timing << '6,0') * " " if string.downcase.index('weekend')
104
+
105
+ %w(sun mon tue wed thu fri sat).each_with_index do |day, i|
106
+ return (timing << i) * " " if string.downcase.index(day)
107
+ end
108
+
109
+ raise ArgumentError, "Couldn't parse: #{@time}"
110
+ end
111
+
112
+ def comma_separated_timing(frequency, max, start = 0)
113
+ return start if frequency.blank? || frequency.zero?
114
+ return '*' if frequency == 1
115
+ return frequency if frequency > (max * 0.5).ceil
116
+
117
+ original_start = start
118
+
119
+ start += frequency unless (max + 1).modulo(frequency).zero? || start > 0
120
+ output = (start..max).step(frequency).to_a
121
+
122
+ max_occurances = (max.to_f / (frequency.to_f)).round
123
+ max_occurances += 1 if original_start.zero?
124
+
125
+ output[0, max_occurances].join(',')
126
+ end
127
+
128
+ end
129
+
130
+ end
131
+ end
@@ -0,0 +1,9 @@
1
+ module Whenever
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 3
5
+ TINY = 8
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end unless defined?(Whenever::VERSION)
@@ -0,0 +1,36 @@
1
+ unless defined?(Whenever)
2
+ $:.unshift(File.dirname(__FILE__))
3
+
4
+ # Hoping to load Rails' Rakefile
5
+ begin
6
+ load 'Rakefile'
7
+ rescue LoadError => e
8
+ nil
9
+ end
10
+ end
11
+
12
+ require 'chronic'
13
+
14
+ # If Rails' rakefile was loaded than so was activesupport, but
15
+ # if this is being used in a non-rails enviroment we need to require it.
16
+ # It was previously defined as a dependency of this gem, but that became
17
+ # problematic. See: http://github.com/javan/whenever/issues#issue/1
18
+ begin
19
+ require 'active_support'
20
+ rescue LoadError => e
21
+ warn 'To user Whenever you need the activesupport gem:'
22
+ warn '$ sudo gem install activesupport'
23
+ exit(1)
24
+ end
25
+
26
+ # Whenever files
27
+ %w{
28
+ base
29
+ version
30
+ job_list
31
+ job_types/default
32
+ job_types/rake_task
33
+ job_types/runner
34
+ outputs/cron
35
+ command_line
36
+ }.each { |file| require File.expand_path(File.dirname(__FILE__) + "/#{file}") }
@@ -0,0 +1,101 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/test_helper")
2
+
3
+ class CommandLineTest < Test::Unit::TestCase
4
+
5
+ context "A command line write" do
6
+ setup do
7
+ File.expects(:exists?).with('config/schedule.rb').returns(true)
8
+ @command = Whenever::CommandLine.new(:write => true, :identifier => 'My identifier')
9
+ @task = "#{two_hours} /my/command"
10
+ Whenever.expects(:cron).returns(@task)
11
+ end
12
+
13
+ should "output the cron job with identifier blocks" do
14
+ output = <<-EXPECTED
15
+ # Begin Whenever generated tasks for: My identifier
16
+ #{@task}
17
+ # End Whenever generated tasks for: My identifier
18
+ EXPECTED
19
+
20
+ assert_equal output, @command.send(:whenever_cron)
21
+ end
22
+
23
+ should "write the crontab when run" do
24
+ @command.expects(:write_crontab).returns(true)
25
+ assert @command.run
26
+ end
27
+ end
28
+
29
+ context "A command line update" do
30
+ setup do
31
+ File.expects(:exists?).with('config/schedule.rb').returns(true)
32
+ @command = Whenever::CommandLine.new(:update => true, :identifier => 'My identifier')
33
+ @task = "#{two_hours} /my/command"
34
+ Whenever.expects(:cron).returns(@task)
35
+ end
36
+
37
+ should "add the cron to the end of the file if there is no existing identifier block" do
38
+ existing = '# Existing crontab'
39
+ @command.expects(:read_crontab).at_least_once.returns(existing)
40
+
41
+ new_cron = <<-EXPECTED
42
+ #{existing}
43
+
44
+ # Begin Whenever generated tasks for: My identifier
45
+ #{@task}
46
+ # End Whenever generated tasks for: My identifier
47
+ EXPECTED
48
+
49
+ assert_equal new_cron, @command.send(:updated_crontab)
50
+
51
+ @command.expects(:write_crontab).with(new_cron).returns(true)
52
+ assert @command.run
53
+ end
54
+
55
+ should "replace an existing block if the identifier matches" do
56
+ existing = <<-EXISTING_CRON
57
+ # Something
58
+
59
+ # Begin Whenever generated tasks for: My identifier
60
+ My whenever job that was already here
61
+ # End Whenever generated tasks for: My identifier
62
+
63
+ # Begin Whenever generated tasks for: Other identifier
64
+ This shouldn't get replaced
65
+ # End Whenever generated tasks for: Other identifier
66
+ EXISTING_CRON
67
+
68
+ @command.expects(:read_crontab).at_least_once.returns(existing)
69
+
70
+ new_cron = <<-NEW_CRON
71
+ # Something
72
+
73
+ # Begin Whenever generated tasks for: My identifier
74
+ #{@task}
75
+ # End Whenever generated tasks for: My identifier
76
+
77
+ # Begin Whenever generated tasks for: Other identifier
78
+ This shouldn't get replaced
79
+ # End Whenever generated tasks for: Other identifier
80
+ NEW_CRON
81
+
82
+ assert_equal new_cron, @command.send(:updated_crontab)
83
+
84
+ @command.expects(:write_crontab).with(new_cron).returns(true)
85
+ assert @command.run
86
+ end
87
+ end
88
+
89
+ context "A command line update with no identifier" do
90
+ setup do
91
+ File.expects(:exists?).with('config/schedule.rb').returns(true)
92
+ Whenever::CommandLine.any_instance.expects(:default_identifier).returns('DEFAULT')
93
+ @command = Whenever::CommandLine.new(:update => true, :file => @file)
94
+ end
95
+
96
+ should "use the default identifier" do
97
+ assert_equal "Whenever generated tasks for: DEFAULT", @command.send(:comment_base)
98
+ end
99
+ end
100
+
101
+ end