Class: Selenium::WebDriver::WebSocketConnection Private
| Relationships & Source Files | |
| Inherits: | Object |
| Defined in: | rb/lib/selenium/webdriver/common/websocket_connection.rb |
Constant Summary
-
CONNECTION_ERRORS =
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 26[ Errno::ECONNRESET, # connection is aborted (browser process was killed) Errno::EPIPE, # broken pipe (browser process was killed) Errno::EBADF, # file descriptor already closed (double-close or GC) IOError, # Ruby socket read/write after close EOFError # socket reached EOF after remote closed cleanly ].freeze
-
MAX_FRAME_SIZE =
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 46
websocket-ruby defaults to a 20MB limit and silently drops larger frames, which can stall the listener. CDP payloads (e.g. large data: URLs) can exceed that, so raise the ceiling for our connections. The gem only exposes the limit as process-global state, so it is raised (never lowered) and not restored on close - concurrent connections share the value. Other bindings rely on their own websocket clients' limits; this constant only affects websocket-ruby.
100 * 1024 * 1024
-
MAX_LOG_MESSAGE_SIZE =
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 379999 -
RESPONSE_WAIT_INTERVAL =
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 350.1 -
RESPONSE_WAIT_TIMEOUT =
# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 3430
Class Method Summary
-
.new(url:) ⇒ WebSocketConnection
constructor
Internal use only
100MB.
Instance Attribute Summary
-
#frame_dropped? ⇒ Boolean
readonly
private
Internal use only
True when the buffered frame could not be decoded (e.g. exceeds MAX_FRAME_SIZE).
Instance Method Summary
- #add_callback(event, &block) Internal use only
- #callbacks Internal use only
-
#close
Internal use only
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.
- #remove_callback(event, id) Internal use only
- #send_cmd(**payload) Internal use only
- #apply_frame_size_limit private Internal use only
- #attach_socket_listener private Internal use only
- #callback_thread(params) private Internal use only
- #close_socket private Internal use only
- #incoming_frame private Internal use only
- #messages private Internal use only
- #next_id private Internal use only
- #process_frame(frame) private Internal use only
- #process_handshake private Internal use only
- #process_incoming_frames private Internal use only
- #socket private Internal use only
- #wait private Internal use only
- #ws private Internal use only
Constructor Details
.new(url:) ⇒ WebSocketConnection
100MB
# 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.
# 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.}", 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.}", id: :ws rescue StandardError => e next if @closing bt = Array(e.backtrace).first(5).join("\n") WebDriver.logger.error "Callback error: #{e.class}: #{e.}\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.
# 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.}", id: :ws end end
#close_socket (private)
[ GitHub ]#incoming_frame (private)
[ GitHub ]#messages (private)
[ GitHub ]# File 'rb/lib/selenium/webdriver/common/websocket_connection.rb', line 127
def @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) = frame.to_s # Firefox will periodically fail on unparsable empty frame return {} if .empty? msg = JSON.parse() @messages_mtx.synchronize { [msg['id']] = msg if msg.key?('id') } WebDriver.logger.debug "WebSocket <- #{msg}"[...MAX_LOG_MESSAGE_SIZE], id: :ws msg end
#process_handshake (private)
[ GitHub ]#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 = process_frame(frame) next unless ['method'] @messages_mtx.synchronize { callbacks[['method']].dup }.each do |callback| @callback_threads.add(callback_thread(['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)
# 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.})" end wait.until do raise IOError, 'WebSocket connection closed while waiting for a response' if @closing @messages_mtx.synchronize { .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