macournoyer-thin 1.0.1

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.
Files changed (135) hide show
  1. data/CHANGELOG +233 -0
  2. data/COPYING +18 -0
  3. data/README +77 -0
  4. data/Rakefile +13 -0
  5. data/benchmark/abc +51 -0
  6. data/benchmark/benchmarker.rb +80 -0
  7. data/benchmark/runner +79 -0
  8. data/bin/thin +6 -0
  9. data/example/adapter.rb +32 -0
  10. data/example/config.ru +23 -0
  11. data/example/monit_sockets +20 -0
  12. data/example/monit_unixsock +20 -0
  13. data/example/myapp.rb +1 -0
  14. data/example/ramaze.ru +12 -0
  15. data/example/thin.god +80 -0
  16. data/example/thin_solaris_smf.erb +36 -0
  17. data/example/thin_solaris_smf.readme.txt +150 -0
  18. data/example/vlad.rake +64 -0
  19. data/ext/thin_parser/common.rl +55 -0
  20. data/ext/thin_parser/ext_help.h +14 -0
  21. data/ext/thin_parser/extconf.rb +6 -0
  22. data/ext/thin_parser/parser.c +452 -0
  23. data/ext/thin_parser/parser.h +49 -0
  24. data/ext/thin_parser/parser.rl +157 -0
  25. data/ext/thin_parser/thin.c +433 -0
  26. data/lib/rack/adapter/loader.rb +79 -0
  27. data/lib/rack/adapter/rails.rb +173 -0
  28. data/lib/rack/handler/thin.rb +18 -0
  29. data/lib/thin.rb +50 -0
  30. data/lib/thin/backends/base.rb +141 -0
  31. data/lib/thin/backends/swiftiply_client.rb +56 -0
  32. data/lib/thin/backends/tcp_server.rb +29 -0
  33. data/lib/thin/backends/unix_server.rb +51 -0
  34. data/lib/thin/command.rb +52 -0
  35. data/lib/thin/connection.rb +186 -0
  36. data/lib/thin/controllers/cluster.rb +127 -0
  37. data/lib/thin/controllers/controller.rb +183 -0
  38. data/lib/thin/controllers/service.rb +75 -0
  39. data/lib/thin/controllers/service.sh.erb +39 -0
  40. data/lib/thin/daemonizing.rb +163 -0
  41. data/lib/thin/headers.rb +39 -0
  42. data/lib/thin/logging.rb +54 -0
  43. data/lib/thin/request.rb +147 -0
  44. data/lib/thin/response.rb +99 -0
  45. data/lib/thin/runner.rb +208 -0
  46. data/lib/thin/server.rb +241 -0
  47. data/lib/thin/stats.html.erb +216 -0
  48. data/lib/thin/stats.rb +52 -0
  49. data/lib/thin/statuses.rb +43 -0
  50. data/lib/thin/version.rb +32 -0
  51. data/spec/backends/swiftiply_client_spec.rb +66 -0
  52. data/spec/backends/tcp_server_spec.rb +33 -0
  53. data/spec/backends/unix_server_spec.rb +37 -0
  54. data/spec/command_spec.rb +20 -0
  55. data/spec/configs/cluster.yml +9 -0
  56. data/spec/configs/single.yml +9 -0
  57. data/spec/connection_spec.rb +105 -0
  58. data/spec/controllers/cluster_spec.rb +212 -0
  59. data/spec/controllers/controller_spec.rb +129 -0
  60. data/spec/controllers/service_spec.rb +50 -0
  61. data/spec/daemonizing_spec.rb +192 -0
  62. data/spec/headers_spec.rb +40 -0
  63. data/spec/logging_spec.rb +46 -0
  64. data/spec/perf/request_perf_spec.rb +50 -0
  65. data/spec/perf/response_perf_spec.rb +19 -0
  66. data/spec/perf/server_perf_spec.rb +39 -0
  67. data/spec/rack/loader_spec.rb +29 -0
  68. data/spec/rack/rails_adapter_spec.rb +106 -0
  69. data/spec/rails_app/app/controllers/application.rb +10 -0
  70. data/spec/rails_app/app/controllers/simple_controller.rb +19 -0
  71. data/spec/rails_app/app/helpers/application_helper.rb +3 -0
  72. data/spec/rails_app/app/views/simple/index.html.erb +15 -0
  73. data/spec/rails_app/config/boot.rb +109 -0
  74. data/spec/rails_app/config/environment.rb +64 -0
  75. data/spec/rails_app/config/environments/development.rb +18 -0
  76. data/spec/rails_app/config/environments/production.rb +19 -0
  77. data/spec/rails_app/config/environments/test.rb +22 -0
  78. data/spec/rails_app/config/initializers/inflections.rb +10 -0
  79. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  80. data/spec/rails_app/config/routes.rb +35 -0
  81. data/spec/rails_app/public/404.html +30 -0
  82. data/spec/rails_app/public/422.html +30 -0
  83. data/spec/rails_app/public/500.html +30 -0
  84. data/spec/rails_app/public/dispatch.cgi +10 -0
  85. data/spec/rails_app/public/dispatch.fcgi +24 -0
  86. data/spec/rails_app/public/dispatch.rb +10 -0
  87. data/spec/rails_app/public/favicon.ico +0 -0
  88. data/spec/rails_app/public/images/rails.png +0 -0
  89. data/spec/rails_app/public/index.html +277 -0
  90. data/spec/rails_app/public/javascripts/application.js +2 -0
  91. data/spec/rails_app/public/javascripts/controls.js +963 -0
  92. data/spec/rails_app/public/javascripts/dragdrop.js +972 -0
  93. data/spec/rails_app/public/javascripts/effects.js +1120 -0
  94. data/spec/rails_app/public/javascripts/prototype.js +4225 -0
  95. data/spec/rails_app/public/robots.txt +5 -0
  96. data/spec/rails_app/script/about +3 -0
  97. data/spec/rails_app/script/console +3 -0
  98. data/spec/rails_app/script/destroy +3 -0
  99. data/spec/rails_app/script/generate +3 -0
  100. data/spec/rails_app/script/performance/benchmarker +3 -0
  101. data/spec/rails_app/script/performance/profiler +3 -0
  102. data/spec/rails_app/script/performance/request +3 -0
  103. data/spec/rails_app/script/plugin +3 -0
  104. data/spec/rails_app/script/process/inspector +3 -0
  105. data/spec/rails_app/script/process/reaper +3 -0
  106. data/spec/rails_app/script/process/spawner +3 -0
  107. data/spec/rails_app/script/runner +3 -0
  108. data/spec/rails_app/script/server +3 -0
  109. data/spec/request/mongrel_spec.rb +39 -0
  110. data/spec/request/parser_spec.rb +215 -0
  111. data/spec/request/persistent_spec.rb +35 -0
  112. data/spec/request/processing_spec.rb +45 -0
  113. data/spec/response_spec.rb +83 -0
  114. data/spec/runner_spec.rb +167 -0
  115. data/spec/server/builder_spec.rb +44 -0
  116. data/spec/server/pipelining_spec.rb +109 -0
  117. data/spec/server/robustness_spec.rb +34 -0
  118. data/spec/server/stopping_spec.rb +45 -0
  119. data/spec/server/swiftiply.yml +6 -0
  120. data/spec/server/swiftiply_spec.rb +32 -0
  121. data/spec/server/tcp_spec.rb +57 -0
  122. data/spec/server/threaded_spec.rb +27 -0
  123. data/spec/server/unix_socket_spec.rb +26 -0
  124. data/spec/server_spec.rb +96 -0
  125. data/spec/spec_helper.rb +219 -0
  126. data/tasks/announce.rake +22 -0
  127. data/tasks/deploy.rake +16 -0
  128. data/tasks/email.erb +30 -0
  129. data/tasks/ext.rake +42 -0
  130. data/tasks/gem.rake +108 -0
  131. data/tasks/rdoc.rake +25 -0
  132. data/tasks/site.rake +15 -0
  133. data/tasks/spec.rake +48 -0
  134. data/tasks/stats.rake +28 -0
  135. metadata +248 -0
@@ -0,0 +1,241 @@
1
+ module Thin
2
+ # The uterly famous Thin HTTP server.
3
+ # It listen for incoming request through a given +backend+
4
+ # and forward all request to +app+.
5
+ #
6
+ # == TCP server
7
+ # Create a new TCP server on bound to <tt>host:port</tt> by specifiying +host+
8
+ # and +port+ as the first 2 arguments.
9
+ #
10
+ # Thin::Server.start('0.0.0.0', 3000, app)
11
+ #
12
+ # == UNIX domain server
13
+ # Create a new UNIX domain socket bound to +socket+ file by specifiying a filename
14
+ # as the first argument. Eg.: /tmp/thin.sock. If the first argument contains a <tt>/</tt>
15
+ # it will be assumed to be a UNIX socket.
16
+ #
17
+ # Thin::Server.start('/tmp/thin.sock', app)
18
+ #
19
+ # == Using a custom backend
20
+ # You can implement your own way to connect the server to its client by creating your
21
+ # own Backend class and pass it as the :backend option.
22
+ #
23
+ # Thin::Server.start('galaxy://faraway', 1345, app, :backend => Thin::Backends::MyFancyBackend)
24
+ #
25
+ # == Rack application (+app+)
26
+ # All requests will be processed through +app+ that must be a valid Rack adapter.
27
+ # A valid Rack adapter (application) must respond to <tt>call(env#Hash)</tt> and
28
+ # return an array of <tt>[status, headers, body]</tt>.
29
+ #
30
+ # == Building an app in place
31
+ # If a block is passed, a <tt>Rack::Builder</tt> instance
32
+ # will be passed to build the +app+. So you can do cool stuff like this:
33
+ #
34
+ # Thin::Server.start('0.0.0.0', 3000) do
35
+ # use Rack::CommonLogger
36
+ # use Rack::ShowExceptions
37
+ # map "/lobster" do
38
+ # use Rack::Lint
39
+ # run Rack::Lobster.new
40
+ # end
41
+ # end
42
+ #
43
+ # == Controlling with signals
44
+ # * QUIT: Gracefull shutdown (see Server#stop)
45
+ # * INT and TERM: Force shutdown (see Server#stop!)
46
+ # Disable signals by passing <tt>:signals => false</tt>
47
+ #
48
+ class Server
49
+ include Logging
50
+ include Daemonizable
51
+ extend Forwardable
52
+
53
+ # Default values
54
+ DEFAULT_TIMEOUT = 30 #sec
55
+ DEFAULT_HOST = '0.0.0.0'
56
+ DEFAULT_PORT = 3000
57
+ DEFAULT_MAXIMUM_CONNECTIONS = 1024
58
+ DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS = 512
59
+
60
+ # Application (Rack adapter) called with the request that produces the response.
61
+ attr_accessor :app
62
+
63
+ # Backend handling the connections to the clients.
64
+ attr_accessor :backend
65
+
66
+ # Maximum number of seconds for incoming data to arrive before the connection
67
+ # is dropped.
68
+ def_delegators :backend, :timeout, :timeout=
69
+
70
+ # Maximum number of file or socket descriptors that the server may open.
71
+ def_delegators :backend, :maximum_connections, :maximum_connections=
72
+
73
+ # Maximum number of connection that can be persistent at the same time.
74
+ # Most browser never close the connection so most of the time they are closed
75
+ # when the timeout occur. If we don't control the number of persistent connection,
76
+ # if would be very easy to overflow the server for a DoS attack.
77
+ def_delegators :backend, :maximum_persistent_connections, :maximum_persistent_connections=
78
+
79
+ # Allow using threads in the backend.
80
+ def_delegators :backend, :threaded?, :threaded=
81
+
82
+ # Address and port on which the server is listening for connections.
83
+ def_delegators :backend, :host, :port
84
+
85
+ # UNIX domain socket on which the server is listening for connections.
86
+ def_delegator :backend, :socket
87
+
88
+ # Disable the use of epoll under Linux
89
+ def_delegators :backend, :no_epoll, :no_epoll=
90
+
91
+ def initialize(*args, &block)
92
+ host, port, options = DEFAULT_HOST, DEFAULT_PORT, {}
93
+
94
+ # Guess each parameter by its type so they can be
95
+ # received in any order.
96
+ args.each do |arg|
97
+ case arg
98
+ when Fixnum, /^\d+$/ then port = arg.to_i
99
+ when String then host = arg
100
+ when Hash then options = arg
101
+ else
102
+ @app = arg if arg.respond_to?(:call)
103
+ end
104
+ end
105
+
106
+ # Try to intelligently select which backend to use.
107
+ @backend = select_backend(host, port, options)
108
+
109
+ load_cgi_multipart_eof_fix
110
+
111
+ @backend.server = self
112
+
113
+ # Set defaults
114
+ @backend.maximum_connections = DEFAULT_MAXIMUM_CONNECTIONS
115
+ @backend.maximum_persistent_connections = DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS
116
+ @backend.timeout = DEFAULT_TIMEOUT
117
+
118
+ # Allow using Rack builder as a block
119
+ @app = Rack::Builder.new(&block).to_app if block
120
+
121
+ # If in debug mode, wrap in logger adapter
122
+ @app = Rack::CommonLogger.new(@app) if Logging.debug?
123
+
124
+ setup_signals unless options[:signals].class == FalseClass
125
+ end
126
+
127
+ # Lil' shortcut to turn this:
128
+ #
129
+ # Server.new(...).start
130
+ #
131
+ # into this:
132
+ #
133
+ # Server.start(...)
134
+ #
135
+ def self.start(*args, &block)
136
+ new(*args, &block).start!
137
+ end
138
+
139
+ # Start the server and listen for connections.
140
+ def start
141
+ raise ArgumentError, 'app required' unless @app
142
+
143
+ log ">> Thin web server (v#{VERSION::STRING} codename #{VERSION::CODENAME})"
144
+ debug ">> Debugging ON"
145
+ trace ">> Tracing ON"
146
+
147
+ log ">> Maximum connections set to #{@backend.maximum_connections}"
148
+ log ">> Listening on #{@backend}, CTRL+C to stop"
149
+
150
+ @backend.start
151
+ end
152
+ alias :start! :start
153
+
154
+ # == Gracefull shutdown
155
+ # Stops the server after processing all current connections.
156
+ # As soon as this method is called, the server stops accepting
157
+ # new requests and wait for all current connections to finish.
158
+ # Calling twice is the equivalent of calling <tt>stop!</tt>.
159
+ def stop
160
+ if running?
161
+ @backend.stop
162
+ unless @backend.empty?
163
+ log ">> Waiting for #{@backend.size} connection(s) to finish, " +
164
+ "can take up to #{timeout} sec, CTRL+C to stop now"
165
+ end
166
+ else
167
+ stop!
168
+ end
169
+ end
170
+
171
+ # == Force shutdown
172
+ # Stops the server closing all current connections right away.
173
+ # This doesn't wait for connection to finish their work and send data.
174
+ # All current requests will be dropped.
175
+ def stop!
176
+ log ">> Stopping ..."
177
+
178
+ @backend.stop!
179
+ end
180
+
181
+ # == Configure the server
182
+ # The process might need to have superuser privilege to configure
183
+ # server with optimal options.
184
+ def config
185
+ @backend.config
186
+ end
187
+
188
+ # Name of the server and type of backend used.
189
+ # This is also the name of the process in which Thin is running as a daemon.
190
+ def name
191
+ "thin server (#{@backend})"
192
+ end
193
+ alias :to_s :name
194
+
195
+ # Return +true+ if the server is running and ready to receive requests.
196
+ # Note that the server might still be running and return +false+ when
197
+ # shuting down and waiting for active connections to complete.
198
+ def running?
199
+ @backend.running?
200
+ end
201
+
202
+ protected
203
+ # Register signals:
204
+ # * INT calls +stop+ to shutdown gracefully.
205
+ # * TERM calls <tt>stop!</tt> to force shutdown.
206
+ def setup_signals
207
+ trap('QUIT') { stop } unless Thin.win?
208
+ trap('INT') { stop! }
209
+ trap('TERM') { stop! }
210
+ end
211
+
212
+ def select_backend(host, port, options)
213
+ case
214
+ when options.has_key?(:backend)
215
+ raise ArgumentError, ":backend must be a class" unless options[:backend].is_a?(Class)
216
+ options[:backend].new(host, port, options)
217
+ when options.has_key?(:swiftiply)
218
+ Backends::SwiftiplyClient.new(host, port, options)
219
+ when host.include?('/')
220
+ Backends::UnixServer.new(host)
221
+ else
222
+ Backends::TcpServer.new(host, port)
223
+ end
224
+ end
225
+
226
+ # Taken from Mongrel cgi_multipart_eof_fix
227
+ # Ruby 1.8.5 has a security bug in cgi.rb, we need to patch it.
228
+ def load_cgi_multipart_eof_fix
229
+ version = RUBY_VERSION.split('.').map { |i| i.to_i }
230
+
231
+ if version[0] <= 1 && version[1] <= 8 && version[2] <= 5 && RUBY_PLATFORM !~ /java/
232
+ begin
233
+ require 'cgi_multipart_eof_fix'
234
+ rescue LoadError
235
+ log "!! Ruby 1.8.5 is not secure please install cgi_multipart_eof_fix:"
236
+ log " gem install cgi_multipart_eof_fix"
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,216 @@
1
+ <%#
2
+ # Taken from Rack::ShowException
3
+ # adapted from Django <djangoproject.com>
4
+ # Copyright (c) 2005, the Lawrence Journal-World
5
+ # Used under the modified BSD license:
6
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
7
+ %>
8
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
9
+ <html lang="en">
10
+ <head>
11
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
12
+ <meta name="robots" content="NONE,NOARCHIVE" />
13
+ <title>Thin Stats</title>
14
+ <style type="text/css">
15
+ html * { padding:0; margin:0; }
16
+ body * { padding:10px 20px; }
17
+ body * * { padding:0; }
18
+ body { font:small sans-serif; }
19
+ body>div { border-bottom:1px solid #ddd; }
20
+ h1 { font-weight:normal; }
21
+ h2 { margin-bottom:.8em; }
22
+ h2 span { font-size:80%; color:#666; font-weight:normal; }
23
+ h3 { margin:1em 0 .5em 0; }
24
+ h4 { margin:0 0 .5em 0; font-weight: normal; }
25
+ table {
26
+ border:1px solid #ccc; border-collapse: collapse; background:white; }
27
+ tbody td, tbody th { vertical-align:top; padding:2px 3px; }
28
+ thead th {
29
+ padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
30
+ font-weight:normal; font-size:11px; border:1px solid #ddd; }
31
+ tbody th { text-align:right; color:#666; padding-right:.5em; }
32
+ table.vars { margin:5px 0 2px 40px; }
33
+ table.vars td, table.req td { font-family:monospace; }
34
+ table td.code { width:100%;}
35
+ table td.code div { overflow:hidden; }
36
+ table.source th { color:#666; }
37
+ table.source td {
38
+ font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
39
+ ul.traceback { list-style-type:none; }
40
+ ul.traceback li.frame { margin-bottom:1em; }
41
+ div.context { margin: 10px 0; }
42
+ div.context ol {
43
+ padding-left:30px; margin:0 10px; list-style-position: inside; }
44
+ div.context ol li {
45
+ font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
46
+ div.context ol.context-line li { color:black; background-color:#ccc; }
47
+ div.context ol.context-line li span { float: right; }
48
+ div.commands { margin-left: 40px; }
49
+ div.commands a { color:black; text-decoration:none; }
50
+ #summary { background: #ffc; }
51
+ #summary h2 { font-weight: normal; color: #666; }
52
+ #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
53
+ #summary ul#quicklinks li { float: left; padding: 0 1em; }
54
+ #summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
55
+ #explanation { background:#eee; }
56
+ #template, #template-not-exist { background:#f6f6f6; }
57
+ #template-not-exist ul { margin: 0 0 0 20px; }
58
+ #traceback { background:#eee; }
59
+ #summary table { border:none; background:transparent; }
60
+ #requests { background:#f6f6f6; padding-left:120px; }
61
+ #requests h2, #requests h3 { position:relative; margin-left:-100px; }
62
+ #requests h3 { margin-bottom:-1em; }
63
+ .error { background: #ffc; }
64
+ .specific { color:#cc3300; font-weight:bold; }
65
+ </style>
66
+ </head>
67
+ <body>
68
+
69
+ <div id="summary">
70
+ <h1>Server stats</h1>
71
+ <h2><%= Thin::SERVER %></h2>
72
+ <table>
73
+ <tr>
74
+ <th>Uptime</th>
75
+ <td><%= Time.now - @start_time %> sec</td>
76
+ </tr>
77
+ <tr>
78
+ <th>PID</th>
79
+ <td><%=h Process.pid %></td>
80
+ </tr>
81
+ </table>
82
+
83
+ <% if @last_request %>
84
+ <h3>Jump to:</h3>
85
+ <ul id="quicklinks">
86
+ <li><a href="#get-info">GET</a></li>
87
+ <li><a href="#post-info">POST</a></li>
88
+ <li><a href="#cookie-info">Cookies</a></li>
89
+ <li><a href="#env-info">ENV</a></li>
90
+ </ul>
91
+ <% end %>
92
+ </div>
93
+
94
+ <div id="stats">
95
+ <h2>Requests</h2>
96
+ <h3>Stats</h3>
97
+ <table class="req">
98
+ <tr>
99
+ <td>Requests</td>
100
+ <td><%= @requests %></td>
101
+ </tr>
102
+ <tr>
103
+ <td>Finished</td>
104
+ <td><%= @requests_finished %></td>
105
+ </tr>
106
+ <tr>
107
+ <td>Errors</td>
108
+ <td><%= @requests - @requests_finished %></td>
109
+ </tr>
110
+ <tr>
111
+ <td>Last request</td>
112
+ <td><%= @last_request_time %> sec</td>
113
+ </tr>
114
+ </table>
115
+ </div>
116
+
117
+ <% if @last_request %>
118
+ <div id="requestinfo">
119
+ <h2>Last Request information</h2>
120
+
121
+ <h3 id="get-info">GET</h3>
122
+ <% unless @last_request.GET.empty? %>
123
+ <table class="req">
124
+ <thead>
125
+ <tr>
126
+ <th>Variable</th>
127
+ <th>Value</th>
128
+ </tr>
129
+ </thead>
130
+ <tbody>
131
+ <% @last_request.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
132
+ <tr>
133
+ <td><%=h key %></td>
134
+ <td class="code"><div><%=h val.inspect %></div></td>
135
+ </tr>
136
+ <% } %>
137
+ </tbody>
138
+ </table>
139
+ <% else %>
140
+ <p>No GET data.</p>
141
+ <% end %>
142
+
143
+ <h3 id="post-info">POST</h3>
144
+ <% unless @last_request.POST.empty? %>
145
+ <table class="req">
146
+ <thead>
147
+ <tr>
148
+ <th>Variable</th>
149
+ <th>Value</th>
150
+ </tr>
151
+ </thead>
152
+ <tbody>
153
+ <% @last_request.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
154
+ <tr>
155
+ <td><%=h key %></td>
156
+ <td class="code"><div><%=h val.inspect %></div></td>
157
+ </tr>
158
+ <% } %>
159
+ </tbody>
160
+ </table>
161
+ <% else %>
162
+ <p>No POST data.</p>
163
+ <% end %>
164
+
165
+
166
+ <h3 id="cookie-info">COOKIES</h3>
167
+ <% unless @last_request.cookies.empty? %>
168
+ <table class="req">
169
+ <thead>
170
+ <tr>
171
+ <th>Variable</th>
172
+ <th>Value</th>
173
+ </tr>
174
+ </thead>
175
+ <tbody>
176
+ <% @last_request.cookies.each { |key, val| %>
177
+ <tr>
178
+ <td><%=h key %></td>
179
+ <td class="code"><div><%=h val.inspect %></div></td>
180
+ </tr>
181
+ <% } %>
182
+ </tbody>
183
+ </table>
184
+ <% else %>
185
+ <p>No cookie data.</p>
186
+ <% end %>
187
+
188
+ <h3 id="env-info">Rack ENV</h3>
189
+ <table class="req">
190
+ <thead>
191
+ <tr>
192
+ <th>Variable</th>
193
+ <th>Value</th>
194
+ </tr>
195
+ </thead>
196
+ <tbody>
197
+ <% @last_request.env.sort_by { |k, v| k.to_s }.each { |key, val| %>
198
+ <tr>
199
+ <td><%=h key %></td>
200
+ <td class="code"><div><%=h val %></div></td>
201
+ </tr>
202
+ <% } %>
203
+ </tbody>
204
+ </table>
205
+
206
+ </div>
207
+ <% end %>
208
+
209
+ <div id="explanation">
210
+ <p>
211
+ You're seeing this page because you use <code>Thin::Stats</code>.
212
+ </p>
213
+ </div>
214
+
215
+ </body>
216
+ </html>