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.
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