adg-whenever 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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