bin_script 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+