Skip to content

Commit

Permalink
Merge pull request #165 from gsamokovarov/revamped-integrations
Browse files Browse the repository at this point in the history
Revamped integrations for CRuby and Rubinius
  • Loading branch information
gsamokovarov committed Sep 19, 2015
2 parents 82d3d08 + b8814b5 commit 72f1466
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 92 deletions.
2 changes: 0 additions & 2 deletions lib/web_console.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
require 'binding_of_caller'

require 'active_support/lazy_load_hooks'
require 'active_support/logger'

Expand Down
2 changes: 1 addition & 1 deletion lib/web_console/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 #<Binding:0x007fee4302b078> in the
Expand Down
25 changes: 25 additions & 0 deletions lib/web_console/integration.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
53 changes: 18 additions & 35 deletions lib/web_console/integration/cruby.rb
Original file line number Diff line number Diff line change
@@ -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
72 changes: 22 additions & 50 deletions lib/web_console/integration/rubinius.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
8 changes: 4 additions & 4 deletions web-console.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 72f1466

Please sign in to comment.