Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: [2.7, "3.0", 3.1, 3.2, 3.3]
ruby: [2.7, "3.0", 3.1, 3.2, 3.3, "jruby-9.4"]
runs-on: ubuntu-latest
env:
FERRUM_PROCESS_TIMEOUT: 25
Expand Down
4 changes: 3 additions & 1 deletion lib/ferrum/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require "ferrum/browser/xvfb"
require "ferrum/browser/options"
require "ferrum/browser/process"
require "ferrum/browser/jruby_process"
require "ferrum/browser/binary"
require "ferrum/browser/version_info"

Expand Down Expand Up @@ -253,7 +254,8 @@ def headless_new?

def start
Utils::ElapsedTime.start
@process = Process.new(options)
process_class = Utils::Platform.jruby? ? JrubyProcess : Process
@process = process_class.new(options)

begin
@process.start
Expand Down
89 changes: 89 additions & 0 deletions lib/ferrum/browser/jruby_process.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true

require "ferrum/browser/process"

module Ferrum
class Browser
class JrubyProcess < Process
def start
# Don't do anything as browser is already running as external process.
return if ws_url

begin
process_builder_args = @command.to_a
# Sometimes subprocesses are launched with the wrong architecture on Apple Silicon.
if ENV_JAVA["os.name"] == "Mac OS X" && ENV_JAVA["os.arch"] == "aarch64"
process_builder_args.unshift("/usr/bin/arch", "-arm64")
end
process_builder = java.lang.ProcessBuilder.new(*process_builder_args)
# unless user directory is on a Windows UNC path
process_builder.directory(java.io.File.new(@user_data_dir)) unless @user_data_dir =~ %r{\A//}
process_builder.redirectErrorStream(true)

if @command.xvfb?
@xvfb = Xvfb.start(@command.options)
ObjectSpace.define_finalizer(self, self.class.process_killer(@xvfb.pid))
process_builder.environment.merge! Hash(@xvfb&.to_env)
end

@java_process = process_builder.start

# The process output is switched to a buffered reader and parsed to get the WebSocket URL.
input_reader = java.io.BufferedReader.new(java.io.InputStreamReader.new(java_process.getInputStream))
parse_ws_url(input_reader, @process_timeout)
parse_json_version(ws_url)
end
end

attr_reader :java_process

def stop
destroy_java_process

remove_user_data_dir if @user_data_dir
ObjectSpace.undefine_finalizer(self)
end

private

def parse_ws_url(read_io, timeout)
output = ""
start = Utils::ElapsedTime.monotonic_time
max_time = start + timeout
regexp = %r{DevTools listening on (ws://.*[a-zA-Z0-9-]{36})}
while Utils::ElapsedTime.monotonic_time < max_time
# The buffered reader is used to read the process output.
if output.match(regexp)
self.ws_url = output.match(regexp)[1].strip
break
elsif (rl = read_io.read_line)
output += rl
end
end

return if ws_url

@logger&.puts(output)
raise ProcessTimeoutError.new(timeout, output)
end

def destroy_java_process
return unless java_process

java_process.destroy
retry_times = 6
while java_process.isAlive && retry_times.positive?
sleep 1
retry_times -= 1
end
if java_process.isAlive
@logger&.puts("Ferrum::Browser::JrubyProcess is still alive, killing it forcibly")
java_process.destroyForcibly
else
@logger&.puts("Ferrum::Browser::JrubyProcess is stopped")
end
@java_process = nil
end
end
end
end
2 changes: 1 addition & 1 deletion lib/ferrum/browser/process.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def close_io(*ios)
ios.each do |io|
io.close unless io.closed?
rescue IOError
raise unless RUBY_ENGINE == "jruby"
raise unless Utils::Platform.jruby?
end
end

Expand Down
8 changes: 5 additions & 3 deletions lib/ferrum/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class Client
attr_reader :ws_url, :options, :subscriber

def initialize(ws_url, options)
@command_id = 0
@command_id = Concurrent::AtomicFixnum.new(0)
@ws_url = ws_url
@options = options
@pendings = Concurrent::Hash.new
Expand Down Expand Up @@ -131,7 +131,7 @@ def build_message(method, params)
private

def start
@thread = Utils::Thread.spawn do
@thread = Utils::Thread.spawn(abort_on_exception: !Utils::Platform.jruby?) do
loop do
message = @ws.messages.pop
break unless message
Expand All @@ -141,12 +141,14 @@ def start
else
@pendings[message["id"]]&.set(message)
end
rescue StandardError => e
@logger&.puts("Ferrum::Client thread raised an exception #{e.class.name}: #{e.message}")
end
end
end

def next_command_id
@command_id += 1
@command_id.increment
end

def raise_browser_error(error)
Expand Down
4 changes: 2 additions & 2 deletions lib/ferrum/client/web_socket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,14 @@ def close
private

def start
@thread = Utils::Thread.spawn do
@thread = Utils::Thread.spawn(abort_on_exception: !Utils::Platform.jruby?) do
loop do
data = @sock.readpartial(512)
break unless data

@driver.parse(data)
end
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, IOError # rubocop:disable Lint/ShadowedException
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::EBADF, IOError # rubocop:disable Lint/ShadowedException
@messages.close
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ferrum/utils/platform.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def mac_arm?
def mri?
defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
end

def jruby?
defined?(JRUBY_VERSION)
end
end
end
end
26 changes: 20 additions & 6 deletions spec/browser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -364,14 +364,28 @@
browser&.quit
end

it "supports stopping the session", skip: Ferrum::Utils::Platform.windows? do
browser = Ferrum::Browser.new
pid = browser.process.pid
context "for MRI Ruby", skip: Ferrum::Utils::Platform.windows? || Ferrum::Utils::Platform.jruby? do
it "supports stopping the session" do
browser = Ferrum::Browser.new
pid = browser.process.pid

expect(Process.kill(0, pid)).to eq(1)
browser.quit

expect(Process.kill(0, pid)).to eq(1)
browser.quit
expect { Process.kill(0, pid) }.to raise_error(Errno::ESRCH)
end
end

expect { Process.kill(0, pid) }.to raise_error(Errno::ESRCH)
context "for JRuby", skip: Ferrum::Utils::Platform.windows? || Ferrum::Utils::Platform.mri? do
it "supports stopping the session" do
browser = Ferrum::Browser.new
pid = browser.process.java_process.pid

expect(Process.kill(0, pid)).to eq(1)
browser.quit

expect { Process.kill(0, pid) }.to raise_error(Errno::ESRCH)
end
end
end

Expand Down
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@

config.after(:all) do
@browser.quit
rescue IOError => e
puts "#{e.class}: #{e.message}"
end

config.before(:each) do
Expand Down
2 changes: 1 addition & 1 deletion spec/unit/process_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
describe Ferrum::Browser::Process do
subject { Ferrum::Browser.new(port: 6000, host: "127.0.0.1") }

unless Ferrum::Utils::Platform.windows?
unless Ferrum::Utils::Platform.windows? || Ferrum::Utils::Platform.jruby?
it "forcibly kills the child if it does not respond to SIGTERM" do
allow(Process).to receive(:spawn).and_return(5678)
allow(Process).to receive(:wait).and_return(nil)
Expand Down