Class: Ferrum::Browser::Process

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/ferrum/browser/process.rb

Overview

Spawns and manages the lifecycle of the browser OS process: builds the launch Command, starts it (optionally under Xvfb for headful mode), waits for the CDP WebSocket endpoint to become available, and handles stopping/restarting the process and cleaning up its user data directory.

Constant Summary collapse

KILL_TIMEOUT =
2
WAIT_KILLED =
0.05

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Process

Returns a new instance of Process.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/ferrum/browser/process.rb', line 95

def initialize(options)
  @pid = @xvfb = @user_data_dir = nil

  if options.ws_url || options.url
    # `:ws_url` option is higher priority than `:url`, parse versions
    # and use it as a ws_url, otherwise use what has been parsed.
    response = parse_json_version(options.ws_url || options.url)
    self.ws_url = options.ws_url || response&.[]("webSocketDebuggerUrl")
    return
  end

  @logger = options.logger
  @process_timeout = options.process_timeout
  @env = Hash(options.env)

  tmpdir = Dir.mktmpdir("ferrum_user_data_dir_")
  ObjectSpace.define_finalizer(self, self.class.directory_remover(tmpdir))
  @user_data_dir = tmpdir
  @command = Command.build(options, tmpdir)
end

Instance Attribute Details

#browser_versionObject (readonly)

Returns the value of attribute browser_version.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def browser_version
  @browser_version
end

#commandObject (readonly)

Returns the value of attribute command.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def command
  @command
end

#default_user_agentObject (readonly)

Returns the value of attribute default_user_agent.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def default_user_agent
  @default_user_agent
end

#hostObject (readonly)

Returns the value of attribute host.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def host
  @host
end

#pidObject (readonly)

Returns the value of attribute pid.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def pid
  @pid
end

#portObject (readonly)

Returns the value of attribute port.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def port
  @port
end

#protocol_versionObject (readonly)

Returns the value of attribute protocol_version.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def protocol_version
  @protocol_version
end

#v8_versionObject (readonly)

Returns the value of attribute v8_version.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def v8_version
  @v8_version
end

#webkit_versionObject (readonly)

Returns the value of attribute webkit_version.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def webkit_version
  @webkit_version
end

#ws_urlObject

Returns the value of attribute ws_url.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def ws_url
  @ws_url
end

#xvfbObject (readonly)

Returns the value of attribute xvfb.



91
92
93
# File 'lib/ferrum/browser/process.rb', line 91

def xvfb
  @xvfb
end

Class Method Details

.directory_remover(path) ⇒ Proc

Builds a finalizer proc that removes the directory at the given path.

Parameters:

  • path (String)

    Directory to remove.

Returns:

  • (Proc)


81
82
83
84
85
86
87
88
89
# File 'lib/ferrum/browser/process.rb', line 81

def self.directory_remover(path)
  proc {
    begin
      FileUtils.remove_entry(path)
    rescue StandardError
      Errno::ENOENT
    end
  }
end

.process_killer(pid) ⇒ Proc

Builds a finalizer proc that kills the process with the given pid.

Parameters:

  • pid (Integer)

    Process id to kill.

Returns:

  • (Proc)


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/ferrum/browser/process.rb', line 51

def self.process_killer(pid)
  proc do
    if Utils::Platform.windows?
      # Process.kill is unreliable on Windows
      ::Process.kill("KILL", pid) unless system("taskkill /f /t /pid #{pid} >NUL 2>NUL")
    else
      ::Process.kill("USR1", pid)
      start = Utils::ElapsedTime.monotonic_time
      while ::Process.wait(pid, ::Process::WNOHANG).nil?
        sleep(WAIT_KILLED)
        next unless Utils::ElapsedTime.timeout?(start, KILL_TIMEOUT)

        ::Process.kill("KILL", pid)
        ::Process.wait(pid)
        break
      end
    end
  rescue Errno::ESRCH, Errno::ECHILD
    # nop
  end
end

.start(*args) ⇒ Process

Builds and starts a new browser process.

Parameters:

Returns:



39
40
41
# File 'lib/ferrum/browser/process.rb', line 39

def self.start(*args)
  new(*args).tap(&:start)
end

Instance Method Details

#inspectString

Custom inspection that omits noisy internal command details.

Returns:

  • (String)


179
180
181
182
183
184
185
186
187
188
# File 'lib/ferrum/browser/process.rb', line 179

def inspect
  "#<#{self.class} " \
    "@user_data_dir=#{@user_data_dir.inspect} " \
    "@command=#<#{@command.class}:#{@command.object_id}> " \
    "@default_user_agent=#{@default_user_agent.inspect} " \
    "@ws_url=#{@ws_url.inspect} " \
    "@v8_version=#{@v8_version.inspect} " \
    "@browser_version=#{@browser_version.inspect} " \
    "@webkit_version=#{@webkit_version.inspect}>"
end

#restartvoid

This method returns an undefined value.

Stops and starts the browser process again.



169
170
171
172
# File 'lib/ferrum/browser/process.rb', line 169

def restart
  stop
  start
end

#startvoid

This method returns an undefined value.

Spawns the browser process and waits for it to become reachable over CDP.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/ferrum/browser/process.rb', line 121

def start
  # Don't do anything as browser is already running as external process.
  return if ws_url

  begin
    read_io, write_io = IO.pipe
    process_options = { in: File::NULL }
    process_options[:pgroup] = true unless Utils::Platform.windows?
    process_options[:out] = process_options[:err] = write_io

    if @command.xvfb?
      @xvfb = Xvfb.start(@command.options)
      ObjectSpace.define_finalizer(self, self.class.process_killer(@xvfb.pid))
    end

    env = Hash(@xvfb&.to_env).merge(@env)
    @pid = ::Process.spawn(env, *@command.to_a, process_options)
    ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))

    parse_ws_url(read_io, @process_timeout)
    parse_json_version(ws_url)
  ensure
    close_io(read_io, write_io)
  end
end

#stopvoid

This method returns an undefined value.

Kills the browser process (and Xvfb, if running) and removes the user data directory.



153
154
155
156
157
158
159
160
161
162
# File 'lib/ferrum/browser/process.rb', line 153

def stop
  if @pid
    kill(@pid)
    kill(@xvfb.pid) if @xvfb&.pid
    @pid = nil
  end

  remove_user_data_dir if @user_data_dir
  ObjectSpace.undefine_finalizer(self)
end