|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +# Licensed to the Software Freedom Conservancy (SFC) under one |
| 4 | +# or more contributor license agreements. See the NOTICE file |
| 5 | +# distributed with this work for additional information |
| 6 | +# regarding copyright ownership. The SFC licenses this file |
| 7 | +# to you under the Apache License, Version 2.0 (the |
| 8 | +# "License"); you may not use this file except in compliance |
| 9 | +# with the License. You may obtain a copy of the License at |
| 10 | +# |
| 11 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 12 | +# |
| 13 | +# Unless required by applicable law or agreed to in writing, |
| 14 | +# software distributed under the License is distributed on an |
| 15 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 16 | +# KIND, either express or implied. See the License for the |
| 17 | +# specific language governing permissions and limitations |
| 18 | +# under the License. |
| 19 | + |
| 20 | +require 'websocket' |
| 21 | + |
| 22 | +module Selenium |
| 23 | + module WebDriver |
| 24 | + class WebSocketConnection |
| 25 | + RESPONSE_WAIT_TIMEOUT = 30 |
| 26 | + RESPONSE_WAIT_INTERVAL = 0.1 |
| 27 | + |
| 28 | + def initialize(url:) |
| 29 | + @callback_threads = ThreadGroup.new |
| 30 | + |
| 31 | + @messages = [] |
| 32 | + @session_id = nil |
| 33 | + @url = url |
| 34 | + |
| 35 | + process_handshake |
| 36 | + @socket_thread = attach_socket_listener |
| 37 | + end |
| 38 | + |
| 39 | + def close |
| 40 | + @callback_threads.list.each(&:exit) |
| 41 | + @socket_thread.exit |
| 42 | + socket.close |
| 43 | + end |
| 44 | + |
| 45 | + def callbacks |
| 46 | + @callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] } |
| 47 | + end |
| 48 | + |
| 49 | + def send_cmd(**payload) |
| 50 | + id = next_id |
| 51 | + data = payload.merge(id: id) |
| 52 | + data = JSON.generate(data) |
| 53 | + WebDriver.logger.debug "WebSocket -> #{data}" |
| 54 | + |
| 55 | + out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text') |
| 56 | + socket.write(out_frame.to_s) |
| 57 | + |
| 58 | + wait.until { @messages.find { |m| m['id'] == id } } |
| 59 | + end |
| 60 | + |
| 61 | + private |
| 62 | + |
| 63 | + def process_handshake |
| 64 | + socket.print(ws.to_s) |
| 65 | + ws << socket.readpartial(1024) |
| 66 | + end |
| 67 | + |
| 68 | + def attach_socket_listener |
| 69 | + Thread.new do |
| 70 | + Thread.current.abort_on_exception = true |
| 71 | + Thread.current.report_on_exception = false |
| 72 | + |
| 73 | + until socket.eof? |
| 74 | + incoming_frame << socket.readpartial(1024) |
| 75 | + |
| 76 | + while (frame = incoming_frame.next) |
| 77 | + message = process_frame(frame) |
| 78 | + next unless message['method'] |
| 79 | + |
| 80 | + params = message['params'] |
| 81 | + callbacks[message['method']].each do |callback| |
| 82 | + @callback_threads.add(callback_thread(params, &callback)) |
| 83 | + end |
| 84 | + end |
| 85 | + end |
| 86 | + end |
| 87 | + end |
| 88 | + |
| 89 | + def incoming_frame |
| 90 | + @incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version) |
| 91 | + end |
| 92 | + |
| 93 | + def process_frame(frame) |
| 94 | + message = frame.to_s |
| 95 | + |
| 96 | + # Firefox will periodically fail on unparsable empty frame |
| 97 | + return {} if message.empty? |
| 98 | + |
| 99 | + message = JSON.parse(message) |
| 100 | + @messages << message |
| 101 | + WebDriver.logger.debug "WebSocket <- #{message}" |
| 102 | + |
| 103 | + message |
| 104 | + end |
| 105 | + |
| 106 | + def callback_thread(params) |
| 107 | + Thread.new do |
| 108 | + Thread.current.abort_on_exception = true |
| 109 | + |
| 110 | + # We might end up blocked forever when we have an error in event. |
| 111 | + # For example, if network interception event raises error, |
| 112 | + # the browser will keep waiting for the request to be proceeded |
| 113 | + # before returning back to the original thread. In this case, |
| 114 | + # we should at least print the error. |
| 115 | + Thread.current.report_on_exception = true |
| 116 | + |
| 117 | + yield params |
| 118 | + end |
| 119 | + end |
| 120 | + |
| 121 | + def wait |
| 122 | + @wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL) |
| 123 | + end |
| 124 | + |
| 125 | + def socket |
| 126 | + @socket ||= begin |
| 127 | + if URI(@url).scheme == 'wss' |
| 128 | + socket = TCPSocket.new(ws.host, ws.port) |
| 129 | + socket = OpenSSL::SSL::SSLSocket.new(socket, OpenSSL::SSL::SSLContext.new) |
| 130 | + socket.sync_close = true |
| 131 | + socket.connect |
| 132 | + |
| 133 | + socket |
| 134 | + else |
| 135 | + TCPSocket.new(ws.host, ws.port) |
| 136 | + end |
| 137 | + end |
| 138 | + end |
| 139 | + |
| 140 | + def ws |
| 141 | + @ws ||= WebSocket::Handshake::Client.new(url: @url) |
| 142 | + end |
| 143 | + |
| 144 | + def next_id |
| 145 | + @id ||= 0 |
| 146 | + @id += 1 |
| 147 | + end |
| 148 | + |
| 149 | + end # BiDi |
| 150 | + end # WebDriver |
| 151 | +end # Selenium |
0 commit comments