mirror of
https://github.com/teampoltergeist/poltergeist.git
synced 2022-11-09 12:05:00 -05:00
154 lines
4.8 KiB
Ruby
154 lines
4.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "timeout"
|
|
require "capybara/poltergeist/utility"
|
|
require 'cliver'
|
|
|
|
module Capybara::Poltergeist
|
|
class Client
|
|
PHANTOMJS_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
|
|
PHANTOMJS_VERSION = ['>= 1.8.1', '< 3.0']
|
|
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
|
|
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
|
|
|
|
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 {
|
|
while !@read_io.eof? && data = @read_io.readpartial(1024)
|
|
@phantomjs_logger.write(data)
|
|
end
|
|
}
|
|
|
|
process_options = {}
|
|
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
|
|
if pid
|
|
kill_phantomjs
|
|
@out_thread.kill
|
|
close_io
|
|
ObjectSpace.undefine_finalizer(self)
|
|
end
|
|
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
|