~ubuntu-branches/debian/sid/ruby-poltergeist/sid

« back to all changes in this revision

Viewing changes to lib/capybara/poltergeist/client.rb

  • Committer: Package Import Robot
  • Author(s): Pirate Praveen
  • Date: 2016-09-15 22:01:34 UTC
  • Revision ID: package-import@ubuntu.com-20160915220134-vd142d9tvojjvcyo
Tags: upstream-1.10.0
ImportĀ upstreamĀ versionĀ 1.10.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
require "timeout"
 
2
require "capybara/poltergeist/utility"
 
3
require 'cliver'
 
4
 
 
5
module Capybara::Poltergeist
 
6
  class Client
 
7
    PHANTOMJS_SCRIPT  = File.expand_path('../client/compiled/main.js', __FILE__)
 
8
    PHANTOMJS_VERSION = ['>= 1.8.1', '< 3.0']
 
9
    PHANTOMJS_NAME    = 'phantomjs'
 
10
 
 
11
    KILL_TIMEOUT = 2 # seconds
 
12
 
 
13
    def self.start(*args)
 
14
      client = new(*args)
 
15
      client.start
 
16
      client
 
17
    end
 
18
 
 
19
    # Returns a proc, that when called will attempt to kill the given process.
 
20
    # This is because implementing ObjectSpace.define_finalizer is tricky.
 
21
    # Hat-Tip to @mperham for describing in detail:
 
22
    # http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
 
23
    def self.process_killer(pid)
 
24
      proc do
 
25
        begin
 
26
          Process.kill('KILL', pid)
 
27
        rescue Errno::ESRCH, Errno::ECHILD
 
28
        end
 
29
      end
 
30
    end
 
31
 
 
32
    attr_reader :pid, :server, :path, :window_size, :phantomjs_options
 
33
 
 
34
    def initialize(server, options = {})
 
35
      @server            = server
 
36
      @path              = Cliver::detect((options[:path] || PHANTOMJS_NAME), *['>=2.1.0', '< 3.0'])
 
37
      @path            ||= Cliver::detect!((options[:path] || PHANTOMJS_NAME), *PHANTOMJS_VERSION).tap do
 
38
        warn "You're running an old version of PhantomJS, update to >= 2.1.1 for a better experience."
 
39
      end
 
40
 
 
41
      @window_size       = options[:window_size]       || [1024, 768]
 
42
      @phantomjs_options = options[:phantomjs_options] || []
 
43
      @phantomjs_logger  = options[:phantomjs_logger]  || $stdout
 
44
 
 
45
      pid = Process.pid
 
46
      at_exit do
 
47
        # do the work in a separate thread, to avoid stomping on $!,
 
48
        # since other libraries depend on it directly.
 
49
        Thread.new do
 
50
          stop if Process.pid == pid
 
51
        end.join
 
52
      end
 
53
    end
 
54
 
 
55
    def start
 
56
      @read_io, @write_io = IO.pipe
 
57
      @out_thread = Thread.new {
 
58
        while !@read_io.eof? && data = @read_io.readpartial(1024)
 
59
          @phantomjs_logger.write(data)
 
60
        end
 
61
      }
 
62
 
 
63
      process_options = {}
 
64
      process_options[:pgroup] = true unless Capybara::Poltergeist.windows?
 
65
 
 
66
      redirect_stdout do
 
67
        @pid = Process.spawn(*command.map(&:to_s), process_options)
 
68
        ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
 
69
      end
 
70
    end
 
71
 
 
72
    def stop
 
73
      if pid
 
74
        kill_phantomjs
 
75
        @out_thread.kill
 
76
        close_io
 
77
        ObjectSpace.undefine_finalizer(self)
 
78
      end
 
79
    end
 
80
 
 
81
    def restart
 
82
      stop
 
83
      start
 
84
    end
 
85
 
 
86
    def command
 
87
      parts = [path]
 
88
      parts.concat phantomjs_options
 
89
      parts << PHANTOMJS_SCRIPT
 
90
      parts << server.port
 
91
      parts.concat window_size
 
92
      parts
 
93
    end
 
94
 
 
95
    private
 
96
 
 
97
    # This abomination is because JRuby doesn't support the :out option of
 
98
    # Process.spawn. To be honest it works pretty bad with pipes too, because
 
99
    # we ought close writing end in parent process immediately but JRuby will
 
100
    # lose all the output from child. Process.popen can be used here and seems
 
101
    # it works with JRuby but I've experienced strange mistakes on Rubinius.
 
102
    def redirect_stdout
 
103
      prev = STDOUT.dup
 
104
      $stdout = @write_io
 
105
      STDOUT.reopen(@write_io)
 
106
      yield
 
107
    ensure
 
108
      STDOUT.reopen(prev)
 
109
      $stdout = STDOUT
 
110
      prev.close
 
111
    end
 
112
 
 
113
    def kill_phantomjs
 
114
      begin
 
115
        if Capybara::Poltergeist.windows?
 
116
          Process.kill('KILL', pid)
 
117
        else
 
118
          Process.kill('TERM', pid)
 
119
          begin
 
120
            Timeout.timeout(KILL_TIMEOUT) { Process.wait(pid) }
 
121
          rescue Timeout::Error
 
122
            Process.kill('KILL', pid)
 
123
            Process.wait(pid)
 
124
          end
 
125
        end
 
126
      rescue Errno::ESRCH, Errno::ECHILD
 
127
        # Zed's dead, baby
 
128
      end
 
129
      @pid = nil
 
130
    end
 
131
 
 
132
    # We grab all the output from PhantomJS like console.log in another thread
 
133
    # and when PhantomJS crashes we try to restart it. In order to do it we stop
 
134
    # server and client and on JRuby see this error `IOError: Stream closed`.
 
135
    # It happens because JRuby tries to close pipe and it is blocked on `eof?`
 
136
    # or `readpartial` call. The error is raised in the related thread and it's
 
137
    # not actually main thread but the thread that listens to the output. That's
 
138
    # why if you put some debug code after `rescue IOError` it won't be shown.
 
139
    # In fact the main thread will continue working after the error even if we
 
140
    # don't use `rescue`. The first attempt to fix it was a try not to block on
 
141
    # IO, but looks like similar issue appers after JRuby upgrade. Perhaps the
 
142
    # only way to fix it is catching the exception what this method overall does.
 
143
    def close_io
 
144
      [@write_io, @read_io].each do |io|
 
145
        begin
 
146
          io.close unless io.closed?
 
147
        rescue IOError
 
148
          raise unless RUBY_ENGINE == 'jruby'
 
149
        end
 
150
      end
 
151
    end
 
152
  end
 
153
end