mirror of
https://github.com/teampoltergeist/poltergeist.git
synced 2022-11-09 12:05:00 -05:00

We grab all the output from PhantomJS (like console.log) in another thread and when PhantomJS exits first as in related test when we send `driver.browser.command('exit')` we will try to restart it. In order to do it we stop server and client. We see this error `IOError: Stream closed` for the client. It happens because JRuby tries to close pipe with https://github.com/jruby/jruby/blob/jruby-1_7/core/src/main/java/org/jruby/RubyIO.java#L2067 which raises an error here https://github.com/jruby/jruby/blob/jruby-1_7/core/src/main/java/org/jruby/RubyIO.java#L4753 It happens not just because IO was closed but because it was blocked on `eof?` or `readpartial` call. As you can see the error will be raised in related thread (it's not actually main thread but the thread that listens to the output) that's why if you put some debug code after `rescue IOError` it won't be shown and in fact the main thread will continue working after the raise. For instance: def restart stop start end it will invoke `start` even if we have seen `IOError` in `stop` method because the error has been raised in another thread. In order to fix it we don't have to give a chance to block IO. `IO.select` shows that there's something to be read in pipe.
96 lines
2.7 KiB
Ruby
96 lines
2.7 KiB
Ruby
require 'socket'
|
|
require 'websocket/driver'
|
|
|
|
module Capybara::Poltergeist
|
|
# This is a 'custom' Web Socket server that is designed to be synchronous. What
|
|
# this means is that it sends a message, and then waits for a response. It does
|
|
# not expect to receive a message at any other time than right after it has sent
|
|
# a message. So it is basically operating a request/response cycle (which is not
|
|
# how Web Sockets are usually used, but it's what we want here, as we want to
|
|
# send a message to PhantomJS and then wait for it to respond).
|
|
class WebSocketServer
|
|
# How much to try to read from the socket at once (it's kinda arbitrary because we
|
|
# just keep reading until we've received a full frame)
|
|
RECV_SIZE = 1024
|
|
|
|
# How many seconds to try to bind to the port for before failing
|
|
BIND_TIMEOUT = 5
|
|
|
|
HOST = '127.0.0.1'
|
|
|
|
attr_reader :port, :driver, :socket, :server
|
|
attr_accessor :timeout
|
|
|
|
def initialize(port = nil, timeout = nil)
|
|
@timeout = timeout
|
|
@server = start_server(port)
|
|
end
|
|
|
|
def start_server(port)
|
|
time = Time.now
|
|
|
|
begin
|
|
TCPServer.open(HOST, port || 0).tap do |server|
|
|
@port = server.addr[1]
|
|
end
|
|
rescue Errno::EADDRINUSE
|
|
if (Time.now - time) < BIND_TIMEOUT
|
|
sleep(0.01)
|
|
retry
|
|
else
|
|
raise
|
|
end
|
|
end
|
|
end
|
|
|
|
def connected?
|
|
!socket.nil?
|
|
end
|
|
|
|
# Accept a client on the TCP server socket, then receive its initial HTTP request
|
|
# and use that to initialize a Web Socket.
|
|
def accept
|
|
@socket = server.accept
|
|
@messages = []
|
|
|
|
@driver = ::WebSocket::Driver.server(self)
|
|
@driver.on(:connect) { |event| @driver.start }
|
|
@driver.on(:message) { |event| @messages << event.data }
|
|
end
|
|
|
|
def write(data)
|
|
@socket.write(data)
|
|
end
|
|
|
|
# Block until the next message is available from the Web Socket.
|
|
# Raises Errno::EWOULDBLOCK if timeout is reached.
|
|
def receive
|
|
start = Time.now
|
|
|
|
until @messages.any?
|
|
raise Errno::EWOULDBLOCK if (Time.now - start) >= timeout
|
|
IO.select([socket], [], [], timeout) or raise Errno::EWOULDBLOCK
|
|
data = socket.recv(RECV_SIZE)
|
|
break if data.empty?
|
|
driver.parse(data)
|
|
end
|
|
|
|
@messages.shift
|
|
end
|
|
|
|
# Send a message and block until there is a response
|
|
def send(message)
|
|
accept unless connected?
|
|
driver.text(message)
|
|
receive
|
|
rescue Errno::EWOULDBLOCK
|
|
raise TimeoutError.new(message)
|
|
end
|
|
|
|
# Closing sockets separately as `close_read`, `close_write`
|
|
# causes IO mistakes on JRuby, using just `close` fixes that.
|
|
def close
|
|
[server, socket].compact.each(&:close)
|
|
end
|
|
end
|
|
end
|