mongrel2 0.15.1 → 0.16.0

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