better_errors 0.0.2 → 0.1.0

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/README.md CHANGED
@@ -1,28 +1,33 @@
1
1
  # Better Errors
2
2
 
3
- Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails.
3
+ Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails in any Rack app as Rack middleware.
4
4
 
5
- ![image](http://i.imgur.com/O9anD.png)
5
+ ![image](http://i.imgur.com/urVDW.png)
6
6
 
7
7
  ## Features
8
8
 
9
9
  * Full stack trace
10
10
  * Source code inspection for all stack frames (with highlighting)
11
11
  * Local and instance variable inspection
12
+ * Live REPL on every stack frame
12
13
 
13
14
  ## Installation
14
15
 
15
16
  Add this line to your application's Gemfile (under the **development** group):
16
17
 
17
- gem 'better_errors'
18
-
19
- And then execute:
18
+ ```ruby
19
+ group :development do
20
+ gem "better_errors"
21
+ end
22
+ ```
20
23
 
21
- $ bundle
24
+ If you would like to use Better Errors' **advanced features** (REPL, local/instance variable inspection, pretty stack frame names), you need to add the [`binding_of_caller`](https://github.com/banister/binding_of_caller) gem to your Gemfile:
22
25
 
23
- Or install it yourself as:
26
+ ```ruby
27
+ gem "binding_of_caller"
28
+ ```
24
29
 
25
- $ gem install better_errors
30
+ This is an optional dependency however, and Better Errors will work without it.
26
31
 
27
32
  ## Usage
28
33
 
@@ -44,6 +49,19 @@ get "/" do
44
49
  end
45
50
  ```
46
51
 
52
+ ## Compatibility
53
+
54
+ * **Supported**
55
+ * MRI 1.9.2, 1.9.3
56
+ * JRuby (1.9 mode) - *advanced features unsupported*
57
+ * Rubinius (1.9 mode) - *advanced features unsupported*
58
+ * **Coming soon**
59
+ * MRI 2.0.0 - the official API for grabbing caller bindings is slated for MRI 2.0.0, but it has not been implemented yet
60
+
61
+ ## Known issues
62
+
63
+ * Calling `yield` from the REPL segfaults MRI 1.9.x.
64
+
47
65
  ## Contributing
48
66
 
49
67
  1. Fork it
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.version = BetterErrors::VERSION
9
9
  s.authors = ["Charlie Somerville"]
10
10
  s.email = ["charlie@charliesomerville.com"]
11
- s.description = %q{Better Errors gives Rails a better error page.}
12
- s.summary = %q{Better Errors gives Rails a better error page}
11
+ s.description = %q{Provides a better error page for Rails and other Rack apps. Includes source code inspection, a live REPL and local/instance variable inspection for all stack frames.}
12
+ s.summary = %q{Better error page for Rails and other Rack apps}
13
13
  s.homepage = "https://github.com/charliesome/better_errors"
14
14
  s.license = "MIT"
15
15
 
@@ -19,8 +19,10 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.add_development_dependency "rake"
21
21
 
22
- s.add_dependency "erubis"
23
- s.add_dependency "coderay"
24
- # optional dependency:
22
+ s.add_dependency "erubis", ">= 2.7.0"
23
+ s.add_dependency "coderay", ">= 1.0.0"
24
+
25
+ # optional dependencies:
25
26
  # s.add_dependency "binding_of_caller"
27
+ # s.add_dependency "pry"
26
28
  end
@@ -0,0 +1,60 @@
1
+ module BetterErrors
2
+ class CodeFormatter
3
+ FILE_TYPES = {
4
+ ".rb" => :ruby,
5
+ "" => :ruby,
6
+ ".html" => :html,
7
+ ".erb" => :erb,
8
+ ".haml" => :haml
9
+ }
10
+
11
+ attr_reader :filename, :line, :context
12
+
13
+ def initialize(filename, line, context = 5)
14
+ @filename = filename
15
+ @line = line
16
+ @context = context
17
+ end
18
+
19
+ def html
20
+ %{<div class="code">#{formatted_lines.join}</div>}
21
+ rescue Errno::ENOENT, Errno::EINVAL
22
+ source_unavailable
23
+ end
24
+
25
+ def source_unavailable
26
+ "<p>Source unavailable</p>"
27
+ end
28
+
29
+ def coderay_scanner
30
+ ext = File.extname(filename)
31
+ FILE_TYPES[ext] || :text
32
+ end
33
+
34
+ def formatted_lines
35
+ line_range.zip(highlighted_lines).map do |current_line, str|
36
+ class_name = current_line == line ? "highlight" : ""
37
+ sprintf '<pre class="%s">%5d %s</pre>', class_name, current_line, str
38
+ end
39
+ end
40
+
41
+ def highlighted_lines
42
+ CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines
43
+ end
44
+
45
+ def context_lines
46
+ range = line_range
47
+ source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
48
+ end
49
+
50
+ def source_lines
51
+ @source_lines ||= File.readlines(filename)
52
+ end
53
+
54
+ def line_range
55
+ min = [line - context, 1].max
56
+ max = [line + context, source_lines.count].min
57
+ min..max
58
+ end
59
+ end
60
+ end
@@ -1,20 +1,21 @@
1
1
  class Exception
2
- attr_reader :__better_errors_bindings_stack
3
-
4
2
  original_initialize = instance_method(:initialize)
5
3
 
6
- define_method :initialize do |*args|
7
- unless Thread.current[:__better_errors_exception_lock]
8
- Thread.current[:__better_errors_exception_lock] = true
9
- begin
10
- @__better_errors_bindings_stack = []
11
- 2.upto(caller.size) do |index|
12
- @__better_errors_bindings_stack << binding.of_caller(index) rescue break
4
+ if BetterErrors.binding_of_caller_available?
5
+ define_method :initialize do |*args|
6
+ unless Thread.current[:__better_errors_exception_lock]
7
+ Thread.current[:__better_errors_exception_lock] = true
8
+ begin
9
+ @__better_errors_bindings_stack = binding.callers.drop(1)
10
+ ensure
11
+ Thread.current[:__better_errors_exception_lock] = false
13
12
  end
14
- ensure
15
- Thread.current[:__better_errors_exception_lock] = false
16
13
  end
14
+ original_initialize.bind(self).call(*args)
17
15
  end
18
- original_initialize.bind(self).call(*args)
19
16
  end
20
- end
17
+
18
+ def __better_errors_bindings_stack
19
+ @__better_errors_bindings_stack || []
20
+ end
21
+ end
@@ -2,23 +2,51 @@ require "json"
2
2
 
3
3
  module BetterErrors
4
4
  class ErrorPage
5
- def self.template_path
6
- __FILE__.gsub(/\.rb$/, ".erb")
5
+ def self.template_path(template_name)
6
+ File.expand_path("../templates/#{template_name}.erb", __FILE__)
7
7
  end
8
8
 
9
- def self.template
10
- Erubis::EscapedEruby.new(File.read(template_path))
9
+ def self.template(template_name)
10
+ Erubis::EscapedEruby.new(File.read(template_path(template_name)))
11
11
  end
12
12
 
13
- attr_reader :exception, :env
13
+ attr_reader :exception, :env, :repls
14
14
 
15
15
  def initialize(exception, env)
16
16
  @exception = real_exception(exception)
17
17
  @env = env
18
+ @start_time = Time.now.to_f
19
+ @repls = []
18
20
  end
19
21
 
20
- def render
21
- self.class.template.result binding
22
+ def render(template_name = "main")
23
+ self.class.template(template_name).result binding
24
+ end
25
+
26
+ def do_variables(opts)
27
+ index = opts["index"].to_i
28
+ @frame = backtrace_frames[index]
29
+ { html: render("variable_info") }
30
+ end
31
+
32
+ def do_eval(opts)
33
+ index = opts["index"].to_i
34
+ code = opts["source"]
35
+
36
+ unless binding = backtrace_frames[index].frame_binding
37
+ return { error: "REPL unavailable in this stack frame" }
38
+ end
39
+
40
+ result, prompt =
41
+ (@repls[index] ||= REPL.provider.new(binding)).send_input(code)
42
+
43
+ { result: result,
44
+ prompt: prompt,
45
+ highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) }
46
+ end
47
+
48
+ def backtrace_frames
49
+ @backtrace_frames ||= StackFrame.from_exception(exception)
22
50
  end
23
51
 
24
52
  private
@@ -33,48 +61,9 @@ module BetterErrors
33
61
  def request_path
34
62
  env["REQUEST_PATH"]
35
63
  end
36
-
37
- def backtrace_frames
38
- @backtrace_frames ||= ErrorFrame.from_exception(exception)
39
- end
40
-
41
- def coderay_scanner_for_ext(ext)
42
- case ext
43
- when "rb"; :ruby
44
- when "html"; :html
45
- when "erb"; :erb
46
- when "haml"; :haml
47
- end
48
- end
49
-
50
- def file_extension(filename)
51
- filename.split(".").last
52
- end
53
-
54
- def code_extract(frame, lines_of_context = 5)
55
- lines = File.readlines(frame.filename)
56
- min_line = [1, frame.line - lines_of_context].max - 1
57
- max_line = [frame.line + lines_of_context, lines.count + 1].min - 1
58
- [min_line, max_line, lines[min_line..max_line].join]
59
- end
60
64
 
61
65
  def highlighted_code_block(frame)
62
- ext = file_extension(frame.filename)
63
- scanner = coderay_scanner_for_ext(ext)
64
- min_line, max_line, code = code_extract(frame)
65
- highlighted_code = CodeRay.scan(code, scanner).div wrap: nil
66
- "".tap do |html|
67
- html << "<div class='code'>"
68
- highlighted_code.each_line.each_with_index do |str, index|
69
- if min_line + index + 1 == frame.line
70
- html << "<pre class='highlight'>"
71
- else
72
- html << "<pre>"
73
- end
74
- html << sprintf("%5d", min_line + index + 1) << " " << str << "</pre>"
75
- end
76
- html << "</div>"
77
- end
66
+ CodeFormatter.new(frame.filename, frame.line).html
78
67
  end
79
68
  end
80
69
  end
@@ -1,3 +1,5 @@
1
+ require "json"
2
+
1
3
  module BetterErrors
2
4
  class Middleware
3
5
  def initialize(app, handler = ErrorPage)
@@ -6,10 +8,47 @@ module BetterErrors
6
8
  end
7
9
 
8
10
  def call(env)
11
+ case env["PATH_INFO"]
12
+ when %r{\A/__better_errors/(?<oid>\d+)/(?<method>\w+)\z}
13
+ internal_call env, $~
14
+ when %r{\A/__better_errors/?\z}
15
+ show_error_page env
16
+ else
17
+ app_call env
18
+ end
19
+ end
20
+
21
+ private
22
+ def app_call(env)
9
23
  @app.call env
10
24
  rescue Exception => ex
11
- error_page = @handler.new ex, env
12
- [500, { "Content-Type" => "text/html; charset=utf-8" }, [error_page.render]]
25
+ @error_page = @handler.new ex, env
26
+ log_exception
27
+ show_error_page(env)
28
+ end
29
+
30
+ def show_error_page(env)
31
+ [500, { "Content-Type" => "text/html; charset=utf-8" }, [@error_page.render]]
32
+ end
33
+
34
+ def log_exception
35
+ return unless BetterErrors.logger
36
+
37
+ message = "\n#{@error_page.exception.class} - #{@error_page.exception.message}:\n"
38
+ @error_page.backtrace_frames.each do |frame|
39
+ message << " #{frame}\n"
40
+ end
41
+
42
+ BetterErrors.logger.fatal message
43
+ end
44
+
45
+ def internal_call(env, opts)
46
+ if opts[:oid].to_i != @error_page.object_id
47
+ return [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(error: "Session expired")]]
48
+ end
49
+
50
+ response = @error_page.send("do_#{opts[:method]}", JSON.parse(env["rack.input"].read))
51
+ [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]]
13
52
  end
14
53
  end
15
54
  end
@@ -1,10 +1,11 @@
1
1
  module BetterErrors
2
2
  class Railtie < Rails::Railtie
3
3
  initializer "better_errors.configure_rails_initialization" do
4
- middleware = Rails.application.middleware
5
- middleware.use BetterErrors::Middleware
6
-
7
- BetterErrors.application_root = Rails.root.to_s
4
+ unless Rails.env.production?
5
+ Rails.application.middleware.use BetterErrors::Middleware
6
+ BetterErrors.logger = Rails.logger
7
+ BetterErrors.application_root = Rails.root.to_s
8
+ end
8
9
  end
9
10
  end
10
11
  end
@@ -0,0 +1,20 @@
1
+ module BetterErrors
2
+ module REPL
3
+ class Basic
4
+ def initialize(binding)
5
+ @binding = binding
6
+ end
7
+
8
+ def send_input(str)
9
+ [execute(str), ">>"]
10
+ end
11
+
12
+ private
13
+ def execute(str)
14
+ "=> #{@binding.eval(str).inspect}\n"
15
+ rescue Exception => e
16
+ "!! #{e.inspect rescue e.class.to_s rescue "Exception"}\n"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,67 @@
1
+ require "fiber"
2
+ require "pry"
3
+
4
+ module BetterErrors
5
+ module REPL
6
+ class Pry
7
+ class Input
8
+ def readline
9
+ Fiber.yield
10
+ end
11
+ end
12
+
13
+ class Output
14
+ def initialize
15
+ @buffer = ""
16
+ end
17
+
18
+ def puts(*args)
19
+ args.each do |arg|
20
+ @buffer << "#{arg.chomp}\n"
21
+ end
22
+ end
23
+
24
+ def tty?
25
+ false
26
+ end
27
+
28
+ def read_buffer
29
+ @buffer
30
+ ensure
31
+ @buffer = ""
32
+ end
33
+ end
34
+
35
+ def initialize(binding)
36
+ @fiber = Fiber.new do
37
+ @pry.repl binding
38
+ end
39
+ @input = Input.new
40
+ @output = Output.new
41
+ @pry = ::Pry.new input: @input, output: @output
42
+ @pry.hooks.clear_all
43
+ @continued_expression = false
44
+ @pry.hooks.add_hook :after_read, "better_errors hacky hook" do
45
+ @continued_expression = false
46
+ end
47
+ @fiber.resume
48
+ end
49
+
50
+ def pry_indent
51
+ @pry.instance_variable_get(:@indent)
52
+ end
53
+
54
+ def send_input(str)
55
+ old_pry_config_color = ::Pry.config.color
56
+ ::Pry.config.color = false
57
+ @continued_expression = true
58
+ @fiber.resume "#{str}\n"
59
+ # TODO - indent with `pry_indent.current_prefix`
60
+ # TODO - use proper pry prompt
61
+ [@output.read_buffer, @continued_expression ? ".." : ">>"]
62
+ ensure
63
+ ::Pry.config.color = old_pry_config_color
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,27 @@
1
+ module BetterErrors
2
+ module REPL
3
+ PROVIDERS = [
4
+ { impl: "better_errors/repl/pry",
5
+ const: :Pry },
6
+ { impl: "better_errors/repl/basic",
7
+ const: :Basic },
8
+ ]
9
+
10
+ def self.provider
11
+ @provider ||= const_get detect[:const]
12
+ end
13
+
14
+ def self.detect
15
+ PROVIDERS.find do |prov|
16
+ test_provider prov
17
+ end
18
+ end
19
+
20
+ def self.test_provider(provider)
21
+ require provider[:impl]
22
+ true
23
+ rescue LoadError
24
+ false
25
+ end
26
+ end
27
+ end
@@ -1,10 +1,18 @@
1
1
  module BetterErrors
2
- class ErrorFrame
2
+ class StackFrame
3
3
  def self.from_exception(exception)
4
+ idx_offset = 0
4
5
  exception.backtrace.each_with_index.map { |frame, idx|
5
- next unless frame =~ /\A(.*):(\d*):in `(.*)'\z/
6
- b = exception.__better_errors_bindings_stack[idx]
7
- ErrorFrame.new($1, $2.to_i, $3, b)
6
+ frame_binding = exception.__better_errors_bindings_stack[idx - idx_offset]
7
+ md = /\A(?<file>.*):(?<line>\d*):in `(?<name>.*)'\z/.match(frame)
8
+
9
+ # prevent mismatching frames in the backtrace with the binding stack
10
+ if frame_binding and frame_binding.eval("__FILE__") != md[:file]
11
+ idx_offset += 1
12
+ frame_binding = nil
13
+ end
14
+
15
+ StackFrame.new(md[:file], md[:line].to_i, md[:name], frame_binding)
8
16
  }.compact
9
17
  end
10
18
 
@@ -20,7 +28,8 @@ module BetterErrors
20
28
  end
21
29
 
22
30
  def application?
23
- starts_with? filename, BetterErrors.application_root if BetterErrors.application_root
31
+ root = BetterErrors.application_root
32
+ starts_with? filename, root if root
24
33
  end
25
34
 
26
35
  def application_path
@@ -59,19 +68,32 @@ module BetterErrors
59
68
 
60
69
  def local_variables
61
70
  return {} unless frame_binding
62
- Hash[frame_binding.eval("local_variables").map { |x| [x, frame_binding.eval(x.to_s)] }]
71
+ frame_binding.eval("local_variables").each_with_object({}) do |name, hash|
72
+ begin
73
+ hash[name] = frame_binding.eval(name.to_s)
74
+ rescue NameError => e
75
+ # local_variables sometimes returns broken variables.
76
+ # https://bugs.ruby-lang.org/issues/7536
77
+ end
78
+ end
63
79
  end
64
80
 
65
81
  def instance_variables
66
82
  return {} unless frame_binding
67
- Hash[frame_binding.eval("instance_variables").map { |x| [x, frame_binding.eval(x.to_s)] }]
83
+ Hash[frame_binding.eval("instance_variables").map { |x|
84
+ [x, frame_binding.eval(x.to_s)]
85
+ }]
86
+ end
87
+
88
+ def to_s
89
+ "#{pretty_path}:#{line}:in `#{name}'"
68
90
  end
69
91
 
70
92
  private
71
93
  def set_pretty_method_name
72
94
  name =~ /\A(block (\([^)]+\) )?in )?/
73
95
  recv = frame_binding.eval("self")
74
- method = frame_binding.eval("__method__")
96
+ return unless method = frame_binding.eval("__method__")
75
97
  @name = if recv.is_a? Module
76
98
  "#{$1}#{recv}.#{method}"
77
99
  else