ferrum 0.6.2 → 0.10.1

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.
data/lib/ferrum.rb CHANGED
@@ -10,8 +10,20 @@ module Ferrum
10
10
  class NotImplementedError < Error; end
11
11
 
12
12
  class StatusError < Error
13
- def initialize(url)
14
- super("Request to #{url} failed to reach server, check DNS and/or server status")
13
+ def initialize(url, message = nil)
14
+ super(message || "Request to #{url} failed to reach server, check DNS and server status")
15
+ end
16
+ end
17
+
18
+ class PendingConnectionsError < StatusError
19
+ attr_reader :pendings
20
+
21
+ def initialize(url, pendings = [])
22
+ @pendings = pendings
23
+
24
+ message = "Request to #{url} reached server, but there are still pending connections: #{pendings.join(', ')}"
25
+
26
+ super(url, message)
15
27
  end
16
28
  end
17
29
 
@@ -30,12 +42,34 @@ module Ferrum
30
42
  end
31
43
  end
32
44
 
45
+ class ProcessTimeoutError < Error
46
+ attr_reader :output
47
+
48
+ def initialize(timeout, output)
49
+ @output = output
50
+ super("Browser did not produce websocket url within #{timeout} seconds, try to increase `:process_timeout`. See https://github.com/rubycdp/ferrum#customization")
51
+ end
52
+ end
53
+
33
54
  class DeadBrowserError < Error
34
- def initialize(message = "Browser is dead")
55
+ def initialize(message = "Browser is dead or given window is closed")
35
56
  super
36
57
  end
37
58
  end
38
59
 
60
+ class NodeIsMovingError < Error
61
+ def initialize(node, prev, current)
62
+ @node, @prev, @current = node, prev, current
63
+ super(message)
64
+ end
65
+
66
+ def message
67
+ "#{@node.inspect} that you're trying to click is moving, hence " \
68
+ "we cannot. Previosuly it was at #{@prev.inspect} but now at " \
69
+ "#{@current.inspect}."
70
+ end
71
+ end
72
+
39
73
  class BrowserError < Error
40
74
  attr_reader :response
41
75
 
@@ -66,8 +100,8 @@ module Ferrum
66
100
  attr_reader :class_name, :message
67
101
 
68
102
  def initialize(response)
69
- super
70
103
  @class_name, @message = response.values_at("className", "description")
104
+ super(response.merge("message" => @message))
71
105
  end
72
106
  end
73
107
 
@@ -4,6 +4,7 @@ require "base64"
4
4
  require "forwardable"
5
5
  require "ferrum/page"
6
6
  require "ferrum/contexts"
7
+ require "ferrum/browser/xvfb"
7
8
  require "ferrum/browser/process"
8
9
  require "ferrum/browser/client"
9
10
 
@@ -16,18 +17,20 @@ module Ferrum
16
17
  extend Forwardable
17
18
  delegate %i[default_context] => :contexts
18
19
  delegate %i[targets create_target create_page page pages windows] => :default_context
19
- delegate %i[goto back forward refresh
20
- at_css at_xpath css xpath current_url title body doctype
20
+ delegate %i[go_to back forward refresh reload stop wait_for_reload
21
+ at_css at_xpath css xpath current_url current_title url title
22
+ body doctype set_content
21
23
  headers cookies network
22
24
  mouse keyboard
23
- screenshot pdf viewport_size
25
+ screenshot pdf mhtml viewport_size
24
26
  frames frame_by main_frame
25
- evaluate evaluate_on evaluate_async execute
26
- add_script_tag add_style_tag
27
- on] => :page
27
+ evaluate evaluate_on evaluate_async execute evaluate_func
28
+ add_script_tag add_style_tag bypass_csp
29
+ on goto] => :page
30
+ delegate %i[default_user_agent] => :process
28
31
 
29
- attr_reader :client, :process, :contexts, :logger, :js_errors,
30
- :slowmo, :base_url, :options, :window_size
32
+ attr_reader :client, :process, :contexts, :logger, :js_errors, :pending_connection_errors,
33
+ :slowmo, :base_url, :options, :window_size, :ws_max_receive_size
31
34
  attr_writer :timeout
32
35
 
33
36
  def initialize(options = nil)
@@ -38,9 +41,11 @@ module Ferrum
38
41
  @original_window_size = @window_size
39
42
 
40
43
  @options = Hash(options.merge(window_size: @window_size))
41
- @logger, @timeout = @options.values_at(:logger, :timeout)
44
+ @logger, @timeout, @ws_max_receive_size =
45
+ @options.values_at(:logger, :timeout, :ws_max_receive_size)
42
46
  @js_errors = @options.fetch(:js_errors, false)
43
- @slowmo = @options[:slowmo].to_i
47
+ @pending_connection_errors = @options.fetch(:pending_connection_errors, true)
48
+ @slowmo = @options[:slowmo].to_f
44
49
 
45
50
  if @options.key?(:base_url)
46
51
  self.base_url = @options[:base_url]
@@ -67,7 +72,9 @@ module Ferrum
67
72
  end
68
73
 
69
74
  def extensions
70
- @extensions ||= Array(@options[:extensions]).map { |p| File.read(p) }
75
+ @extensions ||= Array(@options[:extensions]).map do |ext|
76
+ (ext.is_a?(Hash) && ext[:source]) || File.read(ext)
77
+ end
71
78
  end
72
79
 
73
80
  def timeout
@@ -111,7 +118,7 @@ module Ferrum
111
118
  def start
112
119
  Ferrum.started
113
120
  @process = Process.start(@options)
114
- @client = Client.new(self, @process.ws_url, 0, false)
121
+ @client = Client.new(self, @process.ws_url)
115
122
  @contexts = Contexts.new(self)
116
123
  end
117
124
  end
@@ -7,20 +7,25 @@ require "ferrum/browser/web_socket"
7
7
  module Ferrum
8
8
  class Browser
9
9
  class Client
10
- def initialize(browser, ws_url, start_id = 0, allow_slowmo = true)
11
- @command_id = start_id
12
- @pendings = Concurrent::Hash.new
10
+ INTERRUPTIONS = %w[Fetch.requestPaused Fetch.authRequired].freeze
11
+
12
+ def initialize(browser, ws_url, id_starts_with: 0)
13
13
  @browser = browser
14
- @slowmo = @browser.slowmo if allow_slowmo && @browser.slowmo > 0
15
- @ws = WebSocket.new(ws_url, @browser.logger)
16
- @subscriber = Subscriber.new
14
+ @command_id = id_starts_with
15
+ @pendings = Concurrent::Hash.new
16
+ @ws = WebSocket.new(ws_url, @browser.ws_max_receive_size, @browser.logger)
17
+ @subscriber, @interruptor = Subscriber.build(2)
17
18
 
18
19
  @thread = Thread.new do
19
20
  Thread.current.abort_on_exception = true
20
- Thread.current.report_on_exception = true if Thread.current.respond_to?(:report_on_exception=)
21
+ if Thread.current.respond_to?(:report_on_exception=)
22
+ Thread.current.report_on_exception = true
23
+ end
21
24
 
22
25
  while message = @ws.messages.pop
23
- if message.key?("method")
26
+ if INTERRUPTIONS.include?(message["method"])
27
+ @interruptor.async.call(message)
28
+ elsif message.key?("method")
24
29
  @subscriber.async.call(message)
25
30
  else
26
31
  @pendings[message["id"]]&.set(message)
@@ -33,7 +38,6 @@ module Ferrum
33
38
  pending = Concurrent::IVar.new
34
39
  message = build_message(method, params)
35
40
  @pendings[message[:id]] = pending
36
- sleep(@slowmo) if @slowmo
37
41
  @ws.send_message(message)
38
42
  data = pending.value!(@browser.timeout)
39
43
  @pendings.delete(message[:id])
@@ -46,7 +50,16 @@ module Ferrum
46
50
  end
47
51
 
48
52
  def on(event, &block)
49
- @subscriber.on(event, &block)
53
+ case event
54
+ when *INTERRUPTIONS
55
+ @interruptor.on(event, &block)
56
+ else
57
+ @subscriber.on(event, &block)
58
+ end
59
+ end
60
+
61
+ def subscribed?(event)
62
+ [@interruptor, @subscriber].any? { |s| s.subscribed?(event) }
50
63
  end
51
64
 
52
65
  def close
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Browser
5
+ class Command
6
+ NOT_FOUND = "Could not find an executable for the browser. Try to make " \
7
+ "it available on the PATH or set environment variable for " \
8
+ "example BROWSER_PATH=\"/usr/bin/chrome\"".freeze
9
+
10
+ # Currently only these browsers support CDP:
11
+ # https://github.com/cyrus-and/chrome-remote-interface#implementations
12
+ def self.build(options, user_data_dir)
13
+ defaults = case options[:browser_name]
14
+ when :firefox
15
+ Options::Firefox.options
16
+ when :chrome, :opera, :edge, nil
17
+ Options::Chrome.options
18
+ else
19
+ raise NotImplementedError, "not supported browser"
20
+ end
21
+
22
+ new(defaults, options, user_data_dir)
23
+ end
24
+
25
+ attr_reader :defaults, :path, :options
26
+
27
+ def initialize(defaults, options, user_data_dir)
28
+ @flags = {}
29
+ @defaults = defaults
30
+ @options, @user_data_dir = options, user_data_dir
31
+ @path = options[:browser_path] || ENV["BROWSER_PATH"] || defaults.detect_path
32
+ raise Cliver::Dependency::NotFound.new(NOT_FOUND) unless @path
33
+ merge_options
34
+ end
35
+
36
+ def xvfb?
37
+ !!options[:xvfb]
38
+ end
39
+
40
+ def to_a
41
+ [path] + @flags.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
42
+ end
43
+
44
+ private
45
+
46
+ def merge_options
47
+ @flags = defaults.merge_required(@flags, options, @user_data_dir)
48
+
49
+ unless options[:ignore_default_browser_options]
50
+ @flags = defaults.merge_default(@flags, options)
51
+ end
52
+
53
+ @flags.merge!(options.fetch(:browser_options, {}))
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module Ferrum
6
+ class Browser
7
+ module Options
8
+ class Base
9
+ BROWSER_HOST = "127.0.0.1"
10
+ BROWSER_PORT = "0"
11
+
12
+ include Singleton
13
+
14
+ def self.options
15
+ instance
16
+ end
17
+
18
+ def to_h
19
+ self.class::DEFAULT_OPTIONS
20
+ end
21
+
22
+ def except(*keys)
23
+ to_h.reject { |n, _| keys.include?(n) }
24
+ end
25
+
26
+ def detect_path
27
+ if Ferrum.mac?
28
+ self.class::MAC_BIN_PATH.find { |n| File.exist?(n) }
29
+ else
30
+ self.class::LINUX_BIN_PATH.find do |name|
31
+ path = Cliver.detect(name) and break(path)
32
+ end
33
+ end
34
+ end
35
+
36
+ def merge_required(flags, options, user_data_dir)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def merge_default(flags, options)
41
+ raise NotImplementedError
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Browser
5
+ module Options
6
+ class Chrome < Base
7
+ DEFAULT_OPTIONS = {
8
+ "headless" => nil,
9
+ "disable-gpu" => nil,
10
+ "hide-scrollbars" => nil,
11
+ "mute-audio" => nil,
12
+ "enable-automation" => nil,
13
+ "disable-web-security" => nil,
14
+ "disable-session-crashed-bubble" => nil,
15
+ "disable-breakpad" => nil,
16
+ "disable-sync" => nil,
17
+ "no-first-run" => nil,
18
+ "use-mock-keychain" => nil,
19
+ "keep-alive-for-test" => nil,
20
+ "disable-popup-blocking" => nil,
21
+ "disable-extensions" => nil,
22
+ "disable-hang-monitor" => nil,
23
+ "disable-features" => "site-per-process,TranslateUI",
24
+ "disable-translate" => nil,
25
+ "disable-background-networking" => nil,
26
+ "enable-features" => "NetworkService,NetworkServiceInProcess",
27
+ "disable-background-timer-throttling" => nil,
28
+ "disable-backgrounding-occluded-windows" => nil,
29
+ "disable-client-side-phishing-detection" => nil,
30
+ "disable-default-apps" => nil,
31
+ "disable-dev-shm-usage" => nil,
32
+ "disable-ipc-flooding-protection" => nil,
33
+ "disable-prompt-on-repost" => nil,
34
+ "disable-renderer-backgrounding" => nil,
35
+ "force-color-profile" => "srgb",
36
+ "metrics-recording-only" => nil,
37
+ "safebrowsing-disable-auto-update" => nil,
38
+ "password-store" => "basic",
39
+ # Note: --no-sandbox is not needed if you properly setup a user in the container.
40
+ # https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
41
+ # "no-sandbox" => nil,
42
+ }.freeze
43
+
44
+ MAC_BIN_PATH = [
45
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
46
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
47
+ ].freeze
48
+ LINUX_BIN_PATH = %w[chromium google-chrome-unstable google-chrome-beta
49
+ google-chrome chrome chromium-browser
50
+ google-chrome-stable].freeze
51
+
52
+ def merge_required(flags, options, user_data_dir)
53
+ port = options.fetch(:port, BROWSER_PORT)
54
+ host = options.fetch(:host, BROWSER_HOST)
55
+ flags.merge("remote-debugging-port" => port,
56
+ "remote-debugging-address" => host,
57
+ # Doesn't work on MacOS, so we need to set it by CDP
58
+ "window-size" => options[:window_size].join(","),
59
+ "user-data-dir" => user_data_dir)
60
+ end
61
+
62
+ def merge_default(flags, options)
63
+ unless options.fetch(:headless, true)
64
+ defaults = except("headless", "disable-gpu")
65
+ end
66
+
67
+ defaults ||= DEFAULT_OPTIONS
68
+ defaults.merge(flags)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Browser
5
+ module Options
6
+ class Firefox < Base
7
+ DEFAULT_OPTIONS = {
8
+ "headless" => nil,
9
+ }.freeze
10
+
11
+ MAC_BIN_PATH = [
12
+ "/Applications/Firefox.app/Contents/MacOS/firefox-bin"
13
+ ].freeze
14
+ LINUX_BIN_PATH = %w[firefox].freeze
15
+
16
+ def merge_required(flags, options, user_data_dir)
17
+ port = options.fetch(:port, BROWSER_PORT)
18
+ host = options.fetch(:host, BROWSER_HOST)
19
+ flags.merge("remote-debugger" => "#{host}:#{port}",
20
+ "profile" => user_data_dir)
21
+ end
22
+
23
+ def merge_default(flags, options)
24
+ unless options.fetch(:headless, true)
25
+ defaults = except("headless")
26
+ end
27
+
28
+ defaults ||= DEFAULT_OPTIONS
29
+ defaults.merge(flags)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -5,59 +5,26 @@ require "net/http"
5
5
  require "json"
6
6
  require "addressable"
7
7
  require "tmpdir"
8
+ require "forwardable"
9
+ require "ferrum/browser/options/base"
10
+ require "ferrum/browser/options/chrome"
11
+ require "ferrum/browser/options/firefox"
12
+ require "ferrum/browser/command"
8
13
 
9
14
  module Ferrum
10
15
  class Browser
11
16
  class Process
12
17
  KILL_TIMEOUT = 2
13
18
  WAIT_KILLED = 0.05
14
- PROCESS_TIMEOUT = ENV.fetch("FERRUM_PROCESS_TIMEOUT", 2).to_i
15
- BROWSER_PATH = ENV["BROWSER_PATH"]
16
- BROWSER_HOST = "127.0.0.1"
17
- BROWSER_PORT = "0"
18
- DEFAULT_OPTIONS = {
19
- "headless" => nil,
20
- "disable-gpu" => nil,
21
- "hide-scrollbars" => nil,
22
- "mute-audio" => nil,
23
- "enable-automation" => nil,
24
- "disable-web-security" => nil,
25
- "disable-session-crashed-bubble" => nil,
26
- "disable-breakpad" => nil,
27
- "disable-sync" => nil,
28
- "no-first-run" => nil,
29
- "use-mock-keychain" => nil,
30
- "keep-alive-for-test" => nil,
31
- "disable-popup-blocking" => nil,
32
- "disable-extensions" => nil,
33
- "disable-hang-monitor" => nil,
34
- "disable-features" => "site-per-process,TranslateUI",
35
- "disable-translate" => nil,
36
- "disable-background-networking" => nil,
37
- "enable-features" => "NetworkService,NetworkServiceInProcess",
38
- "disable-background-timer-throttling" => nil,
39
- "disable-backgrounding-occluded-windows" => nil,
40
- "disable-client-side-phishing-detection" => nil,
41
- "disable-default-apps" => nil,
42
- "disable-dev-shm-usage" => nil,
43
- "disable-ipc-flooding-protection" => nil,
44
- "disable-prompt-on-repost" => nil,
45
- "disable-renderer-backgrounding" => nil,
46
- "force-color-profile" => "srgb",
47
- "metrics-recording-only" => nil,
48
- "safebrowsing-disable-auto-update" => nil,
49
- "password-store" => "basic",
50
- # Note: --no-sandbox is not needed if you properly setup a user in the container.
51
- # https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
52
- # "no-sandbox" => nil,
53
- }.freeze
54
-
55
- NOT_FOUND = "Could not find an executable for chrome. Try to make it " \
56
- "available on the PATH or set environment varible for " \
57
- "example BROWSER_PATH=\"/Applications/Chromium.app/Contents/MacOS/Chromium\""
58
-
59
-
60
- attr_reader :host, :port, :ws_url, :pid, :path, :options, :cmd
19
+ PROCESS_TIMEOUT = ENV.fetch("FERRUM_PROCESS_TIMEOUT", 10).to_i
20
+
21
+ attr_reader :host, :port, :ws_url, :pid, :command,
22
+ :default_user_agent, :browser_version, :protocol_version,
23
+ :v8_version, :webkit_version, :xvfb
24
+
25
+
26
+ extend Forwardable
27
+ delegate path: :command
61
28
 
62
29
  def self.start(*args)
63
30
  new(*args).tap(&:start)
@@ -85,65 +52,26 @@ module Ferrum
85
52
  end
86
53
 
87
54
  def self.directory_remover(path)
88
- proc do
89
- begin
90
- FileUtils.remove_entry(path)
91
- rescue Errno::ENOENT
92
- end
93
- end
94
- end
95
-
96
- def self.detect_browser_path
97
- if RUBY_PLATFORM.include?("darwin")
98
- [
99
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
100
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
101
- ].find { |path| File.exist?(path) }
102
- else
103
- %w[chromium google-chrome-unstable google-chrome-beta google-chrome chrome chromium-browser google-chrome-stable].reduce(nil) do |path, exe|
104
- path = Cliver.detect(exe)
105
- break path if path
106
- end
107
- end
55
+ proc { FileUtils.remove_entry(path) rescue Errno::ENOENT }
108
56
  end
109
57
 
110
58
  def initialize(options)
111
- @options = {}
112
-
113
- @path = options[:browser_path] || BROWSER_PATH || self.class.detect_browser_path
114
-
115
59
  if options[:url]
116
60
  url = URI.join(options[:url].to_s, "/json/version")
117
61
  response = JSON.parse(::Net::HTTP.get(url))
118
62
  set_ws_url(response["webSocketDebuggerUrl"])
63
+ parse_browser_versions
119
64
  return
120
65
  end
121
66
 
122
- # Doesn't work on MacOS, so we need to set it by CDP as well
123
- @options.merge!("window-size" => options[:window_size].join(","))
124
-
125
- port = options.fetch(:port, BROWSER_PORT)
126
- @options.merge!("remote-debugging-port" => port)
127
-
128
- host = options.fetch(:host, BROWSER_HOST)
129
- @options.merge!("remote-debugging-address" => host)
130
-
131
- @temp_user_data_dir = Dir.mktmpdir
132
- ObjectSpace.define_finalizer(self, self.class.directory_remover(@temp_user_data_dir))
133
- @options.merge!("user-data-dir" => @temp_user_data_dir)
134
-
135
- @options = DEFAULT_OPTIONS.merge(@options)
136
-
137
- unless options.fetch(:headless, true)
138
- @options.delete("headless")
139
- @options.delete("disable-gpu")
140
- end
141
-
67
+ @pid = @xvfb = @user_data_dir = nil
68
+ @logger = options[:logger]
142
69
  @process_timeout = options.fetch(:process_timeout, PROCESS_TIMEOUT)
143
70
 
144
- @options.merge!(options.fetch(:browser_options, {}))
145
-
146
- @logger = options[:logger]
71
+ tmpdir = Dir.mktmpdir("ferrum_user_data_dir_")
72
+ ObjectSpace.define_finalizer(self, self.class.directory_remover(tmpdir))
73
+ @user_data_dir = tmpdir
74
+ @command = Command.build(options, tmpdir)
147
75
  end
148
76
 
149
77
  def start
@@ -156,21 +84,29 @@ module Ferrum
156
84
  process_options[:pgroup] = true unless Ferrum.windows?
157
85
  process_options[:out] = process_options[:err] = write_io
158
86
 
159
- raise Cliver::Dependency::NotFound.new(NOT_FOUND) unless @path
87
+ if @command.xvfb?
88
+ @xvfb = Xvfb.start(@command.options)
89
+ ObjectSpace.define_finalizer(self, self.class.process_killer(@xvfb.pid))
90
+ end
160
91
 
161
- @cmd = [@path] + @options.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
162
- @pid = ::Process.spawn(*@cmd, process_options)
92
+ @pid = ::Process.spawn(Hash(@xvfb&.to_env), *@command.to_a, process_options)
163
93
  ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
164
94
 
165
95
  parse_ws_url(read_io, @process_timeout)
96
+ parse_browser_versions
166
97
  ensure
167
98
  close_io(read_io, write_io)
168
99
  end
169
100
  end
170
101
 
171
102
  def stop
172
- kill if @pid
173
- remove_temp_user_data_dir if @temp_user_data_dir
103
+ if @pid
104
+ kill(@pid)
105
+ kill(@xvfb.pid) if @xvfb&.pid
106
+ @pid = nil
107
+ end
108
+
109
+ remove_user_data_dir if @user_data_dir
174
110
  ObjectSpace.undefine_finalizer(self)
175
111
  end
176
112
 
@@ -181,14 +117,13 @@ module Ferrum
181
117
 
182
118
  private
183
119
 
184
- def kill
185
- self.class.process_killer(@pid).call
186
- @pid = nil
120
+ def kill(pid)
121
+ self.class.process_killer(pid).call
187
122
  end
188
123
 
189
- def remove_temp_user_data_dir
190
- self.class.directory_remover(@temp_user_data_dir).call
191
- @temp_user_data_dir = nil
124
+ def remove_user_data_dir
125
+ self.class.directory_remover(@user_data_dir).call
126
+ @user_data_dir = nil
192
127
  end
193
128
 
194
129
  def parse_ws_url(read_io, timeout)
@@ -210,8 +145,8 @@ module Ferrum
210
145
  end
211
146
 
212
147
  unless ws_url
213
- @logger.puts output if @logger
214
- raise "Chrome process did not produce websocket url within #{timeout} seconds"
148
+ @logger.puts(output) if @logger
149
+ raise ProcessTimeoutError.new(timeout, output)
215
150
  end
216
151
  end
217
152
 
@@ -221,12 +156,25 @@ module Ferrum
221
156
  @port = @ws_url.port
222
157
  end
223
158
 
159
+ def parse_browser_versions
160
+ return unless ws_url.is_a?(Addressable::URI)
161
+
162
+ version_url = URI.parse(ws_url.merge(scheme: "http", path: "/json/version"))
163
+ response = JSON.parse(::Net::HTTP.get(version_url))
164
+
165
+ @v8_version = response["V8-Version"]
166
+ @browser_version = response["Browser"]
167
+ @webkit_version = response["WebKit-Version"]
168
+ @default_user_agent = response["User-Agent"]
169
+ @protocol_version = response["Protocol-Version"]
170
+ end
171
+
224
172
  def close_io(*ios)
225
173
  ios.each do |io|
226
174
  begin
227
175
  io.close unless io.closed?
228
176
  rescue IOError
229
- raise unless RUBY_ENGINE == 'jruby'
177
+ raise unless RUBY_ENGINE == "jruby"
230
178
  end
231
179
  end
232
180
  end