ethon 0.18.0 → 0.19.1
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/CHANGELOG.md +705 -2
- data/README.md +183 -40
- data/lib/ethon/curl.rb +22 -0
- data/lib/ethon/curls/classes.rb +284 -12
- data/lib/ethon/curls/codes.rb +28 -9
- data/lib/ethon/curls/constants.rb +61 -4
- data/lib/ethon/curls/form_options.rb +2 -1
- data/lib/ethon/curls/functions.rb +60 -11
- data/lib/ethon/curls/infos.rb +104 -5
- data/lib/ethon/curls/introspection.rb +139 -0
- data/lib/ethon/curls/introspection_constants.rb +42 -0
- data/lib/ethon/curls/mime.rb +187 -0
- data/lib/ethon/curls/optional.rb +14 -0
- data/lib/ethon/curls/options.rb +298 -71
- data/lib/ethon/curls/settings.rb +12 -3
- data/lib/ethon/curls/share.rb +165 -0
- data/lib/ethon/curls/share_constants.rb +47 -0
- data/lib/ethon/curls/url.rb +171 -0
- data/lib/ethon/curls/url_constants.rb +78 -0
- data/lib/ethon/easy/callbacks.rb +177 -51
- data/lib/ethon/easy/features.rb +4 -1
- data/lib/ethon/easy/files.rb +113 -0
- data/lib/ethon/easy/form.rb +65 -6
- data/lib/ethon/easy/header.rb +57 -15
- data/lib/ethon/easy/headers_api.rb +79 -0
- data/lib/ethon/easy/http/actionable.rb +25 -3
- data/lib/ethon/easy/http/get.rb +5 -1
- data/lib/ethon/easy/http/post.rb +1 -0
- data/lib/ethon/easy/http/postable.rb +9 -2
- data/lib/ethon/easy/http/putable.rb +19 -4
- data/lib/ethon/easy/http.rb +16 -4
- data/lib/ethon/easy/informations.rb +51 -2
- data/lib/ethon/easy/mime.rb +29 -0
- data/lib/ethon/easy/mirror.rb +17 -4
- data/lib/ethon/easy/operations.rb +264 -11
- data/lib/ethon/easy/options.rb +327 -8
- data/lib/ethon/easy/queryable.rb +59 -14
- data/lib/ethon/easy/response_callbacks.rb +33 -12
- data/lib/ethon/easy/share.rb +28 -0
- data/lib/ethon/easy/url.rb +32 -0
- data/lib/ethon/easy/util.rb +19 -0
- data/lib/ethon/easy.rb +185 -10
- data/lib/ethon/errors/fork_safety.rb +10 -0
- data/lib/ethon/errors/form_add.rb +12 -0
- data/lib/ethon/errors/global_init.rb +1 -1
- data/lib/ethon/errors/header.rb +12 -0
- data/lib/ethon/errors/invalid_option.rb +3 -1
- data/lib/ethon/errors/invalid_value.rb +3 -1
- data/lib/ethon/errors/mime.rb +17 -0
- data/lib/ethon/errors/multi_add.rb +1 -1
- data/lib/ethon/errors/multi_assign.rb +12 -0
- data/lib/ethon/errors/multi_fdset.rb +1 -1
- data/lib/ethon/errors/multi_perform.rb +12 -0
- data/lib/ethon/errors/multi_poll.rb +12 -0
- data/lib/ethon/errors/multi_remove.rb +1 -1
- data/lib/ethon/errors/multi_timeout.rb +2 -2
- data/lib/ethon/errors/multi_wait.rb +12 -0
- data/lib/ethon/errors/multi_wakeup.rb +12 -0
- data/lib/ethon/errors/select.rb +1 -1
- data/lib/ethon/errors/set_option.rb +27 -0
- data/lib/ethon/errors/share.rb +17 -0
- data/lib/ethon/errors/unsupported_option.rb +23 -0
- data/lib/ethon/errors/url.rb +17 -0
- data/lib/ethon/errors.rb +13 -0
- data/lib/ethon/libc.rb +30 -1
- data/lib/ethon/loggable.rb +1 -1
- data/lib/ethon/multi/operations.rb +269 -25
- data/lib/ethon/multi/options.rb +114 -12
- data/lib/ethon/multi/stack.rb +181 -10
- data/lib/ethon/multi.rb +51 -15
- data/lib/ethon/version.rb +1 -1
- data/lib/ethon.rb +8 -9
- metadata +35 -6
|
@@ -5,7 +5,7 @@ module Ethon
|
|
|
5
5
|
# Raises when multi_remove_handle failed.
|
|
6
6
|
class MultiRemove < EthonError
|
|
7
7
|
def initialize(code, easy)
|
|
8
|
-
super("An error
|
|
8
|
+
super("An error occurred removing the easy handle: #{easy} from the multi: #{code}")
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -5,8 +5,8 @@ module Ethon
|
|
|
5
5
|
# Raised when multi_timeout failed.
|
|
6
6
|
class MultiTimeout < EthonError
|
|
7
7
|
def initialize(code)
|
|
8
|
-
super("An error
|
|
9
|
-
# "An error
|
|
8
|
+
super("An error occurred getting the timeout: #{code}")
|
|
9
|
+
# "An error occurred getting the timeout: #{code}: #{Curl.multi_strerror(code)}"
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
end
|
data/lib/ethon/errors/select.rb
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Ethon
|
|
3
|
+
module Errors
|
|
4
|
+
|
|
5
|
+
# Raised when libcurl rejects an option value.
|
|
6
|
+
class SetOption < EthonError
|
|
7
|
+
def initialize(option, code, type = :easy)
|
|
8
|
+
message = begin
|
|
9
|
+
strerror(type, code)
|
|
10
|
+
rescue StandardError
|
|
11
|
+
code.to_s
|
|
12
|
+
end
|
|
13
|
+
# Inspect so control characters in an untrusted option name cannot
|
|
14
|
+
# inject lines into logs that record the message.
|
|
15
|
+
super("libcurl failed to set the option: #{option.inspect}: #{message} (#{code})")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Multi and easy codes overlap, so decode with the matching strerror.
|
|
21
|
+
def strerror(type, code)
|
|
22
|
+
func = type == :multi ? :multi_strerror : :easy_strerror
|
|
23
|
+
Ethon::Curl.respond_to?(func) ? Ethon::Curl.public_send(func, code) : code.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Ethon
|
|
3
|
+
module Errors
|
|
4
|
+
|
|
5
|
+
# Raised when a curl share API call fails.
|
|
6
|
+
class Share < EthonError
|
|
7
|
+
def initialize(code)
|
|
8
|
+
message = if Ethon::Curl.respond_to?(:share_strerror)
|
|
9
|
+
Ethon::Curl.share_strerror(code)
|
|
10
|
+
else
|
|
11
|
+
code.to_s
|
|
12
|
+
end
|
|
13
|
+
super("An error occurred handling the share: #{message}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Ethon
|
|
3
|
+
module Errors
|
|
4
|
+
|
|
5
|
+
# Raised when libcurl does not recognize an option.
|
|
6
|
+
#
|
|
7
|
+
# This happens when Ethon knows an option that the loaded libcurl is too
|
|
8
|
+
# old to support, for example an option added in a newer libcurl release.
|
|
9
|
+
class UnsupportedOption < EthonError
|
|
10
|
+
def initialize(option)
|
|
11
|
+
version = begin
|
|
12
|
+
Ethon::Curl.version_info[:version]
|
|
13
|
+
rescue StandardError
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
version = Ethon::Curl.version if version.nil?
|
|
17
|
+
# Inspect so control characters in an untrusted option name cannot
|
|
18
|
+
# inject lines into logs that record the message.
|
|
19
|
+
super("libcurl #{version} does not support the option: #{option.inspect}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Ethon
|
|
3
|
+
module Errors
|
|
4
|
+
|
|
5
|
+
# Raised when a curl URL API call fails.
|
|
6
|
+
class Url < EthonError
|
|
7
|
+
def initialize(code)
|
|
8
|
+
message = if Ethon::Curl.respond_to?(:url_strerror)
|
|
9
|
+
Ethon::Curl.url_strerror(code)
|
|
10
|
+
else
|
|
11
|
+
code.to_s
|
|
12
|
+
end
|
|
13
|
+
super("An error occurred handling the URL: #{message}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/ethon/errors.rb
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'ethon/errors/ethon_error'
|
|
3
|
+
require 'ethon/errors/url'
|
|
3
4
|
require 'ethon/errors/global_init'
|
|
5
|
+
require 'ethon/errors/form_add'
|
|
4
6
|
require 'ethon/errors/multi_timeout'
|
|
7
|
+
require 'ethon/errors/multi_assign'
|
|
5
8
|
require 'ethon/errors/multi_fdset'
|
|
9
|
+
require 'ethon/errors/share'
|
|
6
10
|
require 'ethon/errors/multi_add'
|
|
7
11
|
require 'ethon/errors/multi_remove'
|
|
12
|
+
require 'ethon/errors/header'
|
|
13
|
+
require 'ethon/errors/multi_perform'
|
|
8
14
|
require 'ethon/errors/select'
|
|
9
15
|
require 'ethon/errors/invalid_option'
|
|
10
16
|
require 'ethon/errors/invalid_value'
|
|
17
|
+
require 'ethon/errors/set_option'
|
|
18
|
+
require 'ethon/errors/unsupported_option'
|
|
19
|
+
require 'ethon/errors/mime'
|
|
20
|
+
require 'ethon/errors/fork_safety'
|
|
21
|
+
require 'ethon/errors/multi_wait'
|
|
22
|
+
require 'ethon/errors/multi_poll'
|
|
23
|
+
require 'ethon/errors/multi_wakeup'
|
|
11
24
|
|
|
12
25
|
module Ethon
|
|
13
26
|
|
data/lib/ethon/libc.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'etc'
|
|
3
|
+
|
|
2
4
|
module Ethon
|
|
3
5
|
|
|
4
6
|
# FFI Wrapper module for Libc.
|
|
@@ -13,8 +15,35 @@ module Ethon
|
|
|
13
15
|
Gem.win_platform?
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
def self.open_max
|
|
19
|
+
value = Etc.sysconf(Etc::SC_OPEN_MAX)
|
|
20
|
+
return value if value && value > 0
|
|
21
|
+
|
|
22
|
+
Process.getrlimit(:NOFILE).first
|
|
23
|
+
rescue NameError, NotImplementedError
|
|
24
|
+
Process.getrlimit(:NOFILE).first
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Size of the native fd_set, rounded up to whole longs. An unaligned
|
|
28
|
+
# ETHON_FD_SIZE would make select write past the Ruby-side struct.
|
|
29
|
+
def self.fd_size(value = ENV['ETHON_FD_SIZE'])
|
|
30
|
+
value = value.to_s
|
|
31
|
+
size = if value.empty?
|
|
32
|
+
[open_max, 65_536].min
|
|
33
|
+
else
|
|
34
|
+
begin
|
|
35
|
+
Integer(value, 10)
|
|
36
|
+
rescue ArgumentError, TypeError
|
|
37
|
+
raise ArgumentError, "ETHON_FD_SIZE must be a positive integer"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
raise ArgumentError, "ETHON_FD_SIZE must be a positive integer" unless size > 0
|
|
41
|
+
|
|
42
|
+
word = FFI::Type::LONG.size
|
|
43
|
+
((size + word - 1) / word) * word
|
|
44
|
+
end
|
|
45
|
+
|
|
16
46
|
unless windows?
|
|
17
|
-
attach_function :getdtablesize, [], :int
|
|
18
47
|
attach_function :free, [:pointer], :void
|
|
19
48
|
end
|
|
20
49
|
end
|
data/lib/ethon/loggable.rb
CHANGED
|
@@ -14,7 +14,68 @@ module Ethon
|
|
|
14
14
|
#
|
|
15
15
|
# @return [ FFI::Pointer ] The multi handle.
|
|
16
16
|
def handle
|
|
17
|
-
|
|
17
|
+
ensure_not_cleaned_up!
|
|
18
|
+
|
|
19
|
+
handle = @handle ||= begin
|
|
20
|
+
# Enforce fork safety before creating a handle for a Multi that was
|
|
21
|
+
# constructed before the fork but never touched.
|
|
22
|
+
Curl.check_pid!
|
|
23
|
+
pointer = Curl.multi_init
|
|
24
|
+
raise NoMemoryError, "curl_multi_init failed" if pointer.null?
|
|
25
|
+
Curl::MultiHandle.new(pointer)
|
|
26
|
+
end
|
|
27
|
+
handle.check_pid!
|
|
28
|
+
handle
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Clean up the multi.
|
|
32
|
+
#
|
|
33
|
+
# Removes all attached easy handles before freeing the native multi
|
|
34
|
+
# handle. Cleanup is terminal: the Multi cannot be used afterwards.
|
|
35
|
+
#
|
|
36
|
+
# @return [ nil ]
|
|
37
|
+
def cleanup
|
|
38
|
+
ensure_not_cleaned_up!
|
|
39
|
+
ensure_not_executing!
|
|
40
|
+
# A handle created before a fork must not be released in the child.
|
|
41
|
+
@handle.check_pid! if @handle
|
|
42
|
+
|
|
43
|
+
error = nil
|
|
44
|
+
begin
|
|
45
|
+
stack.dup.each do |easy|
|
|
46
|
+
begin
|
|
47
|
+
delete(easy)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
# Keep detaching the other handles. A failed close or native
|
|
50
|
+
# detach must not strand every remaining handle on a Multi whose
|
|
51
|
+
# native handle is about to be freed.
|
|
52
|
+
error ||= e
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
ensure
|
|
56
|
+
# Cleanup is terminal. Release the native handle even when detaching
|
|
57
|
+
# one of the easy handles fails. Handles that could not be detached
|
|
58
|
+
# keep their multi ownership so they cannot be reused unsafely.
|
|
59
|
+
begin
|
|
60
|
+
@handle.free if @handle
|
|
61
|
+
ensure
|
|
62
|
+
stack.each do |easy|
|
|
63
|
+
begin
|
|
64
|
+
easy.finish_response_body_io
|
|
65
|
+
rescue StandardError
|
|
66
|
+
# Cleanup is best effort; keep tearing down the multi even if
|
|
67
|
+
# closing a streamed response fails.
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
stack.clear
|
|
71
|
+
@handle = nil
|
|
72
|
+
@running_count = nil
|
|
73
|
+
@assigned_sockets = nil
|
|
74
|
+
@procs = nil
|
|
75
|
+
@cleaned_up = true
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
raise error if error
|
|
18
79
|
end
|
|
19
80
|
|
|
20
81
|
# Initialize variables.
|
|
@@ -26,13 +87,26 @@ module Ethon
|
|
|
26
87
|
def init_vars
|
|
27
88
|
if @execution_mode == :perform
|
|
28
89
|
@timeout = ::FFI::MemoryPointer.new(:long)
|
|
29
|
-
@
|
|
30
|
-
@
|
|
31
|
-
@
|
|
32
|
-
|
|
33
|
-
|
|
90
|
+
@running_count_pointer = ::FFI::MemoryPointer.new(:int)
|
|
91
|
+
@msgs_left = ::FFI::MemoryPointer.new(:int)
|
|
92
|
+
@wait_strategy = if Curl.respond_to?(:multi_poll)
|
|
93
|
+
:poll
|
|
94
|
+
elsif Curl.respond_to?(:multi_wait)
|
|
95
|
+
:wait
|
|
96
|
+
else
|
|
97
|
+
:select
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if @wait_strategy == :select
|
|
101
|
+
@timeval = Curl::Timeval.new
|
|
102
|
+
@fd_read = Curl::FDSet.new
|
|
103
|
+
@fd_write = Curl::FDSet.new
|
|
104
|
+
@fd_excep = Curl::FDSet.new
|
|
105
|
+
@max_fd = ::FFI::MemoryPointer.new(:int)
|
|
106
|
+
end
|
|
34
107
|
elsif @execution_mode == :socket_action
|
|
35
108
|
@running_count_pointer = FFI::MemoryPointer.new(:int)
|
|
109
|
+
@msgs_left = FFI::MemoryPointer.new(:int)
|
|
36
110
|
end
|
|
37
111
|
end
|
|
38
112
|
|
|
@@ -44,14 +118,28 @@ module Ethon
|
|
|
44
118
|
# multi.perform
|
|
45
119
|
def perform
|
|
46
120
|
ensure_execution_mode(:perform)
|
|
121
|
+
ensure_not_cleaned_up!
|
|
122
|
+
ensure_not_executing!
|
|
47
123
|
|
|
48
124
|
Ethon.logger.debug(STARTED_MULTI)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
125
|
+
@executing = true
|
|
126
|
+
begin
|
|
127
|
+
while ongoing?
|
|
128
|
+
run
|
|
129
|
+
timeout = get_timeout
|
|
130
|
+
next if timeout == 0
|
|
131
|
+
case @wait_strategy
|
|
132
|
+
when :poll
|
|
133
|
+
poll(timeout)
|
|
134
|
+
when :wait
|
|
135
|
+
wait(timeout)
|
|
136
|
+
else
|
|
137
|
+
reset_fds
|
|
138
|
+
set_fds(timeout)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
ensure
|
|
142
|
+
@executing = false
|
|
55
143
|
end
|
|
56
144
|
Ethon.logger.debug(PERFORMED_MULTI)
|
|
57
145
|
nil
|
|
@@ -67,12 +155,63 @@ module Ethon
|
|
|
67
155
|
# @deprecated It is no longer necessary to call prepare.
|
|
68
156
|
def prepare
|
|
69
157
|
Ethon.logger.warn(
|
|
70
|
-
"ETHON: It is no longer
|
|
71
|
-
"Multi#prepare.
|
|
158
|
+
"ETHON: It is no longer necessary to call "+
|
|
159
|
+
"Multi#prepare. It's going to be removed "+
|
|
72
160
|
"in future versions."
|
|
73
161
|
)
|
|
74
162
|
end
|
|
75
163
|
|
|
164
|
+
# Wait for activity on the multi's sockets.
|
|
165
|
+
#
|
|
166
|
+
# @example Wait up to one second.
|
|
167
|
+
# multi.wait(1000)
|
|
168
|
+
#
|
|
169
|
+
# @param [ Integer ] timeout_ms Maximum time to wait in milliseconds.
|
|
170
|
+
#
|
|
171
|
+
# @return [ Integer ] Number of file descriptors with activity.
|
|
172
|
+
def wait(timeout_ms = 1000)
|
|
173
|
+
unless Curl.respond_to?(:multi_wait)
|
|
174
|
+
raise NotImplementedError, "curl_multi_wait is not supported by this libcurl"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
numfds = (@numfds ||= ::FFI::MemoryPointer.new(:int))
|
|
178
|
+
code = Curl.multi_wait(handle, ::FFI::Pointer::NULL, 0, timeout_ms, numfds)
|
|
179
|
+
raise Errors::MultiWait.new(code) unless code == :ok
|
|
180
|
+
|
|
181
|
+
numfds.read_int
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Wait for activity using curl_multi_poll.
|
|
185
|
+
#
|
|
186
|
+
# @param [ Integer ] timeout_ms Maximum time to wait in milliseconds.
|
|
187
|
+
#
|
|
188
|
+
# @return [ Integer ] Number of file descriptors with activity.
|
|
189
|
+
def poll(timeout_ms = 1000)
|
|
190
|
+
unless Curl.respond_to?(:multi_poll)
|
|
191
|
+
raise NotImplementedError, "curl_multi_poll is not supported by this libcurl"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
numfds = (@numfds ||= ::FFI::MemoryPointer.new(:int))
|
|
195
|
+
code = Curl.multi_poll(handle, ::FFI::Pointer::NULL, 0, timeout_ms, numfds)
|
|
196
|
+
raise Errors::MultiPoll.new(code) unless code == :ok
|
|
197
|
+
|
|
198
|
+
numfds.read_int
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Wake up a multi handle blocked in curl_multi_poll.
|
|
202
|
+
#
|
|
203
|
+
# @return [ Symbol ] The Curl multi return code.
|
|
204
|
+
def wakeup
|
|
205
|
+
unless Curl.respond_to?(:multi_wakeup)
|
|
206
|
+
raise NotImplementedError, "curl_multi_wakeup is not supported by this libcurl"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
code = Curl.multi_wakeup(handle)
|
|
210
|
+
raise Errors::MultiWakeup.new(code) unless code == :ok
|
|
211
|
+
|
|
212
|
+
code
|
|
213
|
+
end
|
|
214
|
+
|
|
76
215
|
# Continue execution with an external IO loop.
|
|
77
216
|
#
|
|
78
217
|
# @example When no sockets are ready yet, or to begin.
|
|
@@ -84,9 +223,16 @@ module Ethon
|
|
|
84
223
|
# @example When a socket is readable and writable
|
|
85
224
|
# multi.socket_action(io_object, [:in, :out])
|
|
86
225
|
#
|
|
226
|
+
# @param [ IO, Integer, nil ] io The socket to act on, or nil for the
|
|
227
|
+
# timeout case.
|
|
228
|
+
# @param [ Integer, Symbol, Array ] readiness CURL_CSELECT_* readiness
|
|
229
|
+
# bits, either as a number or as symbols such as :in and :out.
|
|
230
|
+
#
|
|
87
231
|
# @return [ Symbol ] The Curl.multi_socket_action return code.
|
|
88
232
|
def socket_action(io = nil, readiness = 0)
|
|
89
233
|
ensure_execution_mode(:socket_action)
|
|
234
|
+
ensure_not_cleaned_up!
|
|
235
|
+
ensure_not_executing!
|
|
90
236
|
|
|
91
237
|
fd = if io.nil?
|
|
92
238
|
::Ethon::Curl::SOCKET_TIMEOUT
|
|
@@ -96,12 +242,17 @@ module Ethon
|
|
|
96
242
|
io.fileno
|
|
97
243
|
end
|
|
98
244
|
|
|
99
|
-
|
|
100
|
-
|
|
245
|
+
@executing = true
|
|
246
|
+
begin
|
|
247
|
+
code = Curl.multi_socket_action(handle, fd, readiness, @running_count_pointer)
|
|
248
|
+
@running_count = @running_count_pointer.read_int
|
|
101
249
|
|
|
102
|
-
|
|
250
|
+
check
|
|
103
251
|
|
|
104
|
-
|
|
252
|
+
code
|
|
253
|
+
ensure
|
|
254
|
+
@executing = false
|
|
255
|
+
end
|
|
105
256
|
end
|
|
106
257
|
|
|
107
258
|
# Return whether the multi still contains requests or not.
|
|
@@ -111,11 +262,86 @@ module Ethon
|
|
|
111
262
|
#
|
|
112
263
|
# @return [ Boolean ] True if ongoing, else false.
|
|
113
264
|
def ongoing?
|
|
114
|
-
|
|
265
|
+
stack.size > 0 || (running_count || 0) > 0
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Associate an arbitrary pointer with a socket in the multi handle.
|
|
269
|
+
#
|
|
270
|
+
# @param [ Integer, IO ] io The socket descriptor.
|
|
271
|
+
# @param [ FFI::Pointer, nil ] pointer The pointer to associate, or nil
|
|
272
|
+
# to clear it.
|
|
273
|
+
#
|
|
274
|
+
# @return [ Symbol ] The Curl multi return code.
|
|
275
|
+
def assign(io, pointer)
|
|
276
|
+
unless pointer.nil? || pointer.is_a?(::FFI::Pointer)
|
|
277
|
+
raise Errors::InvalidValue.new(:assign, pointer)
|
|
278
|
+
end
|
|
279
|
+
# A freed wrapper still looks like a pointer, but libcurl would keep
|
|
280
|
+
# the dangling address.
|
|
281
|
+
if pointer.is_a?(Ethon::Curl::OwnedPointer)
|
|
282
|
+
pointer.check_pid!
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
socket = io.respond_to?(:fileno) ? io.fileno : io
|
|
286
|
+
# FFI would reject a non-integer descriptor with a TypeError instead
|
|
287
|
+
# of the error Ethon uses for invalid option values.
|
|
288
|
+
unless socket.is_a?(Integer)
|
|
289
|
+
raise Errors::InvalidValue.new(:assign, io)
|
|
290
|
+
end
|
|
291
|
+
code = Curl.multi_assign(handle, socket, pointer)
|
|
292
|
+
raise Errors::MultiAssign.new(code) unless code == :ok
|
|
293
|
+
|
|
294
|
+
# libcurl keeps the pointer until the socket is removed or assigned
|
|
295
|
+
# again, so keep it alive from Ruby as well.
|
|
296
|
+
if pointer.nil? || pointer.null?
|
|
297
|
+
(@assigned_sockets ||= {}).delete(socket)
|
|
298
|
+
else
|
|
299
|
+
(@assigned_sockets ||= {})[socket] = pointer
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
code
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Return the easy handles currently attached natively.
|
|
306
|
+
#
|
|
307
|
+
# @return [ Array<FFI::Pointer> ] Native easy handles.
|
|
308
|
+
def native_handles
|
|
309
|
+
unless Curl.respond_to?(:multi_get_handles)
|
|
310
|
+
raise NotImplementedError, "curl_multi_get_handles is not supported by this libcurl"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
pointer = Curl.multi_get_handles(handle)
|
|
314
|
+
raise NoMemoryError, "curl_multi_get_handles failed" if pointer.null?
|
|
315
|
+
|
|
316
|
+
begin
|
|
317
|
+
handles = []
|
|
318
|
+
offset = 0
|
|
319
|
+
loop do
|
|
320
|
+
easy = pointer.get_pointer(offset)
|
|
321
|
+
break if easy.null?
|
|
322
|
+
|
|
323
|
+
handles << easy
|
|
324
|
+
offset += ::FFI.type_size(:pointer)
|
|
325
|
+
end
|
|
326
|
+
handles
|
|
327
|
+
ensure
|
|
328
|
+
Curl.free(pointer)
|
|
329
|
+
end
|
|
115
330
|
end
|
|
116
331
|
|
|
117
332
|
private
|
|
118
333
|
|
|
334
|
+
def ensure_not_cleaned_up!
|
|
335
|
+
return unless @cleaned_up
|
|
336
|
+
raise Errors::EthonError, "Multi has been cleaned up"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def ensure_not_executing!
|
|
340
|
+
return unless @executing
|
|
341
|
+
|
|
342
|
+
raise Errors::EthonError, "Multi is currently executing; wait for it to finish"
|
|
343
|
+
end
|
|
344
|
+
|
|
119
345
|
# Get timeout.
|
|
120
346
|
#
|
|
121
347
|
# @example Get timeout.
|
|
@@ -158,7 +384,8 @@ module Ethon
|
|
|
158
384
|
raise Errors::MultiFdset.new(code) unless code == :ok
|
|
159
385
|
max_fd = @max_fd.read_int
|
|
160
386
|
if max_fd == -1
|
|
161
|
-
|
|
387
|
+
# No sockets are ready; wait for the timer instead of spinning.
|
|
388
|
+
sleep(timeout / 1000.0)
|
|
162
389
|
else
|
|
163
390
|
@timeval[:sec] = timeout / 1000
|
|
164
391
|
@timeval[:usec] = (timeout * 1000) % 1000000
|
|
@@ -177,16 +404,31 @@ module Ethon
|
|
|
177
404
|
#
|
|
178
405
|
# @return [ void ]
|
|
179
406
|
def check
|
|
180
|
-
msgs_left = ::FFI::MemoryPointer.new(:int)
|
|
407
|
+
msgs_left = (@msgs_left ||= ::FFI::MemoryPointer.new(:int))
|
|
181
408
|
while true
|
|
182
409
|
msg = Curl.multi_info_read(handle, msgs_left)
|
|
183
410
|
break if msg.null?
|
|
184
411
|
next if msg[:code] != :done
|
|
185
|
-
easy =
|
|
412
|
+
easy = stack.find{ |e| e.handle == msg[:easy_handle] }
|
|
413
|
+
# The handle may have been removed between the transfer finishing and
|
|
414
|
+
# the completion message being read.
|
|
415
|
+
next unless easy
|
|
186
416
|
easy.return_code = msg[:data][:code]
|
|
187
417
|
Ethon.logger.debug { "ETHON: performed #{easy.log_inspect}" }
|
|
188
|
-
delete
|
|
418
|
+
# delete finishes the transfer's response stream. A failed close or
|
|
419
|
+
# flush must not keep on_complete from running; raise it afterwards.
|
|
420
|
+
teardown_error = nil
|
|
421
|
+
begin
|
|
422
|
+
delete(easy)
|
|
423
|
+
rescue Exception => exception
|
|
424
|
+
# A handle that libcurl still owns must stay tracked for cleanup.
|
|
425
|
+
raise if stack.include?(easy)
|
|
426
|
+
|
|
427
|
+
teardown_error = exception
|
|
428
|
+
end
|
|
189
429
|
easy.complete
|
|
430
|
+
easy.raise_callback_error
|
|
431
|
+
raise teardown_error if teardown_error
|
|
190
432
|
end
|
|
191
433
|
end
|
|
192
434
|
|
|
@@ -197,8 +439,10 @@ module Ethon
|
|
|
197
439
|
#
|
|
198
440
|
# @return [ void ]
|
|
199
441
|
def run
|
|
200
|
-
running_count_pointer = FFI::MemoryPointer.new(:int)
|
|
442
|
+
running_count_pointer = (@running_count_pointer ||= ::FFI::MemoryPointer.new(:int))
|
|
201
443
|
begin code = trigger(running_count_pointer) end while code == :call_multi_perform
|
|
444
|
+
raise Errors::MultiPerform.new(code) unless code == :ok
|
|
445
|
+
|
|
202
446
|
check
|
|
203
447
|
end
|
|
204
448
|
|
|
@@ -221,7 +465,7 @@ module Ethon
|
|
|
221
465
|
#
|
|
222
466
|
# @return [ Integer ] Number running requests.
|
|
223
467
|
def running_count
|
|
224
|
-
@running_count
|
|
468
|
+
@running_count
|
|
225
469
|
end
|
|
226
470
|
end
|
|
227
471
|
end
|