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/CHANGELOG.rdoc +40 -0
- data/Manifest +23 -0
- data/README.rdoc +135 -0
- data/Rakefile +13 -0
- data/bin/whenever +29 -0
- data/bin/wheneverize +69 -0
- data/lib/base.rb +39 -0
- data/lib/command_line.rb +108 -0
- data/lib/job_list.rb +144 -0
- data/lib/job_types/default.rb +27 -0
- data/lib/job_types/rake_task.rb +12 -0
- data/lib/job_types/runner.rb +12 -0
- data/lib/outputs/cron.rb +117 -0
- data/lib/version.rb +9 -0
- data/lib/whenever.rb +27 -0
- data/test/command_line_test.rb +107 -0
- data/test/cron_test.rb +226 -0
- data/test/job_list_test.rb +63 -0
- data/test/output_command_test.rb +70 -0
- data/test/output_env_test.rb +23 -0
- data/test/output_rake_test.rb +74 -0
- data/test/output_runner_test.rb +125 -0
- data/test/test_helper.rb +33 -0
- data/whenever.gemspec +39 -0
- metadata +122 -0
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
|
data/lib/outputs/cron.rb
ADDED
|
@@ -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
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
|