From 1361b76f1a47e2ee636affb9f7aa0b764339d182 Mon Sep 17 00:00:00 2001 From: Genadi Samokovarov Date: Tue, 15 Sep 2015 15:33:25 +0300 Subject: [PATCH 1/4] Use debug_inspector instead of binding_of_caller With support for Ruby 2.2.2 and above, we can get the integration running only by exposing the debug inspector API to Ruby land. --- lib/web_console.rb | 2 -- web-console.gemspec | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/web_console.rb b/lib/web_console.rb index 997a295f..b44a9775 100644 --- a/lib/web_console.rb +++ b/lib/web_console.rb @@ -1,5 +1,3 @@ -require 'binding_of_caller' - require 'active_support/lazy_load_hooks' require 'active_support/logger' diff --git a/web-console.gemspec b/web-console.gemspec index 2aec82f1..0cd78171 100644 --- a/web-console.gemspec +++ b/web-console.gemspec @@ -17,10 +17,10 @@ Gem::Specification.new do |s| rails_version = ">= 4.0" - s.add_dependency "railties", rails_version - s.add_dependency "activemodel", rails_version - s.add_dependency "sprockets-rails", ">= 2.0", "< 4.0" - s.add_dependency "binding_of_caller", ">= 0.7.2" + s.add_dependency "railties", rails_version + s.add_dependency "activemodel", rails_version + s.add_dependency "sprockets-rails", ">= 2.0", "< 4.0" + s.add_dependency "debug_inspector" # We need those for the testing application to run. s.add_development_dependency "actionmailer", rails_version From bfa58426243d382637f9eb8daf7cc8dfbe94d21f Mon Sep 17 00:00:00 2001 From: Genadi Samokovarov Date: Tue, 15 Sep 2015 16:23:02 +0300 Subject: [PATCH 2/4] Prototype the new CRuby integration Use tracepoint as we are not supporting Ruby 1.9.3 anymore. --- lib/web_console/helper.rb | 2 +- lib/web_console/integration/cruby.rb | 53 ++++++++++------------------ 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/lib/web_console/helper.rb b/lib/web_console/helper.rb index 11d4b172..8f8b627c 100644 --- a/lib/web_console/helper.rb +++ b/lib/web_console/helper.rb @@ -11,7 +11,7 @@ module Helper def console(binding = nil) raise DoubleRenderError if request.env['web_console.binding'] - request.env['web_console.binding'] = binding || ::Kernel.binding.of_caller(1) + request.env['web_console.binding'] = binding || ::WebConsole.caller_bindings.first # Make sure nothing is rendered from the view helper. Otherwise # you're gonna see unexpected # in the diff --git a/lib/web_console/integration/cruby.rb b/lib/web_console/integration/cruby.rb index a4e31a2f..7dfe5561 100644 --- a/lib/web_console/integration/cruby.rb +++ b/lib/web_console/integration/cruby.rb @@ -1,40 +1,23 @@ -class Exception - begin - # We share the same exception binding extraction mechanism as better_errors, - # so try to use it if it is already available. It also solves problems like - # charliesome/better_errors#272, caused by an infinite recursion. - require 'better_errors' +require 'debug_inspector' - # The bindings in which the exception originated in. - def bindings - @bindings || __better_errors_bindings_stack - end - rescue LoadError - # The bindings in which the exception originated in. - def bindings - @bindings || [] - end - - # CRuby calls #set_backtrace every time it raises an exception. Overriding - # it to assign the #bindings. - def set_backtrace_with_binding_of_caller(*args) - # Thanks to @charliesome who wrote this bit for better_errors. - unless Thread.current[:__web_console_exception_lock] - Thread.current[:__web_console_exception_lock] = true - begin - # Raising an exception here will cause all of the rubies to go into a - # stack overflow. Some rubies may even segfault. See - # https://bugs.ruby-lang.org/issues/10164 for details. - @bindings = binding.callers.drop(1) - ensure - Thread.current[:__web_console_exception_lock] = false - end - end +def WebConsole.caller_bindings + bindings = RubyVM::DebugInspector.open do |context| + context.backtrace_locations.each_index.map { |i| context.frame_binding(i) } + end - set_backtrace_without_binding_of_caller(*args) - end + # For C functions, we can't extract a binding. In this case, + # DebugInspector#frame_binding would have returned us nil. That's why we need + # to compact the bindings. + # + # Dropping two bindings, removes the current Ruby one in this exact method, + # and the one in the caller method. The caller method binding can be obtained + # by Kernel#binding, if needed. + bindings.compact.drop(2) +end - alias_method :set_backtrace_without_binding_of_caller, :set_backtrace - alias_method :set_backtrace, :set_backtrace_with_binding_of_caller +TracePoint.trace(:raise) do |context| + exc = context.raised_exception + if exc.bindings.empty? + exc.instance_variable_set(:@bindings, WebConsole.caller_bindings) end end From c99cef4efe3819ef0c639275d1aa45fe064b1230 Mon Sep 17 00:00:00 2001 From: Genadi Samokovarov Date: Tue, 15 Sep 2015 16:42:33 +0300 Subject: [PATCH 3/4] Extract the commons in web_console/integration --- lib/web_console/integration.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/web_console/integration.rb b/lib/web_console/integration.rb index be2280c1..04cf85cd 100644 --- a/lib/web_console/integration.rb +++ b/lib/web_console/integration.rb @@ -1,3 +1,28 @@ +module WebConsole + # Returns the Ruby bindings of Kernel#callers locations. + # + # The list of bindings here doesn't map 1 to 1 with Kernel#callers, as we + # can't build Ruby bindings for C functions or the equivalent native + # implementations in JRuby and Rubinius. + # + # This method needs to be overridden by every integration. + def self.caller_bindings + raise NotImplementedError + end +end + +class Exception + # Returns an array of the exception backtrace locations bindings. + # + # The list won't map to the traces in #backtrace 1 to 1, because we can't + # build bindings for every trace (C functions, for example). + # + # Every integration should the instance variable. + def bindings + @bindings || [] + end +end + case RUBY_ENGINE when 'rbx' require 'web_console/integration/rubinius' From b8814b52939bef7b4c95fd6da9dd0d96d1fb5e44 Mon Sep 17 00:00:00 2001 From: Genadi Samokovarov Date: Tue, 15 Sep 2015 17:45:57 +0300 Subject: [PATCH 4/4] Revamp the Rubinius integration * Implement WebConsole.caller_bindings instead of WebConsole::Rubinius.current_bindings. * Drop the WebConsole::Rubinius::InternalLocationFilter class and inline its implementation in WebConsole.caller_bindings. --- lib/web_console/integration/rubinius.rb | 72 ++++++++----------------- 1 file changed, 22 insertions(+), 50 deletions(-) diff --git a/lib/web_console/integration/rubinius.rb b/lib/web_console/integration/rubinius.rb index 1fe91331..e5b7e769 100644 --- a/lib/web_console/integration/rubinius.rb +++ b/lib/web_console/integration/rubinius.rb @@ -1,62 +1,34 @@ -module WebConsole - module Rubinius - # Filters internal Rubinius locations. - # - # There are a couple of reasons why we wanna filter out the locations. - # - # * ::Kernel.raise, is implemented in Ruby for Rubinius. We don't wanna - # have the frame for it to align with the CRuby and JRuby implementations. - # - # * For internal methods location variables can be nil. We can't create a - # bindings for them. - # - # * Bindings from the current file are considered internal and ignored. - # - # We do that all that so we can align the bindings with the backtraces - # entries. - class InternalLocationFilter - def initialize(locations) - @locations = locations - end +def WebConsole.caller_bindings + locations = ::Rubinius::VM.backtrace(1, true) - def filter - @locations.reject do |location| - location.file.start_with?('kernel/delta/kernel.rb') || - location.file == __FILE__ || - location.variables.nil? - end - end - end - - # Gets the current bindings for all available Ruby frames. - # - # Filters the internal Rubinius and WebConsole frames. - def self.current_bindings - locations = ::Rubinius::VM.backtrace(1, true) - - InternalLocationFilter.new(locations).filter.map do |location| - Binding.setup( - location.variables, - location.variables.method, - location.constant_scope, - location.variables.self, - location - ) - end - end + # Kernel.raise, is implemented in Ruby for Rubinius. We don't wanna have + # the frame for it to align with the CRuby and JRuby implementations. + # + # For internal methods location variables can be nil. We can't create a + # bindings for them. + locations.reject! do |location| + location.file.start_with?('kernel/delta/kernel.rb') || location.variables.nil? end -end -::Exception.class_eval do - def bindings - @bindings || [] + bindings = locations.map do |location| + Binding.setup( + location.variables, + location.variables.method, + location.constant_scope, + location.variables.self, + location + ) end + + # Drop the binding of the direct caller. That one can be created by + # Kernel#binding. + bindings.drop(1) end ::Rubinius.singleton_class.class_eval do def raise_exception_with_current_bindings(exc) if exc.bindings.empty? - exc.instance_variable_set(:@bindings, WebConsole::Rubinius.current_bindings) + exc.instance_variable_set(:@bindings, WebConsole.caller_bindings) end raise_exception_without_current_bindings(exc)