ferrum 0.18.0 → 0.19.0
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/README.md +3 -1
- data/lib/ferrum/browser/binary.rb +1 -2
- data/lib/ferrum/browser/command.rb +0 -2
- data/lib/ferrum/browser/options/base.rb +5 -6
- data/lib/ferrum/browser/options/chrome.rb +0 -2
- data/lib/ferrum/browser/process/killer.rb +188 -0
- data/lib/ferrum/browser/process.rb +93 -88
- data/lib/ferrum/browser.rb +15 -7
- data/lib/ferrum/client/subscriber.rb +7 -2
- data/lib/ferrum/client/web_socket.rb +65 -3
- data/lib/ferrum/client.rb +28 -6
- data/lib/ferrum/context.rb +18 -5
- data/lib/ferrum/contexts.rb +75 -16
- data/lib/ferrum/downloads.rb +2 -1
- data/lib/ferrum/errors.rb +46 -2
- data/lib/ferrum/frame/dom.rb +48 -0
- data/lib/ferrum/frame/runtime.rb +9 -8
- data/lib/ferrum/interceptable.rb +25 -4
- data/lib/ferrum/page/screenshot.rb +3 -3
- data/lib/ferrum/page/stream.rb +16 -1
- data/lib/ferrum/page.rb +11 -5
- data/lib/ferrum/target.rb +7 -3
- data/lib/ferrum/utils/thread.rb +3 -1
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum.rb +1 -1
- metadata +3 -3
- data/lib/ferrum/browser/options/firefox.rb +0 -70
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a719b34dd1893e7c0ba2b71e121e1a2b7cc7950b96a1e462ac6a3a9f36c04e0
|
|
4
|
+
data.tar.gz: 4c96a549ff3aa00d76420b43c2f7b4870cacb712d390efe7394ad2a8245f6825
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a4e9981f2e6334b2862a073eb645b893bbebd42399f0e4b3090d98ea3d69ab0d3104bebe34f86e66b3ec2ea01b827efa28b75c2be55482de6f89a8b082b2113
|
|
7
|
+
data.tar.gz: acab89439a3655e51c3173bc30199bbccb6511567803fa16f066b26fbd6e5934f75492b6ed6383c8c59a27e397e23c781226abd439c551bc6be63c001868bc3a
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## [Documentation](https://docs.rubycdp.com/docs/ferrum/introduction)
|
|
4
4
|
|
|
5
5
|
<img align="right"
|
|
6
|
-
width="
|
|
6
|
+
width="160" height="160"
|
|
7
7
|
alt="Ferrum logo"
|
|
8
8
|
src="https://raw.githubusercontent.com/rubycdp/ferrum/main/logo.svg?sanitize=true">
|
|
9
9
|
|
|
@@ -18,6 +18,8 @@ browsers.
|
|
|
18
18
|
* [Cuprite](https://github.com/rubycdp/cuprite) is a pure Ruby driver for [Capybara](https://github.com/teamcapybara/capybara) based on Ferrum.
|
|
19
19
|
* [Vessel](https://github.com/rubycdp/vessel) high-level web crawling framework based on Ferrum and Mechanize.
|
|
20
20
|
|
|
21
|
+
<br clear="right">
|
|
22
|
+
|
|
21
23
|
## Install
|
|
22
24
|
|
|
23
25
|
There's no official Chrome or Chromium package for Linux don't install it this
|
|
@@ -68,7 +68,7 @@ module Ferrum
|
|
|
68
68
|
[paths, exts]
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
# rubocop:disable Style/CollectionCompact
|
|
71
|
+
# rubocop:disable-next Style/CollectionCompact
|
|
72
72
|
def lazy_find(cmds)
|
|
73
73
|
cmds.lazy.map do |cmd, path, ext|
|
|
74
74
|
absolute_path = File.absolute_path(cmd)
|
|
@@ -81,7 +81,6 @@ module Ferrum
|
|
|
81
81
|
cmd
|
|
82
82
|
end.reject(&:nil?) # .compact isn't defined on Enumerator::Lazy
|
|
83
83
|
end
|
|
84
|
-
# rubocop:enable Style/CollectionCompact
|
|
85
84
|
end
|
|
86
85
|
end
|
|
87
86
|
end
|
|
@@ -17,8 +17,6 @@ module Ferrum
|
|
|
17
17
|
# https://github.com/cyrus-and/chrome-remote-interface#implementations
|
|
18
18
|
def self.build(options, user_data_dir)
|
|
19
19
|
defaults = case options.browser_name
|
|
20
|
-
when :firefox
|
|
21
|
-
Options::Firefox.options
|
|
22
20
|
when :chrome, :opera, :edge, nil
|
|
23
21
|
Options::Chrome.options
|
|
24
22
|
else
|
|
@@ -8,10 +8,9 @@ module Ferrum
|
|
|
8
8
|
class Options
|
|
9
9
|
#
|
|
10
10
|
# Abstract base for browser-specific default option builders. Subclasses
|
|
11
|
-
# (e.g. {Options::Chrome}
|
|
12
|
-
# `
|
|
13
|
-
#
|
|
14
|
-
# by {Command}.
|
|
11
|
+
# (e.g. {Options::Chrome}) declare their own `DEFAULT_OPTIONS` and
|
|
12
|
+
# `PLATFORM_PATH` constants and implement {#merge_required}/{#merge_default}
|
|
13
|
+
# to produce the final CLI flags used by {Command}.
|
|
15
14
|
#
|
|
16
15
|
class Base
|
|
17
16
|
include Singleton
|
|
@@ -71,7 +70,7 @@ module Ferrum
|
|
|
71
70
|
#
|
|
72
71
|
# Merges the CLI flags that are always required to drive the browser
|
|
73
72
|
# over CDP (e.g. remote debugging port, user data dir) into `flags`.
|
|
74
|
-
# Abstract; overridden by {Options::Chrome}
|
|
73
|
+
# Abstract; overridden by {Options::Chrome}.
|
|
75
74
|
#
|
|
76
75
|
# @param [Hash{String => Object}] flags
|
|
77
76
|
# The flags accumulated so far.
|
|
@@ -92,7 +91,7 @@ module Ferrum
|
|
|
92
91
|
#
|
|
93
92
|
# Merges this browser's default CLI flags (see {#to_h}) into `flags`,
|
|
94
93
|
# unless `options.ignore_default_browser_options` is set. Abstract;
|
|
95
|
-
# overridden by {Options::Chrome}
|
|
94
|
+
# overridden by {Options::Chrome}.
|
|
96
95
|
#
|
|
97
96
|
# @param [Hash{String => Object}] flags
|
|
98
97
|
# The flags accumulated so far.
|
|
@@ -61,7 +61,6 @@ module Ferrum
|
|
|
61
61
|
"metrics-recording-only" => nil,
|
|
62
62
|
"mute-audio" => nil,
|
|
63
63
|
"no-crash-upload" => nil,
|
|
64
|
-
"no-crashpad" => nil,
|
|
65
64
|
"no-default-browser-check" => nil,
|
|
66
65
|
"no-first-run" => nil,
|
|
67
66
|
"no-startup-window" => nil,
|
|
@@ -104,7 +103,6 @@ module Ferrum
|
|
|
104
103
|
#
|
|
105
104
|
def merge_required(flags, options, user_data_dir)
|
|
106
105
|
flags = flags.merge("remote-debugging-port" => options.port,
|
|
107
|
-
"remote-debugging-address" => options.host,
|
|
108
106
|
"window-size" => options.window_size&.join(","),
|
|
109
107
|
"user-data-dir" => user_data_dir)
|
|
110
108
|
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "ferrum/utils/elapsed_time"
|
|
5
|
+
require "ferrum/utils/platform"
|
|
6
|
+
|
|
7
|
+
module Ferrum
|
|
8
|
+
class Browser
|
|
9
|
+
class Process
|
|
10
|
+
#
|
|
11
|
+
# OS-level process termination and user-data-directory removal, kept
|
|
12
|
+
# separate from the rest of {Process} (spawning, CDP handshake, ...).
|
|
13
|
+
# Both the GC finalizer and the explicit {Process#stop} path need to
|
|
14
|
+
# run the exact same kill/cleanup logic on plain pid/path values --
|
|
15
|
+
# never on `self`. A finalizer proc that closes over the object it's
|
|
16
|
+
# attached to creates a reference cycle GC can never collect, so the
|
|
17
|
+
# finalizer would never run; keeping this logic as free functions
|
|
18
|
+
# taking plain arguments sidesteps that entirely.
|
|
19
|
+
#
|
|
20
|
+
module Killer
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
KILL_TIMEOUT = 2
|
|
24
|
+
WAIT_KILLED = 0.05
|
|
25
|
+
REAP_TIMEOUT = 0.5
|
|
26
|
+
REMOVE_DIR_RETRIES = 5
|
|
27
|
+
REMOVE_DIR_RETRY_DELAY = 0.1
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# Kills the process at the given pid (and its process group, if
|
|
31
|
+
# any), escalating from TERM to KILL if it doesn't exit within
|
|
32
|
+
# {KILL_TIMEOUT} seconds.
|
|
33
|
+
#
|
|
34
|
+
# @param [Integer] pid
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
#
|
|
38
|
+
def kill(pid)
|
|
39
|
+
if Utils::Platform.windows?
|
|
40
|
+
# Process.kill is unreliable on Windows
|
|
41
|
+
::Process.kill("KILL", pid) unless system("taskkill /f /t /pid #{pid} >NUL 2>NUL")
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
send_signal(pid, "TERM")
|
|
46
|
+
start = Utils::ElapsedTime.monotonic_time
|
|
47
|
+
leader_exited = false
|
|
48
|
+
loop do
|
|
49
|
+
# The leader (the pid we actually spawned and can #wait on) may
|
|
50
|
+
# exit well before the rest of its process group does -- e.g. a
|
|
51
|
+
# renderer/zygote that ignores TERM. Reaping the leader must not
|
|
52
|
+
# by itself end the loop, or that child is orphaned forever
|
|
53
|
+
# since KILL is only ever sent from the timeout branch below.
|
|
54
|
+
leader_exited ||= !::Process.wait(pid, ::Process::WNOHANG).nil?
|
|
55
|
+
break if leader_exited && !process_group_alive?(pid)
|
|
56
|
+
|
|
57
|
+
sleep(WAIT_KILLED)
|
|
58
|
+
next unless Utils::ElapsedTime.timeout?(start, KILL_TIMEOUT)
|
|
59
|
+
|
|
60
|
+
send_signal(pid, "KILL")
|
|
61
|
+
wait_reaped(pid) unless leader_exited
|
|
62
|
+
break
|
|
63
|
+
end
|
|
64
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
65
|
+
# nop
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
#
|
|
69
|
+
# Waits for `pid` to become reapable, giving up after `timeout`.
|
|
70
|
+
#
|
|
71
|
+
# Deliberately a bounded poll rather than a blocking `Process.wait`.
|
|
72
|
+
# {#finalizer} installs {#kill} as an `ObjectSpace` finalizer, so
|
|
73
|
+
# it can run while the interpreter is shutting down, and a blocking
|
|
74
|
+
# wait there never returns.
|
|
75
|
+
#
|
|
76
|
+
# @param [Integer] pid
|
|
77
|
+
# @param [Float] timeout
|
|
78
|
+
#
|
|
79
|
+
# @return [Integer, nil]
|
|
80
|
+
# The reaped pid, or `nil` if it didn't exit in time or wasn't ours.
|
|
81
|
+
#
|
|
82
|
+
def wait_reaped(pid, timeout: REAP_TIMEOUT)
|
|
83
|
+
start = Utils::ElapsedTime.monotonic_time
|
|
84
|
+
loop do
|
|
85
|
+
reaped = ::Process.wait(pid, ::Process::WNOHANG)
|
|
86
|
+
return reaped if reaped
|
|
87
|
+
return nil if Utils::ElapsedTime.timeout?(start, timeout)
|
|
88
|
+
|
|
89
|
+
sleep(WAIT_KILLED)
|
|
90
|
+
end
|
|
91
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
# Builds the proc registered as a browser's `ObjectSpace` finalizer,
|
|
97
|
+
# cleaning up after a browser nobody quit.
|
|
98
|
+
#
|
|
99
|
+
# Kills every pid first and removes the directory afterwards. That
|
|
100
|
+
# order is the whole point of this being one finalizer instead of
|
|
101
|
+
# two: Ruby runs an object's finalizers in registration order, and
|
|
102
|
+
# removing the user data directory while the browser is still alive
|
|
103
|
+
# means Chrome recreates it on the way out, leaking it for good.
|
|
104
|
+
#
|
|
105
|
+
# @param [Array<Integer>, Integer, nil] pids
|
|
106
|
+
# Process ids to kill, e.g. the browser and Xvfb.
|
|
107
|
+
# @param [String, nil] path
|
|
108
|
+
# User data directory to remove once they are dead.
|
|
109
|
+
#
|
|
110
|
+
# @return [Proc]
|
|
111
|
+
#
|
|
112
|
+
def finalizer(pids, path)
|
|
113
|
+
pids = Array(pids).compact
|
|
114
|
+
proc do
|
|
115
|
+
pids.each { |pid| kill(pid) }
|
|
116
|
+
remove_directory(path) if path
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
# Signals the whole process group Chrome was spawned into (it's
|
|
122
|
+
# spawned with `pgroup: true`), so its child processes (renderer,
|
|
123
|
+
# GPU, zygote, ...) are cleaned up too instead of being left
|
|
124
|
+
# orphaned. Falls back to signaling just the pid directly if
|
|
125
|
+
# there's no such process group to signal (e.g. Xvfb, which isn't
|
|
126
|
+
# spawned with `pgroup: true`) or the group can't be signaled.
|
|
127
|
+
#
|
|
128
|
+
# @param [Integer] pid
|
|
129
|
+
# @param [String] name
|
|
130
|
+
#
|
|
131
|
+
# @return [void]
|
|
132
|
+
#
|
|
133
|
+
def send_signal(pid, name)
|
|
134
|
+
::Process.kill(name, -pid)
|
|
135
|
+
rescue Errno::EPERM, Errno::ESRCH
|
|
136
|
+
::Process.kill(name, pid)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
#
|
|
140
|
+
# Checks whether any process in pid's process group is still alive.
|
|
141
|
+
#
|
|
142
|
+
# @param [Integer] pid
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean]
|
|
145
|
+
#
|
|
146
|
+
def process_group_alive?(pid)
|
|
147
|
+
::Process.kill(0, -pid)
|
|
148
|
+
true
|
|
149
|
+
rescue Errno::ESRCH
|
|
150
|
+
false
|
|
151
|
+
rescue Errno::EPERM
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
#
|
|
156
|
+
# Removes the given directory, retrying with exponential backoff on
|
|
157
|
+
# transient errors. Chrome can briefly hold file locks right after
|
|
158
|
+
# being killed, so the directory may not be removable on the first
|
|
159
|
+
# try; retrying avoids leaking temp directories in that case.
|
|
160
|
+
#
|
|
161
|
+
# @param [String] path
|
|
162
|
+
# Directory to remove.
|
|
163
|
+
# @param [Integer] retries
|
|
164
|
+
# Maximum number of removal attempts.
|
|
165
|
+
# @param [Float] delay
|
|
166
|
+
# Base delay, in seconds, before the first retry; doubles on each
|
|
167
|
+
# subsequent attempt.
|
|
168
|
+
#
|
|
169
|
+
# @return [void]
|
|
170
|
+
#
|
|
171
|
+
def remove_directory(path, retries: REMOVE_DIR_RETRIES, delay: REMOVE_DIR_RETRY_DELAY)
|
|
172
|
+
retries.times do |attempt|
|
|
173
|
+
FileUtils.remove_entry(path)
|
|
174
|
+
break
|
|
175
|
+
rescue Errno::ENOENT
|
|
176
|
+
break
|
|
177
|
+
rescue Errno::ENOTEMPTY, Errno::EBUSY, Errno::EACCES, Errno::EPERM => e
|
|
178
|
+
raise e if attempt == retries - 1
|
|
179
|
+
|
|
180
|
+
sleep(delay * (2**attempt))
|
|
181
|
+
end
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
warn("[Ferrum] Failed to remove user data dir #{path}: #{e.class}: #{e.message}")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -7,10 +7,11 @@ require "tmpdir"
|
|
|
7
7
|
require "forwardable"
|
|
8
8
|
require "ferrum/browser/options/base"
|
|
9
9
|
require "ferrum/browser/options/chrome"
|
|
10
|
-
require "ferrum/browser/
|
|
10
|
+
require "ferrum/browser/process/killer"
|
|
11
11
|
require "ferrum/browser/command"
|
|
12
12
|
require "ferrum/utils/elapsed_time"
|
|
13
13
|
require "ferrum/utils/platform"
|
|
14
|
+
require "ferrum/utils/thread"
|
|
14
15
|
|
|
15
16
|
module Ferrum
|
|
16
17
|
class Browser
|
|
@@ -21,11 +22,12 @@ module Ferrum
|
|
|
21
22
|
# stopping/restarting the process and cleaning up its user data directory.
|
|
22
23
|
#
|
|
23
24
|
class Process
|
|
24
|
-
KILL_TIMEOUT = 2
|
|
25
|
-
WAIT_KILLED = 0.05
|
|
26
|
-
|
|
27
25
|
extend Forwardable
|
|
28
26
|
|
|
27
|
+
HTTP_SCHEMES = { "ws" => "http", "wss" => "https" }.freeze
|
|
28
|
+
WS_SCHEMES = { "http" => "ws", "https" => "wss" }.freeze
|
|
29
|
+
LOCAL_HOSTS = %w[0.0.0.0 :: 127.0.0.1 ::1 localhost].freeze
|
|
30
|
+
|
|
29
31
|
delegate path: :command
|
|
30
32
|
|
|
31
33
|
#
|
|
@@ -40,66 +42,23 @@ module Ferrum
|
|
|
40
42
|
new(*args).tap(&:start)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
#
|
|
44
|
-
# Builds a finalizer proc that kills the process with the given pid.
|
|
45
|
-
#
|
|
46
|
-
# @param [Integer] pid
|
|
47
|
-
# Process id to kill.
|
|
48
|
-
#
|
|
49
|
-
# @return [Proc]
|
|
50
|
-
#
|
|
51
|
-
def self.process_killer(pid)
|
|
52
|
-
proc do
|
|
53
|
-
if Utils::Platform.windows?
|
|
54
|
-
# Process.kill is unreliable on Windows
|
|
55
|
-
::Process.kill("KILL", pid) unless system("taskkill /f /t /pid #{pid} >NUL 2>NUL")
|
|
56
|
-
else
|
|
57
|
-
::Process.kill("USR1", pid)
|
|
58
|
-
start = Utils::ElapsedTime.monotonic_time
|
|
59
|
-
while ::Process.wait(pid, ::Process::WNOHANG).nil?
|
|
60
|
-
sleep(WAIT_KILLED)
|
|
61
|
-
next unless Utils::ElapsedTime.timeout?(start, KILL_TIMEOUT)
|
|
62
|
-
|
|
63
|
-
::Process.kill("KILL", pid)
|
|
64
|
-
::Process.wait(pid)
|
|
65
|
-
break
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
rescue Errno::ESRCH, Errno::ECHILD
|
|
69
|
-
# nop
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
#
|
|
74
|
-
# Builds a finalizer proc that removes the directory at the given path.
|
|
75
|
-
#
|
|
76
|
-
# @param [String] path
|
|
77
|
-
# Directory to remove.
|
|
78
|
-
#
|
|
79
|
-
# @return [Proc]
|
|
80
|
-
#
|
|
81
|
-
def self.directory_remover(path)
|
|
82
|
-
proc {
|
|
83
|
-
begin
|
|
84
|
-
FileUtils.remove_entry(path)
|
|
85
|
-
rescue StandardError
|
|
86
|
-
Errno::ENOENT
|
|
87
|
-
end
|
|
88
|
-
}
|
|
89
|
-
end
|
|
90
|
-
|
|
91
45
|
attr_reader :host, :port, :ws_url, :pid, :command,
|
|
92
46
|
:default_user_agent, :browser_version, :protocol_version,
|
|
93
47
|
:v8_version, :webkit_version, :xvfb
|
|
94
48
|
|
|
95
49
|
def initialize(options)
|
|
96
50
|
@pid = @xvfb = @user_data_dir = nil
|
|
51
|
+
@requested_host = options.host
|
|
97
52
|
|
|
98
53
|
if options.ws_url || options.url
|
|
99
54
|
# `:ws_url` option is higher priority than `:url`, parse versions
|
|
100
55
|
# and use it as a ws_url, otherwise use what has been parsed.
|
|
101
|
-
|
|
102
|
-
|
|
56
|
+
endpoint = options.ws_url || options.url
|
|
57
|
+
response = parse_json_version(endpoint)
|
|
58
|
+
ws_url = options.ws_url || reachable_ws_url(response&.[]("webSocketDebuggerUrl"), endpoint)
|
|
59
|
+
raise NoWebSocketUrlError, endpoint unless ws_url
|
|
60
|
+
|
|
61
|
+
self.ws_url = ws_url
|
|
103
62
|
return
|
|
104
63
|
end
|
|
105
64
|
|
|
@@ -107,10 +66,8 @@ module Ferrum
|
|
|
107
66
|
@process_timeout = options.process_timeout
|
|
108
67
|
@env = Hash(options.env)
|
|
109
68
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@user_data_dir = tmpdir
|
|
113
|
-
@command = Command.build(options, tmpdir)
|
|
69
|
+
@user_data_dir = Dir.mktmpdir("ferrum_user_data_dir_")
|
|
70
|
+
@command = Command.build(options, @user_data_dir)
|
|
114
71
|
end
|
|
115
72
|
|
|
116
73
|
#
|
|
@@ -128,14 +85,11 @@ module Ferrum
|
|
|
128
85
|
process_options[:pgroup] = true unless Utils::Platform.windows?
|
|
129
86
|
process_options[:out] = process_options[:err] = write_io
|
|
130
87
|
|
|
131
|
-
if @command.xvfb?
|
|
132
|
-
@xvfb = Xvfb.start(@command.options)
|
|
133
|
-
ObjectSpace.define_finalizer(self, self.class.process_killer(@xvfb.pid))
|
|
134
|
-
end
|
|
88
|
+
@xvfb = Xvfb.start(@command.options) if @command.xvfb?
|
|
135
89
|
|
|
136
90
|
env = Hash(@xvfb&.to_env).merge(@env)
|
|
137
91
|
@pid = ::Process.spawn(env, *@command.to_a, process_options)
|
|
138
|
-
ObjectSpace.define_finalizer(self,
|
|
92
|
+
ObjectSpace.define_finalizer(self, Killer.finalizer([@pid, @xvfb&.pid], @user_data_dir))
|
|
139
93
|
|
|
140
94
|
parse_ws_url(read_io, @process_timeout)
|
|
141
95
|
parse_json_version(ws_url)
|
|
@@ -148,17 +102,26 @@ module Ferrum
|
|
|
148
102
|
# Kills the browser process (and Xvfb, if running) and removes the user
|
|
149
103
|
# data directory.
|
|
150
104
|
#
|
|
151
|
-
# @
|
|
105
|
+
# @param [Boolean] wait
|
|
106
|
+
# Whether to block until the process is confirmed dead and its user
|
|
107
|
+
# data directory removed (the default), or return immediately and
|
|
108
|
+
# run that cleanup on a background thread instead. Killing a
|
|
109
|
+
# stubborn process group can block for up to {Killer::KILL_TIMEOUT}
|
|
110
|
+
# seconds, plus retries removing its directory, which matters when quitting
|
|
111
|
+
# many browsers in a hot path. `wait: false` is not used by
|
|
112
|
+
# {#restart}, which always waits so the old process is fully gone
|
|
113
|
+
# before the new one starts.
|
|
152
114
|
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
115
|
+
# @return [Thread, nil]
|
|
116
|
+
# The background cleanup thread when `wait: false`; the caller can
|
|
117
|
+
# `#join` it if they need cleanup to have finished, e.g. before
|
|
118
|
+
# process exit or before reusing a fixed port. `nil` when `wait:
|
|
119
|
+
# true`.
|
|
120
|
+
#
|
|
121
|
+
def stop(wait: true)
|
|
122
|
+
return sync_stop if wait
|
|
159
123
|
|
|
160
|
-
|
|
161
|
-
ObjectSpace.undefine_finalizer(self)
|
|
124
|
+
async_stop
|
|
162
125
|
end
|
|
163
126
|
|
|
164
127
|
#
|
|
@@ -189,12 +152,40 @@ module Ferrum
|
|
|
189
152
|
|
|
190
153
|
private
|
|
191
154
|
|
|
192
|
-
def
|
|
193
|
-
|
|
155
|
+
def sync_stop
|
|
156
|
+
if @pid
|
|
157
|
+
Killer.kill(@pid)
|
|
158
|
+
Killer.kill(@xvfb.pid) if @xvfb&.pid
|
|
159
|
+
@pid = nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
remove_user_data_dir if @user_data_dir
|
|
163
|
+
ObjectSpace.undefine_finalizer(self)
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
#
|
|
168
|
+
# Snapshots what needs killing/removing, clears instance state so the
|
|
169
|
+
# object looks stopped right away, and does the actual work on a
|
|
170
|
+
# background thread. The finalizer is left in place as a backup until
|
|
171
|
+
# that thread finishes, in case the process exits before it does.
|
|
172
|
+
#
|
|
173
|
+
def async_stop
|
|
174
|
+
pid = @pid
|
|
175
|
+
xvfb_pid = @xvfb&.pid
|
|
176
|
+
user_data_dir = @user_data_dir
|
|
177
|
+
@pid = @user_data_dir = nil
|
|
178
|
+
|
|
179
|
+
Utils::Thread.spawn do
|
|
180
|
+
Killer.kill(pid) if pid
|
|
181
|
+
Killer.kill(xvfb_pid) if xvfb_pid
|
|
182
|
+
Killer.remove_directory(user_data_dir) if user_data_dir
|
|
183
|
+
ObjectSpace.undefine_finalizer(self)
|
|
184
|
+
end
|
|
194
185
|
end
|
|
195
186
|
|
|
196
187
|
def remove_user_data_dir
|
|
197
|
-
|
|
188
|
+
Killer.remove_directory(@user_data_dir)
|
|
198
189
|
@user_data_dir = nil
|
|
199
190
|
end
|
|
200
191
|
|
|
@@ -210,7 +201,7 @@ module Ferrum
|
|
|
210
201
|
read_io.wait_readable(max_time - now)
|
|
211
202
|
else
|
|
212
203
|
if output.match(regexp)
|
|
213
|
-
self.ws_url = output.match(regexp)[1].strip
|
|
204
|
+
self.ws_url = rewrite_host(output.match(regexp)[1].strip)
|
|
214
205
|
break
|
|
215
206
|
end
|
|
216
207
|
end
|
|
@@ -228,6 +219,12 @@ module Ferrum
|
|
|
228
219
|
@port = @ws_url.port
|
|
229
220
|
end
|
|
230
221
|
|
|
222
|
+
def rewrite_host(url)
|
|
223
|
+
uri = Addressable::URI.parse(url)
|
|
224
|
+
uri.host = @requested_host
|
|
225
|
+
uri.to_s
|
|
226
|
+
end
|
|
227
|
+
|
|
231
228
|
def close_io(*ios)
|
|
232
229
|
ios.each do |io|
|
|
233
230
|
io.close if io && !io.closed?
|
|
@@ -237,18 +234,7 @@ module Ferrum
|
|
|
237
234
|
end
|
|
238
235
|
|
|
239
236
|
def parse_json_version(url)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if %w[wss ws].include?(url.scheme)
|
|
243
|
-
url.scheme = case url.scheme
|
|
244
|
-
when "ws"
|
|
245
|
-
"http"
|
|
246
|
-
when "wss"
|
|
247
|
-
"https"
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
response = JSON.parse(::Net::HTTP.get(URI(url.to_s)))
|
|
237
|
+
response = JSON.parse(::Net::HTTP.get(URI(json_version_url(url).to_s)))
|
|
252
238
|
|
|
253
239
|
@v8_version = response["V8-Version"]
|
|
254
240
|
@browser_version = response["Browser"]
|
|
@@ -260,6 +246,25 @@ module Ferrum
|
|
|
260
246
|
rescue JSON::ParserError
|
|
261
247
|
# nop
|
|
262
248
|
end
|
|
249
|
+
|
|
250
|
+
def json_version_url(url)
|
|
251
|
+
url = Addressable::URI.parse(url).dup
|
|
252
|
+
url.scheme = HTTP_SCHEMES.fetch(url.scheme, url.scheme)
|
|
253
|
+
url.path = "/json/version"
|
|
254
|
+
url
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def reachable_ws_url(ws_url, endpoint)
|
|
258
|
+
return unless ws_url
|
|
259
|
+
|
|
260
|
+
ws_url = Addressable::URI.parse(ws_url)
|
|
261
|
+
return ws_url.to_s unless LOCAL_HOSTS.include?(ws_url.host)
|
|
262
|
+
|
|
263
|
+
url = Addressable::URI.parse(endpoint).dup
|
|
264
|
+
url.scheme = WS_SCHEMES.fetch(url.scheme, url.scheme)
|
|
265
|
+
url.path = ws_url.path
|
|
266
|
+
url.to_s
|
|
267
|
+
end
|
|
263
268
|
end
|
|
264
269
|
end
|
|
265
270
|
end
|
data/lib/ferrum/browser.rb
CHANGED
|
@@ -29,7 +29,7 @@ module Ferrum
|
|
|
29
29
|
delegate %i[default_context] => :contexts
|
|
30
30
|
delegate %i[targets create_target page pages windows workers service_workers attach_target] => :default_context
|
|
31
31
|
delegate %i[go_to goto go back forward refresh reload stop wait_for_reload
|
|
32
|
-
at_css at_xpath css xpath current_url current_title url title
|
|
32
|
+
at_css at_xpath css xpath wait_for_selector current_url current_title url title
|
|
33
33
|
body doctype content=
|
|
34
34
|
headers cookies network accessibility downloads
|
|
35
35
|
mouse keyboard
|
|
@@ -103,9 +103,8 @@ module Ferrum
|
|
|
103
103
|
# why you have slow responses and fix or block them rather than turn this
|
|
104
104
|
# setting off.
|
|
105
105
|
#
|
|
106
|
-
# @option options [:chrome, :
|
|
107
|
-
# Sets the browser's name.
|
|
108
|
-
# `:firefox` for now.
|
|
106
|
+
# @option options [:chrome, :opera, :edge] :browser_name (:chrome)
|
|
107
|
+
# Sets the browser's name.
|
|
109
108
|
#
|
|
110
109
|
# @option options [String] :browser_path
|
|
111
110
|
# Path to Chrome binary, you can also set ENV variable as
|
|
@@ -238,14 +237,23 @@ module Ferrum
|
|
|
238
237
|
#
|
|
239
238
|
# Terminates the browser process and closes the client connection.
|
|
240
239
|
#
|
|
241
|
-
|
|
240
|
+
# @param [Boolean] wait
|
|
241
|
+
# Whether to block until the process is confirmed dead and its user
|
|
242
|
+
# data directory removed (the default), or return immediately and run
|
|
243
|
+
# that cleanup on a background thread instead. See {Process#stop}.
|
|
244
|
+
#
|
|
245
|
+
# @return [Thread, nil]
|
|
246
|
+
# The background cleanup thread when `wait: false`, `nil` otherwise.
|
|
247
|
+
#
|
|
248
|
+
def quit(wait: true)
|
|
242
249
|
return unless @client
|
|
243
250
|
|
|
244
|
-
contexts
|
|
251
|
+
contexts&.close_connections
|
|
245
252
|
|
|
246
253
|
@client.close
|
|
247
|
-
@process.stop
|
|
254
|
+
thread = @process.stop(wait: wait)
|
|
248
255
|
@client = @process = @contexts = nil
|
|
256
|
+
thread
|
|
249
257
|
end
|
|
250
258
|
|
|
251
259
|
#
|
|
@@ -106,7 +106,7 @@ module Ferrum
|
|
|
106
106
|
private
|
|
107
107
|
|
|
108
108
|
def start
|
|
109
|
-
@regular_thread = Utils::Thread.spawn
|
|
109
|
+
@regular_thread = Utils::Thread.spawn do
|
|
110
110
|
loop do
|
|
111
111
|
message = @regular.pop
|
|
112
112
|
break unless message
|
|
@@ -115,7 +115,7 @@ module Ferrum
|
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
@priority_thread = Utils::Thread.spawn
|
|
118
|
+
@priority_thread = Utils::Thread.spawn do
|
|
119
119
|
loop do
|
|
120
120
|
message = @priority.pop
|
|
121
121
|
break unless message
|
|
@@ -133,6 +133,11 @@ module Ferrum
|
|
|
133
133
|
@on[event]&.each_with_index do |block, index|
|
|
134
134
|
# In case of multiple callbacks we provide current index and total
|
|
135
135
|
block.call(params, index, total)
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
# A raising callback used to terminate the dispatch thread, and with it every
|
|
138
|
+
# event for the rest of the browser's life: targets stopped being registered
|
|
139
|
+
# and any page created later raised `NoSuchTargetError`. Report and carry on.
|
|
140
|
+
warn("Ferrum: #{event} callback raised #{e.class}: #{e.message}\n #{e.backtrace&.first}")
|
|
136
141
|
end
|
|
137
142
|
end
|
|
138
143
|
end
|