ferrum 0.6.2 → 0.10.1

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