Class: Ferrum::Client::WebSocket
- Inherits:
-
Object
- Object
- Ferrum::Client::WebSocket
- Defined in:
- lib/ferrum/client/web_socket.rb
Overview
Low-level WebSocket connection to the browser's CDP endpoint. Opens
the raw TCP/TLS socket, drives the websocket-driver handshake and
framing, and exposes a queue of parsed incoming messages alongside
methods to send commands and close the connection.
Constant Summary collapse
- WEBSOCKET_BUG_SLEEP =
0.05
- DEFAULT_PORTS =
{ "ws" => 80, "wss" => 443 }.freeze
- SKIP_LOGGING_SCREENSHOTS =
!ENV["FERRUM_LOGGING_SCREENSHOTS"]
Instance Attribute Summary collapse
-
#messages ⇒ Object
readonly
Returns the value of attribute messages.
-
#url ⇒ Object
readonly
Returns the value of attribute url.
Instance Method Summary collapse
-
#close ⇒ void
Closes the websocket connection by sending a close frame.
-
#initialize(url, max_receive_size, logger) ⇒ WebSocket
constructor
A new instance of WebSocket.
-
#on_close(_event) ⇒ void
Handles the driver's
:closeevent: closes the message queue and underlying socket, then kills the reader thread. -
#on_message(event) ⇒ void
Handles the driver's
:messageevent: parses the incoming frame as JSON and pushes it onto #messages. -
#on_open(_event) ⇒ void
Handles the driver's
:openevent. -
#send_message(data) ⇒ void
Serializes a CDP command to JSON and sends it as a websocket text frame.
-
#write(data) ⇒ void
Writes raw bytes to the underlying socket.
Constructor Details
#initialize(url, max_receive_size, logger) ⇒ WebSocket
Returns a new instance of WebSocket.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/ferrum/client/web_socket.rb', line 22 def initialize(url, max_receive_size, logger) @url = url @logger = logger uri = URI.parse(@url) port = uri.port || DEFAULT_PORTS[uri.scheme] if port == 443 || url.scheme == "wss" tcp = TCPSocket.new(uri.host, port) ssl_context = OpenSSL::SSL::SSLContext.new @sock = OpenSSL::SSL::SSLSocket.new(tcp, ssl_context) @sock.sync_close = true @sock.connect else @sock = TCPSocket.new(uri.host, port) end max_receive_size ||= ::WebSocket::Driver::MAX_LENGTH @driver = ::WebSocket::Driver.client(self, max_length: max_receive_size) # websocket-driver holds no locks and is called from many threads: commands from # callers, pong/close replies from the reader. One lock keeps frames from interleaving. @driver_mutex = Mutex.new = Queue.new @screenshot_commands = Concurrent::Hash.new if SKIP_LOGGING_SCREENSHOTS @driver.on(:open, &method(:on_open)) @driver.on(:message, &method(:on_message)) @driver.on(:close, &method(:on_close)) start @driver_mutex.synchronize { @driver.start } end |
Instance Attribute Details
#messages ⇒ Object (readonly)
Returns the value of attribute messages.
20 21 22 |
# File 'lib/ferrum/client/web_socket.rb', line 20 def end |
#url ⇒ Object (readonly)
Returns the value of attribute url.
20 21 22 |
# File 'lib/ferrum/client/web_socket.rb', line 20 def url @url end |
Instance Method Details
#close ⇒ void
This method returns an undefined value.
Closes the websocket connection by sending a close frame.
140 141 142 |
# File 'lib/ferrum/client/web_socket.rb', line 140 def close @driver_mutex.synchronize { @driver.close } end |
#on_close(_event) ⇒ void
This method returns an undefined value.
Handles the driver's :close event: closes the message queue and
underlying socket, then kills the reader thread.
96 97 98 99 100 |
# File 'lib/ferrum/client/web_socket.rb', line 96 def on_close(_event) .close @sock.close @thread.kill end |
#on_message(event) ⇒ void
This method returns an undefined value.
Handles the driver's :message event: parses the incoming frame
as JSON and pushes it onto #messages. Malformed payloads are
dropped rather than raised, to avoid crashing the reader thread.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/ferrum/client/web_socket.rb', line 73 def (event) data = safely_parse_json(event.data) output = event.data if SKIP_LOGGING_SCREENSHOTS && @screenshot_commands[data&.dig("id")] @screenshot_commands.delete(data&.dig("id")) output.sub!(/{"data":"[^"]*"}/, %("Set FERRUM_LOGGING_SCREENSHOTS=true to see screenshots in Base64")) end @logger&.puts(" ◀ #{Utils::ElapsedTime.elapsed_time} #{output}\n") # If we couldn't parse JSON data for some reason (parse error or deeply nested object) we # don't push response to @messages. Worse that could happen we raise timeout error due to command didn't return # anything or skip the background notification, but at least we don't crash the thread that crashes the main # thread and the application. .push(data) if data end |
#on_open(_event) ⇒ void
This method returns an undefined value.
Handles the driver's :open event.
61 62 63 64 |
# File 'lib/ferrum/client/web_socket.rb', line 61 def on_open(_event) # https://github.com/faye/websocket-driver-ruby/issues/46 sleep(WEBSOCKET_BUG_SLEEP) end |
#send_message(data) ⇒ void
This method returns an undefined value.
Serializes a CDP command to JSON and sends it as a websocket text frame.
111 112 113 114 115 116 117 |
# File 'lib/ferrum/client/web_socket.rb', line 111 def (data) @screenshot_commands[data[:id]] = true if SKIP_LOGGING_SCREENSHOTS json = data.to_json @driver_mutex.synchronize { @driver.text(json) } @logger&.puts("\n\n▶ #{Utils::ElapsedTime.elapsed_time} #{json}") end |
#write(data) ⇒ void
This method returns an undefined value.
Writes raw bytes to the underlying socket. Called by
websocket-driver to emit frames. Closes #messages instead of
raising if the connection has already been torn down.
129 130 131 132 133 |
# File 'lib/ferrum/client/web_socket.rb', line 129 def write(data) @sock.write(data) rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, IOError # rubocop:disable Lint/ShadowedException .close end |