whenever-benlangfeld 0.9.5

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.
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