schedule_job 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +58 -0
- data/LICENSE.txt +21 -0
- data/README.md +15 -0
- data/Rakefile +8 -0
- data/bin/schedule +9 -0
- data/lib/schedule/cli.rb +118 -0
- data/lib/schedule/cron.citrus +167 -0
- data/lib/schedule/cron.rb +233 -0
- data/lib/schedule/cron.treetop +167 -0
- data/lib/schedule/cron_parser.rb +85 -0
- data/lib/schedule_job.rb +9 -0
- data/schedule_job.gemspec +23 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0aa14b51b198289543636f7048935e3599a08d5eb2920a2bb7e67b80d259f19d
|
4
|
+
data.tar.gz: 8841e4a6ec99c02c3485927d4ba6e4335a0347b52bba332fc131fff727225e4f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0f7832e1cc148800389da6b5f6ff3c053878cc930496f578e37af46c665f5dcb708db558b8220568c35c6ba5363e3a16351ee3453d65503cc494b1ed3b19c61d
|
7
|
+
data.tar.gz: c7d18b320759e7dccac481d1b4b9af06e91c09b0954d80e53650d7112ff53e202b4ed4c02ebb23039d4a634b53de9800aad9f0d1517fd60786d833d353af38e6
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
schedule_job (1.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activesupport (6.1.4.1)
|
10
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
11
|
+
i18n (>= 1.6, < 2)
|
12
|
+
minitest (>= 5.1)
|
13
|
+
tzinfo (~> 2.0)
|
14
|
+
zeitwerk (~> 2.3)
|
15
|
+
citrus (3.0.2)
|
16
|
+
concurrent-ruby (1.1.9)
|
17
|
+
cronex (0.11.1)
|
18
|
+
tzinfo
|
19
|
+
unicode
|
20
|
+
diff-lcs (1.4.4)
|
21
|
+
i18n (1.8.10)
|
22
|
+
concurrent-ruby (~> 1.0)
|
23
|
+
minitest (5.14.4)
|
24
|
+
parse-cron (0.1.4)
|
25
|
+
rake (13.0.6)
|
26
|
+
rspec (3.10.0)
|
27
|
+
rspec-core (~> 3.10.0)
|
28
|
+
rspec-expectations (~> 3.10.0)
|
29
|
+
rspec-mocks (~> 3.10.0)
|
30
|
+
rspec-core (3.10.1)
|
31
|
+
rspec-support (~> 3.10.0)
|
32
|
+
rspec-expectations (3.10.1)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.10.0)
|
35
|
+
rspec-mocks (3.10.2)
|
36
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
37
|
+
rspec-support (~> 3.10.0)
|
38
|
+
rspec-support (3.10.2)
|
39
|
+
tzinfo (2.0.4)
|
40
|
+
concurrent-ruby (~> 1.0)
|
41
|
+
unicode (0.4.4.4)
|
42
|
+
zeitwerk (2.5.1)
|
43
|
+
|
44
|
+
PLATFORMS
|
45
|
+
x86_64-darwin-18
|
46
|
+
|
47
|
+
DEPENDENCIES
|
48
|
+
activesupport
|
49
|
+
bundler
|
50
|
+
citrus
|
51
|
+
cronex
|
52
|
+
parse-cron
|
53
|
+
rake
|
54
|
+
rspec
|
55
|
+
schedule_job!
|
56
|
+
|
57
|
+
BUNDLED WITH
|
58
|
+
2.2.16
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 David Ellis
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Schedule
|
2
|
+
|
3
|
+
schedule is a frontend around crontab to add/remove cron jobs from user cron tables.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```
|
8
|
+
gem install schedule_job
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
## License
|
14
|
+
|
15
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/schedule
ADDED
data/lib/schedule/cli.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
# schedule --every 10m "my_command --foo --bar"
|
4
|
+
|
5
|
+
module Schedule
|
6
|
+
class Cli
|
7
|
+
def self.run(argv = ARGV)
|
8
|
+
cli = Cli.new
|
9
|
+
args = cli.get_args(argv)
|
10
|
+
cli.execute(args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_args(argv = ARGV)
|
14
|
+
options = {}
|
15
|
+
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.banner = "Usage: schedule [options] command"
|
18
|
+
|
19
|
+
opts.on("-d", "--dryrun", "Report what would happen but do not install the scheduled job.") do |dryrun|
|
20
|
+
options[:dryrun] = dryrun
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-l", "--list", "List installed cron jobs.") do |list|
|
24
|
+
options[:list_jobs] = list
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-r", "--rm JOB_ID", "Remove the specified job id as indicated by --list") do |job_id_to_remove|
|
28
|
+
options[:remove_job] = job_id_to_remove
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-u", "--user USER", "User that the crontab belongs to.") do |user|
|
32
|
+
options[:crontab_user] = user
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-e", "--every DURATION", "Run command every DURATION units of time.
|
36
|
+
For example:
|
37
|
+
--every 10m # meaning, every 10 minutes
|
38
|
+
--every 5h # meaning, every 5 hours
|
39
|
+
--every 2d # meaning, every 2 days
|
40
|
+
--every 3M # meaning, every 3 months
|
41
|
+
") do |every|
|
42
|
+
options[:every] = every
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("-c", "--cron CRON", "Run command on the given cron schedule.
|
46
|
+
For example:
|
47
|
+
--cron \"*/5 15 * * 1-5\" # meaning, Every 5 minutes, at 3:00 PM, Monday through Friday
|
48
|
+
--cron \"0 0/30 8-9 5,20 * ?\" # meaning, Every 30 minutes, between 8:00 AM and 9:59 AM, on day 5 and 20 of the month
|
49
|
+
") do |cron_schedule|
|
50
|
+
options[:cron] = cron_schedule
|
51
|
+
end
|
52
|
+
end.parse!
|
53
|
+
|
54
|
+
options[:command] = ARGV.join(" ").strip
|
55
|
+
options
|
56
|
+
end
|
57
|
+
|
58
|
+
def execute(args)
|
59
|
+
case
|
60
|
+
when args[:list_jobs] # read jobs
|
61
|
+
list_jobs(args)
|
62
|
+
when args[:remove_job] # remove job
|
63
|
+
remove_job(args)
|
64
|
+
when args[:cron] || args[:every] # write jobs
|
65
|
+
install_job(args)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def list_jobs(args)
|
70
|
+
user = args[:crontab_user]
|
71
|
+
|
72
|
+
table = Cron::Table.new(user)
|
73
|
+
|
74
|
+
puts table.to_s unless table.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
def install_job(args)
|
78
|
+
dry_run = args[:dryrun]
|
79
|
+
command = args[:command]
|
80
|
+
user = args[:crontab_user]
|
81
|
+
|
82
|
+
if command.empty?
|
83
|
+
puts "A cron job command hasn't been specified. Please specify which command you would like to run."
|
84
|
+
exit(1)
|
85
|
+
end
|
86
|
+
|
87
|
+
job = case
|
88
|
+
when args[:cron]
|
89
|
+
cron_schedule = args[:cron]
|
90
|
+
Cron::Job.new(cron_schedule, command)
|
91
|
+
when args[:every]
|
92
|
+
duration = args[:every]
|
93
|
+
match = duration.match(/(\d+)(m|h|d|M)/)
|
94
|
+
if match
|
95
|
+
qty = match[1].to_i
|
96
|
+
unit_of_time = match[2].to_s
|
97
|
+
Cron::Job.every(qty, unit_of_time, command)
|
98
|
+
else
|
99
|
+
puts "'#{duration}' is an invalid duration. The duration must be specified as: integer followed immediately by m (minutes), h (hours), d (days), M (months) (e.g. 10m)"
|
100
|
+
exit(1)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
table = Cron::Table.new(user)
|
105
|
+
table.add(job, dry_run)
|
106
|
+
end
|
107
|
+
|
108
|
+
def remove_job(args)
|
109
|
+
dry_run = args[:dryrun]
|
110
|
+
user = args[:crontab_user]
|
111
|
+
job_id = args[:remove_job].to_i # this is a 1-based index that should match the indices from the --list command
|
112
|
+
|
113
|
+
table = Cron::Table.new(user)
|
114
|
+
|
115
|
+
table.remove(job_id, dry_run)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
grammar ScheduleCronParser
|
2
|
+
rule user_crontab
|
3
|
+
sep?
|
4
|
+
(environment sep)?
|
5
|
+
jobspecs?
|
6
|
+
sep?
|
7
|
+
end
|
8
|
+
|
9
|
+
rule environment
|
10
|
+
directive (sep directive)*
|
11
|
+
end
|
12
|
+
|
13
|
+
rule directive
|
14
|
+
var space "=" space expr
|
15
|
+
end
|
16
|
+
|
17
|
+
rule var
|
18
|
+
alpha alphanumeric?
|
19
|
+
end
|
20
|
+
|
21
|
+
rule expr
|
22
|
+
command
|
23
|
+
end
|
24
|
+
|
25
|
+
rule jobspecs
|
26
|
+
jobspec (sep jobspec)*
|
27
|
+
end
|
28
|
+
|
29
|
+
rule jobspec
|
30
|
+
schedule_spec space command ws*
|
31
|
+
end
|
32
|
+
|
33
|
+
rule schedule_spec
|
34
|
+
standard | special
|
35
|
+
end
|
36
|
+
|
37
|
+
rule standard
|
38
|
+
minute space hour space dayofmonth space month space dayofweek
|
39
|
+
end
|
40
|
+
|
41
|
+
rule special
|
42
|
+
"@" ("yearly" | "annually" | "monthly" | "weekly" | "daily" | "hourly" | "reboot")
|
43
|
+
end
|
44
|
+
|
45
|
+
rule minute
|
46
|
+
step
|
47
|
+
end
|
48
|
+
|
49
|
+
rule hour
|
50
|
+
step
|
51
|
+
end
|
52
|
+
|
53
|
+
rule dayofmonthtypes
|
54
|
+
step
|
55
|
+
end
|
56
|
+
|
57
|
+
rule monthtypes
|
58
|
+
step | altmonths
|
59
|
+
end
|
60
|
+
|
61
|
+
rule dayofweektypes
|
62
|
+
step | altdays
|
63
|
+
end
|
64
|
+
|
65
|
+
rule dayofmonth
|
66
|
+
dayofmonthtypes ("," dayofmonthtypes)*
|
67
|
+
end
|
68
|
+
|
69
|
+
rule month
|
70
|
+
monthtypes ("," monthtypes)*
|
71
|
+
end
|
72
|
+
|
73
|
+
rule dayofweek
|
74
|
+
dayofweektypes ("," dayofweektypes)*
|
75
|
+
end
|
76
|
+
|
77
|
+
rule command
|
78
|
+
quoted_string
|
79
|
+
| (!("\n" | comment) .)+
|
80
|
+
end
|
81
|
+
|
82
|
+
rule step
|
83
|
+
common ("/" int)?
|
84
|
+
end
|
85
|
+
|
86
|
+
rule common
|
87
|
+
range | int | any
|
88
|
+
end
|
89
|
+
|
90
|
+
rule range
|
91
|
+
int "-" int
|
92
|
+
end
|
93
|
+
|
94
|
+
rule altdays
|
95
|
+
days ("," days)*
|
96
|
+
end
|
97
|
+
|
98
|
+
rule altmonths
|
99
|
+
months ("," months)*
|
100
|
+
end
|
101
|
+
|
102
|
+
rule days
|
103
|
+
"MON" | "TUE" | "WED" | "THU" | "FRI" | "SAT" | "SUN"
|
104
|
+
end
|
105
|
+
|
106
|
+
rule months
|
107
|
+
"JAN" | "FEB" | "MAR" | "APR" | "MAY" | "JUN" | "JUL" | "AUG" | "SEP" | "OCT" | "NOV" | "DEC"
|
108
|
+
end
|
109
|
+
|
110
|
+
rule any
|
111
|
+
"*"
|
112
|
+
end
|
113
|
+
|
114
|
+
rule int
|
115
|
+
[0-9]+
|
116
|
+
end
|
117
|
+
|
118
|
+
rule alpha
|
119
|
+
[a-zA-Z]+
|
120
|
+
end
|
121
|
+
|
122
|
+
rule alphanumeric
|
123
|
+
[a-zA-Z0-9]+
|
124
|
+
end
|
125
|
+
|
126
|
+
rule sep
|
127
|
+
(ws* nl)+
|
128
|
+
end
|
129
|
+
|
130
|
+
rule ws
|
131
|
+
space | comment
|
132
|
+
end
|
133
|
+
|
134
|
+
rule space
|
135
|
+
[ \t]+
|
136
|
+
end
|
137
|
+
|
138
|
+
rule nl
|
139
|
+
[\n]+
|
140
|
+
end
|
141
|
+
|
142
|
+
rule comment
|
143
|
+
"#" (!"\n" .)*
|
144
|
+
end
|
145
|
+
|
146
|
+
rule quoted_string
|
147
|
+
"\"" ( (!("\"" | "\\") .) | escape)* "\""
|
148
|
+
| "'" ( (!("'" | "\\") .) | escape)* "'"
|
149
|
+
end
|
150
|
+
|
151
|
+
rule escape
|
152
|
+
"\\" escape_sequence
|
153
|
+
end
|
154
|
+
|
155
|
+
rule escape_sequence
|
156
|
+
"'"
|
157
|
+
| "\""
|
158
|
+
| "\\"
|
159
|
+
| "b"
|
160
|
+
| "f"
|
161
|
+
| "n"
|
162
|
+
| "r"
|
163
|
+
| "t"
|
164
|
+
| "v"
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require "active_support/core_ext/numeric/time"
|
2
|
+
require "active_support/core_ext/date_time"
|
3
|
+
require "cronex"
|
4
|
+
require "open3"
|
5
|
+
require "parse-cron"
|
6
|
+
require "shellwords"
|
7
|
+
require "stringio"
|
8
|
+
|
9
|
+
require_relative "./cron_parser"
|
10
|
+
|
11
|
+
module Schedule
|
12
|
+
module Cron
|
13
|
+
|
14
|
+
class EnvironmentVar
|
15
|
+
def initialize(name, value)
|
16
|
+
@name = name
|
17
|
+
@value = value
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"#{@name} = #{@value}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Table
|
26
|
+
def initialize(user = nil)
|
27
|
+
@user = user
|
28
|
+
load(user)
|
29
|
+
end
|
30
|
+
|
31
|
+
def empty?
|
32
|
+
@environment_vars.empty? && @jobs.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def load(user = @user)
|
36
|
+
@raw_crontab = read_crontab(user)
|
37
|
+
@environment_vars, @jobs = parse_crontab(@raw_crontab)
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_crontab(user = @user)
|
41
|
+
command = ["crontab", "-l"]
|
42
|
+
command << "-u #{user}" if user
|
43
|
+
command = command.join(" ")
|
44
|
+
|
45
|
+
stdout, stderr, exit_status = Open3.capture3(command)
|
46
|
+
crontab_output = stdout
|
47
|
+
error_output = stderr
|
48
|
+
|
49
|
+
no_crontab = !exit_status.success? && error_output =~ /no crontab/
|
50
|
+
|
51
|
+
raise("Unable to read crontab: #{error_output}") if !exit_status.success? && !no_crontab
|
52
|
+
|
53
|
+
# puts "read_crontab()"
|
54
|
+
# puts "stdout:"
|
55
|
+
# puts stdout
|
56
|
+
# puts "stderr:"
|
57
|
+
# puts stderr
|
58
|
+
|
59
|
+
crontab_output
|
60
|
+
end
|
61
|
+
|
62
|
+
# returns lines that represent valid cron job specification lines
|
63
|
+
def parse_crontab(user_crontab)
|
64
|
+
parser = TableParser.new(user_crontab)
|
65
|
+
|
66
|
+
# user_crontab.each_line do |line|
|
67
|
+
# puts line
|
68
|
+
# puts "valid? #{LineParser.valid?(line)}"
|
69
|
+
# puts LineParser.parse(line)
|
70
|
+
# end
|
71
|
+
|
72
|
+
return [], [] unless parser.valid?
|
73
|
+
|
74
|
+
# puts "valid!"
|
75
|
+
|
76
|
+
environment_vars = parser.environment_vars&.map do |env_directive|
|
77
|
+
name = env_directive[:var].to_s
|
78
|
+
expr = env_directive[:expr].to_s
|
79
|
+
EnvironmentVar.new(name, expr)
|
80
|
+
end || []
|
81
|
+
|
82
|
+
jobs = parser.job_specs&.map do |jobspec|
|
83
|
+
schedule_spec = jobspec.capture(:schedule_spec).to_s
|
84
|
+
command = jobspec.capture(:command).to_s
|
85
|
+
line_number = parser.get_line_number(jobspec)
|
86
|
+
Job.new(schedule_spec, command, line_number, jobspec.offset)
|
87
|
+
end || []
|
88
|
+
|
89
|
+
[environment_vars, jobs]
|
90
|
+
end
|
91
|
+
|
92
|
+
def add(job, dry_run = false)
|
93
|
+
if @dry_run
|
94
|
+
puts "Would schedule: #{job.to_s}"
|
95
|
+
else
|
96
|
+
puts "Scheduling: #{job.to_s}"
|
97
|
+
install_cron_job(job)
|
98
|
+
end
|
99
|
+
puts "Next three runs:"
|
100
|
+
cron_parser = CronParser.new(job.schedule_spec)
|
101
|
+
first_run_time = cron_parser.next(Time.now)
|
102
|
+
second_run_time = cron_parser.next(first_run_time)
|
103
|
+
third_run_time = cron_parser.next(second_run_time)
|
104
|
+
puts "1. #{first_run_time}"
|
105
|
+
puts "2. #{second_run_time}"
|
106
|
+
puts "3. #{third_run_time}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def install_cron_job(job)
|
110
|
+
job_spec = job.specification
|
111
|
+
# puts "Installing new cron job: #{job_spec}"
|
112
|
+
|
113
|
+
new_crontab = [read_crontab(@user).strip, job_spec.strip].reject(&:empty?).join("\n")
|
114
|
+
write_crontab(new_crontab)
|
115
|
+
end
|
116
|
+
|
117
|
+
# because we print the table out with 1-based job IDs, job_id is a 1-based index into @jobs
|
118
|
+
def remove(job_id, dry_run = false)
|
119
|
+
job_index = job_id - 1
|
120
|
+
|
121
|
+
if job_index >= @jobs.size
|
122
|
+
puts "The specified job ID does not exist."
|
123
|
+
exit(1)
|
124
|
+
end
|
125
|
+
|
126
|
+
job = @jobs[job_index]
|
127
|
+
|
128
|
+
if dry_run
|
129
|
+
puts "Would remove: #{job}"
|
130
|
+
else
|
131
|
+
puts "Removing: #{job}"
|
132
|
+
new_crontab = @raw_crontab.lines.reject.with_index{|line, line_index| line_index == job.line_number - 1 }.join
|
133
|
+
write_crontab(new_crontab)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def write_crontab(new_crontab, user = @user)
|
138
|
+
command = ["crontab"]
|
139
|
+
command << "-u #{user}" if user
|
140
|
+
command << "-"
|
141
|
+
command = command.join(" ")
|
142
|
+
|
143
|
+
# command = "(crontab -l ; echo \"#{job_spec}\") | crontab -" # add new job to bottom of crontab, per https://stackoverflow.com/questions/610839/how-can-i-programmatically-create-a-new-cron-job
|
144
|
+
# puts "writing new crontab:"
|
145
|
+
# puts new_crontab
|
146
|
+
# puts "*" * 80
|
147
|
+
|
148
|
+
# puts "write_crontab:"
|
149
|
+
# puts "command: #{command}"
|
150
|
+
# puts "new crontab:"
|
151
|
+
# puts new_crontab
|
152
|
+
|
153
|
+
stdout, stderr, exit_status = Open3.capture3(command, stdin_data: new_crontab)
|
154
|
+
crontab_output = stdout
|
155
|
+
error_output = stderr
|
156
|
+
|
157
|
+
load(user)
|
158
|
+
|
159
|
+
raise("Unable to write to crontab: #{error_output}") unless exit_status.success?
|
160
|
+
end
|
161
|
+
|
162
|
+
def to_s
|
163
|
+
s = StringIO.new
|
164
|
+
if !@environment_vars.empty?
|
165
|
+
s.puts "Environment"
|
166
|
+
s.puts @environment_vars.join("\n")
|
167
|
+
end
|
168
|
+
if !@jobs.empty?
|
169
|
+
s.puts "Jobs"
|
170
|
+
@jobs.each_with_index do |job, i|
|
171
|
+
s.puts "#{i + 1}. #{job}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
s.string
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
class Job
|
180
|
+
def self.every(duration_quantity, duration_unit_of_time, command)
|
181
|
+
now = DateTime.now
|
182
|
+
next_moment = now + 1.minute
|
183
|
+
minute = next_moment.minute
|
184
|
+
hour = next_moment.hour
|
185
|
+
day = next_moment.day
|
186
|
+
schedule_spec = case duration_unit_of_time
|
187
|
+
when "m"
|
188
|
+
"*/#{duration_quantity} * * * *"
|
189
|
+
when "h"
|
190
|
+
"#{minute} */#{duration_quantity} * * *"
|
191
|
+
when "d"
|
192
|
+
"#{minute} #{hour} */#{duration_quantity} * *"
|
193
|
+
when "M"
|
194
|
+
"#{minute} #{hour} #{day} */#{duration_quantity} *"
|
195
|
+
end
|
196
|
+
self.new(schedule_spec, command)
|
197
|
+
end
|
198
|
+
|
199
|
+
attr_reader :schedule_spec
|
200
|
+
attr_reader :line_number
|
201
|
+
|
202
|
+
# schedule_spec is a cron specification string: minutes hours day-of-month month day-of-week
|
203
|
+
# see https://pkg.go.dev/github.com/robfig/cron?utm_source=godoc#hdr-CRON_Expression_Format
|
204
|
+
# see https://github.com/mileusna/crontab
|
205
|
+
# see https://github.com/josiahcarlson/parse-crontab
|
206
|
+
|
207
|
+
# line_numbrer is the 1-based index into the crontab file that this job was found at
|
208
|
+
# pos_offset is the 0-based index into the crontab file that this job was found at
|
209
|
+
def initialize(schedule_spec, command, line_number = nil, pos_offset = nil)
|
210
|
+
@schedule_spec = schedule_spec
|
211
|
+
@command = command
|
212
|
+
@pos_offset = pos_offset
|
213
|
+
@line_number = line_number
|
214
|
+
end
|
215
|
+
|
216
|
+
def specification
|
217
|
+
"#{@schedule_spec} #{escaped_command}"
|
218
|
+
end
|
219
|
+
|
220
|
+
def escaped_command
|
221
|
+
Shellwords.escape(@command).gsub("%", "\%") # % is a special character in cron job specifications; see https://serverfault.com/questions/274475/escaping-double-quotes-and-percent-signs-in-cron
|
222
|
+
end
|
223
|
+
|
224
|
+
def to_s
|
225
|
+
schedule = Cronex::ExpressionDescriptor.new(@schedule_spec).description
|
226
|
+
# str = "#{@command} #{schedule} (line #{@line_number}, pos #{@pos_offset})"
|
227
|
+
str = "#{@command} #{schedule}"
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
grammar Crontab
|
2
|
+
rule user_crontab
|
3
|
+
sep?
|
4
|
+
environment_spec:(environment sep)?
|
5
|
+
jobspecs?
|
6
|
+
sep?
|
7
|
+
end
|
8
|
+
|
9
|
+
rule environment
|
10
|
+
head:directive tail:(sep directive)*
|
11
|
+
end
|
12
|
+
|
13
|
+
rule directive
|
14
|
+
var space "=" space expr
|
15
|
+
end
|
16
|
+
|
17
|
+
rule var
|
18
|
+
alpha alphanumeric?
|
19
|
+
end
|
20
|
+
|
21
|
+
rule expr
|
22
|
+
command
|
23
|
+
end
|
24
|
+
|
25
|
+
rule jobspecs
|
26
|
+
head:jobspec tail:(sep jobspec)*
|
27
|
+
end
|
28
|
+
|
29
|
+
rule jobspec
|
30
|
+
schedule_spec space command ws*
|
31
|
+
end
|
32
|
+
|
33
|
+
rule schedule_spec
|
34
|
+
standard / special
|
35
|
+
end
|
36
|
+
|
37
|
+
rule standard
|
38
|
+
minute space hour space dayofmonth space month space dayofweek
|
39
|
+
end
|
40
|
+
|
41
|
+
rule special
|
42
|
+
"@" ("yearly" / "annually" / "monthly" / "weekly" / "daily" / "hourly" / "reboot")
|
43
|
+
end
|
44
|
+
|
45
|
+
rule minute
|
46
|
+
step
|
47
|
+
end
|
48
|
+
|
49
|
+
rule hour
|
50
|
+
step
|
51
|
+
end
|
52
|
+
|
53
|
+
rule dayofmonthtypes
|
54
|
+
step
|
55
|
+
end
|
56
|
+
|
57
|
+
rule monthtypes
|
58
|
+
step / altmonths
|
59
|
+
end
|
60
|
+
|
61
|
+
rule dayofweektypes
|
62
|
+
step / altdays
|
63
|
+
end
|
64
|
+
|
65
|
+
rule dayofmonth
|
66
|
+
dayofmonthtypes ("," dayofmonthtypes)*
|
67
|
+
end
|
68
|
+
|
69
|
+
rule month
|
70
|
+
monthtypes ("," monthtypes)*
|
71
|
+
end
|
72
|
+
|
73
|
+
rule dayofweek
|
74
|
+
dayofweektypes ("," dayofweektypes)*
|
75
|
+
end
|
76
|
+
|
77
|
+
rule command
|
78
|
+
quoted_string
|
79
|
+
/ (!("\n" / comment) .)+
|
80
|
+
end
|
81
|
+
|
82
|
+
rule step
|
83
|
+
common ("/" int)?
|
84
|
+
end
|
85
|
+
|
86
|
+
rule common
|
87
|
+
range / int / any
|
88
|
+
end
|
89
|
+
|
90
|
+
rule range
|
91
|
+
int "-" int
|
92
|
+
end
|
93
|
+
|
94
|
+
rule altdays
|
95
|
+
days ("," days)*
|
96
|
+
end
|
97
|
+
|
98
|
+
rule altmonths
|
99
|
+
months ("," months)*
|
100
|
+
end
|
101
|
+
|
102
|
+
rule days
|
103
|
+
"MON" / "TUE" / "WED" / "THU" / "FRI" / "SAT" / "SUN"
|
104
|
+
end
|
105
|
+
|
106
|
+
rule months
|
107
|
+
"JAN" / "FEB" / "MAR" / "APR" / "MAY" / "JUN" / "JUL" / "AUG" / "SEP" / "OCT" / "NOV" / "DEC"
|
108
|
+
end
|
109
|
+
|
110
|
+
rule any
|
111
|
+
"*"
|
112
|
+
end
|
113
|
+
|
114
|
+
rule int
|
115
|
+
[0-9]+
|
116
|
+
end
|
117
|
+
|
118
|
+
rule alpha
|
119
|
+
[a-zA-Z]+
|
120
|
+
end
|
121
|
+
|
122
|
+
rule alphanumeric
|
123
|
+
[a-zA-Z0-9]+
|
124
|
+
end
|
125
|
+
|
126
|
+
rule sep
|
127
|
+
(ws* nl)+
|
128
|
+
end
|
129
|
+
|
130
|
+
rule ws
|
131
|
+
space / comment
|
132
|
+
end
|
133
|
+
|
134
|
+
rule space
|
135
|
+
[ \t]+
|
136
|
+
end
|
137
|
+
|
138
|
+
rule nl
|
139
|
+
[\n]+
|
140
|
+
end
|
141
|
+
|
142
|
+
rule comment
|
143
|
+
"#" (!"\n" .)*
|
144
|
+
end
|
145
|
+
|
146
|
+
rule quoted_string
|
147
|
+
"\"" ( (!("\"" / "\\") .) / escape)* "\""
|
148
|
+
/ "'" ( (!("'" / "\\") .) / escape)* "'"
|
149
|
+
end
|
150
|
+
|
151
|
+
rule escape
|
152
|
+
"\\" escape_sequence
|
153
|
+
end
|
154
|
+
|
155
|
+
rule escape_sequence
|
156
|
+
"'"
|
157
|
+
/ "\""
|
158
|
+
/ "\\"
|
159
|
+
/ "b"
|
160
|
+
/ "f"
|
161
|
+
/ "n"
|
162
|
+
/ "r"
|
163
|
+
/ "t"
|
164
|
+
/ "v"
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require "citrus"
|
2
|
+
|
3
|
+
module Citrus
|
4
|
+
class Input < StringScanner
|
5
|
+
def line_index(pos = pos())
|
6
|
+
p = n = 0
|
7
|
+
string.each_line do |line|
|
8
|
+
next_p = p + line.length
|
9
|
+
return n if p <= pos && pos < next_p
|
10
|
+
p = next_p
|
11
|
+
n += 1
|
12
|
+
end
|
13
|
+
0
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Citrus.require("schedule/cron")
|
19
|
+
|
20
|
+
# require "polyglot"
|
21
|
+
# require "treetop"
|
22
|
+
# require "schedule/cron.treetop"
|
23
|
+
|
24
|
+
module Schedule
|
25
|
+
module Cron
|
26
|
+
Grammar = ScheduleCronParser
|
27
|
+
|
28
|
+
class TableParser
|
29
|
+
def self.valid?(user_crontab_string)
|
30
|
+
!!parse(user_crontab_string)
|
31
|
+
rescue Citrus::ParseError => e
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.parse(user_crontab_string)
|
36
|
+
Grammar.parse(user_crontab_string, root: :user_crontab)
|
37
|
+
# ::CrontabParser.new.parse(user_crontab_string, root: :user_crontab)
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(user_crontab_string)
|
41
|
+
@user_crontab = TableParser.parse(user_crontab_string) rescue nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def valid?
|
45
|
+
!!@user_crontab
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_line_index(match)
|
49
|
+
@user_crontab.input.line_index(match.offset)
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_line_number(match)
|
53
|
+
@user_crontab.input.line_number(match.offset)
|
54
|
+
end
|
55
|
+
|
56
|
+
def environment_vars
|
57
|
+
@user_crontab&.capture(:environment)&.captures(:directive)
|
58
|
+
# puts @user_crontab.methods.sort.inspect
|
59
|
+
# puts @user_crontab.inspect
|
60
|
+
# puts "*" * 80
|
61
|
+
# puts @user_crontab.environment_spec.inspect
|
62
|
+
# exit()
|
63
|
+
# @user_crontab&.environment&.directive
|
64
|
+
end
|
65
|
+
|
66
|
+
def job_specs
|
67
|
+
@user_crontab&.capture(:jobspecs)&.captures(:jobspec)
|
68
|
+
# @user_crontab&.jobspecs&.jobspec
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class LineParser
|
73
|
+
def self.valid?(cron_line)
|
74
|
+
!!parse(cron_line)
|
75
|
+
rescue Citrus::ParseError => e
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.parse(cron_line)
|
80
|
+
Grammar.parse(cron_line, root: :jobspec, consume: false)
|
81
|
+
# ::CrontabParser.new.parse(cron_line, root: :jobspec, consume_all_input: false)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/schedule_job.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "schedule_job"
|
3
|
+
spec.version = "1.0.0"
|
4
|
+
spec.authors = ["David Ellis"]
|
5
|
+
spec.email = ["david@conquerthelawn.com"]
|
6
|
+
|
7
|
+
spec.summary = "job scheduler"
|
8
|
+
spec.description = "job scheduler"
|
9
|
+
spec.homepage = "https://github.com/davidkellis/scheduler"
|
10
|
+
spec.license = "MIT"
|
11
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
12
|
+
|
13
|
+
spec.files = Dir["**/**"].
|
14
|
+
grep_v(/.gem$/).
|
15
|
+
grep_v(%r{\A(?:test|spec|features)/})
|
16
|
+
spec.bindir = "bin"
|
17
|
+
spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "rspec"
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: schedule_job
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Ellis
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-11-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: job scheduler
|
56
|
+
email:
|
57
|
+
- david@conquerthelawn.com
|
58
|
+
executables:
|
59
|
+
- schedule
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- Gemfile
|
64
|
+
- Gemfile.lock
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- bin/schedule
|
69
|
+
- lib/schedule/cli.rb
|
70
|
+
- lib/schedule/cron.citrus
|
71
|
+
- lib/schedule/cron.rb
|
72
|
+
- lib/schedule/cron.treetop
|
73
|
+
- lib/schedule/cron_parser.rb
|
74
|
+
- lib/schedule_job.rb
|
75
|
+
- schedule_job.gemspec
|
76
|
+
homepage: https://github.com/davidkellis/scheduler
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata: {}
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 2.7.0
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubygems_version: 3.2.3
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: job scheduler
|
99
|
+
test_files: []
|