1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
6 require 'faye/websocket'
9 # A Rack middleware to handle inbound websocket connection requests and hand
10 # them over to the faye websocket library.
13 DEFAULT_ENDPOINT = '/websocket'
15 # Stop EventMachine on signal, this should give it a chance to to unwind any
17 def die_gracefully_on_signal
18 Signal.trap("INT") { EM.stop }
19 Signal.trap("TERM") { EM.stop }
22 # Create a new RackSocket handler
23 # +app+ The next layer of the Rack stack.
26 # +:handler+ (Required) A class to handle new connections. #initialize will
27 # call handler.new to create the actual handler instance object. When a new
28 # websocket connection is established, #on_connect on the handler instance
29 # object will be called with the new connection.
31 # +:mount+ The HTTP request path that will be recognized for websocket
32 # connect requests, defaults to '/websocket'.
34 # +:websocket_only+ If true, the server will only handle websocket requests,
35 # and all other requests will result in an error. If false, unhandled
36 # non-websocket requests will be passed along on to 'app' in the usual Rack
38 def initialize(app = nil, options = nil)
39 @app = app if app.respond_to?(:call)
40 @options = [app, options].grep(Hash).first || {}
41 @endpoint = @options[:mount] || DEFAULT_ENDPOINT
42 @websocket_only = @options[:websocket_only] || false
44 # from https://gist.github.com/eatenbyagrue/1338545#file-eventmachine-rb
45 if defined?(PhusionPassenger)
46 PhusionPassenger.on_event(:starting_worker_process) do |forked|
47 # for passenger, we need to avoid orphaned threads
48 if forked && EM.reactor_running?
55 ActiveRecord::Base.connection.close
58 die_gracefully_on_signal
61 # faciliates debugging
62 Thread.abort_on_exception = true
63 # just spawn a thread and start it up
68 ActiveRecord::Base.connection.close
73 # Create actual handler instance object from handler class.
74 @handler = @options[:handler].new
77 # Handle websocket connection request, or pass on to the next middleware
78 # supplied in +app+ initialize (unless +:websocket_only+ option is true, in
79 # which case return an error response.)
80 # +env+ the Rack environment with information about the request.
82 request = Rack::Request.new(env)
83 if request.path_info == @endpoint and Faye::WebSocket.websocket?(env)
84 if @handler.overloaded?
85 return [503, {"Content-Type" => "text/plain"}, ["Too many connections, try again later."]]
88 ws = Faye::WebSocket.new(env, nil, :ping => 30)
90 # Notify handler about new connection
91 @handler.on_connect ws
93 # Return async Rack response
95 elsif not @websocket_only
98 [406, {"Content-Type" => "text/plain"}, ["Only websocket connections are permitted on this port."]]