tab_keeper 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +38 -0
- data/bin/console +6 -0
- data/lib/tab_keeper/daily.rb +30 -0
- data/lib/tab_keeper/generator.rb +15 -0
- data/lib/tab_keeper/hourly.rb +45 -0
- data/lib/tab_keeper/job_list.rb +41 -0
- data/lib/tab_keeper/log_redirection.rb +96 -0
- data/lib/tab_keeper/login_shell.rb +24 -0
- data/lib/tab_keeper/minutely.rb +25 -0
- data/lib/tab_keeper/monthly.rb +36 -0
- data/lib/tab_keeper/rails_runner.rb +25 -0
- data/lib/tab_keeper/version.rb +3 -0
- data/lib/tab_keeper/weekly.rb +41 -0
- data/lib/tab_keeper.rb +18 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/tab_keeper/daily_spec.rb +39 -0
- data/spec/tab_keeper/generator_spec.rb +34 -0
- data/spec/tab_keeper/hourly_spec.rb +45 -0
- data/spec/tab_keeper/job_list_spec.rb +51 -0
- data/spec/tab_keeper/log_redirection_spec.rb +95 -0
- data/spec/tab_keeper/login_shell_spec.rb +25 -0
- data/spec/tab_keeper/minutely_spec.rb +18 -0
- data/spec/tab_keeper/monthly_spec.rb +51 -0
- data/spec/tab_keeper/rails_runner_spec.rb +23 -0
- data/spec/tab_keeper/weekly_spec.rb +51 -0
- data/tab_keeper.gemspec +25 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 29235a8d42173d34b28beb02ec01bb82285aa12c
|
4
|
+
data.tar.gz: c3978f99e2fcc255ff2d7f68590e46e162a6c498
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 59272e872de28887507c2ec2a7712695d2a442e0d788c96a05327eadb5d8f1aff727e41c4c4ed9179780cea7fcf2507aa41fdf303a7276e3327a0c0c3d7ad415
|
7
|
+
data.tar.gz: f92e98d7503c162a0679a145c38e1d39e566137d4b7beb43bf7b164d6522f97053002c6ee6dbffc98a12f267690626b11930bf8ba656f476c4dbadf04ef46338
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Isaac Seymour
|
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,38 @@
|
|
1
|
+
# TabKeeper
|
2
|
+
|
3
|
+
Keep tabs on your crontab.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'tab_keeper'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install tab_keeper
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO
|
24
|
+
|
25
|
+
## Development
|
26
|
+
|
27
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test`
|
28
|
+
to run the tests. You can also run `bin/console` for an interactive prompt that will allow
|
29
|
+
you to experiment.
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/isaacseymour/tab_keeper.
|
34
|
+
|
35
|
+
## License
|
36
|
+
|
37
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
38
|
+
|
data/bin/console
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class Daily
|
3
|
+
HOUR_VALUES = (0..23)
|
4
|
+
MINUTE_VALUES = (0..59)
|
5
|
+
|
6
|
+
def initialize(hour: nil, min: 0)
|
7
|
+
@hour = hour
|
8
|
+
@min = min
|
9
|
+
validate!
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"#{min} #{hour} * * *"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :hour, :min
|
19
|
+
|
20
|
+
def validate!
|
21
|
+
unless HOUR_VALUES.include?(hour)
|
22
|
+
raise ArgumentError, "hour must be between 0 and 23"
|
23
|
+
end
|
24
|
+
|
25
|
+
unless MINUTE_VALUES.include?(min)
|
26
|
+
raise ArgumentError, "min must be between 0 and 59"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class Generator
|
3
|
+
def initialize(*pipeline)
|
4
|
+
@pipeline = pipeline.flatten
|
5
|
+
end
|
6
|
+
|
7
|
+
def generate(job_list, **options)
|
8
|
+
job_list.map do |job, timing|
|
9
|
+
timing + " " + @pipeline.reduce(nil) do |previous, pipe|
|
10
|
+
pipe.new(previous, job: job, timing: timing, **options).to_s
|
11
|
+
end
|
12
|
+
end.join("\n\n") + "\n"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class Hourly
|
3
|
+
MINUTE_VALUES = (0..59)
|
4
|
+
# Other values don't divide 24, so result in weird behaviour
|
5
|
+
EVERY_VALUES = [nil, 2, 3, 4, 6, 8, 12]
|
6
|
+
HOUR_VALUES = (0..23)
|
7
|
+
|
8
|
+
def initialize(min: 0, every: nil, only: nil)
|
9
|
+
@min = min
|
10
|
+
@every = every
|
11
|
+
@only = only
|
12
|
+
verify!
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
return "#{min} */#{every} * * *" if every
|
17
|
+
return "#{min} #{only.join(',')} * * *" if only
|
18
|
+
"#{min} * * * *"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :min, :every, :only
|
24
|
+
|
25
|
+
def verify!
|
26
|
+
unless MINUTE_VALUES.include?(min)
|
27
|
+
raise ArgumentError, "min must be between 0 and 59"
|
28
|
+
end
|
29
|
+
|
30
|
+
unless EVERY_VALUES.include?(every)
|
31
|
+
raise ArgumentError, "every must be one of #{EVERY_VALUES.join(', ')}"
|
32
|
+
end
|
33
|
+
|
34
|
+
return if only.nil?
|
35
|
+
raise ArgumentError, "every and only don't make sense together!" if every
|
36
|
+
|
37
|
+
raise ArgumentError, "only must be an array" unless only.is_a?(Array)
|
38
|
+
|
39
|
+
raise ArgumentError, "use Daily instead of a single `only` list" if only.one?
|
40
|
+
|
41
|
+
return if only.all? { |hour| HOUR_VALUES.include?(hour) }
|
42
|
+
raise ArgumentError, "only must be nil, or a list of hours"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class JobList
|
3
|
+
def initialize
|
4
|
+
@jobs = []
|
5
|
+
yield self if block_given?
|
6
|
+
end
|
7
|
+
|
8
|
+
def generate(generator, **options)
|
9
|
+
generator.generate(@jobs, **options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_a
|
13
|
+
@jobs
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(job, timer)
|
17
|
+
# TODO: validate `timer.to_s`
|
18
|
+
@jobs << [job, timer.to_s]
|
19
|
+
end
|
20
|
+
|
21
|
+
def daily(job, **options)
|
22
|
+
add(job, Daily.new(**options))
|
23
|
+
end
|
24
|
+
|
25
|
+
def hourly(job, **options)
|
26
|
+
add(job, Hourly.new(**options))
|
27
|
+
end
|
28
|
+
|
29
|
+
def minutely(job, **options)
|
30
|
+
add(job, Minutely.new(**options))
|
31
|
+
end
|
32
|
+
|
33
|
+
def monthly(job, **options)
|
34
|
+
add(job, Monthly.new(**options))
|
35
|
+
end
|
36
|
+
|
37
|
+
def weekly(job, **options)
|
38
|
+
add(job, Weekly.new(**options))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class LogRedirection
|
3
|
+
def initialize(previous, job: nil,
|
4
|
+
job_name_proc: ->(x) { x },
|
5
|
+
timing: nil,
|
6
|
+
log_directory: nil,
|
7
|
+
error_suffix: nil,
|
8
|
+
include_date_in_log_name: false,
|
9
|
+
**_options)
|
10
|
+
if previous.nil?
|
11
|
+
raise ArgumentError, "#{self.class.name} must not be first in the cron pipeline!"
|
12
|
+
end
|
13
|
+
|
14
|
+
if log_directory.nil?
|
15
|
+
raise ArgumentError, "log_directory must be set for #{self.class.name}"
|
16
|
+
end
|
17
|
+
|
18
|
+
@previous = previous
|
19
|
+
@job = job
|
20
|
+
# Normalise the job name: if it's an array take the first thing, then get the last
|
21
|
+
# thing after a "::" (if it's a class name), or a "/" (if it's a script, and removes
|
22
|
+
# the need to escape yay)
|
23
|
+
job_name = Array(@job).first.to_s.split(/((::)|\/)/).last
|
24
|
+
@job_name = job_name_proc.call(job_name)
|
25
|
+
@timing = timing
|
26
|
+
@log_directory = log_directory
|
27
|
+
@error_suffix = error_suffix
|
28
|
+
@include_date_in_log_name = include_date_in_log_name
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"#{@previous} " \
|
33
|
+
">> #{@log_directory}/#{job_name}#{date_part}.log " \
|
34
|
+
"2>#{error_part}"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :job_name
|
40
|
+
|
41
|
+
def error_part
|
42
|
+
return "&1" unless @error_suffix
|
43
|
+
"> #{@log_directory}/#{job_name}#{date_part}.#{@error_suffix}.log"
|
44
|
+
end
|
45
|
+
|
46
|
+
def date_part
|
47
|
+
return unless include_date?
|
48
|
+
"_`date +#{date_format}`"
|
49
|
+
end
|
50
|
+
|
51
|
+
def include_date?
|
52
|
+
@include_date_in_log_name
|
53
|
+
end
|
54
|
+
|
55
|
+
def date_format
|
56
|
+
[
|
57
|
+
'%Y',
|
58
|
+
'%m',
|
59
|
+
day_component,
|
60
|
+
hour_component,
|
61
|
+
min_component,
|
62
|
+
second_component
|
63
|
+
].compact.join('-').chars.map { |char| char == '%' ? '\%' : char }.join
|
64
|
+
end
|
65
|
+
|
66
|
+
def day_component
|
67
|
+
'%d' if more_than_once_a_month?
|
68
|
+
end
|
69
|
+
|
70
|
+
def hour_component
|
71
|
+
'%H' if more_than_once_a_day?
|
72
|
+
end
|
73
|
+
|
74
|
+
def min_component
|
75
|
+
'%m' if more_than_once_a_day?
|
76
|
+
end
|
77
|
+
|
78
|
+
def second_component
|
79
|
+
'%s' if more_than_once_an_hour?
|
80
|
+
end
|
81
|
+
|
82
|
+
MULTIPLE_CHARACTERS = "*-/,".chars.map(&:freeze).freeze
|
83
|
+
|
84
|
+
def more_than_once_a_month?
|
85
|
+
MULTIPLE_CHARACTERS.any? { |char| @timing.split(' ')[2].include?(char) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def more_than_once_a_day?
|
89
|
+
MULTIPLE_CHARACTERS.any? { |char| @timing.split(' ').first(2).join.include?(char) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def more_than_once_an_hour?
|
93
|
+
MULTIPLE_CHARACTERS.any? { |char| @timing.split(' ').first.include?(char) }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class LoginShell
|
3
|
+
def initialize(previous, job: nil, code_directory: nil, **_options)
|
4
|
+
@command = previous || job
|
5
|
+
@code_directory = code_directory
|
6
|
+
return if code_directory
|
7
|
+
raise ArgumentError, "code `directory` must be configured for LoginShell"
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
"/bin/bash -l -c 'cd #{escaped_path} && #{escaped_command}'"
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def escaped_path
|
17
|
+
@code_directory.chars.map { |char| char == "'" ? "'\\''" : char }.join
|
18
|
+
end
|
19
|
+
|
20
|
+
def escaped_command
|
21
|
+
@command.chars.map { |char| char == "'" ? "'\\''" : char }.join
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class Minutely
|
3
|
+
# Other values would lead to the non-evenly spaced runs, which is non-obvious
|
4
|
+
EVERY_VALUES = [2, 3, 4, 5, 6, 10, 12, 15, 20, 30]
|
5
|
+
|
6
|
+
def initialize(every: nil)
|
7
|
+
@every = every
|
8
|
+
verify!
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
return "* * * * *" unless every
|
13
|
+
"*/#{every} * * * *"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :every
|
19
|
+
|
20
|
+
def verify!
|
21
|
+
return if every.nil? || EVERY_VALUES.include?(every)
|
22
|
+
raise ArgumentError, "every must be nil, or one of #{EVERY_VALUES.join(', ')}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class Monthly
|
3
|
+
MINUTE_VALUES = (0..59)
|
4
|
+
HOUR_VALUES = (0..23)
|
5
|
+
# Cron supports 29-31 too, but that's a recipe for sadness
|
6
|
+
DAY_VALUES = (0..28)
|
7
|
+
|
8
|
+
def initialize(day: nil, hour: nil, min: 0)
|
9
|
+
@day = day
|
10
|
+
@hour = hour
|
11
|
+
@min = min
|
12
|
+
verify!
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"#{min} #{hour} #{day} * *"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :day, :hour, :min
|
22
|
+
|
23
|
+
def verify!
|
24
|
+
unless MINUTE_VALUES.include?(min)
|
25
|
+
raise ArgumentError, "min must be between 0 and 59"
|
26
|
+
end
|
27
|
+
|
28
|
+
unless HOUR_VALUES.include?(hour)
|
29
|
+
raise ArgumentError, "hour must be between 0 and 23"
|
30
|
+
end
|
31
|
+
|
32
|
+
return if DAY_VALUES.include?(day)
|
33
|
+
raise ArgumentError, "day must be between 0 and 28"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class RailsRunner
|
3
|
+
def initialize(previous, job: nil, rails_env: nil, **_options)
|
4
|
+
@to_run = previous || job
|
5
|
+
@rails_env = rails_env
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
["bin/rails runner", env_part, "'#{escaped_previous}'"].compact.join(" ")
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
attr_reader :rails_env, :to_run
|
15
|
+
|
16
|
+
def env_part
|
17
|
+
return unless @rails_env
|
18
|
+
"-e #{rails_env}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def escaped_previous
|
22
|
+
to_run.chars.map { |char| char == "'" ? "'\\''" : char }.join
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module TabKeeper
|
2
|
+
class Weekly
|
3
|
+
MINUTE_VALUES = (0..59)
|
4
|
+
HOUR_VALUES = (0..23)
|
5
|
+
# Order matters here - it must match the ordering used by cron (i.e. 0 = Sunday)
|
6
|
+
DAY_VALUES = %i(sunday monday tuesday wednesday thursday friday saturday)
|
7
|
+
|
8
|
+
def initialize(day: nil, hour: nil, min: 0)
|
9
|
+
@day = day
|
10
|
+
@hour = hour
|
11
|
+
@min = min
|
12
|
+
verify!
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"#{min} #{hour} * * #{day_index}"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :day, :hour, :min
|
22
|
+
|
23
|
+
def day_index
|
24
|
+
return day if day.is_a?(Fixnum)
|
25
|
+
DAY_VALUES.index(day)
|
26
|
+
end
|
27
|
+
|
28
|
+
def verify!
|
29
|
+
unless MINUTE_VALUES.include?(min)
|
30
|
+
raise ArgumentError, "min must be between 0 and 59"
|
31
|
+
end
|
32
|
+
|
33
|
+
unless HOUR_VALUES.include?(hour)
|
34
|
+
raise ArgumentError, "hour must be between 0 and 23"
|
35
|
+
end
|
36
|
+
|
37
|
+
return if DAY_VALUES.include?(day) || (day.is_a?(Fixnum) && DAY_VALUES[day])
|
38
|
+
raise ArgumentError, "day must be between 0 and 6, or a symbol day name"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/tab_keeper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "tab_keeper/version"
|
2
|
+
|
3
|
+
module TabKeeper
|
4
|
+
end
|
5
|
+
|
6
|
+
require "tab_keeper/daily"
|
7
|
+
require "tab_keeper/hourly"
|
8
|
+
require "tab_keeper/minutely"
|
9
|
+
require "tab_keeper/monthly"
|
10
|
+
require "tab_keeper/weekly"
|
11
|
+
|
12
|
+
require "tab_keeper/job_list"
|
13
|
+
|
14
|
+
require "tab_keeper/log_redirection"
|
15
|
+
require "tab_keeper/login_shell"
|
16
|
+
require "tab_keeper/rails_runner"
|
17
|
+
|
18
|
+
require "tab_keeper/generator"
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
lib_path = File.expand_path('../../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
3
|
+
require "tab_keeper"
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.expect_with :rspec do |expectations|
|
7
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
8
|
+
end
|
9
|
+
|
10
|
+
config.mock_with :rspec do |mocks|
|
11
|
+
mocks.verify_partial_doubles = true
|
12
|
+
end
|
13
|
+
|
14
|
+
config.example_status_persistence_file_path = ".rspec_log"
|
15
|
+
|
16
|
+
config.disable_monkey_patching!
|
17
|
+
|
18
|
+
config.warnings = true
|
19
|
+
|
20
|
+
config.order = :random
|
21
|
+
|
22
|
+
Kernel.srand config.seed
|
23
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
RSpec.describe TabKeeper::Daily do
|
2
|
+
subject { described_class.new(hour: hour, min: min).to_s }
|
3
|
+
|
4
|
+
context "midnight" do
|
5
|
+
let(:hour) { 0 }
|
6
|
+
let(:min) { 0 }
|
7
|
+
it { is_expected.to eq("0 0 * * *") }
|
8
|
+
end
|
9
|
+
|
10
|
+
context "7:30am" do
|
11
|
+
let(:hour) { 7 }
|
12
|
+
let(:min) { 30 }
|
13
|
+
it { is_expected.to eq("30 7 * * *") }
|
14
|
+
end
|
15
|
+
|
16
|
+
context "without min supplied" do
|
17
|
+
let(:hour) { 12 }
|
18
|
+
let(:min) { nil }
|
19
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context "without hour supplied" do
|
23
|
+
let(:hour) { nil }
|
24
|
+
let(:min) { 2 }
|
25
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
26
|
+
end
|
27
|
+
|
28
|
+
context "with a weird hour" do
|
29
|
+
let(:hour) { 24 }
|
30
|
+
let(:min) { 0 }
|
31
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
32
|
+
end
|
33
|
+
|
34
|
+
context "with a weird minute" do
|
35
|
+
let(:hour) { 12 }
|
36
|
+
let(:min) { -1 }
|
37
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
RSpec.describe TabKeeper::Generator do
|
2
|
+
subject { described_class.new(*pipeline).generate(job_list, **options) }
|
3
|
+
let(:job_list) do
|
4
|
+
TabKeeper::JobList.new do |tab|
|
5
|
+
tab.minutely("FrequentJob.run", every: 5)
|
6
|
+
tab.daily("MidnightJob.run", hour: 0)
|
7
|
+
tab.daily("LunchtimeJob.run", hour: 12, min: 30)
|
8
|
+
tab.weekly("HappyMondayJob.run", day: :monday, hour: 7, min: 30)
|
9
|
+
tab.monthly("PaydayJob.run", day: 25, hour: 16, min: 15)
|
10
|
+
end.to_a
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:pipeline) { [TabKeeper::RailsRunner, TabKeeper::LoginShell] }
|
14
|
+
let(:options) do
|
15
|
+
{
|
16
|
+
code_directory: '/path/to/code',
|
17
|
+
rails_env: :production
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
it do
|
22
|
+
is_expected.to eq <<-CRONTAB
|
23
|
+
*/5 * * * * /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''FrequentJob.run'\\'''
|
24
|
+
|
25
|
+
0 0 * * * /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''MidnightJob.run'\\'''
|
26
|
+
|
27
|
+
30 12 * * * /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''LunchtimeJob.run'\\'''
|
28
|
+
|
29
|
+
30 7 * * 1 /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''HappyMondayJob.run'\\'''
|
30
|
+
|
31
|
+
15 16 25 * * /bin/bash -l -c 'cd /path/to/code && bin/rails runner -e production '\\''PaydayJob.run'\\'''
|
32
|
+
CRONTAB
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
RSpec.describe TabKeeper::Hourly do
|
2
|
+
subject { described_class.new(every: every, only: only, min: min).to_s }
|
3
|
+
let(:every) { nil }
|
4
|
+
let(:only) { nil }
|
5
|
+
|
6
|
+
context "every hour" do
|
7
|
+
let(:min) { 0 }
|
8
|
+
it { is_expected.to eq("0 * * * *") }
|
9
|
+
end
|
10
|
+
|
11
|
+
context "every half-past" do
|
12
|
+
let(:min) { 30 }
|
13
|
+
it { is_expected.to eq("30 * * * *") }
|
14
|
+
end
|
15
|
+
|
16
|
+
context "every 3rd hour" do
|
17
|
+
let(:min) { 0 }
|
18
|
+
let(:every) { 3 }
|
19
|
+
it { is_expected.to eq("0 */3 * * *") }
|
20
|
+
end
|
21
|
+
|
22
|
+
context "on specified hours" do
|
23
|
+
let(:min) { 15 }
|
24
|
+
let(:only) { [3, 9, 17] }
|
25
|
+
it { is_expected.to eq("15 3,9,17 * * *") }
|
26
|
+
end
|
27
|
+
|
28
|
+
context "without the minute" do
|
29
|
+
let(:min) { nil }
|
30
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
31
|
+
end
|
32
|
+
|
33
|
+
context "with every and only specified" do
|
34
|
+
let(:min) { 0 }
|
35
|
+
let(:every) { 3 }
|
36
|
+
let(:only) { [1, 2, 3] }
|
37
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
38
|
+
end
|
39
|
+
|
40
|
+
context "with only as a single hour" do
|
41
|
+
let(:only) { 12 }
|
42
|
+
let(:min) { 0 }
|
43
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
RSpec.describe TabKeeper::JobList do
|
2
|
+
subject(:instance) { described_class.new }
|
3
|
+
|
4
|
+
it "yields itself" do
|
5
|
+
yielded = nil
|
6
|
+
returned = described_class.new { |tab| yielded = tab }
|
7
|
+
expect(yielded).to be(returned)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#add" do
|
11
|
+
pending "rejects invalid timers" do
|
12
|
+
expect { instance.add("thing", "* *-2 50 * -1") }.to raise_error(ArgumentError)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#daily" do
|
17
|
+
it "passes options through to Daily.new" do
|
18
|
+
expect(TabKeeper::Daily).to receive(:new).with(hour: 5).and_call_original
|
19
|
+
instance.daily("thing", hour: 5)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#hourly" do
|
24
|
+
it "passes options through to Hourly.new" do
|
25
|
+
expect(TabKeeper::Hourly).to receive(:new).with(min: 5).and_call_original
|
26
|
+
instance.hourly("thing", min: 5)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#minutely" do
|
31
|
+
it "passes options through to Minutely.new" do
|
32
|
+
expect(TabKeeper::Minutely).to receive(:new).and_call_original
|
33
|
+
instance.minutely("thing")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#monthly" do
|
38
|
+
it "passes options through to Monthly.new" do
|
39
|
+
expect(TabKeeper::Monthly).to receive(:new).with(day: 14, hour: 5).and_call_original
|
40
|
+
instance.monthly("thing", day: 14, hour: 5)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#weekly" do
|
45
|
+
it "passes options through to Weekly.new" do
|
46
|
+
expect(TabKeeper::Weekly).to receive(:new).with(day: :monday, hour: 5).
|
47
|
+
and_call_original
|
48
|
+
instance.weekly("thing", day: :monday, hour: 5)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
RSpec.describe TabKeeper::LogRedirection do
|
2
|
+
let(:instance) do
|
3
|
+
described_class.new("scripts/thing",
|
4
|
+
job: "scripts/thing",
|
5
|
+
job_name_proc: job_name_proc,
|
6
|
+
timing: timing,
|
7
|
+
log_directory: "/path/to/logs",
|
8
|
+
include_date_in_log_name: include_date_in_log_name,
|
9
|
+
error_suffix: error_suffix)
|
10
|
+
end
|
11
|
+
subject { instance.to_s }
|
12
|
+
let(:job_name_proc) { ->(job) { job } }
|
13
|
+
let(:error_suffix) { nil }
|
14
|
+
let(:include_date_in_log_name) { true }
|
15
|
+
|
16
|
+
context "without date in the log file name" do
|
17
|
+
let(:include_date_in_log_name) { false }
|
18
|
+
let(:timing) { "* * * * *" }
|
19
|
+
it { is_expected.to eq("scripts/thing >> /path/to/logs/thing.log 2>&1") }
|
20
|
+
end
|
21
|
+
|
22
|
+
context "with a separate error log" do
|
23
|
+
let(:include_date_in_log_name) { false }
|
24
|
+
let(:error_suffix) { "wah" }
|
25
|
+
let(:timing) { "* * * * *" }
|
26
|
+
it do
|
27
|
+
is_expected.to eq("scripts/thing >> /path/to/logs/thing.log " \
|
28
|
+
"2>> /path/to/logs/thing.wah.log")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "with minutely timing" do
|
33
|
+
let(:timing) { TabKeeper::Minutely.new(every: 5).to_s }
|
34
|
+
it do
|
35
|
+
is_expected.to eq(
|
36
|
+
"scripts/thing >> /path/to/logs/thing_" \
|
37
|
+
"`date +\\%Y-\\%m-\\%d-\\%H-\\%m-\\%s`.log 2>&1")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "with hourly timing" do
|
42
|
+
let(:timing) { TabKeeper::Hourly.new(every: 2).to_s }
|
43
|
+
it do
|
44
|
+
is_expected.to eq(
|
45
|
+
"scripts/thing >> /path/to/logs/thing_" \
|
46
|
+
"`date +\\%Y-\\%m-\\%d-\\%H-\\%m`.log 2>&1")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "with daily timing" do
|
51
|
+
let(:timing) { TabKeeper::Daily.new(hour: 14).to_s }
|
52
|
+
it do
|
53
|
+
is_expected.to eq(
|
54
|
+
"scripts/thing >> /path/to/logs/thing_" \
|
55
|
+
"`date +\\%Y-\\%m-\\%d`.log 2>&1")
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with a separate error file" do
|
59
|
+
let(:error_suffix) { "error" }
|
60
|
+
it do
|
61
|
+
is_expected.to eq(
|
62
|
+
"scripts/thing " \
|
63
|
+
">> /path/to/logs/thing_`date +\\%Y-\\%m-\\%d`.log " \
|
64
|
+
"2>> /path/to/logs/thing_`date +\\%Y-\\%m-\\%d`.error.log")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "with a fun job name proc" do
|
69
|
+
let(:job_name_proc) { -> (job) { "#{job}_sadness" } }
|
70
|
+
it do
|
71
|
+
is_expected.to eq(
|
72
|
+
"scripts/thing >> /path/to/logs/thing_sadness_" \
|
73
|
+
"`date +\\%Y-\\%m-\\%d`.log 2>&1")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "with weekly timing" do
|
79
|
+
let(:timing) { TabKeeper::Weekly.new(hour: 14, day: :tuesday).to_s }
|
80
|
+
it do
|
81
|
+
is_expected.to eq(
|
82
|
+
"scripts/thing >> /path/to/logs/thing_" \
|
83
|
+
"`date +\\%Y-\\%m-\\%d`.log 2>&1")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "with monthly timing" do
|
88
|
+
let(:timing) { TabKeeper::Monthly.new(day: 5, hour: 12).to_s }
|
89
|
+
it do
|
90
|
+
is_expected.to eq(
|
91
|
+
"scripts/thing >> /path/to/logs/thing_" \
|
92
|
+
"`date +\\%Y-\\%m`.log 2>&1")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
RSpec.describe TabKeeper::LoginShell do
|
2
|
+
subject { described_class.new(previous, job: job, code_directory: code_directory).to_s }
|
3
|
+
let(:job) { "bin/some_script" }
|
4
|
+
let(:code_directory) { "/path/to/code" }
|
5
|
+
let(:previous) { nil }
|
6
|
+
|
7
|
+
it { is_expected.to eq("/bin/bash -l -c 'cd /path/to/code && bin/some_script'") }
|
8
|
+
|
9
|
+
context "with a previous command" do
|
10
|
+
let(:previous) { "SOMEVAR=value bin/some_script" }
|
11
|
+
it do
|
12
|
+
is_expected.
|
13
|
+
to eq("/bin/bash -l -c 'cd /path/to/code && SOMEVAR=value bin/some_script'")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "when the job needs escaping" do
|
18
|
+
let(:job) { "bin/run 'MyJob.run('\\''arg'\\'')'" }
|
19
|
+
it do
|
20
|
+
is_expected.to eq(
|
21
|
+
"/bin/bash -l -c 'cd /path/to/code && " \
|
22
|
+
"bin/run '\\''MyJob.run('\\''\\'\\'''\\''arg'\\''\\'\\'''\\'')'\\'''")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
RSpec.describe TabKeeper::Minutely do
|
2
|
+
subject { described_class.new(every: every).to_s }
|
3
|
+
|
4
|
+
context "5 minutes" do
|
5
|
+
let(:every) { 5 }
|
6
|
+
it { is_expected.to eq("*/5 * * * *") }
|
7
|
+
end
|
8
|
+
|
9
|
+
context "every minute" do
|
10
|
+
let(:every) { nil }
|
11
|
+
it { is_expected.to eq("* * * * *") }
|
12
|
+
end
|
13
|
+
|
14
|
+
context "every 30 seconds" do
|
15
|
+
let(:every) { 0.5 }
|
16
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
RSpec.describe TabKeeper::Monthly do
|
2
|
+
subject { described_class.new(day: day, hour: hour, min: min).to_s }
|
3
|
+
let(:day) { 1 }
|
4
|
+
let(:hour) { 13 }
|
5
|
+
let(:min) { 0 }
|
6
|
+
|
7
|
+
context "on the first of the month" do
|
8
|
+
let(:day) { 1 }
|
9
|
+
it { is_expected.to eq("0 13 1 * *") }
|
10
|
+
end
|
11
|
+
|
12
|
+
context "on the 15th of the month" do
|
13
|
+
let(:day) { 15 }
|
14
|
+
it { is_expected.to eq("0 13 15 * *") }
|
15
|
+
end
|
16
|
+
|
17
|
+
context "without the minute" do
|
18
|
+
let(:min) { nil }
|
19
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context "without the hour" do
|
23
|
+
let(:hour) { nil }
|
24
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
25
|
+
end
|
26
|
+
|
27
|
+
context "without the day" do
|
28
|
+
let(:day) { nil }
|
29
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
30
|
+
end
|
31
|
+
|
32
|
+
context "with a weird minute" do
|
33
|
+
let(:min) { 60 }
|
34
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
35
|
+
end
|
36
|
+
|
37
|
+
context "with a weird hour" do
|
38
|
+
let(:hour) { -1 }
|
39
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with a weird day" do
|
43
|
+
let(:day) { 29 }
|
44
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with a symbol day" do
|
48
|
+
let(:day) { :thursday }
|
49
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
RSpec.describe TabKeeper::RailsRunner do
|
2
|
+
subject { described_class.new(previous, job: "MyJob.run", rails_env: rails_env).to_s }
|
3
|
+
|
4
|
+
let(:previous) { "MyJob.run" }
|
5
|
+
let(:rails_env) { :staging }
|
6
|
+
|
7
|
+
it { is_expected.to eq("bin/rails runner -e staging 'MyJob.run'") }
|
8
|
+
|
9
|
+
context "when there are 's to escape" do
|
10
|
+
let(:previous) { "MyJob.run('arg')" }
|
11
|
+
it { is_expected.to eq("bin/rails runner -e staging 'MyJob.run('\\''arg'\\'')'") }
|
12
|
+
end
|
13
|
+
|
14
|
+
context "when this is the first transformer in the pipeline" do
|
15
|
+
let(:previous) { nil }
|
16
|
+
it { is_expected.to eq("bin/rails runner -e staging 'MyJob.run'") }
|
17
|
+
end
|
18
|
+
|
19
|
+
context "without a rails_env set" do
|
20
|
+
let(:rails_env) { nil }
|
21
|
+
it { is_expected.to eq("bin/rails runner 'MyJob.run'") }
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
RSpec.describe TabKeeper::Weekly do
|
2
|
+
subject { described_class.new(day: day, hour: hour, min: min).to_s }
|
3
|
+
let(:day) { :wednesday }
|
4
|
+
let(:hour) { 13 }
|
5
|
+
let(:min) { 0 }
|
6
|
+
|
7
|
+
context "every Thursday" do
|
8
|
+
let(:day) { :thursday }
|
9
|
+
it { is_expected.to eq("0 13 * * 4") }
|
10
|
+
end
|
11
|
+
|
12
|
+
context "every 3rd day of the week" do
|
13
|
+
let(:day) { 3 }
|
14
|
+
it { is_expected.to eq("0 13 * * 3") }
|
15
|
+
end
|
16
|
+
|
17
|
+
context "without the minute" do
|
18
|
+
let(:min) { nil }
|
19
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context "without the hour" do
|
23
|
+
let(:hour) { nil }
|
24
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
25
|
+
end
|
26
|
+
|
27
|
+
context "without the day" do
|
28
|
+
let(:day) { nil }
|
29
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
30
|
+
end
|
31
|
+
|
32
|
+
context "with a weird minute" do
|
33
|
+
let(:min) { 60 }
|
34
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
35
|
+
end
|
36
|
+
|
37
|
+
context "with a weird hour" do
|
38
|
+
let(:hour) { -1 }
|
39
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with a weird day" do
|
43
|
+
let(:day) { 8 }
|
44
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with a string day" do
|
48
|
+
let(:day) { "thursday" }
|
49
|
+
specify { expect { subject }.to raise_error(ArgumentError) }
|
50
|
+
end
|
51
|
+
end
|
data/tab_keeper.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tab_keeper/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "tab_keeper"
|
8
|
+
spec.version = TabKeeper::VERSION
|
9
|
+
spec.authors = ["Isaac Seymour"]
|
10
|
+
spec.email = ["i.seymour@oxon.org"]
|
11
|
+
|
12
|
+
spec.summary = %q{Manage crons}
|
13
|
+
spec.homepage = "https://github.com/isaacseymour/tab_keeper"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
23
|
+
spec.add_development_dependency "rspec", "~> 3"
|
24
|
+
spec.add_development_dependency "pry", "~> 0"
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tab_keeper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Isaac Seymour
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-20 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: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
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:
|
56
|
+
email:
|
57
|
+
- i.seymour@oxon.org
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".travis.yml"
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
- bin/console
|
69
|
+
- lib/tab_keeper.rb
|
70
|
+
- lib/tab_keeper/daily.rb
|
71
|
+
- lib/tab_keeper/generator.rb
|
72
|
+
- lib/tab_keeper/hourly.rb
|
73
|
+
- lib/tab_keeper/job_list.rb
|
74
|
+
- lib/tab_keeper/log_redirection.rb
|
75
|
+
- lib/tab_keeper/login_shell.rb
|
76
|
+
- lib/tab_keeper/minutely.rb
|
77
|
+
- lib/tab_keeper/monthly.rb
|
78
|
+
- lib/tab_keeper/rails_runner.rb
|
79
|
+
- lib/tab_keeper/version.rb
|
80
|
+
- lib/tab_keeper/weekly.rb
|
81
|
+
- spec/spec_helper.rb
|
82
|
+
- spec/tab_keeper/daily_spec.rb
|
83
|
+
- spec/tab_keeper/generator_spec.rb
|
84
|
+
- spec/tab_keeper/hourly_spec.rb
|
85
|
+
- spec/tab_keeper/job_list_spec.rb
|
86
|
+
- spec/tab_keeper/log_redirection_spec.rb
|
87
|
+
- spec/tab_keeper/login_shell_spec.rb
|
88
|
+
- spec/tab_keeper/minutely_spec.rb
|
89
|
+
- spec/tab_keeper/monthly_spec.rb
|
90
|
+
- spec/tab_keeper/rails_runner_spec.rb
|
91
|
+
- spec/tab_keeper/weekly_spec.rb
|
92
|
+
- tab_keeper.gemspec
|
93
|
+
homepage: https://github.com/isaacseymour/tab_keeper
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata: {}
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 2.4.5.1
|
114
|
+
signing_key:
|
115
|
+
specification_version: 4
|
116
|
+
summary: Manage crons
|
117
|
+
test_files:
|
118
|
+
- spec/spec_helper.rb
|
119
|
+
- spec/tab_keeper/daily_spec.rb
|
120
|
+
- spec/tab_keeper/generator_spec.rb
|
121
|
+
- spec/tab_keeper/hourly_spec.rb
|
122
|
+
- spec/tab_keeper/job_list_spec.rb
|
123
|
+
- spec/tab_keeper/log_redirection_spec.rb
|
124
|
+
- spec/tab_keeper/login_shell_spec.rb
|
125
|
+
- spec/tab_keeper/minutely_spec.rb
|
126
|
+
- spec/tab_keeper/monthly_spec.rb
|
127
|
+
- spec/tab_keeper/rails_runner_spec.rb
|
128
|
+
- spec/tab_keeper/weekly_spec.rb
|