teampoltergeist--poltergeist/lib/capybara/poltergeist/client.rb

152 lines
4.8 KiB
Ruby

# frozen_string_literal: true
require 'capybara/poltergeist/utility'
require 'cliver'
module Capybara::Poltergeist
class Client
PHANTOMJS_SCRIPT = File.expand_path('client/compiled/main.js', __dir__)
PHANTOMJS_VERSION = ['>= 1.8.1', '< 3.0'].freeze
PHANTOMJS_NAME = 'phantomjs'
KILL_TIMEOUT = 2 # seconds
def self.start(*args)
client = new(*args)
client.start
client
end
# 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:
# http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
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
next unless (Time.now - start) > KILL_TIMEOUT
Process.kill('KILL', pid)
Process.wait(pid)
break
end
end
rescue Errno::ESRCH, Errno::ECHILD
# Zed's dead, baby
end
end
end
attr_reader :pid, :server, :path, :window_size, :phantomjs_options
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
def start
@read_io, @write_io = IO.pipe
@out_thread = Thread.new do
while !@read_io.eof? && (data = @read_io.readpartial(1024))
@phantomjs_logger.write(data)
end
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
def stop
return unless pid
kill_phantomjs
@out_thread.kill
close_io
ObjectSpace.undefine_finalizer(self)
end
def restart
stop
start
end
def command
parts = [path]
parts.concat phantomjs_options
parts << PHANTOMJS_SCRIPT
parts << server.port
parts.concat window_size
parts << server.host
parts
end
private
# 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.
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
def kill_phantomjs
self.class.process_killer(pid).call
@pid = nil
end
# 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.
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
end
end