stackdeck 0.1.0 → 0.2.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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -0,0 +1,347 @@
1
+ require 'erb'
2
+ require 'cgi'
3
+ require 'stackdeck'
4
+
5
+ module Rack
6
+ # Rack::ShowStackDeck catches all exceptions raised from the app it
7
+ # wraps. It shows a useful backtrace with the sourcefile and
8
+ # clickable context, the whole Rack environment and the request
9
+ # data.
10
+ #
11
+ # Be careful when you use this on public-facing sites as it could
12
+ # reveal information helpful to attackers.
13
+ #
14
+ # Rack::ShowStackDeck is based on, and functionally very similar to,
15
+ # Rack::ShowExceptions -- the key difference is that it uses the
16
+ # StackDeck library to show the full cross-language backtrace, where
17
+ # relevant.
18
+
19
+ class ShowStackDeck
20
+ FILTER_ENV = [/^rack\./]
21
+ HIDE_FIELD = []
22
+
23
+ def initialize(app, options={})
24
+ @app = app
25
+ @template = ERB.new(TEMPLATE)
26
+ @exception_status = Hash.new(500)
27
+ @exception_status.update(options[:exception_status]) if options[:exception_status]
28
+ end
29
+
30
+ def call(env)
31
+ @app.call(env)
32
+ rescue StandardError, ScriptError, Timeout::Error => e
33
+ data, status = pretty_data(env, e)
34
+
35
+ [status, {"Content-Type" => "text/html"}, [@template.result(data)]]
36
+ end
37
+
38
+ private
39
+
40
+ def filter_environment(env)
41
+ filtered_env = {}
42
+ env.each do |k, v|
43
+ next if FILTER_ENV.any? {|hide| hide === k }
44
+ filtered_env[k] = v
45
+ end
46
+
47
+ filtered_env
48
+ end
49
+
50
+ def hide_request_field?(name)
51
+ HIDE_FIELD.any? {|hide| hide === name }
52
+ end
53
+
54
+ def pretty_data(env, exception)
55
+ req = Rack::Request.new(env)
56
+ path = ('/' + req.script_name + req.path_info).squeeze("/")
57
+
58
+ frames = exception.stack_deck
59
+
60
+ exception_name = exception.respond_to?(:name) ? exception.name : exception.class.name
61
+ html_message = exception.respond_to?(:html_message) ? exception.html_message : h( exception.to_s )
62
+ status = exception.respond_to?(:http_status) ? exception.http_status : @exception_status[exception.class.name]
63
+
64
+
65
+ hidden = Object.new
66
+
67
+ info_groups = [
68
+ ['GET', (req.GET rescue nil)],
69
+ ['POST', (req.POST rescue nil)],
70
+ ['Cookies', (req.cookies rescue nil)],
71
+ ['Session', env["rack.session"]],
72
+ ['Rack ENV', filter_environment(env)],
73
+ ].map {|(name, data)|
74
+ [name,
75
+ case data
76
+ when nil
77
+ nil
78
+ when Hash
79
+ [%w(Variable Value)] + data.to_a.sort.map {|(k,v)| [k.to_s, hide_request_field?(k) ? hidden : v] } unless data.empty?
80
+ when Array
81
+ [%w(Value)] + data.map {|a| [a] }
82
+ else
83
+ [%w(Value)] + [data]
84
+ end
85
+ ]
86
+ }
87
+
88
+ first_frames = []
89
+ seen_languages = []
90
+ frames.each do |frame|
91
+ next if seen_languages.include?( frame.language )
92
+ first_frames << frame
93
+ seen_languages << frame.language
94
+ end
95
+
96
+ [binding, status]
97
+ end
98
+
99
+ def h(obj) # :nodoc:
100
+ obj = obj.inspect unless String === obj
101
+ Utils.escape_html(obj).gsub(/\n/, "<br />\n")
102
+ end
103
+
104
+ def ident(s) # :nodoc:
105
+ s.downcase.gsub(/[^a-z]+/, '-')
106
+ end
107
+
108
+ def html_frame(frame, with_context=false)
109
+ html = if frame.filename && frame.function == ''
110
+ "<code>#{h frame.filename}</code>: in function"
111
+ elsif frame.filename && frame.function
112
+ "<code>#{h frame.filename}</code>: in <code>#{h frame.function}</code>"
113
+ elsif frame.filename
114
+ "<code>#{h frame.filename}</code>"
115
+ elsif frame.function == ''
116
+ "in function"
117
+ elsif frame.function
118
+ "in <code>#{h frame.function}</code>"
119
+ elsif frame.language
120
+ "in #{h frame.language}"
121
+ else
122
+ "unknown"
123
+ end
124
+
125
+ clues = []
126
+ clues << "on line #{frame.lineno}" if frame.lineno && !(with_context && frame.context?)
127
+ clues << frame.clue if frame.clue
128
+ unless clues.empty?
129
+ html << " (#{h clues.join(', ')})"
130
+ end
131
+
132
+ html
133
+ end
134
+
135
+ # :stopdoc:
136
+
137
+ # adapted from Django <djangoproject.com>
138
+ # Copyright (c) 2005, the Lawrence Journal-World
139
+ # Used under the modified BSD license:
140
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
141
+ TEMPLATE = <<'HTML'
142
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
143
+ <html lang="en">
144
+ <head>
145
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
146
+ <meta name="robots" content="NONE,NOARCHIVE" />
147
+ <title><%=h exception_name %> at <%=h path %></title>
148
+ <style type="text/css">
149
+ html * { padding:0; margin:0; }
150
+ body * { padding:10px 20px; }
151
+ body * * { padding:0; }
152
+ body { font:small sans-serif; }
153
+ body>div { border-bottom:1px solid #ddd; }
154
+ h1 { font-weight:normal; }
155
+ h2 { margin-bottom:.8em; }
156
+ h2 span { font-size:80%; color:#666; font-weight:normal; }
157
+ h2 code { font-family:monospace; color:black; font-weight:normal; font-size: 80%; }
158
+ h3 { margin:1em 0 .5em 0; }
159
+ table {
160
+ border:1px solid #ccc; border-collapse: collapse; background:white; }
161
+ tbody td, tbody th { vertical-align:top; padding:2px 3px; }
162
+ thead th {
163
+ padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
164
+ font-weight:normal; font-size:11px; border:1px solid #ddd; }
165
+ tbody th { text-align:right; color:#666; padding-right:.5em; }
166
+ table.req td { border-top: 1px solid #eee; }
167
+ table.req td.code { border-left: 1px solid #eee; }
168
+ table td.code { font-family: monospace; width: 100%; }
169
+ table td.code div { overflow:hidden; }
170
+ ul.traceback { list-style-type:none; }
171
+ ul.traceback li.frame { margin-bottom:1em; }
172
+ div.context { margin: 10px 0; }
173
+ div.context.solid { padding-left: 30px; margin: 10px 10px; }
174
+ div.context.solid div.context-line { padding-left: 18px; font-family:monospace; white-space: pre-wrap; background-color:#ccc; }
175
+ div.context ol {
176
+ padding-left:30px; margin:0 10px; list-style-position: inside; }
177
+ div.context ol li {
178
+ padding-left: 28px; text-indent: -28px; font-family:monospace; white-space:pre-wrap; color:#666; cursor:pointer; }
179
+ div.context ol.context-line li { position: relative; padding-right: 4em; color:black; background-color:#ccc; }
180
+ div.context ol.context-line li span { position: absolute; right: 0.5em; content: "..."; }
181
+ #summary { background: #ffc; }
182
+ #summary h2 { font-weight: normal; color: #666; }
183
+ #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
184
+ #summary ul#quicklinks>li { float: left; padding: 0 1em; }
185
+ #summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
186
+ #explanation { background:#eee; }
187
+ #traceback { background:#eee; }
188
+ #requestinfo { background:#f6f6f6; padding-left:120px; }
189
+ #summary table { border:none; background:transparent; }
190
+ #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
191
+ #requestinfo h3 { margin-bottom:-1em; }
192
+ </style>
193
+ <script type="text/javascript">
194
+ //<!--
195
+ function getElementsByClassName(oElm, strTagName, strClassName){
196
+ // Written by Jonathan Snook, http://www.snook.ca/jon;
197
+ // Add-ons by Robert Nyman, http://www.robertnyman.com
198
+ var arrElements = (strTagName == "*" && document.all)? document.all :
199
+ oElm.getElementsByTagName(strTagName);
200
+ var arrReturnElements = new Array();
201
+ strClassName = strClassName.replace(/\-/g, "\\-");
202
+ var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
203
+ var oElement;
204
+ for(var i=0; i<arrElements.length; i++){
205
+ oElement = arrElements[i];
206
+ if(oRegExp.test(oElement.className)){
207
+ arrReturnElements.push(oElement);
208
+ }
209
+ }
210
+ return (arrReturnElements)
211
+ }
212
+ function hideAll(elems) {
213
+ for (var e = 0; e < elems.length; e++) {
214
+ elems[e].style.display = 'none';
215
+ }
216
+ }
217
+ window.onload = function() {
218
+ hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
219
+ hideAll(getElementsByClassName(document, 'ol', 'post-context'));
220
+ }
221
+ function toggle() {
222
+ for (var i = 0; i < arguments.length; i++) {
223
+ var e = document.getElementById(arguments[i]);
224
+ if (e) {
225
+ e.style.display = e.style.display == 'none' ? 'block' : 'none';
226
+ }
227
+ }
228
+ return false;
229
+ }
230
+ //-->
231
+ </script>
232
+ </head>
233
+ <body>
234
+
235
+ <div id="summary">
236
+ <h1><%=h exception_name %> at <%=h path %></h1>
237
+ <h2><%= html_message %></h2>
238
+ <table>
239
+ <% first_frames.each do |frame| %>
240
+ <tr>
241
+ <th><%=h frame.language %></th>
242
+ <td><%= html_frame(frame) %></td>
243
+ </tr>
244
+ <% end %>
245
+ <tr>
246
+ <th>Web</th>
247
+ <td><code><%=h req.request_method %> <%=h(req.host + (req.port == 80 ? '' : ":#{req.port}") + path)%></code></td>
248
+ </tr>
249
+ </table>
250
+
251
+ <h3>Jump to:</h3>
252
+ <ul id="quicklinks">
253
+ <% info_groups.each do |(label, data)| %>
254
+ <% unless data.nil? || data.empty? %>
255
+ <li><a href="#<%=h ident(label) %>-info"><%=h label %></a></li>
256
+ <% else %>
257
+ <li><%=h label %></li>
258
+ <% end %>
259
+ <% end %>
260
+ </ul>
261
+ </div>
262
+
263
+ <div id="traceback">
264
+ <h2>Traceback <span>(innermost first)</span></h2>
265
+ <ul class="traceback">
266
+ <% frames.each { |frame| %>
267
+ <li class="frame <%=h ident(frame.language) %>"><%= html_frame(frame, true) %>
268
+
269
+ <% if frame.context? && (frame.context.before || frame.context.after) %>
270
+ <div class="context" id="c<%=h frame.object_id %>" onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')">
271
+ <% if frame.context.before %>
272
+ <ol start="<%=h frame.context.before_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
273
+ <% frame.context.before.each { |line| %>
274
+ <li><%=h line %></li>
275
+ <% } %>
276
+ </ol>
277
+ <% end %>
278
+
279
+ <ol start="<%=h frame.lineno %>" class="context-line">
280
+ <li><%=h frame.context.line %><span></span></li>
281
+ </ol>
282
+
283
+ <% if frame.context.after %>
284
+ <ol start='<%=h frame.lineno+1 %>' class="post-context" id="post<%=h frame.object_id %>">
285
+ <% frame.context.after.each { |line| %>
286
+ <li><%=h line %></li>
287
+ <% } %>
288
+ </ol>
289
+ <% end %>
290
+ </div>
291
+
292
+ <% elsif frame.context? %>
293
+ <div class="context solid">
294
+ <div class="context-line"><%=h frame.context.line %></div>
295
+ </div>
296
+
297
+ <% end %>
298
+ </li>
299
+ <% } %>
300
+ </ul>
301
+ </div>
302
+
303
+ <div id="requestinfo">
304
+ <h2>Request information</h2>
305
+
306
+ <% info_groups.each do |(label, data)| %>
307
+ <h3 id="<%=h ident(label) %>-info"><%=h label %></h3>
308
+ <% unless data.nil? || data.empty?
309
+ header = data.shift %>
310
+ <table class="req">
311
+ <thead>
312
+ <tr>
313
+ <% header.each do |hd| %>
314
+ <th><%=h hd %></th>
315
+ <% end %>
316
+ </tr>
317
+ </thead>
318
+ <tbody>
319
+ <% data.each do |(key, val)| %>
320
+ <tr>
321
+ <td><%=h key %></td>
322
+ <td class="code"><div><%= val == hidden ? "<em>(hidden)</em>" : h(val) %></div></td>
323
+ </tr>
324
+ <% end %>
325
+ </tbody>
326
+ </table>
327
+ <% else %>
328
+ <p>No data.</p>
329
+ <% end %>
330
+ <% end %>
331
+
332
+ </div>
333
+
334
+ <div id="explanation">
335
+ <p>
336
+ You're seeing this error because you use <code>Rack::ShowStackDeck</code>.
337
+ </p>
338
+ </div>
339
+
340
+ </body>
341
+ </html>
342
+ HTML
343
+
344
+ # :startdoc:
345
+ end
346
+ end
347
+
@@ -15,7 +15,7 @@ module StackDeck
15
15
  pre_range, post_range = context_ranges(lines, index)
16
16
  @before_lineno = pre_range.begin
17
17
  @before = lines[pre_range]
18
- @line = lines[index].chomp
18
+ @line = lines[index]
19
19
  @after = lines[post_range]
20
20
  else
21
21
  @line = lines.join("\n")
@@ -24,7 +24,7 @@ module StackDeck
24
24
 
25
25
  class File < Context
26
26
  def initialize(filename, lineno)
27
- super File.readlines(filename), lineno
27
+ super ::File.readlines(filename).map {|l| l.chomp }, lineno
28
28
  end
29
29
  end
30
30
  end
@@ -2,24 +2,42 @@
2
2
  module StackDeck
3
3
  module ExceptionSupport
4
4
  def higher_stack_deck; @higher_stack_deck ||= []; end
5
- def stack_deck
5
+ def append_to_stack_deck(frames)
6
+ higher_stack_deck.concat(frames)
7
+ set_backtrace_from_stack_deck!
8
+ end
9
+ def stack_deck(stop_at_boundary=true)
6
10
  deck = []
7
11
  deck.concat(internal_stack_deck || []) if respond_to? :internal_stack_deck
8
12
  deck.concat higher_stack_deck
9
- deck.concat StackDeck::Frame::Ruby.extract(backtrace) if backtrace
13
+ remainder = @stack_deck_backtrace_remaining || backtrace
14
+ deck.concat StackDeck::Frame::Ruby.extract(remainder) if remainder
10
15
  deck.compact!
11
- StackDeck.apply_boundary!(deck)
16
+ StackDeck.apply_boundary!(deck) if stop_at_boundary
12
17
  deck
13
18
  end
14
19
  def copy_ruby_stack_to_deck(ignored_part=caller)
15
- upper_backtrace, lower_backtrace = StackDeck.split_list(backtrace, ignored_part)
20
+ @stack_deck_backtrace_remaining ||= backtrace
21
+ upper_backtrace, lower_backtrace = StackDeck.split_list(@stack_deck_backtrace_remaining, ignored_part)
16
22
  higher_stack_deck.concat StackDeck::Frame::Ruby.extract(upper_backtrace)
17
- set_backtrace lower_backtrace
23
+ @stack_deck_backtrace_remaining = lower_backtrace
24
+ set_backtrace_from_stack_deck!
25
+ end
26
+ def set_backtrace_from_stack_deck!
27
+ @stack_deck_backtrace_remaining ||= backtrace
28
+ set_backtrace_without_stack_deck stack_deck(false).map {|fr| fr.to_s }
29
+ end
30
+ def set_backtrace_with_stack_deck(frames)
31
+ @stack_deck_backtrace_remaining = nil
32
+ @higher_stack_deck = []
33
+ set_backtrace_without_stack_deck frames
18
34
  end
19
35
  end
20
36
  end
21
37
 
22
38
  class Exception
23
39
  include StackDeck::ExceptionSupport
40
+ alias :set_backtrace_without_stack_deck :set_backtrace
41
+ alias :set_backtrace :set_backtrace_with_stack_deck
24
42
  end
25
43
 
@@ -34,6 +34,9 @@ module StackDeck
34
34
  def context
35
35
  @context ||= Context::File.new(filename, lineno) if filename
36
36
  end
37
+ def context?
38
+ !filename.nil?
39
+ end
37
40
  def same_line?(other)
38
41
  other && self.filename == other.filename && self.lineno == other.lineno
39
42
  end
@@ -41,7 +44,19 @@ module StackDeck
41
44
  def boundary?; false; end
42
45
 
43
46
  def to_s
44
- "#{filename}:#{lineno}: in `#{function}' [#{language}]"
47
+ if filename
48
+ if function && function != ''
49
+ "#{filename}:#{lineno}:in `#{function}' [#{language}]"
50
+ else
51
+ "#{filename}:#{lineno} [#{language}]"
52
+ end
53
+ else
54
+ if function && function != ''
55
+ "(#{language}):#{lineno}:in `#{function}'"
56
+ else
57
+ "(#{language}):#{lineno}"
58
+ end
59
+ end
45
60
  end
46
61
 
47
62
  class Ruby < Frame
@@ -58,7 +73,11 @@ module StackDeck
58
73
  filename == __FILE__ && function == 'boundary'
59
74
  end
60
75
  def to_s
61
- "#{filename}:#{lineno}: in `#{function}'"
76
+ if function && function != ''
77
+ "#{filename}:#{lineno}:in `#{function}'"
78
+ else
79
+ "#{filename}:#{lineno}"
80
+ end
62
81
  end
63
82
  end
64
83
 
@@ -95,6 +114,9 @@ module StackDeck
95
114
  def context
96
115
  @context ||= Context.new(@query.split(/\n/), lineno) if @query
97
116
  end
117
+ def context?
118
+ !@query.nil?
119
+ end
98
120
  end
99
121
  end
100
122
  end
@@ -33,18 +33,21 @@ module StackDeck
33
33
  def context
34
34
  @context ||= Context::PgProc.new(@db_connection, function, lineno) if @db_connection
35
35
  end
36
+ def context?
37
+ !context.nil?
38
+ end
36
39
  def language
37
40
  @language.gsub(/ function$/, '')
38
41
  end
39
42
  end
40
43
 
41
- def self.extract(ex)
44
+ def self.extract(ex, conn=nil)
42
45
  postgres_stack = []
43
46
  if ex.internal_query
44
47
  postgres_stack << Frame::SQL.from_char(ex.internal_query, ex.internal_position)
45
48
  end
46
49
  if ex.context
47
- postgres_stack.concat ex.context.split(/\n/).map {|s| parse(s) }
50
+ postgres_stack.concat ex.context.split(/\n/).map {|s| parse(s, conn) }
48
51
  end
49
52
  if ex.query
50
53
  postgres_stack << Frame::SQL.from_char(ex.query, ex.query_position)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stackdeck
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Draper
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-10 00:00:00 +10:30
12
+ date: 2010-02-09 00:00:00 +10:30
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -29,6 +29,7 @@ files:
29
29
  - README.rdoc
30
30
  - Rakefile
31
31
  - VERSION
32
+ - lib/rack/show_stackdeck.rb
32
33
  - lib/stackdeck.rb
33
34
  - lib/stackdeck/context.rb
34
35
  - lib/stackdeck/exception.rb