scron 1.0.0

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 (4) hide show
  1. data/scron +14 -0
  2. data/scron.rb +123 -0
  3. data/scron_test.rb +124 -0
  4. metadata +84 -0
data/scron ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require File.expand_path('scron', File.dirname(__FILE__))
4
+
5
+ ARGV.options do |o|
6
+ o.set_summary_indent(' ')
7
+ o.banner = "Usage: #{File.basename($0)} [OPTION]"
8
+ o.define_head "Scheduler for laptops/machines which aren't on 24/7"
9
+ o.on('-e', '--edit', 'edit jobs') { Scron.edit; exit }
10
+ o.on('-r', '--run', 'run jobs') { Scron.run; exit }
11
+ o.on('-h', '--help', 'show this help message') { puts o; exit }
12
+ o.parse!
13
+ puts o
14
+ end
@@ -0,0 +1,123 @@
1
+ require 'date'
2
+
3
+ class Scron
4
+ VERSION = '1.0.0'
5
+ SCHEDULE_FILE = "#{ENV['HOME']}/.scron"
6
+ HISTORY_FILE = "#{ENV['HOME']}/.scrondb"
7
+ LOG_FILE = "#{ENV['HOME']}/.scronlog"
8
+ EDITOR = ENV['EDITOR'] || 'vi'
9
+ attr_reader :history, :schedules
10
+
11
+ def initialize(text, history_text)
12
+ @history = History.new(history_text)
13
+ @schedules = text.split("\n").
14
+ reject {|l| l =~ /^\s+$/}.
15
+ map {|l| Schedule.new(l, @history)}
16
+ end
17
+
18
+ def self.run
19
+ scron = Scron.new(read(SCHEDULE_FILE), read(HISTORY_FILE))
20
+ overdue = scron.schedules.select {|s| s.overdue?}
21
+ return if overdue.size == 0
22
+
23
+ logger = []
24
+ overdue.each do |schedule|
25
+ output = `#{schedule.command}`
26
+ logger << "=> #{now.strftime(History::FORMAT)} #{schedule.command} (#{$?.to_i})"
27
+ logger << output unless output == ''
28
+ scron.history.touch(schedule.command) if $?.to_i == 0
29
+ end
30
+ File.open(HISTORY_FILE, "w") {|f| f.puts scron.history.to_s}
31
+ File.open(LOG_FILE, "a") {|f| f.puts logger.map {|l| l.strip}.join("\n")}
32
+ end
33
+
34
+ def self.now
35
+ @now ||= DateTime.now
36
+ end
37
+
38
+ def self.edit
39
+ `#{EDITOR} #{SCHEDULE_FILE} < \`tty\` > \`tty\``
40
+ end
41
+
42
+ private
43
+ def self.read(filename)
44
+ File.exist?(filename) ? File.read(filename) : ''
45
+ end
46
+ end
47
+
48
+ class Schedule
49
+ attr_reader :interval, :command
50
+ WEEKDAYS = {'Mo' => 1, 'Tu' => 2, 'We' => 3, 'Th' => 4, 'Fr' => 5,
51
+ 'Sa' => 6, 'Su' => 7}
52
+
53
+ def initialize(line, history)
54
+ interval, command = line.split(/\s+/, 2)
55
+ @interval = interval.split(',').map {|i| parse_days(i)}.min
56
+ @command = command.strip
57
+ @overdue = history[command].nil? ||
58
+ (Scron.now - history[command]).to_f > @interval
59
+ end
60
+
61
+ def overdue?
62
+ !!@overdue
63
+ end
64
+
65
+ private
66
+ def parse_days(interval)
67
+ now = Scron.now
68
+ if WEEKDAYS[interval]
69
+ (now.cwday - WEEKDAYS[interval]) % 7 + 1
70
+ elsif interval =~ /^\d+(st|nd|rd|th)$/
71
+ day = interval.to_i
72
+ delta = now.day >= day ?
73
+ now.day - day :
74
+ now - last_month(day)
75
+ delta.to_i + 1
76
+ elsif interval =~ /^(\d+)\/(\d+)$/
77
+ year, month, day = Scron.now.year, $1.to_i, $2.to_i
78
+ year -= 1 if Scron.now.month < month ||
79
+ (Scron.now.month == month && Scron.now.day < day)
80
+ (Scron.now - DateTime.new(year, month, day)).to_i + 1
81
+ elsif interval =~ /^\d+d$/
82
+ interval.to_i
83
+ else
84
+ raise ArgumentError.new("Unable to parse: #{interval}")
85
+ end
86
+ end
87
+
88
+ def last_month(day)
89
+ last = Scron.now << 1
90
+ [day, 30, 29, 28].each do |d|
91
+ date = DateTime.new(last.year, last.month, d) rescue nil
92
+ return date if date
93
+ end
94
+ end
95
+ end
96
+
97
+ class History
98
+ FORMAT = '%Y-%m-%d.%H:%M'
99
+
100
+ def initialize(text)
101
+ @history = {}
102
+ text.split("\n").reject {|l| l =~ /^\s+$/}.each do |line|
103
+ timestamp, command = line.split(/\s+/, 2)
104
+ @history[command.strip] = DateTime.parse(timestamp, FORMAT)
105
+ end
106
+ end
107
+
108
+ def [](command)
109
+ @history[command]
110
+ end
111
+
112
+ def touch(command)
113
+ @history[command] = Scron.now
114
+ end
115
+
116
+ def to_s
117
+ lines = []
118
+ @history.each do |command, timestamp|
119
+ lines << "#{timestamp.strftime(FORMAT)} #{command}"
120
+ end
121
+ lines.join("\n")
122
+ end
123
+ end
@@ -0,0 +1,124 @@
1
+ require 'rubygems'
2
+ require 'scron'
3
+ require 'minitest/autorun'
4
+
5
+ class ScronTest < MiniTest::Unit::TestCase
6
+ def setup
7
+ Scron.instance_variable_set(:@now, DateTime.new(2010, 3, 15))
8
+ end
9
+
10
+ def test_files
11
+ assert_equal("#{ENV['HOME']}/.scron", Scron::SCHEDULE_FILE)
12
+ assert_equal("#{ENV['HOME']}/.scrondb", Scron::HISTORY_FILE)
13
+ assert_equal("#{ENV['HOME']}/.scronlog", Scron::LOG_FILE)
14
+ end
15
+
16
+ def test_editor
17
+ assert_includes([ENV['EDITOR'] || 'vi'], Scron::EDITOR)
18
+ end
19
+
20
+ def test_empty
21
+ assert_equal('', Scron.send(:read, './non-existent-file'))
22
+ refute_equal('', Scron.send(:read, 'README.md'))
23
+ end
24
+
25
+ def test_no_schedules
26
+ scron = Scron.new('', '')
27
+ assert_equal([], scron.schedules)
28
+ end
29
+
30
+ def test_initialize_schedules
31
+ scron = Scron.new(
32
+ "30d cmd arg1 arg2\n" +
33
+ "7d /path/to/script.rb\n" +
34
+ "1d /path/to/script2.rb",
35
+ "2100-01-01.01:00 cmd arg1 arg2\n" +
36
+ "2000-01-01.01:00 /path/to/script.rb")
37
+ assert_equal(3, scron.schedules.size)
38
+ refute(scron.schedules[0].overdue?)
39
+ assert(scron.schedules[1].overdue?)
40
+ assert(scron.schedules[2].overdue?)
41
+ end
42
+ end
43
+
44
+ class ScheduleTest
45
+ def test_parse_day_interval
46
+ sched = Schedule.new('1d c', History.new(''))
47
+ assert_equal(1, sched.send(:parse_days, '1d'))
48
+ assert_equal(30, sched.send(:parse_days, '30d'))
49
+ end
50
+
51
+ def test_parse_day_of_week
52
+ sched = Schedule.new('1d c', History.new(''))
53
+ assert_equal(1, sched.send(:parse_days, 'Mo'))
54
+ assert_equal(7, sched.send(:parse_days, 'Tu'))
55
+ assert_equal(6, sched.send(:parse_days, 'We'))
56
+ assert_equal(5, sched.send(:parse_days, 'Th'))
57
+ assert_equal(4, sched.send(:parse_days, 'Fr'))
58
+ assert_equal(3, sched.send(:parse_days, 'Sa'))
59
+ assert_equal(2, sched.send(:parse_days, 'Su'))
60
+ end
61
+
62
+ def test_parse_day_of_month
63
+ sched = Schedule.new('1d c', History.new(''))
64
+ assert_equal(15, sched.send(:parse_days, '1st'))
65
+ assert_equal(1, sched.send(:parse_days, '15th'))
66
+ assert_equal(21, sched.send(:parse_days, '23rd'))
67
+ assert_equal(16, sched.send(:parse_days, '31st'))
68
+ end
69
+
70
+ def test_parse_day_of_year
71
+ sched = Schedule.new('1d c', History.new(''))
72
+ assert_equal(74, sched.send(:parse_days, '1/1'))
73
+ assert_equal(1, sched.send(:parse_days, '3/15'))
74
+ assert_equal(81, sched.send(:parse_days, '12/25'))
75
+ end
76
+
77
+ def test_initialize_command
78
+ sched = Schedule.new('30d cmd arg1 arg2', History.new(''))
79
+ assert_equal('cmd arg1 arg2', sched.command)
80
+ assert_equal(30, sched.interval)
81
+ assert(sched.overdue?)
82
+ end
83
+
84
+ def test_bad_date
85
+ sched = Schedule.new('1d c', History.new(''))
86
+ assert_raises(ArgumentError) { sched.send(:parse_days, '2/31') }
87
+ assert_raises(ArgumentError) { sched.send(:parse_days, '1') }
88
+ end
89
+
90
+ def test_multiple_intervals
91
+ assert_equal(1, Schedule.new('1d,2d,3d cmd', History.new('')).interval)
92
+ assert_equal(2, Schedule.new('Fr,Sa,Su cmd', History.new('')).interval)
93
+ assert_equal(15, Schedule.new('1st,23rd cmd', History.new('')).interval)
94
+ end
95
+
96
+ def test_overdue_history
97
+ sched = Schedule.new('30d cmd', History.new('2000-01-01.01:00 cmd'))
98
+ assert(sched.overdue?)
99
+ end
100
+
101
+ def test_recent_history
102
+ sched = Schedule.new('30d cmd', History.new('2100-01-01.01:00 cmd'))
103
+ refute(sched.overdue?)
104
+ end
105
+ end
106
+
107
+ class HistoryTest
108
+ def test_initialize
109
+ history = History.new('2100-01-01.01:00 cmd arg1 arg2')
110
+ assert_equal(DateTime.new(2100, 1, 1, 1, 0), history['cmd arg1 arg2'])
111
+ end
112
+
113
+ def test_update_command
114
+ history = History.new('')
115
+ history.touch('cmd')
116
+ assert_kind_of(DateTime, history['cmd'])
117
+ end
118
+
119
+ def test_output
120
+ history = History.new('')
121
+ history.touch('cmd')
122
+ assert_match(/^20\d{2}-\d{2}-\d{2}.\d{2}:\d{2} cmd$/, history.to_s)
123
+ end
124
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scron
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Hugh Bien
14
+ autorequire:
15
+ bindir: .
16
+ cert_chain: []
17
+
18
+ date: 2011-10-06 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: minitest
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ description: Run commands at scheduled intervals. If an interval is missed, the command will be run as soon as possible.
35
+ email:
36
+ - hugh@hughbien.com
37
+ executables:
38
+ - scron
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - scron.rb
45
+ - scron_test.rb
46
+ - scron
47
+ - ./scron
48
+ homepage: https://github.com/hughbien/scron
49
+ licenses: []
50
+
51
+ post_install_message:
52
+ rdoc_options: []
53
+
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 23
71
+ segments:
72
+ - 1
73
+ - 3
74
+ - 6
75
+ version: 1.3.6
76
+ requirements: []
77
+
78
+ rubyforge_project:
79
+ rubygems_version: 1.8.11
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Scheduler for laptops/machines which aren't on 24/7
83
+ test_files: []
84
+