Skip to content

Commit

Permalink
* Refactor Ferrum module
Browse files Browse the repository at this point in the history
* Split methods into Utils
  • Loading branch information
route committed Nov 15, 2021
1 parent c0d52c0 commit 2ec7533
Show file tree
Hide file tree
Showing 18 changed files with 224 additions and 181 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ a block with this page, after which the page is closed.
- `Ferrum::Page#bypass_csp` accepts hash as argument `enabled: true` by default
- `Ferrum::Context#has_target?` -> `Ferrum::Context#target?`
- We now start looking for Chrome first instead of Chromium, the order for checking binaries has changed
- Multiple methods are moved into `Utils`:
- Ferrum.with_attempts -> Ferrum::Utils::Attempt.with_retry
- Ferrum.started -> Ferrum::Utils::ElapsedTime.start
- Ferrum.elapsed_time -> Ferrum::Utils::ElapsedTime.elapsed_time
- Ferrum.monotonic_time -> Ferrum::Utils::ElapsedTime.monotonic_time
- Ferrum.timeout? -> Ferrum::Utils::ElapsedTime.timeout?
- Ferrum.windows? -> Ferrum::Utils::Platform.windows?
- Ferrum.mac? -> Ferrum::Utils::Platform.mac?
- Ferrum.mri? -> Ferrum::Utils::Platform.mri?

## [0.11](https://github.com/rubycdp/ferrum/compare/v0.10.2...v0.11) - (Mar 11, 2021) ##

Expand Down
158 changes: 4 additions & 154 deletions lib/ferrum.rb
Original file line number Diff line number Diff line change
@@ -1,161 +1,11 @@
# frozen_string_literal: true

require "concurrent-ruby"
require "ferrum/utils/platform"
require "ferrum/utils/elapsed_time"
require "ferrum/utils/attempt"
require "ferrum/errors"
require "ferrum/browser"
require "ferrum/node"

module Ferrum
class Error < StandardError; end

class NoSuchPageError < Error; end

class NoSuchTargetError < Error; end

class NotImplementedError < Error; end

class StatusError < Error
def initialize(url, message = nil)
super(message || "Request to #{url} failed to reach server, check DNS and server status")
end
end

class PendingConnectionsError < StatusError
attr_reader :pendings

def initialize(url, pendings = [])
@pendings = pendings

message = "Request to #{url} reached server, but there are still pending connections: #{pendings.join(', ')}"

super(url, message)
end
end

class TimeoutError < Error
def message
"Timed out waiting for response. It's possible that this happened " \
"because something took a very long time (for example a page load " \
"was slow). If so, setting the :timeout option to a higher value might " \
"help."
end
end

class ScriptTimeoutError < Error
def message
"Timed out waiting for evaluated script to return a value"
end
end

class ProcessTimeoutError < Error
attr_reader :output

def initialize(timeout, output)
@output = output
super("Browser did not produce websocket url within #{timeout} seconds, try to increase `:process_timeout`. See https://github.com/rubycdp/ferrum#customization")
end
end

class DeadBrowserError < Error
def initialize(message = "Browser is dead or given window is closed")
super
end
end

class NodeMovingError < Error
def initialize(node, prev, current)
@node = node
@prev = prev
@current = current
super(message)
end

def message
"#{@node.inspect} that you're trying to click is moving, hence " \
"we cannot. Previously it was at #{@prev.inspect} but now at " \
"#{@current.inspect}."
end
end

class CoordinatesNotFoundError < Error
def initialize(message = "Could not compute content quads")
super
end
end

class BrowserError < Error
attr_reader :response

def initialize(response)
@response = response
super(response["message"])
end

def code
response["code"]
end

def data
response["data"]
end
end

class NodeNotFoundError < BrowserError; end

class NoExecutionContextError < BrowserError
def initialize(response = nil)
response ||= { "message" => "There's no context available" }
super(response)
end
end

class JavaScriptError < BrowserError
attr_reader :class_name, :message, :stack_trace

def initialize(response, stack_trace = nil)
@class_name, @message = response.values_at("className", "description")
@stack_trace = stack_trace
super(response.merge("message" => @message))
end
end

class << self
def windows?
RbConfig::CONFIG["host_os"] =~ /mingw|mswin|cygwin/
end

def mac?
RbConfig::CONFIG["host_os"] =~ /darwin/
end

def mri?
defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
end

def started
@started ||= monotonic_time
end

def elapsed_time(start = nil)
monotonic_time - (start || @started)
end

def monotonic_time
Concurrent.monotonic_time
end

def timeout?(start, timeout)
elapsed_time(start) > timeout
end

def with_attempts(errors:, max:, wait:)
attempts ||= 1
yield
rescue *Array(errors)
raise if attempts >= max

attempts += 1
sleep(wait)
retry
end
end
end
2 changes: 1 addition & 1 deletion lib/ferrum/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def crash
private

def start
Ferrum.started
Utils::ElapsedTime.start
@process = Process.start(@options)
@client = Client.new(self, @process.ws_url)
@contexts = Contexts.new(self)
Expand Down
4 changes: 2 additions & 2 deletions lib/ferrum/browser/options/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def except(*keys)
end

def detect_path
if Ferrum.mac?
if Utils::Platform.mac?
self.class::MAC_BIN_PATH.find { |n| File.exist?(n) }
elsif Ferrum.windows?
elsif Utils::Platform.windows?
self.class::WINDOWS_BIN_PATH.find { |path| File.exist?(path) }
else
self.class::LINUX_BIN_PATH.find do |name|
Expand Down
12 changes: 6 additions & 6 deletions lib/ferrum/browser/process.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ def self.start(*args)

def self.process_killer(pid)
proc do
if Ferrum.windows?
if Utils::Platform.windows?
# Process.kill is unreliable on Windows
::Process.kill("KILL", pid) unless system("taskkill /f /t /pid #{pid} >NUL 2>NUL")
else
::Process.kill("USR1", pid)
start = Ferrum.monotonic_time
start = Utils::ElapsedTime.monotonic_time
while ::Process.wait(pid, ::Process::WNOHANG).nil?
sleep(WAIT_KILLED)
next unless Ferrum.timeout?(start, KILL_TIMEOUT)
next unless Utils::ElapsedTime.timeout?(start, KILL_TIMEOUT)

::Process.kill("KILL", pid)
::Process.wait(pid)
Expand Down Expand Up @@ -88,7 +88,7 @@ def start
begin
read_io, write_io = IO.pipe
process_options = { in: File::NULL }
process_options[:pgroup] = true unless Ferrum.windows?
process_options[:pgroup] = true unless Utils::Platform.windows?
process_options[:out] = process_options[:err] = write_io

if @command.xvfb?
Expand Down Expand Up @@ -135,10 +135,10 @@ def remove_user_data_dir

def parse_ws_url(read_io, timeout)
output = ""
start = Ferrum.monotonic_time
start = Utils::ElapsedTime.monotonic_time
max_time = start + timeout
regexp = %r{DevTools listening on (ws://.*)}
while (now = Ferrum.monotonic_time) < max_time
while (now = Utils::ElapsedTime.monotonic_time) < max_time
begin
output += read_io.read_nonblock(512)
rescue IO::WaitReadable
Expand Down
4 changes: 2 additions & 2 deletions lib/ferrum/browser/web_socket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def on_message(event)
output.sub!(/{"data":"(.*)"}/, %("Set FERRUM_LOGGING_SCREENSHOTS=true to see screenshots in Base64"))
end

@logger&.puts(" ◀ #{Ferrum.elapsed_time} #{output}\n")
@logger&.puts(" ◀ #{Utils::ElapsedTime.elapsed_time} #{output}\n")
end

def on_close(_event)
Expand All @@ -74,7 +74,7 @@ def send_message(data)

json = data.to_json
@driver.text(json)
@logger&.puts("\n\n#{Ferrum.elapsed_time} #{json}")
@logger&.puts("\n\n#{Utils::ElapsedTime.elapsed_time} #{json}")
end

def write(data)
Expand Down
116 changes: 116 additions & 0 deletions lib/ferrum/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# frozen_string_literal: true

module Ferrum
class Error < StandardError; end

class NoSuchPageError < Error; end

class NoSuchTargetError < Error; end

class NotImplementedError < Error; end

class StatusError < Error
def initialize(url, message = nil)
super(message || "Request to #{url} failed to reach server, check DNS and server status")
end
end

class PendingConnectionsError < StatusError
attr_reader :pendings

def initialize(url, pendings = [])
@pendings = pendings

message = "Request to #{url} reached server, but there are still pending connections: #{pendings.join(', ')}"

super(url, message)
end
end

class TimeoutError < Error
def message
"Timed out waiting for response. It's possible that this happened " \
"because something took a very long time (for example a page load " \
"was slow). If so, setting the :timeout option to a higher value might " \
"help."
end
end

class ScriptTimeoutError < Error
def message
"Timed out waiting for evaluated script to return a value"
end
end

class ProcessTimeoutError < Error
attr_reader :output

def initialize(timeout, output)
@output = output
super("Browser did not produce websocket url within #{timeout} seconds, try to increase `:process_timeout`. See https://github.com/rubycdp/ferrum#customization")
end
end

class DeadBrowserError < Error
def initialize(message = "Browser is dead or given window is closed")
super
end
end

class NodeMovingError < Error
def initialize(node, prev, current)
@node = node
@prev = prev
@current = current
super(message)
end

def message
"#{@node.inspect} that you're trying to click is moving, hence " \
"we cannot. Previously it was at #{@prev.inspect} but now at " \
"#{@current.inspect}."
end
end

class CoordinatesNotFoundError < Error
def initialize(message = "Could not compute content quads")
super
end
end

class BrowserError < Error
attr_reader :response

def initialize(response)
@response = response
super(response["message"])
end

def code
response["code"]
end

def data
response["data"]
end
end

class NodeNotFoundError < BrowserError; end

class NoExecutionContextError < BrowserError
def initialize(response = nil)
response ||= { "message" => "There's no context available" }
super(response)
end
end

class JavaScriptError < BrowserError
attr_reader :class_name, :message, :stack_trace

def initialize(response, stack_trace = nil)
@class_name, @message = response.values_at("className", "description")
@stack_trace = stack_trace
super(response.merge("message" => @message))
end
end
end
2 changes: 1 addition & 1 deletion lib/ferrum/frame/runtime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def call(expression:, arguments: [], on: nil, wait: 0, handle: true, **options)
sleep = INTERMITTENT_SLEEP
attempts = INTERMITTENT_ATTEMPTS

Ferrum.with_attempts(errors: errors, max: attempts, wait: sleep) do
Utils::Attempt.with_retry(errors: errors, max: attempts, wait: sleep) do
params = options.dup

if on
Expand Down
Loading

0 comments on commit 2ec7533

Please sign in to comment.