watcher 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig CHANGED
Binary file
data/History.txt CHANGED
@@ -1,3 +1,60 @@
1
+ == 1.2.0 2008-07-30
2
+
3
+ * 6 major enhancements:
4
+ * Major re-write of entire Watcher code, separating logic into two class
5
+ files: watcher.rb and event.rb.
6
+ * Watcher verbosity levels consolidated to three levels: debug, verbose, and
7
+ always, in order from lowest priority to highest priority. Debug events
8
+ will rarely be logged, while always events will always be logged.
9
+ * The Watcher instantiation parameter, :fail_actions Array, is unsupported
10
+ and replaced by a Hash named :default_fail. Like :fail_actions, this
11
+ represents the set of actions to occur if no other fail actions are
12
+ specified when invoking one of Watcher's log event methods. Unlike the
13
+ :fail_actions Array, this is a Hash, whose possible key/value pairs are
14
+ described below.
15
+ * Fail actions logic has been completely rewritten.
16
+ * Invocation of Watcher.debug, Watcher.verbose, and Watcher.always
17
+ requires a fail_action parameter. This Hash must contain a :failure
18
+ key, equal to either :error or :warn.
19
+ * If :warn, then an exception will merely cause a warning event to
20
+ be recorded.
21
+ * If :error, an exception will cause an error event to be recorded, then
22
+ re-raise the original exception.
23
+ * Two other keys are permitted in the fail_action parameter. The presence
24
+ of one necessitates the presence of the other.
25
+ * :tries -- the total number of tries Watcher will attempt to execute
26
+ the code in the block.
27
+ * :proc -- the Proc instance that Watcher will invoke before each
28
+ retry attempt.
29
+ * If neither :tries nor :proc is given, Watcher takes action as if
30
+ :tries was set to 1 and :proc was set to lambda { }. However, Watcher
31
+ will throw an ArgumentError exception if :tries is specified, and not
32
+ a Fixnum with a value of at least 2.
33
+ * NB: The Proc will be invokes no more than (:tries - 1) times. This
34
+ is very different than Watcher 1.1.0, where the key was :retry, and
35
+ whose value determined the number of retries that would occur. In
36
+ the previous version, the number of times the Proc would be invoked
37
+ was equal to the :retry value. This is no longer the case.
38
+ * Indentation logic re-written.
39
+ * Log entries suppressed due to Watcher's verbosity setting are queued up.
40
+ If an exception is thrown, Watcher will replay those previously
41
+ suppressed log entries to allow a more detailed analysis of the cause of
42
+ the exception.
43
+ * Format of log output is nested in a hierarchical fashion, allowing
44
+ user to more easily trace the progression of tasks when trouble-shooting
45
+ program output via its logs.
46
+ * Unit tests written, although perhaps not yet entirely comprehensive.
47
+
48
+ * 3 minor enhancements:
49
+ * Allows either append, or overwrite mode for logs. Watcher can be
50
+ created with the :write parameter set to either :append or :overwrite.
51
+ The default is :overwrite.
52
+ * Watcher will ensure output stream is an actual file before it attempts to
53
+ send it via email. If the output stream is not a file, then it logs an
54
+ error message to the output stream.
55
+ * Eliminated use of continuations in preference to using the standard retry
56
+ statement.
57
+
1
58
  == 1.1.0 2008-07-22
2
59
 
3
60
  * 1 major enhancement:
data/Manifest.txt CHANGED
@@ -4,9 +4,10 @@ Manifest.txt
4
4
  PostInstall.txt
5
5
  README.txt
6
6
  Rakefile
7
- TODO
7
+ TODO.txt
8
8
  config/hoe.rb
9
9
  config/requirements.rb
10
+ lib/event.rb
10
11
  lib/watcher.rb
11
12
  lib/watcher/version.rb
12
13
  script/console
@@ -17,7 +18,10 @@ setup.rb
17
18
  tasks/deployment.rake
18
19
  tasks/environment.rake
19
20
  tasks/website.rake
20
- test/watcher-example.rb
21
+ test/tc_event.rb
22
+ test/test_helper.rb
23
+ test/test_watcher.rb
24
+ test/ts_package.rb
21
25
  website/index.html
22
26
  website/index.txt
23
27
  website/javascripts/rounded_corners_lite.inc.js
data/README.txt CHANGED
@@ -33,42 +33,44 @@ for the various actions taken.
33
33
 
34
34
  * Exception handling without the boiler-plate code.
35
35
  * Multiple tiers of verbosity.
36
- * Flexible logging capabilities, including to files, STDERR, email.
36
+ * Flexible logging capabilities, including to files, STDERR, and email.
37
37
  * Nested, and hierarchical log output for descriptive stack-frame level
38
38
  understanding where an exception was thrown.
39
39
  * Ability to change the time format of your logs by supplying an arbitrary
40
40
  Proc to return the time in the format of your choice.
41
- * Ability to customize the layout of the logs, by turning off hierarchical
42
- view, or changing Strings used to indicate events.
41
+ * Ability to customize the layout of the logs.
43
42
  * Email log files automatically, based on number of errors or warnings.
44
43
  * Ability to invoke arbitrary sequence of events if exception is thrown,
45
- including invocation of Proc objects.
44
+ by means of invocation of Proc object.
46
45
 
47
46
  == SYNOPSIS:
48
47
 
49
48
  # create Watcher instance with slightly modified defaults
50
- $watcher=Watcher.create(:output=>log_file, :verbosity=>:warn)
49
+ $watcher = Watcher.create(:output => log_file)
51
50
 
52
- $watcher.info("starting #{File.basename($0)}") do
51
+ $watcher.debug("starting #{File.basename($0)}") do
53
52
  # Look for CONFIG_PATH. If not found, log warning then re-write it
54
- fail_actions=[:log, lambda {write_config(config_path)}]
55
- $watcher.info("searching for [#{config_path}].", fail_actions) do
56
- if not File.exists?(config_path)
57
- raise RuntimeError.new("WARNING: Missing [#{config_path}]. Creating a default config file. (You should probably verify it...)")
58
- end
53
+ params = {:tries => 2, :failure => :error,
54
+ :proc => lambda { |e| write_config(config_path) }}
55
+ msg = "ensuring existance of config file [#{config_path}]"
56
+ $watcher.verbose(msg, params) do
57
+ raise "file not found" unless File.file?(File.expand_path(config_path))
59
58
  end
60
59
 
61
60
  # invoke utility, which uses $watcher as well
62
- fsc=FileSystemChecker.new(:config_path=>config_path)
63
- fsc.check_file_systems(:all=>all_flag, :remount=>remount_flag)
64
- if email_flag and $watcher.errors > 0
65
- $watcher.send_email(email,'FAIL: Required filesystems are not mounted')
66
- end
61
+ fsc = FileSystemChecker.new(:config_path => config_path)
62
+ fsc.check_file_systems(:all => all_flag, :remount => remount_flag)
63
+ end
64
+
65
+ if email_flag and ($watcher.errors > 0 or $watcher.warnings > 0)
66
+ $watcher.send_email(email, 'FAIL: Required filesystems are not mounted')
67
67
  end
68
68
 
69
69
  == REQUIREMENTS:
70
70
 
71
- * Passion for coding.
71
+ * hoe
72
+
73
+ * newgem
72
74
 
73
75
  == INSTALL:
74
76
 
data/TODO.txt ADDED
@@ -0,0 +1,33 @@
1
+ 1. CRITICAL
2
+
3
+ a. None.
4
+
5
+ 2. MINOR
6
+
7
+ a. Update source comments in watcher.rb and event.rb
8
+
9
+ b. Ensure 78 character max in source files.
10
+
11
+ c. Write some non-trivial example code. I have developed and ported
12
+ several non-trivial utilities from Logger to Watcher, and am extremely
13
+ happy with the results. Now let me get something ready to package
14
+ with this gem.
15
+
16
+ d. Trap signals to clean up properly.
17
+
18
+ e. Log rotation.
19
+
20
+ 3. LOW
21
+
22
+ a. Figure out how to get ri documentation working.
23
+
24
+ b. Provide a Watcher.destroy method which closes the Watcher instance and
25
+ changes the @@watcher class attribute back to nil.
26
+
27
+ c. Validate data when attributes accessors invoked.
28
+
29
+ d. Use a Ruby MTA, as opposed to the UNIX mail program.
30
+
31
+ e. As populate Event instance, remove k,v from Hash as assign, then
32
+ ensure fail_actions is empty to validate no extra invalid options
33
+ sent.
data/config/hoe.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'watcher/version'
2
2
 
3
3
  AUTHOR = 'Karrick McDermott' # can also be an array of Authors
4
- EMAIL = "karrick@karrick.net"
4
+ EMAIL = "karrick@karrick.org"
5
5
  DESCRIPTION = "Watcher provides advanced integrated exception handling and logging functionality to your Ruby programs."
6
6
  GEM_NAME = 'watcher' # what ppl will type to install your gem
7
7
  RUBYFORGE_PROJECT = 'watcher' # The unix name for your project
@@ -13,7 +13,7 @@ EXTRA_DEPENDENCIES = [
13
13
 
14
14
  @config_file = "~/.rubyforge/user-config.yml"
15
15
  @config = nil
16
- RUBYFORGE_USERNAME = "unknown" # Don't change this. Modify your "~/.rubyforge/user-config.yml" file.
16
+ RUBYFORGE_USERNAME = "unknown"
17
17
  def rubyforge_username
18
18
  unless @config
19
19
  begin
@@ -55,21 +55,21 @@ $hoe = Hoe.new(GEM_NAME, VERS) do |p|
55
55
  p.description = DESCRIPTION
56
56
  p.summary = DESCRIPTION
57
57
  p.url = HOMEPATH
58
- p.remote_rdoc_dir = '' # Release to root (Watcher is my only project)
59
- p.rsync_args << ' --exclude=statsvn/'
60
58
  p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
61
59
  p.test_globs = ["test/**/test_*.rb"]
62
60
  p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
61
+ # p.remote_rdoc_dir = '' # Release to root (Watcher is my only project)
62
+ # p.rsync_args << ' --exclude=statsvn/'
63
63
 
64
64
  # == Optional
65
65
  p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
66
66
  #p.extra_deps = EXTRA_DEPENDENCIES
67
67
 
68
- #p.spec_extras = {} # A hash of extra values to set in the gemspec.
69
- end
68
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
69
+ end
70
70
 
71
71
  CHANGES = $hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
72
72
  PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
73
73
  $hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
74
74
  $hoe.rsync_args = '-av --delete --ignore-errors'
75
- $hoe.spec.post_install_message = File.open(File.dirname(__FILE__) + "/../PostInstall.txt").read rescue ""
75
+ $hoe.spec.post_install_message = File.open(File.dirname(__FILE__) + "/../PostInstall.txt").read rescue ""
data/lib/event.rb ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/ruby -w
2
+ # -*- Ruby -*-
3
+ # Event
4
+ #
5
+ # Summary:: Event instance handles exception recovery, proper hierarchical
6
+ # format String for the log based on its level in the call-stack.
7
+ # Author:: Karrick McDermott (karrick@karrick.org)
8
+ # Date:: 2008-07-22
9
+ # Copyright:: Copyright (c) 2008 by Karrick McDermott. All rights reserved.
10
+ # License:: Simplified BSD License.
11
+
12
+ ######################################################################
13
+
14
+ # Add this directory to front of require path.
15
+ $:.unshift(File.dirname(__FILE__)) unless
16
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
17
+
18
+ ######################################################################
19
+ # An Event instance tracks the title of the task, and the required indentation
20
+ # to display the task in the log. This is required to store an Array of
21
+ # events that took place--but below the verbosity threshhold--to replay them
22
+ # into the log at the appropriate indention level should an exception be
23
+ # thrown.
24
+ class Event
25
+ ##### Publically read-write attributes
26
+
27
+ ##### Publically read-only attributes
28
+ attr_reader :failure # what to do if all tries fail (:warn | :error)
29
+ attr_reader :hier # shows nested levels
30
+ attr_reader :proc # Proc to call before a retry
31
+ attr_reader :tries # number of tries remaining
32
+ attr_reader :title # event title
33
+
34
+ # Get an Event which is the child of this event
35
+ def child
36
+ self.dup.child!
37
+ end
38
+
39
+ # Make this event into its child
40
+ def child!
41
+ if @hier.count('.') % 2 == 0 # if even number of periods
42
+ @hier << '.a'
43
+ else
44
+ @hier << '.0'
45
+ end
46
+ self
47
+ end
48
+
49
+ # Get copy of Event hier to the next task at the same level of the call-stack
50
+ def sibling
51
+ self.dup.sibling!
52
+ end
53
+
54
+ # Take this Event hier to the next task at the same level of the call-stack
55
+ def sibling!
56
+ # Now change the last hier to its successor
57
+ @hier.sub!(/[^.]+$/) { $&.succ }
58
+ self
59
+ end
60
+
61
+ ########## Return String ready for log
62
+ def to_s
63
+ @time + ': ' + @hier + ' ### ' + @title
64
+ end
65
+
66
+ ########################################
67
+ private
68
+ ########################################
69
+
70
+ # Create a new Event instance with the specified task title, and an optional
71
+ # prefix for parent event id (used for log replay)
72
+ def initialize(time, hier, title, fail_actions, relationship)
73
+ @hier = hier
74
+ @time = time
75
+ @title = title
76
+
77
+ # validate title
78
+ unless title.respond_to? :to_s
79
+ # Defensive programming against Watcher's caller:
80
+ msg = "Event.initialize: The first argument (title) must respond to" \
81
+ + " the :to_s method. Your argument class is #{title.class}." \
82
+ + " Aborting task."
83
+ raise ArgumentError.new(msg)
84
+ end
85
+
86
+ # validate time
87
+ unless time.respond_to? :to_s
88
+ # Defensive programming against Watcher's caller:
89
+ msg = "Event.initialize: The second argument (time) must respond to" \
90
+ + " the :to_s method. Your argument class is #{time.class}." \
91
+ + " Aborting [#{title}]."
92
+ raise ArgumentError.new(msg)
93
+ end
94
+
95
+ # validate fail actions
96
+ if fail_actions.kind_of? Hash
97
+ # validate fail_actions (throws ArgumentError if bad; let caller catch)
98
+ @proc = fail_actions[:proc]
99
+ @tries = fail_actions[:tries]
100
+ @failure = fail_actions[:failure]
101
+ validate_fail_actions
102
+ else
103
+ # Defensive programming against Watcher's caller:
104
+ msg = "Event.initialize: The fourth argument (fail_actions) must be" \
105
+ + " a Hash, not a #{fail_actions.class}. Aborting [#{title}]."
106
+ raise ArgumentError.new(msg)
107
+ end
108
+
109
+ # validate hier argument
110
+ if hier == nil
111
+ # If no hier event, then this Event has no parent or siblings.
112
+ @hier = '0'
113
+ elsif not hier.kind_of? String
114
+ # Defensive programming against Watcher.monitor
115
+ msg = "Event.initialize: sixth argument (hier) must be either nil," \
116
+ + " or of non-zero length String, not a #{hier.class}. Aborting [#{title}]."
117
+ raise ArgumentError.new(msg)
118
+ elsif hier == ''
119
+ # Defensive programming against Watcher.monitor
120
+ msg = "Event.initialize: sixth argument (hier) must be either nil," \
121
+ + " or of non-zero length String. Aborting [#{title}]."
122
+ raise ArgumentError.new(msg)
123
+ else # this event is related to hier
124
+ case relationship
125
+ when :child
126
+ @hier = hier
127
+ child!
128
+ when :sibling
129
+ @hier = hier
130
+ sibling!
131
+ else
132
+ msg = "Event.initialize: fifth argument (relationship) must be one" \
133
+ + " of either :child or :sibling"
134
+ raise ArgumentError.new(msg)
135
+ end
136
+ end
137
+ end
138
+
139
+ # Deprecated, but worked fine at the time it was taken out.
140
+ # # Take this Event hier to back up one hier from the call-stack
141
+ # def dec
142
+ # # knock off the last segment
143
+ # @hier.sub!(/\.?[^.]+$/, '')
144
+ # # call sibling to increment parent hier
145
+ # sibling!
146
+ # end
147
+
148
+ # Validate fail_actions parameter (throws ArgumentError if bad)
149
+ # * Must include either :warn or :error, but not both in Hash.
150
+ # * If :tries is specified, it must be a Fixnum, and then :proc must also
151
+ # be specified, and a valid Proc instance.
152
+ # * If :tries is not specified, then :proc may not be specified.
153
+ def validate_fail_actions
154
+ # Either :warn or :error, not both, and not neither.
155
+ if @failure != :warn and @failure != :error
156
+ msg = "Event.validate_fail_actions: Must specify either :warn or :error"
157
+ raise ArgumentError.new(msg)
158
+ end
159
+
160
+ # If :tries is set, then it must be a Fixnum
161
+ if @tries != nil
162
+ if @tries.class != Fixnum or @tries < 2
163
+ raise ArgumentError.new(':tries must be at least 2 in order to specify a Proc')
164
+ else # must have a valid Proc
165
+ if @proc.class != Proc
166
+ raise ArgumentError.new('must specify a Proc instance when :tries is set')
167
+ end
168
+ end
169
+ else # tries is nil
170
+ # Ensure :proc not set
171
+ if @proc != nil
172
+ raise ArgumentError.new(':tries must be at least 2 in order to specify a Proc')
173
+ end
174
+ @tries = 1 # just one try since :proc was nil
175
+ end
176
+ end
177
+ end
data/lib/watcher.rb CHANGED
@@ -9,50 +9,46 @@
9
9
  # Copyright:: Copyright (c) 2008 by Karrick McDermott. All rights reserved.
10
10
  # License:: Simplified BSD License.
11
11
 
12
+ ######################################################################
13
+
14
+ # add this directory to front of require path
15
+ $:.unshift(File.dirname(__FILE__)) unless
16
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
17
+
18
+ # Co-located in same directory as watcher.
19
+ require 'event'
20
+
12
21
  ######################################################################
13
22
  class Watcher
14
- VERSION = '1.1.0'
23
+ # Array of actions to perform when an Exception is thrown by code in the
24
+ # yield block.
25
+ # * The Watcher instance maintains a default @default_fail Hash for use when
26
+ # a given invocation does not specify a different set of actions to
27
+ # perform.
28
+ # * The default value is a Hash with one key, the value of which causes
29
+ # Watcher to re-raise the exception if one should occur.
30
+ attr_accessor :default_fail
31
+
32
+ # A String to include inside the log entry when an Exception is thrown.
33
+ # * This may be required in some circumstances where a separate log
34
+ # monitoring program is scanning log files looking for a string such as,
35
+ # ERROR or FAILURE.
36
+ # * The default value is nil.
37
+ attr_accessor :error_symbol
38
+
15
39
  # Tracks the number of times an Exception has invoked the :raise fail
16
40
  # action.
17
41
  # * Fail actions to log an error (:log), invoke a Proc (lambda {...}), and
18
42
  # retry (:retry) are ignored because flow of the program is allowed to
19
43
  # continue for each of those, whereas a :raise will abort processing of
20
44
  # subsequent tasks until caught.
21
- # * If a task raises an Exception, but the task is enclosed in another task
45
+ # * If a title raises an Exception, but the title is enclosed in another title
22
46
  # that merely logs the Exception, then the @errors attribute is still
23
47
  # incremented. This is useful for signalling when a warning condition
24
48
  # occured, but the program is able to continue the processing of
25
49
  # subsequent tasks.
26
50
  attr_reader :errors
27
51
 
28
- # Tracks the number of times an Exception has been raised, regardless of
29
- # whether the Exception was re-thrown.
30
- attr_reader :warnings
31
-
32
- # Array of actions to perform when an Exception is thrown by code in the
33
- # yield block.
34
- # * The Watcher instance maintains a default @fail_actions Array for use
35
- # when a given invocation does not specify a different set of actions to
36
- # perform.
37
- # * The default value is an Array of two actions, [:log, :raise], which
38
- # notes the error in the logs, and re-throws the Exception.
39
- attr_accessor :fail_actions
40
-
41
- # A String to include inside the log entry when an Exception is thrown.
42
- # * This may be required in some circumstances where a separate log
43
- # monitoring program is scanning log files looking for a string such as,
44
- # ERROR or FAILURE.
45
- # * The default value is nil.
46
- attr_accessor :fail_symbol
47
-
48
- # The number of spaces to indent a child level task under its parent task.
49
- # * A Boolean value of false or an Integer value of 0 turns off indentation.
50
- # * Any other value will set how many characters will be used for indenting
51
- # the logs.
52
- # * This may only be set at the time the Watcher object is created.
53
- # * The default value is 3.
54
- attr_reader :indent
55
-
56
52
  # A String of characters to use when joining multiple error lines into a
57
53
  # single line for output to the log.
58
54
  # * If nil, the Exception message is reported verbatim. If not nil, the
@@ -61,11 +57,7 @@ class Watcher
61
57
  # * The default value is ' (LF) '.
62
58
  attr_accessor :merge_newlines
63
59
 
64
- # Determines whether Watcher will display the task completion log events.
65
- # * The default value is false.
66
- attr_accessor :show_completion
67
-
68
- # Holds a Proc object assigned the task of formatting the system time to a
60
+ # Holds a Proc object assigned the title of formatting the system time to a
69
61
  # String for display.
70
62
  # * You can easily change how the times are displayed by passing a Proc
71
63
  # object or a block to this attribute.
@@ -78,28 +70,27 @@ class Watcher
78
70
  attr_accessor :time_formatter
79
71
 
80
72
  # Determines level of reporting that Watcher object sends to the log file.
81
- # * Possible values are the Symbols :debug, :info, :warn, :error, and
73
+ # * Possible values are the Symbols :debug, :verbose, :warn, :error, and
82
74
  # :fatal.
83
- # * The default value is :info.
75
+ # * The default value is :warn.
84
76
  attr_accessor :verbosity
85
77
 
86
78
  # Provide ability for Watcher to prioritize levels of verbosity.
87
79
  # * It wasn't designed to be directly used by client code, but I suppose it
88
80
  # could be modified by consumer code in this script if the situation
89
81
  # warranted.
90
- @@verbosity_levels = {:debug=>0, :info=>1, :warn=>2, :error=>3, :fatal=>4}
91
-
92
- # Stores the Strings used by Watcher to use when writting events to the log
93
- # files.
94
- # * Provided as a means to customize the output, say for language
95
- # localization purposes.
96
- # * <b>Warning</b>, if you substitute your own Hash for @verbosity_strings
97
- # that does not have keys for the five levels of verbosity used by
98
- # Watcher, you may cause Watcher to start throwing exceptions with it
99
- # attempts to coerce the NilClass into a String.
100
- # * The default value of @verbosity_strings is a Hash that maps the
101
- # verbosity levels to a reasonable String value.
102
- attr_accessor :verbosity_strings
82
+ @@verbosity_levels = {:debug => 0, :verbose => 1, :always => 2}
83
+
84
+ # A String to include inside the log entry when an Exception is thrown.
85
+ # * This may be required in some circumstances where a separate log
86
+ # monitoring program is scanning log files looking for a string such as,
87
+ # ERROR or FAILURE.
88
+ # * The default value is nil.
89
+ attr_accessor :warn_symbol
90
+
91
+ # Tracks the number of times an Exception has been raised, regardless of
92
+ # whether the Exception was re-thrown.
93
+ attr_reader :warnings
103
94
 
104
95
  # We override the default accessibility for the :new method because we want
105
96
  # to ensure there is only one Watcher object created.
@@ -110,10 +101,11 @@ class Watcher
110
101
  # * You cannot use the #new method directly as you can with regular classes.
111
102
  # * Repeated calls to #Watcher.create simply return the same Watcher object.
112
103
  # * When a Watcher object is created, the default class initialization
113
- # values are usually perfectly suitable for the task at hand. This is
104
+ # values are usually perfectly suitable for the title at hand. This is
114
105
  # more-so true when just starting on a new project, when the default is to
115
- # send logging information to STDERR, which is usually what you want when
116
- # developing a program using the interactive method, such as with irb.
106
+ # send logging information to STDOUT, which is usually what you want when
107
+ # developing a program using the interactive method, such as with irb,
108
+ # or when running a process from the command shell.
117
109
  # * It is easy to change any of the class attributes either at object
118
110
  # instantiation, or while the program is being executed. During
119
111
  # instantiation simply create a Hash object with a key Symbol for every
@@ -121,28 +113,28 @@ class Watcher
121
113
  # instance, to create a globabl variable, aptly named <tt>$watcher</tt>:
122
114
  #
123
115
  # <tt>
124
- # $watcher = Watcher.create({:fail_actions=>:warn, :verbosity=>:info})
116
+ # $watcher = Watcher.create(:default_fail=>:warn, :verbosity=>:warn)
125
117
  # </tt>
126
118
  #
127
119
  # * Each Watcher object instance stores an IO object for buffering data to
128
120
  # the output log. This can either be a regular file, STDERR, STDOUT, or
129
- # perhaps a FIFO or a pipe. If you do not want to use STDERR or a
121
+ # perhaps a FIFO or a pipe. If you do not want to use STDOUT or a
130
122
  # regular file, simply make the connection, and pass it to your Watcher
131
123
  # instance.
132
- # * The default value of @output is an IO instance of STDERR.
124
+ # * The default value of @output is an IO instance of STDOUT.
133
125
  def Watcher.create(attributes={})
134
126
  @@watcher ||= new(attributes)
135
127
  end
136
128
 
137
- # Used to identify a low-level task during development.
129
+ # Used to identify a low-level title during development.
138
130
  # * This is most frequently invoked without a block, in which case it simply
139
- # appends the task start and stop messages to the log if the verbosity
131
+ # appends the title start and stop messages to the log if the verbosity
140
132
  # level is at :debug.
141
133
  # * Use this when in lieu of a group of 'puts' statements littered through
142
134
  # your source code.
143
135
  # * See detailed decription for #monitor for how this method works.
144
- def debug(task, fail_actions=@fail_actions)
145
- monitor(task,:debug,block_given?,fail_actions) { yield }
136
+ def debug(title, event_fail=@default_fail)
137
+ monitor(title, :debug, block_given?, event_fail) { yield }
146
138
  end
147
139
 
148
140
  # Usually the method of choice for your general logging purpose
@@ -152,24 +144,30 @@ class Watcher
152
144
  # * I recommend using this for every level of program logic that you desire
153
145
  # to track.
154
146
  # * See detailed decription for #monitor for how this method works.
155
- def info(task, fail_actions=@fail_actions)
156
- monitor(task,:info,block_given?,fail_actions) { yield }
147
+ def verbose(title, event_fail=@default_fail)
148
+ monitor(title, :verbose, block_given?, event_fail) { yield }
149
+ end
150
+
151
+ # Use this sparingly. (User specifies this verbosity level so your program
152
+ # will not show anything, unless there is an error.)
153
+ def always(title, event_fail=@default_fail)
154
+ monitor(title, :always, block_given?, event_fail) { yield }
157
155
  end
158
156
 
159
157
  # Each Watcher object instance can optionally send a copy of its
160
158
  # log file to a given email account.
161
159
  # * You might want to call this if there were any errors while completing
162
160
  # your tasks.
161
+ # * NOTE: It is the responsibility of the calling routine to send the logs
162
+ # to a file if you intend to send the email.
163
+ # $watcher = Watcher.create(:output=>log_file, ...)
163
164
  def send_email(email,subject)
164
165
  @output.flush # flush the output buffer
165
- system "mail -s '#{subject}' #{email} < #{@output_path}"
166
-
167
- # reopen the output buffer for append mode
168
- @output = File.open(@output_path, 'a')
169
- unless @output # Redirect to STDERR if open failed.
170
- @output = STDERR
171
- @debug_out << "could not open [#{output}]. " + e.message + "\n" \
172
- if WATCHER_DEBUG
166
+ if File.file?(@output_path)
167
+ system "mail -s '#{subject}' #{email} < #{@output_path}"
168
+ else
169
+ msg = "Cannot send log file as email because no log file was created"
170
+ raise ArgumentError.new(msg)
173
171
  end
174
172
  end
175
173
 
@@ -187,8 +185,8 @@ class Watcher
187
185
  if WATCHER_DEBUG
188
186
  # Open our dump file, and let Ruby environment close it when we exit.
189
187
  begin
190
- # mode 'w' truncates file if not empty.
191
- @debug_out = File.open(WATCHER_DEBUG_OUTPUT_FILENAME,'w')
188
+ # mode 'a' appends to file, and 'w' truncates file if not empty.
189
+ @debug_out = File.open(WATCHER_DEBUG_OUTPUT_FILENAME,'a')
192
190
  rescue Exception => e
193
191
  # if we couldn't open it, just send debugging data to STDERR,
194
192
  # and insert a message indicating the failure.
@@ -196,14 +194,18 @@ class Watcher
196
194
  @debug_out << "could not open [#{WATCHER_DEBUG_OUTPUT_FILENAME}]. " \
197
195
  + e.message + "\n"
198
196
  end
197
+ @debug_out << "****************************************\n"
198
+ @debug_out << Time.now.to_s + "* starting " + File.basename($0) + "\n"
199
+ @debug_out.flush unless @debug_out == STDERR or @debug_out == STDOUT
199
200
  end
200
201
 
201
202
  # Watcher debugging facility
202
203
  if WATCHER_DEBUG and WATCHER_DEBUG_ATTRIBUTES
203
204
  @debug_out << "=== Explicitly requested attributes: \n"
204
205
  attributes.keys.each do |k|
205
- @debug_out << "\t:#{k}=>#{attributes[k].inspect}\n"
206
+ @debug_out << "\t:#{k} => #{attributes[k].inspect}\n"
206
207
  end
208
+ @debug_out.flush unless @debug_out == STDERR or @debug_out == STDOUT
207
209
  end
208
210
 
209
211
  # Here we define the default values for a newly created Watcher object.
@@ -213,168 +215,122 @@ class Watcher
213
215
  # * Any keys provided by the user simply cause the associated key
214
216
  # values provided by the consumer code to overwrite the values
215
217
  # provided in the literal Hash below. Isn't Ruby fun?
216
- attributes.delete(:output) if attributes[:output] == nil
217
- attributes={:fail_actions=>[:log,:raise], :fail_symbol=>nil, :indent=>3,
218
- :merge_newlines=>' (LF) ', :output=>STDERR, :show_completion=>false,
219
- :time_formatter=>lambda { Time.now.to_s }, :verbosity=>:info,
220
- :verbosity_strings=>{:debug=>' DEBUG:',:info=>' INFO :',
221
- :warn=>' WARN :',:error=>' ERROR:',
222
- :fatal=>' FATAL:'}
218
+ # attributes.delete(:output) if attributes[:output] == nil
219
+ attributes.delete(:output_path) if attributes[:output_path] == nil
220
+ attributes={
221
+ :default_fail => {:failure => :error},
222
+ :error_symbol => nil, :warn_symbol => nil,
223
+ :merge_newlines => ' (LF) ', :output_path => STDOUT,
224
+ :time_formatter => lambda { Time.now.to_s },
225
+ :verbosity => :always,
226
+ :write => :overwrite
223
227
  }.merge(attributes)
224
228
 
225
229
  # Watcher debugging facility
226
230
  if WATCHER_DEBUG and WATCHER_DEBUG_ATTRIBUTES
227
231
  @debug_out << "=== Merged attributes: \n" << "\n"
228
232
  attributes.keys.each do |k|
229
- @debug_out << "\t:#{k}=>#{attributes[k].inspect}\n"
233
+ @debug_out << "\t:#{k} => #{attributes[k].inspect}\n"
230
234
  end
235
+ @debug_out.flush unless @debug_out == STDERR or @debug_out == STDOUT
231
236
  end
232
237
 
238
+ # initialize public attributes
239
+ @errors = 0
240
+ @warnings = 0
241
+
242
+ # initialize private attributes
243
+ @last_hier = nil # no last events at start
244
+ @event_relationship = :child # first event will be a child of...nil
245
+
246
+ # Array of Event instances for working with nested levels.
247
+ @events = Array.new
248
+
233
249
  # Here the product of the merges Hashes is used to populate the
234
250
  # instantiated Watcher object attributes.
235
251
  # Nothing to see here, move on... Unless you want to improve
236
252
  # Watcher...
237
- @current_indent=0
238
- @errors=0
239
- @warnings=0
240
- @fail_actions=attributes[:fail_actions]
241
- @fail_symbol=attributes[:fail_symbol]
242
- @indent=attributes[:indent]
243
- @merge_newlines=attributes[:merge_newlines]
244
- @output=attributes[:output]
245
- @show_completion=attributes[:show_completion]
246
- @time_formatter=attributes[:time_formatter]
247
- @verbosity=attributes[:verbosity]
248
- @verbosity_strings=attributes[:verbosity_strings]
253
+ @default_fail = attributes[:default_fail]
254
+ @error_symbol = attributes[:error_symbol]
255
+ @warn_symbol = attributes[:warn_symbol]
256
+ @merge_newlines = attributes[:merge_newlines]
257
+ @output_path = attributes[:output_path]
258
+ @time_formatter = attributes[:time_formatter]
259
+ @verbosity = attributes[:verbosity]
260
+ @write_mode = attributes[:write]
261
+
262
+ # after the attribute merge, perform sanity checks
263
+ if @time_formatter.class != Proc
264
+ raise ArgumentError(":time_formatter must be a Proc instance")
265
+ end
266
+
267
+ # make sure verbosity makes sense
268
+ unless @@verbosity_levels.include? @verbosity
269
+ msg = ":verbosity must be one of ["
270
+ @@verbosity_levels.keys.each { |k| msg << k.inspect << ' ' }
271
+ msg << ']'
272
+ raise ArgumentError.new(msg)
273
+ end
274
+
275
+ # verify file output mode
276
+ @write_mode = case @write_mode
277
+ when :append
278
+ 'a'
279
+ when :overwrite
280
+ 'w'
281
+ else
282
+ msg = "Watcher.initialize: :write must be one of either :overwrite" \
283
+ + " or :append, not #{@write_mode.inspect}."
284
+ raise ArgumentError.new(msg)
285
+ end
249
286
 
250
287
  # Watcher debugging facility
251
288
  if WATCHER_DEBUG and WATCHER_DEBUG_ATTRIBUTES
252
289
  @debug_out << "=== Instance attributes: \n" << "\n"
253
- @debug_out << " @current_indent\t\t=#{@current_indent}" << "\n"
254
- @debug_out << " @fail_actions\t\t=#{@fail_actions.inspect}" << "\n"
255
- @debug_out << " @fail_symbol\t\t=\"#{@fail_symbol}\"" << "\n"
256
- @debug_out << " @indent\t\t\t=#{@indent}" << "\n"
290
+ @debug_out << " @default_fail\t\t=#{@default_fail.inspect}" << "\n"
291
+ @debug_out << " @error_symbol\t\t=\"#{@error_symbol}\"" << "\n"
292
+ @debug_out << " @warn_symbol\t\t=\"#{@warn_symbol}\"" << "\n"
257
293
  @debug_out << " @merge_newlines\t\t=#{@merge_newlines}" << "\n"
258
- @debug_out << " @output\t\t\t=#{@output}" << "\n"
259
- @debug_out << " @show_completion\t\t=#{@show_completion}" << "\n"
294
+ @debug_out << " @output_path\t\t\t=#{@output_path}" << "\n"
260
295
  @debug_out << " @time_formatter\t\t=#{@time_formatter}" << "\n"
261
296
  @debug_out << " @verbosity\t\t\t=:#{@verbosity}" << "\n"
262
- @debug_out << " @verbosity_strings\t\t=" \
263
- + "#{@verbosity_strings.inspect}\n"
297
+ @debug_out.flush unless @debug_out == STDERR or @debug_out == STDOUT
264
298
  end
265
299
 
266
300
  # Open the log file if not STDERR nor STDOUR
267
- if @output != STDERR and @output != STDOUT
268
- @output_path = File.expand_path(@output)
269
- # mode 'w' truncates file if not empty.
270
- @output = File.open(@output_path, 'w')
271
- unless @output # Redirect to STDERR if open failed.
272
- @output = STDERR
273
- @debug_out << "could not open [#{output}]. " + e.message + "\n" \
274
- if WATCHER_DEBUG
301
+ if @output_path != STDERR and @output_path != STDOUT
302
+ @output_path = File.expand_path(@output_path)
303
+ @output = File.open(@output_path, @write_mode)
304
+ unless @output # Redirect to STDOUT if open failed.
305
+ @output = STDOUT
306
+ @debug_out << "could not open [#{output_path}]. " + e.message \
307
+ + "\n" if WATCHER_DEBUG
275
308
  end
309
+ else
310
+ @output = @output_path
276
311
  end
277
312
  end # initialize
278
313
 
279
- # Returns a String representing the indentation characters to use
280
- # for the event.
281
- # * A new event increments the indentation level.
282
- # * A done event and an error event decrements the indentation level.
283
- # * A nil event represents a note, with no potential child-tasks. (No
284
- # indentation change is required.)
285
- def get_log_indentation_string(event=nil)
286
- if @indent.class != Fixnum # don't indent unless numberical value
287
- case event
288
- when nil then result = '='
289
- when :new then result = '>'
290
- when :done then result = '<'
291
- when :error then result = '*'
292
- else
293
- result = '='
294
- @debug_out << " =WD= Invalid event Symbol = #{event.inspect}\n" \
295
- if WATCHER_DEBUG
296
- end
297
- else # we got a Fixnum, so use indented log output
298
- result = ' ' # plan to place at least one space after the time mark
299
- # If WATCHER_DEBUG, we want to use periods instead of spaces to indent
300
- space = '.'
301
- unless WATCHER_DEBUG
302
- # create some run-time protection from bad input
303
- # TODO: Validate data when attributes accessors invoked
304
- @current_indent = 0 if @current_indent < 0
305
- @indent = 3 if @indent < 3
306
- space = ' ' # if not debugging Watcher, go ahead and use spaces
307
- end
308
-
309
- #--
310
- # If @indent is >= 3, arrows are created at each indent level to show
311
- # the parent-child relationship between tasks.
312
- # * head refers to the head of a log event arrow, and
313
- # * tail refers to the tail of a log event arrow.
314
- #++
315
- case event
316
- when nil
317
- # same as :new but do not increment the level at the end
318
- @current_indent.times do
319
- result += space * (@indent-1)
320
- end
321
- result += 'I' # tail
322
- result += '=' * (@indent-2)
323
- result += '>' # head
324
- when :new
325
- # increate indent after make this String
326
- @current_indent.times do
327
- result += space * (@indent-1)
328
- end
329
- result += 'N' # tail
330
- result += '-' * (@indent-2)
331
- result += '>' # head
332
- @current_indent += 1
333
- when :done
334
- # decrease indent before make this String
335
- @current_indent -= 1
336
- @current_indent.times do
337
- result += space * (@indent-1)
338
- end
339
- result += 'C' # head
340
- result += '-' * (@indent-1)
341
- when :error
342
- # decrease indent before make this String
343
- @current_indent -= 1
344
- @current_indent.times do
345
- result += space * (@indent-1)
346
- end
347
- result += 'E' # head
348
- result += '-' * (@indent-2)
349
- else
350
- # invalid event code invoked from within Watcher.monitor
351
- @debug_out << " =WD= Invalid event Symbol = #{event.inspect}\n" \
352
- if WATCHER_DEBUG
353
- end
354
- end
355
- result
356
- end
357
-
358
314
  # Get the time String, but protect against if plug-in Proc causes an
359
315
  # Exception.
360
316
  def get_protected_log_time_string
361
- begin
317
+ @time = begin
362
318
  @time_formatter.call
363
319
  rescue
364
- Time.now.utc.to_s + " == WARNING: Time Formatter raised Exception! == "
320
+ Time.now.to_s + " (WARNING! :time_formatter Proc raised #{$!.class} exception) "
365
321
  end
366
322
  end
367
323
 
368
- # When either #debug or #info is invoked, it ends up passing the
324
+ # When either #debug or #verbose is invoked, it ends up passing the
369
325
  # batton to the private #monitor method.
370
326
  # * #monitor first sets up some local variables, including setting up a
371
- # variable to store the effective fail_actions for reasons decribed below,
372
- # then optionally makes a log entry concerning the task you desire to
327
+ # variable to store the effective default_fail for reasons decribed below,
328
+ # then optionally makes a log entry concerning the title you desire to
373
329
  # execute. This log entry only takes place if the verbosity level of the
374
- # task is equal to or exceeds the Watcher object's stored verbosity
330
+ # title is equal to or exceeds the Watcher object's stored verbosity
375
331
  # minimum level, @verbosity.
376
332
  # * After preparations are complete, Watcher checks whether you sent the
377
- # #debug or #info method a block to execute. If you did not send a block,
333
+ # #debug or #verbose method a block to execute. If you did not send a block,
378
334
  # Watcher simply returns control to your code at the point immediately
379
335
  # following where you invoked Watcher.
380
336
  # * If you did pass a block to execute, Watcher sets up a
@@ -382,215 +338,188 @@ class Watcher
382
338
  # block inside that protective environment.
383
339
  # * If code invoked by that block triggers an Exception, Watcher passes
384
340
  # control to the #protect method, which orchestrates a recovery path based
385
- # on the consumer provided fail_actions Array. If the #debug or #info
341
+ # on the consumer provided default_fail Array. If the #debug or #verbose
386
342
  # methods were not given a specific set of actions to perform if an
387
343
  # Exception occurs, then the Watcher object uses its object instance
388
- # attribute @fail_actions to provide the list of actions to complete. For
344
+ # attribute @event_fail to provide the list of actions to complete. For
389
345
  # information how this works, see the documentation for the #protect
390
346
  # method.
391
- def monitor(task,run_level,block_was_given,fail_actions) #:doc:
392
- # Watcher debugging facility
393
- if WATCHER_DEBUG and WATCHER_DEBUG_FAILACTIONS
394
- @debug_out << "fail_actions = #{fail_actions.inspect}\n"
347
+ def monitor(title, run_level, block_was_given, event_fail) #:doc:
348
+ # Why have @events, an Array of events?
349
+ # * When new title is started, if its title level meets or exceeds the
350
+ # Watcher verbosity level, the @events Array is cleared and the event
351
+ # is sent to the log. The @events Array must be cleared because
352
+ # preceeding indentation will not otherwise line up.
353
+ # * When new title is started, if its title level is lower than the Watcher
354
+ # verbosity level, its event Hash is appended to the @events Array for
355
+ # later log replay if exception is thrown.
356
+ # * When title is successfully completed, its matching event Hash is
357
+ # located (from the right) of the @events Hash, and it and all following
358
+ # events are removed from the @events Array.
359
+ # * When title is unsuccessfully completed, a event replay is performed and
360
+ # all events in the @events Hash are sent to the logs regardless of
361
+ # their event level. This way an understandable stack-frame replay is
362
+ # possible to determine the actual cause of the exception.
363
+
364
+ if WATCHER_DEBUG and WATCHER_DEBUG_FAIL_ACTIONS
365
+ @output.puts "WATCHER MONITOR -- FAIL_ACTIONS for [#{title}]"
366
+ event_fail.each { |k,v| @output << " #{k.inspect} == #{v.inspect}\n" }
395
367
  end
396
368
 
397
- # assume no exception for this task
398
- success_flag = true
369
+ # Validate input; abort if input parameters are not clean.
370
+ # (@time, @last_hier, @title, @fail_actions, @relationship)
371
+ event = Event.new(get_protected_log_time_string, @last_hier, title, \
372
+ event_fail, @event_relationship)
399
373
 
400
- # validate input upon initial invocation, not when handling Exception...
401
- # abort task if input parameters are not clean
402
- if not task.respond_to?(:to_s)
403
- @output << get_protected_log_time_string
404
- @output << "WATCHER: the 'task' must be printable!\n"
405
- return # abort task
406
- elsif not @@verbosity_levels.has_key?(run_level)
407
- @output << get_protected_log_time_string + "WATCHER: The given " \
408
- + "run_level, #{runlevel.inspect}, is not a member of " \
409
- + "Watcher::@@verbosity_levels. Task = [#{task}]\n"
410
- return # abort task
411
- else
412
- # always validate the requested fail actions by helper method
413
- protect(task, fail_actions) # e=nil, therefore just validate
374
+ # Validate input; abort if input parameters are not clean.
375
+ unless @@verbosity_levels.has_key? run_level
376
+ # Defensive programming
377
+ msg = "Watcher.monitor: The second argument (run_level) must be one" \
378
+ + ' of ' + @@verbosity_levels.inspect + ". Your argument was" \
379
+ + run_level.inspect + ". Aborting [#{title}]"
380
+ raise ArgumentError.new(msg)
414
381
  end
415
382
 
416
- # Watcher debugging facility
417
- if WATCHER_DEBUG and WATCHER_DEBUG_LEVELS
418
- @debug_out << get_protected_log_time_string \
419
- + " =WD= task level = :#{run_level}, " \
420
- + "verbosity level = :#{@verbosity}\n"
383
+ # Either append this event to @events Array, or send it to the log
384
+ if @@verbosity_levels[run_level] < @@verbosity_levels[@verbosity]
385
+ # This event is lower than our minimum verbosity, so store it in our
386
+ # Array for replay if needed later.
387
+ @events << event
388
+ @debug_out << "* HIDDEN: #{event}\n" if WATCHER_DEBUG and WATCHER_DEBUG_VERBOSITY
389
+ else
390
+ # forget previous replays since this gets printed
391
+ @debug_out << "* VISIBLE:#{event.to_s} (*) clearing previous #{@events.size} events from array\n" if WATCHER_DEBUG and WATCHER_DEBUG_VERBOSITY
392
+ @events = Array.new
393
+
394
+ # NOTE: event.to_s retuns time-stamp then its event hier
395
+ @output << event.to_s + "\n"
396
+ @output.flush # Do this always, not just 'if WATCHER_DEBUG'
421
397
  end
398
+
399
+ # If event has a block, protect and execute the block
400
+ if block_was_given # block_given? from #debug or #verbose
401
+ if WATCHER_DEBUG and WATCHER_DEBUG_OUTPUT and false
402
+ @output << " +monitor_block(#{event.title})"
403
+ end
404
+ monitor_block(event) { yield }
405
+ end # block given
422
406
 
423
- # log the event if the task run level >= verbosity
424
- if @@verbosity_levels[run_level] >= @@verbosity_levels[@verbosity]
425
- output = get_protected_log_time_string
426
- # if no new block given, then task event = nil, (don't indent)
427
- output << get_log_indentation_string(block_was_given ? :new : nil)
428
- output << task << "\n"
429
- @debug_out << output if WATCHER_DEBUG and WATCHER_DEBUG_OUTPUT
430
- @output << output
407
+ # Once this Event is complete, make the next event a sibling of this event
408
+ @event_relationship = :sibling
409
+ @last_hier = event.hier.dup
410
+
411
+ if WATCHER_DEBUG and WATCHER_DEBUG_VERBOSITY
412
+ @debug_out << "(L) @last_hier = " + @last_hier + "\n"
413
+ @debug_out.flush
431
414
  end
415
+ end
416
+
417
+ def monitor_block (event)
418
+ # During execution of the parameterized block, the following events must
419
+ # be children events of this event. After the block completes, following
420
+ # events are sibling events of this event.
421
+ @event_relationship = :child
422
+ @last_hier = event.hier.dup
423
+
424
+ # Event knows its own max tries value
425
+ tries_remaining = event.tries
432
426
 
433
- # okay, preparations complete, if a block was given, do it!
434
- if block_was_given # block_given? from #debug or #info
435
- begin
436
- # Create a Continuation object. A :retry fail action will come back
437
- # to this point in code. A classic "begin, rescue, retry" does not
438
- # work, because the retry would occur from an invoked method, protect.
439
- callcc{|@continuation|}
440
- yield
441
- # Log the event if the task run level is greater or equal to the
442
- # verbosity
443
- # * The task complete log entry only takes place after your block
444
- # returns.
445
- # * If you did not send a task block to complete, Watcher will
446
- # conditionally log the event based on the verbosity level, then
447
- # exit back to your code just after you invoked Watcher.
448
- # * PROGRAMMER NOTE: I moved the code from the ensure block to just
449
- # after the yield code in the begin block.
450
- # * The reason for this is that there are really two logical ways
451
- # to complete a task: (1) successfully, and (2) unsuccessfully.
452
- # 1. This code represents the path by successful code, while the
453
- # rescue block represents the patch by unsuccessful code.
454
- # 2. Because the ensure block is always taking place, a failed block
455
- # of consumer code would log the error, then enter the ensure
456
- # block and log the task complete. That gives the wrong
457
- # impression.
458
- if @@verbosity_levels[run_level] >= @@verbosity_levels[@verbosity]
459
- # Only display if user wants logging of compeltion events
460
- # * Although we don't show the line when @show_completion is false,
461
- # we must still call #get_log_indentation_string(:done) to adjust
462
- # the @current_indent attribute properly.
463
- output << get_protected_log_time_string
464
- output << get_log_indentation_string(:done) << task << "\n"
465
- if @show_completion
466
- @debug_out << output if WATCHER_DEBUG and WATCHER_DEBUG_OUTPUT
467
- @output << output
468
- end
469
- end
470
- rescue
471
- # catch any Exceptions thrown by the executed block
472
- success_flag = protect(task, fail_actions, $!)
427
+ begin
428
+ tries_remaining -= 1
429
+ yield
430
+ rescue # from exception thrown during original yield
431
+ # Start by sending all queued (hidden) events to the log. (They were
432
+ # only queued up so we could log them in event of a failure.)
433
+ # NOTE: @events could be empty...that's okay.
434
+ @events.each do |e|
435
+ @output << e.to_s
436
+ @output << " (flushed)" if WATCHER_DEBUG and WATCHER_DEBUG_OUTPUT
437
+ @output << "\n"
473
438
  end
474
- end
475
-
476
- # return whether no exceptions occured
477
- success_flag
478
- end
439
+ # After we output all hidden events, we erase the @events Array,
440
+ # so we don't output them on the next retry failure.
441
+ @events = Array.new
442
+
443
+ @output << get_protected_log_time_string + ': ' \
444
+ + event.hier + ' ***'
479
445
 
480
- # Iterate through fail_actions parameter for a task, and for each action,
481
- # either perform or simply ensure it's a valid action.
482
- # * #protect is called by #monitor when your code invokes #debug or #info in
483
- # order to validate the fail_actions parameter.
484
- # * #protect is also called by #monitor when the associated code block
485
- # triggers an Exception. It receives an Array of actions to perform,
486
- # <tt>fail_actions</tt>, and executes each one in turn.
487
- # * Perhaps the most complex--however reasonable--example would be if you
488
- # wanted to log the event, then invoke some snippet of code to attempt to
489
- # resolve the problem, then retry the block. This can be done by
490
- # specifying the following fail actions. <b>(Be careful, however, as this
491
- # can easily lead to an infinite loop if the recovery Proc does not
492
- # resolve the cause of the original Exception!)</b>
493
- # * This is meant to provide you a bit of flexibility on how to handle
494
- # conditions, however a complex Array of fail_actions may defeat your goal
495
- # of code simplification.
496
- def protect(task, fail_actions, e=nil) #:doc:
497
- # * If e is nil, then return true if valid group, false or raise
498
- # exception if invalid fail action found.
499
- # * If e is an Exception, perform required actions.
500
-
501
- # only increase warning if also performing actions
502
- if e != nil
503
- if e.kind_of?(Exception)
504
- @warnings += 1
446
+ # Place error or warning flag in the log
447
+ if tries_remaining == 0 and event.failure == :error
448
+ if @error_symbol != nil and @error_symbol != ''
449
+ @output << ' ' + @error_symbol
450
+ end
505
451
  else
506
- err_msg = "invalid exception instance (Watcher.monitor error)"
507
- raise ArgumentError.new(err_msg)
452
+ if @warn_symbol != nil and @warn_symbol != ''
453
+ @output << ' ' + @warn_symbol
454
+ end
508
455
  end
509
- @debug_out << "e = #{e.inspect}\n" if WATCHER_DEBUG
510
- end
511
-
512
- if fail_actions.class == Symbol or fail_actions.class == Proc
513
- # NOTE: we pass the fail_actions (plural) parameter
514
- protect_action(task, fail_actions, e)
515
- elsif fail_actions.class == Array
516
- fail_actions.each do |fail_action|
517
- # NOTE: we pass the fail_action (singular) loop iteration variable
518
- protect_action(task, fail_action, e)
456
+
457
+ # Append to log result of merging newlines from exception message
458
+ if @merge_newlines != nil and @merge_newlines != ''
459
+ @debug_out.puts "[#{$!.message}]" if WATCHER_DEBUG and WATCHER_DEBUG_OUTPUT
460
+ message = $!.message.split(/\n/).join @merge_newlines
519
461
  end
520
- else
521
- err_msg = "unsupported fail action for task = [#{task}] " \
522
- + "(must be Symbol or Array of Symbols and Procs)"
523
- raise ArgumentError.new(err_msg)
524
- end
525
- end
462
+ @output << ' ' + message + "\n"
463
+ @output.flush
526
464
 
527
- # Either perform action, or simply validate it's valid
528
- def protect_action(task, fail_action, e)
529
- # * If e is nil, then validate the action against the possible
530
- # actions. Raise ArgumentError Exception if invalid action found.
531
- # * If e is a kind of Exception, perform required action.
532
-
533
- # only time-stamp if also performing actions
534
- if e != nil
535
- # create the time stamp and indentation portion of the log entry
536
- log_base = get_protected_log_time_string \
537
- + get_log_indentation_string(:error)
538
- @debug_out << "e = #{e.inspect}\n" if WATCHER_DEBUG
539
- end
465
+ if tries_remaining > 0
466
+ begin
467
+ # Call the user-supplied recovery Proc with the original exception
468
+ event.proc.call($!)
469
+ rescue # from exception during user's recovery proc
470
+ # If user-supplied Proc caused an Exception, then bail
471
+ @errors += 1
472
+ raise $!
473
+ end
540
474
 
541
- if fail_action.class == Proc
542
- if e != nil
543
- # return result from user-specified code
544
- return fail_action.call(e) # send the Exception instance
545
- end
546
- elsif fail_action.class == Symbol
547
- case fail_action
548
- when :log
549
- if e != nil
550
- output = log_base + '*' + task + '*' + ' ==> '
551
- output << "#{@fail_symbol} ==> " if @fail_symbol
552
- if @merge_newlines != false
553
- output << e.message.split(/\n/).join(@merge_newlines)
554
- else # no merge, but multiple lines
555
- output << e.message
556
- end
557
- @output << output << "\n"
475
+ @output << get_protected_log_time_string + ': ' \
476
+ + event.hier + ' ### '
477
+ @output << case tries_remaining
478
+ when 1
479
+ "(1 try left)\n"
480
+ else
481
+ '(' + tries_remaining.to_s + " tries left)\n"
482
+ end
483
+ @output.flush
484
+
485
+ retry
486
+ else # no more retries remaining
487
+ if not $!.kind_of? Exception
488
+ raise "$! no longer an exception [#{$!.inspect}]"
558
489
  end
559
- when :raise
560
- if e != nil
490
+ if event.failure == :error
561
491
  @errors += 1
562
- raise
492
+ raise $!
493
+ else
494
+ @warnings += 1
495
+ @output.flush
563
496
  end
564
- when :retry # FIXME: this is not well tested
565
- @continuation.call if e != nil
566
- else
567
- err_msg = "unsupported fail action Symbol for task = [#{task}] " \
568
- + "(must be one of [:log, :raise, :retry])"
569
- raise ArgumentError.new(err_msg)
570
- end
571
- else # only Symbols and Procs can be designated fail actions
572
- raise ArgumentError.new("invalid fail action (Watcher.protect error)")
573
- end
497
+ end # perform a retry attempt
498
+ ensure
499
+ # Once this Event is complete, make the next event a sibling of this event
500
+ @event_relationship = :sibling
501
+ @last_hier = event.hier.dup
502
+ end # rescue from top tier exception
574
503
  end
575
-
576
- # Set to true when Watcher should execute itself in debugging mode.
577
- WATCHER_DEBUG=false
578
504
 
579
- # Name of file that Watcher creates for its debugging output.
580
- WATCHER_DEBUG_OUTPUT_FILENAME='watcher-debug.log'
505
+ # Watcher version
506
+ VERSION = '1.2.0'
581
507
 
582
- # Set to true when debugging Watcher instance attribute settings.
583
- WATCHER_DEBUG_ATTRIBUTES=false
584
-
585
- # Set to true when debugging Watcher handling of fail actions.
586
- WATCHER_DEBUG_FAILACTIONS=false
508
+ # Set to true when Watcher should execute itself in debugging mode.
509
+ WATCHER_DEBUG = false
587
510
 
511
+ # Set to true when debugging Watcher failure actions.
512
+ WATCHER_DEBUG_FAIL_ACTIONS = false
513
+
588
514
  # Set to true when debugging Watcher verbosity levels.
589
- WATCHER_DEBUG_LEVELS=false
515
+ WATCHER_DEBUG_VERBOSITY = false
590
516
 
591
- # Set to true when debugging Watcher.monitor.
592
- WATCHER_DEBUG_MONITOR=false
517
+ # Name of file that Watcher creates for its debugging output.
518
+ WATCHER_DEBUG_OUTPUT_FILENAME = 'watcher-debug.log'
593
519
 
520
+ # Set to true when debugging Watcher instance attribute settings.
521
+ WATCHER_DEBUG_ATTRIBUTES = false
522
+
594
523
  # Set to true when debugging Watcher's log strings.
595
- WATCHER_DEBUG_OUTPUT=false
524
+ WATCHER_DEBUG_OUTPUT = false
596
525
  end