File: web_socket_server.rb

package info (click to toggle)
ruby-poltergeist 1.18.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, buster
  • size: 368 kB
  • sloc: ruby: 1,528; makefile: 3
file content (111 lines) | stat: -rw-r--r-- 3,344 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# frozen_string_literal: true

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, :host
    attr_accessor :timeout

    def initialize(port = nil, timeout = nil, custom_host = nil)
      @timeout = timeout
      @server  = start_server(port, custom_host)
      @receive_mutex = Mutex.new
    end

    def start_server(port, custom_host)
      time = Time.now

      begin
        TCPServer.open(custom_host || HOST, port || 0).tap do |server|
          @port = server.addr[1]
          @host = server.addr[2]
        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) do |event|
        command_id = JSON.load(event.data)['command_id']
        @messages[command_id] = event.data
      end
    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(cmd_id, receive_timeout=nil)
      receive_timeout ||= timeout
      start = Time.now

      until @messages.has_key?(cmd_id)
        raise Errno::EWOULDBLOCK if (Time.now - start) >= receive_timeout
        if @receive_mutex.try_lock
          begin
            IO.select([socket], [], [], receive_timeout) or raise Errno::EWOULDBLOCK
            data = socket.recv(RECV_SIZE)
            break if data.empty?
            driver.parse(data)
          ensure
            @receive_mutex.unlock
          end
        else
          sleep(0.05)
        end
      end
      @messages.delete(cmd_id)
    end

    # Send a message and block until there is a response
    def send(cmd_id, message, accept_timeout=nil)
      accept unless connected?
      driver.text(message)
      receive(cmd_id, accept_timeout)
    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