class Capybara::Poltergeist::Client

Constants

KILL_TIMEOUT
PHANTOMJS_NAME
PHANTOMJS_SCRIPT
PHANTOMJS_VERSION

Attributes

path[R]
phantomjs_options[R]
pid[R]
server[R]
window_size[R]

Public Class Methods

new(server, options = {}) click to toggle source
# File lib/capybara/poltergeist/client.rb, line 49
def initialize(server, options = {})
  @server            = server
  @path              = Cliver::detect((options[:path] || PHANTOMJS_NAME), *['>=2.1.0', '< 3.0'])
  @path            ||= Cliver::detect!((options[:path] || PHANTOMJS_NAME), *PHANTOMJS_VERSION).tap do
    warn "You're running an old version of PhantomJS, update to >= 2.1.1 for a better experience."
  end

  @window_size       = options[:window_size]       || [1024, 768]
  @phantomjs_options = options[:phantomjs_options] || []
  @phantomjs_logger  = options[:phantomjs_logger]  || $stdout
end
process_killer(pid) click to toggle source

Returns a proc, that when called will attempt to kill the given process. This is because implementing ObjectSpace.define_finalizer is tricky. Hat-Tip to @mperham for describing in detail: www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/

# File lib/capybara/poltergeist/client.rb, line 24
def self.process_killer(pid)
  proc do
    begin
      if Capybara::Poltergeist.windows?
        Process.kill('KILL', pid)
      else
        Process.kill('TERM', pid)
        start = Time.now
        while Process.wait(pid, Process::WNOHANG).nil?
          sleep 0.05
          if (Time.now - start) > KILL_TIMEOUT
            Process.kill('KILL', pid)
            Process.wait(pid)
            break
          end
        end
      end
    rescue Errno::ESRCH, Errno::ECHILD
      # Zed's dead, baby
    end
  end
end
start(*args) click to toggle source
# File lib/capybara/poltergeist/client.rb, line 14
def self.start(*args)
  client = new(*args)
  client.start
  client
end

Public Instance Methods

command() click to toggle source
# File lib/capybara/poltergeist/client.rb, line 93
def command
  parts = [path]
  parts.concat phantomjs_options
  parts << PHANTOMJS_SCRIPT
  parts << server.port
  parts.concat window_size
  parts << server.host
  parts
end
restart() click to toggle source
# File lib/capybara/poltergeist/client.rb, line 88
def restart
  stop
  start
end
start() click to toggle source
# File lib/capybara/poltergeist/client.rb, line 61
def start
  @read_io, @write_io = IO.pipe
  @out_thread = Thread.new {
    while !@read_io.eof? && data = @read_io.readpartial(1024)
      @phantomjs_logger.write(data)
    end
  }

  process_options = {in: File::NULL}
  process_options[:pgroup] = true unless Capybara::Poltergeist.windows?
  process_options[:out] = @write_io if Capybara::Poltergeist.mri?

  redirect_stdout do
    @pid = Process.spawn(*command.map(&:to_s), process_options)
    ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
  end
end
stop() click to toggle source
# File lib/capybara/poltergeist/client.rb, line 79
def stop
  if pid
    kill_phantomjs
    @out_thread.kill
    close_io
    ObjectSpace.undefine_finalizer(self)
  end
end

Private Instance Methods

close_io() click to toggle source

We grab all the output from PhantomJS like console.log in another thread and when PhantomJS crashes we try to restart it. In order to do it we stop server and client and on JRuby see this error `IOError: Stream closed`. It happens because JRuby tries to close pipe and it is blocked on `eof?` or `readpartial` call. The error is raised in the related thread and it's not actually main thread but the thread that listens to the output. That's why if you put some debug code after `rescue IOError` it won't be shown. In fact the main thread will continue working after the error even if we don't use `rescue`. The first attempt to fix it was a try not to block on IO, but looks like similar issue appers after JRuby upgrade. Perhaps the only way to fix it is catching the exception what this method overall does.

# File lib/capybara/poltergeist/client.rb, line 143
def close_io
  [@write_io, @read_io].each do |io|
    begin
      io.close unless io.closed?
    rescue IOError
      raise unless RUBY_ENGINE == 'jruby'
    end
  end
end
kill_phantomjs() click to toggle source
# File lib/capybara/poltergeist/client.rb, line 127
def kill_phantomjs
  self.class.process_killer(pid).call
  @pid = nil
end
redirect_stdout() { || ... } click to toggle source

This abomination is because JRuby doesn't support the :out option of Process.spawn. To be honest it works pretty bad with pipes too, because we ought close writing end in parent process immediately but JRuby will lose all the output from child. Process.popen can be used here and seems it works with JRuby but I've experienced strange mistakes on Rubinius.

# File lib/capybara/poltergeist/client.rb, line 110
def redirect_stdout
  if Capybara::Poltergeist.mri?
    yield
  else
    begin
      prev = STDOUT.dup
      $stdout = @write_io
      STDOUT.reopen(@write_io)
      yield
    ensure
      STDOUT.reopen(prev)
      $stdout = STDOUT
      prev.close
    end
  end
end