ferrum 0.14 → 0.16
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 +309 -158
- data/lib/ferrum/browser/command.rb +4 -0
- data/lib/ferrum/browser/options/chrome.rb +8 -3
- data/lib/ferrum/browser/options.rb +38 -25
- data/lib/ferrum/browser/process.rb +42 -17
- data/lib/ferrum/browser.rb +38 -50
- data/lib/ferrum/client/subscriber.rb +76 -0
- data/lib/ferrum/client/web_socket.rb +126 -0
- data/lib/ferrum/client.rb +171 -0
- data/lib/ferrum/context.rb +19 -15
- data/lib/ferrum/contexts.rb +46 -12
- data/lib/ferrum/cookies.rb +28 -1
- data/lib/ferrum/downloads.rb +60 -0
- data/lib/ferrum/errors.rb +10 -3
- data/lib/ferrum/headers.rb +1 -1
- data/lib/ferrum/network/exchange.rb +10 -1
- data/lib/ferrum/network/intercepted_request.rb +5 -5
- data/lib/ferrum/network/request.rb +9 -0
- data/lib/ferrum/network.rb +36 -24
- data/lib/ferrum/node.rb +11 -0
- data/lib/ferrum/page/frames.rb +7 -9
- data/lib/ferrum/page/screenshot.rb +54 -28
- data/lib/ferrum/page.rb +192 -118
- data/lib/ferrum/proxy.rb +1 -1
- data/lib/ferrum/target.rb +25 -5
- data/lib/ferrum/utils/elapsed_time.rb +0 -2
- data/lib/ferrum/utils/event.rb +19 -0
- data/lib/ferrum/utils/platform.rb +4 -0
- data/lib/ferrum/utils/thread.rb +18 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum.rb +3 -0
- metadata +28 -17
- data/lib/ferrum/browser/client.rb +0 -103
- data/lib/ferrum/browser/subscriber.rb +0 -36
- data/lib/ferrum/browser/web_socket.rb +0 -91
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ferrum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.16'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Vorotilin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-12-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: base64
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.2'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: concurrent-ruby
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -56,22 +70,16 @@ dependencies:
|
|
56
70
|
name: websocket-driver
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
|
-
- - "
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0.6'
|
62
|
-
- - "<"
|
73
|
+
- - "~>"
|
63
74
|
- !ruby/object:Gem::Version
|
64
|
-
version: '0.
|
75
|
+
version: '0.7'
|
65
76
|
type: :runtime
|
66
77
|
prerelease: false
|
67
78
|
version_requirements: !ruby/object:Gem::Requirement
|
68
79
|
requirements:
|
69
|
-
- - "
|
70
|
-
- !ruby/object:Gem::Version
|
71
|
-
version: '0.6'
|
72
|
-
- - "<"
|
80
|
+
- - "~>"
|
73
81
|
- !ruby/object:Gem::Version
|
74
|
-
version: '0.
|
82
|
+
version: '0.7'
|
75
83
|
description: Ferrum allows you to control headless Chrome browser
|
76
84
|
email:
|
77
85
|
- d.vorotilin@gmail.com
|
@@ -84,22 +92,23 @@ files:
|
|
84
92
|
- lib/ferrum.rb
|
85
93
|
- lib/ferrum/browser.rb
|
86
94
|
- lib/ferrum/browser/binary.rb
|
87
|
-
- lib/ferrum/browser/client.rb
|
88
95
|
- lib/ferrum/browser/command.rb
|
89
96
|
- lib/ferrum/browser/options.rb
|
90
97
|
- lib/ferrum/browser/options/base.rb
|
91
98
|
- lib/ferrum/browser/options/chrome.rb
|
92
99
|
- lib/ferrum/browser/options/firefox.rb
|
93
100
|
- lib/ferrum/browser/process.rb
|
94
|
-
- lib/ferrum/browser/subscriber.rb
|
95
101
|
- lib/ferrum/browser/version_info.rb
|
96
|
-
- lib/ferrum/browser/web_socket.rb
|
97
102
|
- lib/ferrum/browser/xvfb.rb
|
103
|
+
- lib/ferrum/client.rb
|
104
|
+
- lib/ferrum/client/subscriber.rb
|
105
|
+
- lib/ferrum/client/web_socket.rb
|
98
106
|
- lib/ferrum/context.rb
|
99
107
|
- lib/ferrum/contexts.rb
|
100
108
|
- lib/ferrum/cookies.rb
|
101
109
|
- lib/ferrum/cookies/cookie.rb
|
102
110
|
- lib/ferrum/dialog.rb
|
111
|
+
- lib/ferrum/downloads.rb
|
103
112
|
- lib/ferrum/errors.rb
|
104
113
|
- lib/ferrum/frame.rb
|
105
114
|
- lib/ferrum/frame/dom.rb
|
@@ -128,7 +137,9 @@ files:
|
|
128
137
|
- lib/ferrum/target.rb
|
129
138
|
- lib/ferrum/utils/attempt.rb
|
130
139
|
- lib/ferrum/utils/elapsed_time.rb
|
140
|
+
- lib/ferrum/utils/event.rb
|
131
141
|
- lib/ferrum/utils/platform.rb
|
142
|
+
- lib/ferrum/utils/thread.rb
|
132
143
|
- lib/ferrum/version.rb
|
133
144
|
homepage: https://github.com/rubycdp/ferrum
|
134
145
|
licenses:
|
@@ -148,14 +159,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
148
159
|
requirements:
|
149
160
|
- - ">="
|
150
161
|
- !ruby/object:Gem::Version
|
151
|
-
version: 2.
|
162
|
+
version: 2.7.0
|
152
163
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
164
|
requirements:
|
154
165
|
- - ">="
|
155
166
|
- !ruby/object:Gem::Version
|
156
167
|
version: '0'
|
157
168
|
requirements: []
|
158
|
-
rubygems_version: 3.
|
169
|
+
rubygems_version: 3.5.22
|
159
170
|
signing_key:
|
160
171
|
specification_version: 4
|
161
172
|
summary: Ruby headless Chrome driver
|
@@ -1,103 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "ferrum/browser/subscriber"
|
4
|
-
require "ferrum/browser/web_socket"
|
5
|
-
|
6
|
-
module Ferrum
|
7
|
-
class Browser
|
8
|
-
class Client
|
9
|
-
INTERRUPTIONS = %w[Fetch.requestPaused Fetch.authRequired].freeze
|
10
|
-
|
11
|
-
def initialize(ws_url, connectable, logger: nil, ws_max_receive_size: nil, id_starts_with: 0)
|
12
|
-
@connectable = connectable
|
13
|
-
@command_id = id_starts_with
|
14
|
-
@pendings = Concurrent::Hash.new
|
15
|
-
@ws = WebSocket.new(ws_url, ws_max_receive_size, logger)
|
16
|
-
@subscriber, @interrupter = Subscriber.build(2)
|
17
|
-
|
18
|
-
@thread = Thread.new do
|
19
|
-
Thread.current.abort_on_exception = true
|
20
|
-
Thread.current.report_on_exception = true if Thread.current.respond_to?(:report_on_exception=)
|
21
|
-
|
22
|
-
loop do
|
23
|
-
message = @ws.messages.pop
|
24
|
-
break unless message
|
25
|
-
|
26
|
-
if INTERRUPTIONS.include?(message["method"])
|
27
|
-
@interrupter.async.call(message)
|
28
|
-
elsif message.key?("method")
|
29
|
-
@subscriber.async.call(message)
|
30
|
-
else
|
31
|
-
@pendings[message["id"]]&.set(message)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def command(method, params = {})
|
38
|
-
pending = Concurrent::IVar.new
|
39
|
-
message = build_message(method, params)
|
40
|
-
@pendings[message[:id]] = pending
|
41
|
-
@ws.send_message(message)
|
42
|
-
data = pending.value!(@connectable.timeout)
|
43
|
-
@pendings.delete(message[:id])
|
44
|
-
|
45
|
-
raise DeadBrowserError if data.nil? && @ws.messages.closed?
|
46
|
-
raise TimeoutError unless data
|
47
|
-
|
48
|
-
error, response = data.values_at("error", "result")
|
49
|
-
raise_browser_error(error) if error
|
50
|
-
response
|
51
|
-
end
|
52
|
-
|
53
|
-
def on(event, &block)
|
54
|
-
case event
|
55
|
-
when *INTERRUPTIONS
|
56
|
-
@interrupter.on(event, &block)
|
57
|
-
else
|
58
|
-
@subscriber.on(event, &block)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def subscribed?(event)
|
63
|
-
[@interrupter, @subscriber].any? { |s| s.subscribed?(event) }
|
64
|
-
end
|
65
|
-
|
66
|
-
def close
|
67
|
-
@ws.close
|
68
|
-
# Give a thread some time to handle a tail of messages
|
69
|
-
@pendings.clear
|
70
|
-
@thread.kill unless @thread.join(1)
|
71
|
-
end
|
72
|
-
|
73
|
-
private
|
74
|
-
|
75
|
-
def build_message(method, params)
|
76
|
-
{ method: method, params: params }.merge(id: next_command_id)
|
77
|
-
end
|
78
|
-
|
79
|
-
def next_command_id
|
80
|
-
@command_id += 1
|
81
|
-
end
|
82
|
-
|
83
|
-
def raise_browser_error(error)
|
84
|
-
case error["message"]
|
85
|
-
# Node has disappeared while we were trying to get it
|
86
|
-
when "No node with given id found",
|
87
|
-
"Could not find node with given id",
|
88
|
-
"Inspected target navigated or closed"
|
89
|
-
raise NodeNotFoundError, error
|
90
|
-
# Context is lost, page is reloading
|
91
|
-
when "Cannot find context with specified id"
|
92
|
-
raise NoExecutionContextError, error
|
93
|
-
when "No target with given id found"
|
94
|
-
raise NoSuchPageError
|
95
|
-
when /Could not compute content quads/
|
96
|
-
raise CoordinatesNotFoundError
|
97
|
-
else
|
98
|
-
raise BrowserError, error
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Browser
|
5
|
-
class Subscriber
|
6
|
-
include Concurrent::Async
|
7
|
-
|
8
|
-
def self.build(size)
|
9
|
-
(0..size).map { new }
|
10
|
-
end
|
11
|
-
|
12
|
-
def initialize
|
13
|
-
super
|
14
|
-
@on = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
|
15
|
-
end
|
16
|
-
|
17
|
-
def on(event, &block)
|
18
|
-
@on[event] << block
|
19
|
-
true
|
20
|
-
end
|
21
|
-
|
22
|
-
def subscribed?(event)
|
23
|
-
@on.key?(event)
|
24
|
-
end
|
25
|
-
|
26
|
-
def call(message)
|
27
|
-
method, params = message.values_at("method", "params")
|
28
|
-
total = @on[method].size
|
29
|
-
@on[method].each_with_index do |block, index|
|
30
|
-
# In case of multiple callbacks we provide current index and total
|
31
|
-
block.call(params, index, total)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,91 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
require "socket"
|
5
|
-
require "websocket/driver"
|
6
|
-
|
7
|
-
module Ferrum
|
8
|
-
class Browser
|
9
|
-
class WebSocket
|
10
|
-
WEBSOCKET_BUG_SLEEP = 0.05
|
11
|
-
SKIP_LOGGING_SCREENSHOTS = !ENV["FERRUM_LOGGING_SCREENSHOTS"]
|
12
|
-
|
13
|
-
attr_reader :url, :messages
|
14
|
-
|
15
|
-
def initialize(url, max_receive_size, logger)
|
16
|
-
@url = url
|
17
|
-
@logger = logger
|
18
|
-
uri = URI.parse(@url)
|
19
|
-
@sock = TCPSocket.new(uri.host, uri.port)
|
20
|
-
max_receive_size ||= ::WebSocket::Driver::MAX_LENGTH
|
21
|
-
@driver = ::WebSocket::Driver.client(self, max_length: max_receive_size)
|
22
|
-
@messages = Queue.new
|
23
|
-
|
24
|
-
@screenshot_commands = Concurrent::Hash.new if SKIP_LOGGING_SCREENSHOTS
|
25
|
-
|
26
|
-
@driver.on(:open, &method(:on_open))
|
27
|
-
@driver.on(:message, &method(:on_message))
|
28
|
-
@driver.on(:close, &method(:on_close))
|
29
|
-
|
30
|
-
@thread = Thread.new do
|
31
|
-
Thread.current.abort_on_exception = true
|
32
|
-
Thread.current.report_on_exception = true if Thread.current.respond_to?(:report_on_exception=)
|
33
|
-
|
34
|
-
begin
|
35
|
-
loop do
|
36
|
-
data = @sock.readpartial(512)
|
37
|
-
break unless data
|
38
|
-
|
39
|
-
@driver.parse(data)
|
40
|
-
end
|
41
|
-
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
|
42
|
-
@messages.close
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
@driver.start
|
47
|
-
end
|
48
|
-
|
49
|
-
def on_open(_event)
|
50
|
-
# https://github.com/faye/websocket-driver-ruby/issues/46
|
51
|
-
sleep(WEBSOCKET_BUG_SLEEP)
|
52
|
-
end
|
53
|
-
|
54
|
-
def on_message(event)
|
55
|
-
data = JSON.parse(event.data)
|
56
|
-
@messages.push(data)
|
57
|
-
|
58
|
-
output = event.data
|
59
|
-
if SKIP_LOGGING_SCREENSHOTS && @screenshot_commands[data["id"]]
|
60
|
-
@screenshot_commands.delete(data["id"])
|
61
|
-
output.sub!(/{"data":"(.*)"}/, %("Set FERRUM_LOGGING_SCREENSHOTS=true to see screenshots in Base64"))
|
62
|
-
end
|
63
|
-
|
64
|
-
@logger&.puts(" ◀ #{Utils::ElapsedTime.elapsed_time} #{output}\n")
|
65
|
-
end
|
66
|
-
|
67
|
-
def on_close(_event)
|
68
|
-
@messages.close
|
69
|
-
@thread.kill
|
70
|
-
end
|
71
|
-
|
72
|
-
def send_message(data)
|
73
|
-
@screenshot_commands[data[:id]] = true if SKIP_LOGGING_SCREENSHOTS
|
74
|
-
|
75
|
-
json = data.to_json
|
76
|
-
@driver.text(json)
|
77
|
-
@logger&.puts("\n\n▶ #{Utils::ElapsedTime.elapsed_time} #{json}")
|
78
|
-
end
|
79
|
-
|
80
|
-
def write(data)
|
81
|
-
@sock.write(data)
|
82
|
-
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
|
83
|
-
@messages.close
|
84
|
-
end
|
85
|
-
|
86
|
-
def close
|
87
|
-
@driver.close
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|