stackdeck 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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