cheap_advice 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
1
+ $: << File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'cheap_advice'
4
+ require 'pp'
5
+ require 'time'
6
+
7
+ class MyClass
8
+ def foo
9
+ 42
10
+ end
11
+ end
12
+
13
+ class MyOtherClass
14
+ def bar
15
+ 43
16
+ end
17
+ end
18
+
19
+ a = MyClass.new
20
+ b = MyOtherClass.new
21
+
22
+ trace_advice = CheapAdvice.new(:around) do | ar, body |
23
+ ar.advice[:log] << "#{Time.now.iso8601(6)} " <<
24
+ "#{ar.rcvr.class} #{ar.meth} #{ar.rcvr.object_id}\n"
25
+ body.call
26
+ ar.advice[:log] << "#{Time.now.iso8601(6)} " <<
27
+ "#{ar.rcvr.class} #{ar.meth} #{ar.rcvr.object_id} " <<
28
+ "=> #{ar.result.inspect}\n"
29
+ end
30
+ trace_advice[:log] = $stderr # File.open("trace.log", "a+")
31
+
32
+ puts "\nWithout advice:"
33
+ pp a.foo
34
+ pp b.bar
35
+
36
+ puts "\nWith advice enabled:"
37
+ trace_advice.advise!(MyClass, :foo)
38
+ trace_advice.advise!(MyOtherClass, :bar)
39
+ pp a.foo
40
+ pp b.bar
41
+
42
+ puts "\nWith advice disabled:"
43
+ trace_advice.disable!
44
+ pp a.foo
45
+ pp b.bar
46
+
47
+ puts "\nWith advice re-enabled:"
48
+ trace_advice.enable!
49
+ pp a.foo
50
+ pp b.bar
51
+
@@ -0,0 +1,17 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ 'MyClass':
11
+ :advice: trace
12
+
13
+ 'MyClass#foo':
14
+ :enabled: false
15
+
16
+ 'MyClass#bar':
17
+ :enabled: false
@@ -0,0 +1,17 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ MyClass:
11
+ :advice: trace
12
+
13
+ MyClass#foo:
14
+ :enabled: true
15
+
16
+ MyClass#bar:
17
+ :enabled: false
@@ -0,0 +1,23 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: true
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+ :default:
10
+ :log_prefix: "YO "
11
+
12
+ MyClass:
13
+ :advice: trace
14
+
15
+ MyClass#foo: ~
16
+ MyClass#bar:
17
+ :options:
18
+ :trace:
19
+ :logger:
20
+ :name: :alternate
21
+ :log_after: false
22
+ :log_result: false
23
+
@@ -0,0 +1,20 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ MyClass:
11
+ :advice: trace
12
+
13
+ MyClass#foo:
14
+ :enabled: false
15
+
16
+ MyClass#bar:
17
+ :enabled: false
18
+
19
+ NoClass#no_method:
20
+ :enabled: true
@@ -0,0 +1,22 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ MyClass:
11
+ :advice: trace
12
+
13
+ MyClass#foo:
14
+ :enabled: true
15
+ :options:
16
+ :trace:
17
+ :log_before: false
18
+ :log_after: true
19
+ :log_result: true
20
+
21
+ MyClass#bar: false
22
+
@@ -0,0 +1,25 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ MyClass:
11
+ :advice: trace
12
+
13
+ MyClass#foo:
14
+ :enabled: true
15
+ :options:
16
+ :trace:
17
+ :log_before: true
18
+ :log_args: true
19
+ :log_after: true
20
+ :log_result: false
21
+
22
+ MyClass#bar: false
23
+
24
+ MyClass#raise_error: true
25
+
@@ -0,0 +1,99 @@
1
+ $: << File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'cheap_advice'
4
+ require 'cheap_advice/configuration'
5
+ require 'cheap_advice/trace'
6
+ require 'yaml'
7
+ require 'benchmark'
8
+
9
+ require 'rubygems'
10
+ =begin
11
+ gem 'ruby-debug'
12
+ require 'ruby-debug'
13
+ =end
14
+
15
+ ##################################################
16
+ # Target
17
+
18
+ class MyClass
19
+ def foo(arg)
20
+ 42 + arg
21
+ end
22
+ def bar(arg)
23
+ 24 + arg
24
+ end
25
+ def raise_error(arg)
26
+ raise ArgumentError, "#{arg.inspect}"
27
+ end
28
+ end
29
+
30
+ class MySubclass < MyClass
31
+ end
32
+
33
+ trace_advice = nil
34
+ trace_config = nil
35
+
36
+ Benchmark.bm(40) do | bm |
37
+
38
+ ##################################################
39
+ # Advice
40
+
41
+ bm.report("trace_advice setup") do
42
+ trace_advice = CheapAdvice::Trace.new
43
+
44
+ # Configure Trace loggers by name.
45
+ trace_advice.logger[:default] = {
46
+ :target => File.open(File.expand_path("../ex02-default.log", __FILE__), "w"),
47
+ }
48
+
49
+ trace_advice.logger[:alternate] = {
50
+ :target => File.open(File.expand_path("../ex02-alternate.log", __FILE__), "w"),
51
+ :formatter => CheapAdvice::Trace::YamlFormatter
52
+ }
53
+ end
54
+
55
+ ##################################################
56
+ # Advice Configuration
57
+
58
+
59
+ bm.report("trace_config setup") do
60
+ # Register Trace advice with configuration.
61
+ trace_config = CheapAdvice::Configuration.new
62
+ trace_config.advice[:trace] = trace_advice
63
+ end
64
+
65
+ config_yml = File.expand_path("../ex02-trace-*.yml", __FILE__)
66
+
67
+ Dir[config_yml].sort.each do | config_yml |
68
+ # Configure.
69
+ config_hash = YAML.load_file(config_yml)
70
+ trace_config.config = config_hash[:advice]
71
+
72
+ bm.report("configure! using #{File.basename(config_yml)}") do
73
+ trace_config.configure!
74
+ end
75
+
76
+ msg = "Using #{config_yml}"
77
+ # puts msg
78
+ trace_advice.log_all(msg)
79
+
80
+ ##################################################
81
+ # Target activity
82
+
83
+ bm.report("Target activity") do
84
+ a = MyClass.new
85
+ a.foo(123)
86
+ a.bar(456)
87
+ b = MySubclass.new
88
+ b.foo(789)
89
+ b.bar(012)
90
+ begin
91
+ a.raise_error(:aSymbol)
92
+ rescue Exception
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+
99
+
@@ -0,0 +1,511 @@
1
+ require 'thread'
2
+
3
+ # Provides cheap advice mechanism for Ruby.
4
+ # See: http://github.com/kstephens/cheap_advice
5
+ #
6
+ class CheapAdvice
7
+ EMPTY_Hash = { }.freeze
8
+ EMPTY_Array = [ ].freeze
9
+ EMPTY_String = ''.freeze
10
+
11
+ class Error < ::Exception; end
12
+
13
+ module Options
14
+ # Hash accesible by #[] and #[]=.
15
+ attr_accessor :options
16
+ def initialize
17
+ @mutex = Mutex.new
18
+ @options = { }
19
+ end
20
+
21
+ def [](k)
22
+ # @mutex.synchronize do
23
+ @options[k]
24
+ # end
25
+ end
26
+
27
+ def []=(k, v)
28
+ # @mutex.synchronize do
29
+ @options[k] = v
30
+ # end
31
+ end
32
+ end
33
+ include Options
34
+
35
+ # Procs called before, after, and around the original method.
36
+ attr_accessor :before, :after, :around
37
+
38
+ # Collection of Advised method bindings.
39
+ attr_accessor :advised
40
+
41
+ # Module or Array of Modules to extend new Advised objects with.
42
+ attr_accessor :advised_extend
43
+
44
+ NULL_PROC = lambda { | ar | }
45
+ NULL_AROUND_PROC = lambda { | ar, body | body.call }
46
+
47
+ # options:
48
+ # :before
49
+ # :after
50
+ # :around
51
+ def initialize *opts, &blk
52
+ super()
53
+
54
+ @advised = [ ]
55
+ @advised_for = { }
56
+
57
+ opts_hash = Hash === opts[-1] ? opts.pop : { }
58
+ opts_key = opts.shift
59
+ @options = opts_hash
60
+
61
+ @before = (opts_key == :before ? blk : opts_hash[:before]) ||
62
+ NULL_PROC
63
+ @after = (opts_key == :after ? blk : opts_hash[:after]) ||
64
+ NULL_PROC
65
+ @around = (opts_key == :around ? blk : opts_hash[:around]) ||
66
+ NULL_AROUND_PROC
67
+
68
+ @blk = blk
69
+ end
70
+
71
+
72
+ # Apply advice a method on a Module (or Class).
73
+ #
74
+ # The advised method is enabled immediately (this may change in a future release).
75
+ #
76
+ # Returns an Advised object that describes what method was advised.
77
+ #
78
+ # mod can be a String, a Module or an Array of either.
79
+ # method can be a String, a Symbol or an Array of either.
80
+ # if either are Arrays the result will be an Array of Advised objects.
81
+ #
82
+ # The type of method scope can be specified by:
83
+ # * :instance (default)
84
+ # * :class
85
+ # * :module
86
+ #
87
+ # Any additional Hash options are propagated to the Advised binding object, which
88
+ # can be accessed from the ActivationRecord passed to the Advice block(s).
89
+ #
90
+ # Examples:
91
+ # advice = CheapAdvice(:around) { | ar, body | ...; body.call; ... }
92
+ # advice.advise! MyClass, :instance_method, options_hash
93
+ # advice.advise! MyClass, :class_method, :class
94
+ #
95
+ # Each Advised object is extended with #advised_extend.
96
+ # The #advised Array lists all Advised object.
97
+ #
98
+ def advise! mod, method, *opts
99
+ return mod.map { | x | advise! x, method, *opts } if
100
+ Array === mod
101
+ return method.map { | x | advise! mod, x, *opts } if
102
+ Array === method
103
+
104
+ opts_hash = Hash === opts[-1] ? opts.pop : { }
105
+ kind = opts.shift
106
+ kind ||= :instance
107
+
108
+ method = method.to_sym
109
+
110
+ @mutex.synchronize do
111
+ unless @enabled_once
112
+ self.enabled!
113
+ @enabled_once = true
114
+ end
115
+
116
+ advised = advised_for mod, method, kind, opts_hash
117
+
118
+ advised.enable! # Should this really be automatically enabled??
119
+
120
+ advised
121
+ end
122
+ end
123
+
124
+ # Called once the first time this advice is enabled.
125
+ # Instances can override this method.
126
+ def enabled!
127
+ self
128
+ end
129
+
130
+ def advised_select mod, meth, kind
131
+ @advised.select do | ad |
132
+ (mod ? mod == ad.mod : true) &&
133
+ (meth ? meth == ad.meth : true) &&
134
+ (kind ? kind == ad.kind : true)
135
+ end
136
+ end
137
+
138
+ # Returns the existing Advised binding or creates a new one.
139
+ def advised_for mod, meth, kind, opts
140
+ (@advised_for[[ mod, meth, kind ]] ||=
141
+ construct_advised_for(mod, meth, kind)).set_options!(opts)
142
+ end
143
+
144
+ # Constructs an Advised binding from this Advice.
145
+ def construct_advised_for mod, meth, kind
146
+ advice = self
147
+
148
+ advised = Advised.new(advice, mod, meth, kind)
149
+
150
+ case @advised_extend
151
+ when nil
152
+ when Module
153
+ advised.extend(@advised_extend)
154
+ when Array
155
+ @advised_extend.each { | m | advised.extend(m) }
156
+ else
157
+ raise TypeError, "advised_extend: expected nil, Module or Array of Modules, given #{@advised_extend.class}"
158
+ end
159
+
160
+ advised.register_advice_methods!
161
+
162
+ advised.define_new_method!
163
+
164
+ @advised << advised
165
+
166
+ advised
167
+ end
168
+
169
+
170
+ # Disables all currently Advised methods.
171
+ def disable!
172
+ @mutex.synchronize do
173
+ @advised.each { | x | x.disable! }
174
+ end
175
+ self
176
+ end
177
+ alias :unadvise! :disable!
178
+
179
+
180
+ # Enables all currently Advised methods.
181
+ def enable!
182
+ @mutex.synchronize do
183
+ @advised.each { | x | x.enable! }
184
+ end
185
+ self
186
+ end
187
+ alias :readvise! :enable!
188
+
189
+
190
+ # Represents the application/binding of advice to a class and method.
191
+ class Advised
192
+ include Options
193
+
194
+ @@mutex = Mutex.new
195
+ @@advised_id ||= 0
196
+
197
+ # The Advice being applied to the Module and method.
198
+ attr_reader :advice
199
+ # The Module, method and kind (instance, class or module method)
200
+ attr_reader :mod, :meth, :kind
201
+ alias :cls :mod # Deprecated.
202
+
203
+ # The unique Advised id used to generate unique method names.
204
+ attr_reader :advised_id
205
+
206
+ # The name of the old and new method being patched in.
207
+ attr_reader :old_meth, :new_meth
208
+
209
+ # The name of the before, after and around methods.
210
+ attr_reader :before_meth, :after_meth, :around_meth
211
+
212
+ # True if the Advised methods are currently installed.
213
+ attr_reader :enabled
214
+
215
+ def initialize *args
216
+ @mutex = Mutex.new
217
+ @advice, @mod, @meth, @kind, @options = *args
218
+
219
+ case @kind
220
+ when :instance, :class, :module
221
+ else
222
+ raise ArgumentError, "invalid kind #{kind.inspect}"
223
+ end
224
+
225
+ @options ||= { }
226
+
227
+ @@mutex.synchronize do
228
+ @advised_id = @@advised_id += 1
229
+ end
230
+
231
+ @old_meth = :"__advice_old_#{@@advised_id}_#{@meth}"
232
+ @new_meth = :"__advice_new_#{@@advised_id}_#{@meth}"
233
+
234
+ @before_meth = :"__advice_before_#{@@advised_id}_#{@meth}"
235
+ @after_meth = :"__advice_after_#{@@advised_id}_#{@meth}"
236
+ @around_meth = :"__advice_around_#{@@advised_id}_#{@meth}"
237
+
238
+ @enabled =
239
+ @advice_methods_applied = false
240
+ end
241
+
242
+ def set_options! options
243
+ @options = options || { }
244
+ self
245
+ end
246
+
247
+ INSTANCE_SEP = '#'.freeze
248
+ MODULE_SEP = '.'.freeze
249
+
250
+ # The string name for the method.
251
+ # Returns "Foo#bar" for an instance method named :bar on class Foo.
252
+ # Returns "Foo.bar" for a class or module method named .bar on class Foo.
253
+ def meth_to_s
254
+ @meth_to_s ||=
255
+ "#{@mod}#{@kind == :instance ? INSTANCE_SEP : MODULE_SEP}#{@meth}".freeze
256
+ end
257
+
258
+ # True if the advice, mod, method and kind are equal.
259
+ def == x
260
+ return false unless self.class === x
261
+ @advice == x.advice && @mod == x.mod && @meth == x.meth && @kind == x.kind
262
+ end
263
+
264
+ # Support for Hash.
265
+ def hash
266
+ @advice.hash ^ @mod.hash ^ @meth.hash ^ @kind.hash
267
+ end
268
+
269
+ # Returns the target Module for the kind of method.
270
+ def mod_target
271
+ case @kind
272
+ when :instance
273
+ mod_resolve
274
+ when :class, :module
275
+ (class << mod_resolve; self; end)
276
+ else
277
+ raise ArgumentError, "mod_target: invalid kind #{kind.inspect}"
278
+ end
279
+ end
280
+
281
+ # Resolves mod Strings to the actual target Module.
282
+ def mod_resolve
283
+ case @mod
284
+ when Module
285
+ @mod
286
+ when String, Symbol
287
+ @mod.to_s.split('::').
288
+ reject { | name | name.empty?}.
289
+ inject(Object) { | namespace, name | namespace.const_get(name) }
290
+ else
291
+ raise TypeError, "mod_resolve: expected Module, String, Symbol, given #{@mod.class}"
292
+ end
293
+ end
294
+
295
+ # Registers the before, after and around advice methods.
296
+ def register_advice_methods!
297
+ scope # force calculation of scope before aliasing methods.
298
+
299
+ @mutex.synchronize do
300
+ return self if @advice_methods_registered
301
+
302
+ this = self
303
+ mod_target.instance_eval do
304
+ define_method(this.before_meth, &this.advice.before)
305
+ define_method(this.after_meth, &this.advice.after)
306
+ define_method(this.around_meth, &this.advice.around)
307
+ end
308
+
309
+ @advice_methods_registered = true
310
+ end
311
+ self
312
+ end
313
+
314
+ # Defines the new advised method in the target Module.
315
+ def define_new_method!
316
+ advised = self
317
+
318
+ advised.mod_target.instance_eval do
319
+ define_method advised.new_meth do | *args, &block |
320
+ ar = ActivationRecord.new(advised, self, args, block)
321
+
322
+ # Proc to invoke the old method with :before and :after advise hooks.
323
+ body = Proc.new do
324
+ self.__send__(advised.before_meth, ar)
325
+ begin
326
+ ar.result = self.__send__(advised.old_meth, *ar.args, &ar.block)
327
+ rescue ::Object => err
328
+ ar.error = err
329
+ ensure
330
+ self.__send__(advised.after_meth, ar)
331
+ end
332
+ ar.result
333
+ end
334
+
335
+ # Invoke the :around advice with the body Proc.
336
+ self.__send__(advised.around_meth, ar, body)
337
+
338
+ # Reraise Exception, if occured.
339
+ raise ar.error if ar.error
340
+
341
+ # Return the message result to caller.
342
+ ar.result
343
+ end # define_method
344
+ end # instance_eval
345
+
346
+ self
347
+ end
348
+
349
+ if RUBY_VERSION =~ /^1\.8/
350
+ def scope
351
+ @scope ||= @mutex.synchronize do
352
+ this = self
353
+ mod_target.instance_eval do
354
+ case
355
+ when private_instance_methods(false).include?(this.meth.to_s)
356
+ :private
357
+ when protected_instance_methods(false).include?(this.meth.to_s)
358
+ :protected
359
+ else
360
+ :public
361
+ end
362
+ end
363
+ end
364
+ end
365
+ else
366
+ def scope
367
+ @scope ||= @mutex.synchronize do
368
+ this = self
369
+ mod_target.instance_eval do
370
+ case
371
+ when private_instance_methods(false).include?(this.meth)
372
+ :private
373
+ when protected_instance_methods(false).include?(this.meth)
374
+ :protected
375
+ else
376
+ :public
377
+ end
378
+ end
379
+ end
380
+ end
381
+ end
382
+
383
+ # Enables the advice on this method.
384
+ def enable!
385
+ @mutex.synchronize do
386
+ return self if @enabled
387
+
388
+ this = self
389
+ mod_target.instance_eval do
390
+ case this.scope
391
+ when :public
392
+ else
393
+ public this.meth
394
+ end
395
+
396
+ alias_method this.old_meth, this.meth if
397
+ ! method_defined? this.old_meth
398
+
399
+ alias_method this.meth, this.new_meth
400
+
401
+ case this.scope
402
+ when :private
403
+ private this.meth
404
+ when :protected
405
+ protected this.meth
406
+ end
407
+ end
408
+
409
+ enabled!
410
+ @enabled = true
411
+ end
412
+ self
413
+ end
414
+ alias :advise! :enable!
415
+
416
+ # Disables the advice on this method.
417
+ def disable!
418
+ @mutex.synchronize do
419
+ return self if ! @enabled
420
+
421
+ this = self
422
+ mod_target.instance_eval do
423
+ if method_defined? this.old_meth
424
+ alias_method this.meth, this.old_meth
425
+
426
+ case this.scope
427
+ when :private
428
+ private this.meth
429
+ when :protected
430
+ protected this.meth
431
+ end
432
+ end
433
+ end
434
+
435
+ disabled!
436
+ @enabled = false
437
+ end
438
+
439
+ self
440
+ end
441
+ alias :unadvise! :disable!
442
+
443
+ # Called when Advised is enabled.
444
+ # Instances can override this method.
445
+ def enabled!
446
+ self
447
+ end
448
+
449
+ # Called when Advised is enabled.
450
+ # Instances can override this method.
451
+ def disabled!
452
+ self
453
+ end
454
+
455
+ end
456
+
457
+
458
+ # Represents the activation record of a method invocation.
459
+ class ActivationRecord
460
+ # The Advised method binding object.
461
+ attr_reader :advised
462
+
463
+ # The original message receiver, arguments and block (if given).
464
+ # Can be modified by the advice blocks.
465
+ attr_accessor :rcvr, :args, :block
466
+
467
+ # The original message return result available in the :around or :after advice blocks.
468
+ # Value can be changed in the advice blocks to alter the return result.
469
+ attr_accessor :result
470
+
471
+ # Any Exception rescued from the original method.
472
+ # Usually nil if no exception was raised;
473
+ # if not nil, Exception is reraised after the :around advice block.
474
+ # Can be modified by the :after advice block.
475
+ attr_accessor :error
476
+ alias :exception :error
477
+ alias :exception= :error=
478
+
479
+ # Arbitrary data accessed by #[], #[]=.
480
+ # Advice blocks can use this to store arbitrary data.
481
+ # May be a frozen, empty Hash.
482
+ attr_accessor :data
483
+
484
+ def initialize *args
485
+ @advised, @rcvr, @args, @block = *args
486
+ end
487
+
488
+ def data
489
+ @data || EMPTY_Hash
490
+ end
491
+
492
+ def [] key
493
+ (@data || EMPTY_Hash)[key]
494
+ end
495
+
496
+ def []= key, value
497
+ (@data ||= { })[key] = value
498
+ end
499
+
500
+ # This methods are delegated to #advised.
501
+ DELEGATE_TO_ADVISED = [ :advice, :mod, :meth, :kind, :meth_to_s ].freeze
502
+ eval(DELEGATE_TO_ADVISED.map{|m| "def #{m}; @advised.#{m}; end; "} * "\n")
503
+
504
+ # The call stack with CheapAdvice methods filtered out.
505
+ def caller(offset = 0)
506
+ ::Kernel.caller(offset + 2)
507
+ end
508
+ end
509
+
510
+ end
511
+