[rubygems/rubygems] Vendor timeout in RubyGems too

https://github.com/rubygems/rubygems/commit/e2e7440ede
This commit is contained in:
David Rodríguez 2023-06-30 21:22:32 +02:00 committed by Hiroshi SHIBATA
parent 90317472e8
commit a7c9163b5d
14 changed files with 245 additions and 44 deletions

View File

@ -106,7 +106,7 @@ class Gem::CommandManager
# Register all the subcommands supported by the gem command.
def initialize
require "timeout"
require_relative "timeout"
@commands = {}
BUILTIN_COMMANDS.each do |name|
@ -149,7 +149,7 @@ class Gem::CommandManager
def run(args, build_args=nil)
process_args(args, build_args)
rescue StandardError, Timeout::Error => ex
rescue StandardError, Gem::Timeout::Error => ex
if ex.respond_to?(:detailed_message)
msg = ex.detailed_message(highlight: false).sub(/\A(.*?)(?: \(.+?\))/) { $1 }
else

View File

@ -37,13 +37,13 @@ module Gem::GemcutterUtilities
thread.abort_on_exception = true
thread.report_on_exception = false
thread[:otp] = new(options, host).poll_for_otp(webauthn_url, credentials)
rescue Gem::WebauthnVerificationError, Timeout::Error => e
rescue Gem::WebauthnVerificationError, Gem::Timeout::Error => e
thread[:error] = e
end
end
def poll_for_otp(webauthn_url, credentials)
Timeout.timeout(TIMEOUT_IN_SECONDS) do
Gem::Timeout.timeout(TIMEOUT_IN_SECONDS) do
loop do
response = webauthn_verification_poll_response(webauthn_url, credentials)
raise Gem::WebauthnVerificationError, response.message unless response.is_a?(Gem::Net::HTTPSuccess)

View File

@ -46,7 +46,7 @@ module Gem::Net #:nodoc:
# == Strategies
#
# - If you will make only a few GET requests,
# consider using {OpenURI}[https://docs.ruby-lang.org/en/master/OpenURI.html].
# consider using {OpenURI}[rdoc-ref:OpenURI].
# - If you will make only a few requests of all kinds,
# consider using the various singleton convenience methods in this class.
# Each of the following methods automatically starts and finishes
@ -106,7 +106,7 @@ module Gem::Net #:nodoc:
# It consists of some or all of: scheme, hostname, path, query, and fragment;
# see {URI syntax}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax].
#
# A Ruby {URI::Generic}[https://docs.ruby-lang.org/en/master/URI/Generic.html] object
# A Ruby {URI::Generic}[rdoc-ref:URI::Generic] object
# represents an internet URI.
# It provides, among others, methods
# +scheme+, +hostname+, +path+, +query+, and +fragment+.
@ -1217,7 +1217,7 @@ module Gem::Net #:nodoc:
# - The name of an encoding.
# - An alias for an encoding name.
#
# See {Encoding}[https://docs.ruby-lang.org/en/master/Encoding.html].
# See {Encoding}[rdoc-ref:Encoding].
#
# Examples:
#
@ -1308,7 +1308,7 @@ module Gem::Net #:nodoc:
# Sets the maximum number of times to retry an idempotent request in case of
# \Gem::Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
# Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError,
# Timeout::Error.
# Gem::Timeout::Error.
# The initial value is 1.
#
# Argument +retries+ must be a non-negative numeric value:
@ -1490,11 +1490,11 @@ module Gem::Net #:nodoc:
attr_accessor :cert_store
# Sets or returns the available SSL ciphers.
# See {OpenSSL::SSL::SSLContext#ciphers=}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-ciphers-3D].
# See {OpenSSL::SSL::SSLContext#ciphers=}[rdoc-ref:OpenSSL::SSL::SSLContext#ciphers-3D].
attr_accessor :ciphers
# Sets or returns the extra X509 certificates to be added to the certificate chain.
# See {OpenSSL::SSL::SSLContext#add_certificate}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-add_certificate].
# See {OpenSSL::SSL::SSLContext#add_certificate}[rdoc-ref:OpenSSL::SSL::SSLContext#add_certificate].
attr_accessor :extra_chain_cert
# Sets or returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
@ -1504,15 +1504,15 @@ module Gem::Net #:nodoc:
attr_accessor :ssl_timeout
# Sets or returns the SSL version.
# See {OpenSSL::SSL::SSLContext#ssl_version=}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-ssl_version-3D].
# See {OpenSSL::SSL::SSLContext#ssl_version=}[rdoc-ref:OpenSSL::SSL::SSLContext#ssl_version-3D].
attr_accessor :ssl_version
# Sets or returns the minimum SSL version.
# See {OpenSSL::SSL::SSLContext#min_version=}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-min_version-3D].
# See {OpenSSL::SSL::SSLContext#min_version=}[rdoc-ref:OpenSSL::SSL::SSLContext#min_version-3D].
attr_accessor :min_version
# Sets or returns the maximum SSL version.
# See {OpenSSL::SSL::SSLContext#max_version=}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-max_version-3D].
# See {OpenSSL::SSL::SSLContext#max_version=}[rdoc-ref:OpenSSL::SSL::SSLContext#max_version-3D].
attr_accessor :max_version
# Sets or returns the callback for the server certification verification.
@ -1528,7 +1528,7 @@ module Gem::Net #:nodoc:
# Sets or returns whether to verify that the server certificate is valid
# for the hostname.
# See {OpenSSL::SSL::SSLContext#verify_hostname=}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#attribute-i-verify_mode].
# See {OpenSSL::SSL::SSLContext#verify_hostname=}[rdoc-ref:OpenSSL::SSL::SSLContext#attribute-i-verify_mode].
attr_accessor :verify_hostname
# Returns the X509 certificate chain (an array of strings)
@ -1598,7 +1598,7 @@ module Gem::Net #:nodoc:
end
debug "opening connection to #{conn_addr}:#{conn_port}..."
s = Timeout.timeout(@open_timeout, Gem::Net::OpenTimeout) {
s = Gem::Timeout.timeout(@open_timeout, Gem::Net::OpenTimeout) {
begin
TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
rescue => e
@ -2358,7 +2358,7 @@ module Gem::Net #:nodoc:
Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT,
# avoid a dependency on OpenSSL
defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
Timeout::Error => exception
Gem::Timeout::Error => exception
if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method)
count += 1
@socket.close if @socket

View File

@ -589,7 +589,7 @@ module Gem::Net
HAS_BODY = true
end
# Response class for <tt>Request Timeout</tt> responses (status code 408).
# Response class for <tt>Request Gem::Timeout</tt> responses (status code 408).
#
# The server timed out waiting for the request.
#
@ -980,7 +980,7 @@ module Gem::Net
HAS_BODY = true
end
# Response class for <tt>Gateway Timeout</tt> responses (status code 504).
# Response class for <tt>Gateway Gem::Timeout</tt> responses (status code 504).
#
# The server was acting as a gateway or proxy
# and did not receive a timely response from the upstream server.

View File

@ -20,7 +20,7 @@
#
require 'socket'
require 'timeout'
require_relative '../../../timeout/lib/timeout'
require 'io/wait'
module Gem::Net # :nodoc:
@ -68,16 +68,16 @@ module Gem::Net # :nodoc:
ProtocRetryError = ProtoRetriableError
##
# OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot
# OpenTimeout, a subclass of Gem::Timeout::Error, is raised if a connection cannot
# be created within the open_timeout.
class OpenTimeout < Timeout::Error; end
class OpenTimeout < Gem::Timeout::Error; end
##
# ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the
# ReadTimeout, a subclass of Gem::Timeout::Error, is raised if a chunk of the
# response cannot be read within the read_timeout.
class ReadTimeout < Timeout::Error
class ReadTimeout < Gem::Timeout::Error
def initialize(io = nil)
@io = io
end
@ -93,10 +93,10 @@ module Gem::Net # :nodoc:
end
##
# WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the
# WriteTimeout, a subclass of Gem::Timeout::Error, is raised if a chunk of the
# response cannot be written within the write_timeout. Not raised on Windows.
class WriteTimeout < Timeout::Error
class WriteTimeout < Gem::Timeout::Error
def initialize(io = nil)
@io = io
end

View File

@ -261,7 +261,7 @@ class Gem::RemoteFetcher
end
data
rescue Timeout::Error, IOError, SocketError, SystemCallError,
rescue Gem::Timeout::Error, IOError, SocketError, SystemCallError,
*(OpenSSL::SSL::SSLError if Gem::HAVE_OPENSSL) => e
raise FetchError.new("#{e.class}: #{e}", uri)
end

View File

@ -244,7 +244,7 @@ class Gem::Request
# HACK: work around EOFError bug in Gem::Net::HTTP
# NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible
# to install gems.
rescue EOFError, Timeout::Error,
rescue EOFError, Gem::Timeout::Error,
Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE
requests = @requests[connection.object_id]

3
lib/rubygems/timeout.rb Normal file
View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
require_relative "timeout/lib/timeout"

View File

@ -0,0 +1,199 @@
# frozen_string_literal: true
# Timeout long-running blocks
#
# == Synopsis
#
# require 'rubygems/timeout/lib/timeout'
# status = Gem::Timeout::timeout(5) {
# # Something that should be interrupted if it takes more than 5 seconds...
# }
#
# == Description
#
# Gem::Timeout provides a way to auto-terminate a potentially long-running
# operation if it hasn't finished in a fixed amount of time.
#
# Previous versions didn't use a module for namespacing, however
# #timeout is provided for backwards compatibility. You
# should prefer Gem::Timeout.timeout instead.
#
# == Copyright
#
# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc.
# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan
module Gem::Timeout
VERSION = "0.4.1"
# Internal error raised to when a timeout is triggered.
class ExitException < Exception
def exception(*)
self
end
end
# Raised by Gem::Timeout.timeout when the block times out.
class Error < RuntimeError
def self.handle_timeout(message)
exc = ExitException.new(message)
begin
yield exc
rescue ExitException => e
raise new(message) if exc.equal?(e)
raise
end
end
end
# :stopdoc:
CONDVAR = ConditionVariable.new
QUEUE = Queue.new
QUEUE_MUTEX = Mutex.new
TIMEOUT_THREAD_MUTEX = Mutex.new
@timeout_thread = nil
private_constant :CONDVAR, :QUEUE, :QUEUE_MUTEX, :TIMEOUT_THREAD_MUTEX
class Request
attr_reader :deadline
def initialize(thread, timeout, exception_class, message)
@thread = thread
@deadline = GET_TIME.call(Process::CLOCK_MONOTONIC) + timeout
@exception_class = exception_class
@message = message
@mutex = Mutex.new
@done = false # protected by @mutex
end
def done?
@mutex.synchronize do
@done
end
end
def expired?(now)
now >= @deadline
end
def interrupt
@mutex.synchronize do
unless @done
@thread.raise @exception_class, @message
@done = true
end
end
end
def finished
@mutex.synchronize do
@done = true
end
end
end
private_constant :Request
def self.create_timeout_thread
watcher = Thread.new do
requests = []
while true
until QUEUE.empty? and !requests.empty? # wait to have at least one request
req = QUEUE.pop
requests << req unless req.done?
end
closest_deadline = requests.min_by(&:deadline).deadline
now = 0.0
QUEUE_MUTEX.synchronize do
while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and QUEUE.empty?
CONDVAR.wait(QUEUE_MUTEX, closest_deadline - now)
end
end
requests.each do |req|
req.interrupt if req.expired?(now)
end
requests.reject!(&:done?)
end
end
ThreadGroup::Default.add(watcher) unless watcher.group.enclosed?
watcher.name = "Gem::Timeout stdlib thread"
watcher.thread_variable_set(:"\0__detached_thread__", true)
watcher
end
private_class_method :create_timeout_thread
def self.ensure_timeout_thread_created
unless @timeout_thread and @timeout_thread.alive?
TIMEOUT_THREAD_MUTEX.synchronize do
unless @timeout_thread and @timeout_thread.alive?
@timeout_thread = create_timeout_thread
end
end
end
end
# We keep a private reference so that time mocking libraries won't break
# Gem::Timeout.
GET_TIME = Process.method(:clock_gettime)
private_constant :GET_TIME
# :startdoc:
# Perform an operation in a block, raising an error if it takes longer than
# +sec+ seconds to complete.
#
# +sec+:: Number of seconds to wait for the block to terminate. Any number
# may be used, including Floats to specify fractional seconds. A
# value of 0 or +nil+ will execute the block without any timeout.
# +klass+:: Exception Class to raise if the block fails to terminate
# in +sec+ seconds. Omitting will use the default, Gem::Timeout::Error
# +message+:: Error message to raise with Exception Class.
# Omitting will use the default, "execution expired"
#
# Returns the result of the block *if* the block completed before
# +sec+ seconds, otherwise throws an exception, based on the value of +klass+.
#
# The exception thrown to terminate the given block cannot be rescued inside
# the block unless +klass+ is given explicitly. However, the block can use
# ensure to prevent the handling of the exception. For that reason, this
# method cannot be relied on to enforce timeouts for untrusted blocks.
#
# If a scheduler is defined, it will be used to handle the timeout by invoking
# Scheduler#timeout_after.
#
# Note that this is both a method of module Gem::Timeout, so you can <tt>include
# Gem::Timeout</tt> into your classes so they have a #timeout method, as well as
# a module method, so you can call it directly as Gem::Timeout.timeout().
def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+
return yield(sec) if sec == nil or sec.zero?
message ||= "execution expired"
if Fiber.respond_to?(:current_scheduler) && (scheduler = Fiber.current_scheduler)&.respond_to?(:timeout_after)
return scheduler.timeout_after(sec, klass || Error, message, &block)
end
Gem::Timeout.ensure_timeout_thread_created
perform = Proc.new do |exc|
request = Request.new(Thread.current, sec, exc, message)
QUEUE_MUTEX.synchronize do
QUEUE << request
CONDVAR.signal
end
begin
return yield(sec)
ensure
request.finished
end
end
if klass
perform.call(klass)
else
Error.handle_timeout(message, &perform)
end
end
module_function :timeout
end

View File

@ -554,7 +554,7 @@ PeIQQkFng2VVot/WAQbv3ePqWq07g1BBcwIBAg==
@fetcher = fetcher
def fetcher.fetch_http(uri, mtime = nil, head = nil)
raise Timeout::Error, "timed out"
raise Gem::Timeout::Error, "timed out"
end
url = "http://example.com/uri"
@ -563,7 +563,7 @@ PeIQQkFng2VVot/WAQbv3ePqWq07g1BBcwIBAg==
fetcher.fetch_path url
end
assert_match(/Timeout::Error: timed out \(#{Regexp.escape url}\)\z/,
assert_match(/Gem::Timeout::Error: timed out \(#{Regexp.escape url}\)\z/,
e.message)
assert_equal url, e.uri
end

View File

@ -2,7 +2,7 @@
require_relative "helper"
require "rubygems/request"
require "timeout"
require "rubygems/timeout"
class TestGemRequestConnectionPool < Gem::TestCase
class FakeHttp
@ -141,8 +141,8 @@ class TestGemRequestConnectionPool < Gem::TestCase
pool.checkout
Thread.new do
assert_raise(Timeout::Error) do
Timeout.timeout(1) do
assert_raise(Gem::Timeout::Error) do
Gem::Timeout.timeout(1) do
pool.checkout
end
end

View File

@ -2,7 +2,6 @@
require_relative "helper"
require "rubygems/user_interaction"
require "timeout"
class TestGemSilentUI < Gem::TestCase
def setup

View File

@ -2,7 +2,7 @@
require_relative "helper"
require "rubygems/user_interaction"
require "timeout"
require "rubygems/timeout/lib/timeout"
class TestGemStreamUI < Gem::TestCase
# increase timeout with RJIT for --jit-wait testing
@ -40,7 +40,7 @@ class TestGemStreamUI < Gem::TestCase
end
def test_ask
Timeout.timeout(5) do
Gem::Timeout.timeout(5) do
expected_answer = "Arthur, King of the Britons"
@in.string = "#{expected_answer}\n"
actual_answer = @sui.ask("What is your name?")
@ -51,14 +51,14 @@ class TestGemStreamUI < Gem::TestCase
def test_ask_no_tty
@in.tty = false
Timeout.timeout(SHORT_TIMEOUT) do
Gem::Timeout.timeout(SHORT_TIMEOUT) do
answer = @sui.ask("what is your favorite color?")
assert_nil answer
end
end
def test_ask_for_password
Timeout.timeout(5) do
Gem::Timeout.timeout(5) do
expected_answer = "Arthur, King of the Britons"
@in.string = "#{expected_answer}\n"
actual_answer = @sui.ask_for_password("What is your name?")
@ -69,7 +69,7 @@ class TestGemStreamUI < Gem::TestCase
def test_ask_for_password_no_tty
@in.tty = false
Timeout.timeout(SHORT_TIMEOUT) do
Gem::Timeout.timeout(SHORT_TIMEOUT) do
answer = @sui.ask_for_password("what is the airspeed velocity of an unladen swallow?")
assert_nil answer
end
@ -78,7 +78,7 @@ class TestGemStreamUI < Gem::TestCase
def test_ask_yes_no_no_tty_with_default
@in.tty = false
Timeout.timeout(SHORT_TIMEOUT) do
Gem::Timeout.timeout(SHORT_TIMEOUT) do
answer = @sui.ask_yes_no("do coconuts migrate?", false)
assert_equal false, answer
@ -90,7 +90,7 @@ class TestGemStreamUI < Gem::TestCase
def test_ask_yes_no_no_tty_without_default
@in.tty = false
Timeout.timeout(SHORT_TIMEOUT) do
Gem::Timeout.timeout(SHORT_TIMEOUT) do
assert_raise(Gem::OperationNotSupportedError) do
@sui.ask_yes_no("do coconuts migrate?")
end

View File

@ -45,8 +45,8 @@ class WebauthnPollerTest < Gem::TestCase
end
def test_poll_thread_timeout_error
raise_error = ->(*_args) { raise Timeout::Error, "execution expired" }
Timeout.stub(:timeout, raise_error) do
raise_error = ->(*_args) { raise Gem::Timeout::Error, "execution expired" }
Gem::Timeout.stub(:timeout, raise_error) do
thread = Gem::GemcutterUtilities::WebauthnPoller.poll_thread({}, @host, @webauthn_url, @credentials)
thread.join
assert_equal thread[:error].message, "execution expired"
@ -72,8 +72,8 @@ class WebauthnPollerTest < Gem::TestCase
msg: "OK"
)
assert_raise Timeout::Error do
Timeout.timeout(0.1) do
assert_raise Gem::Timeout::Error do
Gem::Timeout.timeout(0.1) do
Gem::GemcutterUtilities::WebauthnPoller.new({}, @host).poll_for_otp(@webauthn_url, @credentials)
end
end