ferrum 0.13 → 0.15
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/LICENSE +1 -1
- data/README.md +288 -154
- data/lib/ferrum/browser/command.rb +8 -0
- data/lib/ferrum/browser/options/chrome.rb +17 -5
- data/lib/ferrum/browser/options.rb +38 -25
- data/lib/ferrum/browser/process.rb +44 -17
- data/lib/ferrum/browser.rb +34 -52
- data/lib/ferrum/client/subscriber.rb +76 -0
- data/lib/ferrum/{browser → client}/web_socket.rb +36 -22
- data/lib/ferrum/client.rb +169 -0
- data/lib/ferrum/context.rb +19 -15
- data/lib/ferrum/contexts.rb +46 -12
- data/lib/ferrum/cookies/cookie.rb +57 -0
- data/lib/ferrum/cookies.rb +40 -4
- data/lib/ferrum/downloads.rb +60 -0
- data/lib/ferrum/errors.rb +2 -1
- data/lib/ferrum/frame.rb +1 -0
- data/lib/ferrum/headers.rb +1 -1
- data/lib/ferrum/network/exchange.rb +29 -2
- data/lib/ferrum/network/intercepted_request.rb +8 -17
- data/lib/ferrum/network/request.rb +23 -39
- data/lib/ferrum/network/request_params.rb +57 -0
- data/lib/ferrum/network/response.rb +25 -5
- data/lib/ferrum/network.rb +43 -16
- data/lib/ferrum/node.rb +21 -1
- data/lib/ferrum/page/frames.rb +5 -5
- data/lib/ferrum/page/screenshot.rb +42 -24
- data/lib/ferrum/page.rb +183 -131
- data/lib/ferrum/proxy.rb +1 -1
- data/lib/ferrum/target.rb +25 -5
- data/lib/ferrum/utils/elapsed_time.rb +0 -2
- data/lib/ferrum/utils/event.rb +19 -0
- data/lib/ferrum/utils/platform.rb +4 -0
- data/lib/ferrum/utils/thread.rb +18 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum.rb +3 -0
- metadata +14 -114
- data/lib/ferrum/browser/client.rb +0 -102
- data/lib/ferrum/browser/subscriber.rb +0 -36
@@ -39,10 +39,18 @@ module Ferrum
|
|
39
39
|
!!options.xvfb
|
40
40
|
end
|
41
41
|
|
42
|
+
def headless_new?
|
43
|
+
@flags["headless"] == "new"
|
44
|
+
end
|
45
|
+
|
42
46
|
def to_a
|
43
47
|
[path] + @flags.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
|
44
48
|
end
|
45
49
|
|
50
|
+
def to_s
|
51
|
+
to_a.join(" \\ \n ")
|
52
|
+
end
|
53
|
+
|
46
54
|
private
|
47
55
|
|
48
56
|
def merge_options
|
@@ -6,7 +6,6 @@ module Ferrum
|
|
6
6
|
class Chrome < Base
|
7
7
|
DEFAULT_OPTIONS = {
|
8
8
|
"headless" => nil,
|
9
|
-
"disable-gpu" => nil,
|
10
9
|
"hide-scrollbars" => nil,
|
11
10
|
"mute-audio" => nil,
|
12
11
|
"enable-automation" => nil,
|
@@ -19,8 +18,9 @@ module Ferrum
|
|
19
18
|
"keep-alive-for-test" => nil,
|
20
19
|
"disable-popup-blocking" => nil,
|
21
20
|
"disable-extensions" => nil,
|
21
|
+
"disable-component-extensions-with-background-pages" => nil,
|
22
22
|
"disable-hang-monitor" => nil,
|
23
|
-
"disable-features" => "site-per-process,TranslateUI",
|
23
|
+
"disable-features" => "site-per-process,IsolateOrigins,TranslateUI",
|
24
24
|
"disable-translate" => nil,
|
25
25
|
"disable-background-networking" => nil,
|
26
26
|
"enable-features" => "NetworkService,NetworkServiceInProcess",
|
@@ -32,11 +32,13 @@ module Ferrum
|
|
32
32
|
"disable-ipc-flooding-protection" => nil,
|
33
33
|
"disable-prompt-on-repost" => nil,
|
34
34
|
"disable-renderer-backgrounding" => nil,
|
35
|
+
"disable-site-isolation-trials" => nil,
|
35
36
|
"force-color-profile" => "srgb",
|
36
37
|
"metrics-recording-only" => nil,
|
37
38
|
"safebrowsing-disable-auto-update" => nil,
|
38
39
|
"password-store" => "basic",
|
39
|
-
"no-startup-window" => nil
|
40
|
+
"no-startup-window" => nil,
|
41
|
+
"remote-allow-origins" => "*"
|
40
42
|
# NOTE: --no-sandbox is not needed if you properly setup a user in the container.
|
41
43
|
# https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
|
42
44
|
# "no-sandbox" => nil,
|
@@ -61,7 +63,6 @@ module Ferrum
|
|
61
63
|
def merge_required(flags, options, user_data_dir)
|
62
64
|
flags = flags.merge("remote-debugging-port" => options.port,
|
63
65
|
"remote-debugging-address" => options.host,
|
64
|
-
# Doesn't work on MacOS, so we need to set it by CDP
|
65
66
|
"window-size" => options.window_size&.join(","),
|
66
67
|
"user-data-dir" => user_data_dir)
|
67
68
|
|
@@ -74,9 +75,20 @@ module Ferrum
|
|
74
75
|
end
|
75
76
|
|
76
77
|
def merge_default(flags, options)
|
77
|
-
defaults =
|
78
|
+
defaults = case options.headless
|
79
|
+
when false
|
80
|
+
except("headless", "disable-gpu")
|
81
|
+
when "new"
|
82
|
+
except("headless").merge("headless" => "new")
|
83
|
+
end
|
78
84
|
|
79
85
|
defaults ||= DEFAULT_OPTIONS
|
86
|
+
# On Windows, the --disable-gpu flag is a temporary work around for a few bugs.
|
87
|
+
# See https://bugs.chromium.org/p/chromium/issues/detail?id=737678 for more information.
|
88
|
+
defaults = defaults.merge("disable-gpu" => nil) if Utils::Platform.windows?
|
89
|
+
# Use Metal on Apple Silicon
|
90
|
+
# https://github.com/google/angle#platform-support-via-backing-renderers
|
91
|
+
defaults = defaults.merge("use-angle" => "metal") if Utils::Platform.mac_arm?
|
80
92
|
defaults.merge(flags)
|
81
93
|
end
|
82
94
|
end
|
@@ -3,7 +3,6 @@
|
|
3
3
|
module Ferrum
|
4
4
|
class Browser
|
5
5
|
class Options
|
6
|
-
HEADLESS = true
|
7
6
|
BROWSER_PORT = "0"
|
8
7
|
BROWSER_HOST = "127.0.0.1"
|
9
8
|
WINDOW_SIZE = [1024, 768].freeze
|
@@ -12,55 +11,56 @@ module Ferrum
|
|
12
11
|
PROCESS_TIMEOUT = ENV.fetch("FERRUM_PROCESS_TIMEOUT", 10).to_i
|
13
12
|
DEBUG_MODE = !ENV.fetch("FERRUM_DEBUG", nil).nil?
|
14
13
|
|
15
|
-
attr_reader :window_size, :
|
14
|
+
attr_reader :window_size, :logger, :ws_max_receive_size,
|
16
15
|
:js_errors, :base_url, :slowmo, :pending_connection_errors,
|
17
|
-
:url, :env, :process_timeout, :browser_name, :browser_path,
|
18
|
-
:save_path, :
|
19
|
-
:ignore_default_browser_options, :
|
16
|
+
:url, :ws_url, :env, :process_timeout, :browser_name, :browser_path,
|
17
|
+
:save_path, :proxy, :port, :host, :headless, :browser_options,
|
18
|
+
:ignore_default_browser_options, :xvfb, :flatten
|
19
|
+
attr_accessor :timeout, :default_user_agent
|
20
20
|
|
21
21
|
def initialize(options = nil)
|
22
22
|
@options = Hash(options&.dup)
|
23
|
+
|
23
24
|
@port = @options.fetch(:port, BROWSER_PORT)
|
24
25
|
@host = @options.fetch(:host, BROWSER_HOST)
|
25
26
|
@timeout = @options.fetch(:timeout, DEFAULT_TIMEOUT)
|
26
27
|
@window_size = @options.fetch(:window_size, WINDOW_SIZE)
|
27
28
|
@js_errors = @options.fetch(:js_errors, false)
|
28
|
-
@headless = @options.fetch(:headless,
|
29
|
+
@headless = @options.fetch(:headless, true)
|
30
|
+
@flatten = @options.fetch(:flatten, true)
|
29
31
|
@pending_connection_errors = @options.fetch(:pending_connection_errors, true)
|
30
32
|
@process_timeout = @options.fetch(:process_timeout, PROCESS_TIMEOUT)
|
31
|
-
@browser_options = @options.fetch(:browser_options, {})
|
32
33
|
@slowmo = @options[:slowmo].to_f
|
33
34
|
|
34
|
-
@
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
@env = @options[:env]
|
36
|
+
@xvfb = @options[:xvfb]
|
37
|
+
@save_path = @options[:save_path]
|
38
|
+
@browser_name = @options[:browser_name]
|
39
|
+
@browser_path = @options[:browser_path]
|
40
|
+
@ws_max_receive_size = @options[:ws_max_receive_size]
|
41
|
+
@ignore_default_browser_options = @options[:ignore_default_browser_options]
|
39
42
|
|
40
|
-
@options[:
|
41
|
-
@proxy = parse_proxy(@options[:proxy])
|
43
|
+
@proxy = validate_proxy(@options[:proxy])
|
42
44
|
@logger = parse_logger(@options[:logger])
|
43
45
|
@base_url = parse_base_url(@options[:base_url]) if @options[:base_url]
|
44
46
|
@url = @options[:url].to_s if @options[:url]
|
47
|
+
@ws_url = @options[:ws_url].to_s if @options[:ws_url]
|
45
48
|
|
46
|
-
@options.freeze
|
47
|
-
@browser_options.freeze
|
49
|
+
@options = @options.merge(window_size: @window_size).freeze
|
50
|
+
@browser_options = @options.fetch(:browser_options, {}).freeze
|
48
51
|
end
|
49
52
|
|
50
|
-
def
|
51
|
-
@
|
53
|
+
def base_url=(value)
|
54
|
+
@base_url = parse_base_url(value)
|
52
55
|
end
|
53
56
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
raise ArgumentError, "`base_url` should be absolute and include schema: #{BASE_URL_SCHEMA.join(' | ')}"
|
57
|
+
def extensions
|
58
|
+
@extensions ||= Array(@options[:extensions]).map do |extension|
|
59
|
+
(extension.is_a?(Hash) && extension[:source]) || File.read(extension)
|
58
60
|
end
|
59
|
-
|
60
|
-
parsed
|
61
61
|
end
|
62
62
|
|
63
|
-
def
|
63
|
+
def validate_proxy(options)
|
64
64
|
return unless options
|
65
65
|
|
66
66
|
raise ArgumentError, "proxy options must be a Hash" unless options.is_a?(Hash)
|
@@ -72,6 +72,10 @@ module Ferrum
|
|
72
72
|
options
|
73
73
|
end
|
74
74
|
|
75
|
+
def to_h
|
76
|
+
@options
|
77
|
+
end
|
78
|
+
|
75
79
|
private
|
76
80
|
|
77
81
|
def parse_logger(logger)
|
@@ -79,6 +83,15 @@ module Ferrum
|
|
79
83
|
|
80
84
|
!logger && DEBUG_MODE ? $stdout.tap { |s| s.sync = true } : logger
|
81
85
|
end
|
86
|
+
|
87
|
+
def parse_base_url(value)
|
88
|
+
parsed = Addressable::URI.parse(value)
|
89
|
+
unless BASE_URL_SCHEMA.include?(parsed&.normalized_scheme)
|
90
|
+
raise ArgumentError, "`base_url` should be absolute and include schema: #{BASE_URL_SCHEMA.join(' | ')}"
|
91
|
+
end
|
92
|
+
|
93
|
+
parsed
|
94
|
+
end
|
82
95
|
end
|
83
96
|
end
|
84
97
|
end
|
@@ -62,11 +62,15 @@ module Ferrum
|
|
62
62
|
def initialize(options)
|
63
63
|
@pid = @xvfb = @user_data_dir = nil
|
64
64
|
|
65
|
+
if options.ws_url
|
66
|
+
response = parse_json_version(options.ws_url)
|
67
|
+
self.ws_url = response&.[]("webSocketDebuggerUrl") || options.ws_url
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
65
71
|
if options.url
|
66
|
-
|
67
|
-
|
68
|
-
self.ws_url = response["webSocketDebuggerUrl"]
|
69
|
-
parse_browser_versions
|
72
|
+
response = parse_json_version(options.url)
|
73
|
+
self.ws_url = response&.[]("webSocketDebuggerUrl")
|
70
74
|
return
|
71
75
|
end
|
72
76
|
|
@@ -100,7 +104,7 @@ module Ferrum
|
|
100
104
|
ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
|
101
105
|
|
102
106
|
parse_ws_url(read_io, @process_timeout)
|
103
|
-
|
107
|
+
parse_json_version(ws_url)
|
104
108
|
ensure
|
105
109
|
close_io(read_io, write_io)
|
106
110
|
end
|
@@ -122,6 +126,17 @@ module Ferrum
|
|
122
126
|
start
|
123
127
|
end
|
124
128
|
|
129
|
+
def inspect
|
130
|
+
"#<#{self.class} " \
|
131
|
+
"@user_data_dir=#{@user_data_dir.inspect} " \
|
132
|
+
"@command=#<#{@command.class}:#{@command.object_id}> " \
|
133
|
+
"@default_user_agent=#{@default_user_agent.inspect} " \
|
134
|
+
"@ws_url=#{@ws_url.inspect} " \
|
135
|
+
"@v8_version=#{@v8_version.inspect} " \
|
136
|
+
"@browser_version=#{@browser_version.inspect} " \
|
137
|
+
"@webkit_version=#{@webkit_version.inspect}>"
|
138
|
+
end
|
139
|
+
|
125
140
|
private
|
126
141
|
|
127
142
|
def kill(pid)
|
@@ -137,7 +152,7 @@ module Ferrum
|
|
137
152
|
output = ""
|
138
153
|
start = Utils::ElapsedTime.monotonic_time
|
139
154
|
max_time = start + timeout
|
140
|
-
regexp = %r{DevTools listening on (ws://.*)}
|
155
|
+
regexp = %r{DevTools listening on (ws://.*[a-zA-Z0-9-]{36})}
|
141
156
|
while (now = Utils::ElapsedTime.monotonic_time) < max_time
|
142
157
|
begin
|
143
158
|
output += read_io.read_nonblock(512)
|
@@ -163,25 +178,37 @@ module Ferrum
|
|
163
178
|
@port = @ws_url.port
|
164
179
|
end
|
165
180
|
|
166
|
-
def
|
167
|
-
|
181
|
+
def close_io(*ios)
|
182
|
+
ios.each do |io|
|
183
|
+
io.close unless io.closed?
|
184
|
+
rescue IOError
|
185
|
+
raise unless RUBY_ENGINE == "jruby"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def parse_json_version(url)
|
190
|
+
url = URI.join(url, "/json/version")
|
191
|
+
|
192
|
+
if %w[wss ws].include?(url.scheme)
|
193
|
+
url.scheme = case url.scheme
|
194
|
+
when "ws"
|
195
|
+
"http"
|
196
|
+
when "wss"
|
197
|
+
"https"
|
198
|
+
end
|
199
|
+
end
|
168
200
|
|
169
|
-
|
170
|
-
response = JSON.parse(::Net::HTTP.get(version_url))
|
201
|
+
response = JSON.parse(::Net::HTTP.get(URI(url.to_s)))
|
171
202
|
|
172
203
|
@v8_version = response["V8-Version"]
|
173
204
|
@browser_version = response["Browser"]
|
174
205
|
@webkit_version = response["WebKit-Version"]
|
175
206
|
@default_user_agent = response["User-Agent"]
|
176
207
|
@protocol_version = response["Protocol-Version"]
|
177
|
-
end
|
178
208
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
rescue IOError
|
183
|
-
raise unless RUBY_ENGINE == "jruby"
|
184
|
-
end
|
209
|
+
response
|
210
|
+
rescue StandardError
|
211
|
+
# nop
|
185
212
|
end
|
186
213
|
end
|
187
214
|
end
|
data/lib/ferrum/browser.rb
CHANGED
@@ -4,11 +4,11 @@ require "base64"
|
|
4
4
|
require "forwardable"
|
5
5
|
require "ferrum/page"
|
6
6
|
require "ferrum/proxy"
|
7
|
+
require "ferrum/client"
|
7
8
|
require "ferrum/contexts"
|
8
9
|
require "ferrum/browser/xvfb"
|
9
10
|
require "ferrum/browser/options"
|
10
11
|
require "ferrum/browser/process"
|
11
|
-
require "ferrum/browser/client"
|
12
12
|
require "ferrum/browser/binary"
|
13
13
|
require "ferrum/browser/version_info"
|
14
14
|
|
@@ -20,18 +20,20 @@ module Ferrum
|
|
20
20
|
delegate %i[go_to goto go back forward refresh reload stop wait_for_reload
|
21
21
|
at_css at_xpath css xpath current_url current_title url title
|
22
22
|
body doctype content=
|
23
|
-
headers cookies network
|
23
|
+
headers cookies network downloads
|
24
24
|
mouse keyboard
|
25
|
-
screenshot pdf mhtml viewport_size
|
25
|
+
screenshot pdf mhtml viewport_size device_pixel_ratio
|
26
26
|
frames frame_by main_frame
|
27
27
|
evaluate evaluate_on evaluate_async execute evaluate_func
|
28
28
|
add_script_tag add_style_tag bypass_csp
|
29
29
|
on position position=
|
30
|
-
playback_rate playback_rate=
|
31
|
-
|
30
|
+
playback_rate playback_rate=
|
31
|
+
disable_javascript set_viewport resize] => :page
|
32
32
|
|
33
|
-
attr_reader :client, :process, :contexts, :options
|
34
|
-
|
33
|
+
attr_reader :client, :process, :contexts, :options
|
34
|
+
|
35
|
+
delegate %i[timeout timeout= base_url base_url= default_user_agent default_user_agent= extensions] => :options
|
36
|
+
delegate %i[command] => :client
|
35
37
|
|
36
38
|
#
|
37
39
|
# Initializes the browser.
|
@@ -45,6 +47,9 @@ module Ferrum
|
|
45
47
|
# @option options [Boolean] :xvfb (false)
|
46
48
|
# Run browser in a virtual framebuffer.
|
47
49
|
#
|
50
|
+
# @option options [Boolean] :flatten (true)
|
51
|
+
# Use one websocket connection to the browser and all the pages in flatten mode.
|
52
|
+
#
|
48
53
|
# @option options [(Integer, Integer)] :window_size ([1024, 768])
|
49
54
|
# The dimensions of the browser window in which to test, expressed as a
|
50
55
|
# 2-element array, e.g. `[1024, 768]`.
|
@@ -124,28 +129,9 @@ module Ferrum
|
|
124
129
|
@options = Options.new(options)
|
125
130
|
@client = @process = @contexts = nil
|
126
131
|
|
127
|
-
@timeout = @options.timeout
|
128
|
-
@window_size = @options.window_size
|
129
|
-
@base_url = @options.base_url if @options.base_url
|
130
|
-
|
131
132
|
start
|
132
133
|
end
|
133
134
|
|
134
|
-
#
|
135
|
-
# Sets the base URL.
|
136
|
-
#
|
137
|
-
# @param [String] value
|
138
|
-
# The new base URL value.
|
139
|
-
#
|
140
|
-
# @raise [ArgumentError] when path is not absolute or doesn't include schema
|
141
|
-
#
|
142
|
-
# @return [Addressable::URI]
|
143
|
-
# The parsed base URI value.
|
144
|
-
#
|
145
|
-
def base_url=(value)
|
146
|
-
@base_url = options.parse_base_url(value)
|
147
|
-
end
|
148
|
-
|
149
135
|
#
|
150
136
|
# Creates a new page.
|
151
137
|
#
|
@@ -163,7 +149,7 @@ module Ferrum
|
|
163
149
|
params = {}
|
164
150
|
|
165
151
|
if proxy
|
166
|
-
options.
|
152
|
+
options.validate_proxy(proxy)
|
167
153
|
params.merge!(proxyServer: "#{proxy[:host]}:#{proxy[:port]}")
|
168
154
|
params.merge!(proxyBypassList: proxy[:bypass]) if proxy[:bypass]
|
169
155
|
end
|
@@ -177,17 +163,11 @@ module Ferrum
|
|
177
163
|
block_given? ? yield(page) : page
|
178
164
|
ensure
|
179
165
|
if block_given?
|
180
|
-
page
|
166
|
+
page&.close
|
181
167
|
context.dispose if new_context
|
182
168
|
end
|
183
169
|
end
|
184
170
|
|
185
|
-
def extensions
|
186
|
-
@extensions ||= Array(options.extensions).map do |ext|
|
187
|
-
(ext.is_a?(Hash) && ext[:source]) || File.read(ext)
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
171
|
#
|
192
172
|
# Evaluate JavaScript to modify things before a page load.
|
193
173
|
#
|
@@ -205,13 +185,6 @@ module Ferrum
|
|
205
185
|
extensions << expression
|
206
186
|
end
|
207
187
|
|
208
|
-
def command(*args)
|
209
|
-
@client.command(*args)
|
210
|
-
rescue DeadBrowserError
|
211
|
-
restart
|
212
|
-
raise
|
213
|
-
end
|
214
|
-
|
215
188
|
#
|
216
189
|
# Closes browser tabs opened by the `Browser` instance.
|
217
190
|
#
|
@@ -227,7 +200,6 @@ module Ferrum
|
|
227
200
|
# browser.quit
|
228
201
|
#
|
229
202
|
def reset
|
230
|
-
@window_size = options.window_size
|
231
203
|
contexts.reset
|
232
204
|
end
|
233
205
|
|
@@ -237,16 +209,15 @@ module Ferrum
|
|
237
209
|
end
|
238
210
|
|
239
211
|
def quit
|
212
|
+
return unless @client
|
213
|
+
|
214
|
+
contexts.close_connections
|
215
|
+
|
240
216
|
@client.close
|
241
217
|
@process.stop
|
242
218
|
@client = @process = @contexts = nil
|
243
219
|
end
|
244
220
|
|
245
|
-
def resize(**options)
|
246
|
-
@window_size = [options[:width], options[:height]]
|
247
|
-
page.resize(**options)
|
248
|
-
end
|
249
|
-
|
250
221
|
def crash
|
251
222
|
command("Browser.crash")
|
252
223
|
end
|
@@ -262,15 +233,26 @@ module Ferrum
|
|
262
233
|
VersionInfo.new(command("Browser.getVersion"))
|
263
234
|
end
|
264
235
|
|
236
|
+
def headless_new?
|
237
|
+
process&.command&.headless_new?
|
238
|
+
end
|
239
|
+
|
265
240
|
private
|
266
241
|
|
267
242
|
def start
|
268
243
|
Utils::ElapsedTime.start
|
269
|
-
@process = Process.
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
244
|
+
@process = Process.new(options)
|
245
|
+
|
246
|
+
begin
|
247
|
+
@process.start
|
248
|
+
@options.default_user_agent = @process.default_user_agent
|
249
|
+
|
250
|
+
@client = Client.new(@process.ws_url, options)
|
251
|
+
@contexts = Contexts.new(@client)
|
252
|
+
rescue StandardError
|
253
|
+
@process.stop
|
254
|
+
raise
|
255
|
+
end
|
274
256
|
end
|
275
257
|
end
|
276
258
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Client
|
5
|
+
class Subscriber
|
6
|
+
INTERRUPTIONS = %w[Fetch.requestPaused Fetch.authRequired].freeze
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@regular = Queue.new
|
10
|
+
@priority = Queue.new
|
11
|
+
@on = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
|
12
|
+
|
13
|
+
start
|
14
|
+
end
|
15
|
+
|
16
|
+
def <<(message)
|
17
|
+
if INTERRUPTIONS.include?(message["method"])
|
18
|
+
@priority.push(message)
|
19
|
+
else
|
20
|
+
@regular.push(message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def on(event, &block)
|
25
|
+
@on[event] << block
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def subscribed?(event)
|
30
|
+
@on.key?(event)
|
31
|
+
end
|
32
|
+
|
33
|
+
def close
|
34
|
+
@regular_thread&.kill
|
35
|
+
@priority_thread&.kill
|
36
|
+
end
|
37
|
+
|
38
|
+
def clear(session_id:)
|
39
|
+
@on.delete_if { |k, _| k.match?(session_id) }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def start
|
45
|
+
@regular_thread = Utils::Thread.spawn(abort_on_exception: false) do
|
46
|
+
loop do
|
47
|
+
message = @regular.pop
|
48
|
+
break unless message
|
49
|
+
|
50
|
+
call(message)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
@priority_thread = Utils::Thread.spawn(abort_on_exception: false) do
|
55
|
+
loop do
|
56
|
+
message = @priority.pop
|
57
|
+
break unless message
|
58
|
+
|
59
|
+
call(message)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def call(message)
|
65
|
+
method, session_id, params = message.values_at("method", "sessionId", "params")
|
66
|
+
event = SessionClient.event_name(method, session_id)
|
67
|
+
|
68
|
+
total = @on[event].size
|
69
|
+
@on[event].each_with_index do |block, index|
|
70
|
+
# In case of multiple callbacks we provide current index and total
|
71
|
+
block.call(params, index, total)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -5,18 +5,30 @@ require "socket"
|
|
5
5
|
require "websocket/driver"
|
6
6
|
|
7
7
|
module Ferrum
|
8
|
-
class
|
8
|
+
class Client
|
9
9
|
class WebSocket
|
10
|
-
WEBSOCKET_BUG_SLEEP = 0.
|
10
|
+
WEBSOCKET_BUG_SLEEP = 0.05
|
11
|
+
DEFAULT_PORTS = { "ws" => 80, "wss" => 443 }.freeze
|
11
12
|
SKIP_LOGGING_SCREENSHOTS = !ENV["FERRUM_LOGGING_SCREENSHOTS"]
|
12
13
|
|
13
14
|
attr_reader :url, :messages
|
14
15
|
|
15
16
|
def initialize(url, max_receive_size, logger)
|
16
|
-
@url
|
17
|
-
@logger
|
18
|
-
uri
|
19
|
-
|
17
|
+
@url = url
|
18
|
+
@logger = logger
|
19
|
+
uri = URI.parse(@url)
|
20
|
+
port = uri.port || DEFAULT_PORTS[uri.scheme]
|
21
|
+
|
22
|
+
if port == 443
|
23
|
+
tcp = TCPSocket.new(uri.host, port)
|
24
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
25
|
+
@sock = OpenSSL::SSL::SSLSocket.new(tcp, ssl_context)
|
26
|
+
@sock.sync_close = true
|
27
|
+
@sock.connect
|
28
|
+
else
|
29
|
+
@sock = TCPSocket.new(uri.host, port)
|
30
|
+
end
|
31
|
+
|
20
32
|
max_receive_size ||= ::WebSocket::Driver::MAX_LENGTH
|
21
33
|
@driver = ::WebSocket::Driver.client(self, max_length: max_receive_size)
|
22
34
|
@messages = Queue.new
|
@@ -27,21 +39,7 @@ module Ferrum
|
|
27
39
|
@driver.on(:message, &method(:on_message))
|
28
40
|
@driver.on(:close, &method(:on_close))
|
29
41
|
|
30
|
-
|
31
|
-
Thread.current.abort_on_exception = true
|
32
|
-
Thread.current.report_on_exception = true if Thread.current.respond_to?(:report_on_exception=)
|
33
|
-
|
34
|
-
begin
|
35
|
-
loop do
|
36
|
-
data = @sock.readpartial(512)
|
37
|
-
break unless data
|
38
|
-
|
39
|
-
@driver.parse(data)
|
40
|
-
end
|
41
|
-
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
|
42
|
-
@messages.close
|
43
|
-
end
|
44
|
-
end
|
42
|
+
start
|
45
43
|
|
46
44
|
@driver.start
|
47
45
|
end
|
@@ -66,6 +64,7 @@ module Ferrum
|
|
66
64
|
|
67
65
|
def on_close(_event)
|
68
66
|
@messages.close
|
67
|
+
@sock.close
|
69
68
|
@thread.kill
|
70
69
|
end
|
71
70
|
|
@@ -79,13 +78,28 @@ module Ferrum
|
|
79
78
|
|
80
79
|
def write(data)
|
81
80
|
@sock.write(data)
|
82
|
-
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
|
81
|
+
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, IOError # rubocop:disable Lint/ShadowedException
|
83
82
|
@messages.close
|
84
83
|
end
|
85
84
|
|
86
85
|
def close
|
87
86
|
@driver.close
|
88
87
|
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def start
|
92
|
+
@thread = Utils::Thread.spawn do
|
93
|
+
loop do
|
94
|
+
data = @sock.readpartial(512)
|
95
|
+
break unless data
|
96
|
+
|
97
|
+
@driver.parse(data)
|
98
|
+
end
|
99
|
+
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, IOError # rubocop:disable Lint/ShadowedException
|
100
|
+
@messages.close
|
101
|
+
end
|
102
|
+
end
|
89
103
|
end
|
90
104
|
end
|
91
105
|
end
|