123456789_123456789_123456789_123456789_123456789_

Class: Selenium::WebDriver::WebSocketConnection Private

Relationships & Source Files
Inherits: Object
Defined in: rb/lib/selenium/webdriver/common/websocket_connection.rb

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(url:) ⇒ WebSocketConnection

100MB

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 48

def initialize(url:)
  @callback_threads = ThreadGroup.new

  @callbacks_mtx = Mutex.new
  @messages_mtx = Mutex.new
  @closing_mtx = Mutex.new

  @closing = false
  @session_id = nil
  @url = url

  apply_frame_size_limit
  process_handshake
  @socket_thread = attach_socket_listener
end

Instance Attribute Details

#frame_dropped?Boolean (readonly, private)

True when the buffered frame could not be decoded (e.g. exceeds MAX_FRAME_SIZE). websocket-ruby swallows the error and keeps returning nil, so surface it and close the connection here instead of leaving a dead listener on an open socket.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 176

def frame_dropped?
  return false unless incoming_frame.error?

  WebDriver.logger.error("WebSocket frame dropped (#{incoming_frame.error}); if payloads can legitimately " \
                         "exceed #{WebSocket.max_frame_size} bytes, set WebSocket.max_frame_size " \
                         'to a higher value', id: :ws)
  close_socket
  true
end

Instance Method Details

#add_callback(event, &block)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 83

def add_callback(event, &block)
  @callbacks_mtx.synchronize do
    callbacks[event] << block
    block.object_id
  end
end

#apply_frame_size_limit (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 186

def apply_frame_size_limit
  WebSocket.max_frame_size = MAX_FRAME_SIZE if WebSocket.max_frame_size < MAX_FRAME_SIZE
end

#attach_socket_listener (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 136

def attach_socket_listener
  Thread.new do
    Thread.current.report_on_exception = false

    loop do
      break if @closing

      incoming_frame << socket.readpartial(1024)

      process_incoming_frames
      break if frame_dropped?
    end
  rescue *CONNECTION_ERRORS, WebSocket::Error => e
    WebDriver.logger.debug "WebSocket listener closed: #{e.class}: #{e.message}", id: :ws
  end
end

#callback_thread(params) (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 207

def callback_thread(params)
  Thread.new do
    Thread.current.abort_on_exception = false
    Thread.current.report_on_exception = false
    next if @closing

    yield params
  rescue Error::WebDriverError, *CONNECTION_ERRORS => e
    WebDriver.logger.debug "Callback aborted: #{e.class}: #{e.message}", id: :ws
  rescue StandardError => e
    next if @closing

    bt = Array(e.backtrace).first(5).join("\n")
    WebDriver.logger.error "Callback error: #{e.class}: #{e.message}\n#{bt}", id: :ws
  end
end

#callbacks

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 79

def callbacks
  @callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
end

#close

Idempotent: the listener may already have initiated shutdown (see #frame_dropped?), so always close the socket and join threads rather than short-circuiting on @closing.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 67

def close
  close_socket

  # Let threads unwind instead of calling exit
  @socket_thread&.join(0.5) unless @socket_thread == Thread.current
  @callback_threads.list.each do |thread|
    thread.join(0.5)
  rescue StandardError => e
    WebDriver.logger.debug "Failed to join thread during close: #{e.class}: #{e.message}", id: :ws
  end
end

#close_socket (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 153

def close_socket
  @closing_mtx.synchronize { @closing = true }
  socket.close
rescue *CONNECTION_ERRORS => e
  WebDriver.logger.debug "WebSocket socket closed: #{e.class}: #{e.message}", id: :ws
end

#incoming_frame (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 190

def incoming_frame
  @incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
end

#messages (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 127

def messages
  @messages ||= {}
end

#next_id (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 245

def next_id
  @id = (@id || 0) + 1
end

#process_frame(frame) (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 194

def process_frame(frame)
  message = frame.to_s

  # Firefox will periodically fail on unparsable empty frame
  return {} if message.empty?

  msg = JSON.parse(message)
  @messages_mtx.synchronize { messages[msg['id']] = msg if msg.key?('id') }

  WebDriver.logger.debug "WebSocket <- #{msg}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
  msg
end

#process_handshake (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 131

def process_handshake
  socket.print(ws.to_s)
  ws << socket.readpartial(1024) until ws.finished?
end

#process_incoming_frames (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 160

def process_incoming_frames
  while (frame = incoming_frame.next)
    break if @closing

    message = process_frame(frame)
    next unless message['method']

    @messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback|
      @callback_threads.add(callback_thread(message['params'], &callback))
    end
  end
end

#remove_callback(event, id)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 90

def remove_callback(event, id)
  @callbacks_mtx.synchronize do
    return if @closing

    callbacks_for_event = callbacks[event]
    return if callbacks_for_event.reject! { |cb| cb.object_id == id }

    ids = callbacks_for_event.map(&:object_id)
    raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
  end
end

#send_cmd(**payload)

Raises:

  • (IOError)
[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 102

def send_cmd(**payload)
  # IOError to match what writing to an already-closed socket raises
  raise IOError, 'WebSocket connection is closed' if @closing

  id = next_id
  data = payload.merge(id: id)
  WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
  data = JSON.generate(data)
  out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')

  begin
    socket.write(out_frame.to_s)
  rescue *CONNECTION_ERRORS => e
    raise e, "WebSocket is closed (#{e.class}: #{e.message})"
  end

  wait.until do
    raise IOError, 'WebSocket connection closed while waiting for a response' if @closing

    @messages_mtx.synchronize { messages.delete(id) }
  end
end

#socket (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 228

def socket
  @socket ||= if URI(@url).scheme == 'wss'
                socket = TCPSocket.new(ws.host, ws.port)
                socket = OpenSSL::SSL::SSLSocket.new(socket, OpenSSL::SSL::SSLContext.new)
                socket.sync_close = true
                socket.connect

                socket
              else
                TCPSocket.new(ws.host, ws.port)
              end
end

#wait (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 224

def wait
  @wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL)
end

#ws (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 241

def ws
  @ws ||= WebSocket::Handshake::Client.new(url: @url)
end