interception 0.1.pre.1 → 0.1.pre.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,3 +4,6 @@ ext/Makefile
4
4
  *.o
5
5
  *.so
6
6
  *.gem
7
+ *.lock
8
+ doc/
9
+ .yardoc
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
5
+ - jruby-18mode
6
+ - jruby-19mode
7
+ - rbx-18mode
8
+ - rbx-19mode
9
+ - 1.8.7
10
+ - ree
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+
2
+ task :clean do
3
+ sh 'rm -f ext/*.o ext/*.so ext/*.dylib'
4
+ sh 'rm -f ext/org/pryrepl/*.class'
5
+ end
6
+
7
+ desc "Compile *.c files"
8
+ task :compile => [:clean] do
9
+ cd 'ext/' do
10
+ sh 'ruby extconf.rb'
11
+ sh 'make'
12
+ end
13
+ end
14
+
15
+ desc "Run example"
16
+ task :example do
17
+ sh "ruby -I./lib/ ./examples/example.rb "
18
+ end
19
+
20
+ desc "Run example 2"
21
+ task :example2 do
22
+ sh "ruby -I./lib/ ./examples/example2.rb "
23
+ end
24
+
25
+ desc "Run tests"
26
+ task :test do
27
+ sh 'rspec spec -r ./spec/spec_helpers.rb'
28
+ end
29
+
30
+ task :default => [:compile, :test]
31
+
32
+
@@ -0,0 +1,21 @@
1
+ $:.unshift File.expand_path '../../lib', __FILE__
2
+ require 'pryception'
3
+
4
+ Interception.prycept do
5
+
6
+ def a
7
+ begin
8
+ begin
9
+ raise "foo"
10
+
11
+ rescue => e
12
+ raise "bar"
13
+ end
14
+
15
+ rescue => e
16
+ 1 / 0
17
+
18
+ end
19
+ end
20
+ a
21
+ end
@@ -0,0 +1,20 @@
1
+ $:.unshift File.expand_path '../../lib', __FILE__
2
+ require 'pryception'
3
+
4
+ def alpha
5
+ x = 1
6
+ beta
7
+ end
8
+
9
+ def beta
10
+ y = 30
11
+ gamma(1, 2)
12
+ end
13
+
14
+ def gamma(x)
15
+ greeting = x
16
+ end
17
+
18
+ Interception.prycept do
19
+ alpha
20
+ end
data/ext/extconf.rb CHANGED
@@ -1,12 +1,13 @@
1
1
  require 'rbconfig'
2
2
 
3
+
3
4
  if RbConfig::CONFIG['ruby_install_name'] == 'jruby'
4
5
 
5
6
  File.open("Makefile", "w") do |f|
6
7
  f.write "install:\n\tjrubyc --javac org/pryrepl/InterceptionEventHook.java\n"
7
8
  end
8
9
 
9
- elsif RbConfig::CONFIG['ruby_install_name'] == 'ruby'
10
+ elsif RbConfig::CONFIG['ruby_install_name'] =~ /^ruby/
10
11
 
11
12
  require 'mkmf'
12
13
  $CFLAGS += " -DRUBY_19" if RUBY_VERSION =~ /^1.9/
data/ext/interception.c CHANGED
@@ -2,12 +2,26 @@
2
2
 
3
3
  static VALUE rb_mInterception;
4
4
 
5
+ extern struct FRAME {
6
+ VALUE self;
7
+ int argc;
8
+ ID last_func;
9
+ ID orig_func;
10
+ VALUE last_class;
11
+ struct FRAME *prev;
12
+ struct FRAME *tmp;
13
+ struct RNode *node;
14
+ int iter;
15
+ int flags;
16
+ unsigned long uniq;
17
+ } *ruby_frame;
18
+
5
19
  #ifdef RUBY_19
6
20
 
7
21
  void
8
22
  interception_hook(rb_event_flag_t evflag, VALUE data, VALUE self, ID mid, VALUE klass)
9
23
  {
10
- VALUE binding = rb_funcall(rb_mKernel, rb_intern("binding"), 0, NULL);
24
+ VALUE binding = rb_funcall(self, rb_intern("binding"), 0, NULL);
11
25
  rb_funcall(rb_mInterception, rb_intern("rescue"), 2, rb_errinfo(), binding);
12
26
  }
13
27
 
@@ -24,7 +38,12 @@ interception_start(VALUE self)
24
38
  void
25
39
  interception_hook(rb_event_t event, NODE *node, VALUE self, ID mid, VALUE klass)
26
40
  {
27
- VALUE binding = rb_funcall(rb_mKernel, rb_intern("binding"), 0, NULL);
41
+ VALUE bself = ruby_frame->prev->self;
42
+ if (node == ruby_frame->node) {
43
+ bself = ruby_frame->prev->prev->self;
44
+ }
45
+
46
+ VALUE binding = rb_funcall(bself, rb_intern("binding"), 0, NULL);
28
47
  rb_funcall(rb_mInterception, rb_intern("rescue"), 2, ruby_errinfo, binding);
29
48
  }
30
49
 
@@ -48,5 +67,5 @@ Init_interception()
48
67
  {
49
68
  rb_mInterception = rb_define_module("Interception");
50
69
  rb_define_singleton_method(rb_mInterception, "start", interception_start, 0);
51
- rb_define_singleton_method(rb_mInterception, "stop", interception_start, 0);
70
+ rb_define_singleton_method(rb_mInterception, "stop", interception_stop, 0);
52
71
  }
data/interception.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "interception"
3
- s.version = "0.1.pre.1"
3
+ s.version = "0.1.pre.2"
4
4
  s.author = "Conrad Irwin"
5
5
  s.email = "conrad.irwin@gmail.com"
6
6
  s.homepage = "http://github.com/ConradIrwin/interception"
@@ -12,4 +12,5 @@ Gem::Specification.new do |s|
12
12
  s.require_path = "lib"
13
13
 
14
14
  s.add_development_dependency 'rake'
15
+ s.add_development_dependency 'rspec'
15
16
  end
@@ -0,0 +1,67 @@
1
+ # Platform specific implementations of Interception.start and Interception.stop
2
+ class << Interception
3
+ private
4
+
5
+ # For Rubinius we just monkeypatch Kernel#raise_exception,
6
+ #
7
+ # This is normally a thin wrapper around raising an exception on the VM
8
+ # (so the layer of abstraction below Kernel#raise).
9
+ if defined? Rubinius
10
+
11
+ def start
12
+ class << Rubinius
13
+
14
+ alias raise_with_no_interception raise_exception
15
+
16
+ def raise_exception(exc)
17
+ bt = Rubinius::VM.backtrace(1, true).drop_while do |x|
18
+ x.variables.method.file.to_s.start_with?("kernel/")
19
+ end.first
20
+ b = Binding.setup(bt.variables, bt.variables.method, bt.constant_scope, bt.variables.self, bt)
21
+
22
+ Interception.rescue(exc, b)
23
+ raise_with_no_interception(exc)
24
+ end
25
+ end
26
+ end
27
+
28
+ def stop
29
+ class << Rubinius
30
+ alias raise_exception raise_with_no_interception
31
+ end
32
+ end
33
+
34
+ # For JRuby we use the underlying hooks mechanism.
35
+ #
36
+ # It seems to work even if I don't pass --debug, but it still
37
+ # warns about it. So disable the warnings and install the hook.
38
+ elsif defined?(JRuby)
39
+
40
+ require 'java'
41
+ $CLASSPATH << File.expand_path('../../ext/', __FILE__)
42
+ java_import org.pryrepl.InterceptionEventHook
43
+
44
+ def start
45
+ old_verbose = $VERBOSE
46
+ $VERBOSE = nil
47
+ JRuby.runtime.add_event_hook(hook)
48
+ ensure
49
+ $VERBOSE = old_verbose
50
+ end
51
+
52
+ def stop
53
+ JRuby.runtime.remove_event_hook(hook)
54
+ end
55
+
56
+ def hook
57
+ @hook ||= InterceptionEventHook.new(proc do |e, b|
58
+ self.rescue(e, b)
59
+ end)
60
+ end
61
+
62
+ else #MRI
63
+
64
+ require File.expand_path('../../ext/interception', __FILE__)
65
+
66
+ end
67
+ end
data/lib/interception.rb CHANGED
@@ -1,19 +1,69 @@
1
1
  require 'thread'
2
2
 
3
+
4
+ # Provides global facility for monitoring exceptions raised in your application.
3
5
  module Interception
4
6
 
5
7
  class << self
6
- attr_accessor :mutex, :listeners
8
+ attr_accessor :mutex, :listeners, :rescueing
7
9
  end
8
-
9
10
  self.mutex = Mutex.new
10
11
  self.listeners = []
12
+ self.rescueing = false
11
13
 
14
+ # Listen for any exceptions raised.
15
+ #
16
+ # The listener block that you pass in will be executed as though inside Kernel#raise,
17
+ # so your entire program is still actively running. If you have a gem like
18
+ # pry-stack_explorer you can access the stack frames that lead to the exception
19
+ # occurring.
20
+ #
21
+ # NOTE: Be careful when writing a listener, if your listener raises an
22
+ # exception it will mask the original exception (though it will not recursively
23
+ # call your listener).
24
+ #
25
+ # @example
26
+ #
27
+ # # To report exceptions for the entire run of the program:
28
+ # Interception.listen do |exception, binding|
29
+ # Emailer.spam!('on-duty@startup.com', exception, binding.eval('self.class.name'))
30
+ # end
31
+ #
32
+ # @example
33
+ #
34
+ # # To log exceptions for the duration of a given block.
35
+ # def log_exceptions(&block)
36
+ # Interception.listen(block) do |exception, binding|
37
+ # puts "#{binding.eval("self.inspect")} raised #{exception.inspect}"
38
+ # end
39
+ # end
40
+ #
41
+ # @example
42
+ #
43
+ # # You can also turn listeners on and off manually
44
+ #
45
+ # listener = Proc.new{ |exception, binding|
46
+ # binding.pry
47
+ # }
48
+ # Interception.listen(listener)
49
+ # Async::Redis.get("foo") do
50
+ # Interception.unlisten(listener)
51
+ # end
52
+ #
53
+ # @param [Proc] for_block (nil) If you pass for_block in, then you will only
54
+ # intercept exceptions raised while that block
55
+ # is running.
56
+ # @param [Proc] listen_block The block to call when an exception occurs,
57
+ # takes two arguments, the exception and the
58
+ # binding
59
+ # @return [Object] The return value of the for_block (if present)
60
+ # @yield [exception, binding]
61
+ # @see .unlisten
12
62
  def self.listen(for_block=nil, &listen_block)
13
- raise "no block given" unless listen_block || for_block
63
+ raise ArgumentError, "no block given" unless listen_block || for_block
14
64
  mutex.synchronize{
15
- listeners << listen_block || for_block
16
- start
65
+ start if listeners.empty?
66
+ listeners << (listen_block || for_block)
17
67
  }
18
68
 
19
69
  if listen_block && for_block
@@ -22,9 +72,15 @@ module Interception
22
72
  ensure
23
73
  unlisten listen_block
24
74
  end
75
+ else
76
+ listen_block
25
77
  end
26
78
  end
27
79
 
80
+ # Disable a previously added listener
81
+ #
82
+ # @param [Proc] listen_block The listen block you wish to remove.
83
+ # @see .listen
28
84
  def self.unlisten(listen_block)
29
85
  mutex.synchronize{
30
86
  listeners.delete listen_block
@@ -32,53 +88,33 @@ module Interception
32
88
  }
33
89
  end
34
90
 
35
- def self.rescue(e, binding)
91
+ # Called by platform-specific implementations whenever an exception is raised.
92
+ #
93
+ # The arguments will be forwarded on to all listeners added via {listen} that
94
+ # haven't been removed via {unlisten}.
95
+ #
96
+ # For efficiency, this block will never be called unless there are active
97
+ # listeners.
98
+ #
99
+ # @param [Exception] exception The exception that was raised
100
+ # @param [Binding] binding The binding from which it was raised
101
+ def self.rescue(exception, binding)
102
+ return if rescueing
103
+ self.rescueing = true
36
104
  listeners.each do |l|
37
- l.call(e, binding)
105
+ l.call(exception, binding)
38
106
  end
107
+ ensure
108
+ self.rescueing = false
39
109
  end
40
110
 
41
- if defined? Rubinius
42
- def self.start
43
- class << Rubinius
44
- alias raise_with_no_interception raise_exception
45
-
46
- def raise_exception(exc)
47
- bt = Rubinius::VM.backtrace(1, true).drop_while do |x|
48
- x.variables.method.file.to_s.start_with?("kernel/")
49
- end.first
50
- b = Binding.setup(bt.variables, bt.variables.method, bt.constant_scope, bt.variables.self, bt)
51
-
52
- Interception.rescue(exc, b)
53
- raise_with_no_interception(exc)
54
- end
55
- end
56
- end
111
+ # Start sending events to rescue.
112
+ # Implemented per-platform
113
+ def self.start; raise NotImplementedError end
57
114
 
58
- def self.stop
59
- class << Rubinius
60
- alias raise_exception raise_with_no_interception
61
- end
62
- end
63
- elsif defined?(JRuby)
64
- $CLASSPATH << File.expand_path('../../ext/', __FILE__)
65
- java_import org.pryrepl.InterceptionEventHook
115
+ # Stop sending events to rescue.
116
+ # Implemented per-platform
117
+ def self.stop; raise NotImplementedError end
66
118
 
67
- def self.start
68
- JRuby.runtime.add_event_hook(hook)
69
- end
70
-
71
- def self.stop
72
- JRuby.runtime.remove_event_hook(hook)
73
- end
74
-
75
- def self.hook
76
- @hook ||= InterceptionEventHook.new(proc do |e, b|
77
- self.rescue(e, b)
78
- end)
79
- end
80
-
81
- else
82
- require File.expand_path('../../ext/interception.so', __FILE__)
83
- end
119
+ require File.expand_path('../cross_platform.rb', __FILE__)
84
120
  end
data/lib/pryception.rb ADDED
@@ -0,0 +1,66 @@
1
+ require 'rubygems'
2
+ require 'interception'
3
+ require 'pry'
4
+
5
+ begin
6
+ require 'pry-stack_explorer'
7
+ rescue LoadError
8
+ end
9
+
10
+ module Interception
11
+
12
+ class << self
13
+
14
+ # Intercept all exceptions that arise in the block and start a Pry session
15
+ # at the fail site.
16
+ def prycept(&block)
17
+ raised = []
18
+
19
+ Interception.listen(block) do |exception, binding|
20
+ if defined?(PryStackExplorer)
21
+ raised << [exception, binding.callers]
22
+ else
23
+ raised << [exception, Array(binding)]
24
+ end
25
+ end
26
+
27
+ ensure
28
+ if raised.any?
29
+ exception, bindings = raised.last
30
+ enter_exception_context(exception, bindings)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Sanitize the call stack.
37
+ # @param [Array<Binding>] bindings The call stack.
38
+ def prune_call_stack!(bindings)
39
+ bindings.delete_if { |b| b.eval("self") == self || b.eval("__method__") == :prycept }
40
+ end
41
+
42
+ # Start a Pry session in the context of the exception.
43
+ # @param [Exception] exception The exception.
44
+ # @param [Array<Binding>] bindings The call stack.
45
+ def enter_exception_context(exception, bindings)
46
+ inject_local("_ex_", exception, bindings.first)
47
+ inject_local("_raised_", [exception, bindings.first], bindings.first)
48
+
49
+ prune_call_stack!(bindings)
50
+ if defined?(PryStackExplorer)
51
+ pry :call_stack => bindings
52
+ else
53
+ bindings.first.pry
54
+ end
55
+ end
56
+
57
+ # Inject a local variable into a binding.
58
+ def inject_local(var, object, binding)
59
+ Thread.current[:__intercept_var__] = object
60
+ binding.eval("#{var} = Thread.current[:__intercept_var__]")
61
+ ensure
62
+ Thread.current[:__intercept_var__] = nil
63
+ end
64
+ end
65
+ end
66
+
data/pryly.rb ADDED
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'interception'
3
+ require 'pry'
4
+ require 'pry-stack_explorer'
5
+ def pryly(&block)
6
+ raised = []
7
+
8
+ Interception.listen(block) do |exception, binding|
9
+ raised << [exception, binding.callers]
10
+ end
11
+
12
+ ensure
13
+ if raised.last
14
+ e, bindings = raised.last
15
+ $foo = e
16
+ $bar = raised
17
+ bindings.first.eval("_ex_ = $foo")
18
+ bindings.first.eval("_raised_ = $bar")
19
+ bindings = bindings.drop_while { |b| b.eval("self") == Interception || b.eval("__method__") == :pryly }
20
+ pry :call_stack => bindings
21
+ end
22
+ end
23
+
24
+ pryly do
25
+
26
+ def a
27
+ begin
28
+ begin
29
+ raise "foo"
30
+
31
+ rescue => e
32
+ raise "bar"
33
+ end
34
+
35
+ rescue => e
36
+ 1 / 0
37
+
38
+ end
39
+ end
40
+ a
41
+ end
@@ -0,0 +1,141 @@
1
+ Interception.listen(proc {
2
+ begin; raise "fooo"; rescue; end
3
+ }) do |e, b|
4
+ $initial_eb = [e,b]
5
+ end
6
+
7
+ describe Interception do
8
+
9
+ before do
10
+ @exceptions = []
11
+ Interception.listen do |e, b|
12
+ @exceptions << [e, b]
13
+ end
14
+ end
15
+
16
+ after do
17
+ Interception.listeners.each do |l|
18
+ Interception.unlisten l
19
+ end
20
+ end
21
+
22
+ it "should allow keeping a log of all exceptions raised" do
23
+ begin
24
+ raise "foo"
25
+ rescue => e
26
+ #
27
+ end
28
+
29
+ @exceptions.map(&:first).should == [e]
30
+ end
31
+
32
+ it "should catch the correct binding" do
33
+ shoulder = :bucket
34
+ begin
35
+ raise "foo"
36
+ rescue => e
37
+ #
38
+ end
39
+
40
+ @exceptions.map{ |e, b| b.eval('shoulder') }.should == [:bucket]
41
+ end
42
+
43
+ it "should catch the binding on the correct line" do
44
+ shoulder = :bucket
45
+
46
+ line = nil
47
+ begin
48
+ line = __LINE__; raise "foo"
49
+ rescue => e
50
+ #
51
+ end
52
+
53
+ @exceptions.map{ |e, b| b.eval('__LINE__') }.should == [line]
54
+ end
55
+
56
+ it "should catch all nested exceptions" do
57
+
58
+ begin
59
+ begin
60
+ raise "foo"
61
+ rescue => e1
62
+ raise "bar"
63
+ end
64
+ rescue => e2
65
+ #
66
+ end
67
+
68
+ @exceptions.map(&:first).should == [e1, e2]
69
+ end
70
+
71
+ it "should be able to listen for the duration of a block" do
72
+
73
+ e1, e2 = nil
74
+ block = proc{
75
+ begin
76
+ line = __LINE__; raise "foo"
77
+ rescue => e1
78
+ #
79
+ end
80
+ }
81
+ Interception.listen(block) do |e, b|
82
+ e2 = e
83
+ end
84
+
85
+ e1.should == e2
86
+ end
87
+
88
+ it "should allow nested calls to listen with a block" do
89
+ e1, e2 = nil
90
+ b1, b2 = nil
91
+ block = proc{
92
+ begin
93
+ raise "foo"
94
+ rescue => e1
95
+ #
96
+ end
97
+ }
98
+ block2 = proc{
99
+ Interception.listen(block) do |e, b|
100
+ e2 = e
101
+ b1 = b
102
+ end
103
+ }
104
+ Interception.listen(block2) do |e, b|
105
+ b2 = b
106
+ end
107
+
108
+ e1.should == e2
109
+ b1.should == b2
110
+ end
111
+
112
+ it "should be able to handle NoMethodErrors" do
113
+ shoulder = :bucket
114
+
115
+ begin
116
+ line = __LINE__; "snorkle".desnrok
117
+ rescue => e1
118
+ #
119
+ end
120
+
121
+ @exceptions.map{ |e, b| [e] + b.eval('[__LINE__, shoulder, self]') }.should == [[e1, line, :bucket, self]]
122
+ e1.message.should =~ /desnrok/
123
+ end
124
+
125
+ it "should be able to handle division by 0 errors" do
126
+ shoulder = :bucket
127
+
128
+ begin
129
+ line = __LINE__; 1 / 0
130
+ rescue => e1
131
+ #
132
+ end
133
+
134
+ @exceptions.map{ |e, b| [e] + b.eval('[__LINE__, shoulder, self]') }.should == [[e1, line, :bucket, self]]
135
+ ZeroDivisionError.should === e1
136
+ end
137
+
138
+ it "should have the right exception and binding at the top level" do
139
+ $initial_eb.last.eval("self").should == TOPLEVEL_BINDING.eval("self")
140
+ end
141
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path('../../lib/interception', __FILE__)
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: interception
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: 4
5
- version: 0.1.pre.1
5
+ version: 0.1.pre.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Conrad Irwin
@@ -22,6 +22,17 @@ dependencies:
22
22
  requirement: *2056
23
23
  prerelease: false
24
24
  type: :development
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ version_requirements: &2074 !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ! '>='
30
+ - !ruby/object:Gem::Version
31
+ version: '0'
32
+ none: false
33
+ requirement: *2074
34
+ prerelease: false
35
+ type: :development
25
36
  description: Provides a cross-platform ability to intercept all exceptions as they are raised.
26
37
  email: conrad.irwin@gmail.com
27
38
  executables: []
@@ -30,11 +41,21 @@ extensions:
30
41
  extra_rdoc_files: []
31
42
  files:
32
43
  - .gitignore
44
+ - .travis.yml
45
+ - Gemfile
46
+ - Rakefile
47
+ - examples/example.rb
48
+ - examples/example2.rb
33
49
  - ext/extconf.rb
34
50
  - ext/interception.c
35
51
  - ext/org/pryrepl/InterceptionEventHook.java
36
52
  - interception.gemspec
53
+ - lib/cross_platform.rb
37
54
  - lib/interception.rb
55
+ - lib/pryception.rb
56
+ - pryly.rb
57
+ - spec/interception_spec.rb
58
+ - spec/spec_helpers.rb
38
59
  homepage: http://github.com/ConradIrwin/interception
39
60
  licenses: []
40
61
  post_install_message: