class Capybara::Poltergeist::Client
Constants
- KILL_TIMEOUT
- PHANTOMJS_NAME
- PHANTOMJS_SCRIPT
- PHANTOMJS_VERSION
Attributes
Public Class Methods
# File lib/capybara/poltergeist/client.rb, line 45 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
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 23 def self.process_killer(pid) proc do begin if Capybara::Poltergeist.windows? Process.kill('KILL', pid) else Process.kill('TERM', pid) begin Timeout.timeout(KILL_TIMEOUT) { Process.wait(pid) } rescue Timeout::Error Process.kill('KILL', pid) Process.wait(pid) end end rescue Errno::ESRCH, Errno::ECHILD # Zed's dead, baby end end end
# File lib/capybara/poltergeist/client.rb, line 13 def self.start(*args) client = new(*args) client.start client end
Public Instance Methods
# File lib/capybara/poltergeist/client.rb, line 88 def command parts = [path] parts.concat phantomjs_options parts << PHANTOMJS_SCRIPT parts << server.port parts.concat window_size parts end
# File lib/capybara/poltergeist/client.rb, line 83 def restart stop start end
# File lib/capybara/poltergeist/client.rb, line 57 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? redirect_stdout do @pid = Process.spawn(*command.map(&:to_s), process_options) ObjectSpace.define_finalizer(self, self.class.process_killer(@pid)) end end
# File lib/capybara/poltergeist/client.rb, line 74 def stop if pid kill_phantomjs @out_thread.kill close_io ObjectSpace.undefine_finalizer(self) end end
Private Instance Methods
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 131 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
# File lib/capybara/poltergeist/client.rb, line 115 def kill_phantomjs self.class.process_killer(pid).call @pid = nil end
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 104 def redirect_stdout prev = STDOUT.dup $stdout = @write_io STDOUT.reopen(@write_io) yield ensure STDOUT.reopen(prev) $stdout = STDOUT prev.close end