ethon 0.14.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87e1825a32b82c558aded416768e7dffc8a1be6918bca888187b343e17159432
4
- data.tar.gz: 4773483d3d4e5bf46298d55c4d4967622666bf28f6665445d621ab2b4de30213
3
+ metadata.gz: 30f75feeae963db1b4648a0b88e028361c78fda7aec43524a35f93b287ab2305
4
+ data.tar.gz: eb0ccd002c324dedaaceaa001172a61c520cd58e34d1a92440182faada17df74
5
5
  SHA512:
6
- metadata.gz: bc51f162c86e89ddc1176368a508870e0be217a11027ae17d93149cbc04ae822d2d03ce6e2fa0e15269bbafefbda9e2dbfd1e64aba0b4e3fddbf820b958a3b82
7
- data.tar.gz: 8c755c396544992d6062fe28f3a7a741c3269941665e89995a5f5060d08b4ed8d4f5768bc0601efa7780b1407c49d1839e4557f3ac591abc0529438ad8506163
6
+ metadata.gz: b6ec09378cd37ec552caee9a9153fb9e0582a770b18da882ec27f59028799885e1a9d9d7c496f1cfb4803ba3eb14bdd5e508f930cdc9523d16747d2a87ba1dfb
7
+ data.tar.gz: 77a50827108e8c6bc44f293bae5c2bfe6e871c94f40f93904ff611245143ef7900cf4cc2aa8a1511d0797dd80c03578e06b4e256f929deab544c73f9ef0d9e33
@@ -20,7 +20,7 @@ jobs:
20
20
  fail-fast: false
21
21
  matrix:
22
22
  os: [ubuntu, macos]
23
- ruby-version: [2.5, 2.6, 2.7, 3.0, head, debug, truffleruby, truffleruby-head]
23
+ ruby-version: [2.5, 2.6, 2.7, 3.0, head, debug, truffleruby]
24
24
  continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
25
25
  steps:
26
26
  - uses: actions/checkout@v2
@@ -30,7 +30,7 @@ jobs:
30
30
  then
31
31
  brew install curl
32
32
  else
33
- sudo apt install -y libcurl4-openssl-dev
33
+ sudo apt update && sudo apt install -y --no-install-recommends libcurl4-openssl-dev
34
34
  fi
35
35
  - name: Set up Ruby
36
36
  uses: ruby/setup-ruby@v1
data/CHANGELOG.md CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  ## Master
4
4
 
5
- [Full Changelog](https://github.com/typhoeus/ethon/compare/v0.12.0...master)
5
+ [Full Changelog](https://github.com/typhoeus/ethon/compare/v0.15.0...master)
6
+
7
+ * Added `redirect_url` value to available informations and `Easy::Mirror`.
8
+ ([Adrien Rey-Jarthon](https://github.com/jarthod)
9
+
10
+ ## 0.15.0
11
+
12
+ [Full Changelog](https://github.com/typhoeus/ethon/compare/v0.14.0...v0.15.0)
6
13
 
7
14
  ## 0.12.0
8
15
 
data/README.md CHANGED
@@ -70,6 +70,29 @@ easy.perform
70
70
  This is really handy when making requests since you don't have to care about setting
71
71
  everything up correctly.
72
72
 
73
+ ## Http2
74
+ Standard http2 servers require the client to connect once and create a session (multi) and then add simple requests to the multi handler.
75
+ The `perform` method then takes all the requests in the multi handler and sends them to the server.
76
+
77
+ See the following example
78
+ ```ruby
79
+ multi = Ethon::Multi.new
80
+ easy = Ethon::Easy.new
81
+
82
+ easy.http_request("www.example.com/get", :get, { http_version: :httpv2_0 })
83
+
84
+ # Sending a request with http version 2 will send an Upgrade header to the server, which many older servers will not support
85
+ # See below for more info: https://everything.curl.dev/http/http2
86
+ # If this is a problem, send the below:
87
+ easy.http_request("www.example.com/get", :get, { http_version: :httpv2_prior_knowledge })
88
+
89
+ # To set the server to use http2 with https and http1 with http, send the following:
90
+ easy.http_request("www.example.com/get", :get, { http_version: :httpv2_tls }
91
+
92
+ multi.add(easy)
93
+ multi.perform
94
+ ```
95
+
73
96
  ## LICENSE
74
97
 
75
98
  (The MIT License)
@@ -32,8 +32,18 @@ module Ethon
32
32
 
33
33
  def clear; self[:fd_count] = 0; end
34
34
  else
35
- # FD Set size.
36
- FD_SETSIZE = ::Ethon::Libc.getdtablesize
35
+ # https://github.com/typhoeus/ethon/issues/182
36
+ FD_SETSIZE = begin
37
+ # Allow to override the (new) default cap
38
+ if ENV['ETHON_FD_SIZE']
39
+ ENV['ETHON_FD_SIZE']
40
+
41
+ # auto-detect ulimit, but cap at 2^16
42
+ else
43
+ [::Ethon::Libc.getdtablesize, 65_536].min
44
+ end
45
+ end
46
+
37
47
  layout :fds_bits, [:long, FD_SETSIZE / ::FFI::Type::LONG.size]
38
48
 
39
49
  # :nodoc:
@@ -59,5 +59,22 @@ module Ethon
59
59
  VERSION_NTLM_WB = (1<<15) # NTLM delegating to winbind helper
60
60
  VERSION_HTTP2 = (1<<16) # HTTP2 support built
61
61
  VERSION_GSSAPI = (1<<17) # GSS-API is supported
62
+
63
+ SOCKET_BAD = -1
64
+ SOCKET_TIMEOUT = SOCKET_BAD
65
+
66
+ PollAction = enum(:poll_action, [
67
+ :none,
68
+ :in,
69
+ :out,
70
+ :inout,
71
+ :remove
72
+ ])
73
+
74
+ SocketReadiness = bitmask(:socket_readiness, [
75
+ :in, # CURL_CSELECT_IN - 0x01 (bit 0)
76
+ :out, # CURL_CSELECT_OUT - 0x02 (bit 1)
77
+ :err, # CURL_CSELECT_ERR - 0x04 (bit 2)
78
+ ])
62
79
  end
63
80
  end
@@ -36,6 +36,7 @@ module Ethon
36
36
  base.attach_function :multi_fdset, :curl_multi_fdset, [:pointer, Curl::FDSet.ptr, Curl::FDSet.ptr, Curl::FDSet.ptr, :pointer], :multi_code
37
37
  base.attach_function :multi_strerror, :curl_multi_strerror, [:int], :string
38
38
  base.attach_function :multi_setopt, :curl_multi_setopt, [:pointer, :multi_option, :varargs], :multi_code
39
+ base.attach_function :multi_socket_action, :curl_multi_socket_action, [:pointer, :int, :socket_readiness, :pointer], :multi_code
39
40
 
40
41
  base.attach_function :version, :curl_version, [], :string
41
42
  base.attach_function :version_info, :curl_version_info, [], Curl::VersionInfoData.ptr
@@ -82,6 +82,12 @@ module Ethon
82
82
  when :callback
83
83
  va_type=:callback
84
84
  raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
85
+ when :socket_callback
86
+ va_type=:socket_callback
87
+ raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
88
+ when :timer_callback
89
+ va_type=:timer_callback
90
+ raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
85
91
  when :debug_callback
86
92
  va_type=:debug_callback
87
93
  raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
@@ -137,6 +143,8 @@ module Ethon
137
143
  :dontuse_object => :objectpoint, # An object we don't support (e.g. FILE*)
138
144
  :cbdata => :objectpoint,
139
145
  :callback => :functionpoint,
146
+ :socket_callback => :functionpoint,
147
+ :timer_callback => :functionpoint,
140
148
  :debug_callback => :functionpoint,
141
149
  :progress_callback => :functionpoint,
142
150
  :off_t => :off_t,
@@ -208,10 +216,10 @@ module Ethon
208
216
  # Documentation @ http://curl.haxx.se/libcurl/c/curl_multi_setopt.html
209
217
  option_type :multi
210
218
 
211
- option :multi, :socketfunction, :callback, 1
219
+ option :multi, :socketfunction, :socket_callback, 1
212
220
  option :multi, :socketdata, :cbdata, 2
213
221
  option :multi, :pipelining, :int, 3
214
- option :multi, :timerfunction, :callback, 4
222
+ option :multi, :timerfunction, :timer_callback, 4
215
223
  option :multi, :timerdata, :cbdata, 5
216
224
  option :multi, :maxconnects, :int, 6
217
225
  option :multi, :max_host_connections, :int, 7
@@ -299,6 +307,7 @@ module Ethon
299
307
  option :easy, :port, :int, 3
300
308
  option :easy, :tcp_nodelay, :bool, 121
301
309
  option :easy, :address_scope, :int, 171
310
+ option :easy, :tcp_fastopen, :bool, 212
302
311
  option :easy, :tcp_keepalive, :bool, 213
303
312
  option :easy, :tcp_keepidle, :int, 214
304
313
  option :easy, :tcp_keepintvl, :int, 215
@@ -344,7 +353,7 @@ module Ethon
344
353
  option :easy, :cookiesession, :bool, 96
345
354
  option :easy, :cookielist, :string, 135
346
355
  option :easy, :httpget, :bool, 80
347
- option :easy, :http_version, :enum, 84, [:none, :httpv1_0, :httpv1_1, :httpv2_0]
356
+ option :easy, :http_version, :enum, 84, [:none, :httpv1_0, :httpv1_1, :httpv2_0, :httpv2_tls, :httpv2_prior_knowledge]
348
357
  option :easy, :ignore_content_length, :bool, 136
349
358
  option :easy, :http_content_decoding, :bool, 158
350
359
  option :easy, :http_transfer_decoding, :bool, 157
@@ -432,7 +441,7 @@ module Ethon
432
441
  option_alias :easy, :keypasswd, :sslkeypasswd
433
442
  option :easy, :sslengine, :string, 89
434
443
  option :easy, :sslengine_default, :none, 90
435
- option :easy, :sslversion, :enum, 32, [:default, :tlsv1, :sslv2, :sslv3, :tlsv1_0, :tlsv1_1, :tlsv1_2]
444
+ option :easy, :sslversion, :enum, 32, [:default, :tlsv1, :sslv2, :sslv3, :tlsv1_0, :tlsv1_1, :tlsv1_2, :tlsv1_3]
436
445
  option :easy, :ssl_verifypeer, :bool, 64
437
446
  option :easy, :cainfo, :string, 65
438
447
  option :easy, :issuercert, :string, 170
@@ -450,6 +459,29 @@ module Ethon
450
459
  option :easy, :gssapi_delegation, :bitmask, 210, [:none, :policy_flag, :flag]
451
460
  option :easy, :pinnedpublickey, :string, 230
452
461
  option_alias :easy, :pinnedpublickey, :pinned_public_key
462
+ ## PROXY SSL OPTIONS
463
+ option :easy, :proxy_cainfo, :string, 246
464
+ option :easy, :proxy_capath, :string, 247
465
+ option :easy, :proxy_ssl_verifypeer, :bool, 248
466
+ option :easy, :proxy_ssl_verifyhost, :int, 249
467
+ option :easy, :proxy_sslversion, :enum, 250, [:default, :tlsv1, :sslv2, :sslv3, :tlsv1_0, :tlsv1_1, :tlsv1_2, :tlsv1_3]
468
+ option :easy, :proxy_tlsauth_username, :string, 251
469
+ option :easy, :proxy_tlsauth_password, :string, 252
470
+ option :easy, :proxy_tlsauth_type, :enum, 253, [:none, :srp]
471
+ option :easy, :proxy_sslcert, :string, 254
472
+ option :easy, :proxy_sslcerttype, :string, 255
473
+ option :easy, :proxy_sslkey, :string, 256
474
+ option :easy, :proxy_sslkeytype, :string, 257
475
+ option :easy, :proxy_keypasswd, :string, 258
476
+ option_alias :easy, :proxy_keypasswd, :proxy_sslcertpasswd
477
+ option_alias :easy, :proxy_keypasswd, :proxy_sslkeypasswd
478
+ option :easy, :proxy_ssl_cipher_list, :string, 259
479
+ option :easy, :proxy_crlfile, :string, 260
480
+ option :easy, :proxy_ssl_options, :bitmask, 261, [nil, :allow_beast]
481
+ option :easy, :pre_proxy, :string, 262
482
+ option :easy, :proxy_pinnedpublickey, :string, 263
483
+ option_alias :easy, :proxy_pinnedpublickey, :proxy_pinned_public_key
484
+ option :easy, :proxy_issuercert, :string, 296
453
485
  ## SSH OPTIONS
454
486
  option :easy, :ssh_auth_types, :bitmask, 151, [:none, :publickey, :password, :host, :keyboard, :agent, {:any => [:all], :default => [:any]}]
455
487
  option :easy, :ssh_host_public_key_md5, :string, 162
@@ -2,6 +2,8 @@
2
2
  module Ethon
3
3
  module Curl
4
4
  callback :callback, [:pointer, :size_t, :size_t, :pointer], :size_t
5
+ callback :socket_callback, [:pointer, :int, :poll_action, :pointer, :pointer], :multi_code
6
+ callback :timer_callback, [:pointer, :long, :pointer], :multi_code
5
7
  callback :debug_callback, [:pointer, :debug_info_type, :pointer, :size_t, :pointer], :int
6
8
  callback :progress_callback, [:pointer, :long_long, :long_long, :long_long, :long_long], :int
7
9
  ffi_lib_flags :now, :global
@@ -52,8 +52,9 @@ module Ethon
52
52
  # @return [ Proc ] The callback.
53
53
  def header_write_callback
54
54
  @header_write_callback ||= proc {|stream, size, num, object|
55
+ result = headers
55
56
  @response_headers << stream.read_string(size * num)
56
- size * num
57
+ result != :abort ? size * num : -1
57
58
  }
58
59
  end
59
60
 
@@ -71,7 +71,28 @@ module Ethon
71
71
 
72
72
  # Return the total number of redirections that were
73
73
  # actually followed.
74
- :redirect_count => :long
74
+ :redirect_count => :long,
75
+
76
+ # URL a redirect would take you to, had you enabled redirects (Added in 7.18.2)
77
+ :redirect_url => :string,
78
+
79
+ # Return the bytes, the total amount of bytes that were uploaded
80
+ :size_upload => :double,
81
+
82
+ # Return the bytes, the total amount of bytes that were downloaded.
83
+ # The amount is only for the latest transfer and will be reset again
84
+ # for each new transfer. This counts actual payload data, what's
85
+ # also commonly called body. All meta and header data are excluded
86
+ # and will not be counted in this number.
87
+ :size_download => :double,
88
+
89
+ # Return the bytes/second, the average upload speed that curl
90
+ # measured for the complete upload
91
+ :speed_upload => :double,
92
+
93
+ # Return the bytes/second, the average download speed that curl
94
+ # measured for the complete download
95
+ :speed_download => :double
75
96
  }
76
97
 
77
98
  AVAILABLE_INFORMATIONS.each do |name, type|
@@ -4,20 +4,6 @@ module Ethon
4
4
  # This module contains the logic to prepare and perform
5
5
  # an easy.
6
6
  module Operations
7
-
8
- class PointerHelper
9
- class<<self
10
- def synchronize( &block )
11
- (@mutex ||= Mutex.new).synchronize( &block )
12
- end
13
-
14
- def release( pointer )
15
- synchronize { Curl.easy_cleanup pointer }
16
- end
17
- end
18
- synchronize{}
19
- end
20
-
21
7
  # Returns a pointer to the curl easy handle.
22
8
  #
23
9
  # @example Return the handle.
@@ -25,7 +11,7 @@ module Ethon
25
11
  #
26
12
  # @return [ FFI::Pointer ] A pointer to the curl easy handle.
27
13
  def handle
28
- @handle ||= FFI::AutoPointer.new(Curl.easy_init, PointerHelper.method(:release) )
14
+ @handle ||= FFI::AutoPointer.new(Curl.easy_init, Curl.method(:easy_cleanup))
29
15
  end
30
16
 
31
17
  # Sets a pointer to the curl easy handle.
@@ -43,7 +43,12 @@ module Ethon
43
43
  return if @headers_called
44
44
  @headers_called = true
45
45
  if defined?(@on_headers) and not @on_headers.nil?
46
- @on_headers.each{ |callback| callback.call(self) }
46
+ result = nil
47
+ @on_headers.each do |callback|
48
+ result = callback.call(self)
49
+ break if result == :abort
50
+ end
51
+ result
47
52
  end
48
53
  end
49
54
 
@@ -24,12 +24,16 @@ module Ethon
24
24
  #
25
25
  # @return [ void ]
26
26
  def init_vars
27
- @timeout = ::FFI::MemoryPointer.new(:long)
28
- @timeval = Curl::Timeval.new
29
- @fd_read = Curl::FDSet.new
30
- @fd_write = Curl::FDSet.new
31
- @fd_excep = Curl::FDSet.new
32
- @max_fd = ::FFI::MemoryPointer.new(:int)
27
+ if @execution_mode == :perform
28
+ @timeout = ::FFI::MemoryPointer.new(:long)
29
+ @timeval = Curl::Timeval.new
30
+ @fd_read = Curl::FDSet.new
31
+ @fd_write = Curl::FDSet.new
32
+ @fd_excep = Curl::FDSet.new
33
+ @max_fd = ::FFI::MemoryPointer.new(:int)
34
+ elsif @execution_mode == :socket_action
35
+ @running_count_pointer = FFI::MemoryPointer.new(:int)
36
+ end
33
37
  end
34
38
 
35
39
  # Perform multi.
@@ -39,6 +43,8 @@ module Ethon
39
43
  # @example Perform multi.
40
44
  # multi.perform
41
45
  def perform
46
+ ensure_execution_mode(:perform)
47
+
42
48
  Ethon.logger.debug(STARTED_MULTI)
43
49
  while ongoing?
44
50
  run
@@ -67,9 +73,38 @@ module Ethon
67
73
  )
68
74
  end
69
75
 
70
- private
76
+ # Continue execution with an external IO loop.
77
+ #
78
+ # @example When no sockets are ready yet, or to begin.
79
+ # multi.socket_action
80
+ #
81
+ # @example When a socket is readable
82
+ # multi.socket_action(io_object, [:in])
83
+ #
84
+ # @example When a socket is readable and writable
85
+ # multi.socket_action(io_object, [:in, :out])
86
+ #
87
+ # @return [ Symbol ] The Curl.multi_socket_action return code.
88
+ def socket_action(io = nil, readiness = 0)
89
+ ensure_execution_mode(:socket_action)
90
+
91
+ fd = if io.nil?
92
+ ::Ethon::Curl::SOCKET_TIMEOUT
93
+ elsif io.is_a?(Integer)
94
+ io
95
+ else
96
+ io.fileno
97
+ end
98
+
99
+ code = Curl.multi_socket_action(handle, fd, readiness, @running_count_pointer)
100
+ @running_count = @running_count_pointer.read_int
71
101
 
72
- # Return wether the multi still requests or not.
102
+ check
103
+
104
+ code
105
+ end
106
+
107
+ # Return whether the multi still contains requests or not.
73
108
  #
74
109
  # @example Return if ongoing.
75
110
  # multi.ongoing?
@@ -79,6 +114,8 @@ module Ethon
79
114
  easy_handles.size > 0 || (!defined?(@running_count) || running_count > 0)
80
115
  end
81
116
 
117
+ private
118
+
82
119
  # Get timeout.
83
120
  #
84
121
  # @example Get timeout.
data/lib/ethon/multi.rb CHANGED
@@ -74,10 +74,21 @@ module Ethon
74
74
  # is 1 will not be treated as unlimited. Instead it will open only 1
75
75
  # connection and try to pipeline on it.
76
76
  # (Added in 7.30.0)
77
+ # @option options :execution_mode [Boolean]
78
+ # Either :perform (default) or :socket_action, specifies the usage
79
+ # method that will be used on this multi object. The default :perform
80
+ # mode provides a #perform function that uses curl_multi_perform
81
+ # behind the scenes to automatically continue execution until all
82
+ # requests have completed. The :socket_action mode provides an API
83
+ # that allows the {Multi} object to be integrated into an external
84
+ # IO loop, by calling #socket_action and responding to the
85
+ # socketfunction and timerfunction callbacks, using the underlying
86
+ # curl_multi_socket_action semantics.
77
87
  #
78
88
  # @return [ Multi ] The new multi.
79
89
  def initialize(options = {})
80
90
  Curl.init
91
+ @execution_mode = options.delete(:execution_mode) || :perform
81
92
  set_attributes(options)
82
93
  init_vars
83
94
  end
@@ -100,5 +111,16 @@ module Ethon
100
111
  method("#{key}=").call(value)
101
112
  end
102
113
  end
114
+
115
+ private
116
+
117
+ # Internal function to gate functions to a specific execution mode
118
+ #
119
+ # @raise ArgumentError
120
+ #
121
+ # @api private
122
+ def ensure_execution_mode(expected_mode)
123
+ raise ArgumentError, "Expected the Multi to be in #{expected_mode} but it was in #{@execution_mode}" if expected_mode != @execution_mode
124
+ end
103
125
  end
104
126
  end
data/lib/ethon/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  module Ethon
3
3
 
4
4
  # Ethon version.
5
- VERSION = '0.14.0'
5
+ VERSION = '0.16.0'
6
6
  end
@@ -56,4 +56,26 @@ describe Ethon::Easy::Callbacks do
56
56
  end
57
57
  end
58
58
  end
59
+
60
+ describe "#header_write_callback" do
61
+ let(:header_write_callback) { easy.instance_variable_get(:@header_write_callback) }
62
+ let(:stream) { double(:read_string => "") }
63
+ context "when header returns not :abort" do
64
+ it "returns number bigger than 0" do
65
+ expect(header_write_callback.call(stream, 1, 1, nil) > 0).to be(true)
66
+ end
67
+ end
68
+
69
+ context "when header returns :abort" do
70
+ before do
71
+ easy.on_headers.clear
72
+ easy.on_headers { :abort }
73
+ end
74
+ let(:header_write_callback) { easy.instance_variable_get(:@header_write_callback) }
75
+
76
+ it "returns -1 to indicate abort to libcurl" do
77
+ expect(header_write_callback.call(stream, 1, 1, nil)).to eq(-1)
78
+ end
79
+ end
80
+ end
59
81
  end
@@ -81,6 +81,12 @@ describe Ethon::Easy::Informations do
81
81
  end
82
82
  end
83
83
 
84
+ describe "#redirect_url" do
85
+ it "returns nil as there is no redirect" do
86
+ expect(easy.redirect_url).to be(nil)
87
+ end
88
+ end
89
+
84
90
  describe "#request_size" do
85
91
  it "returns 53" do
86
92
  expect(easy.request_size).to eq(53)
@@ -94,5 +100,27 @@ describe Ethon::Easy::Informations do
94
100
  end
95
101
  end
96
102
 
103
+ describe "#size_upload" do
104
+ it "returns float" do
105
+ expect(easy.size_upload).to be_a(Float)
106
+ end
107
+ end
97
108
 
109
+ describe "#size_download" do
110
+ it "returns float" do
111
+ expect(easy.size_download).to be_a(Float)
112
+ end
113
+ end
114
+
115
+ describe "#speed_upload" do
116
+ it "returns float" do
117
+ expect(easy.speed_upload).to be_a(Float)
118
+ end
119
+ end
120
+
121
+ describe "#speed_download" do
122
+ it "returns float" do
123
+ expect(easy.speed_download).to be_a(Float)
124
+ end
125
+ end
98
126
  end
@@ -10,7 +10,8 @@ describe Ethon::Easy::Mirror do
10
10
  :return_code, :response_code, :response_body, :response_headers,
11
11
  :total_time, :starttransfer_time, :appconnect_time,
12
12
  :pretransfer_time, :connect_time, :namelookup_time, :redirect_time,
13
- :effective_url, :primary_ip, :redirect_count, :debug_info
13
+ :size_upload, :size_download, :speed_upload, :speed_upload,
14
+ :effective_url, :primary_ip, :redirect_count, :redirect_url, :debug_info
14
15
  ].each do |name|
15
16
  it "contains #{name}" do
16
17
  expect(described_class::INFORMATIONS_TO_MIRROR).to include(name)
@@ -106,6 +106,7 @@ describe Ethon::Easy::Operations do
106
106
 
107
107
  it "doesn't follow" do
108
108
  expect(easy.response_code).to eq(302)
109
+ expect(easy.redirect_url).to eq("http://localhost:3001/")
109
110
  end
110
111
  end
111
112
 
@@ -115,6 +116,7 @@ describe Ethon::Easy::Operations do
115
116
 
116
117
  it "follows" do
117
118
  expect(easy.response_code).to eq(200)
119
+ expect(easy.redirect_url).to eq(nil)
118
120
  end
119
121
 
120
122
  context "when infinite redirect loop" do
@@ -124,6 +126,7 @@ describe Ethon::Easy::Operations do
124
126
  context "when max redirect set" do
125
127
  it "follows only x times" do
126
128
  expect(easy.response_code).to eq(302)
129
+ expect(easy.redirect_url).to eq("http://localhost:3001/bad_redirect")
127
130
  end
128
131
  end
129
132
  end
@@ -20,6 +20,119 @@ describe Ethon::Multi::Options do
20
20
  end
21
21
  end
22
22
 
23
+ context "socket_action mode" do
24
+ let(:multi) { Ethon::Multi.new(execution_mode: :socket_action) }
25
+
26
+ describe "#socketfunction callbacks" do
27
+ it "allows multi_code return values" do
28
+ calls = []
29
+ multi.socketfunction = proc do |handle, sock, what, userp, socketp|
30
+ calls << what
31
+ :ok
32
+ end
33
+
34
+ easy = Ethon::Easy.new
35
+ easy.url = "http://localhost:3001/?delay=1"
36
+ multi.add(easy)
37
+ expect(calls).to eq([])
38
+ 5.times do
39
+ multi.socket_action
40
+ break unless calls.empty?
41
+ sleep 0.1
42
+ end
43
+ expect(calls.last).to eq(:in).or(eq(:out))
44
+ multi.delete(easy)
45
+ expect(calls.last).to eq(:remove)
46
+ end
47
+
48
+ it "allows integer return values (compatibility)" do
49
+ called = false
50
+ multi.socketfunction = proc do |handle, sock, what, userp, socketp|
51
+ called = true
52
+ 0
53
+ end
54
+
55
+ easy = Ethon::Easy.new
56
+ easy.url = "http://localhost:3001/?delay=1"
57
+ multi.add(easy)
58
+ 5.times do
59
+ multi.socket_action
60
+ break if called
61
+ sleep 0.1
62
+ end
63
+ multi.delete(easy)
64
+
65
+ expect(called).to be_truthy
66
+ end
67
+
68
+ it "errors on invalid return codes" do
69
+ called = false
70
+ multi.socketfunction = proc do |handle, sock, what, userp, socketp|
71
+ called = true
72
+ "hi"
73
+ end
74
+
75
+ easy = Ethon::Easy.new
76
+ easy.url = "http://localhost:3001/?delay=1"
77
+ multi.add(easy)
78
+ expect {
79
+ 5.times do
80
+ multi.socket_action
81
+ break if called
82
+ sleep 0.1
83
+ end
84
+ }.to raise_error(ArgumentError)
85
+ expect { multi.delete(easy) }.to raise_error(ArgumentError)
86
+ end
87
+ end
88
+
89
+ describe "#timerfunction callbacks" do
90
+ it "allows multi_code return values" do
91
+ calls = []
92
+ multi.timerfunction = proc do |handle, timeout_ms, userp|
93
+ calls << timeout_ms
94
+ :ok
95
+ end
96
+
97
+ easy = Ethon::Easy.new
98
+ easy.url = "http://localhost:3001/?delay=1"
99
+ multi.add(easy)
100
+ expect(calls.last).to be >= 0 # adds an immediate timeout
101
+
102
+ multi.delete(easy)
103
+ expect(calls.last).to eq(-1) # cancels the timer
104
+ end
105
+
106
+ it "allows integer return values (compatibility)" do
107
+ called = false
108
+ multi.timerfunction = proc do |handle, timeout_ms, userp|
109
+ called = true
110
+ 0
111
+ end
112
+
113
+ easy = Ethon::Easy.new
114
+ easy.url = "http://localhost:3001/?delay=1"
115
+ multi.add(easy)
116
+ multi.socket_action
117
+ multi.delete(easy)
118
+
119
+ expect(called).to be_truthy
120
+ end
121
+
122
+ it "errors on invalid return codes" do
123
+ called = false
124
+ multi.timerfunction = proc do |handle, timeout_ms, userp|
125
+ called = true
126
+ "hi"
127
+ end
128
+
129
+ easy = Ethon::Easy.new
130
+ easy.url = "http://localhost:3001/?delay=1"
131
+ expect { multi.add(easy) }.to raise_error(ArgumentError)
132
+ end
133
+ end
134
+ end
135
+
23
136
  describe "#value_for" do
24
137
  context "when option in bool" do
25
138
  context "when value true" do
@@ -8,6 +8,16 @@ describe Ethon::Multi do
8
8
  Ethon::Multi.new
9
9
  end
10
10
 
11
+ context "with default options" do
12
+ it "allows running #perform with the default execution_mode" do
13
+ Ethon::Multi.new.perform
14
+ end
15
+
16
+ it "refuses to run #socket_action" do
17
+ expect { Ethon::Multi.new.socket_action }.to raise_error(ArgumentError)
18
+ end
19
+ end
20
+
11
21
  context "when options not empty" do
12
22
  context "when pipelining is set" do
13
23
  let(:options) { { :pipelining => true } }
@@ -17,6 +27,126 @@ describe Ethon::Multi do
17
27
  Ethon::Multi.new(options)
18
28
  end
19
29
  end
30
+
31
+ context "when execution_mode option is :socket_action" do
32
+ let(:options) { { :execution_mode => :socket_action } }
33
+ let(:multi) { Ethon::Multi.new(options) }
34
+
35
+ it "refuses to run #perform" do
36
+ expect { multi.perform }.to raise_error(ArgumentError)
37
+ end
38
+
39
+ it "allows running #socket_action" do
40
+ multi.socket_action
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "#socket_action" do
47
+ let(:options) { { :execution_mode => :socket_action } }
48
+ let(:select_state) { { :readers => [], :writers => [], :timeout => 0 } }
49
+ let(:multi) {
50
+ multi = Ethon::Multi.new(options)
51
+ multi.timerfunction = proc do |handle, timeout_ms, userp|
52
+ timeout_ms = nil if timeout_ms == -1
53
+ select_state[:timeout] = timeout_ms
54
+ :ok
55
+ end
56
+ multi.socketfunction = proc do |handle, sock, what, userp, socketp|
57
+ case what
58
+ when :remove
59
+ select_state[:readers].delete(sock)
60
+ select_state[:writers].delete(sock)
61
+ when :in
62
+ select_state[:readers].push(sock) unless select_state[:readers].include? sock
63
+ select_state[:writers].delete(sock)
64
+ when :out
65
+ select_state[:readers].delete(sock)
66
+ select_state[:writers].push(sock) unless select_state[:writers].include? sock
67
+ when :inout
68
+ select_state[:readers].push(sock) unless select_state[:readers].include? sock
69
+ select_state[:writers].push(sock) unless select_state[:writers].include? sock
70
+ else
71
+ raise ArgumentError, "invalid value for 'what' in socketfunction callback"
72
+ end
73
+ :ok
74
+ end
75
+ multi
76
+ }
77
+
78
+ def fds_to_ios(fds)
79
+ fds.map do |fd|
80
+ IO.for_fd(fd).tap { |io| io.autoclose = false }
81
+ end
82
+ end
83
+
84
+ def perform_socket_action_until_complete
85
+ multi.socket_action # start things off
86
+
87
+ while multi.ongoing?
88
+ readers, writers, _ = IO.select(
89
+ fds_to_ios(select_state[:readers]),
90
+ fds_to_ios(select_state[:writers]),
91
+ [],
92
+ select_state[:timeout]
93
+ )
94
+
95
+ to_notify = Hash.new { |hash, key| hash[key] = [] }
96
+ unless readers.nil?
97
+ readers.each do |reader|
98
+ to_notify[reader] << :in
99
+ end
100
+ end
101
+ unless writers.nil?
102
+ writers.each do |writer|
103
+ to_notify[writer] << :out
104
+ end
105
+ end
106
+
107
+ to_notify.each do |io, readiness|
108
+ multi.socket_action(io, readiness)
109
+ end
110
+
111
+ # if we didn't have anything to notify, then we timed out
112
+ multi.socket_action if to_notify.empty?
113
+ end
114
+ ensure
115
+ multi.easy_handles.dup.each do |h|
116
+ multi.delete(h)
117
+ end
118
+ end
119
+
120
+ it "supports an end-to-end request" do
121
+ easy = Ethon::Easy.new
122
+ easy.url = "http://localhost:3001/"
123
+ multi.add(easy)
124
+
125
+ perform_socket_action_until_complete
126
+
127
+ expect(multi.ongoing?).to eq(false)
128
+ end
129
+
130
+ it "supports multiple concurrent requests" do
131
+ handles = []
132
+ 10.times do
133
+ easy = Ethon::Easy.new
134
+ easy.url = "http://localhost:3001/?delay=1"
135
+ multi.add(easy)
136
+ handles << easy
137
+ end
138
+
139
+ start = Time.now
140
+ perform_socket_action_until_complete
141
+ duration = Time.now - start
142
+
143
+ # these should have happened concurrently
144
+ expect(duration).to be < 2
145
+ expect(multi.ongoing?).to eq(false)
146
+
147
+ handles.each do |handle|
148
+ expect(handle.response_code).to eq(200)
149
+ end
20
150
  end
21
151
  end
22
152
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ethon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hans Hasselberg
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-26 00:00:00.000000000 Z
11
+ date: 2022-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -137,7 +137,7 @@ homepage: https://github.com/typhoeus/ethon
137
137
  licenses:
138
138
  - MIT
139
139
  metadata: {}
140
- post_install_message:
140
+ post_install_message:
141
141
  rdoc_options: []
142
142
  require_paths:
143
143
  - lib
@@ -152,8 +152,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
152
  - !ruby/object:Gem::Version
153
153
  version: 1.3.6
154
154
  requirements: []
155
- rubygems_version: 3.0.3
156
- signing_key:
155
+ rubygems_version: 3.3.7
156
+ signing_key:
157
157
  specification_version: 4
158
158
  summary: Libcurl wrapper.
159
159
  test_files: