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

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