ferrum 0.13 → 0.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +288 -154
  4. data/lib/ferrum/browser/command.rb +8 -0
  5. data/lib/ferrum/browser/options/chrome.rb +17 -5
  6. data/lib/ferrum/browser/options.rb +38 -25
  7. data/lib/ferrum/browser/process.rb +44 -17
  8. data/lib/ferrum/browser.rb +34 -52
  9. data/lib/ferrum/client/subscriber.rb +76 -0
  10. data/lib/ferrum/{browser → client}/web_socket.rb +36 -22
  11. data/lib/ferrum/client.rb +169 -0
  12. data/lib/ferrum/context.rb +19 -15
  13. data/lib/ferrum/contexts.rb +46 -12
  14. data/lib/ferrum/cookies/cookie.rb +57 -0
  15. data/lib/ferrum/cookies.rb +40 -4
  16. data/lib/ferrum/downloads.rb +60 -0
  17. data/lib/ferrum/errors.rb +2 -1
  18. data/lib/ferrum/frame.rb +1 -0
  19. data/lib/ferrum/headers.rb +1 -1
  20. data/lib/ferrum/network/exchange.rb +29 -2
  21. data/lib/ferrum/network/intercepted_request.rb +8 -17
  22. data/lib/ferrum/network/request.rb +23 -39
  23. data/lib/ferrum/network/request_params.rb +57 -0
  24. data/lib/ferrum/network/response.rb +25 -5
  25. data/lib/ferrum/network.rb +43 -16
  26. data/lib/ferrum/node.rb +21 -1
  27. data/lib/ferrum/page/frames.rb +5 -5
  28. data/lib/ferrum/page/screenshot.rb +42 -24
  29. data/lib/ferrum/page.rb +183 -131
  30. data/lib/ferrum/proxy.rb +1 -1
  31. data/lib/ferrum/target.rb +25 -5
  32. data/lib/ferrum/utils/elapsed_time.rb +0 -2
  33. data/lib/ferrum/utils/event.rb +19 -0
  34. data/lib/ferrum/utils/platform.rb +4 -0
  35. data/lib/ferrum/utils/thread.rb +18 -0
  36. data/lib/ferrum/version.rb +1 -1
  37. data/lib/ferrum.rb +3 -0
  38. metadata +14 -114
  39. data/lib/ferrum/browser/client.rb +0 -102
  40. 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 = except("headless", "disable-gpu") unless options.headless
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, :timeout, :logger, :ws_max_receive_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, :extensions, :proxy, :port, :host, :headless,
19
- :ignore_default_browser_options, :browser_options, :xvfb
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, 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
- @ws_max_receive_size, @env, @browser_name, @browser_path,
35
- @save_path, @extensions, @ignore_default_browser_options, @xvfb = @options.values_at(
36
- :ws_max_receive_size, :env, :browser_name, :browser_path, :save_path, :extensions,
37
- :ignore_default_browser_options, :xvfb
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[:window_size] = @window_size
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 to_h
51
- @options
53
+ def base_url=(value)
54
+ @base_url = parse_base_url(value)
52
55
  end
53
56
 
54
- def parse_base_url(value)
55
- parsed = Addressable::URI.parse(value)
56
- unless BASE_URL_SCHEMA.include?(parsed&.normalized_scheme)
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 parse_proxy(options)
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
- url = URI.join(options.url, "/json/version")
67
- response = JSON.parse(::Net::HTTP.get(url))
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
- parse_browser_versions
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 parse_browser_versions
167
- return unless ws_url.is_a?(Addressable::URI)
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
- version_url = URI.parse(ws_url.merge(scheme: "http", path: "/json/version"))
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
- def close_io(*ios)
180
- ios.each do |io|
181
- io.close unless io.closed?
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
@@ -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=] => :page
31
- delegate %i[default_user_agent] => :process
30
+ playback_rate playback_rate=
31
+ disable_javascript set_viewport resize] => :page
32
32
 
33
- attr_reader :client, :process, :contexts, :options, :window_size, :base_url
34
- attr_accessor :timeout
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.parse_proxy(proxy)
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.close
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.start(options)
270
- @client = Client.new(@process.ws_url, self,
271
- logger: options.logger,
272
- ws_max_receive_size: options.ws_max_receive_size)
273
- @contexts = Contexts.new(self)
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 Browser
8
+ class Client
9
9
  class WebSocket
10
- WEBSOCKET_BUG_SLEEP = 0.01
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 = url
17
- @logger = logger
18
- uri = URI.parse(@url)
19
- @sock = TCPSocket.new(uri.host, uri.port)
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
- @thread = Thread.new do
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