class Capybara::Poltergeist::Client
Constants
- KILL_TIMEOUT
- PHANTOMJS_NAME
- PHANTOMJS_SCRIPT
- PHANTOMJS_VERSION
Attributes
Public Class Methods
# File lib/capybara/poltergeist/client.rb, line 50 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 25 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
# File lib/capybara/poltergeist/client.rb, line 15 def self.start(*args) client = new(*args) client.start client end
Public Instance Methods
# File lib/capybara/poltergeist/client.rb, line 94 def command parts = [path] parts.concat phantomjs_options parts << PHANTOMJS_SCRIPT parts << server.port parts.concat window_size parts << server.host parts end
# File lib/capybara/poltergeist/client.rb, line 89 def restart stop start end
# File lib/capybara/poltergeist/client.rb, line 62 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
# File lib/capybara/poltergeist/client.rb, line 80 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 144 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 128 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 111 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