chrono_trigger 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ == 0.0.4 2009-04-09
2
+ * 1 enhancement
3
+ * Added chrono_trigger binary and supporting files
4
+
5
+ == 0.0.2 2009-04-02
6
+
7
+ * 1 minor enhancement:
8
+ * Updated to add tasks file
9
+
10
+ == 0.0.1 2009-04-02
11
+
12
+ * 1 major enhancement:
13
+ * Initial release
@@ -0,0 +1,24 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ Rakefile
6
+ bin/chrono_trigger
7
+ lib/chrono_trigger.rb
8
+ lib/chrono_trigger/cron_entry.rb
9
+ lib/chrono_trigger/process.rb
10
+ lib/chrono_trigger/runner.rb
11
+ lib/chrono_trigger/shell.rb
12
+ lib/chrono_trigger/tasks.rb
13
+ lib/chrono_trigger/trigger.rb
14
+ lib/triggers/test_triggers.rb
15
+ script/console
16
+ script/destroy
17
+ script/generate
18
+ tasks/chrono_trigger.rake
19
+ test/test_chrono_trigger.rb
20
+ test/test_cron_entry.rb
21
+ test/test_helper.rb
22
+ test/test_shell.rb
23
+ test/test_trigger.rb
24
+ test/triggers.rb
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,59 @@
1
+ = chrono_trigger
2
+
3
+ This is a branch of http://github.com/gregfitz23/chrono_trigger/tree/master as it hasn't been updated in sometime.
4
+ New code can be found at https://github.com/darful/chrono_trigger.
5
+
6
+ == DESCRIPTION:
7
+
8
+ A cron framework for defining cron tasks using a readable DSL.
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ == SYNOPSIS:
13
+
14
+ Create trigger files directory.
15
+ Triggers should follow the pattern:
16
+
17
+ trigger "name" do
18
+ runs { code to execute }
19
+ on :monday
20
+ every :minutes=>10
21
+ at :hour=>9, :minute=>[30,50]
22
+ end
23
+ Run `chrono_trigger -t{full path to trigger file}`.
24
+ Other available options are:
25
+ * -a - Specify an application context for the triggers to run against.
26
+ * -e - Specify the environment the triggers should run against
27
+
28
+ == REQUIREMENTS:
29
+
30
+ * ActiveSupport >= 2.0.2
31
+
32
+ == INSTALL:
33
+
34
+ * sudo gem install chrono_trigger
35
+
36
+ == LICENSE:
37
+
38
+ (The MIT License)
39
+
40
+ Copyright (c) 2009 FIXME full name
41
+
42
+ Permission is hereby granted, free of charge, to any person obtaining
43
+ a copy of this software and associated documentation files (the
44
+ 'Software'), to deal in the Software without restriction, including
45
+ without limitation the rights to use, copy, modify, merge, publish,
46
+ distribute, sublicense, and/or sell copies of the Software, and to
47
+ permit persons to whom the Software is furnished to do so, subject to
48
+ the following conditions:
49
+
50
+ The above copyright notice and this permission notice shall be
51
+ included in all copies or substantial portions of the Software.
52
+
53
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
54
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
55
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
56
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
57
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
58
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
59
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,50 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen rake/testtask].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/chrono_trigger'
3
+
4
+ ## Generate all the Rake tasks
5
+ ## Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ # $hoe = Hoe.new('chrono_trigger', ChronoTrigger::VERSION) do |p|
7
+ # p.developer('Greg Fitzgerald', 'fitzgerald@healthcentral.com')
8
+ # p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ # # p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
10
+ # p.rubyforge_name = "gregfitz23-chrono_trigger"
11
+ # p.extra_deps = [
12
+ # # ['activesupport','>= 2.0.2'],
13
+ # ]
14
+ # p.extra_dev_deps = [
15
+ # ['newgem', ">= #{::Newgem::VERSION}"],
16
+ # ['thoughtbot_shoulda', ">= 2.0.6"]
17
+ # ]
18
+ #
19
+ # p.clean_globs |= %w[**/.DS_Store tmp *.log]
20
+ # path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
21
+ # p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
22
+ # p.rsync_args = '-av --delete --ignore-errors'
23
+ # end
24
+ #
25
+ # require 'newgem/tasks' # load /tasks/*.rake
26
+ # Dir['tasks/**/*.rake'].each { |t| load t }
27
+ #
28
+ # # TODO - want other tests/tasks run by default? Add them to the list
29
+ # task :default => [:spec, :features]
30
+
31
+ Rake::TestTask.new do |t|
32
+ t.libs << 'test'
33
+ end
34
+
35
+ desc "Run tests"
36
+ task :default => :test
37
+
38
+ begin
39
+ require 'jeweler'
40
+ Jeweler::Tasks.new do |s|
41
+ s.name = "chrono_trigger"
42
+ s.summary = "TODO"
43
+ s.email = "darful@gmail.com"
44
+ s.homepage = ""
45
+ s.description = "TODO"
46
+ s.authors = ["Jon Ciccone"]
47
+ end
48
+ rescue LoadError
49
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
50
+ end
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 7
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ require 'chrono_trigger/runner'
5
+
6
+ ChronoTrigger::Runner.run
7
+
@@ -0,0 +1,69 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "chrono_trigger"
8
+ s.version = "0.1.7"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jon Ciccone"]
12
+ s.date = "2012-08-22"
13
+ s.description = "Binding to tag"
14
+ s.email = "darful@gmail.com"
15
+ s.executables = ["chrono_trigger"]
16
+ s.extra_rdoc_files = [
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ "History.txt",
21
+ "Manifest.txt",
22
+ "PostInstall.txt",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION.yml",
26
+ "bin/chrono_trigger",
27
+ "chrono_trigger.gemspec",
28
+ "lib/chrono_trigger.rb",
29
+ "lib/chrono_trigger/cron_entry.rb",
30
+ "lib/chrono_trigger/process.rb",
31
+ "lib/chrono_trigger/runner.rb",
32
+ "lib/chrono_trigger/shell.rb",
33
+ "lib/chrono_trigger/tasks.rb",
34
+ "lib/chrono_trigger/trigger.rb",
35
+ "lib/tasks/chrono_trigger.rake",
36
+ "lib/triggers/test_triggers.rb",
37
+ "script/console",
38
+ "script/destroy",
39
+ "script/generate",
40
+ "test/test_chrono_trigger.rb",
41
+ "test/test_cron_entry.rb",
42
+ "test/test_helper.rb",
43
+ "test/test_shell.rb",
44
+ "test/test_trigger.rb",
45
+ "test/triggers.rb"
46
+ ]
47
+ s.homepage = ""
48
+ s.rdoc_options = ["--charset=UTF-8"]
49
+ s.require_paths = ["lib"]
50
+ s.rubygems_version = "1.8.24"
51
+ s.summary = "binding to tag"
52
+ s.test_files = [
53
+ "test/test_chrono_trigger.rb",
54
+ "test/test_cron_entry.rb",
55
+ "test/test_helper.rb",
56
+ "test/test_shell.rb",
57
+ "test/test_trigger.rb",
58
+ "test/triggers.rb"
59
+ ]
60
+
61
+ if s.respond_to? :specification_version then
62
+ s.specification_version = 3
63
+
64
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
65
+ else
66
+ end
67
+ else
68
+ end
69
+ end
@@ -0,0 +1,11 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ module ChronoTrigger
5
+ VERSION = '0.0.2'
6
+ end
7
+
8
+ require "activesupport" unless defined? ActiveSupport
9
+ require "chrono_trigger/shell"
10
+ require "chrono_trigger/trigger"
11
+ require "chrono_trigger/cron_entry"
@@ -0,0 +1,71 @@
1
+ module ChronoTrigger
2
+
3
+ class CronEntry
4
+
5
+ def initialize(options={})
6
+ set_days(options[:days])
7
+ set_hours(options[:hours])
8
+ set_minutes(options[:minutes])
9
+ end
10
+
11
+ DAYS_CONVERSION = {
12
+ :sunday => 0,
13
+ :monday => 1,
14
+ :tuesday => 2,
15
+ :wednesday => 3,
16
+ :thursday => 4,
17
+ :friday => 5,
18
+ :saturday => 6
19
+ }
20
+
21
+ CALENDAR_DAYS = *(1..31)
22
+
23
+ def set_hours(*args)
24
+ args.compact!
25
+ args.flatten!
26
+ raise ChronoTrigger::ConfigurationException.new("Hours must be less than 24") if args.any? {|hour| hour >= 24}
27
+ @hours = args
28
+ end
29
+
30
+ def set_days(*args)
31
+ args.compact!
32
+ args.flatten!
33
+ args.each {|day| raise ChronoTrigger::ConfigurationException.new("Day #{day} setting is invalid") if !DAYS_CONVERSION.keys.include?(day)}
34
+ @days = args.map { |day| DAYS_CONVERSION[day] }
35
+ end
36
+
37
+ def set_minutes(*args)
38
+ args.compact!
39
+ args.flatten!
40
+ raise ChronoTrigger::ConfigurationException.new("Minutes must be less than 60") if args.any? {|minute| minute >= 60}
41
+ @minutes = args
42
+ end
43
+
44
+ def set_calendar_days(*args)
45
+ args.compact!
46
+ args.flatten!
47
+ args.each {|calendar_day| raise ChronoTrigger::ConfigurationException.new("Calendar Day #{calendar_day} setting is invalid") if !CALENDAR_DAYS.include?(calendar_day)}
48
+ @calendar_days = args
49
+ end
50
+
51
+ def matches?(datetime)
52
+ if @minutes.blank? && !@days.blank?
53
+ raise ChronoTrigger::ConfigurationException.new("Days were specified for a CronEntry with no minutes specified")
54
+ end
55
+
56
+ if (@minutes.blank? || @hours.blank?) && !@calendar_days.blank?
57
+ raise ChronoTrigger::ConfigurationException.new("Calendar Days were specified for a CronEntry with no minutes and/or no hours specified")
58
+ end
59
+
60
+ if !@days.blank? && !@calendar_days.blank?
61
+ raise ChronoTrigger::ConfigurationException.new("Calendar Days and Days were specified. This is unsupported.")
62
+ end
63
+
64
+ return false if !@minutes.blank? && !@minutes.include?(datetime.min)
65
+ return false if !@hours.blank? && !@hours.include?(datetime.hour)
66
+ return false if (!@days.blank? && !@days.include?(datetime.wday)) || (!@calendar_days.blank? && !@calendar_days.include?(datetime.day))
67
+ return true
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,37 @@
1
+ module ChronoTrigger
2
+
3
+ class Process
4
+
5
+ def run(options={})
6
+ @t = Thread.new do
7
+ setup(options)
8
+
9
+ shell = ChronoTrigger::Shell.new
10
+ options[:trigger_files] ? shell.load_triggers(options[:trigger_files].split(":")) : shell.load_triggers
11
+ loop do
12
+ shell.execute_triggers
13
+ sleep 1.minute.to_i
14
+ end
15
+ end
16
+
17
+ @t.join
18
+ end
19
+
20
+ def stop
21
+ @t.exit
22
+ end
23
+
24
+ private
25
+ def setup(options={})
26
+ if application_context = options[:application_context]
27
+ ENV['RAILS_ENV'] = options[:env] || "development"
28
+
29
+ application_path = File.join(application_context, 'config', 'environment')
30
+ STDOUT.puts "Loading application environment at #{File.join(application_context, 'config', 'environment')} for '#{ENV['RAILS_ENV']}' enviroment."
31
+ require(application_path)
32
+ end
33
+
34
+ require "chrono_trigger"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,292 @@
1
+ require File.join(File.dirname(__FILE__), 'process')
2
+ require "logger"
3
+ require 'optparse'
4
+ require 'yaml'
5
+
6
+ module ChronoTrigger
7
+ class Runner
8
+
9
+ attr_accessor :options
10
+ private :options, :options=
11
+
12
+ def self.run
13
+ new
14
+ end
15
+
16
+ def self.shutdown
17
+ @@instance.shutdown
18
+ end
19
+
20
+ def initialize
21
+ @@instance = self
22
+ parse_options
23
+
24
+ @process = ProcessHelper.new(options[:logger], options[:pid_file], options[:user], options[:group])
25
+
26
+ if options[:stop]
27
+ @process.kill
28
+ exit(1)
29
+ end
30
+
31
+ pid = @process.running?
32
+ if pid
33
+ if options[:force]
34
+ STDOUT.puts "Shutting down existing ChronoTrigger."
35
+ @process.kill
36
+ @process = ProcessHelper.new(options[:logger], options[:pid_file], options[:user], options[:group])
37
+ else
38
+ STDERR.puts "There is already a ChronoTrigger process running (pid #{pid}), exiting."
39
+ exit(1)
40
+ end
41
+ elsif pid.nil?
42
+ STDERR.puts "Cleaning up stale pidfile at #{options[:pid_file]}."
43
+ end
44
+
45
+ start
46
+ end
47
+
48
+ def parse_options
49
+ self.options = {
50
+ :log_level => Logger::INFO,
51
+ :daemonize => false,
52
+ :pid_file => File.join('', 'var', 'run', 'chrono_trigger.pid'),
53
+ :env => "development"
54
+ }
55
+
56
+ OptionParser.new do |opts|
57
+ opts.summary_width = 25
58
+
59
+ opts.banner = "ChronoTrigger - Execute cron jobs within the context of a Rails application\n\n",
60
+ "usage: chrono_trigger [options...]\n",
61
+ " chrono_trigger --help\n",
62
+ " chrono_trigger --version\n"
63
+
64
+ opts.separator ""
65
+ opts.separator ""; opts.separator "ChronoTrigger Options:"
66
+
67
+ opts.on("-tTRIGGER_FILES", "--triggers TRIGGER_FILES", "Path to file(s) specifying triggers to be executed. Multiple files should be separated by a :. When also specifying -a, this path will be relative to the application path") do |trigger_files|
68
+ options[:trigger_files] = trigger_files
69
+ end
70
+
71
+ opts.on("-f", "--force", "Force restart of ChronoTrigger process (can be used in conjunction with -P).") do
72
+ options[:force] = true
73
+ end
74
+
75
+ opts.on("-s", "--stop", "Stop a currently running ChronoTrigger process (can be used in conjunction with -P).") do
76
+ options[:stop] = true
77
+ end
78
+
79
+ opts.separator ""
80
+ opts.separator ""; opts.separator "Rails options:"
81
+
82
+ opts.on("-aAPPLICATION", "--application RAILS", "Path to Rails application context to execture triggers in.") do |application_context|
83
+ options[:application_context] = application_context
84
+ end
85
+
86
+ opts.on("-eENVIRONMENT", "--environment ENVIRONMENT", "Rails environment to execute triggers in.") do |environment|
87
+ options[:env] = environment
88
+ end
89
+
90
+ opts.separator ""
91
+ opts.separator ""; opts.separator "Process:"
92
+
93
+ opts.on("-PFILE", "--pid FILENAME", "save PID in FILENAME when using -d option.", "(default: #{options[:pid_file]})") do |pid_file|
94
+ options[:pid_file] = File.expand_path(pid_file)
95
+ end
96
+
97
+ opts.on("-u", "--user USER", "User to run as") do |user|
98
+ options[:user] = user.to_i == 0 ? Etc.getpwnam(user).uid : user.to_i
99
+ end
100
+
101
+ opts.on("-gGROUP", "--group GROUP", "Group to run as") do |group|
102
+ options[:group] = group.to_i == 0 ? Etc.getgrnam(group).gid : group.to_i
103
+ end
104
+
105
+ opts.separator ""; opts.separator "Logging:"
106
+
107
+ opts.on("-L", "--log [FILE]", "Path to print debugging information.") do |log_path|
108
+ options[:logger] = File.expand_path(log_path)
109
+ end
110
+
111
+ opts.on("-v", "Increase logging verbosity (may be used multiple times).") do
112
+ options[:log_level] -= 1
113
+ end
114
+
115
+ opts.on("-d", "Run as a daemon.") do
116
+ options[:daemonize] = true
117
+ end
118
+ end.parse!
119
+ end
120
+
121
+ def start
122
+ drop_privileges
123
+
124
+ @process.daemonize if options[:daemonize]
125
+
126
+ if application_context = options[:application_context]
127
+ Dir.chdir(application_context)
128
+ end
129
+
130
+ setup_signal_traps
131
+ @process.write_pid_file
132
+
133
+ STDOUT.puts "Starting ChronoTrigger."
134
+ @chrono_trigger_process = ChronoTrigger::Process.new
135
+ @chrono_trigger_process.run(options)
136
+
137
+ @process.remove_pid_file
138
+ end
139
+
140
+ def drop_privileges
141
+ ::Process.egid = options[:group] if options[:group]
142
+ ::Process.euid = options[:user] if options[:user]
143
+ end
144
+
145
+ def shutdown
146
+ begin
147
+ STDOUT.puts "Shutting down."
148
+ @chrono_trigger_process.stop
149
+ exit(1)
150
+ rescue Object => e
151
+ STDERR.puts "There was an error shutting down: #{e}"
152
+ exit(70)
153
+ end
154
+ end
155
+
156
+ def setup_signal_traps
157
+ Signal.trap("INT") { shutdown }
158
+ Signal.trap("TERM") { shutdown }
159
+ end
160
+ end
161
+
162
+ class ProcessHelper
163
+
164
+ def initialize(log_file = nil, pid_file = nil, user = nil, group = nil)
165
+ @log_file = log_file
166
+ @pid_file = pid_file
167
+ @user = user
168
+ @group = group
169
+ end
170
+
171
+ def safefork
172
+ begin
173
+ if pid = fork
174
+ return pid
175
+ end
176
+ rescue Errno::EWOULDBLOCK
177
+ sleep 5
178
+ retry
179
+ end
180
+ end
181
+
182
+ def daemonize
183
+ sess_id = detach_from_terminal
184
+ exit if pid = safefork
185
+
186
+ Dir.chdir("/")
187
+ File.umask 0000
188
+
189
+ close_io_handles
190
+ redirect_io
191
+
192
+ return sess_id
193
+ end
194
+
195
+ def detach_from_terminal
196
+ srand
197
+ safefork and exit
198
+
199
+ unless sess_id = ::Process.setsid
200
+ raise "Couldn't detach from controlling terminal."
201
+ end
202
+
203
+ trap 'SIGHUP', 'IGNORE'
204
+
205
+ sess_id
206
+ end
207
+
208
+ def close_io_handles
209
+ ObjectSpace.each_object(IO) do |io|
210
+ unless [STDIN, STDOUT, STDERR].include?(io)
211
+ begin
212
+ io.close unless io.closed?
213
+ rescue Exception
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ def redirect_io
220
+ begin; STDIN.reopen('/dev/null'); rescue Exception; end
221
+
222
+ if @log_file
223
+ begin
224
+ STDOUT.reopen(@log_file, "a")
225
+ STDOUT.sync = true
226
+ rescue Exception
227
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
228
+ end
229
+ else
230
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
231
+ end
232
+
233
+ begin; STDERR.reopen(STDOUT); rescue Exception; end
234
+ STDERR.sync = true
235
+ end
236
+
237
+ def rescue_exception
238
+ begin
239
+ yield
240
+ rescue Exception
241
+ end
242
+ end
243
+
244
+ def write_pid_file
245
+ return unless @pid_file
246
+ FileUtils.mkdir_p(File.dirname(@pid_file))
247
+ File.open(@pid_file, "w") { |f| f.write(::Process.pid) }
248
+ File.chmod(0644, @pid_file)
249
+ end
250
+
251
+ def remove_pid_file
252
+ return unless @pid_file
253
+ File.unlink(@pid_file) if File.exists?(@pid_file)
254
+ end
255
+
256
+ def running?
257
+ return false unless @pid_file
258
+
259
+ pid = File.read(@pid_file).chomp.to_i rescue nil
260
+ pid = nil if pid == 0
261
+ return false unless pid
262
+
263
+ begin
264
+ ::Process.kill(0, pid)
265
+ return pid
266
+ rescue Errno::ESRCH
267
+ return nil
268
+ rescue Errno::EPERM
269
+ return pid
270
+ end
271
+ end
272
+
273
+ def kill
274
+ return false unless @pid_file
275
+
276
+ pid = File.read(@pid_file).chomp.to_i rescue nil
277
+ pid = nil if pid == 0
278
+ return false unless pid
279
+
280
+ begin
281
+ ::Process.kill("TERM", pid)
282
+ remove_pid_file
283
+ return pid
284
+ rescue Errno::ESRCH
285
+ return nil
286
+ rescue Errno::EPERM
287
+ return pid
288
+ end
289
+
290
+ end
291
+ end
292
+ end