bin_script 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ log
2
+ locks
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Will automatically pull in this gem and all its
4
+ # dependencies specified in the gemspec
5
+ gem "bin_script", :path => File.expand_path("..", __FILE__)
6
+
7
+ gem 'rails'
8
+ gem 'activesupport'
9
+ # These are development dependencies
10
+ gem "rspec"
11
+ gem 'rake'
data/Gemfile.lock ADDED
@@ -0,0 +1,104 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ bin_script (0.1)
5
+ activesupport (>= 2.3.2)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ actionmailer (3.2.3)
11
+ actionpack (= 3.2.3)
12
+ mail (~> 2.4.4)
13
+ actionpack (3.2.3)
14
+ activemodel (= 3.2.3)
15
+ activesupport (= 3.2.3)
16
+ builder (~> 3.0.0)
17
+ erubis (~> 2.7.0)
18
+ journey (~> 1.0.1)
19
+ rack (~> 1.4.0)
20
+ rack-cache (~> 1.2)
21
+ rack-test (~> 0.6.1)
22
+ sprockets (~> 2.1.2)
23
+ activemodel (3.2.3)
24
+ activesupport (= 3.2.3)
25
+ builder (~> 3.0.0)
26
+ activerecord (3.2.3)
27
+ activemodel (= 3.2.3)
28
+ activesupport (= 3.2.3)
29
+ arel (~> 3.0.2)
30
+ tzinfo (~> 0.3.29)
31
+ activeresource (3.2.3)
32
+ activemodel (= 3.2.3)
33
+ activesupport (= 3.2.3)
34
+ activesupport (3.2.3)
35
+ i18n (~> 0.6)
36
+ multi_json (~> 1.0)
37
+ arel (3.0.2)
38
+ builder (3.0.0)
39
+ diff-lcs (1.1.3)
40
+ erubis (2.7.0)
41
+ hike (1.2.1)
42
+ i18n (0.6.0)
43
+ journey (1.0.3)
44
+ json (1.6.6)
45
+ mail (2.4.4)
46
+ i18n (>= 0.4.0)
47
+ mime-types (~> 1.16)
48
+ treetop (~> 1.4.8)
49
+ mime-types (1.18)
50
+ multi_json (1.2.0)
51
+ polyglot (0.3.3)
52
+ rack (1.4.1)
53
+ rack-cache (1.2)
54
+ rack (>= 0.4)
55
+ rack-ssl (1.3.2)
56
+ rack
57
+ rack-test (0.6.1)
58
+ rack (>= 1.0)
59
+ rails (3.2.3)
60
+ actionmailer (= 3.2.3)
61
+ actionpack (= 3.2.3)
62
+ activerecord (= 3.2.3)
63
+ activeresource (= 3.2.3)
64
+ activesupport (= 3.2.3)
65
+ bundler (~> 1.0)
66
+ railties (= 3.2.3)
67
+ railties (3.2.3)
68
+ actionpack (= 3.2.3)
69
+ activesupport (= 3.2.3)
70
+ rack-ssl (~> 1.3.2)
71
+ rake (>= 0.8.7)
72
+ rdoc (~> 3.4)
73
+ thor (~> 0.14.6)
74
+ rake (0.9.2.2)
75
+ rdoc (3.12)
76
+ json (~> 1.4)
77
+ rspec (2.9.0)
78
+ rspec-core (~> 2.9.0)
79
+ rspec-expectations (~> 2.9.0)
80
+ rspec-mocks (~> 2.9.0)
81
+ rspec-core (2.9.0)
82
+ rspec-expectations (2.9.1)
83
+ diff-lcs (~> 1.1.3)
84
+ rspec-mocks (2.9.0)
85
+ sprockets (2.1.2)
86
+ hike (~> 1.2)
87
+ rack (~> 1.0)
88
+ tilt (~> 1.1, != 1.3.0)
89
+ thor (0.14.6)
90
+ tilt (1.3.3)
91
+ treetop (1.4.10)
92
+ polyglot
93
+ polyglot (>= 0.3.1)
94
+ tzinfo (0.3.32)
95
+
96
+ PLATFORMS
97
+ ruby
98
+
99
+ DEPENDENCIES
100
+ activesupport
101
+ bin_script!
102
+ rails
103
+ rake
104
+ rspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Makarchev K, Lifshits D
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,68 @@
1
+ Rails BinScript
2
+ ===============
3
+
4
+ Easy writing and executing bins (espesually for crontab or god) in Rails project.
5
+ For my purposes much better than Rake, Thor and Rails Runner.
6
+
7
+ Features:
8
+
9
+ 1. Each bin is a class
10
+ 2. Easy writing tests
11
+ 3. Bin use lock file and logger with formatter when executing
12
+
13
+ Rails 2.3 and 3 compatible
14
+
15
+ ``` ruby
16
+ gem 'bin_script'
17
+ ```
18
+
19
+ rails g bin:bin bla
20
+ (for 2.3 copy generator into lib/generators and run: ./script/generate bin bla)
21
+
22
+ Call like:
23
+
24
+ $ cd project && ./bin/bla.rb -e production -a -b -c -d "asdf"
25
+
26
+ Examples (default features):
27
+
28
+ $ ./bin/bla.rb -e production
29
+ $ ./bin/bla.rb -e production -L ./locks/bla.lock
30
+ $ ./bin/bla.rb -e production -l ./log/bla.log
31
+ $ ./bin/bla.rb -e production --daemonize --pidfile=./tmp/bla.pid
32
+
33
+
34
+
35
+ Example Bin
36
+ -----------
37
+ app/models/bin/stuff_script.rb
38
+
39
+ ``` ruby
40
+ class StuffScript < BinScript
41
+ optional :u, "Update string"
42
+ required :d, "Date in format YYYY-MM-DD or YYYY-MM"
43
+ noarg :t, "Test run"
44
+
45
+ def test?
46
+ params(:t)
47
+ end
48
+
49
+ def do!
50
+ if test?
51
+ logger.info "update string #{params(:u)}"
52
+ else
53
+ logger.info "data #{Time.parse(params(:d))}"
54
+ end
55
+ end
56
+ end
57
+ ```
58
+
59
+ ### Custom exception notifier (create initializer with:)
60
+
61
+ ``` ruby
62
+ class BinScript
63
+ def notify_about_error(exception)
64
+ Mailter.some_notification(exception)...
65
+ end
66
+ end
67
+ ```
68
+
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require "bundler"
3
+ Bundler.setup
4
+
5
+ require 'rspec/core/rake_task'
6
+
7
+ task :default => :spec
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ require 'rubygems/package_task'
11
+ require 'rubygems/specification'
12
+
13
+ spec = eval(File.read('bin_script.gemspec'))
14
+
15
+ Gem::PackageTask.new(spec) do |pkg|
16
+ pkg.gem_spec = spec
17
+ end
data/bin/bin_helper ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- encoding: utf-8 -*-
3
+ # This helper is useful for requiring from bin scripts.
4
+ # Detect RAILS_ENV from '-e' parameter in ARGV (or 'development' as default) and load rails environment if need.
5
+ #
6
+ # Sample ./bin/script_name.rb
7
+ # #!/usr/bin/env ruby
8
+ #
9
+ # #NO_RAILS = true # Uncomment this line if you don't need rails environment in your script
10
+ # require File.join(File.dirname(__FILE__),'_bin_helper')
11
+ #
12
+ # Helper will detect class name for this script automatically. So for example above helper call something like this: ScriptName.new.run!
13
+
14
+ APP_ROOT ||= File.expand_path(File.dirname($0)) + "/../"
15
+
16
+ if defined?(DAEMONIZE) || ARGV.include?('--daemonize') # для демонизации бина
17
+ ARGV.delete('--daemonize')
18
+
19
+ # Stolen from activesupport
20
+ module Process
21
+ def self.daemon
22
+ exit if fork # Parent exits, child continues.
23
+ Process.setsid # Become session leader.
24
+ exit if fork # Zap session leader. See [1].
25
+ return 0
26
+ end
27
+ end
28
+
29
+ Process.daemon
30
+
31
+ if ARGV.include?('--pidfile')
32
+ pidfile = nil
33
+
34
+ ARGV.each_index do |i|
35
+ if ARGV[i] == '--pidfile' && ARGV.length > i
36
+ pidfile = ARGV[i+1]
37
+
38
+ ARGV.delete('--pidfile')
39
+ ARGV.delete(pidfile) if pidfile
40
+ break
41
+ end
42
+ end
43
+
44
+ if pidfile
45
+ File.open(pidfile, 'w'){|f| f.write($$) }
46
+ end
47
+ end
48
+ end
49
+
50
+ require 'rubygems'
51
+
52
+ # Define RAILS_ENV
53
+ ARGV.each_index do |i|
54
+ if ARGV[i] == '-e' && ARGV.length > i
55
+ RAILS_ENV = ARGV[i+1]
56
+ break
57
+ end
58
+ end
59
+
60
+ # Если задан ключ -h, то нам нужно только показать хэлп и environment грузить не обязательно
61
+ NO_RAILS = true if ARGV.include?('-h') && !defined?(NO_RAILS)
62
+
63
+ # Set defaults
64
+ RAILS_ENV = 'development' if defined?(RAILS_ENV).nil?
65
+
66
+ if !defined?(RAILS_ROOT) && !defined?(NO_RAILS)
67
+ # Load rails envoronment if not yet and we need it
68
+ require File.join(APP_ROOT, %w{config environment})
69
+ else
70
+ require 'active_support'
71
+ end
72
+
73
+ # Load BinScript class source
74
+ require 'bin_script' unless defined?(BinScript) == 'constant'
75
+
76
+ # Call script runner
77
+ BinScript.run_script
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.dirname(__FILE__) + "/lib/bin_script/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = %q{bin_script}
6
+ s.version = BinScript::VERSION
7
+
8
+ s.authors = ["Lifshits Dmitry", "Makarchev Konstantin"]
9
+ s.autorequire = %q{init}
10
+
11
+ s.description = %q{Easy writing and executing bins (espesually for crontab or god) in Rails project}
12
+
13
+ s.summary = %q{Easy writing and executing bins (espesually for crontab or god) in Rails project
14
+ For my purposes much better than Rake, Thor and Rails Runner}
15
+
16
+ s.email = %q{kostya27@gmail.com}
17
+ s.homepage = %q{http://github.com/kostya/bin_script}
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+
24
+ s.add_dependency 'activesupport', ">=2.3.2"
25
+ s.add_development_dependency "rspec"
26
+
27
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/lib/bin_script'
data/lib/bin_script.rb ADDED
@@ -0,0 +1,2 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.dirname(__FILE__) + '/bin_script/bin_script'
@@ -0,0 +1,389 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'getoptlong'
3
+ require 'pathname'
4
+ require 'rails' unless defined?(Rails)
5
+ require File.dirname(__FILE__) + '/lock_file'
6
+ require File.dirname(__FILE__) + '/xlogger'
7
+ require File.dirname(__FILE__) + '/class_inheritable_attributes'
8
+ #require 'active_support/core_ext/class/attribute.rb'
9
+
10
+ class BinScript
11
+ include ClassLevelInheritableAttributes
12
+ class_inheritable_attributes :parameters, :log_level, :enable_locking, :enable_logging, :date_log_postfix, :disable_puts_for_tests
13
+
14
+ # Default parameters
15
+ @parameters = [
16
+ {:key => :e, :type => :optional, :description => "Rails environment ID (default - development)"},
17
+ {:key => :h, :type => :noarg, :description => "Print usage message", :alias => [:H, :help]},
18
+ {:key => :l, :type => :optional, :description => "Path to log file (default \#{Rails.root}/log/[script_name].log"},
19
+ {:key => :L, :type => :optional, :description => "Path to lock file (default \#{Rails.root}/lock/[script_name].lock"}
20
+ ]
21
+
22
+ # Default log level INFO or DEBUG for test env
23
+ @log_level = (Rails.env.development? ? XLogger::DEBUG : XLogger::INFO)
24
+
25
+ # Enable locking by default
26
+ @enable_locking = true
27
+
28
+ # Enable logging by default
29
+ @enable_logging = true
30
+
31
+ # По умолчанию мы при каждом запуске скрипта продолжаем писать в основной лог. Но можно записать сюда формат, который
32
+ # можно скормить time.strftime(format) и результат подставится в конец имени файла лога. Таким образом можно делать
33
+ # отдельные лог-файлы для каждого запуска, для каждого дня/месяца/года/часа и т.д...
34
+ # Например "_%Y-%m-%d_%H-%M-%S" - каждый запуск новый лог
35
+ # "_%Y-%m-%d" - каждый день новый лог
36
+ @date_log_postfix = ''
37
+
38
+ # По умолчанию puts работает как обычно, но BinScript может ничего не выводить если RAILS_ENV=test
39
+ # Если скрипт что-то выводит, это попадает в лог спеков, что не красиво. Так что для таких скриптов лучше включать эту опцию
40
+ @disable_puts_for_tests = false
41
+
42
+ # Allowed parameter types. Equivalence aliases with GetoptLong constants.
43
+ PARAMETER_TYPES = {
44
+ :noarg => GetoptLong::NO_ARGUMENT,
45
+ :optional => GetoptLong::OPTIONAL_ARGUMENT,
46
+ :required => GetoptLong::REQUIRED_ARGUMENT
47
+ }
48
+
49
+ # Place for logger object
50
+ attr_accessor :logger
51
+
52
+ # Place for store exit status.
53
+ attr_accessor :exit_status
54
+
55
+ # Create shortcuts to simplify logging from scripts
56
+ singleton = (class << self; self end)
57
+ Logger::Severity.constants.each do |level|
58
+ method = level.to_s.downcase
59
+
60
+ # Define class level helper method
61
+ singleton.class_eval do
62
+ define_method :info do |message|
63
+ return unless Rails.logger
64
+ Rails.logger.send(method,message)
65
+ end
66
+ end
67
+
68
+ # Define instance level helper method
69
+ define_method method do |message|
70
+ return unless @logger
71
+ @logger.send(method,message)
72
+ end
73
+ end
74
+
75
+ class << self
76
+ # Get parameter by key
77
+ def get_parameter(key)
78
+ param = @parameters.find{|p| p[:key] == key || (p[:alias].present? && p[:alias].include?(key))}
79
+ raise "Can't find parameter with key #{key.inspect} for class #{self.inspect}!" if param.nil?
80
+ param
81
+ end
82
+
83
+ # Prepare readable script name
84
+ def script_name
85
+ self.to_s.underscore.gsub('/','__')
86
+ end
87
+
88
+ # Parse script filename. Extract important path parts
89
+ def parse_script_file_name(filename)
90
+ result = {}
91
+ # Prepare important parts of source script filename
92
+ parts = filename.split(File::SEPARATOR)
93
+ parts = parts[parts.index('bin')+1..-1]
94
+ parts.map!{|p| File.basename(p).split('.').first}
95
+
96
+ result[:parts] = parts
97
+
98
+ result[:class] = calculate_script_class_name(parts)
99
+ result[:files] = calculate_script_class_filename(parts)
100
+
101
+ result
102
+ end
103
+
104
+ # Prepare class name from filename parts
105
+ def calculate_script_class_name(parts)
106
+ # Calculate class name and paths from source script filename parts
107
+ if(parts.length > 1)
108
+ class_name = parts.map{|p| p.camelize}.join('::') + parts.first.camelize
109
+ else
110
+ class_name = parts.first.camelize
111
+ end
112
+ class_name += "Script"
113
+ end
114
+
115
+ # Prepare class name from filename parts
116
+ def calculate_script_class_filename(parts)
117
+ files = []
118
+
119
+ # Calculate and add to list file with script class itself
120
+ class_file = File.join(%w{app models bin}, parts)
121
+ class_file += '_nagios' if(parts.length > 1)
122
+ class_file += '_script.rb'
123
+ files << class_file
124
+
125
+ # Add intermediate classes
126
+ parts[0..-2].each { |p| files << "app/models/#{p}_script.rb" }
127
+
128
+ files.reverse
129
+ end
130
+
131
+ # Run script detected by the filename of source script file
132
+ def run_script(filename = $0)
133
+ cfg = parse_script_file_name(Pathname.new(filename).realpath.to_s)
134
+ cfg[:files].each { |f| require File.join(rails_root, f) }
135
+
136
+ # Create instance and call run! for script class
137
+ klass = cfg[:class].constantize
138
+ script = klass.new
139
+ script.run!
140
+
141
+ # Exit with specified exit status
142
+ exit script.exit_status || 0
143
+ end
144
+
145
+ # Prepare aliases for adding parameters in child classes
146
+ PARAMETER_TYPES.keys.each do |type|
147
+ define_method type do |key, opts|
148
+ param = {:key => key, :type => type}
149
+ if opts.is_a?(String)
150
+ param[:description] = opts
151
+ else
152
+ param = param.merge(opts)
153
+ end
154
+
155
+ # We want aliases always to be an array
156
+ param[:alias] = [param[:alias]].flatten.compact
157
+
158
+ @parameters = @parameters + [param]
159
+ end
160
+ end
161
+
162
+ # Remove parameter
163
+ def remove_parameter(key)
164
+ new = []
165
+ @parameters.each { |p| new << p if p[:key] != key }
166
+ @parameters = new
167
+ end
168
+
169
+ # RAILS_ROOT is not availible if rails environment was not loaded. So detect application root in this case from current file path
170
+ def rails_root
171
+ Pathname.new(Rails.root ? Rails.root : File.join(File.dirname(__FILE__), %w{.. ..})).realpath.to_s
172
+ end
173
+
174
+ # Prepare ARGV parameters as hash
175
+ def get_argv_values
176
+ values = {}
177
+ o = GetoptLong.new(*get_getoptlong_params)
178
+ o.quiet = true # Don't write arg error to STDERR
179
+ o.each { |opt, arg| values[opt[1..-1].to_sym] = arg }
180
+ values
181
+ end
182
+
183
+ # Prepare usage message
184
+ def usage(message = nil)
185
+ usage_msg = ''
186
+ usage_msg += "Error: #{message}\n\n" unless message.nil?
187
+ usage_msg += "Use: ./bin/#{script_name}.rb [OPTIONS]\n\nAvailible options:\n\n"
188
+ @parameters.each do |param|
189
+ usage_msg += " #{prefix_key param[:key]} #{param[:description]}\n"
190
+ end
191
+ usage_msg += "\n"
192
+ usage_msg
193
+ end
194
+
195
+ private
196
+ # Prepare parameters in Getoptlong lib format
197
+ def get_getoptlong_params
198
+ result = []
199
+ @parameters.each do |param|
200
+ cfg = [prefix_key(param[:key])]
201
+ param[:alias].each{|als| cfg << prefix_key(als) } unless param[:alias].blank?
202
+ cfg << PARAMETER_TYPES[param[:type]]
203
+ result << cfg
204
+ end
205
+ result
206
+ end
207
+
208
+ # Prepare argument name with short or long prefix
209
+ def prefix_key(key)
210
+ key = key.to_s
211
+ (key.length > 1 ? "--" : "-") + key
212
+ end
213
+ end
214
+
215
+ def puts(*arg)
216
+ return if self.class.disable_puts_for_tests && Rails.env.test?
217
+ Kernel.puts(*arg)
218
+ end
219
+
220
+ # Initialize script
221
+ def initialize
222
+ begin
223
+ @source_argv = ARGV.dup
224
+ @overrided_parameters = {}
225
+ @params_values = (Rails.env.test? ? {} : self.class.get_argv_values)
226
+
227
+ # Create logger if logging enabled
228
+ if self.class.enable_logging
229
+ @logger = XLogger.new(:file => log_filename, :dont_touch_rails_logger => (Rails.env.test?))
230
+ @logger.level = self.class.log_level
231
+ end
232
+
233
+ rescue GetoptLong::InvalidOption, GetoptLong::MissingArgument, GetoptLong::NeedlessArgument => e
234
+ usage_exit e.message
235
+ end
236
+ end
237
+
238
+ # Create lock file, call script code and unlock file even if error happend.
239
+ def run!
240
+
241
+ # Print usage and exit if asked
242
+ usage_exit if params(:h)
243
+
244
+ # Create and check lock file if enabled
245
+ if self.class.enable_locking
246
+ @lock = LockFile.new(lock_filename)
247
+ @lock.quiet = true # Don't write errors to STDERR
248
+ info "Use lock file: #{@lock.path}"
249
+ if(@lock.lock)
250
+ warn "Lock file '#{@lock.path}' already open in exclusive mode. Exit!"
251
+ exit
252
+ end
253
+ end
254
+
255
+ begin
256
+ # Log important info and call script job
257
+ info "Log level = #{@logger.level}" if self.class.enable_logging
258
+ info "Parameters: #{@source_argv.join(', ')}"
259
+ info "Starting script #{self.class.script_name}..."
260
+ start = Time.now
261
+
262
+ # Инкрементируем счетчик запусков этого скрипта
263
+ inc_counter("#{self.class.script_name}_times")
264
+
265
+ do!
266
+ duration = Time.now - start
267
+ info "Script #{self.class.script_name} finished!"
268
+ info "Script job duration: #{duration}"
269
+ info "Exit status: #{@exit_status}" if @exit_status
270
+
271
+ # Инкрементируем время работы э
272
+ inc_counter("#{self.class.script_name}_long", duration)
273
+
274
+ # Log benchmarker info if it's not empty
275
+ log_benchmarker_data
276
+ rescue Exception => e
277
+ # Print error info if it's not test env or exit
278
+ if !Rails.env.test? && e.class != SystemExit && e.class != Interrupt
279
+ msg = self.class.prepare_exception_message(e)
280
+ puts "\n" + msg
281
+ fatal msg
282
+ notify_about_error(e)
283
+ end
284
+
285
+ # Инкрементируем счетчик ошибок этого скрипта
286
+ inc_counter("#{self.class.script_name}_raised")
287
+ ensure
288
+ # Unlock lock file
289
+ @lock.unlock if self.class.enable_locking && @lock
290
+ end
291
+ end
292
+
293
+ # Print usage message and exit
294
+ def usage_exit(message = nil)
295
+ error "Exit with error message: #{message}"
296
+ Kernel.puts(self.class.usage(message))
297
+ exit
298
+ end
299
+
300
+ # Dummy for do! method
301
+ def do!; end
302
+
303
+ # Override one or more parameters for testing purposes
304
+ def override_parameters(args)
305
+ if args.is_a?(Symbol)
306
+ override_parameter(self.class.get_parameter(args))
307
+ elsif args.is_a?(Hash)
308
+ args.each{|key, value| override_parameter(self.class.get_parameter(key), value)}
309
+ else
310
+ raise "Parameter should be Symbol or Hash"
311
+ end
312
+ end
313
+
314
+ # Return parameter value by key
315
+ def params(key)
316
+ param = self.class.get_parameter(key)
317
+
318
+ # Use dafault key (if call by alias)
319
+ key = param[:key]
320
+
321
+ case param[:type]
322
+ when :noarg
323
+ return (@overrided_parameters.has_key?(key) && @overrided_parameters[key]) || !@params_values[key].nil?
324
+ when :optional
325
+ #return nil unless @overrided_parameters.has_key?(key) || @params_values.has_key?(key)
326
+ return @overrided_parameters[key] || @params_values[key] || param[:default]
327
+ when :required
328
+ value = @overrided_parameters[key] || @params_values[key] || param[:default]
329
+ return value
330
+ end
331
+ end
332
+
333
+ # Prepare filename of log file
334
+ def lock_filename
335
+ params(:L).blank? ? File.join(self.class.rails_root, 'locks', "#{self.class.script_name}.lock") : params(:L)
336
+ end
337
+
338
+ # Prepare filename of log file
339
+ def log_filename
340
+ params(:l).blank? ? File.join(self.class.rails_root, 'log', "#{self.class.script_name}#{log_filename_time_part}.log") : params(:l)
341
+ end
342
+
343
+ private
344
+
345
+ # Current time logname part.
346
+ def log_filename_time_part
347
+ Time.now.strftime(self.class.date_log_postfix)
348
+ end
349
+
350
+ # Override value for one parameter
351
+ def override_parameter(param, value = nil)
352
+ value = case param[:type]
353
+ when :noarg
354
+ true
355
+ when :optional
356
+ value.to_s
357
+ when :required
358
+ value
359
+ end
360
+ @overrided_parameters[param[:key]] = value
361
+ end
362
+
363
+ # Print benchmarker statistic to log if its not empty
364
+ def log_benchmarker_data
365
+ benchmark_data = {} #benchmark_get_data
366
+ return if benchmark_data.empty?
367
+ info "Benchmarker data:"
368
+ info benchmark_data.to_yaml
369
+ end
370
+
371
+ # Prepare string with exception details
372
+ def self.prepare_exception_message(e)
373
+ <<-EXCEPTION
374
+ Exception happend
375
+ Type: #{e.class.inspect}
376
+ Error occurs: #{e.message}
377
+ Backtrace: #{e.backtrace.join("\n")}
378
+ EXCEPTION
379
+ end
380
+
381
+ def inc_counter(id, counter = 1)
382
+ # stub
383
+ end
384
+
385
+ def notify_about_error(ex)
386
+ # stub
387
+ end
388
+
389
+ end
@@ -0,0 +1,25 @@
1
+ module ClassLevelInheritableAttributes
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ def class_inheritable_attributes(*args)
8
+ @class_inheritable_attributes ||= [:class_inheritable_attributes]
9
+ @class_inheritable_attributes += args
10
+ args.each do |arg|
11
+ class_eval %(
12
+ class << self; attr_accessor :#{arg} end
13
+ )
14
+ end
15
+ @class_inheritable_attributes
16
+ end
17
+
18
+ def inherited(subclass)
19
+ @class_inheritable_attributes.each do |class_inheritable_attribute|
20
+ instance_var = "@#{class_inheritable_attribute}"
21
+ subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ # -*- encoding: utf-8 -*-
2
+ class LockFile
3
+ attr_accessor :logger, :path, :quiet
4
+
5
+ # By default print errors
6
+ @quiet = false
7
+
8
+ def initialize(path, logger = nil)
9
+ if path == nil
10
+ raise "file path cannot be nil"
11
+ end
12
+ @path = path
13
+ @logger = logger || $logger
14
+ end
15
+
16
+ def lock(waiting_unlock = false)
17
+ flags = File::WRONLY | File::TRUNC | File::CREAT
18
+
19
+ @f = File.open(@path, flags)
20
+ flags = File::LOCK_EX
21
+ flags |= File::LOCK_NB if waiting_unlock == false
22
+
23
+ unless @f.flock flags
24
+ log "File '#{@path}' already open in exclusive mode.\n"
25
+ return true
26
+ end
27
+ return false
28
+ end
29
+
30
+ def unlock
31
+ @f.close
32
+ File.delete( @f.path)
33
+ rescue Errno::ENOENT
34
+ #do nothing
35
+ end
36
+
37
+ def log(str)
38
+ return if @quiet
39
+ if @logger
40
+ @logger.warn str
41
+ else
42
+ print str
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,4 @@
1
+ # -*- encoding: utf-8 -*-
2
+ class BinScript
3
+ VERSION = "0.1"
4
+ end
@@ -0,0 +1,60 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'logger'
4
+
5
+ class XLogger < Logger
6
+ def initialize(hint = {})
7
+
8
+ if(hint[:rotate])
9
+ super(hint[:file] || STDOUT, hint[:rotate])
10
+ else
11
+ super(hint[:file] || STDOUT)
12
+ end
13
+
14
+ STDOUT.sync = true
15
+ self.formatter = Formatter.new
16
+ self.datetime_format = hint[:date_format] || "%d.%m %H:%M:%S"
17
+
18
+ # Don't change default logger if asked
19
+ if hint[:dont_touch_rails_logger].blank? && defined?(ActiveRecord::Base)
20
+ ActiveRecord::Base.logger = self
21
+ def Rails.logger; ActiveRecord::Base.logger; end
22
+
23
+ # This raise warning to STDOUT
24
+ #Object.const_set "RAILS_DEFAULT_LOGGER", self
25
+ end
26
+
27
+ self.level = hint[:log_level] || rails_env_log_level
28
+ log_sql if hint[:log_sql] || !Rails.env.production?
29
+ end
30
+
31
+ class Formatter < Logger::Formatter
32
+ def call(severity, time, progname, msg)
33
+ if severity == 'INFO' && msg.nil?
34
+ # use this if you want a simple blank line without date in your logs:
35
+ # just call a logger.info without any params
36
+ "\n"
37
+ else
38
+ format_datetime(time) << " " << msg2str(msg) << "\n"
39
+ end
40
+ end
41
+ end
42
+
43
+ def log_sql
44
+ ActiveRecord::Base.connection.logger = self if defined?(ActiveRecord::Base)
45
+ end
46
+
47
+ def rails_env_log_level
48
+ Rails.env.production? ? Logger::INFO : Logger::DEBUG
49
+ end
50
+ end
51
+
52
+ module ActiveRecord
53
+ module ConnectionAdapters
54
+ class AbstractAdapter
55
+ def logger=(val)
56
+ @logger = val
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ # for Rails 3
2
+ if Rails::VERSION::MAJOR >= 3
3
+
4
+ module Bin
5
+ class BinGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+ def add_files
9
+ template "script.rb", "bin/#{file_path}.rb"
10
+ template "script_class.rb", "app/models/bin/#{file_path}_script.rb"
11
+ template "spec.rb", "spec/models/bin/#{file_path}_script_spec.rb"
12
+ chmod "bin/#{file_path}.rb", 0755
13
+ end
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ # for Rails 2.3
20
+ if Rails::VERSION::MAJOR == 2
21
+
22
+ class BinGenerator < Rails::Generator::NamedBase
23
+ def manifest
24
+ record do |m|
25
+ m.template "script.rb", "bin/#{file_path}.rb", :chmod => 0755
26
+ m.template "script_class.rb", "app/models/bin/#{file_path}_script.rb"
27
+ m.directory "spec/models/bin"
28
+ m.template "spec.rb", "spec/models/bin/#{file_path}_script_spec.rb"
29
+ end
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ path = File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath)
5
+
6
+ APP_ROOT = File.dirname(path)
7
+ ENV['BUNDLE_GEMFILE'] ||= path
8
+
9
+ require 'rubygems'
10
+ require 'bundler/setup'
11
+
12
+ load Gem.bin_path('bin_script', 'bin_helper')
@@ -0,0 +1,42 @@
1
+ class <%= class_name %>Script < BinScript
2
+ # You can define script parameters this way:
3
+ # {type} {key}, {options}
4
+ #
5
+ # Availible types: noarg, optional and required
6
+ # Key is the Symbol with parameter key
7
+ # Options is a hash that may contain this keys:
8
+ # :description - parameters descriptions to automatic prepare usage message
9
+ # :alias - symbol or array of symbols with aliases for this parameters
10
+ # :default - default value for this parameter
11
+ #
12
+ # Instead of options hash you can use just string with description.
13
+ #
14
+ # Some examples:
15
+ # noarg :n, "This is a parameter without argument. 'params(:n)' in script will return true or false."
16
+ # optional :o, :description => "Optional parameter that can has argument. 'params()'"
17
+ # optional :oo, :description => "Optional parameter with default value and aliases. 'params(:o)' and 'params(:oo)' will return argument value or default key value", :default => "some default value", :alias => :oo
18
+ # required :r, :description => "Required argemtn. Script will exit if you use this parameter without argument value. Also you can use multiple aliases.", :alias => [:rr, :rrr]
19
+ #
20
+ # You may override default log level (Logger::INFO) for this script log this way:
21
+ # self.log_level = Logger::INFO
22
+ #
23
+ # Availible log levels:
24
+ # DEBUG = 0
25
+ # INFO = 1
26
+ # WARN = 2
27
+ # ERROR = 3
28
+ # FATAL = 4
29
+ # UNKNOWN = 5
30
+ #
31
+ # По умолчанию мы при каждом запуске скрипта продолжаем писать в основной лог. Но можно записать сюда формат, который
32
+ # можно скормить time.strftime(format) и результат подставится в конец имени файла лога. Таким образом можно делать
33
+ # отдельные лог-файлы для каждого запуска, для каждого дня/месяца/года/часа и т.д...
34
+ # Например "_%Y-%m-%d_%H-%M-%S" - каждый запуск новый лог
35
+ # "_%Y-%m-%d" - каждый день новый лог
36
+ # self.date_log_postfix = "_%Y-%m-%d"
37
+
38
+ # Do script job!
39
+ def do!
40
+ logger.info "Script <%= class_name %> works!"
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe <%= class_name %>Script do
4
+ before :each do
5
+ @bin = <%= class_name %>Script.new
6
+
7
+ # if want to point some params into bin => @bin.override_parameters({:i => 10, :a => '20'})
8
+ end
9
+
10
+ it 'should do! and not raise' do
11
+ @bin.do!
12
+ end
13
+ end
@@ -0,0 +1,184 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe BinScript do
4
+ before :each do
5
+ root = Pathname.new(File.dirname(__FILE__) + '/../').realpath.to_s
6
+ Rails.stub!(:root).and_return(root)
7
+ end
8
+
9
+ class TestScript < BinScript
10
+ noarg :n, "Test parameter that can't have argument"
11
+ optional :o, :description => "Test parameter that can have argument", :alias => :oo
12
+ required :r, :description => "Test parameter that should have argument", :alias => [:rr, :rrr]
13
+ end
14
+
15
+ describe "class name detection" do
16
+ before(:all) do
17
+ @test_date = [
18
+ {:filename => "/prj/bin/nagios/some.rb", :parts => ['nagios','some'], :class => "Nagios::SomeNagiosScript", :files => ["app/models/nagios_script.rb", "app/models/bin/nagios/some_nagios_script.rb"]},
19
+ {:filename => "/prj/bin/another.rb", :parts => ['another'], :class => "AnotherScript", :files => ["app/models/bin/another_script.rb"]}
20
+ ]
21
+ @test_keys = [:parts, :class, :files]
22
+ end
23
+ it 'should detect class name and filenames from source script filename' do
24
+ @test_date.each do |test|
25
+ result = BinScript.parse_script_file_name(test[:filename])
26
+ @test_keys.each { |key| result[key].should == test[key] }
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "parameters" do
32
+ it 'should accept string as description' do
33
+ TestScript.get_parameter(:n)[:description].should == "Test parameter that can't have argument"
34
+ end
35
+
36
+ it 'should accept hash as second parameter' do
37
+ TestScript.get_parameter(:o)[:description].should == "Test parameter that can have argument"
38
+ end
39
+
40
+ it "should accept parameters of all types" do
41
+ {:n => :noarg, :o => :optional, :r => :required}.each do |key,type|
42
+ TestScript.get_parameter(key)[:type].should == type
43
+ end
44
+ end
45
+
46
+ describe "required parameters" do
47
+ it "should return correct requred argument value" do
48
+ @script.override_parameters(:r => 'value')
49
+ @script.params(:r).should == 'value'
50
+ end
51
+ end
52
+
53
+ describe "overrided parameters for testing" do
54
+ it "should return false or true for noarg parameters even without value" do
55
+ @script.params(:n).should be_false
56
+ @script.override_parameters(:n)
57
+ @script.params(:n).should be_true
58
+ end
59
+
60
+ describe "optional parameters" do
61
+ it "should return nil if parameter not set or empty string if no argument defined" do
62
+ @script.params(:o).should be_nil
63
+ @script.override_parameters(:o)
64
+ @script.params(:o).should == ''
65
+ end
66
+
67
+ it "should return value of argument" do
68
+ @script.params(:o).should be_nil
69
+ @script.override_parameters(:o => 'value')
70
+ @script.params(:o).should == 'value'
71
+ end
72
+ end
73
+
74
+ describe "aliases" do
75
+ it 'should accept and handle one alias' do
76
+ @script.override_parameters(:o => 'value')
77
+ @script.params(:o).should == 'value'
78
+ @script.params(:o).should == @script.params(:oo)
79
+ end
80
+
81
+ it 'should accept and handle some alias' do
82
+ @script.override_parameters(:r => 'value')
83
+ @script.params(:r).should == 'value'
84
+ @script.params(:r).should == @script.params(:rr)
85
+ @script.params(:rr).should == @script.params(:rrr)
86
+ end
87
+
88
+ it 'should override parameters by alias' do
89
+ @script.override_parameters(:oo => 'value')
90
+ @script.params(:o).should == 'value'
91
+ end
92
+
93
+ describe "default values" do
94
+ class DevValTestScript < BinScript
95
+ optional :o, :default => "def opt value"
96
+ required :r, :default => "def req value"
97
+ end
98
+ it 'should return default value if value for optional argument was not defiend' do
99
+ DevValTestScript.new.params(:o).should == 'def opt value'
100
+ DevValTestScript.new.params(:r).should == 'def req value'
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ describe "usage" do
108
+ it "should generate usage message" do
109
+ USAGE = <<USAGE
110
+ Use: ./bin/test_script.rb [OPTIONS]
111
+
112
+ Availible options:
113
+
114
+ -e Rails environment ID (default - development)
115
+ -h Print usage message
116
+ -l Path to log file (default \#{Rails.root}/log/[script_name].log
117
+ -L Path to lock file (default \#{Rails.root}/lock/[script_name].lock
118
+ -n Test parameter that can't have argument
119
+ -o Test parameter that can have argument
120
+ -r Test parameter that should have argument
121
+
122
+ USAGE
123
+ TestScript.usage.should == USAGE
124
+ end
125
+ end
126
+
127
+ before(:each) do
128
+ @script = TestScript.new
129
+ end
130
+
131
+ describe "locking" do
132
+ it "should set default lock file" do
133
+ @script.lock_filename.should == File.join(Rails.root, %w{locks test_script.lock})
134
+ end
135
+
136
+ it "should overwrite lock filename with option -L" do
137
+ @script.override_parameters(:L => '/tmp/test_script.lock')
138
+ @script.lock_filename.should == '/tmp/test_script.lock'
139
+ end
140
+
141
+ it "should create lock file while execute do! and delete it after even when exceptions occurs" do
142
+ # Extend test script
143
+ class TestScript
144
+ attr_accessor :lock_file_has_been_created
145
+ attr_accessor :raise_before_finish
146
+ def do!
147
+ self.lock_file_has_been_created = File.exist?(lock_filename)
148
+ raise "Test exception" if self.raise_before_finish
149
+ end
150
+ end
151
+ @script.raise_before_finish = false
152
+ @script.lock_file_has_been_created.should_not be_true
153
+ @script.run!
154
+ @script.lock_file_has_been_created.should be_true
155
+ File.exist?(@script.lock_filename).should be_false
156
+
157
+ # And now test case when execptions occurs while script executed
158
+ @script.lock_file_has_been_created = false
159
+ @script.raise_before_finish = true
160
+ @script.run!
161
+ @script.lock_file_has_been_created.should be_true
162
+ @script.raise_before_finish = true
163
+ File.exist?(@script.lock_filename).should be_false
164
+ end
165
+ end
166
+
167
+ describe "logging" do
168
+ it "should set default log file correctly" do
169
+ @script.log_filename.should == File.join(Rails.root, %w{log test_script.log})
170
+ end
171
+
172
+ it "should overwrite log filename with option -l" do
173
+ @script.override_parameters(:l => '/tmp/test_script.log')
174
+ @script.log_filename.should == '/tmp/test_script.log'
175
+ end
176
+
177
+ it "should create default logger" do
178
+ default_log = File.join(Rails.root, %w{tmp test_script.log})
179
+ File.delete(default_log) if File.exist?(default_log)
180
+ @script.logger.should_not be_nil
181
+ end
182
+ end
183
+
184
+ end
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require "bundler"
3
+ Bundler.setup
4
+ ENV['RAILS_ENV'] ||= 'test'
5
+
6
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
7
+ require 'bin_script'
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bin_script
3
+ version: !ruby/object:Gem::Version
4
+ hash: 9
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ version: "0.1"
10
+ platform: ruby
11
+ authors:
12
+ - Lifshits Dmitry
13
+ - Makarchev Konstantin
14
+ autorequire: init
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-04-07 00:00:00 +04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activesupport
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 2
32
+ - 3
33
+ - 2
34
+ version: 2.3.2
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :development
50
+ version_requirements: *id002
51
+ description: Easy writing and executing bins (espesually for crontab or god) in Rails project
52
+ email: kostya27@gmail.com
53
+ executables:
54
+ - bin_helper
55
+ extensions: []
56
+
57
+ extra_rdoc_files: []
58
+
59
+ files:
60
+ - .gitignore
61
+ - Gemfile
62
+ - Gemfile.lock
63
+ - LICENSE
64
+ - README.markdown
65
+ - Rakefile
66
+ - bin/bin_helper
67
+ - bin_script.gemspec
68
+ - init.rb
69
+ - lib/bin_script.rb
70
+ - lib/bin_script/bin_script.rb
71
+ - lib/bin_script/class_inheritable_attributes.rb
72
+ - lib/bin_script/lock_file.rb
73
+ - lib/bin_script/version.rb
74
+ - lib/bin_script/xlogger.rb
75
+ - lib/generators/bin/bin_generator.rb
76
+ - lib/generators/bin/templates/script.rb
77
+ - lib/generators/bin/templates/script_class.rb
78
+ - lib/generators/bin/templates/spec.rb
79
+ - spec/bin_script_spec.rb
80
+ - spec/spec_helper.rb
81
+ has_rdoc: true
82
+ homepage: http://github.com/kostya/bin_script
83
+ licenses: []
84
+
85
+ post_install_message:
86
+ rdoc_options: []
87
+
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ hash: 3
96
+ segments:
97
+ - 0
98
+ version: "0"
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ requirements: []
109
+
110
+ rubyforge_project:
111
+ rubygems_version: 1.3.7
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: Easy writing and executing bins (espesually for crontab or god) in Rails project For my purposes much better than Rake, Thor and Rails Runner
115
+ test_files: []
116
+