mongrel2 0.15.1 → 0.16.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.tar.gz.sig +0 -0
- data/ChangeLog +51 -1
- data/History.rdoc +5 -0
- data/Manifest.txt +5 -0
- data/README.rdoc +14 -9
- data/bin/m2sh.rb +2 -0
- data/data/mongrel2/bootstrap.html +24 -22
- data/data/mongrel2/css/master.css +30 -1
- data/data/mongrel2/js/websock-test.js +108 -0
- data/data/mongrel2/websock-test.html +33 -0
- data/examples/config.rb +10 -10
- data/examples/ws-echo.rb +251 -0
- data/lib/mongrel2.rb +3 -2
- data/lib/mongrel2/config/server.rb +2 -0
- data/lib/mongrel2/connection.rb +2 -2
- data/lib/mongrel2/handler.rb +42 -28
- data/lib/mongrel2/request.rb +4 -3
- data/lib/mongrel2/testing.rb +201 -2
- data/lib/mongrel2/websocket.rb +561 -0
- data/spec/lib/constants.rb +2 -1
- data/spec/mongrel2/httprequest_spec.rb +1 -0
- data/spec/mongrel2/request_spec.rb +1 -1
- data/spec/mongrel2/websocket_spec.rb +295 -0
- metadata +48 -46
- metadata.gz.sig +0 -0
data/examples/ws-echo.rb
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
require 'pathname'
|
5
|
+
require 'logger'
|
6
|
+
require 'mongrel2/config'
|
7
|
+
require 'mongrel2/logging'
|
8
|
+
require 'mongrel2/handler'
|
9
|
+
|
10
|
+
|
11
|
+
# An example of a WebSocket (RFC6455) Mongrel2 application that echoes back whatever
|
12
|
+
# (non-control) frames you send it. It will also ping all of its clients and disconnect
|
13
|
+
# ones that don't reply within a given period.
|
14
|
+
class WebSocketEchoServer < Mongrel2::Handler
|
15
|
+
include Mongrel2::WebSocket::Constants
|
16
|
+
|
17
|
+
# Number of seconds to wait between heartbeat PING frames
|
18
|
+
HEARTBEAT_RATE = 5.0
|
19
|
+
|
20
|
+
# Number of seconds to wait for a data frame or a PONG before considering
|
21
|
+
# a socket idle
|
22
|
+
SOCKET_IDLE_TIMEOUT = 15.0
|
23
|
+
|
24
|
+
|
25
|
+
# Add an instance variable to keep track of connections, and one for the
|
26
|
+
# heartbeat thread.
|
27
|
+
def initialize( * )
|
28
|
+
super
|
29
|
+
@connections = {}
|
30
|
+
@heartbeat = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
######
|
35
|
+
public
|
36
|
+
######
|
37
|
+
|
38
|
+
# A Hash of last seen times keyed by [sender ID, connection ID] tuple
|
39
|
+
attr_reader :connections
|
40
|
+
|
41
|
+
# The Thread that sends PING frames to connected sockets and cull any
|
42
|
+
# that don't reply within SOCKET_IDLE_TIMEOUT seconds
|
43
|
+
attr_reader :heartbeat
|
44
|
+
|
45
|
+
|
46
|
+
# Called by Mongrel2::Handler when it starts accepting requests. Overridden
|
47
|
+
# to start up the heartbeat thread.
|
48
|
+
def start_accepting_requests
|
49
|
+
self.start_heartbeat
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# Called by Mongrel2::Handler when the server is restarted. Overridden to
|
55
|
+
# restart the heartbeat thread.
|
56
|
+
def restart
|
57
|
+
self.stop_heartbeat
|
58
|
+
super
|
59
|
+
self.start_heartbeat
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Called by Mongrel2::Handler when the server is shut down. Overridden to
|
64
|
+
# stop the heartbeat thread.
|
65
|
+
def shutdown
|
66
|
+
self.stop_heartbeat
|
67
|
+
super
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
# Mongrel2 will send a disconnect notice when a client's connection closes;
|
72
|
+
# delete the connection when it does.
|
73
|
+
def handle_disconnect( request )
|
74
|
+
self.log.info "Client %d: disconnect." % [ request.conn_id ]
|
75
|
+
self.connections.delete( [request.sender_id, request.conn_id] )
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Non-websocket (e.g., plain HTTP) requests would ordinarily just get
|
81
|
+
# a 204 NO CONTENT response, but we tell Mongrel2 to just drop such connections
|
82
|
+
# immediately.
|
83
|
+
def handle( request )
|
84
|
+
self.log.info "Regular HTTP request (%s): closing channel." % [ request ]
|
85
|
+
self.conn.reply_close( request )
|
86
|
+
return nil
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# This is the main handler for WebSocket requests. Each frame comes in as a
|
91
|
+
# Mongrel::WebSocket::Frame object, and then is dispatched according to what
|
92
|
+
# opcode it has.
|
93
|
+
def handle_websocket( frame )
|
94
|
+
|
95
|
+
# Log each frame
|
96
|
+
self.log.info "%s/%d: %s%s [%s]: %p" % [
|
97
|
+
frame.sender_id,
|
98
|
+
frame.conn_id,
|
99
|
+
frame.opcode.to_s.upcase,
|
100
|
+
frame.fin? ? '' : '(cont)',
|
101
|
+
frame.headers.x_forwarded_for,
|
102
|
+
frame.payload[ 0, 20 ],
|
103
|
+
]
|
104
|
+
|
105
|
+
# If a client sends an invalid frame, close their connection, but politely.
|
106
|
+
if !frame.valid?
|
107
|
+
self.log.error " invalid frame from client: %s" % [ frame.errors.join(';') ]
|
108
|
+
res = frame.response( :close )
|
109
|
+
res.set_status( CLOSE_PROTOCOL_ERROR )
|
110
|
+
return res
|
111
|
+
end
|
112
|
+
|
113
|
+
# Update the 'last-seen' time unless the connection is closing
|
114
|
+
unless frame.opcode == :close
|
115
|
+
@connections[ [frame.sender_id, frame.conn_id] ] = Time.now
|
116
|
+
end
|
117
|
+
|
118
|
+
# Use the opcode to decide what method to call
|
119
|
+
self.log.debug "Handling a %s frame." % [ frame.opcode ]
|
120
|
+
handler = self.method( "handle_%s_frame" % [frame.opcode] )
|
121
|
+
return handler.call( frame )
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# Handle TEXT, BINARY, and CONTINUATION frames by replying with an echo of the
|
126
|
+
# same data. Fragmented frames get echoed back as-is without any reassembly.
|
127
|
+
def handle_text_frame( frame )
|
128
|
+
self.log.info "Echoing data frame: %p" % [ frame ]
|
129
|
+
|
130
|
+
# Make the response frame
|
131
|
+
response = frame.response
|
132
|
+
response.fin = frame.fin?
|
133
|
+
response.payload = frame.payload
|
134
|
+
|
135
|
+
return response
|
136
|
+
end
|
137
|
+
alias_method :handle_binary_frame, :handle_text_frame
|
138
|
+
alias_method :handle_continuation_frame, :handle_text_frame
|
139
|
+
|
140
|
+
|
141
|
+
# Handle close frames
|
142
|
+
def handle_close_frame( frame )
|
143
|
+
|
144
|
+
# There will still be a connection slot if this close originated with
|
145
|
+
# the client. In that case, reply with the ACK CLOSE frame
|
146
|
+
self.conn.reply( frame.response(:close) ) if
|
147
|
+
self.connections.delete( [frame.sender_id, frame.conn_id] )
|
148
|
+
|
149
|
+
self.conn.reply_close( frame )
|
150
|
+
return nil
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# Handle a PING frame; the response is a PONG with the same payload.
|
155
|
+
def handle_ping_frame( frame )
|
156
|
+
return frame.response
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
# Handle a PONG frame; nothing really to do
|
161
|
+
def handle_pong_frame( frame )
|
162
|
+
return nil
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
# Start a thread that will periodically ping connected sockets and remove any
|
167
|
+
# connections that don't reply
|
168
|
+
def start_heartbeat
|
169
|
+
self.log.info "Starting heartbeat thread."
|
170
|
+
@heartbeat = Thread.new do
|
171
|
+
Thread.current.abort_on_exception = true
|
172
|
+
self.log.debug " Heartbeat thread started: %p" % [ Thread.current ]
|
173
|
+
|
174
|
+
# Use a thread-local variable to signal the thread to shut down
|
175
|
+
Thread.current[ :shutdown ] = false
|
176
|
+
until Thread.current[ :shutdown ]
|
177
|
+
|
178
|
+
# If there are active connections, remove any that have
|
179
|
+
# timed out and ping the rest
|
180
|
+
unless self.connections.empty?
|
181
|
+
self.cull_idle_sockets
|
182
|
+
self.ping_all_sockets
|
183
|
+
end
|
184
|
+
|
185
|
+
self.log.debug " heartbeat thread sleeping"
|
186
|
+
sleep( HEARTBEAT_RATE )
|
187
|
+
self.log.debug " heartbeat thread waking up"
|
188
|
+
end
|
189
|
+
|
190
|
+
self.log.info "Hearbeat thread exiting."
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
# Tell the heartbeat thread to exit.
|
196
|
+
def stop_heartbeat
|
197
|
+
@heartbeat[ :shutdown ] = true
|
198
|
+
@heartbeat.run.join if @heartbeat.alive?
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
# Disconnect any sockets that haven't sent any frames for at least
|
203
|
+
# SOCKET_IDLE_TIMEOUT seconds.
|
204
|
+
def cull_idle_sockets
|
205
|
+
self.log.debug "Culling idle sockets."
|
206
|
+
|
207
|
+
earliest = Time.now - SOCKET_IDLE_TIMEOUT
|
208
|
+
|
209
|
+
self.connections.each do |(sender_id, conn_id), lastframe|
|
210
|
+
next unless earliest > lastframe
|
211
|
+
|
212
|
+
# Make a CLOSE frame
|
213
|
+
frame = Mongrel2::WebSocket::Frame.new( sender_id, conn_id, '', {}, '' )
|
214
|
+
frame.opcode = :close
|
215
|
+
frame.set_status( CLOSE_EXCEPTION )
|
216
|
+
|
217
|
+
# Use the connection directly so we can send a frame and close the
|
218
|
+
# connection
|
219
|
+
self.conn.reply( frame )
|
220
|
+
self.conn.send_close( sender_id, conn_id )
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
# Send a PING frame to all connected sockets.
|
226
|
+
def ping_all_sockets
|
227
|
+
self.log.debug "Pinging all connected sockets."
|
228
|
+
|
229
|
+
self.connections.each do |(sender_id, conn_id), hash|
|
230
|
+
frame = Mongrel2::WebSocket::Frame.new( sender_id, conn_id, '', {}, 'heartbeat' )
|
231
|
+
frame.opcode = :ping
|
232
|
+
frame.fin = true
|
233
|
+
|
234
|
+
self.log.debug " %s/%d: PING" % [ sender_id, conn_id ]
|
235
|
+
self.conn.reply( frame )
|
236
|
+
end
|
237
|
+
|
238
|
+
self.log.debug " done with pings."
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
end # class RequestDumper
|
243
|
+
|
244
|
+
Mongrel2.log.level = $DEBUG||$VERBOSE ? Logger::DEBUG : Logger::INFO
|
245
|
+
Mongrel2.log.formatter = Mongrel2::Logging::ColorFormatter.new( Mongrel2.log ) if $stdin.tty?
|
246
|
+
|
247
|
+
# Point to the config database, which will cause the handler to use
|
248
|
+
# its ID to look up its own socket info.
|
249
|
+
Mongrel2::Config.configure( :configdb => 'examples.sqlite' )
|
250
|
+
WebSocketEchoServer.run( 'ws-echo' )
|
251
|
+
|
data/lib/mongrel2.rb
CHANGED
@@ -14,10 +14,10 @@ module Mongrel2
|
|
14
14
|
abort "\n\n>>> Mongrel2 requires Ruby 1.9.2 or later. <<<\n\n" if RUBY_VERSION < '1.9.2'
|
15
15
|
|
16
16
|
# Library version constant
|
17
|
-
VERSION = '0.
|
17
|
+
VERSION = '0.16.0'
|
18
18
|
|
19
19
|
# Version-control revision constant
|
20
|
-
REVISION = %q$Revision:
|
20
|
+
REVISION = %q$Revision: 4fc45a463af8 $
|
21
21
|
|
22
22
|
|
23
23
|
require 'mongrel2/logging'
|
@@ -57,6 +57,7 @@ module Mongrel2
|
|
57
57
|
require 'mongrel2/httprequest'
|
58
58
|
require 'mongrel2/jsonrequest'
|
59
59
|
require 'mongrel2/xmlrequest'
|
60
|
+
require 'mongrel2/websocket'
|
60
61
|
require 'mongrel2/response'
|
61
62
|
require 'mongrel2/control'
|
62
63
|
|
@@ -5,10 +5,12 @@ require 'pathname'
|
|
5
5
|
|
6
6
|
require 'mongrel2' unless defined?( Mongrel2 )
|
7
7
|
require 'mongrel2/config' unless defined?( Mongrel2::Config )
|
8
|
+
require 'mongrel2/constants'
|
8
9
|
|
9
10
|
|
10
11
|
# Mongrel2 Server configuration class
|
11
12
|
class Mongrel2::Config::Server < Mongrel2::Config( :server )
|
13
|
+
include Mongrel2::Constants
|
12
14
|
|
13
15
|
### As of Mongrel2/1.7.5:
|
14
16
|
# CREATE TABLE server (id INTEGER PRIMARY KEY,
|
data/lib/mongrel2/connection.rb
CHANGED
@@ -136,7 +136,7 @@ class Mongrel2::Connection
|
|
136
136
|
### via the server specified by +sender_id+. The +client_ids+ should be an Array of
|
137
137
|
### Integer IDs no longer than Mongrel2::MAX_IDENTS.
|
138
138
|
def broadcast( sender_id, conn_ids, data )
|
139
|
-
idlist = conn_ids.map( &:to_s ).join( ' ' )
|
139
|
+
idlist = conn_ids.flatten.map( &:to_s ).join( ' ' )
|
140
140
|
self.send( sender_id, idlist, data )
|
141
141
|
end
|
142
142
|
|
@@ -188,7 +188,7 @@ class Mongrel2::Connection
|
|
188
188
|
### Returns a string containing a human-readable representation of the Connection,
|
189
189
|
### suitable for debugging.
|
190
190
|
def inspect
|
191
|
-
state = if @
|
191
|
+
state = if @request_sock
|
192
192
|
if self.closed?
|
193
193
|
"closed"
|
194
194
|
else
|
data/lib/mongrel2/handler.rb
CHANGED
@@ -8,6 +8,8 @@ require 'mongrel2/config'
|
|
8
8
|
require 'mongrel2/request'
|
9
9
|
require 'mongrel2/httprequest'
|
10
10
|
require 'mongrel2/jsonrequest'
|
11
|
+
require 'mongrel2/xmlrequest'
|
12
|
+
require 'mongrel2/websocket'
|
11
13
|
|
12
14
|
# Mongrel2 Handler application class. Instances of this class are the applications
|
13
15
|
# which connection to one or more Mongrel2 routes and respond to requests.
|
@@ -18,27 +20,27 @@ require 'mongrel2/jsonrequest'
|
|
18
20
|
# document with a timestamp.
|
19
21
|
#
|
20
22
|
# #!/usr/bin/env ruby
|
21
|
-
#
|
23
|
+
#
|
22
24
|
# require 'mongrel2/handler'
|
23
|
-
#
|
25
|
+
#
|
24
26
|
# class HelloWorldHandler < Mongrel2::Handler
|
25
|
-
#
|
26
|
-
# ### The main method to override -- accepts requests and
|
27
|
+
#
|
28
|
+
# ### The main method to override -- accepts requests and
|
27
29
|
# ### returns responses.
|
28
30
|
# def handle( request )
|
29
31
|
# response = request.response
|
30
|
-
#
|
32
|
+
#
|
31
33
|
# response.status = 200
|
32
34
|
# response.headers.content_type = 'text/plain'
|
33
35
|
# response.puts "Hello, world, it's #{Time.now}!"
|
34
|
-
#
|
36
|
+
#
|
35
37
|
# return response
|
36
38
|
# end
|
37
|
-
#
|
39
|
+
#
|
38
40
|
# end # class HelloWorldHandler
|
39
|
-
#
|
41
|
+
#
|
40
42
|
# HelloWorldHandler.run( 'helloworld-handler' )
|
41
|
-
#
|
43
|
+
#
|
42
44
|
# This assumes the Mongrel2 SQLite config database is in the current
|
43
45
|
# directory, and is named 'config.sqlite' (the Mongrel2 default), but
|
44
46
|
# if it's somewhere else, you can point the Mongrel2::Config class
|
@@ -46,7 +48,7 @@ require 'mongrel2/jsonrequest'
|
|
46
48
|
#
|
47
49
|
# require 'mongrel2/config'
|
48
50
|
# Mongrel2::Config.configure( :configdb => 'mongrel2.db' )
|
49
|
-
#
|
51
|
+
#
|
50
52
|
# Mongrel2 also includes support for Configurability, so you can
|
51
53
|
# configure it along with your database connection, etc. Just add a
|
52
54
|
# 'mongrel2' section to the config with a 'configdb' key that points
|
@@ -55,14 +57,14 @@ require 'mongrel2/jsonrequest'
|
|
55
57
|
# # config.yaml
|
56
58
|
# db:
|
57
59
|
# uri: postgres://www@localhost/db01
|
58
|
-
#
|
60
|
+
#
|
59
61
|
# mongrel2:
|
60
62
|
# configdb: mongrel2.db
|
61
|
-
#
|
63
|
+
#
|
62
64
|
# whatever_else:
|
63
65
|
# ...
|
64
|
-
#
|
65
|
-
# Now just loading and installing the config configures Mongrel2 as
|
66
|
+
#
|
67
|
+
# Now just loading and installing the config configures Mongrel2 as
|
66
68
|
# well:
|
67
69
|
#
|
68
70
|
# require 'configurability/config'
|
@@ -77,7 +79,7 @@ require 'mongrel2/jsonrequest'
|
|
77
79
|
# app = HelloWorldHandler.new( 'helloworld-handler',
|
78
80
|
# 'tcp://otherhost:9999', 'tcp://otherhost:9998' )
|
79
81
|
# app.run
|
80
|
-
#
|
82
|
+
#
|
81
83
|
class Mongrel2::Handler
|
82
84
|
include Mongrel2::Loggable,
|
83
85
|
Mongrel2::Constants
|
@@ -159,22 +161,20 @@ class Mongrel2::Handler
|
|
159
161
|
def start_accepting_requests
|
160
162
|
until @conn.closed?
|
161
163
|
req = @conn.receive
|
162
|
-
self.log.info( req.inspect )
|
163
|
-
|
164
164
|
res = self.dispatch_request( req )
|
165
165
|
|
166
166
|
if res
|
167
167
|
self.log.info( res.inspect )
|
168
168
|
@conn.reply( res ) unless @conn.closed?
|
169
|
-
else
|
170
|
-
self.log.info " no response; ignoring."
|
171
169
|
end
|
172
170
|
end
|
173
171
|
rescue ZMQ::Error => err
|
174
|
-
|
175
|
-
|
172
|
+
unless @conn.closed?
|
173
|
+
self.log.error "%p while accepting requests: %s" % [ err.class, err.message ]
|
174
|
+
self.log.debug { err.backtrace.join("\n ") }
|
176
175
|
|
177
|
-
|
176
|
+
retry
|
177
|
+
end
|
178
178
|
end
|
179
179
|
|
180
180
|
|
@@ -196,8 +196,12 @@ class Mongrel2::Handler
|
|
196
196
|
when Mongrel2::XMLRequest
|
197
197
|
self.log.debug "XML message request."
|
198
198
|
return self.handle_xml( request )
|
199
|
+
when Mongrel2::WebSocket::Frame
|
200
|
+
self.log.debug "WEBSOCKET message request."
|
201
|
+
return self.handle_websocket( request )
|
199
202
|
else
|
200
|
-
self.log.error "Unhandled request type %p" %
|
203
|
+
self.log.error "Unhandled request type %s (%p)" %
|
204
|
+
[ request.headers['METHOD'], request.class ]
|
201
205
|
return nil
|
202
206
|
end
|
203
207
|
end
|
@@ -235,22 +239,32 @@ class Mongrel2::Handler
|
|
235
239
|
end
|
236
240
|
|
237
241
|
|
238
|
-
### Handle a JSON message +request+. If not overridden, JSON message ('@route')
|
242
|
+
### Handle a JSON message +request+. If not overridden, JSON message ('@route')
|
239
243
|
### requests are ignored.
|
240
244
|
def handle_json( request )
|
241
|
-
self.log.warn "Unhandled JSON message request (%p)" % [ request.headers
|
245
|
+
self.log.warn "Unhandled JSON message request (%p)" % [ request.headers.path ]
|
242
246
|
return nil
|
243
247
|
end
|
244
248
|
|
245
249
|
|
246
|
-
### Handle an XML message +request+. If not overridden, XML message ('<route')
|
250
|
+
### Handle an XML message +request+. If not overridden, XML message ('<route')
|
247
251
|
### requests are ignored.
|
248
252
|
def handle_xml( request )
|
249
|
-
self.log.warn "Unhandled XML message request (%p)" % [ request.headers
|
253
|
+
self.log.warn "Unhandled XML message request (%p)" % [ request.headers.pack ]
|
250
254
|
return nil
|
251
255
|
end
|
252
256
|
|
253
257
|
|
258
|
+
### Handle a WebSocket frame in +request+. If not overridden, WebSocket connections are
|
259
|
+
### closed with a policy error status.
|
260
|
+
def handle_websocket( request )
|
261
|
+
self.log.warn "Unhandled WEBSOCKET message request (%p)" % [ request.headers.path ]
|
262
|
+
res = request.response
|
263
|
+
res.make_close_frame( WebSocket::CLOSE_POLICY_VIOLATION )
|
264
|
+
return res
|
265
|
+
end
|
266
|
+
|
267
|
+
|
254
268
|
### Handle a disconnect notice from Mongrel2 via the given +request+. Its return value
|
255
269
|
### is ignored.
|
256
270
|
def handle_disconnect( request )
|
@@ -267,7 +281,7 @@ class Mongrel2::Handler
|
|
267
281
|
#
|
268
282
|
|
269
283
|
### Set up signal handlers for SIGINT, SIGTERM, SIGINT, and SIGUSR1 that will call the
|
270
|
-
### #on_hangup_signal, #on_termination_signal, #on_interrupt_signal, and
|
284
|
+
### #on_hangup_signal, #on_termination_signal, #on_interrupt_signal, and
|
271
285
|
### #on_user1_signal methods, respectively.
|
272
286
|
def set_signal_handlers
|
273
287
|
Signal.trap( :HUP, &self.method(:on_hangup_signal) )
|