Path: blob/trunk/rb/lib/selenium/webdriver/common/websocket_connection.rb
1865 views
# frozen_string_literal: true12# Licensed to the Software Freedom Conservancy (SFC) under one3# or more contributor license agreements. See the NOTICE file4# distributed with this work for additional information5# regarding copyright ownership. The SFC licenses this file6# to you under the Apache License, Version 2.0 (the7# "License"); you may not use this file except in compliance8# with the License. You may obtain a copy of the License at9#10# http://www.apache.org/licenses/LICENSE-2.011#12# Unless required by applicable law or agreed to in writing,13# software distributed under the License is distributed on an14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY15# KIND, either express or implied. See the License for the16# specific language governing permissions and limitations17# under the License.1819require 'websocket'2021module Selenium22module WebDriver23class WebSocketConnection24CONNECTION_ERRORS = [25Errno::ECONNRESET, # connection is aborted (browser process was killed)26Errno::EPIPE # broken pipe (browser process was killed)27].freeze2829RESPONSE_WAIT_TIMEOUT = 3030RESPONSE_WAIT_INTERVAL = 0.13132MAX_LOG_MESSAGE_SIZE = 99993334def initialize(url:)35@callback_threads = ThreadGroup.new3637@session_id = nil38@url = url3940process_handshake41@socket_thread = attach_socket_listener42end4344def close45@callback_threads.list.each(&:exit)46@socket_thread.exit47socket.close48end4950def callbacks51@callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }52end5354def add_callback(event, &block)55callbacks[event] << block56block.object_id57end5859def remove_callback(event, id)60return if callbacks[event].reject! { |callback| callback.object_id == id }6162ids = callbacks[event]&.map(&:object_id)63raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"64end6566def send_cmd(**payload)67id = next_id68data = payload.merge(id: id)69WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :bidi70data = JSON.generate(data)71out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')72socket.write(out_frame.to_s)7374wait.until { messages.delete(id) }75end7677private7879# We should be thread-safe to use the hash without synchronization80# because its keys are WebSocket message identifiers and they should be81# unique within a devtools session.82def messages83@messages ||= {}84end8586def process_handshake87socket.print(ws.to_s)88ws << socket.readpartial(1024)89end9091def attach_socket_listener92Thread.new do93Thread.current.abort_on_exception = true94Thread.current.report_on_exception = false9596until socket.eof?97incoming_frame << socket.readpartial(1024)9899while (frame = incoming_frame.next)100message = process_frame(frame)101next unless message['method']102103params = message['params']104callbacks[message['method']].each do |callback|105@callback_threads.add(callback_thread(params, &callback))106end107end108end109rescue *CONNECTION_ERRORS110Thread.stop111end112end113114def incoming_frame115@incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)116end117118def process_frame(frame)119message = frame.to_s120121# Firefox will periodically fail on unparsable empty frame122return {} if message.empty?123124message = JSON.parse(message)125messages[message['id']] = message126WebDriver.logger.debug "WebSocket <- #{message}"[...MAX_LOG_MESSAGE_SIZE], id: :bidi127128message129end130131def callback_thread(params)132Thread.new do133Thread.current.abort_on_exception = true134135# We might end up blocked forever when we have an error in event.136# For example, if network interception event raises error,137# the browser will keep waiting for the request to be proceeded138# before returning back to the original thread. In this case,139# we should at least print the error.140Thread.current.report_on_exception = true141142yield params143rescue Error::WebDriverError, *CONNECTION_ERRORS144Thread.stop145end146end147148def wait149@wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL)150end151152def socket153@socket ||= if URI(@url).scheme == 'wss'154socket = TCPSocket.new(ws.host, ws.port)155socket = OpenSSL::SSL::SSLSocket.new(socket, OpenSSL::SSL::SSLContext.new)156socket.sync_close = true157socket.connect158159socket160else161TCPSocket.new(ws.host, ws.port)162end163end164165def ws166@ws ||= WebSocket::Handshake::Client.new(url: @url)167end168169def next_id170@id ||= 0171@id += 1172end173end # BiDi174end # WebDriver175end # Selenium176177178