adg-whenever 0.2.2

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/lib/job_list.rb ADDED
@@ -0,0 +1,144 @@
1
+ module Whenever
2
+ class JobList
3
+
4
+ def initialize(options)
5
+ @jobs = Hash.new
6
+ @env = Hash.new
7
+
8
+ config = case options
9
+ when String then options
10
+ when Hash
11
+ if options[:string]
12
+ options[:string]
13
+ elsif options[:file]
14
+ @filename = options[:file]
15
+ file_read = File.read(@filename)
16
+ end
17
+ end
18
+
19
+ if file_read
20
+ eval(ERB.new(file_read).result(binding))
21
+ else
22
+ eval(config)
23
+ end
24
+ end
25
+
26
+ def set(variable, value)
27
+ instance_variable_set("@#{variable}".to_sym, value)
28
+ self.class.send(:attr_reader, variable.to_sym)
29
+ end
30
+
31
+ def env(variable, value)
32
+ @env[variable.to_s] = value
33
+ end
34
+
35
+ def every(frequency, options = {})
36
+ @current_time_scope = frequency
37
+ @options = options
38
+ yield
39
+ end
40
+
41
+ def command(task, options = {})
42
+ options[:cron_log] ||= @cron_log unless options[:cron_log] === false
43
+ options[:class] ||= Whenever::Job::Default
44
+ @jobs[@current_time_scope] ||= []
45
+ @jobs[@current_time_scope] << options[:class].new(@options.merge(:task => task).merge(options))
46
+ end
47
+
48
+ def runner(task, options = {})
49
+ options.reverse_merge!(:environment => @environment, :path => @path)
50
+ options[:class] = Whenever::Job::Runner
51
+ command(task, options)
52
+ end
53
+
54
+ def rake(task, options = {})
55
+ options.reverse_merge!(:environment => @environment, :path => @path)
56
+ options[:class] = Whenever::Job::RakeTask
57
+ command(task, options)
58
+ end
59
+
60
+ def generate_cron_output
61
+ [environment_variables, cron_jobs].compact.join
62
+ end
63
+
64
+ def scheduled_jobs
65
+ @scheduled_jobs ||= begin
66
+ returning scheduled = [] do
67
+ @jobs.each do |time, jobs|
68
+ jobs.each do |j|
69
+ scheduled << ScheduledJob.new(j, time)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def schedule_for_task(task)
77
+ scheduled_job = scheduled_job_for_task(task)
78
+ scheduled_job ? scheduled_job.schedule : "Schedule for #{task} not found"
79
+ end
80
+
81
+ def schedule_data_for_task(task)
82
+ if scheduled_job = scheduled_job_for_task(task)
83
+ scheduled_job.schedule_data
84
+ end
85
+ end
86
+
87
+ # Expect arguments in form:
88
+ # - task: "Otl::Eirb::ProtocolJob" # => class name as string
89
+ # - schedule: {"interval"=>"days", "frequency"=>"2", "at"=>"10:00pm"}
90
+ def update_schedule_for_task(task, schedule)
91
+ lines = File.readlines(@filename)
92
+ lines.each_with_index do |line, i|
93
+ if line.include?(task)
94
+ j = i - 1
95
+ until lines[j] =~ /^every/
96
+ j -= 1
97
+ end
98
+ lines[j] = "every #{schedule['frequency']}.#{schedule['interval']}, :at => '#{schedule['at']}' do\n"
99
+ File.open(@filename, 'w') do |f|
100
+ lines.each do |line|
101
+ f.write(line)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ scheduled_job_for_task(task)
107
+ end
108
+
109
+ def scheduled_job_for_task(task)
110
+ scheduled_jobs.detect { |sj| sj.task =~ Regexp.new(task) }
111
+ end
112
+
113
+ private
114
+
115
+ def environment_variables
116
+ return if @env.empty?
117
+
118
+ output = []
119
+ @env.each do |key, val|
120
+ output << "#{key}=#{val}\n"
121
+ end
122
+ output << "\n"
123
+
124
+ output.join
125
+ end
126
+
127
+ def cron_jobs
128
+ return if @jobs.empty?
129
+
130
+ output = []
131
+ @jobs.each do |time, jobs|
132
+ jobs.each do |job|
133
+ cron = Whenever::Output::Cron.output(time, job)
134
+ cron << " >> #{job.cron_log} 2>&1" if job.cron_log
135
+ cron << "\n\n"
136
+ output << cron
137
+ end
138
+ end
139
+
140
+ output.join
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,27 @@
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
+ end
14
+
15
+ def output
16
+ task
17
+ end
18
+
19
+ protected
20
+
21
+ def path_required
22
+ raise ArgumentError, "No path available; set :path, '/your/path' in your schedule file" if @path.blank?
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module Whenever
2
+ module Job
3
+ class RakeTask < Whenever::Job::Default
4
+
5
+ def output
6
+ path_required
7
+ "cd #{@path} && RAILS_ENV=#{@environment} /usr/bin/env rake #{task}"
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Whenever
2
+ module Job
3
+ class Runner < Whenever::Job::Default
4
+
5
+ def output
6
+ path_required
7
+ %Q(#{File.join(@path, 'script', 'runner')} -e #{@environment} "#{task}")
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,117 @@
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.output(time, job)
15
+ out = new(time, job.output, job.at)
16
+ "#{out.time_in_cron_syntax} #{out.task}"
17
+ end
18
+
19
+ def time_in_cron_syntax
20
+ case @time
21
+ when Symbol then parse_symbol
22
+ when String then parse_as_string
23
+ else parse_time
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ def parse_symbol
30
+ shortcut = case @time
31
+ when :reboot then '@reboot'
32
+ when :year, :yearly then '@annually'
33
+ when :day, :daily then '@daily'
34
+ when :midnight then '@midnight'
35
+ when :month, :monthly then '@monthly'
36
+ when :week, :weekly then '@weekly'
37
+ when :hour, :hourly then '@hourly'
38
+ end
39
+
40
+ if shortcut
41
+ if @at > 0
42
+ raise ArgumentError, "You cannot specify an ':at' when using the shortcuts for times."
43
+ else
44
+ return shortcut
45
+ end
46
+ else
47
+ parse_as_string
48
+ end
49
+ end
50
+
51
+ def parse_time
52
+ timing = Array.new(5, '*')
53
+ case @time
54
+ when 0.seconds...1.minute
55
+ raise ArgumentError, "Time must be in minutes or higher"
56
+ when 1.minute...1.hour
57
+ minute_frequency = @time / 60
58
+ timing[0] = comma_separated_timing(minute_frequency, 59)
59
+ when 1.hour...1.day
60
+ hour_frequency = (@time / 60 / 60).round
61
+ timing[0] = @at.is_a?(Time) ? @at.min : @at
62
+ timing[1] = comma_separated_timing(hour_frequency, 23)
63
+ when 1.day...1.month
64
+ day_frequency = (@time / 24 / 60 / 60).round
65
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
66
+ timing[1] = @at.is_a?(Time) ? @at.hour : @at
67
+ timing[2] = comma_separated_timing(day_frequency, 31, 1)
68
+ when 1.month..12.months
69
+ month_frequency = (@time / 30 / 24 / 60 / 60).round
70
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
71
+ timing[1] = @at.is_a?(Time) ? @at.hour : 0
72
+ timing[2] = @at.is_a?(Time) ? @at.day : (@at.zero? ? 1 : @at)
73
+ timing[3] = comma_separated_timing(month_frequency, 12, 1)
74
+ else
75
+ return parse_as_string
76
+ end
77
+ timing.join(' ')
78
+ end
79
+
80
+ def parse_as_string
81
+ return unless @time
82
+ string = @time.to_s
83
+
84
+ timing = Array.new(4, '*')
85
+ timing[0] = @at.is_a?(Time) ? @at.min : 0
86
+ timing[1] = @at.is_a?(Time) ? @at.hour : 0
87
+
88
+ return (timing << 'mon-fri') * " " if string.downcase.index('weekday')
89
+ return (timing << 'sat,sun') * " " if string.downcase.index('weekend')
90
+
91
+ %w(sun mon tue wed thu fri sat).each do |day|
92
+ return (timing << day) * " " if string.downcase.index(day)
93
+ end
94
+
95
+ raise ArgumentError, "Couldn't parse: #{@time}"
96
+ end
97
+
98
+ def comma_separated_timing(frequency, max, start = 0)
99
+ return start if frequency.blank? || frequency.zero?
100
+ return '*' if frequency == 1
101
+ return frequency if frequency > (max * 0.5).ceil
102
+
103
+ original_start = start
104
+
105
+ start += frequency unless (max + 1).modulo(frequency).zero? || start > 0
106
+ output = (start..max).step(frequency).to_a
107
+
108
+ max_occurances = (max.to_f / (frequency.to_f)).round
109
+ max_occurances += 1 if original_start.zero?
110
+
111
+ output[0, max_occurances].join(',')
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Whenever
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 2
5
+ TINY = 2
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end unless defined?(Whenever::VERSION)
data/lib/whenever.rb ADDED
@@ -0,0 +1,27 @@
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
+ # Dependencies
13
+ require 'activesupport'
14
+ require 'chronic'
15
+
16
+ # Whenever files
17
+ %w{
18
+ base
19
+ version
20
+ job_list
21
+ job_types/default
22
+ job_types/rake_task
23
+ job_types/runner
24
+ outputs/cron
25
+ command_line
26
+ scheduled_job
27
+ }.each { |file| require File.expand_path(File.dirname(__FILE__) + "/#{file}") }
@@ -0,0 +1,107 @@
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 unindent(output).chomp, @command.send(:whenever_cron).chomp
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 unindent(new_cron).chomp, @command.send(:updated_crontab).chomp
50
+
51
+ @command.expects(:write_crontab).with(unindent(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
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
67
+ @command.expects(:read_crontab).at_least_once.returns(unindent(existing))
68
+
69
+ new_cron = <<-new_cron
70
+ # Something
71
+
72
+ # Begin Whenever generated tasks for: My identifier
73
+ #{@task}
74
+ # End Whenever generated tasks for: My identifier
75
+
76
+ # Begin Whenever generated tasks for: Other identifier
77
+ This shouldn't get replaced
78
+ # End Whenever generated tasks for: Other identifier
79
+ new_cron
80
+
81
+ assert_equal unindent(new_cron).chomp, @command.send(:updated_crontab).chomp
82
+
83
+ @command.expects(:write_crontab).with(unindent(new_cron)).returns(true)
84
+ assert @command.run
85
+ end
86
+ end
87
+
88
+ context "A command line update with no identifier" do
89
+ setup do
90
+ File.expects(:exists?).with('config/schedule.rb').returns(true)
91
+ Whenever::CommandLine.any_instance.expects(:default_identifier).returns('DEFAULT')
92
+ @command = Whenever::CommandLine.new(:update => true, :file => @file)
93
+ end
94
+
95
+ should "use the default identifier" do
96
+ assert_equal "Whenever generated tasks for: DEFAULT", @command.send(:comment_base)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def unindent(string)
103
+ indentation = string[/\A\s*/]
104
+ string.strip.gsub(/^#{indentation}/, "")
105
+ end
106
+
107
+ end