ferrum 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/ferrum/accessibility/ax_node.rb +108 -0
- data/lib/ferrum/accessibility.rb +106 -0
- data/lib/ferrum/browser/binary.rb +41 -0
- data/lib/ferrum/browser/command.rb +20 -0
- data/lib/ferrum/browser/options/base.rb +67 -0
- data/lib/ferrum/browser/options/chrome.rb +36 -1
- data/lib/ferrum/browser/options/firefox.rb +32 -0
- data/lib/ferrum/browser/options.rb +41 -2
- data/lib/ferrum/browser/process.rb +51 -0
- data/lib/ferrum/browser/xvfb.rb +24 -0
- data/lib/ferrum/browser.rb +32 -10
- data/lib/ferrum/client/subscriber.rb +58 -0
- data/lib/ferrum/client/web_socket.rb +62 -11
- data/lib/ferrum/client.rb +199 -8
- data/lib/ferrum/context.rb +97 -4
- data/lib/ferrum/contexts.rb +119 -9
- data/lib/ferrum/cookies/cookie.rb +6 -0
- data/lib/ferrum/cookies.rb +5 -0
- data/lib/ferrum/dialog.rb +18 -2
- data/lib/ferrum/downloads.rb +52 -0
- data/lib/ferrum/errors.rb +58 -3
- data/lib/ferrum/frame/dom.rb +17 -0
- data/lib/ferrum/frame/runtime.rb +48 -7
- data/lib/ferrum/frame.rb +58 -1
- data/lib/ferrum/headers.rb +6 -0
- data/lib/ferrum/interceptable.rb +62 -0
- data/lib/ferrum/keyboard.rb +25 -0
- data/lib/ferrum/mouse.rb +6 -0
- data/lib/ferrum/network/auth_request.rb +81 -2
- data/lib/ferrum/network/error.rb +15 -0
- data/lib/ferrum/network/exchange.rb +10 -0
- data/lib/ferrum/network/intercepted_request.rb +94 -2
- data/lib/ferrum/network/request.rb +1 -1
- data/lib/ferrum/network/response.rb +2 -0
- data/lib/ferrum/network.rb +121 -9
- data/lib/ferrum/node.rb +305 -15
- data/lib/ferrum/page/animation.rb +5 -0
- data/lib/ferrum/page/frames.rb +69 -7
- data/lib/ferrum/page/screencast.rb +5 -0
- data/lib/ferrum/page/screenshot.rb +52 -20
- data/lib/ferrum/page/stream.rb +56 -0
- data/lib/ferrum/page/tracing.rb +6 -0
- data/lib/ferrum/page.rb +139 -42
- data/lib/ferrum/proxy.rb +52 -2
- data/lib/ferrum/rgba.rb +10 -0
- data/lib/ferrum/target.rb +124 -1
- data/lib/ferrum/utils/attempt.rb +20 -0
- data/lib/ferrum/utils/elapsed_time.rb +38 -0
- data/lib/ferrum/utils/event.rb +14 -0
- data/lib/ferrum/utils/platform.rb +21 -0
- data/lib/ferrum/utils/thread.rb +12 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum/worker.rb +125 -0
- data/lib/ferrum.rb +7 -0
- metadata +6 -16
|
@@ -14,6 +14,12 @@ require "ferrum/utils/platform"
|
|
|
14
14
|
|
|
15
15
|
module Ferrum
|
|
16
16
|
class Browser
|
|
17
|
+
#
|
|
18
|
+
# Spawns and manages the lifecycle of the browser OS process: builds the
|
|
19
|
+
# launch {Command}, starts it (optionally under {Xvfb} for headful mode),
|
|
20
|
+
# waits for the CDP WebSocket endpoint to become available, and handles
|
|
21
|
+
# stopping/restarting the process and cleaning up its user data directory.
|
|
22
|
+
#
|
|
17
23
|
class Process
|
|
18
24
|
KILL_TIMEOUT = 2
|
|
19
25
|
WAIT_KILLED = 0.05
|
|
@@ -22,10 +28,26 @@ module Ferrum
|
|
|
22
28
|
|
|
23
29
|
delegate path: :command
|
|
24
30
|
|
|
31
|
+
#
|
|
32
|
+
# Builds and starts a new browser process.
|
|
33
|
+
#
|
|
34
|
+
# @param [Array] args
|
|
35
|
+
# Arguments forwarded to {#initialize}.
|
|
36
|
+
#
|
|
37
|
+
# @return [Process]
|
|
38
|
+
#
|
|
25
39
|
def self.start(*args)
|
|
26
40
|
new(*args).tap(&:start)
|
|
27
41
|
end
|
|
28
42
|
|
|
43
|
+
#
|
|
44
|
+
# Builds a finalizer proc that kills the process with the given pid.
|
|
45
|
+
#
|
|
46
|
+
# @param [Integer] pid
|
|
47
|
+
# Process id to kill.
|
|
48
|
+
#
|
|
49
|
+
# @return [Proc]
|
|
50
|
+
#
|
|
29
51
|
def self.process_killer(pid)
|
|
30
52
|
proc do
|
|
31
53
|
if Utils::Platform.windows?
|
|
@@ -48,6 +70,14 @@ module Ferrum
|
|
|
48
70
|
end
|
|
49
71
|
end
|
|
50
72
|
|
|
73
|
+
#
|
|
74
|
+
# Builds a finalizer proc that removes the directory at the given path.
|
|
75
|
+
#
|
|
76
|
+
# @param [String] path
|
|
77
|
+
# Directory to remove.
|
|
78
|
+
#
|
|
79
|
+
# @return [Proc]
|
|
80
|
+
#
|
|
51
81
|
def self.directory_remover(path)
|
|
52
82
|
proc {
|
|
53
83
|
begin
|
|
@@ -83,6 +113,11 @@ module Ferrum
|
|
|
83
113
|
@command = Command.build(options, tmpdir)
|
|
84
114
|
end
|
|
85
115
|
|
|
116
|
+
#
|
|
117
|
+
# Spawns the browser process and waits for it to become reachable over CDP.
|
|
118
|
+
#
|
|
119
|
+
# @return [void]
|
|
120
|
+
#
|
|
86
121
|
def start
|
|
87
122
|
# Don't do anything as browser is already running as external process.
|
|
88
123
|
return if ws_url
|
|
@@ -109,6 +144,12 @@ module Ferrum
|
|
|
109
144
|
end
|
|
110
145
|
end
|
|
111
146
|
|
|
147
|
+
#
|
|
148
|
+
# Kills the browser process (and Xvfb, if running) and removes the user
|
|
149
|
+
# data directory.
|
|
150
|
+
#
|
|
151
|
+
# @return [void]
|
|
152
|
+
#
|
|
112
153
|
def stop
|
|
113
154
|
if @pid
|
|
114
155
|
kill(@pid)
|
|
@@ -120,11 +161,21 @@ module Ferrum
|
|
|
120
161
|
ObjectSpace.undefine_finalizer(self)
|
|
121
162
|
end
|
|
122
163
|
|
|
164
|
+
#
|
|
165
|
+
# Stops and starts the browser process again.
|
|
166
|
+
#
|
|
167
|
+
# @return [void]
|
|
168
|
+
#
|
|
123
169
|
def restart
|
|
124
170
|
stop
|
|
125
171
|
start
|
|
126
172
|
end
|
|
127
173
|
|
|
174
|
+
#
|
|
175
|
+
# Custom inspection that omits noisy internal command details.
|
|
176
|
+
#
|
|
177
|
+
# @return [String]
|
|
178
|
+
#
|
|
128
179
|
def inspect
|
|
129
180
|
"#<#{self.class} " \
|
|
130
181
|
"@user_data_dir=#{@user_data_dir.inspect} " \
|
data/lib/ferrum/browser/xvfb.rb
CHANGED
|
@@ -2,10 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
module Ferrum
|
|
4
4
|
class Browser
|
|
5
|
+
#
|
|
6
|
+
# Manages an Xvfb virtual display server, letting a "headful" browser run
|
|
7
|
+
# in environments without a real display (e.g. CI). Spawned by {Process}
|
|
8
|
+
# when the `:xvfb` option is set, and exposes the `DISPLAY` environment
|
|
9
|
+
# variable the browser process needs to attach to it.
|
|
10
|
+
#
|
|
5
11
|
class Xvfb
|
|
6
12
|
NOT_FOUND = "Could not find an executable for the Xvfb. Try to install " \
|
|
7
13
|
"it with your package manager"
|
|
8
14
|
|
|
15
|
+
#
|
|
16
|
+
# Builds and starts a new Xvfb instance.
|
|
17
|
+
#
|
|
18
|
+
# @param [Array] args
|
|
19
|
+
# Arguments forwarded to {#initialize}.
|
|
20
|
+
#
|
|
21
|
+
# @return [Xvfb]
|
|
22
|
+
#
|
|
9
23
|
def self.start(*args)
|
|
10
24
|
new(*args).tap(&:start)
|
|
11
25
|
end
|
|
@@ -20,11 +34,21 @@ module Ferrum
|
|
|
20
34
|
@display_id = (Time.now.to_f * 1000).to_i % 100_000_000
|
|
21
35
|
end
|
|
22
36
|
|
|
37
|
+
#
|
|
38
|
+
# Spawns the Xvfb process on the configured display.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
#
|
|
23
42
|
def start
|
|
24
43
|
@pid = ::Process.spawn("#{@path} :#{display_id} -screen 0 #{screen_size}")
|
|
25
44
|
::Process.detach(@pid)
|
|
26
45
|
end
|
|
27
46
|
|
|
47
|
+
#
|
|
48
|
+
# Environment variables needed to point the browser at this Xvfb display.
|
|
49
|
+
#
|
|
50
|
+
# @return [Hash{String => String}]
|
|
51
|
+
#
|
|
28
52
|
def to_env
|
|
29
53
|
{ "DISPLAY" => ":#{display_id}" }
|
|
30
54
|
end
|
data/lib/ferrum/browser.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "base64"
|
|
4
4
|
require "forwardable"
|
|
5
5
|
require "ferrum/page"
|
|
6
|
-
require "ferrum/
|
|
6
|
+
require "ferrum/worker"
|
|
7
7
|
require "ferrum/client"
|
|
8
8
|
require "ferrum/contexts"
|
|
9
9
|
require "ferrum/browser/xvfb"
|
|
@@ -13,15 +13,25 @@ require "ferrum/browser/binary"
|
|
|
13
13
|
require "ferrum/browser/version_info"
|
|
14
14
|
|
|
15
15
|
module Ferrum
|
|
16
|
+
#
|
|
17
|
+
# The main entry point of the library. Instantiating it spawns (or connects
|
|
18
|
+
# to) a browser process and opens a CDP connection to it, exposing a single
|
|
19
|
+
# default {Page} that most of the top-level methods (`go_to`, `at_css`,
|
|
20
|
+
# `screenshot`, etc.) are delegated to for convenience.
|
|
21
|
+
#
|
|
22
|
+
# @note A `Browser` can host multiple {Context}s (like incognito profiles)
|
|
23
|
+
# and multiple {Page}s within each of them, reachable through {#contexts}
|
|
24
|
+
# and {#create_page}.
|
|
25
|
+
#
|
|
16
26
|
class Browser
|
|
17
27
|
extend Forwardable
|
|
18
28
|
|
|
19
29
|
delegate %i[default_context] => :contexts
|
|
20
|
-
delegate %i[targets create_target page pages windows] => :default_context
|
|
30
|
+
delegate %i[targets create_target page pages windows workers service_workers attach_target] => :default_context
|
|
21
31
|
delegate %i[go_to goto go back forward refresh reload stop wait_for_reload
|
|
22
32
|
at_css at_xpath css xpath current_url current_title url title
|
|
23
33
|
body doctype content=
|
|
24
|
-
headers cookies network downloads
|
|
34
|
+
headers cookies network accessibility downloads
|
|
25
35
|
mouse keyboard
|
|
26
36
|
screenshot pdf mhtml viewport_size device_pixel_ratio
|
|
27
37
|
start_screencast stop_screencast
|
|
@@ -34,7 +44,8 @@ module Ferrum
|
|
|
34
44
|
|
|
35
45
|
attr_reader :client, :process, :contexts, :options
|
|
36
46
|
|
|
37
|
-
delegate %i[timeout timeout=
|
|
47
|
+
delegate %i[timeout timeout= protocol_timeout protocol_timeout=
|
|
48
|
+
base_url base_url= default_user_agent default_user_agent= extensions] => :options
|
|
38
49
|
delegate %i[command] => :client
|
|
39
50
|
|
|
40
51
|
#
|
|
@@ -70,18 +81,23 @@ module Ferrum
|
|
|
70
81
|
# When present, debug output is written to this object.
|
|
71
82
|
#
|
|
72
83
|
# @option options [Integer, Float] :slowmo
|
|
73
|
-
# Set a delay in seconds to wait before sending command.
|
|
74
|
-
# Useful companion of headless option, so that you have time to see
|
|
84
|
+
# Set a delay in seconds to wait before sending a command.
|
|
85
|
+
# Useful companion of a headless option, so that you have time to see
|
|
75
86
|
# changes.
|
|
76
87
|
#
|
|
77
88
|
# @option options [Numeric] :timeout (5)
|
|
78
|
-
# The number of seconds we'll wait for a response when communicating
|
|
79
|
-
# browser.
|
|
89
|
+
# The number of seconds we'll wait for a response when communicating
|
|
90
|
+
# with the browser: navigations, JS evaluation, DOM queries, dispatching input, etc.
|
|
91
|
+
#
|
|
92
|
+
# @option options [Numeric] :protocol_timeout (5)
|
|
93
|
+
# The number of seconds we'll wait for an individual internal CDP
|
|
94
|
+
# bookkeeping call to respond, e.g. `Target.createTarget`,
|
|
95
|
+
# `Target.attachToTarget`. These normally resolve in milliseconds.
|
|
80
96
|
#
|
|
81
97
|
# @option options [Boolean] :js_errors
|
|
82
98
|
# When true, JavaScript errors get re-raised in Ruby.
|
|
83
99
|
#
|
|
84
|
-
# @option options [Boolean] :pending_connection_errors (
|
|
100
|
+
# @option options [Boolean] :pending_connection_errors (true)
|
|
85
101
|
# When main frame is still waiting for slow responses while timeout is
|
|
86
102
|
# reached {PendingConnectionsError} is raised. It's better to figure out
|
|
87
103
|
# why you have slow responses and fix or block them rather than turn this
|
|
@@ -172,7 +188,7 @@ module Ferrum
|
|
|
172
188
|
ensure
|
|
173
189
|
if block_given?
|
|
174
190
|
page&.close
|
|
175
|
-
context
|
|
191
|
+
context&.dispose if new_context
|
|
176
192
|
end
|
|
177
193
|
end
|
|
178
194
|
|
|
@@ -211,11 +227,17 @@ module Ferrum
|
|
|
211
227
|
contexts.reset
|
|
212
228
|
end
|
|
213
229
|
|
|
230
|
+
#
|
|
231
|
+
# Restarts the browser process, keeping the same options.
|
|
232
|
+
#
|
|
214
233
|
def restart
|
|
215
234
|
quit
|
|
216
235
|
start
|
|
217
236
|
end
|
|
218
237
|
|
|
238
|
+
#
|
|
239
|
+
# Terminates the browser process and closes the client connection.
|
|
240
|
+
#
|
|
219
241
|
def quit
|
|
220
242
|
return unless @client
|
|
221
243
|
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Ferrum
|
|
4
4
|
class Client
|
|
5
|
+
#
|
|
6
|
+
# Dispatches incoming CDP events to registered callbacks. Messages are
|
|
7
|
+
# queued and processed on dedicated threads, with `Fetch.requestPaused`
|
|
8
|
+
# and `Fetch.authRequired` given priority so request interception isn't
|
|
9
|
+
# delayed behind other events.
|
|
10
|
+
#
|
|
5
11
|
class Subscriber
|
|
6
12
|
INTERRUPTIONS = %w[Fetch.requestPaused Fetch.authRequired].freeze
|
|
7
13
|
|
|
@@ -13,6 +19,16 @@ module Ferrum
|
|
|
13
19
|
start
|
|
14
20
|
end
|
|
15
21
|
|
|
22
|
+
#
|
|
23
|
+
# Enqueues an incoming CDP message for dispatch to subscribers.
|
|
24
|
+
# `Fetch.requestPaused`/`Fetch.authRequired` messages jump the regular
|
|
25
|
+
# queue so request interception isn't delayed behind other events.
|
|
26
|
+
#
|
|
27
|
+
# @param [Hash] message
|
|
28
|
+
# The raw CDP message, as parsed from the websocket.
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
31
|
+
#
|
|
16
32
|
def <<(message)
|
|
17
33
|
if INTERRUPTIONS.include?(message["method"])
|
|
18
34
|
@priority.push(message)
|
|
@@ -21,26 +37,68 @@ module Ferrum
|
|
|
21
37
|
end
|
|
22
38
|
end
|
|
23
39
|
|
|
40
|
+
#
|
|
41
|
+
# Registers a callback for a CDP event.
|
|
42
|
+
#
|
|
43
|
+
# @param [String] event
|
|
44
|
+
# The event key, as built by {SessionClient.event_name}.
|
|
45
|
+
#
|
|
46
|
+
# @return [Integer]
|
|
47
|
+
# The callback's index within the event's callback list, used to
|
|
48
|
+
# unsubscribe via {#off}.
|
|
49
|
+
#
|
|
24
50
|
def on(event, &block)
|
|
25
51
|
@on[event] ||= Concurrent::Array.new
|
|
26
52
|
@on[event] << block
|
|
27
53
|
@on[event].index(block)
|
|
28
54
|
end
|
|
29
55
|
|
|
56
|
+
#
|
|
57
|
+
# Unregisters a callback for a CDP event.
|
|
58
|
+
#
|
|
59
|
+
# @param [String] event
|
|
60
|
+
# The event key, as built by {SessionClient.event_name}.
|
|
61
|
+
#
|
|
62
|
+
# @param [Integer] id
|
|
63
|
+
# The callback's index, as returned by {#on}.
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
#
|
|
30
67
|
def off(event, id)
|
|
31
68
|
@on[event].delete_at(id)
|
|
32
69
|
true
|
|
33
70
|
end
|
|
34
71
|
|
|
72
|
+
#
|
|
73
|
+
# Whether there's at least one callback registered for the event.
|
|
74
|
+
#
|
|
75
|
+
# @param [String] event
|
|
76
|
+
# The event key, as built by {SessionClient.event_name}.
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
#
|
|
35
80
|
def subscribed?(event)
|
|
36
81
|
@on.key?(event)
|
|
37
82
|
end
|
|
38
83
|
|
|
84
|
+
#
|
|
85
|
+
# Stops the regular and priority dispatch threads.
|
|
86
|
+
#
|
|
87
|
+
# @return [void]
|
|
88
|
+
#
|
|
39
89
|
def close
|
|
40
90
|
@regular_thread&.kill
|
|
41
91
|
@priority_thread&.kill
|
|
42
92
|
end
|
|
43
93
|
|
|
94
|
+
#
|
|
95
|
+
# Removes all callbacks registered for a given session.
|
|
96
|
+
#
|
|
97
|
+
# @param [String] session_id
|
|
98
|
+
# The session id to match against registered event keys.
|
|
99
|
+
#
|
|
100
|
+
# @return [void]
|
|
101
|
+
#
|
|
44
102
|
def clear(session_id:)
|
|
45
103
|
@on.delete_if { |k, _| k.match?(session_id) }
|
|
46
104
|
end
|
|
@@ -6,6 +6,12 @@ require "websocket/driver"
|
|
|
6
6
|
|
|
7
7
|
module Ferrum
|
|
8
8
|
class Client
|
|
9
|
+
#
|
|
10
|
+
# Low-level WebSocket connection to the browser's CDP endpoint. Opens
|
|
11
|
+
# the raw TCP/TLS socket, drives the `websocket-driver` handshake and
|
|
12
|
+
# framing, and exposes a queue of parsed incoming messages alongside
|
|
13
|
+
# methods to send commands and close the connection.
|
|
14
|
+
#
|
|
9
15
|
class WebSocket
|
|
10
16
|
WEBSOCKET_BUG_SLEEP = 0.05
|
|
11
17
|
DEFAULT_PORTS = { "ws" => 80, "wss" => 443 }.freeze
|
|
@@ -30,7 +36,10 @@ module Ferrum
|
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
max_receive_size ||= ::WebSocket::Driver::MAX_LENGTH
|
|
33
|
-
@driver
|
|
39
|
+
@driver = ::WebSocket::Driver.client(self, max_length: max_receive_size)
|
|
40
|
+
# websocket-driver holds no locks and is called from many threads: commands from
|
|
41
|
+
# callers, pong/close replies from the reader. One lock keeps frames from interleaving.
|
|
42
|
+
@driver_mutex = Mutex.new
|
|
34
43
|
@messages = Queue.new
|
|
35
44
|
|
|
36
45
|
@screenshot_commands = Concurrent::Hash.new if SKIP_LOGGING_SCREENSHOTS
|
|
@@ -41,22 +50,28 @@ module Ferrum
|
|
|
41
50
|
|
|
42
51
|
start
|
|
43
52
|
|
|
44
|
-
@driver.start
|
|
53
|
+
@driver_mutex.synchronize { @driver.start }
|
|
45
54
|
end
|
|
46
55
|
|
|
56
|
+
#
|
|
57
|
+
# Handles the driver's `:open` event.
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
#
|
|
47
61
|
def on_open(_event)
|
|
48
62
|
# https://github.com/faye/websocket-driver-ruby/issues/46
|
|
49
63
|
sleep(WEBSOCKET_BUG_SLEEP)
|
|
50
64
|
end
|
|
51
65
|
|
|
66
|
+
#
|
|
67
|
+
# Handles the driver's `:message` event: parses the incoming frame
|
|
68
|
+
# as JSON and pushes it onto {#messages}. Malformed payloads are
|
|
69
|
+
# dropped rather than raised, to avoid crashing the reader thread.
|
|
70
|
+
#
|
|
71
|
+
# @return [void]
|
|
72
|
+
#
|
|
52
73
|
def on_message(event)
|
|
53
74
|
data = safely_parse_json(event.data)
|
|
54
|
-
# If we couldn't parse JSON data for some reason (parse error or deeply nested object) we
|
|
55
|
-
# don't push response to @messages. Worse that could happen we raise timeout error due to command didn't return
|
|
56
|
-
# anything or skip the background notification, but at least we don't crash the thread that crashes the main
|
|
57
|
-
# thread and the application.
|
|
58
|
-
@messages.push(data) if data
|
|
59
|
-
|
|
60
75
|
output = event.data
|
|
61
76
|
if SKIP_LOGGING_SCREENSHOTS && @screenshot_commands[data&.dig("id")]
|
|
62
77
|
@screenshot_commands.delete(data&.dig("id"))
|
|
@@ -64,30 +79,66 @@ module Ferrum
|
|
|
64
79
|
end
|
|
65
80
|
|
|
66
81
|
@logger&.puts(" ◀ #{Utils::ElapsedTime.elapsed_time} #{output}\n")
|
|
82
|
+
|
|
83
|
+
# If we couldn't parse JSON data for some reason (parse error or deeply nested object) we
|
|
84
|
+
# don't push response to @messages. Worse that could happen we raise timeout error due to command didn't return
|
|
85
|
+
# anything or skip the background notification, but at least we don't crash the thread that crashes the main
|
|
86
|
+
# thread and the application.
|
|
87
|
+
@messages.push(data) if data
|
|
67
88
|
end
|
|
68
89
|
|
|
90
|
+
#
|
|
91
|
+
# Handles the driver's `:close` event: closes the message queue and
|
|
92
|
+
# underlying socket, then kills the reader thread.
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
95
|
+
#
|
|
69
96
|
def on_close(_event)
|
|
70
97
|
@messages.close
|
|
71
98
|
@sock.close
|
|
72
99
|
@thread.kill
|
|
73
100
|
end
|
|
74
101
|
|
|
102
|
+
#
|
|
103
|
+
# Serializes a CDP command to JSON and sends it as a websocket text
|
|
104
|
+
# frame.
|
|
105
|
+
#
|
|
106
|
+
# @param [Hash] data
|
|
107
|
+
# The message to send, must include an `:id` key.
|
|
108
|
+
#
|
|
109
|
+
# @return [void]
|
|
110
|
+
#
|
|
75
111
|
def send_message(data)
|
|
76
112
|
@screenshot_commands[data[:id]] = true if SKIP_LOGGING_SCREENSHOTS
|
|
77
113
|
|
|
78
114
|
json = data.to_json
|
|
79
|
-
@driver.text(json)
|
|
115
|
+
@driver_mutex.synchronize { @driver.text(json) }
|
|
80
116
|
@logger&.puts("\n\n▶ #{Utils::ElapsedTime.elapsed_time} #{json}")
|
|
81
117
|
end
|
|
82
118
|
|
|
119
|
+
#
|
|
120
|
+
# Writes raw bytes to the underlying socket. Called by
|
|
121
|
+
# `websocket-driver` to emit frames. Closes {#messages} instead of
|
|
122
|
+
# raising if the connection has already been torn down.
|
|
123
|
+
#
|
|
124
|
+
# @param [String] data
|
|
125
|
+
# The raw bytes to write.
|
|
126
|
+
#
|
|
127
|
+
# @return [void]
|
|
128
|
+
#
|
|
83
129
|
def write(data)
|
|
84
130
|
@sock.write(data)
|
|
85
131
|
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, IOError # rubocop:disable Lint/ShadowedException
|
|
86
132
|
@messages.close
|
|
87
133
|
end
|
|
88
134
|
|
|
135
|
+
#
|
|
136
|
+
# Closes the websocket connection by sending a close frame.
|
|
137
|
+
#
|
|
138
|
+
# @return [void]
|
|
139
|
+
#
|
|
89
140
|
def close
|
|
90
|
-
@driver.close
|
|
141
|
+
@driver_mutex.synchronize { @driver.close }
|
|
91
142
|
end
|
|
92
143
|
|
|
93
144
|
private
|
|
@@ -98,7 +149,7 @@ module Ferrum
|
|
|
98
149
|
data = @sock.readpartial(512)
|
|
99
150
|
break unless data
|
|
100
151
|
|
|
101
|
-
@driver.parse(data)
|
|
152
|
+
@driver_mutex.synchronize { @driver.parse(data) }
|
|
102
153
|
end
|
|
103
154
|
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, IOError # rubocop:disable Lint/ShadowedException
|
|
104
155
|
@messages.close
|