diff --git a/lib/ferrum/client.rb b/lib/ferrum/client.rb index 4321476e..24069745 100644 --- a/lib/ferrum/client.rb +++ b/lib/ferrum/client.rb @@ -299,6 +299,7 @@ def close @pendings.clear @thread.kill unless @thread.join(1) @subscriber.close + @ws.force_close end # diff --git a/lib/ferrum/client/web_socket.rb b/lib/ferrum/client/web_socket.rb index cdbca237..95697e68 100644 --- a/lib/ferrum/client/web_socket.rb +++ b/lib/ferrum/client/web_socket.rb @@ -141,6 +141,24 @@ def close @driver_mutex.synchronize { @driver.close } end + # + # Closes the underlying socket whether or not the close handshake ever + # completed. + # + # {#close} only sends a close frame; the socket itself is closed by + # {#on_close}, which runs when the browser answers with a close frame of + # its own. A browser that is already dead never answers, so without this + # the socket stays open until GC finalizes it -- long enough to exhaust + # the process's file descriptors when browsers are restarted in a loop. + # + # @return [void] + # + def force_close + @sock.close unless @sock.closed? + rescue IOError, SystemCallError + # nop + end + private def start diff --git a/sig/ferrum/client/web_socket.rbs b/sig/ferrum/client/web_socket.rbs index bb5338d8..8c31e9a0 100644 --- a/sig/ferrum/client/web_socket.rbs +++ b/sig/ferrum/client/web_socket.rbs @@ -30,6 +30,8 @@ module Ferrum def close: () -> void + def force_close: () -> void + private def start: () -> void diff --git a/spec/client_spec.rb b/spec/client_spec.rb index b1585041..f3cbbe33 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -38,6 +38,16 @@ def messages expect(Ferrum::Utils::ElapsedTime.elapsed_time(start)).to be < 1 end + it "closes the socket when the browser dies before answering the close frame" do + remote.create_page + socket = remote.client.instance_variable_get(:@ws).instance_variable_get(:@sock) + Process.kill("KILL", -remote.process.pid) + + remote.quit + + expect(socket).to be_closed + end + it "reports an unexpected reader error instead of raising in the main thread" do page = remote.create_page driver = remote.client.instance_variable_get(:@ws).instance_variable_get(:@driver)