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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +370 -78
- data/lib/ferrum.rb +38 -4
- data/lib/ferrum/browser.rb +19 -12
- data/lib/ferrum/browser/client.rb +23 -10
- data/lib/ferrum/browser/command.rb +57 -0
- data/lib/ferrum/browser/options/base.rb +46 -0
- data/lib/ferrum/browser/options/chrome.rb +73 -0
- data/lib/ferrum/browser/options/firefox.rb +34 -0
- data/lib/ferrum/browser/process.rb +56 -108
- data/lib/ferrum/browser/subscriber.rb +9 -1
- data/lib/ferrum/browser/web_socket.rb +23 -4
- data/lib/ferrum/browser/xvfb.rb +37 -0
- data/lib/ferrum/context.rb +3 -3
- data/lib/ferrum/cookies.rb +7 -0
- data/lib/ferrum/dialog.rb +2 -2
- data/lib/ferrum/frame.rb +20 -5
- data/lib/ferrum/frame/dom.rb +34 -37
- data/lib/ferrum/frame/runtime.rb +90 -84
- data/lib/ferrum/headers.rb +1 -1
- data/lib/ferrum/keyboard.rb +3 -3
- data/lib/ferrum/mouse.rb +14 -3
- data/lib/ferrum/network.rb +81 -20
- data/lib/ferrum/network/error.rb +8 -15
- data/lib/ferrum/network/exchange.rb +24 -21
- data/lib/ferrum/network/intercepted_request.rb +12 -3
- data/lib/ferrum/network/response.rb +4 -0
- data/lib/ferrum/node.rb +70 -26
- data/lib/ferrum/page.rb +66 -26
- data/lib/ferrum/page/frames.rb +12 -15
- data/lib/ferrum/page/screenshot.rb +64 -12
- data/lib/ferrum/rbga.rb +38 -0
- data/lib/ferrum/version.rb +1 -1
- metadata +13 -7
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
|
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
|
|
data/lib/ferrum/browser.rb
CHANGED
@@ -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[
|
20
|
-
at_css at_xpath css xpath current_url
|
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
|
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
|
-
@
|
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
|
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
|
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
|
-
|
11
|
-
|
12
|
-
|
10
|
+
INTERRUPTIONS = %w[Fetch.requestPaused Fetch.authRequired].freeze
|
11
|
+
|
12
|
+
def initialize(browser, ws_url, id_starts_with: 0)
|
13
13
|
@browser = browser
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
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
|
-
|
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
|
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
|
-
|
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",
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
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
|
-
|
123
|
-
@
|
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
|
-
|
145
|
-
|
146
|
-
@
|
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
|
-
|
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
|
-
@
|
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
|
-
|
173
|
-
|
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(
|
186
|
-
@pid = nil
|
120
|
+
def kill(pid)
|
121
|
+
self.class.process_killer(pid).call
|
187
122
|
end
|
188
123
|
|
189
|
-
def
|
190
|
-
self.class.directory_remover(@
|
191
|
-
@
|
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
|
214
|
-
raise
|
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 ==
|
177
|
+
raise unless RUBY_ENGINE == "jruby"
|
230
178
|
end
|
231
179
|
end
|
232
180
|
end
|