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.
@@ -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
+
@@ -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.15.1'
17
+ VERSION = '0.16.0'
18
18
 
19
19
  # Version-control revision constant
20
- REVISION = %q$Revision: e943c9068b73 $
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,
@@ -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 @request_socket
191
+ state = if @request_sock
192
192
  if self.closed?
193
193
  "closed"
194
194
  else
@@ -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
- self.log.error "%p while accepting requests: %s" % [ err.class, err.message ]
175
- self.log.debug { err.backtrace.join("\n ") }
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
- retry unless @conn.closed?
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" % [ request.class ]
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[:path] ]
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[:path] ]
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) )