scron 1.0.0

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