ferrum 0.13 → 0.15
Sign up to get free protection for your applications and to get access to all the features.
- 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
|