class Ferrum::Browser::Process

Constants

KILL_TIMEOUT
PROCESS_TIMEOUT
WAIT_KILLED

Attributes

browser_version[R]
command[R]
default_user_agent[R]
host[R]
pid[R]
port[R]
protocol_version[R]
v8_version[R]
webkit_version[R]
ws_url[R]
xvfb[R]

Public Class Methods

directory_remover(path) click to toggle source
# File lib/ferrum/browser/process.rb, line 54
def self.directory_remover(path)
  proc { FileUtils.remove_entry(path) rescue Errno::ENOENT }
end
new(options) click to toggle source
# File lib/ferrum/browser/process.rb, line 58
def initialize(options)
  if options[:url]
    url = URI.join(options[:url].to_s, "/json/version")
    response = JSON.parse(::Net::HTTP.get(url))
    set_ws_url(response["webSocketDebuggerUrl"])
    parse_browser_versions
    return
  end

  @pid = @xvfb = @user_data_dir = nil
  @logger = options[:logger]
  @process_timeout = options.fetch(:process_timeout, PROCESS_TIMEOUT)

  tmpdir = Dir.mktmpdir("ferrum_user_data_dir_")
  ObjectSpace.define_finalizer(self, self.class.directory_remover(tmpdir))
  @user_data_dir = tmpdir
  @command = Command.build(options, tmpdir)
end
process_killer(pid) click to toggle source
# File lib/ferrum/browser/process.rb, line 33
def self.process_killer(pid)
  proc do
    begin
      if Ferrum.windows?
        ::Process.kill("KILL", pid)
      else
        ::Process.kill("USR1", pid)
        start = Ferrum.monotonic_time
        while ::Process.wait(pid, ::Process::WNOHANG).nil?
          sleep(WAIT_KILLED)
          next unless Ferrum.timeout?(start, KILL_TIMEOUT)
          ::Process.kill("KILL", pid)
          ::Process.wait(pid)
          break
        end
      end
    rescue Errno::ESRCH, Errno::ECHILD
    end
  end
end
start(*args) click to toggle source
# File lib/ferrum/browser/process.rb, line 29
def self.start(*args)
  new(*args).tap(&:start)
end

Public Instance Methods

restart() click to toggle source
# File lib/ferrum/browser/process.rb, line 113
def restart
  stop
  start
end
start() click to toggle source
# File lib/ferrum/browser/process.rb, line 77
def start
  # Don't do anything as browser is already running as external process.
  return if ws_url

  begin
    read_io, write_io = IO.pipe
    process_options = { in: File::NULL }
    process_options[:pgroup] = true unless Ferrum.windows?
    process_options[:out] = process_options[:err] = write_io

    if @command.xvfb?
      @xvfb = Xvfb.start(@command.options)
      ObjectSpace.define_finalizer(self, self.class.process_killer(@xvfb.pid))
    end

    @pid = ::Process.spawn(Hash(@xvfb&.to_env), *@command.to_a, process_options)
    ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))

    parse_ws_url(read_io, @process_timeout)
    parse_browser_versions
  ensure
    close_io(read_io, write_io)
  end
end
stop() click to toggle source
# File lib/ferrum/browser/process.rb, line 102
def stop
  if @pid
    kill(@pid)
    kill(@xvfb.pid) if @xvfb&.pid
    @pid = nil
  end

  remove_user_data_dir if @user_data_dir
  ObjectSpace.undefine_finalizer(self)
end

Private Instance Methods

close_io(*ios) click to toggle source
# File lib/ferrum/browser/process.rb, line 172
def close_io(*ios)
  ios.each do |io|
    begin
      io.close unless io.closed?
    rescue IOError
      raise unless RUBY_ENGINE == "jruby"
    end
  end
end
kill(pid) click to toggle source
# File lib/ferrum/browser/process.rb, line 120
def kill(pid)
  self.class.process_killer(pid).call
end
parse_browser_versions() click to toggle source
# File lib/ferrum/browser/process.rb, line 159
def parse_browser_versions
  return unless ws_url.is_a?(Addressable::URI)

  version_url = URI.parse(ws_url.merge(scheme: "http", path: "/json/version"))
  response = JSON.parse(::Net::HTTP.get(version_url))

  @v8_version = response["V8-Version"]
  @browser_version = response["Browser"]
  @webkit_version = response["WebKit-Version"]
  @default_user_agent = response["User-Agent"]
  @protocol_version = response["Protocol-Version"]
end
parse_ws_url(read_io, timeout) click to toggle source
# File lib/ferrum/browser/process.rb, line 129
def parse_ws_url(read_io, timeout)
  output = ""
  start = Ferrum.monotonic_time
  max_time = start + timeout
  regexp = /DevTools listening on (ws:\/\/.*)/
  while (now = Ferrum.monotonic_time) < max_time
    begin
      output += read_io.read_nonblock(512)
    rescue IO::WaitReadable
      IO.select([read_io], nil, nil, max_time - now)
    else
      if output.match(regexp)
        set_ws_url(output.match(regexp)[1].strip)
        break
      end
    end
  end

  unless ws_url
    @logger.puts(output) if @logger
    raise ProcessTimeoutError.new(timeout, output)
  end
end
remove_user_data_dir() click to toggle source
# File lib/ferrum/browser/process.rb, line 124
def remove_user_data_dir
  self.class.directory_remover(@user_data_dir).call
  @user_data_dir = nil
end
set_ws_url(url) click to toggle source
# File lib/ferrum/browser/process.rb, line 153
def set_ws_url(url)
  @ws_url = Addressable::URI.parse(url)
  @host = @ws_url.host
  @port = @ws_url.port
end