whenever-benlangfeld 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.travis.yml +18 -0
  4. data/CHANGELOG.md +333 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +22 -0
  7. data/README.md +260 -0
  8. data/Rakefile +10 -0
  9. data/bin/whenever +41 -0
  10. data/bin/wheneverize +68 -0
  11. data/gemfiles/activesupport4.1.gemfile +5 -0
  12. data/gemfiles/activesupport4.2.gemfile +5 -0
  13. data/lib/whenever.rb +34 -0
  14. data/lib/whenever/capistrano.rb +7 -0
  15. data/lib/whenever/capistrano/v2/hooks.rb +8 -0
  16. data/lib/whenever/capistrano/v2/recipes.rb +48 -0
  17. data/lib/whenever/capistrano/v2/support.rb +53 -0
  18. data/lib/whenever/capistrano/v3/tasks/whenever.rake +45 -0
  19. data/lib/whenever/command_line.rb +135 -0
  20. data/lib/whenever/cron.rb +153 -0
  21. data/lib/whenever/job.rb +54 -0
  22. data/lib/whenever/job_list.rb +155 -0
  23. data/lib/whenever/numeric.rb +13 -0
  24. data/lib/whenever/numeric_seconds.rb +48 -0
  25. data/lib/whenever/os.rb +7 -0
  26. data/lib/whenever/output_redirection.rb +57 -0
  27. data/lib/whenever/setup.rb +26 -0
  28. data/lib/whenever/tasks/whenever.rake +1 -0
  29. data/lib/whenever/version.rb +3 -0
  30. data/test/functional/command_line_test.rb +331 -0
  31. data/test/functional/output_at_test.rb +207 -0
  32. data/test/functional/output_default_defined_jobs_test.rb +296 -0
  33. data/test/functional/output_defined_job_test.rb +85 -0
  34. data/test/functional/output_env_test.rb +29 -0
  35. data/test/functional/output_jobs_for_roles_test.rb +65 -0
  36. data/test/functional/output_redirection_test.rb +248 -0
  37. data/test/test_case.rb +32 -0
  38. data/test/test_helper.rb +37 -0
  39. data/test/unit/capistrano_support_test.rb +147 -0
  40. data/test/unit/cron_test.rb +244 -0
  41. data/test/unit/job_test.rb +114 -0
  42. data/whenever.gemspec +27 -0
  43. metadata +167 -0
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/{functional,unit}/**/*_test.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'whenever'
5
+ require 'whenever/version'
6
+
7
+ options = {}
8
+
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: whenever [options]"
11
+ opts.on('-i', '--update-crontab [identifier]', 'Default: full path to schedule.rb file') do |identifier|
12
+ options[:update] = true
13
+ options[:identifier] = identifier if identifier
14
+ end
15
+ opts.on('-w', '--write-crontab [identifier]', 'Default: full path to schedule.rb file') do |identifier|
16
+ options[:write] = true
17
+ options[:identifier] = identifier if identifier
18
+ end
19
+ opts.on('-c', '--clear-crontab [identifier]') do |identifier|
20
+ options[:clear] = true
21
+ options[:identifier] = identifier if identifier
22
+ end
23
+ opts.on('-s', '--set [variables]', 'Example: --set \'environment=staging&path=/my/sweet/path\'') do |set|
24
+ options[:set] = set if set
25
+ end
26
+ opts.on('-f', '--load-file [schedule file]', 'Default: config/schedule.rb') do |file|
27
+ options[:file] = file if file
28
+ end
29
+ opts.on('-u', '--user [user]', 'Default: current user') do |user|
30
+ options[:user] = user if user
31
+ end
32
+ opts.on('-k', '--cut [lines]', 'Cut lines from the top of the cronfile') do |lines|
33
+ options[:cut] = lines.to_i if lines
34
+ end
35
+ opts.on('-r', '--roles [role1,role2]', 'Comma-separated list of server roles to generate cron jobs for') do |roles|
36
+ options[:roles] = roles.split(',').map(&:to_sym) if roles
37
+ end
38
+ opts.on('-v', '--version') { puts "Whenever v#{Whenever::VERSION}"; exit(0) }
39
+ end.parse!
40
+
41
+ Whenever::CommandLine.execute(options)
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This file is based heavily on Capistrano's `capify` command
4
+
5
+ require 'optparse'
6
+ require 'fileutils'
7
+
8
+ OptionParser.new do |opts|
9
+ opts.banner = "Usage: #{File.basename($0)} [path]"
10
+
11
+ begin
12
+ opts.parse!(ARGV)
13
+ rescue OptionParser::ParseError => e
14
+ warn e.message
15
+ puts opts
16
+ exit 1
17
+ end
18
+ end
19
+
20
+ unless ARGV.empty?
21
+ if !File.exist?(ARGV.first)
22
+ abort "`#{ARGV.first}' does not exist."
23
+ elsif !File.directory?(ARGV.first)
24
+ abort "`#{ARGV.first}' is not a directory."
25
+ elsif ARGV.length > 1
26
+ abort "Too many arguments; please specify only the directory to wheneverize."
27
+ end
28
+ end
29
+
30
+ content = <<-FILE
31
+ # Use this file to easily define all of your cron jobs.
32
+ #
33
+ # It's helpful, but not entirely necessary to understand cron before proceeding.
34
+ # http://en.wikipedia.org/wiki/Cron
35
+
36
+ # Example:
37
+ #
38
+ # set :output, "/path/to/my/cron_log.log"
39
+ #
40
+ # every 2.hours do
41
+ # command "/usr/bin/some_great_command"
42
+ # runner "MyModel.some_method"
43
+ # rake "some:great:rake:task"
44
+ # end
45
+ #
46
+ # every 4.days do
47
+ # runner "AnotherModel.prune_old_records"
48
+ # end
49
+
50
+ # Learn more: http://github.com/javan/whenever
51
+ FILE
52
+
53
+ file = 'config/schedule.rb'
54
+ base = ARGV.empty? ? '.' : ARGV.shift
55
+
56
+ file = File.join(base, file)
57
+ if File.exist?(file)
58
+ warn "[skip] `#{file}' already exists"
59
+ elsif File.exist?(file.downcase)
60
+ warn "[skip] `#{file.downcase}' exists, which could conflict with `#{file}'"
61
+ elsif !File.exist?(File.dirname(file))
62
+ warn "[skip] directory `#{File.dirname(file)}' does not exist"
63
+ else
64
+ puts "[add] writing `#{file}'"
65
+ File.open(file, "w") { |f| f.write(content) }
66
+ end
67
+
68
+ puts "[done] wheneverized!"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activesupport", "~> 4.1.0"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activesupport", "~> 4.2.0"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,34 @@
1
+ require 'whenever/numeric'
2
+ require 'whenever/numeric_seconds'
3
+ require 'whenever/job_list'
4
+ require 'whenever/job'
5
+ require 'whenever/command_line'
6
+ require 'whenever/cron'
7
+ require 'whenever/output_redirection'
8
+ require 'whenever/os'
9
+
10
+ module Whenever
11
+ def self.cron(options)
12
+ Whenever::JobList.new(options).generate_cron_output
13
+ end
14
+
15
+ def self.seconds(number, units)
16
+ Whenever::NumericSeconds.seconds(number, units)
17
+ end
18
+
19
+ def self.path
20
+ Dir.pwd
21
+ end
22
+
23
+ def self.bin_rails?
24
+ File.exist?(File.join(path, 'bin', 'rails'))
25
+ end
26
+
27
+ def self.script_rails?
28
+ File.exist?(File.join(path, 'script', 'rails'))
29
+ end
30
+
31
+ def self.bundler?
32
+ File.exist?(File.join(path, 'Gemfile'))
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ require 'capistrano/version'
2
+
3
+ if defined?(Capistrano::VERSION) && Gem::Version.new(Capistrano::VERSION).release >= Gem::Version.new('3.0.0')
4
+ load File.expand_path("../tasks/whenever.rake", __FILE__)
5
+ else
6
+ require 'whenever/capistrano/v2/hooks'
7
+ end
@@ -0,0 +1,8 @@
1
+ require "whenever/capistrano/v2/recipes"
2
+
3
+ Capistrano::Configuration.instance(:must_exist).load do
4
+ # Write the new cron jobs near the end.
5
+ before "deploy:finalize_update", "whenever:update_crontab"
6
+ # If anything goes wrong, undo.
7
+ after "deploy:rollback", "whenever:update_crontab"
8
+ end
@@ -0,0 +1,48 @@
1
+ require 'whenever/capistrano/v2/support'
2
+
3
+ Capistrano::Configuration.instance(:must_exist).load do
4
+ Whenever::CapistranoSupport.load_into(self)
5
+
6
+ _cset(:whenever_roles) { :db }
7
+ _cset(:whenever_options) { {:roles => fetch(:whenever_roles)} }
8
+ _cset(:whenever_command) { "whenever" }
9
+ _cset(:whenever_identifier) { fetch :application }
10
+ _cset(:whenever_environment) { fetch :rails_env, fetch(:stage, "production") }
11
+ _cset(:whenever_variables) { "environment=#{fetch :whenever_environment}" }
12
+ _cset(:whenever_update_flags) { "--update-crontab #{fetch :whenever_identifier} --set #{fetch :whenever_variables}" }
13
+ _cset(:whenever_clear_flags) { "--clear-crontab #{fetch :whenever_identifier}" }
14
+
15
+ namespace :whenever do
16
+ desc "Update application's crontab entries using Whenever"
17
+ task :update_crontab do
18
+ args = {
19
+ :command => fetch(:whenever_command),
20
+ :flags => fetch(:whenever_update_flags),
21
+ :path => fetch(:latest_release)
22
+ }
23
+
24
+ if whenever_servers.any?
25
+ args = whenever_prepare_for_rollback(args) if task_call_frames[0].task.fully_qualified_name == 'deploy:rollback'
26
+ whenever_run_commands(args)
27
+
28
+ on_rollback do
29
+ args = whenever_prepare_for_rollback(args)
30
+ whenever_run_commands(args)
31
+ end
32
+ end
33
+ end
34
+
35
+ desc "Clear application's crontab entries using Whenever"
36
+ task :clear_crontab do
37
+ if whenever_servers.any?
38
+ args = {
39
+ :command => fetch(:whenever_command),
40
+ :flags => fetch(:whenever_clear_flags),
41
+ :path => fetch(:latest_release)
42
+ }
43
+
44
+ whenever_run_commands(args)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,53 @@
1
+ module Whenever
2
+ module CapistranoSupport
3
+ def self.load_into(capistrano_configuration)
4
+ capistrano_configuration.load do
5
+
6
+ def whenever_options
7
+ fetch(:whenever_options)
8
+ end
9
+
10
+ def whenever_roles
11
+ Array(whenever_options[:roles])
12
+ end
13
+
14
+ def whenever_servers
15
+ find_servers(whenever_options)
16
+ end
17
+
18
+ def whenever_server_roles
19
+ whenever_servers.inject({}) do |map, server|
20
+ map[server] = role_names_for_host(server) & whenever_roles
21
+ map
22
+ end
23
+ end
24
+
25
+ def whenever_prepare_for_rollback args
26
+ if fetch(:previous_release)
27
+ # rollback to the previous release's crontab
28
+ args[:path] = fetch(:previous_release)
29
+ else
30
+ # clear the crontab if no previous release
31
+ args[:path] = fetch(:release_path)
32
+ args[:flags] = fetch(:whenever_clear_flags)
33
+ end
34
+ args
35
+ end
36
+
37
+ def whenever_run_commands(args)
38
+ unless [:command, :path, :flags].all? { |a| args.include?(a) }
39
+ raise ArgumentError, ":command, :path, & :flags are required"
40
+ end
41
+
42
+ whenever_server_roles.each do |server, roles|
43
+ roles_arg = roles.empty? ? "" : " --roles #{roles.join(',')}"
44
+
45
+ command = "cd #{args[:path]} && #{args[:command]} #{args[:flags]}#{roles_arg}"
46
+ run command, whenever_options.merge(:hosts => server)
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ namespace :whenever do
2
+ def setup_whenever_task(*args, &block)
3
+ args = Array(fetch(:whenever_command)) + args
4
+
5
+ on roles fetch(:whenever_roles) do |host|
6
+ args_for_host = block_given? ? args + Array(yield(host)) : args
7
+ within release_path do
8
+ with rails_env: fetch(:whenever_environment) do
9
+ with fetch(:whenever_command_environment_variables) do
10
+ execute *args_for_host
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ desc "Update application's crontab entries using Whenever"
18
+ task :update_crontab do
19
+ setup_whenever_task do |host|
20
+ roles = host.roles_array.join(",")
21
+ [fetch(:whenever_update_flags), "--roles=#{roles}"]
22
+ end
23
+ end
24
+
25
+ desc "Clear application's crontab entries using Whenever"
26
+ task :clear_crontab do
27
+ setup_whenever_task(fetch(:whenever_clear_flags))
28
+ end
29
+
30
+ after "deploy:updated", "whenever:update_crontab"
31
+ after "deploy:reverted", "whenever:update_crontab"
32
+ end
33
+
34
+ namespace :load do
35
+ task :defaults do
36
+ set :whenever_roles, ->{ :db }
37
+ set :whenever_command, ->{ [:bundle, :exec, :whenever] }
38
+ set :whenever_command_environment_variables, ->{ {} }
39
+ set :whenever_identifier, ->{ fetch :application }
40
+ set :whenever_environment, ->{ fetch :rails_env, fetch(:stage, "production") }
41
+ set :whenever_variables, ->{ "environment=#{fetch :whenever_environment}" }
42
+ set :whenever_update_flags, ->{ "--update-crontab #{fetch :whenever_identifier} --set #{fetch :whenever_variables}" }
43
+ set :whenever_clear_flags, ->{ "--clear-crontab #{fetch :whenever_identifier}" }
44
+ end
45
+ end
@@ -0,0 +1,135 @@
1
+ require 'fileutils'
2
+
3
+ module Whenever
4
+ class CommandLine
5
+ def self.execute(options={})
6
+ new(options).run
7
+ end
8
+
9
+ def initialize(options={})
10
+ @options = options
11
+
12
+ @options[:file] ||= 'config/schedule.rb'
13
+ @options[:cut] ||= 0
14
+ @options[:identifier] ||= default_identifier
15
+
16
+ if !File.exist?(@options[:file]) && @options[:clear].nil?
17
+ warn("[fail] Can't find file: #{@options[:file]}")
18
+ exit(1)
19
+ end
20
+
21
+ if [@options[:update], @options[:write], @options[:clear]].compact.length > 1
22
+ warn("[fail] Can only update, write or clear. Choose one.")
23
+ exit(1)
24
+ end
25
+
26
+ unless @options[:cut].to_s =~ /[0-9]*/
27
+ warn("[fail] Can't cut negative lines from the crontab #{options[:cut]}")
28
+ exit(1)
29
+ end
30
+ @options[:cut] = @options[:cut].to_i
31
+ end
32
+
33
+ def run
34
+ if @options[:update] || @options[:clear]
35
+ write_crontab(updated_crontab)
36
+ elsif @options[:write]
37
+ write_crontab(whenever_cron)
38
+ else
39
+ puts Whenever.cron(@options)
40
+ puts "## [message] Above is your schedule file converted to cron syntax; your crontab file was not updated."
41
+ puts "## [message] Run `whenever --help' for more 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
+ command = ['crontab']
69
+ command << "-u #{@options[:user]}" if @options[:user]
70
+ # Solaris/SmartOS cron does not support the - option to read from stdin.
71
+ command << "-" unless OS.solaris?
72
+
73
+ IO.popen(command.join(' '), 'r+') do |crontab|
74
+ crontab.write(contents)
75
+ crontab.close_write
76
+ end
77
+
78
+ success = $?.exitstatus.zero?
79
+
80
+ if success
81
+ action = 'written' if @options[:write]
82
+ action = 'updated' if @options[:update]
83
+ puts "[write] crontab file #{action}"
84
+ exit(0)
85
+ else
86
+ warn "[fail] Couldn't write crontab; try running `whenever' with no options to ensure your schedule file is valid."
87
+ exit(1)
88
+ end
89
+ end
90
+
91
+ def updated_crontab
92
+ # Check for unopened or unclosed identifier blocks
93
+ if read_crontab =~ Regexp.new("^#{comment_open}\s*$") && (read_crontab =~ Regexp.new("^#{comment_close}\s*$")).nil?
94
+ warn "[fail] Unclosed indentifier; Your crontab file contains '#{comment_open}', but no '#{comment_close}'"
95
+ exit(1)
96
+ elsif (read_crontab =~ Regexp.new("^#{comment_open}\s*$")).nil? && read_crontab =~ Regexp.new("^#{comment_close}\s*$")
97
+ warn "[fail] Unopened indentifier; Your crontab file contains '#{comment_close}', but no '#{comment_open}'"
98
+ exit(1)
99
+ end
100
+
101
+ # If an existing identier block is found, replace it with the new cron entries
102
+ if read_crontab =~ Regexp.new("^#{comment_open}\s*$") && read_crontab =~ Regexp.new("^#{comment_close}\s*$")
103
+ # If the existing crontab file contains backslashes they get lost going through gsub.
104
+ # .gsub('\\', '\\\\\\') preserves them. Go figure.
105
+ read_crontab.gsub(Regexp.new("^#{comment_open}\s*$.+^#{comment_close}\s*$", Regexp::MULTILINE), whenever_cron.chomp.gsub('\\', '\\\\\\'))
106
+ else # Otherwise, append the new cron entries after any existing ones
107
+ [read_crontab, whenever_cron].join("\n\n")
108
+ end.gsub(/\n{3,}/, "\n\n") # More than two newlines becomes just two.
109
+ end
110
+
111
+ def prepare(contents)
112
+ # Strip n lines from the top of the file as specified by the :cut option.
113
+ # Use split with a -1 limit option to ensure the join is able to rebuild
114
+ # the file with all of the original seperators in-tact.
115
+ stripped_contents = contents.split($/,-1)[@options[:cut]..-1].join($/)
116
+
117
+ # Some cron implementations require all non-comment lines to be newline-
118
+ # terminated. (issue #95) Strip all newlines and replace with the default
119
+ # platform record seperator ($/)
120
+ stripped_contents.gsub!(/\s+$/, $/)
121
+ end
122
+
123
+ def comment_base
124
+ "Whenever generated tasks for: #{@options[:identifier]}"
125
+ end
126
+
127
+ def comment_open
128
+ "# Begin #{comment_base}"
129
+ end
130
+
131
+ def comment_close
132
+ "# End #{comment_base}"
133
+ end
134
+ end
135
+ end