ethon 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +2 -2
- data/CHANGELOG.md +8 -1
- data/README.md +23 -0
- data/lib/ethon/curls/classes.rb +12 -2
- data/lib/ethon/curls/constants.rb +17 -0
- data/lib/ethon/curls/functions.rb +1 -0
- data/lib/ethon/curls/options.rb +36 -4
- data/lib/ethon/curls/settings.rb +2 -0
- data/lib/ethon/easy/callbacks.rb +2 -1
- data/lib/ethon/easy/informations.rb +22 -1
- data/lib/ethon/easy/operations.rb +1 -15
- data/lib/ethon/easy/response_callbacks.rb +6 -1
- data/lib/ethon/multi/operations.rb +45 -8
- data/lib/ethon/multi.rb +22 -0
- data/lib/ethon/version.rb +1 -1
- data/spec/ethon/easy/callbacks_spec.rb +22 -0
- data/spec/ethon/easy/informations_spec.rb +28 -0
- data/spec/ethon/easy/mirror_spec.rb +2 -1
- data/spec/ethon/easy/operations_spec.rb +3 -0
- data/spec/ethon/multi/options_spec.rb +113 -0
- data/spec/ethon/multi_spec.rb +130 -0
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30f75feeae963db1b4648a0b88e028361c78fda7aec43524a35f93b287ab2305
|
4
|
+
data.tar.gz: eb0ccd002c324dedaaceaa001172a61c520cd58e34d1a92440182faada17df74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b6ec09378cd37ec552caee9a9153fb9e0582a770b18da882ec27f59028799885e1a9d9d7c496f1cfb4803ba3eb14bdd5e508f930cdc9523d16747d2a87ba1dfb
|
7
|
+
data.tar.gz: 77a50827108e8c6bc44f293bae5c2bfe6e871c94f40f93904ff611245143ef7900cf4cc2aa8a1511d0797dd80c03578e06b4e256f929deab544c73f9ef0d9e33
|
data/.github/workflows/ruby.yml
CHANGED
@@ -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
|
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.
|
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)
|
data/lib/ethon/curls/classes.rb
CHANGED
@@ -32,8 +32,18 @@ module Ethon
|
|
32
32
|
|
33
33
|
def clear; self[:fd_count] = 0; end
|
34
34
|
else
|
35
|
-
#
|
36
|
-
FD_SETSIZE =
|
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
|
data/lib/ethon/curls/options.rb
CHANGED
@@ -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, :
|
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, :
|
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
|
data/lib/ethon/curls/settings.rb
CHANGED
@@ -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
|
data/lib/ethon/easy/callbacks.rb
CHANGED
@@ -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,
|
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
|
-
|
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
|
-
@
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
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
@@ -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
|
-
:
|
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
|
data/spec/ethon/multi_spec.rb
CHANGED
@@ -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.
|
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:
|
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.
|
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:
|