Skip to content

Commit e89eb14

Browse files
committed
Draft simple BiDi connection for Firefox nightly
1 parent 63466e5 commit e89eb14

12 files changed

Lines changed: 329 additions & 115 deletions

File tree

rb/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ ruby_gem(
8080
ruby_library(
8181
name = "bidi",
8282
srcs = glob([
83+
"lib/selenium/webdriver/bidi/**/*.rb",
84+
"lib/selenium/webdriver/bidi.rb",
8385
"lib/selenium/webdriver/devtools/**/*.rb",
8486
"lib/selenium/webdriver/devtools.rb",
8587
]) + [":mutation-listener"],

rb/lib/selenium/devtools.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
# specific language governing permissions and limitations
1818
# under the License.
1919

20-
require 'websocket'
21-
2220
module Selenium
2321
module DevTools
2422
class << self

rb/lib/selenium/webdriver.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ module WebDriver
3636
Rectangle = Struct.new(:x, :y, :width, :height)
3737
Location = Struct.new(:latitude, :longitude, :altitude)
3838

39+
autoload :BiDi, 'selenium/webdriver/bidi'
3940
autoload :Chrome, 'selenium/webdriver/chrome'
4041
autoload :DevTools, 'selenium/webdriver/devtools'
4142
autoload :Edge, 'selenium/webdriver/edge'

rb/lib/selenium/webdriver/bidi.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
module Selenium
21+
module WebDriver
22+
class BiDi
23+
autoload :Session, 'selenium/webdriver/bidi/session'
24+
25+
def initialize(url:)
26+
@ws = WebSocketConnection.new(url: url)
27+
end
28+
29+
def close
30+
@ws.close
31+
end
32+
33+
def callbacks
34+
@ws.callbacks
35+
end
36+
37+
def session
38+
Session.new(self)
39+
end
40+
41+
def send_cmd(method, **params)
42+
data = {method: method, params: params.reject { |_, v| v.nil? }}
43+
message = @ws.send_cmd(**data)
44+
raise Error::WebDriverError, error_message(message) if message['error']
45+
46+
message['result']
47+
end
48+
49+
def error_message(message)
50+
"#{message['error']}: #{message['message']}\n#{message['stacktrace']}"
51+
end
52+
53+
end # BiDi
54+
end # WebDriver
55+
end # Selenium
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
module Selenium
21+
module WebDriver
22+
class BiDi
23+
class Session
24+
Status = Struct.new(:ready, :message)
25+
26+
def initialize(bidi)
27+
@bidi = bidi
28+
end
29+
30+
def status
31+
status = @bidi.send_cmd('session.status')
32+
Status.new(status['ready'], status['message'])
33+
end
34+
35+
end # Session
36+
end # BiDi
37+
end # WebDriver
38+
end # Selenium

rb/lib/selenium/webdriver/common.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
require 'selenium/webdriver/common/driver_extensions/uploads_files'
7777
require 'selenium/webdriver/common/driver_extensions/full_page_screenshot'
7878
require 'selenium/webdriver/common/driver_extensions/has_addons'
79+
require 'selenium/webdriver/common/driver_extensions/has_bidi'
7980
require 'selenium/webdriver/common/driver_extensions/has_devtools'
8081
require 'selenium/webdriver/common/driver_extensions/has_authentication'
8182
require 'selenium/webdriver/common/driver_extensions/has_logs'
@@ -91,3 +92,4 @@
9192
require 'selenium/webdriver/common/driver'
9293
require 'selenium/webdriver/common/element'
9394
require 'selenium/webdriver/common/shadow_root'
95+
require 'selenium/webdriver/common/websocket_connection'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
module Selenium
21+
module WebDriver
22+
module DriverExtensions
23+
module HasBiDi
24+
25+
#
26+
# Retrieves WebDriver BiDi connection.
27+
#
28+
# @return [BiDi]
29+
#
30+
31+
def bidi
32+
@bidi ||= Selenium::WebDriver::BiDi.new(url: capabilities[:web_socket_url])
33+
end
34+
35+
end # HasBiDi
36+
end # DriverExtensions
37+
end # WebDriver
38+
end # Selenium
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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

Comments
 (0)