Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/rb/lib/selenium/webdriver/common/websocket_connection.rb
1865 views
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
CONNECTION_ERRORS = [
26
Errno::ECONNRESET, # connection is aborted (browser process was killed)
27
Errno::EPIPE # broken pipe (browser process was killed)
28
].freeze
29
30
RESPONSE_WAIT_TIMEOUT = 30
31
RESPONSE_WAIT_INTERVAL = 0.1
32
33
MAX_LOG_MESSAGE_SIZE = 9999
34
35
def initialize(url:)
36
@callback_threads = ThreadGroup.new
37
38
@session_id = nil
39
@url = url
40
41
process_handshake
42
@socket_thread = attach_socket_listener
43
end
44
45
def close
46
@callback_threads.list.each(&:exit)
47
@socket_thread.exit
48
socket.close
49
end
50
51
def callbacks
52
@callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
53
end
54
55
def add_callback(event, &block)
56
callbacks[event] << block
57
block.object_id
58
end
59
60
def remove_callback(event, id)
61
return if callbacks[event].reject! { |callback| callback.object_id == id }
62
63
ids = callbacks[event]&.map(&:object_id)
64
raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
65
end
66
67
def send_cmd(**payload)
68
id = next_id
69
data = payload.merge(id: id)
70
WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :bidi
71
data = JSON.generate(data)
72
out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')
73
socket.write(out_frame.to_s)
74
75
wait.until { messages.delete(id) }
76
end
77
78
private
79
80
# We should be thread-safe to use the hash without synchronization
81
# because its keys are WebSocket message identifiers and they should be
82
# unique within a devtools session.
83
def messages
84
@messages ||= {}
85
end
86
87
def process_handshake
88
socket.print(ws.to_s)
89
ws << socket.readpartial(1024)
90
end
91
92
def attach_socket_listener
93
Thread.new do
94
Thread.current.abort_on_exception = true
95
Thread.current.report_on_exception = false
96
97
until socket.eof?
98
incoming_frame << socket.readpartial(1024)
99
100
while (frame = incoming_frame.next)
101
message = process_frame(frame)
102
next unless message['method']
103
104
params = message['params']
105
callbacks[message['method']].each do |callback|
106
@callback_threads.add(callback_thread(params, &callback))
107
end
108
end
109
end
110
rescue *CONNECTION_ERRORS
111
Thread.stop
112
end
113
end
114
115
def incoming_frame
116
@incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
117
end
118
119
def process_frame(frame)
120
message = frame.to_s
121
122
# Firefox will periodically fail on unparsable empty frame
123
return {} if message.empty?
124
125
message = JSON.parse(message)
126
messages[message['id']] = message
127
WebDriver.logger.debug "WebSocket <- #{message}"[...MAX_LOG_MESSAGE_SIZE], id: :bidi
128
129
message
130
end
131
132
def callback_thread(params)
133
Thread.new do
134
Thread.current.abort_on_exception = true
135
136
# We might end up blocked forever when we have an error in event.
137
# For example, if network interception event raises error,
138
# the browser will keep waiting for the request to be proceeded
139
# before returning back to the original thread. In this case,
140
# we should at least print the error.
141
Thread.current.report_on_exception = true
142
143
yield params
144
rescue Error::WebDriverError, *CONNECTION_ERRORS
145
Thread.stop
146
end
147
end
148
149
def wait
150
@wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL)
151
end
152
153
def socket
154
@socket ||= if URI(@url).scheme == 'wss'
155
socket = TCPSocket.new(ws.host, ws.port)
156
socket = OpenSSL::SSL::SSLSocket.new(socket, OpenSSL::SSL::SSLContext.new)
157
socket.sync_close = true
158
socket.connect
159
160
socket
161
else
162
TCPSocket.new(ws.host, ws.port)
163
end
164
end
165
166
def ws
167
@ws ||= WebSocket::Handshake::Client.new(url: @url)
168
end
169
170
def next_id
171
@id ||= 0
172
@id += 1
173
end
174
end # BiDi
175
end # WebDriver
176
end # Selenium
177
178