~ubuntu-branches/debian/sid/ruby-poltergeist/sid

« back to all changes in this revision

Viewing changes to lib/capybara/poltergeist/web_socket_server.rb

  • Committer: Package Import Robot
  • Author(s): Pirate Praveen
  • Date: 2016-09-15 22:01:34 UTC
  • Revision ID: package-import@ubuntu.com-20160915220134-vd142d9tvojjvcyo
Tags: upstream-1.10.0
ImportĀ upstreamĀ versionĀ 1.10.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
require 'socket'
 
2
require 'websocket/driver'
 
3
 
 
4
module Capybara::Poltergeist
 
5
  # This is a 'custom' Web Socket server that is designed to be synchronous. What
 
6
  # this means is that it sends a message, and then waits for a response. It does
 
7
  # not expect to receive a message at any other time than right after it has sent
 
8
  # a message. So it is basically operating a request/response cycle (which is not
 
9
  # how Web Sockets are usually used, but it's what we want here, as we want to
 
10
  # send a message to PhantomJS and then wait for it to respond).
 
11
  class WebSocketServer
 
12
    # How much to try to read from the socket at once (it's kinda arbitrary because we
 
13
    # just keep reading until we've received a full frame)
 
14
    RECV_SIZE = 1024
 
15
 
 
16
    # How many seconds to try to bind to the port for before failing
 
17
    BIND_TIMEOUT = 5
 
18
 
 
19
    HOST = '127.0.0.1'
 
20
 
 
21
    attr_reader :port, :driver, :socket, :server
 
22
    attr_accessor :timeout
 
23
 
 
24
    def initialize(port = nil, timeout = nil)
 
25
      @timeout = timeout
 
26
      @server  = start_server(port)
 
27
      @receive_mutex = Mutex.new
 
28
    end
 
29
 
 
30
    def start_server(port)
 
31
      time = Time.now
 
32
 
 
33
      begin
 
34
        TCPServer.open(HOST, port || 0).tap do |server|
 
35
          @port = server.addr[1]
 
36
        end
 
37
      rescue Errno::EADDRINUSE
 
38
        if (Time.now - time) < BIND_TIMEOUT
 
39
          sleep(0.01)
 
40
          retry
 
41
        else
 
42
          raise
 
43
        end
 
44
      end
 
45
    end
 
46
 
 
47
    def connected?
 
48
      !socket.nil?
 
49
    end
 
50
 
 
51
    # Accept a client on the TCP server socket, then receive its initial HTTP request
 
52
    # and use that to initialize a Web Socket.
 
53
    def accept
 
54
      @socket   = server.accept
 
55
      @messages = {}
 
56
 
 
57
      @driver = ::WebSocket::Driver.server(self)
 
58
      @driver.on(:connect) { |event| @driver.start }
 
59
      @driver.on(:message) do |event|
 
60
        command_id = JSON.load(event.data)['command_id']
 
61
        @messages[command_id] = event.data
 
62
      end
 
63
    end
 
64
 
 
65
    def write(data)
 
66
      @socket.write(data)
 
67
    end
 
68
 
 
69
    # Block until the next message is available from the Web Socket.
 
70
    # Raises Errno::EWOULDBLOCK if timeout is reached.
 
71
    def receive(cmd_id, receive_timeout=nil)
 
72
      receive_timeout ||= timeout
 
73
      start = Time.now
 
74
 
 
75
      until @messages.has_key?(cmd_id)
 
76
        raise Errno::EWOULDBLOCK if (Time.now - start) >= receive_timeout
 
77
        if @receive_mutex.try_lock
 
78
          begin
 
79
            IO.select([socket], [], [], receive_timeout) or raise Errno::EWOULDBLOCK
 
80
            data = socket.recv(RECV_SIZE)
 
81
            break if data.empty?
 
82
            driver.parse(data)
 
83
          ensure
 
84
            @receive_mutex.unlock
 
85
          end
 
86
        else
 
87
          sleep(0.05)
 
88
        end
 
89
      end
 
90
      @messages.delete(cmd_id)
 
91
    end
 
92
 
 
93
    # Send a message and block until there is a response
 
94
    def send(cmd_id, message, accept_timeout=nil)
 
95
      accept unless connected?
 
96
      driver.text(message)
 
97
      receive(cmd_id, accept_timeout)
 
98
    rescue Errno::EWOULDBLOCK
 
99
      raise TimeoutError.new(message)
 
100
    end
 
101
 
 
102
    # Closing sockets separately as `close_read`, `close_write`
 
103
    # causes IO mistakes on JRuby, using just `close` fixes that.
 
104
    def close
 
105
      [server, socket].compact.each(&:close)
 
106
    end
 
107
  end
 
108
end